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 eventQueue array of pending events.
  • The flushTimer debounce handle.
  • Cached SUPABASE_URL and SUPABASE_KEY values pulled once from import.meta.env for the sendBeacon path.
  • The visibilitychange document 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_URL and import.meta.env.VITE_SUPABASE_ANON_KEY — read once at module load for the sync sendBeacon path.
  • document.hidden and document.visibilityState via the visibilitychange event.
  • navigator.sendBeacon when available.

PUSHES TO

  • Supabase player_events table — async batched insert via the Supabase client in flush().
  • Supabase REST endpoint /rest/v1/player_events — direct POST via navigator.sendBeacon (or keepalive fetch fallback) in flushSync(), authenticated with the anon key as a query param and bearer header.
  • console.error for 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_type and properties are sent; any enrichment must happen on the Supabase side or in the caller.
  • Does not validate event_type against a known list.
  • Does not attach a beforeunload or pagehide listener itself — the caller (App.tsx) is responsible for invoking flushAnalyticsSync on 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 from trackEvent.
  • FLUSH_INTERVAL timer fires (10 seconds) after the most recent event below the batch threshold.
  • document.visibilitychange fires with document.hidden === true — async flush.
  • External call to flushAnalyticsSync (intended for beforeunload / 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 via navigator.sendBeacon (or keepalive fetch fallback). 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.sendBeacon because regular fetch is routinely cancelled by the browser during page teardown. sendBeacon is the only reliable mechanism during unload.
  • sendBeacon does not support custom headers, so the Supabase anon key is passed as the apikey query-string parameter — Supabase REST accepts auth via query param for this reason.
  • The payload is wrapped in a Blob with type: 'application/json' so sendBeacon sets the correct Content-Type.
  • If sendBeacon returns false (queue too large), the code falls back to a fetch call with keepalive: true, which is the second-best option during unload.
  • A second fallback handles very old browsers without sendBeacon at all — a keepalive fetch directly.
  • flush() uses eventQueue.splice(0, MAX_BATCH_SIZE) to atomically take up to one batch’s worth of events, leaving any overflow for the next flush.
  • flushSync() uses eventQueue.splice(0) to drain the entire queue in one shot — there is no second chance during unload.
  • The visibilitychange auto-listener is registered behind a typeof 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.