com.apptrackx.sdk
v0.2.0
Published
Mobile measurement for Unity: install attribution and in-app events. Android and WebGL. iOS is not implemented (P2.SDKU.03).
Downloads
213
Maintainers
Readme
AppTrackX for Unity
Install attribution and in-app events for Unity games. Android and WebGL.
iOS is not implemented — see What is missing before you plan around this package.
What this is
A C# layer over two SDKs that already exist in this repository:
| Unity target | Bridged to | How |
| ------------ | ------------------- | -------------------------------------- |
| Android | sdks/android | JNI → AppTrackXUnityBridge.java |
| WebGL | sdks/web | apptrackx.jslib → window.AppTrackX |
| iOS | nothing yet | every call throws |
| Editor | nothing, on purpose | every call logs a warning |
| Desktop | nothing | every call throws |
It decides nothing about attribution, holds no queue and retries nothing. Everything that can be wrong about measurement is wrong in one of those two SDKs, not here.
Install
Add the registry and the package to Packages/manifest.json:
{
"scopedRegistries": [
{
"name": "AppTrackX",
"url": "https://registry.npmjs.org",
"scopes": ["com.apptrackx"]
}
],
"dependencies": {
"com.apptrackx.sdk": "0.2.0"
}
}Unity's Package Manager speaks npm, so registry.npmjs.org works as a scoped
registry directly — there is no separate service to sign up for. The scope
limits it to com.apptrackx, so nothing else in your project starts resolving
from there.
Pin the exact version. A range means a build can change what it measures without anybody deciding to.
Or by path, while developing
{
"dependencies": {
"com.apptrackx.sdk": "file:../../path/to/apptrackx/sdks/unity"
}
}Android also needs the native SDK
Unity compiles the Java bridge in this package, but the SDK it calls is a Kotlin library that is not in the package. It has been on Maven Central since 2026-08-17, so depend on the coordinate rather than building an AAR.
Enable Project Settings → Player → Android → Publishing Settings → Custom Main Gradle Template, then add it to the file Unity generates:
// Assets/Plugins/Android/mainTemplate.gradle
dependencies {
implementation "com.apptrackx:apptrackx-android:0.2.0"
}google() must be among the repositories as well — every Unity Android
template has it, and it is where com.android.installreferrer lives, which is
published nowhere else.
Nothing is needed for this on WebGL.
Verified on Unity 6000.3.8f1, on a physical device, 2026-08-20:
| | |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Android build with the template above | APK built, 0 errors |
| The same build with the template removed | fails — AppTrackXUnityBridge.java:10: error: package com.apptrackx.sdk does not exist |
| Native SDK in the APK | 27 com.apptrackx.sdk.* classes across 4 dex files |
| On device | initialized=True, a device id generated, and the apptrackx-sdk thread running |
| Reaching the collector | install rejected (401): Invalid signature against deliberately fake credentials, and two events dropped on 4xx as documented |
The second row is the one that matters: this step is not optional, and skipping it is a compile failure rather than a silent no-op.
WebGL also needs the Web SDK on the page
The Unity package bridges the Web SDK; it does not contain it. The build cannot
bundle it either — it is an IIFE that expects the page's own scope, where
location, document.referrer and storage are. Add it to your WebGL template
before the Unity loader:
<script src="/apptrackx.iife.js"></script>If it is not there, the jslib logs once explaining exactly this and measures nothing. It does not fail the build.
Use
using AppTrackXSdk;
void Start()
{
AppTrackX.Ready += status =>
Debug.Log($"device {status.DeviceId}, first open: {status.IsFirstOpen}");
AppTrackX.DeferredDeepLink += link => Router.Open(link.Url); // Android only
#if UNITY_ANDROID
AppTrackX.Initialize(new AppTrackXConfig
{
AppToken = "your-app-token",
AppSecret = "your-app-secret",
});
#elif UNITY_WEBGL
AppTrackX.Initialize(new AppTrackXConfig
{
AppToken = "your-app-token",
Endpoint = "https://go.apptrackx.com",
});
#endif
}
void OnPurchase()
{
AppTrackX.TrackEvent("purchase", 19.99m, "USD",
new Dictionary<string, object> { { "sku", "gems_500" } });
}Subscribe to Ready before Initialize. The Android SDK can answer from a
cached install state fast enough that a handler attached afterwards misses the
only time it fires.
Identify the player (optional, Android)
AppTrackX.SetUserId("player_123"); // after sign-in
AppTrackX.SetUserId(null); // on sign-outYour own id for the player, sent as user_id on every event tracked after
the call and kept across restarts. Coin Callbacks forwards it so your server
knows which player to credit; without it a callback identifies the device only.
Must be 1 to 128 characters with no control characters — anything else is
ignored with a warning in Logcat and the current id is kept. A blank string is
ignored too, rather than clearing the id: pass null to clear. On WebGL and in
the editor it logs and does nothing. Needs 0.2.0 and the 0.2.0 native SDK.
Things that will bite you
The two platforms are not the same product
Every other wrapper in this repository bridges one SDK and refuses the other platform. This one bridges two genuinely different SDKs, and the differences are real rather than cosmetic:
| | Android | WebGL |
| ------------------ | --------------------- | ------------------------------- |
| AppSecret | required | refused — see below |
| Endpoint | optional override | required |
| Environment | selects the collector | ignored — use Endpoint |
| Consent | not a thing | holds every event until set |
| DeferredDeepLink | fires | never fires; the web has none |
| IsFirstOpen | first app open | first visit from this browser |
Initialize validates against the platform you are actually building and throws
naming the field. That check runs in the editor too, so a WebGL config carrying
an Android field is caught before it ships.
Never put AppSecret in a WebGL build
A WebGL build is downloaded by everyone who opens the page. A secret compiled into it is readable from the browser's network tab by someone with no tools and no skill, and it signs install reports — whoever has it can forge them against your app.
Shipping the secret inside an APK is a different and accepted bargain; the
collector treats a signature as evidence of the app, not of the user. The Web
SDK has no secret at all and is authenticated by Origin, which you allowlist in
the app's settings (W4-40).
Initialize throws on WebGL if AppSecret is set, and ToWebJson never reads
the field, so two separate mistakes would have to line up. If a build has already
gone out with one, rotate the secret — refusing to start cannot recall a file
that has already been served.
On WebGL, nothing is sent until consent is granted
The Web SDK's default holds every event in a queue. That is correct — it is what a consent banner needs — and it is the most confusing thing about porting a game that works on Android. The symptom is an empty dashboard with no error anywhere.
AppTrackX.SetConsent(true); // ignored on Android, required on WebGLor set WebConsentGranted = true if consent is already established by other
means. The bridge warns once in the browser console if events are piling up
undecided.
Revenue is decimal or string, never double
AppTrackX.TrackEvent("purchase", 19.99m, "USD"); // ✓
AppTrackX.TrackEvent("purchase", "19.99", "USD"); // ✓
AppTrackX.TrackEvent("purchase", 19.99, "USD"); // ✗ will not compileThe third line is the natural one to write and 19.99 as a double is
19.989999999999998. The overload exists purely so the compiler explains that
rather than saying "cannot convert double to decimal".
Amounts are formatted with the invariant culture on the way out. ToString()
would produce "19,99" on a device in Berlin and "19.99" on one in Boston,
from identical code — and only one of those is a number to the collector.
The editor logs instead of throwing
Unsupported platforms throw; the editor does not. A shipped iOS build that
silently no-ops gives you a game reporting zero installs from half its players,
so throwing there costs nothing. Breaking Play mode for a developer who wired
everything up correctly, on a platform where there was never any data, is not the
same trade. AppTrackX.IsSupported is false in the editor.
Tests
pnpm test:unity # 40 edit-mode tests, in batch mode
pnpm test:unity --clean # rebuild the throwaway project first
UNITY_PATH=... pnpm test:unityUnity cannot test a package on its own, so run-tests.mjs generates a throwaway
project at unity-test-project/ (gitignored), references this package by path
and runs the tests inside it. A cold run takes about a minute; the project is
disposable.
These are beside pnpm verify, not inside it — the Unity toolchain is not a
dependency of this repository and most machines working on it do not have one.
The WebGL bridge is different: apptrackx.jslib is plain JavaScript, so it is
tested by sdks/web/src/unity-bridge.test.ts and does run inside
pnpm verify. It lives there because the jslib is a consumer of the Web SDK's
public surface, so changing that surface breaks a test in the package that
changed it.
What is missing
- iOS (
P2.SDKU.03,P2.SDKU.04). Needs the native iOS SDK (P2.SDKI), which needs Xcode and a macOS machine this project does not have. Every call throwsAppTrackXUnsupportedPlatformException. .unitypackagebuild (P2.SDKU.07). UPM by path only for now.- Sample project (
P2.SDKU.09). - Neither bridge has run on a device. The C# and the jslib are tested; the
Java has been compiled by nothing. Java, C# and JavaScript share no compiler,
so the agreement between the three — method names, the
AppTrackXUnityBridgeobject name, the JSON shapes — is pinned by assertions on each side and verified by none. That needs a phone and a browser.
WebGL is not in the P2.SDKU table at all. That table describes Unity as
bridging the two native SDKs, which is why the execution plan lists Unity as
blocked on iOS. Bridging the Web SDK instead was asked for on 2026-08-14 and is
an addition to the plan rather than a row being ticked.
