PURPOSE
Analytics event batching pipeline. Queues events in memory and flushes them to the Supabase player_events table in batches, with a guaranteed-delivery path for page unload via navigator.sendBeacon.
OWNS
- The in-memory
eventQueuearray of pending events. - The
flushTimerdebounce handle. - Cached
SUPABASE_URLandSUPABASE_KEYvalues pulled once fromimport.meta.envfor the sendBeacon path. - The
visibilitychangedocument listener that auto-flushes when the tab is hidden. - The v4 ship-rework event-name constants (
V4_MOD_TEMPLATE_UNLOCK,V4_MOD_BUY_PLACE,V4_MOD_DESTROY,V4_PULL_UNLOCK,V4_PULL_XP_GAIN).
READS FROM
./supabase— the shared Supabase client, used for the async batched insert path.import.meta.env.VITE_SUPABASE_URLandimport.meta.env.VITE_SUPABASE_ANON_KEY— read once at module load for the sync sendBeacon path.document.hiddenanddocument.visibilityStatevia thevisibilitychangeevent.navigator.sendBeaconwhen available.
PUSHES TO
- Supabase
player_eventstable — async batched insert via the Supabase client inflush(). - Supabase REST endpoint
/rest/v1/player_events— direct POST vianavigator.sendBeacon(or keepalivefetchfallback) influshSync(), authenticated with the anon key as a query param and bearer header. console.errorfor flush failures.
DOES NOT
- Does not retry failed flushes — errors are logged and the batch is dropped.
- Does not persist the queue across reloads — in-memory only.
- Does not deduplicate events.
- Does not add timestamps, user IDs, or session IDs — only
event_typeandpropertiesare sent; any enrichment must happen on the Supabase side or in the caller. - Does not validate
event_typeagainst a known list. - Does not attach a
beforeunloadorpagehidelistener itself — the caller (App.tsx) is responsible for invokingflushAnalyticsSyncon unload. - Does not flush synchronously on regular
visibilitychange— uses the async path because the page is only being backgrounded, not torn down.
Signals
Flush is triggered by any of:
- Queue length reaches
MAX_BATCH_SIZE(50 events) — immediate async flush fromtrackEvent. FLUSH_INTERVALtimer fires (10 seconds) after the most recent event below the batch threshold.document.visibilitychangefires withdocument.hidden === true— async flush.- External call to
flushAnalyticsSync(intended forbeforeunload/pagehide) — sync flush via sendBeacon.
After a successful flush, if the queue is non-empty (more events arrived during the in-flight insert), scheduleFlush is called again to drain the remainder on the next interval tick.
Entry points
trackEvent(name, properties?)— primary public API. Pushes one event onto the queue and either flushes immediately (queue at cap) or schedules a debounced flush.flushAnalytics()— async; clears the flush timer and awaits a batched insert via the Supabase client.flushAnalyticsSync()— sync; clears the flush timer and dispatches vianavigator.sendBeacon(or keepalivefetchfallback). Intended for page-unload handlers only.- Exported constants:
V4_MOD_TEMPLATE_UNLOCK,V4_MOD_BUY_PLACE,V4_MOD_DESTROY,V4_PULL_UNLOCK,V4_PULL_XP_GAIN— canonical event-name strings for the v4 ship-rework Track A (mods) and Track C (pulls) instrumentation.
Pattern notes
- The unload path uses
navigator.sendBeaconbecause regularfetchis routinely cancelled by the browser during page teardown.sendBeaconis the only reliable mechanism during unload. sendBeacondoes not support custom headers, so the Supabase anon key is passed as theapikeyquery-string parameter — Supabase REST accepts auth via query param for this reason.- The payload is wrapped in a
Blobwithtype: 'application/json'sosendBeaconsets the correct Content-Type. - If
sendBeaconreturnsfalse(queue too large), the code falls back to afetchcall withkeepalive: true, which is the second-best option during unload. - A second fallback handles very old browsers without
sendBeaconat all — a keepalivefetchdirectly. flush()useseventQueue.splice(0, MAX_BATCH_SIZE)to atomically take up to one batch’s worth of events, leaving any overflow for the next flush.flushSync()useseventQueue.splice(0)to drain the entire queue in one shot — there is no second chance during unload.- The
visibilitychangeauto-listener is registered behind atypeof document !== 'undefined'guard so the module is safe to import in SSR / Node test contexts. - Module-level mutable state (
eventQueue,flushTimer) is singleton-by-import — there is exactly one queue per page. - All flush errors are swallowed after logging; analytics is best-effort and must never throw into caller code paths.