Skip to content

Sentry error monitoring (SRC-1397) ​

Sentry tells us when something breaks in production, with a stack trace, before a parent has to report it. Until the secrets below exist, nothing is sent: every piece of the integration is a no-op without its DSN.

What is wired ​

WherePackageTurned on by
workers/api, workers/foundry, workers/tool-plane@sentry/cloudflare via withMonitoring()GitHub secret SENTRY_DSN (passed to the Worker as a var at deploy)
Every React app (admin, b21, coral, gull, learn, marlins, mentors, mm, partners, tutors, ui, web)@sentry/react via initSentry()GitHub secret VITE_SENTRY_DSN (baked in at build)
Source maps for the React apps@sentry/cli in .github/scripts/sentry-sourcemaps.shGitHub secret SENTRY_AUTH_TOKEN (+ SENTRY_ORG, SENTRY_PROJECT)

All shared code lives in packages/sentry:

  • scrub.ts: the privacy scrubber, used by both sides (test: scrub.test.ts).
  • browser.ts: initSentry(app), called once from each app's src/main.tsx, and reportReactError(error, componentStack), called from each app's ErrorBoundary. The SDK is a dynamic import behind a literal import.meta.env.VITE_SENTRY_DSN: without the DSN Vite drops it from the build entirely; with it, ~55 KB gzip load after the app has started, never in the entry chunk (test: browser.test.ts builds a one-line app with Vite and checks both).
  • worker.ts: withMonitoring(app, handler) wraps each Worker's default export; withMonitoringWorkflow / withMonitoringDurableObject wrap the Workflow classes (DictationForge, LessonForge, ItemForge) and the Durable Object (EventStage). Without SENTRY_DSN the original handler or class runs untouched (checked per invocation, per construction for classes).

The release is the git commit SHA (GITHUB_SHA): deploy.sh passes SENTRY_RELEASE to Workers, deploy-app.sh sets VITE_SENTRY_RELEASE for the React builds.

Settings: every dataCollection switch off (see below), traces sampled at 10%, no session replay.

What gets reported ​

The SDK alone only sees exceptions that ESCAPE a handler, and most of ours never do. So:

  • API: app.onError (every route exception, which Hono turns into a 500) calls captureError, and every logEvent("error", ...) line is sent as an event grouped by its event name (the cron and queue jobs catch per job and log). The reporter is wired in workers/api/src/index.ts.
  • tool-plane: the HTTP and per-RPC catch blocks report anything that is not an expected ToolError.
  • foundry: the cron drain and sweep catch blocks report; Workflow step failures are captured by the Workflow wrapper.
  • React apps: React 19 hands an error an ErrorBoundary caught to onCaughtError (console only), never to window.onerror, so each ErrorBoundary.componentDidCatch calls reportReactError.

Every error-level log line counts against the Sentry quota (free plan: 5,000 errors a month), so a noisy logEvent("error") is a bug to fix, not a line to keep.

Privacy: what is never sent ​

Nemo12 serves children and families. Two layers:

  1. Not collected. PRIVACY_DEFAULTS.dataCollection turns off every SDK collection switch: userInfo (which also makes the SDK send infer_ip: "never", so Sentry stores no client IP for events or sessions), cookies, request/response headers, bodies, URL query params, AI prompts and outputs (genAI), DB query data, queue payloads, stack frame variables. Since @sentry/* 11 this is the only switch the SDK reads: sendDefaultPii was removed and setting it does nothing, while the SDK's own defaults collect everything. worker.test.ts builds a real SDK client with our options and checks what it would send.
  2. Scrubbed. Before anything leaves the browser or a Worker, scrubEvent (errors), scrubSpan (spans, which the SDK streams by default, so beforeSendTransaction never runs) and an integration that cleans the trace context (baggage header, envelope headers) remove or mask:
    • user identity: only an opaque user.id survives (no email, name, username, IP);
    • cookies and auth headers (cookie, authorization, x-api-key, client IP headers);
    • request bodies and query strings, everywhere (URLs, breadcrumbs, messages, span attributes);
    • email addresses in free text, also percent-encoded (parent%40gmail.com);
    • path segments that look like tokens (16+ url-safe characters with digits and letters): the calendar feed /v1/calendar/feed/<token>.ics, /email/preferences/<token>, /e/u/<token>;
    • span attributes for query, fragment, every request/response header, user agent, client address, user.* except the id, and AI prompts/outputs;
    • any field whose name looks personal (email, name, phone, password, token, address, birth...).

If you change the scrubber, extend packages/sentry/scrub.test.ts in the same commit.

One-time setup (owner, by hand) ​

  1. Go to https://sentry.io/signup/ and create a free account (the Developer plan is free).

  2. Create an organization if asked. Note its slug (Settings > General, e.g. nemo12).

  3. Create a project: Projects > Create Project > platform React > name nemo12. One project for everything is fine: every event carries an app tag (learn, api, ...) to filter by.

  4. Copy the DSN: Project Settings > Client Keys (DSN). It looks like https://abc123@o123.ingest.sentry.io/456.

  5. Create an auth token for source maps: Settings > Developer Settings > Organization Tokens > Create New Token. Copy it once; Sentry will not show it again.

  6. In GitHub open the repo > Settings > Secrets and variables > Actions > New repository secret, and add five secrets:

    NameValue
    SENTRY_DSNthe DSN from step 4 (used by the Workers)
    VITE_SENTRY_DSNthe same DSN (used by the React apps)
    SENTRY_AUTH_TOKENthe token from step 5
    SENTRY_ORGthe organization slug from step 2
    SENTRY_PROJECTthe project slug from step 3 (e.g. nemo12)
  7. Required before the first deploy with the secrets: in Sentry, Project Settings > Security & Privacy, turn on Prevent Storing of IP Addresses and Data Scrubber (with default scrubbers). Our SDK settings already send no IP; this is the second line, for a kids product it is not optional.

  8. Nothing else. The next deploy of each app picks the secrets up. Do not also set SENTRY_DSN as a Cloudflare secret: the deploy passes it as a var and the two would clash.

nemo12.com sends a Content-Security-Policy (report-only today, apps/web/worker/security.ts) whose connect-src allows https://*.sentry.io: a CSP *. wildcard matches any subdomain depth, so it covers the regional ingest hosts (o123.ingest.us.sentry.io) that *.ingest.sentry.io would miss.

Turning it off ​

Delete SENTRY_DSN and VITE_SENTRY_DSN from GitHub secrets and redeploy. Without them the code does nothing.