From eaf04dfd38310b1f7b6e0cf314b181a2fe51fe35 Mon Sep 17 00:00:00 2001 From: Aleksandar Grbic Date: Fri, 29 May 2026 18:46:25 +0200 Subject: [PATCH] fix(observability): OTel is the only tracer; Sentry stays error-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default Sentry tracesSampleRate to 0 on both API and UI. @sentry/bun v10+ is built on @sentry/opentelemetry, so any non-zero Sentry sample rate registers a second tracer alongside the OTel SDK in config/otel/otel.ts and double-instruments HTTP / fetch / ioredis. The stack already ships Tempo as the trace backend, so the cleaner stance is one tracer, not two. UI keeps browserTracingIntegration loaded — Sentry's browser SDK still writes the W3C traceparent header on /api/* fetches with rate 0, which is what the API's OTel SDK reads to continue the trace and ship spans to Tempo. Error events on either side keep their trace_id from the shared OTel context, so the GlitchTip → Tempo pivot still works. Operator can flip SENTRY_TRACES_SAMPLE_RATE back to non-zero if they want transactions in Sentry as well — comment in sentry.ts spells out the trade-off. Docs updated: tracing.mdx Browser→API FAQ explains the choice; error-tracking.mdx points to tracing.mdx for the rationale. --- apps/api/.env.example | 6 +++++- apps/api/src/config/env/schema.ts | 10 +++++++++- apps/api/src/config/env/validate.ts | 2 +- apps/api/src/config/sentry/sentry.ts | 10 +++++++++- .../content/docs/topics/error-tracking.mdx | 2 ++ apps/docs/src/content/docs/topics/tracing.mdx | 7 +++++++ apps/ui/src/app/main.tsx | 19 +++++++++++++------ 7 files changed, 46 insertions(+), 10 deletions(-) diff --git a/apps/api/.env.example b/apps/api/.env.example index 400b5156..3cd6b8af 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -62,7 +62,11 @@ E2E_TEST_ENDPOINTS_ENABLED=false # infra/compose/docs/glitchtip.md) or sentry.io — same # wire protocol. Empty DSN means Sentry is not initialized. SENTRY_DSN= -SENTRY_TRACES_SAMPLE_RATE=0.1 +# Default 0: OpenTelemetry → Tempo is the trace backend; Sentry / GlitchTip +# stays error-capture-only. Set non-zero only if you want transactions in +# Sentry as well — running both tracers concurrently double-instruments +# the request path (Sentry's Bun SDK is built on @sentry/opentelemetry). +SENTRY_TRACES_SAMPLE_RATE=0 # Email — pick one provider; only that provider's keys are required. # Default: Cloudflare Email Service (https://developers.cloudflare.com/email-service/). diff --git a/apps/api/src/config/env/schema.ts b/apps/api/src/config/env/schema.ts index c06412eb..91a2072d 100644 --- a/apps/api/src/config/env/schema.ts +++ b/apps/api/src/config/env/schema.ts @@ -73,10 +73,18 @@ export const envSchema = t.Object({ * sentry.io for hosted. Empty DSN = Sentry is not initialized. */ SENTRY_DSN: t.String({ default: "" }), + /* + * Default 0: OTel is the single source of trace data (shipped via OTLP to + * Tempo). Sentry is error-capture-only — events still carry `trace_id` from + * the shared OTel context, so GlitchTip → Tempo click-through works. Flip + * non-zero only if you want Sentry / GlitchTip to record transactions in + * addition to errors; running both tracers concurrently double-instruments + * HTTP / fetch / ioredis paths through `@sentry/opentelemetry`. + */ SENTRY_TRACES_SAMPLE_RATE: t.Number({ minimum: 0, maximum: 1, - default: 0.1, + default: 0, }), /* diff --git a/apps/api/src/config/env/validate.ts b/apps/api/src/config/env/validate.ts index 3ea9de38..0af035f3 100644 --- a/apps/api/src/config/env/validate.ts +++ b/apps/api/src/config/env/validate.ts @@ -121,7 +121,7 @@ const readRateLimit = (source: EnvSource) => ({ const readSentry = (source: EnvSource) => ({ SENTRY_DSN: source.SENTRY_DSN ?? "", - SENTRY_TRACES_SAMPLE_RATE: toFloat(source.SENTRY_TRACES_SAMPLE_RATE, 0.1), + SENTRY_TRACES_SAMPLE_RATE: toFloat(source.SENTRY_TRACES_SAMPLE_RATE, 0), }); const readOpenTelemetry = (source: EnvSource) => ({ diff --git a/apps/api/src/config/sentry/sentry.ts b/apps/api/src/config/sentry/sentry.ts index c0508d94..dcf7d3e3 100644 --- a/apps/api/src/config/sentry/sentry.ts +++ b/apps/api/src/config/sentry/sentry.ts @@ -19,8 +19,16 @@ export const initializeSentry = (): void => { Sentry.init({ dsn: env.SENTRY_DSN, environment: env.NODE_ENV, + /* + * `@sentry/bun` v10+ is built on `@sentry/opentelemetry`, so any + * sample rate > 0 registers a second tracer alongside the + * OpenTelemetry SDK in `config/otel/otel.ts` — both then instrument + * HTTP / fetch / ioredis. Default is 0 (env-tunable): OTel ships + * spans to Tempo, Sentry stays error-capture-only. Error events + * still pick up `trace_id` from the shared OTel context, so + * GlitchTip → Tempo click-through is preserved. + */ tracesSampleRate: env.SENTRY_TRACES_SAMPLE_RATE, - // Bun-side defaults; tune per workload. sampleRate: 1.0, release: env.APP_NAME, }); diff --git a/apps/docs/src/content/docs/topics/error-tracking.mdx b/apps/docs/src/content/docs/topics/error-tracking.mdx index a59dfddc..7e4b2a40 100644 --- a/apps/docs/src/content/docs/topics/error-tracking.mdx +++ b/apps/docs/src/content/docs/topics/error-tracking.mdx @@ -132,6 +132,8 @@ API side: Sentry initialises once at boot. If `SENTRY_DSN` is empty, init is a n UI side: Sentry initialises 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 → Tempo pivot works. See [Distributed tracing](/topics/tracing/) for the why. + ## Self-hosting with GlitchTip GlitchTip is Apache-licensed and Sentry-API-compatible. It runs as part of the default compose stack: diff --git a/apps/docs/src/content/docs/topics/tracing.mdx b/apps/docs/src/content/docs/topics/tracing.mdx index 66ede8eb..e3995332 100644 --- a/apps/docs/src/content/docs/topics/tracing.mdx +++ b/apps/docs/src/content/docs/topics/tracing.mdx @@ -101,6 +101,13 @@ Tempo. headers to every `/api/*` fetch. The API's OTel SDK reads them and continues the trace, so a single trace ID spans the browser action and everything it triggered server-side. + + Sentry's browser `tracesSampleRate` is `0` and the API's + `SENTRY_TRACES_SAMPLE_RATE` defaults to `0` — Sentry is error-capture-only; + OTel is the single tracer that ships spans to Tempo. `@sentry/bun` v10+ is + built on `@sentry/opentelemetry`, so any non-zero Sentry sample rate + registers a second tracer on top of the OTel SDK and double-instruments + HTTP / fetch / DB. Flip it back if you want transactions in Sentry as well. diff --git a/apps/ui/src/app/main.tsx b/apps/ui/src/app/main.tsx index 56c8d68b..323a77ae 100644 --- a/apps/ui/src/app/main.tsx +++ b/apps/ui/src/app/main.tsx @@ -15,16 +15,23 @@ if (env.VITE_SENTRY_DSN !== "") { Sentry.init({ dsn: env.VITE_SENTRY_DSN, environment: env.MODE, - tracesSampleRate: env.PROD ? 0.1 : 1.0, - replaysSessionSampleRate: env.PROD ? 0.0 : 0.0, + /* + * 0 keeps Sentry error-capture-only on the browser side. `browserTracingIntegration` + * stays loaded because it's what writes the W3C `traceparent` header on + * outbound `/api/*` fetches — the API's OpenTelemetry SDK reads that + * header to continue the trace server-side and ship spans to Tempo. With + * rate 0 the browser doesn't send transactions to GlitchTip but still + * generates trace ids, so a browser-raised error event still carries + * `trace_id` for the GlitchTip → Tempo pivot. + */ + tracesSampleRate: 0, + replaysSessionSampleRate: 0, replaysOnErrorSampleRate: 1.0, integrations: [Sentry.browserTracingIntegration()], /* * Propagate `sentry-trace` + `traceparent` headers only on same-origin - * API calls. The API's Pino logger picks up the trace id via its - * Sentry mixin, so a UI-originated request creates a single trace - * spanning browser → API → Postgres. Defaulting to "all origins" - * would leak the trace id to CDNs and third-party services. + * API calls. Defaulting to "all origins" would leak the trace id to + * CDNs and third-party services. */ tracePropagationTargets: ["/api/", /^https?:\/\/[^/]+\/api\//] });