PURPOSE
Ad-hoc client diagnostic events fanned out across two channels in parallel: Sentry for searchable/alertable bug reports, and Supabase client_diag_events (migration 043) as a durable backup that survives a misconfigured Sentry DSN or a user whose network blocks sentry.io. Both sends are fire-and-forget and never throw, so callers can fire from render loops, error handlers, or unload paths without guarding.
OWNS
- Module-level mutable
_currentPlayerId(nullable string) used to attribute Supabase rows to the authenticated player. - The
DiagEventshape:event_type(stable identifier doubling as Sentry tag and Supabase column), optionallevel(info | warning | error, defaults towarning), optionalmessage(defaults toevent_type), optionalpayloadrecord of arbitrary structured context. - The dual-channel send semantics for a single logical diagnostic event.
READS FROM
import.meta.env.VITE_SUPABASE_URLandimport.meta.env.VITE_SUPABASE_ANON_KEY— resolved at module load intoSUPABASE_URLandSUPABASE_KEYconstants (default to empty string when unset).BUILD_VERSIONfrom../core/configfor theapp_versioncolumn.navigator.userAgentwhennavigatoris defined; otherwise serializesnull.- Module-private
_currentPlayerId.
PUSHES TO
- Sentry via dynamic
import('@sentry/browser'), callingSentry.captureMessage(message, level)inside awithScopeblock. The event_type is attached as abugtag, and every payload entry is attached as a Sentry extra. - Supabase REST endpoint
${SUPABASE_URL}/rest/v1/client_diag_eventsviafetchPOST withapikeyandAuthorization: Bearerheaders, JSON-array body, andkeepalive: trueso the request survives page unload.
DOES NOT
- Does not throw under any condition. Sentry import failures are swallowed with
.catch(() => {}); Supabase fetch rejections are swallowed the same way; the entire Supabase block is wrapped in a defensivetry/catchto absorb anyimport.metaweirdness. - Does not retry, queue, batch, or persist failed sends.
- Does not initialize Sentry. If
Sentry.initwas skipped (no DSN),captureMessageis a no-op and that is the intended fallback. - Does not POST to Supabase when either
SUPABASE_URLorSUPABASE_KEYis empty — early-returns silently. - Does not validate or sanitize
payloadcontents. - Does not await either send; callers cannot observe success or failure.
Signals
- Sentry tag:
bug=event_type. - Sentry message:
message(orevent_typeif message omitted). - Sentry level:
info | warning | error(defaultwarning). - Sentry extras: one per
payloadentry. - Supabase row columns posted:
player_id,event_type,level,message,payload,app_version,user_agent.
Entry points
setDiagPlayerId(playerId: string | null): void— stores the authenticated player ID for subsequent Supabase event attribution.logDiag(evt: DiagEvent): void— fires one diagnostic event to both channels.
Pattern notes
- Sentry is loaded via dynamic
import()so bundles do not pay the Sentry cost when the DSN is disabled at the deploy. - Both channels run unconditionally on every
logDiagcall; there is no preference order or short-circuit between them. keepalive: trueon the fetch is what permits callinglogDiagfrom unload handlers and crash paths.- The Supabase body is wrapped in an array (
[{ ... }]) to match PostgREST’s batch-insert shape. _currentPlayerIdis module-private; the only mutator issetDiagPlayerId. The default isnull, which is posted asplayer_id: nullfor unauthenticated sessions.