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.

FieldTypeMeaning
nodeIdstring | nullMission node id from the RunDefinition
planetIdnumber | nullPlanet (3, 12, or 21) the run was launched from
isChallengebooleanWhether this is a challenge-mode run
sandboxbooleanWhether the run uses sandbox config
weaponIdsstring[]Starting weapon ids resolved from the ship loadout
shipIdstringHull/ship id
seednumberRNG 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.

FieldTypeMeaning
finalLevelnumberLast level reached before exit
totalKillsnumberLifetime kills this run
finalScorenumberScore computed by telemetry.finalize()
causeOfDeathstring'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.

FieldTypeMeaning
nodeIdstringMission node id
planetIdnumberPlanet id (3, 12, 21)
shipIdstringHull selected for the deploy
weaponSlotsstring[]Weapon ids loaded into slots
isChallengebooleanChallenge-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.

FieldTypeMeaning
matchIdstring (uuid)Server-issued match id
nodeIdstringMission node id
planetIdnumberPlanet id
survivedbooleanOutcome
killsnumberMirrored from MissionResult.combat.totalKills
durationSnumberRun length in seconds
tierReachednumberHighest difficulty tier hit
creditsEarnednumberCurrency 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.

FieldTypeMeaning
shipIdstringShip that was unlocked
raritystringRarity 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.

FieldTypeMeaning
shipIdstringShip that gained XP
oldXpnumberXP before this pull
newXpnumberXP after this pull (always oldXp + 1)
starUpbooleanTrue 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.

FieldTypeMeaning
sessionIdstringUnique session id (perftest_<ts>_<rand>)
versionstringBUILD_VERSION
versionTagstringPERF_VERSION_TAG
deviceobjectua, screen, dpr, effectiveDpr, cores, memGB, isMobile
flagsobjectAll 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.

FieldTypeMeaning
sessionIdstringSession group key
snapshotIdxnumberMonotonic counter (0-based)
runNumbernumberHow many auto-restarts have occurred
runElapsedSecnumberSeconds since the current run started
versionstringBUILD_VERSION
versionTagstringPERF_VERSION_TAG
deviceobjectSame shape as perftest_start
flagsobjectSame shape as perftest_start
perfobjectdiagGetPerfSnapshot() output — per-pass timings, canvas health, VRAM, entity counts
spikeCountnumberNumber of spike events drained since the last snapshot
worstSpikesobject[]Top-5 spikes by frameMs (full per-pass attribution)
memoryobject | nullJS heap (usedMB, totalMB, limitMB) — Chromium-only

perftest_end

One-shot at session shutdown.

FieldTypeMeaning
sessionIdstringSession group key
totalSnapshotsnumberFinal snapshotIdx value
totalRunsnumberAuto-restart count for the session
versionstringBUILD_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 flood player_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 ISO ts.

global_error

Captured from window.addEventListener('error', ...) — uncaught exceptions on the main thread.

FieldTypeMeaning
messagestringev.message
filenamestringev.filename
linenonumberev.lineno
colnonumberev.colno
ua, screen, dpr, mem, cores, tsmixedDevice context + timestamp

unhandled_rejection

Captured from window.addEventListener('unhandledrejection', ...) — promise rejections with no .catch.

FieldTypeMeaning
messagestringreason.message or String(reason)
stackstringFirst 500 chars of reason.stack
ua, screen, dpr, mem, cores, tsmixedDevice 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.

FieldTypeMeaning
sourcestringSubsystem label (e.g. 'sticker', 'shadow', 'postFx')
detailstringFree-form description of the failure
...extraobjectCaller-supplied context (canvas size, GL state, etc.)
ua, screen, dpr, mem, cores, tsmixedDevice 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.

FieldTypeMeaning
sourcestringSubsystem label
detailstringDescription ('sustained <30fps for 5s', 'canvas mem > 200MB', etc.)
...extraobjectCaller-supplied context
ua, screen, dpr, mem, cores, tsmixedDevice 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 uses navigator.sendBeacon with apikey as a query param (since beacons can’t set custom headers). Falls back to keepalive fetch on 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