Unity
Installation
Phase Analytics ships as a Unity Package Manager (UPM) package from the Phase monorepo. There is no .unitypackage download.
Add the UPM package
Add to Packages/manifest.json (recommended for teams and CI):
{
"dependencies": {
"com.phase.analytics": "https://github.com/Phase-Analytics/Phase.git?path=packages/phase-unity#v0.2.0"
}
}Return to Unity and let Package Manager resolve the dependency.
Pin the release tag:
#v0.2.0Install required dependency
The package declares com.unity.nuget.newtonsoft-json in package.json. Unity should install it automatically when you add Phase Analytics.
If Package Manager shows a missing dependency warning:
- Open Package Manager
- Select Packages: Unity Registry
- Search Newtonsoft Json
- Install com.unity.nuget.newtonsoft-json (3.2.1+)
The SDK uses Newtonsoft.Json for batch payloads and offline queue serialization. Runtime/link.xml preserves the assembly for IL2CPP builds.
Verify the import
After resolution, confirm the package is healthy before wiring game code:
- Package Manager lists
Phase Analytics(com.phase.analytics) - Console has no warnings like
has no meta file, but it's in an immutable folder - Console has no
CS8773(file-scoped namespace) errors - Assembly
Phase.Analyticsappears under Project → Assemblies (or compiles without errors)
If any check fails, upgrade to #v0.1.11 or newer, remove Library/PackageCache/com.phase.analytics@*, and re-import. Do not patch files inside PackageCache by hand.
Get Your API Key
- Sign in to Phase Dashboard
- Create a new project or select an existing one
- Open API Keys tab
- Copy your API Key (starts with
phase_)
Setup
Initialize once at startup, then call IdentifyAsync to register the device and start a session. Call InitializeAsync and IdentifyAsync from the Unity main thread (Start, Awake, or a coroutine started there). With AutoBootstrap enabled (default), the SDK spawns a DontDestroyOnLoad PhaseLifecycleHook that flushes when the app goes to the background, saves the queue on quit and resumes sessions when the app returns.
Attach a bootstrap MonoBehaviour to a GameObject in your first scene (or a persistent loader scene):
using Phase.Analytics;
using Phase.Analytics.Config;
using Phase.Analytics.Models;
using UnityEngine;
public sealed class GameBootstrap : MonoBehaviour
{
[SerializeField] private string apiKey = "phase_xxx";
private async void Start()
{
var ok = await PhaseAnalytics.InitializeAsync(new PhaseConfig
{
ApiKey = apiKey,
LogLevel = LogLevel.Info,
});
if (!ok)
{
Debug.LogWarning("[Phase] Initialize failed. Check API key and network.");
return;
}
await PhaseAnalytics.IdentifyAsync();
PhaseAnalytics.Track("app_opened");
}
}InitializeAsync is idempotent. Events tracked before IdentifyAsync completes are buffered and delivered once a session exists. Both must run on the main thread so Unity APIs and the lifecycle hook stay safe after await.
Configuration
PhaseConfig is passed to PhaseAnalytics.InitializeAsync():
Prop
Type
Platform: on iOS/Android player builds, identify sends platform: "ios" or "android". In the Editor it is null.
Threading and HTTP
Call InitializeAsync and IdentifyAsync from the Unity main thread (Start, Awake, or a coroutine started there). The SDK uses ConfigureAwait(false) internally so continuations often run on the thread pool.
Unity-only APIs (GameObject, Application, UnityWebRequest) are gated to the main thread: lifecycle hook creation, device info snapshot, network reachability polling, and optional UnityWebRequest transport.
Default HTTP is System.Net.Http (SystemNetHttpTransport), which is safe from pool threads. Set UseUnityWebRequestTransport = true only if you need UnityWebRequest; those calls are queued to PhaseLifecycleHook.Update.
Usage
Identify User
Call IdentifyAsync() once at startup. This registers the device and starts a session.
// No PII by default
await PhaseAnalytics.IdentifyAsync();
// Optional properties after login
await PhaseAnalytics.IdentifyAsync(new DeviceProperties
{
["user_id"] = "123",
["plan"] = "premium",
["beta_tester"] = true,
});Privacy by default:
- No personal data is collected without explicit properties
- Device ID is generated locally (ULID) and stored under
Application.persistentDataPath - Only technical metadata is collected when enabled (OS, model, app version, locale)
Adding custom properties:
You can attach user/device properties. Important: If you add PII (personally identifiable information), ensure you have proper user consent:
// After user login and consent
await PhaseAnalytics.IdentifyAsync(new DeviceProperties
{
["user_id"] = accountId,
["plan"] = "premium",
});
// If adding PII, get consent first
if (userHasConsentedToAnalytics)
{
await PhaseAnalytics.IdentifyAsync(new DeviceProperties
{
["email"] = user.Email,
["display_name"] = user.DisplayName,
});
}Properties must be flat primitives: string, number, bool, or null.
Track Events
Track custom events with optional parameters. The event id and timestamp are taken when you call Track. Events tracked before IdentifyAsync() completes are buffered (up to 200 events, for up to one hour) and delivered once a session exists. If more than 200 pile up, the oldest are dropped.
// Event without parameters
PhaseAnalytics.Track("app_opened");
// Event with parameters
PhaseAnalytics.Track("purchase_completed", new EventParams
{
["amount"] = 99.99,
["currency"] = "USD",
["product_id"] = "premium_plan",
});
// Level / gameplay
PhaseAnalytics.Track("level_complete", new EventParams
{
["level"] = 5,
["score"] = 1200,
["duration_sec"] = 42.5,
});Track is non-blocking (fire-and-forget). Events go into a persistent queue and are sent in batches (see Delivery). Failures are logged when LogLevel is enabled and events are retried when appropriate.
Event naming rules:
- Alphanumeric characters, underscores (
_), hyphens (-), periods (.), forward slashes (/), and spaces - 1–256 characters
- Examples:
purchase,user.signup,payment/success,Button Clicked
Event parameters:
- Flat primitive object only
- Values must be
string,number,bool, ornull - Max 32 keys
- Key max 32 characters
- String value max 256 characters
- Serialized payload max 8 KB
- Empty objects are normalized away
Track Screens
Use TrackScreen to record a screen view. parameters follow the same rules as Track, and delivery rules are the same (buffered before identify, sent in batches).
PhaseAnalytics.TrackScreen("Shop");
PhaseAnalytics.TrackScreen("Shop", new EventParams
{
["tab"] = "coins",
});Unity has no automatic screen tracking, so call TrackScreen from your own menu or scene code.
Scenes and levels (manual events)
Model scenes and levels with Track (or TrackScreen for screen-like views):
using Phase.Analytics;
using Phase.Analytics.Models;
using UnityEngine;
using UnityEngine.SceneManagement;
public sealed class SceneAnalytics : MonoBehaviour
{
private void OnEnable() => SceneManager.sceneLoaded += OnSceneLoaded;
private void OnDisable() => SceneManager.sceneLoaded -= OnSceneLoaded;
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
PhaseAnalytics.Track("scene_loaded", new EventParams
{
["scene"] = scene.name,
["mode"] = mode.ToString(),
});
}
}Flush
Data is sent automatically (see Delivery). FlushAsync() sends queued data now and saves whatever could not be delivered, for example before a scripted shutdown.
await PhaseAnalytics.FlushAsync();Reset
ResetAsync() forgets the current device, for example on logout. It ends the session, tries to deliver queued data, clears the queue and generates a new device ID. Call IdentifyAsync() again to resume tracking.
await PhaseAnalytics.ResetAsync();
await PhaseAnalytics.IdentifyAsync();Clear local data
Wipes persisted SDK storage on device (GDPR-style). Does not delete server-side data.
await PhaseAnalytics.ClearLocalDataAsync();
// Re-initialize before tracking again
await PhaseAnalytics.InitializeAsync(new PhaseConfig { ApiKey = "phase_xxx" });
await PhaseAnalytics.IdentifyAsync();API reference
| Member | Description |
|---|---|
InitializeAsync(PhaseConfig) | Sets up storage, HTTP, managers, optional lifecycle hook. Returns false on invalid config. Idempotent. |
IdentifyAsync(DeviceProperties?) | Registers device and starts session. Events tracked earlier are buffered and delivered afterwards. |
Track(string, EventParams?) | Queues a custom event. Buffered if called before identify. |
TrackScreen(string, EventParams?) | Queues a screen view. Same delivery rules as Track. |
FlushAsync() | Sends queued data now and saves what could not be delivered. |
ResetAsync() | Forgets the device: ends the session, tries to deliver queued data, clears the queue and creates a new device ID. |
ClearLocalDataAsync() | Deletes local Phase storage directory. |
IsInitialized | true after successful InitializeAsync. |
IsIdentified | true after successful IdentifyAsync. |
Type Reference
DeviceProperties
Custom user/device attributes passed to IdentifyAsync():
Prop
Type
EventParams
Event parameters passed to Track() and TrackScreen():
Prop
Type
How It Works
Lifecycle and sessions
When AutoBootstrap is true, PhaseLifecycleHook is created at the start of InitializeAsync (before async I/O):
- Listens for
OnApplicationPause/ focus changes - Flushes the queue when the app goes to the background and saves it to disk on quit
- Resumes session handling when the app returns
- Polls network reachability on the main thread (
Update)
Session pings run on a timer while identified. Events tracked before IdentifyAsync succeeds are buffered and delivered once a session exists.
Delivery
Devices, sessions, events and heartbeats go through one persistent queue and are sent in batches:
- When 20 items are queued, every 10 seconds while the queue is not empty, when the app goes to the background, and when connectivity returns
- Items leave the queue only after the server acknowledges them or rejects them permanently. Retryable failures are kept for up to 5 attempts
- Each event gets a unique id when
Trackis called, stable across retries, so the server can deduplicate - Changing the API key clears data queued for the previous key
Offline Support
The queue is saved on disk under:
Application.persistentDataPath/phase-analytics-data/
When offline or when requests fail, items stay in the queue and survive app restarts. They are sent when connectivity returns and on lifecycle boundaries. Batches with 25 or more items are gzip-compressed.
Privacy
- No personal data is collected by default
- Device IDs are generated locally and stored persistently
- Geolocation is resolved server-side from IP when
UserLocaleis enabled - No IDFA or advertising identifiers in v1
- All metadata collection is optional via
DeviceInfo/UserLocale
Performance
Trackdoes not block the game thread- Rate limit: 15 events per second
- 50 ms deduplication window for identical event name + parameters
- Queue writes are debounced (1 s, plus on pause/quit); the queue is capped at 1000 items / 1 MB
- Batches of up to 50 items per request; only the latest heartbeat per session is kept
- Failed requests retry with jittered backoff and honor
Retry-After; requests time out after 10 seconds
Production builds (IL2CPP)
Editor Play Mode is not enough to validate analytics. Ship only after a device smoke test.
Project settings
- Scripting backend: IL2CPP (recommended for iOS; typical for Android release)
- Target: iOS 12+ / Android API 21+
- Do not strip
Phase.Analyticsor Newtonsoft — the package includesRuntime/link.xml
Build and smoke test
- Build Release to a physical device
- Cold start → confirm
InitializeAsync+IdentifyAsyncin logs (ifLogLevel.Info) - Trigger a few
Trackcalls - Background the app (home button) to force flush
- Open Phase Dashboard and confirm events within a few minutes
Use a Development or Release IL2CPP build on hardware (Simulator can miss networking edge cases).
Ensure App Transport Security allows your BaseUrl (default https://api.phase.sh is fine).
Offline test: enable airplane mode, Track events, disable airplane mode, background the app, verify flush in the dashboard.
Troubleshooting
Package import (git UPM)
| Issue | What to check |
|---|---|
has no meta file, but it's in an immutable folder | Upgrade to #v0.1.0+. Git UPM cannot generate .meta at import time. |
CS8773 file-scoped namespace | Upgrade to #v0.1.2+ (Runtime/csc.rsp). Asmdef langVersion alone is insufficient on Unity 6 git UPM. |
CS0246 Timer in UnityNetworkMonitor | Upgrade to #v0.1.3+ (using System.Threading) |
CS0103 ValidationConstants | Upgrade to #v0.1.4+ |
CS0104 ambiguous Logger | Upgrade to #v0.1.4+ (PhaseLifecycleHook) |
CS0246 / Phase not found | Phase.Analytics assembly did not compile. Fix import/compile errors first; do not enable game asmdef references until the package builds. |
| Package stuck on old version | Remove Library/PackageCache/com.phase.analytics@*, bump hash/tag in manifest.json, restart Unity. |
Runtime
| Issue | What to check |
|---|---|
Internal_CreateGameObject / main thread | Fixed in #v0.1.7+. Call InitializeAsync from main thread (Start / coroutine). |
Create can only be called from the main thread (UWR) | Upgrade to #v0.1.9+ (default System.Net.Http). |
Session not found / batch dropped after init | Failed session/API from UWR off-thread before #v0.1.9. Upgrade to #v0.1.9. |
InitializeAsync returns false | API key must start with phase_; BaseUrl must be HTTPS unless AllowInsecureDev is true |
| No events in dashboard | Make sure IdentifyAsync completes; test on device, not Editor-only (DisableInEditor skips network) |
| HTTP 401 | Invalid or revoked API key |
| Queue not flushing | Device online, identified, and app backgrounded or session active; check LogLevel.Info |
| Duplicate events | 50 ms dedupe collapses identical name + params; intentional for burst clicks |
IL2CPP / release
| Issue | What to check |
|---|---|
| Stripping / missing types | Keep package link.xml; do not strip Phase.Analytics or Newtonsoft |
| Newtonsoft errors at runtime | Ensure com.unity.nuget.newtonsoft-json is installed |
| Works in Editor, not on device | Run IL2CPP Release on hardware; verify ATS (iOS) and network permissions (Android) |
Differences from other SDKs
| Feature | Expo / React Native | Unity |
|---|---|---|
| Screen tracking | Automatic (optional, Expo Router) or manual trackScreen | Manual TrackScreen |
platform field | Sent (ios / android) | Sent on mobile player builds |
| Distribution | npm phase-analytics | Git UPM #v0.2.0 |
| Init pattern | Provider / hook | InitializeAsync + MonoBehaviour bootstrap |
For the same event schema and dashboard, keep event names and parameter keys aligned across your mobile clients.