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.1.9"
}
}Return to Unity and let Package Manager resolve the dependency.
- Window → Package Manager
- + → Add package from git URL…
- Paste:
https://github.com/Phase-Analytics/Phase.git?path=packages/phase-unity#v0.1.9- Select Add
Pin the release tag:
#v0.1.9Install 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.9 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 before tracking. 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 on pause/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. IdentifyAsync must succeed before events are sent. Both must run on the main thread so Unity APIs and the lifecycle hook stay safe after await.
Import the bundled sample from Package Manager:
- Window → Package Manager
- Select Phase Analytics in the left list
- Open Samples
- Import Phase Analytics Sample
- Add
PhaseAnalyticsBootstrapto a scene and set your API key in the Inspector
The sample lives under Samples~/PhaseAnalyticsSample in the package (not copied into your Assets/ until imported).
For day-to-day Editor work without hitting production:
await PhaseAnalytics.InitializeAsync(new PhaseConfig
{
ApiKey = "phase_xxx",
DisableInEditor = true,
DebugData = true,
LogLevel = LogLevel.Info,
});DisableInEditor skips network I/O in the Unity Editor (calls still queue logic where applicable).
DebugData sends x-phase-debug-data: 1 so events are marked as debug in the dashboard.
Always validate on a physical device with an IL2CPP Release build before shipping.
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
Required: Call IdentifyAsync() before Track(). 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. Note: IdentifyAsync() must complete successfully first.
// 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). Failures are logged when LogLevel is enabled and events are queued for retry 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
Scenes and levels (manual events)
Model scenes and levels with Track:
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(),
});
}
}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. Required before reliable tracking. |
Track(string, EventParams?) | Queues a custom event. Requires prior identify. |
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():
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 offline queue when the app backgrounds or quits
- Resumes session handling when the app returns
- Polls network reachability on the main thread (
Update)
Session pings run on a timer while identified. Events sent without a valid session are rejected client-side until IdentifyAsync succeeds.
Offline Support
Events are stored under:
Application.persistentDataPath/phase-analytics-data/
When offline or when requests fail, items stay in the queue. The SDK flushes after IdentifyAsync when the network is available and on lifecycle boundaries. Large backlogs use gzip batch upload (100+ queued items).
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
- Offline queue capped at 1000 items; batches up to 1000 items per request
- Failed requests retry with exponential backoff (HTTP layer)
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.
Use an IL2CPP Release build on a physical device.
Confirm INTERNET permission (Unity adds it by default for network builds).
Offline test: same flow as iOS after reconnecting.
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 | Call IdentifyAsync before Track; 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) | Manual Track events |
platform field | Sent (ios / android) | Sent on mobile player builds |
| Distribution | npm phase-analytics | Git UPM #v0.1.9 |
| 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.