Unity SDK
This guide explains how to automatically collect performance telemetry using the Framedash Unity SDK.
Requirements
Section titled “Requirements”- Unity 2022.3 or later
- .NET Standard 2.1 / .NET Framework 4.x
Installation
Section titled “Installation”Unity Package Manager (Recommended)
Section titled “Unity Package Manager (Recommended)”- Open Window > Package Manager. Unity 6.5 renamed this path to Window > Package Management > Package Manager; older versions keep the shorter Window > Package Manager.
- Select ”+” > “Add package from git URL…”
- Enter the following URL:
https://github.com/crane-valley/framedash-unity-sdk.gitTo pin a specific release, append a version tag:
https://github.com/crane-valley/framedash-unity-sdk.git#v0.1.7To install from a script or CI, add the dependency directly to Packages/manifest.json. The package name is com.framedash.sdk:
{ "dependencies": { "com.framedash.sdk": "https://github.com/crane-valley/framedash-unity-sdk.git#v0.1.7" }}Initial Setup
Section titled “Initial Setup”Initialize the SDK once at startup, for example from a MonoBehaviour on a persistent GameObject:
using System.Collections.Generic;using Framedash;using UnityEngine;
public sealed class GameBootstrap : MonoBehaviour{ // Assign the API key in the Inspector, or leave it empty to fall back to // the FRAMEDASH_API_KEY environment variable. See the note below for the // full resolution order and platform caveats. [SerializeField] private string _apiKey;
private void Awake() { TelemetrySDK.Initialize( apiKey: _apiKey, buildId: Application.version); }}endpointUrl is optional and defaults to https://ingest.framedash.dev/v1/events; pass it explicitly to target a local or self-hosted ingest endpoint. playerId sets the player ID at initialization, the same as calling SetPlayerId afterward. enableOfflineQueue defaults to true; pass false to disable the disk offline queue. The full signature is Initialize(string apiKey = null, string endpointUrl = null, string buildId = null, string playerId = null, bool enableOfflineQueue = true).
Automatic Performance Data Collection
Section titled “Automatic Performance Data Collection”The SDK automatically collects the following data:
- FPS: Frame rate
- Frame Time: Processing time per frame
- Memory: Unity Profiler total allocated memory
Sending Custom Events
Section titled “Sending Custom Events”TelemetrySDK.Instance.Track( eventName: "player_death", mapId: "map_01", position: transform.position);To attach categorical attributes or numeric metrics, pass the optional dictionaries:
// Ensure 'using System.Collections.Generic;' is at the top of your fileTelemetrySDK.Instance.Track( eventName: "player_death", mapId: "map_01", position: transform.position, attributes: new Dictionary<string, string> { { "cause", "fall_damage" } }, metrics: new Dictionary<string, float> { { "health", 0f } });Identifying the Player (Optional)
Section titled “Identifying the Player (Optional)”By default events are sent anonymously. Call SetPlayerId after the player logs in to associate subsequent events with them:
TelemetrySDK.Instance.SetPlayerId(playerId);DAU counts only non-empty player IDs. Sessions that remain anonymous still contribute events and sessions, but they do not increase DAU or MAU. Call SetPlayerId before the activity you want included in player-based KPIs.
Runtime Sampling Override
Section titled “Runtime Sampling Override”The SDK applies a global sampling rate (default 1.0 = keep all). High-frequency events can opt into a lower per-event-name rate that overrides the global rate at runtime:
TelemetrySDK.Instance.SetEventSamplingRate("ai_pathfind_step", 0.05f); // ~5%TelemetrySDK.Instance.RemoveEventSamplingRate("ai_pathfind_step"); // back to the global rateSetEventSamplingRate(string eventName, float rate) sets the per-event rate, clamped to [0, 1]. RemoveEventSamplingRate(string eventName) drops the override so the event falls back to the global rate. Auto-collected events (session_start, perf_heartbeat) bypass sampling.
Map Load-Time Capture
Section titled “Map Load-Time Capture”Available in Unity SDK 0.1.3 and later. The SDK can time how long a map or level takes to load and report it as a map_load event. The load time feeds the build-comparison / perf-diff regression gate and the dashboard load-time charts.
Wrap a load you control with BeginMapLoad / EndMapLoad:
TelemetrySDK.Instance.BeginMapLoad("Level_01");// ... load the scene ...TelemetrySDK.Instance.EndMapLoad();BeginMapLoad starts a timer on a monotonic clock that pause and time-scale do not affect, and EndMapLoad stops it and emits the event. Calling BeginMapLoad again before EndMapLoad replaces the pending measurement.
If a custom or streaming loader already measures the time itself, report it directly:
TelemetrySDK.Instance.ReportMapLoad("Level_01", 1234f); // load time in millisecondsReportMapLoad drops the sample entirely when loadTimeMs is NaN, infinite, or negative; the value is not clamped.
Both paths emit a map_load event carrying metrics["load_time_ms"] and attributes["map_name"]. The event leaves map_id empty on purpose, so it stays out of spatial heatmaps and the activation gate. These calls run on the main thread, never throw, and are a no-op before Initialize. When a custom or streaming loader finishes on a worker thread, marshal back to the main thread (for example through the player-loop update or a captured SynchronizationContext) before calling EndMapLoad or ReportMapLoad. The SDK does not marshal for you, and a call from another thread silently drops the event.
Disk I/O Metrics
Section titled “Disk I/O Metrics”Available in Unity SDK 0.1.3 and later. The SDK can attach disk-read counters to the perf_heartbeat event under the io.read_bytes, io.read_time_ms, and io.read_ops metric keys. Each value is a delta since the previous heartbeat, and the keys appear only after a real sample has landed (there is no zero-stuffing). Like the other performance metrics, io.* feeds the perf-diff / builds-compare regression gate and the dashboard charts. There are no io.* threshold alerts.
In the Unity Editor and in Development Builds the SDK samples AsyncReadManagerMetrics automatically. Release players collect no automatic io.* samples.
Memory-Category Metrics
Section titled “Memory-Category Metrics”Available in Unity SDK 0.1.4 and later. The SDK samples memory-category usage at the heartbeat cadence and automatically attaches it as mem.* metrics on perf_heartbeat events. Unlike UE5’s bTrackMemoryDetail, no opt-in is needed.
mem.vram: allocated graphics-driver memory in bytes, read fromProfiler.GetAllocatedMemoryForGraphicsDriver.mem.heap: managed-heap usage in bytes, read fromProfiler.GetMonoUsedSizeLong.
A zero reading is omitted: an absent key means the value was not collected, and the SDK never sends a fabricated 0.
Besides perf_heartbeat, these keys attach to position-qualified events (any event with a non-empty map_id). Because perf_heartbeat carries an empty map_id and never enters the spatial heatmap grid, the SDK attaches the same sample to position-qualified events so per-cell memory heatmaps work. Position-qualified events carry a cached sample refreshed at the heartbeat cadence, so the event path performs no engine reads.
Caller-supplied metric keys always win, both on a key collision and on capacity: mem.* fills only the slots left below the 50-metric ingest cap, with mem.vram first.
In-Editor SceneView Heatmap
Section titled “In-Editor SceneView Heatmap”Available in Unity SDK 0.1.4 and later. The editor-only Framedash.Editor assembly fetches the project’s maps and cloud-aggregated heatmap cells from the Framedash REST API and renders them inside the Unity editor.
It requires a Read API Key with the analytics:read scope plus the Project ID, never the game’s write-only Ingest key.
In Unity SDK 0.1.6 and later, you can leave Read API Key empty and set FRAMEDASH_ANALYTICS_API_KEY before launching Unity. The environment value is never saved under UserSettings/. An explicitly entered Read API Key takes precedence over the environment variable.
The fetched heatmap cells are drawn as translucent quads in the SceneView at their recorded world coordinates, so you can inspect the spatial heatmap inside the editor without a packaged build or the dashboard.
Settings persist per-project under UserSettings/, which is never packaged and never tracked by version control.
Verbose Logging
Section titled “Verbose Logging”During your first integration, enable verbose logging to confirm delivery:
TelemetrySDK.Instance.VerboseLogging = true;A successful flush logs [Framedash] Flushed N events (HTTP 202). Until you call SetPlayerId, every session also logs the warning [Framedash] No player_id set. Events will be sent as anonymous...; this is informational, not an error. Auto-collected events (session_start at initialization and perf_heartbeat every 10 seconds) batch together with your manual events, so Flushed N events can report more than the number you tracked — that is expected, not duplication. If events do not arrive, see Troubleshooting.
Headless / CI
Section titled “Headless / CI”The SDK transmits on the Unity player loop: Flush sends through a coroutine and UnityWebRequest, which only advance while the loop is running. A plain Unity.exe -batchmode -executeMethod ... call runs in Edit mode, where coroutines do not tick, so events are buffered but never sent.
To emit telemetry from CI, drive the player loop with a PlayMode test (Unity Test Framework). Enter Play mode, initialize the SDK, track your events, and let the test run long enough for a flush to complete before exiting:
using System.Collections;using Framedash;using NUnit.Framework;using UnityEngine;using UnityEngine.TestTools;
public sealed class TelemetrySmokeTest{ [UnityTest] public IEnumerator SendsAMarkerEvent() { TelemetrySDK.Instance.VerboseLogging = true; // Reads the key from the FRAMEDASH_API_KEY environment variable (no hardcoded secret). TelemetrySDK.Initialize(buildId: "ci-smoke"); // If the project's global SamplingRate is < 1.0, the marker can be sampled // out while the automatic session_start still returns HTTP 2xx, so the log // assertion passes with no marker (a false-positive verification). Force this // event's rate to 1.0 so the marker is always kept. TelemetrySDK.Instance.SetEventSamplingRate("ci_marker", 1f); TelemetrySDK.Instance.Track(eventName: "ci_marker", mapId: "ci", position: default); // Wait past the 10s auto-heartbeat interval so at least one perf_heartbeat fires, // then make Flush() the very last SDK call so nothing is tracked after it. The // default flush interval is 30s (longer than this test), so force the send and // wait for the HTTP request to complete before exit. yield return new WaitForSeconds(12f); TelemetrySDK.Instance.Flush(); // final flush; a single event never hits the batch-size threshold yield return new WaitForSeconds(3f); // minimum settle time before exit; a slow send may still be in flight, so confirm delivery via the HTTP 202 log line (below) }}The test needs its own assembly definition so the Unity Test Framework can compile and discover it. Place this .asmdef next to the test file:
{ "name": "Framedash.SmokeTests", "references": [ "UnityEngine.TestRunner", "Framedash.Runtime" ], "includePlatforms": [], "excludePlatforms": [], "defineConstraints": ["UNITY_INCLUDE_TESTS"], "precompiledReferences": ["nunit.framework.dll"], "autoReferenced": false, "overrideReferences": true}The recommended layout keeps the .asmdef and the test file together in Assets/Tests/PlayMode/:
Assets/ Tests/ PlayMode/ Framedash.SmokeTests.asmdef TelemetrySmokeTest.csRun the test from CI with:
Unity.exe -batchmode -nographics -projectPath <path> -runTests -testPlatform PlayMode -testResults <path>\results.xml -logFile -On Windows, make your shell wait for the real exit. Unity.exe -batchmode ... -runTests returns to the shell in about 2-3 seconds while the editor keeps running and only finishes the tests roughly 20 seconds later, so a script that reads the immediate exit code sees a false pass. Launch it with Start-Process -Wait -PassThru and read the exit code from the finished process:
$proc = Start-Process -FilePath "Unity.exe" -Wait -PassThru -ArgumentList @( "-batchmode", "-nographics", "-projectPath", '"<path>"', "-runTests", "-testPlatform", "PlayMode", "-testResults", '"<path>\results.xml"', "-logFile", '"<path>\unity.log"')if ($proc.ExitCode -ne 0) { throw "Unity tests failed (exit code $($proc.ExitCode))" }$resultsPath = "<path>\results.xml"if (-not (Test-Path -LiteralPath $resultsPath)) { throw "Unity did not write test results" }[xml]$results = Get-Content -Raw -LiteralPath $resultsPath$testRun = $results.'test-run'if (-not $testRun) { throw "Invalid test results XML format" }$testCount = if ($testRun.testcasecount) { [int]$testRun.testcasecount } else { [int]$testRun.total }if ($testCount -lt 1) { throw "Unity discovered zero PlayMode tests" }-Wait blocks until Unity actually exits and -PassThru returns the process, so $proc.ExitCode reflects the test outcome (0 = pass). The XML assertion is equally important: Unity can exit successfully after discovering zero tests, so CI must require at least one result. Each path placeholder is wrapped in embedded double quotes ('"<path>"') so a workspace path containing spaces (for example C:\build agent\game) survives Start-Process argument splitting. Send -logFile to a real file rather than - so you can inspect it afterward.
A green test run does not by itself prove telemetry was delivered: the test can pass while the flush is still in flight or has failed. Before you treat the pipeline as green, confirm delivery. Grep the Unity log for the flush-success line, which carries the HTTP 202 result:
[Framedash] Flushed N events (HTTP 202)Alternatively, query the REST API for the marker event you sent. Only treat the pipeline as passing once one of those confirms the event landed.
FRAMEDASH_API_KEY supplies the key in CI (the sample above relies on it, so no secret is hardcoded). Call TelemetrySDK.Initialize(buildId: ...) (as above) or the no-argument TelemetrySDK.Initialize().
When the offline queue is enabled (the default), events buffer in it on a graceful exit or a transient send failure, and are sent on the next initialization. A hard kill loses whatever is still buffered, so in CI wait for the HTTP 202 line in the log before killing the process rather than relying on the queue. With enableOfflineQueue: false, unflushed events are simply dropped. See Troubleshooting.
CI / Automated Sessions
Section titled “CI / Automated Sessions”In an automated test or profiling run, tag the whole session so every event carries the CI build and its branch/commit/scenario. Call the automated-session API once from your test entry point:
using Framedash;
// All arguments are optional.TelemetrySDK.Instance.BeginAutomatedSession( buildId: "build-123", branch: "main", commit: "abc1234", scenario: "nightly");// ... run the automated scenario ...TelemetrySDK.Instance.EndAutomatedSession();The full signature is void BeginAutomatedSession(string buildId = null, string branch = null, string commit = null, string scenario = null). In CI, prefer BeginAutomatedSessionFromEnvironment(), which reads FRAMEDASH_BUILD_ID, FRAMEDASH_GIT_BRANCH, FRAMEDASH_GIT_COMMIT, and FRAMEDASH_TEST_SCENARIO (the variables framedash run-profile-test exports):
TelemetrySDK.Instance.BeginAutomatedSessionFromEnvironment();// ... run the automated scenario ...TelemetrySDK.Instance.EndAutomatedSession();An automated session tags every event in the session with a build_id override and ci.branch / ci.commit / ci.scenario attributes, which is what feeds the build-comparison / perf-diff CI gate. It does not change any event’s source: the SDK’s automatic events (session_start, perf_heartbeat) stay source=automated and your Track events stay source=player, in CI as in normal play. See CI Profiling for the full pipeline.
Next Steps
Section titled “Next Steps”- Data Model: Telemetry data structure
- Troubleshooting: When events do not show up
- Heatmaps: Visualizing collected data