Sentry Pipeline
Sentry is the searchable, alertable error and event sink for the browser
client. Initialization, per-call scoping, and the choice between
captureException and captureMessage all follow a single pattern so issues
pivot cleanly in the Sentry UI.
Initialization
src/main.tsx calls Sentry.init exactly once, before React mounts the app,
and only when VITE_SENTRY_DSN is present in the build environment:
dsn:import.meta.env.VITE_SENTRY_DSN— the DSN identifies the project on sentry.io and routes events to it.release:BUILD_VERSION(fromengine/core/config.ts) — every event is tagged with the exact build, so regressions can be isolated to a deploy.tracesSampleRate:0.1— 10 % of transactions are sampled for performance tracing. Errors are not sampled; every captured exception/message is sent.environment:import.meta.env.MODE—productionon Vercel,developmentfor local Vite, separating live-traffic noise from dev experiments.
If VITE_SENTRY_DSN is not present the init is skipped entirely. To make
that state visible (instead of silently swallowing every later capture), the
fallback branch calls logDiag with event_type: 'sentry_init_skipped',
level: 'warning', which writes a row to Supabase client_diag_events. This
prevents the failure mode that hit v5.113.1: “no Sentry events” being read as
“no bugs” when the DSN was actually missing.
Production gating is implicit: the DSN is only set in the production Vercel
env, so dev builds never report. There is no separate enabled flag.
Per-call scoping (Sentry.withScope)
Every call site that captures into Sentry uses Sentry.withScope to attach
tags + extras for that one event without polluting the global scope:
Sentry.withScope((scope) => {
scope.setTag('push_target', target);
scope.setExtra('push_body', body);
scope.setLevel('error');
Sentry.captureMessage(`PUSH failed: HTTP ${status}`, 'error');
});Tags vs extras:
- Tags are indexed and filterable in the Sentry UI. Use them for low-
cardinality dimensions you want to pivot on —
push_failure_reason,push_target,bug(event_type),push_hull, etc. - Extras are stored on the event but not indexed. Use them for arbitrary context dumps — full request bodies, response bodies (truncated to ~2000 chars), stack snapshots, telemetry payloads.
The scope is per-call. It is set up, used inside the callback, and discarded
when withScope returns. Concurrent calls do not contaminate each other’s
tags.
captureException vs captureMessage
The two capture functions are not interchangeable:
Sentry.captureException(err, opts?)— pass a realError(or thrown value). Sentry de-dupes by stack-trace fingerprint, so repeated network failures from the same throw site collapse into one issue. Used inplaygroundPush.tsfor fetch failures (network_error) and JSON-parse failures (bad_response_shape).Sentry.captureMessage(text, level)— pass a string when there is no underlyingErrorobject, just a condition you want logged. Sentry fingerprints on the message string, so include the discriminating tag inside the text (e.g.target=${body.target}) to avoid collapsing distinct failure modes into one issue. Used for HTTP-error responses, SPA-fallback drops, serverok:falseresponses, and thelogDiagad-hoc events.
Levels: 'info' | 'warning' | 'error'. The diagnostic helper
(engine/telemetry/diag.ts) defaults to warning; explicit error is used
for hard failures (push pipeline) and explicit info is reserved for non-bug
breadcrumbs (e.g. Supabase request traces in metagame/services/supabase.ts).
Dual-channel for diagnostics: logDiag
The logDiag(evt) helper in engine/telemetry/diag.ts writes the same event
to both Sentry and Supabase client_diag_events. This is deliberate
redundancy:
- Sentry has the better search/alert UI, so it is the primary surface.
- Supabase is the durable backup — survives Sentry quota exhaustion, DSN misconfiguration on a deploy, or user networks that block sentry.io.
Both sends are fire-and-forget. Sentry is loaded via dynamic import() so the
~50 kB browser SDK doesn’t enter the bundle if it ends up unused. The
Supabase POST uses keepalive: true so it survives page-unload races.
Related call sites
src/main.tsx—Sentry.initand thesentry_init_skippedfallback.services/playgroundPush.ts— push-pipeline failure capture with fullpush_failure_reasontag taxonomy.engine/telemetry/diag.ts—logDiagdual-channel helper.engine/diagnostics/zoom-watcher.ts— zoom-drift alerting viacaptureMessage('warning')+ breadcrumbs.engine/world/events.ts,engine/world/event-spawner.ts— boot-time snapshot messages for the event manager and spawner.metagame/services/supabase.ts— RPC-call breadcrumbs.