Phase
Get Started

Unity

llms.txt

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):

Packages/manifest.json
{
  "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.0

Install 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:

  1. Open Package Manager
  2. Select Packages: Unity Registry
  3. Search Newtonsoft Json
  4. 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:

  1. Package Manager lists Phase Analytics (com.phase.analytics)
  2. Console has no warnings like has no meta file, but it's in an immutable folder
  3. Console has no CS8773 (file-scoped namespace) errors
  4. Assembly Phase.Analytics appears 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

  1. Sign in to Phase Dashboard
  2. Create a new project or select an existing one
  3. Open API Keys tab
  4. 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):

Unity
GameBootstrap.cs
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.

Unity
GameBootstrap.cs
// 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, or null
  • 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):

Unity
SceneLoader.cs
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

MemberDescription
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.
IsInitializedtrue after successful InitializeAsync.
IsIdentifiedtrue 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 Track is 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 UserLocale is enabled
  • No IDFA or advertising identifiers in v1
  • All metadata collection is optional via DeviceInfo / UserLocale

Performance

  • Track does 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.Analytics or Newtonsoft — the package includes Runtime/link.xml

Build and smoke test

  1. Build Release to a physical device
  2. Cold start → confirm InitializeAsync + IdentifyAsync in logs (if LogLevel.Info)
  3. Trigger a few Track calls
  4. Background the app (home button) to force flush
  5. 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)

IssueWhat to check
has no meta file, but it's in an immutable folderUpgrade to #v0.1.0+. Git UPM cannot generate .meta at import time.
CS8773 file-scoped namespaceUpgrade to #v0.1.2+ (Runtime/csc.rsp). Asmdef langVersion alone is insufficient on Unity 6 git UPM.
CS0246 Timer in UnityNetworkMonitorUpgrade to #v0.1.3+ (using System.Threading)
CS0103 ValidationConstantsUpgrade to #v0.1.4+
CS0104 ambiguous LoggerUpgrade to #v0.1.4+ (PhaseLifecycleHook)
CS0246 / Phase not foundPhase.Analytics assembly did not compile. Fix import/compile errors first; do not enable game asmdef references until the package builds.
Package stuck on old versionRemove Library/PackageCache/com.phase.analytics@*, bump hash/tag in manifest.json, restart Unity.

Runtime

IssueWhat to check
Internal_CreateGameObject / main threadFixed 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 initFailed session/API from UWR off-thread before #v0.1.9. Upgrade to #v0.1.9.
InitializeAsync returns falseAPI key must start with phase_; BaseUrl must be HTTPS unless AllowInsecureDev is true
No events in dashboardMake sure IdentifyAsync completes; test on device, not Editor-only (DisableInEditor skips network)
HTTP 401Invalid or revoked API key
Queue not flushingDevice online, identified, and app backgrounded or session active; check LogLevel.Info
Duplicate events50 ms dedupe collapses identical name + params; intentional for burst clicks

IL2CPP / release

IssueWhat to check
Stripping / missing typesKeep package link.xml; do not strip Phase.Analytics or Newtonsoft
Newtonsoft errors at runtimeEnsure com.unity.nuget.newtonsoft-json is installed
Works in Editor, not on deviceRun IL2CPP Release on hardware; verify ATS (iOS) and network permissions (Android)

Differences from other SDKs

FeatureExpo / React NativeUnity
Screen trackingAutomatic (optional, Expo Router) or manual trackScreenManual TrackScreen
platform fieldSent (ios / android)Sent on mobile player builds
Distributionnpm phase-analyticsGit UPM #v0.2.0
Init patternProvider / hookInitializeAsync + MonoBehaviour bootstrap

For the same event schema and dashboard, keep event names and parameter keys aligned across your mobile clients.