Telemetry Event Types
trackEvent(name, properties?) from src/metagame/services/analytics.ts is the single funnel for analytics events sent to the Supabase player_events table. Each call appends { event_type, properties } to an in-memory queue; the queue flushes when it reaches 50 events, when a 10-second timer fires, when the tab is hidden (visibilitychange), or when the page unloads (via navigator.sendBeacon to guarantee delivery during teardown). Flushed rows insert into player_events as (event_type text, properties jsonb).
This page catalogues the canonical event types the game emits. Per-run forensic samples (level_start, boss_spawn, player_death, etc.) are sent through a different pipeline — the Sampler writes to telemetry_samples, not player_events — and are out of scope here.
Run lifecycle events
run_start
Fired when a mission boots, before the gameplay loop ticks. Payload identifies the run shape so analytics can join later events back to the same attempt.
| Field | Type | Meaning |
|---|---|---|
nodeId | string | null | Mission node id from the RunDefinition |
planetId | number | null | Planet (3, 12, or 21) the run was launched from |
isChallenge | boolean | Whether this is a challenge-mode run |
sandbox | boolean | Whether the run uses sandbox config |
weaponIds | string[] | Starting weapon ids resolved from the ship loadout |
shipId | string | Hull/ship id |
seed | number | RNG seed for the run |
run_end
Fired when the gameplay loop terminates — death or survival. Sent alongside the full RunTelemetry payload (which goes to a separate run_telemetry table via sendRunTelemetry), giving a compact summary row in player_events for high-level dashboards.
| Field | Type | Meaning |
|---|---|---|
finalLevel | number | Last level reached before exit |
totalKills | number | Lifetime kills this run |
finalScore | number | Score computed by telemetry.finalize() |
causeOfDeath | string | 'destroyed', 'survived', or specific source label |
mission_deployed
Fired when the player commits to a mission from the mission board (post-deploy modal close, pre-engine boot). Captures the loadout the player chose so we can correlate run outcomes back to deploy decisions.
| Field | Type | Meaning |
|---|---|---|
nodeId | string | Mission node id |
planetId | number | Planet id (3, 12, 21) |
shipId | string | Hull selected for the deploy |
weaponSlots | string[] | Weapon ids loaded into slots |
isChallenge | boolean | Challenge-mode flag |
mission_completed
Fired after finalize_run RPC succeeds, once server state is authoritative. This is the “the run is fully banked” signal — distinct from run_end which fires the instant the engine stops.
| Field | Type | Meaning |
|---|---|---|
matchId | string (uuid) | Server-issued match id |
nodeId | string | Mission node id |
planetId | number | Planet id |
survived | boolean | Outcome |
kills | number | Mirrored from MissionResult.combat.totalKills |
durationS | number | Run length in seconds |
tierReached | number | Highest difficulty tier hit |
creditsEarned | number | Currency rewarded by computeArcadeCredits() |
Pull / gacha events
The actual gacha telemetry is split across two existing event names defined in analytics.ts. The conceptual umbrella pull_made is the union of the two.
v4_pull_unlock (aka V4_PULL_UNLOCK)
Per-result event emitted when a pull unlocks a new ship.
| Field | Type | Meaning |
|---|---|---|
shipId | string | Ship that was unlocked |
rarity | string | Rarity bucket (common / rare / epic / legendary) |
v4_pull_xp_gain (aka V4_PULL_XP_GAIN)
Per-result event emitted when a pull returns a duplicate and converts to ship XP / star progress.
| Field | Type | Meaning |
|---|---|---|
shipId | string | Ship that gained XP |
oldXp | number | XP before this pull |
newXp | number | XP after this pull (always oldXp + 1) |
starUp | boolean | True if the new XP crossed a star threshold |
Performance test events
Activated via the ?perftest URL param. Auto-plays the game with cranked-up enemy counts and streams snapshots for overnight soak testing. All three events share a sessionId so they group together.
perftest_start
One-shot at session boot. Carries device fingerprint + active PERF_FLAGS so A/B comparisons across builds are possible.
| Field | Type | Meaning |
|---|---|---|
sessionId | string | Unique session id (perftest_<ts>_<rand>) |
version | string | BUILD_VERSION |
versionTag | string | PERF_VERSION_TAG |
device | object | ua, screen, dpr, effectiveDpr, cores, memGB, isMobile |
flags | object | All PERF_FLAGS toggles (noStickers, noTerrain, dprOverride, etc.) |
perftest_snapshot
Emitted every 30 seconds during a perftest session. The bread-and-butter row — render perf, spike data, memory, run progress.
| Field | Type | Meaning |
|---|---|---|
sessionId | string | Session group key |
snapshotIdx | number | Monotonic counter (0-based) |
runNumber | number | How many auto-restarts have occurred |
runElapsedSec | number | Seconds since the current run started |
version | string | BUILD_VERSION |
versionTag | string | PERF_VERSION_TAG |
device | object | Same shape as perftest_start |
flags | object | Same shape as perftest_start |
perf | object | diagGetPerfSnapshot() output — per-pass timings, canvas health, VRAM, entity counts |
spikeCount | number | Number of spike events drained since the last snapshot |
worstSpikes | object[] | Top-5 spikes by frameMs (full per-pass attribution) |
memory | object | null | JS heap (usedMB, totalMB, limitMB) — Chromium-only |
perftest_end
One-shot at session shutdown.
| Field | Type | Meaning |
|---|---|---|
sessionId | string | Session group key |
totalSnapshots | number | Final snapshotIdx value |
totalRuns | number | Auto-restart count for the session |
version | string | BUILD_VERSION |
Error / diagnostic events
Emitted by src/starship-survivors/engine/core/remote-errors.ts via the injected trackEvent callback. Booted from App.tsx once at startup. All three of global_error, unhandled_rejection, and render_error go through the same _send() helper, which:
- Deduplicates by
${eventType}:${source}:${message}for 30 seconds (DEDUP_INTERVAL_MS = 30_000) so a tight error loop can’t floodplayer_events. - Caps the dedup map at 200 entries (evicts entries older than the dedup interval when full).
- Stamps every payload with
_deviceCtx(ua,screen,dpr,mem,cores) and an ISOts.
global_error
Captured from window.addEventListener('error', ...) — uncaught exceptions on the main thread.
| Field | Type | Meaning |
|---|---|---|
message | string | ev.message |
filename | string | ev.filename |
lineno | number | ev.lineno |
colno | number | ev.colno |
ua, screen, dpr, mem, cores, ts | mixed | Device context + timestamp |
unhandled_rejection
Captured from window.addEventListener('unhandledrejection', ...) — promise rejections with no .catch.
| Field | Type | Meaning |
|---|---|---|
message | string | reason.message or String(reason) |
stack | string | First 500 chars of reason.stack |
ua, screen, dpr, mem, cores, ts | mixed | Device context + timestamp |
render_error
Reported imperatively via reportRenderError(source, detail, extra?). The signal for canvas context loss, sticker pipeline crashes, render-pass exceptions — anywhere the engine catches a render-layer failure rather than letting it propagate.
| Field | Type | Meaning |
|---|---|---|
source | string | Subsystem label (e.g. 'sticker', 'shadow', 'postFx') |
detail | string | Free-form description of the failure |
...extra | object | Caller-supplied context (canvas size, GL state, etc.) |
ua, screen, dpr, mem, cores, ts | mixed | Device context + timestamp |
perf_warning
Reported imperatively via reportPerfWarning(source, detail, extra?). Used when the engine detects sustained low FPS, canvas memory spikes, or other “things are getting bad but we haven’t crashed” conditions.
| Field | Type | Meaning |
|---|---|---|
source | string | Subsystem label |
detail | string | Description ('sustained <30fps for 5s', 'canvas mem > 200MB', etc.) |
...extra | object | Caller-supplied context |
ua, screen, dpr, mem, cores, ts | mixed | Device context + timestamp |
Pipeline guarantees
- Batched, not real-time. Events sit in memory up to 10 seconds before flushing. Don’t rely on them for synchronous decision-making.
- Beacon-safe on unload. The
flushAnalyticsSync()path usesnavigator.sendBeaconwithapikeyas a query param (since beacons can’t set custom headers). Falls back to keepalivefetchon browsers without beacon support. - No PII. Properties carry game state and device fingerprint, not identifiers like email or full UA cookies. Player association is via the auth session attached server-side to the insert.
- Errors are deduplicated. Identical error events within 30 seconds collapse to a single row. This is intentional — a render crash that fires 60 fps would otherwise consume the entire daily Supabase quota.
See also
- run-finalize-rpc — server-side finalize that emits
mission_completed-equivalent state changes. - match-history-record — the durable per-run row that pairs with
run_end. - perftest-mode — how
?perftestboots the perf snapshot loop.