Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
Expand Down
10 changes: 9 additions & 1 deletion apps/api/src/config/env/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),

/*
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/config/env/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down
10 changes: 9 additions & 1 deletion apps/api/src/config/sentry/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/src/content/docs/topics/error-tracking.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions apps/docs/src/content/docs/topics/tracing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</FaqItem>
</FaqGroup>

Expand Down
19 changes: 13 additions & 6 deletions apps/ui/src/app/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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\//]
});
Expand Down
Loading