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 (from engine/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.MODEproduction on Vercel, development for 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 real Error (or thrown value). Sentry de-dupes by stack-trace fingerprint, so repeated network failures from the same throw site collapse into one issue. Used in playgroundPush.ts for fetch failures (network_error) and JSON-parse failures (bad_response_shape).
  • Sentry.captureMessage(text, level) — pass a string when there is no underlying Error object, 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, server ok:false responses, and the logDiag ad-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.

  • src/main.tsxSentry.init and the sentry_init_skipped fallback.
  • services/playgroundPush.ts — push-pipeline failure capture with full push_failure_reason tag taxonomy.
  • engine/telemetry/diag.tslogDiag dual-channel helper.
  • engine/diagnostics/zoom-watcher.ts — zoom-drift alerting via captureMessage('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.