Error Tracking (Sentry)
Lumio's three Next.js apps — apps/web, apps/admin, apps/id — each ship with a @sentry/nextjs integration. The Rust backend is on sentry-rust via lo-telemetry; this page is about the frontend setup.
When NEXT_PUBLIC_SENTRY_DSN is unset, the integration is a no-op — apps boot normally, no events are sent. Local dev typically leaves the DSN blank; staging and production fill it via Cloudflare Pages env / docker-compose env.
Files per app
Each of apps/{web,admin,id}/ has the same five files:
| File | Loaded by | Purpose |
|---|---|---|
next.config.ts | build | Wraps withSentryConfig (source-map upload, build-time injection) |
sentry.client.config.ts | browser | Browser-side Sentry init + bindSentry for @lumio/logger |
sentry.server.config.ts | Node runtime | SSR / route-handler / Server-Action capture |
sentry.edge.config.ts | Edge runtime | Edge route handlers |
instrumentation.ts | server | Bootstrap — picks the right runtime config + binds logger to Sentry |
Configuration is duplicated across the three apps on purpose — the apps may evolve independently (different sample rates, different deny-lists). The shared bits are kept identical for now to keep behaviour consistent.
Environment variables
| Variable | Where | Purpose |
|---|---|---|
NEXT_PUBLIC_SENTRY_DSN | Browser + server | Public DSN. Required to enable client-side capture. |
SENTRY_DSN | Server only | Optional — falls back to NEXT_PUBLIC_SENTRY_DSN when unset. Useful when you want the SSR / Edge runtimes on a different project than the browser. |
SENTRY_ORG | Build (CI) | Sentry org slug — required for source-map upload. Leave blank locally. |
SENTRY_PROJECT | Build (CI) | Sentry project slug — same. |
SENTRY_AUTH_TOKEN | Build (CI) | Auth token for source-map upload. Generate one in Sentry → Settings → Auth Tokens. |
LUMIO_ENV / NEXT_PUBLIC_LUMIO_ENV | Both | Tags every event with the environment (development / staging / production). Falls back to NODE_ENV. |
Source-map upload only runs when SENTRY_AUTH_TOKEN + SENTRY_ORG + SENTRY_PROJECT are all set — typically only in CI for staging / production builds.
What gets captured
- Unhandled exceptions in browser code (React error boundaries, Promise rejections, sync throws).
- SSR / route-handler errors through the
onRequestErrorexport ininstrumentation.ts(Sentry's recommended hook). - Anything emitted via
logger.error(...)orlogger.warn(...)— the logger callsSentry.captureException/Sentry.captureMessagewhen bound. See Logging for the binding details.
What gets stripped
beforeSend in every runtime config drops the Authorization and Cookie headers from the outgoing event payload. sendDefaultPii: false keeps Sentry's automatic IP / user-agent collection off so cookie-issued JWTs and popout tokens never end up in Sentry.
If you want to enrich events with explicit user info (e.g. account ID), set it manually after login:
import * as Sentry from "@sentry/nextjs";
Sentry.setUser({ id: account.id, ip_address: undefined });
ip_address: undefined opts out of Sentry's IP capture even if sendDefaultPii ever flips on later.
Replays
replayIntegration({ maskAllText: true, blockAllMedia: true }) is enabled with replaysOnErrorSampleRate: 1.0 and replaysSessionSampleRate: 0. Translation: every session is silent until something throws, then Sentry uploads the last ~30 seconds as a replay with all text masked + media blocked.
If you find a streamer needs full-session replay for support purposes, raise replaysSessionSampleRate for that app only — but be aware of GDPR + cost implications. The default (errors-only) is the right baseline.
Performance traces
tracesSampleRate is 0.1 in production (10 % of transactions) and 1.0 everywhere else. Lower it further if your Sentry quota gets stressed; raise it on a feature branch when investigating a specific perf issue.
Suppressed noise
denyUrls blocks the standard browser-extension noise (chrome-extension://, moz-extension://, safari-web-extension://). Add more entries when you see recurring noise from a specific extension or third-party script.
Adding a new captured event manually
Most code should just throw / call logger.error. For business-event tracking (e.g. "user upgraded plan"), use Sentry's addBreadcrumb or captureMessage directly:
import * as Sentry from "@sentry/nextjs";
Sentry.captureMessage("user upgraded plan", {
level: "info",
extra: { account_id: accountId, plan_slug: plan.slug },
});
These don't surface as errors but let you search them in Sentry's "Issues" tab when triaging.
Debugging the integration locally
Set the DSN in .env.local:
NEXT_PUBLIC_SENTRY_DSN=https://<key>@o<org>.ingest.sentry.io/<project>
LUMIO_ENV=development
Then trigger an error from any client component:
"use client";
import { useEffect } from "react";
export default function DebugPage() {
useEffect(() => {
throw new Error("sentry smoketest");
}, []);
return null;
}
Reload the page, open Sentry → Issues, the event should land within a few seconds with environment: development. Filter that out before shipping or the dashboard fills with smoketest noise.