Error tracking
Both apps/api and apps/ui ship Sentry SDKs configured for minimal overhead. The same SDK connects to two backends: self-hosted GlitchTip (on by default in the compose stack, Sentry-API-compatible, zero per-event cost) or hosted Sentry (if you’d rather pay for managed). The SDKs don’t know which backend they talk to. Choosing between them is a one-env-var change.
Correlation: GlitchTip, Loki, and requestId
Section titled “Correlation: GlitchTip, Loki, and requestId”Errors are useful only if you can quickly reach the context around them. The stack wires a single chain of IDs across browser, API, log store, and error tracker so any one surface takes you to the other three.
flowchart LR user["Browser action"] -->|sentry-trace header| api["API request handler"] api -->|Pino mixin| logs["Loki log lines<br/>(trace_id, requestId, userId)"] api -->|on error| gt["GlitchTip event<br/>(tagged: trace_id, user.id)"] logs -.->|click trace_id link| gt gt -.->|click trace_id tag| logs
What flows automatically:
trace_idandspan_id: the UI’s Sentry SDK addssentry-traceandtraceparentheaders to every/api/*fetch viabrowserTracingIntegration. The API’s Sentry init reads them. The Pino logger’s mixin injects them on every log record. Promtail promotes them to Loki structured metadata.userId: the API’s auth plugin callsSentry.setUser({id, email})on every authenticated request. The Pino mixin readsuserIdfrom the Sentry scope and emits it on each log line. The UI’sSentryUserSyncprovider mirrors the same call after login, MFA verify, account switch, or page reload (anywhereuseMeresolves).requestId: a UUID assigned by the API’s request-logger middleware, returned as thex-request-idheader and present on every log line for that request.
Where the click-through lives:
- Grafana to GlitchTip: the “BoringStack API logs” dashboard defines clickable data links on
trace_idanduserIdin the live log panel. Expand any log line, click the field to open GlitchTip filtered to that trace or user in a new tab. Dashboard variables$glitchtip_urland$glitchtip_orgconfigure the target (defaults fit the bundled GlitchTip; edit once per environment). - Grafana to Grafana:
requestIdlink opens Explore with a Loki query pre-filtered to that request’s lines. - GlitchTip to Grafana: configure once in GlitchTip’s project settings (UI, not code) with a
tags.trace_idexternal link template pointing at Grafana Explore with{compose_service="api-dev"} | json | trace_id="<value>". Each event detail then shows a “View logs” link.
How it’s wired
Section titled “How it’s wired”flowchart LR
api["apps/api<br/>@sentry/bun"] -- "events" --> backend{DSN points where?}
ui["apps/ui<br/>@sentry/react"] -- "events + replays-on-error" --> backend
backend -- "https://...sentry.io/..." --> sentry["hosted Sentry"]
backend -- "https://...glitchtip.localhost/..." --> glitchtip["self-hosted GlitchTip"]
Both apps/api (@sentry/bun) and apps/ui (@sentry/react) emit events to the same Sentry-compatible wire protocol. The DSN env var picks the destination: a sentry.io hostname for hosted Sentry, or a glitchtip.localhost hostname for the self-hosted overlay. The SDKs don’t know which backend they’re talking to.
API side: Sentry initializes once at boot. If SENTRY_DSN is empty, init is a no-op. The shared captureError helper wires into unhandled-rejection and uncaught-exception handlers, so anything that escapes the request loop reaches the backend.
UI side: Sentry initializes once at app mount when VITE_SENTRY_DSN is set. Replays-on-error capture the error context. Full-session replays are off to avoid capturing video of every session.
Sentry transactions are off by default (SENTRY_TRACES_SAMPLE_RATE=0 on the API, tracesSampleRate: 0 on the UI). OpenTelemetry is the single tracer that ships spans to Tempo. Error events still pick up trace_id from the shared context so the GlitchTip to Tempo pivot works. See Distributed tracing for why.
Self-hosting with GlitchTip
Section titled “Self-hosting with GlitchTip”GlitchTip is Apache-licensed and Sentry-API-compatible. It runs as part of the default compose stack:
./dev.sh up -d # GlitchTip is on by defaultWITH_GLITCHTIP=0 ./dev.sh up -d # opt out if you'd rather not run itFirst boot bootstraps a superuser (admin@localhost with a random password dev.sh generates and persists to compose/.env as GLITCHTIP_SUPERUSER_PASSWORD — there is deliberately no published default), a default org, and two projects (API and Frontend). The dev.sh up -d command then runs scripts/glitchtip-fetch-dsn.sh in the background. It pulls the DSNs from GlitchTip’s Django ORM, writes them into compose/.env as SENTRY_DSN and VITE_SENTRY_DSN, and restarts api-dev and ui-dev. Visit http://glitchtip.localhost to browse events. The manual DSN paste step is gone.
The overlay reuses the base stack’s Postgres (in a separate glitchtip database) and Valkey (DB 1). Adding GlitchTip costs two extra containers, not a separate database server.
For production hardening (Basic Auth, HTTPS, real SMTP), see the GlitchTip docs under infra/compose.
Switching backends
Section titled “Switching backends”Change only the DSN. No SDK changes. The infrastructure is the variable, not the code.
Sentry (hosted)
Section titled “Sentry (hosted)”- API:
SENTRY_DSN=https://...@sentry.io/... - UI:
VITE_SENTRY_DSN=https://...@sentry.io/...
GlitchTip (self-hosted)
Section titled “GlitchTip (self-hosted)”- API:
SENTRY_DSN=https://...@glitchtip.example.com/... - UI:
VITE_SENTRY_DSN=https://...@glitchtip.example.com/...
Source
Section titled “Source”- API init:
src/config/sentry.ts+src/config/error-handlers.ts. - UI init:
src/app/main.tsx. - GlitchTip overlay:
compose/docker-compose.glitchtip.yml.
Related
Section titled “Related”- Observability; metrics and logs that sit alongside error events.
- Profiles & overlays; how GlitchTip composes onto the stack and how to opt out.
- Security pipeline; the broader telemetry surface this fits into.
- Notifications; how user-facing errors are surfaced back into the app.