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.1.9"
  }
}

Return to Unity and let Package Manager resolve the dependency.

  1. Window → Package Manager
  2. + → Add package from git URL…
  3. Paste:
https://github.com/Phase-Analytics/Phase.git?path=packages/phase-unity#v0.1.9
  1. Select Add

Pin the release tag:

#v0.1.9

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

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

  1. Window → Package Manager
  2. Select Phase Analytics in the left list
  3. Open Samples
  4. Import Phase Analytics Sample
  5. Add PhaseAnalyticsBootstrap to 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.

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. 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, or null
  • 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:

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(),
        });
    }
}

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. Required before reliable tracking.
Track(string, EventParams?)Queues a custom event. Requires prior identify.
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():

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 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
  • 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.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.

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)

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 dashboardCall IdentifyAsync before Track; 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)Manual Track events
platform fieldSent (ios / android)Sent on mobile player builds
Distributionnpm phase-analyticsGit UPM #v0.1.9
Init patternProvider / hookInitializeAsync + MonoBehaviour bootstrap

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