remote-errors.ts

Bridges engine-side errors to the metagame analytics pipeline. Sole module that forwards render crashes, uncaught exceptions, unhandled promise rejections, and perf warnings to the player_events Supabase table via the injected trackEvent() batcher. Avoids importing metagame deps directly — receives trackEvent at boot to break the circular import.

Event types

Event nameSource
render_errorSticker/canvas context loss, render pass crash (reportRenderError)
global_errorUncaught exceptions via window.onerror
unhandled_rejectionUnhandled promise rejections via window.onunhandledrejection
perf_warningSustained low FPS or canvas memory spikes (reportPerfWarning)

Exports

SymbolSignatureRole
initRemoteErrors(trackFn) => voidBoot — install global listeners, capture device context, store the callback
reportRenderError(source, detail, extra?) => voidPush a render_error event
reportPerfWarning(source, detail, extra?) => voidPush a perf_warning event

Boot — initRemoteErrors

Called once from App.tsx after analytics is ready. Stores the trackEvent callback in module-local _track. Captures device context once: ua, screen (WxH), dpr, mem (navigator.deviceMemory or 'unknown'), cores (navigator.hardwareConcurrency or 'unknown'). Registers window listeners for error and unhandledrejection.

Promise rejection events serialize reason.message || String(reason) and slice reason.stack to 500 chars.

Dedup

ConstantValueEffect
DEDUP_INTERVAL_MS30_000Same key is dropped if last send was within 30s

Dedup key: ${eventType}:${source||''}:${message||detail||''}. Stored in _sent: Map<string, number> (last send timestamp).

Map is capped at 200 entries; on overflow, entries older than now - DEDUP_INTERVAL_MS are pruned. No hard cap beyond pruning sweep.

Send path — _send

  1. Short-circuit if _track is null (not booted yet — safe to call from hot paths pre-boot).
  2. Build dedup key, drop if recent.
  3. Record timestamp, prune map if oversized.
  4. Call _track(eventType, { ..._deviceCtx, ...props, ts: ISOString }).

Device context is spread first, so per-event props can override (e.g. an event-level ua would win).

Hot-path safety

reportRenderError and reportPerfWarning are designed for render-loop and per-frame use — they no-op when not booted, dedupe identical messages, and never throw. Caller passes a stable source string for effective dedup.

EXTRACT-CANDIDATE

  • Event-type list (render_error, global_error, unhandled_rejection, perf_warning) is duplicated in the JSDoc header and again in event-type mapping in Supabase ingestion; a shared RemoteErrorEvent union type would lock both ends.
  • DEDUP_INTERVAL_MS = 30_000 and the 200-entry cap are unnamed magic numbers in the prune branch — extract DEDUP_MAP_MAX = 200.
  • Stack trace slice length (500) is inline magic in the unhandledrejection handler — extract STACK_TRACE_MAX_CHARS = 500.
  • Device-context capture (ua, screen, dpr, mem, cores) duplicates similar logic likely present in analytics boot; consider a shared captureDeviceContext() helper if a second caller appears.
  • The dedup key builder is implicit string concat — a keyFor(eventType, props) helper would centralize the rule.