749dc467f1
## Summary Stamps the active OpenTelemetry `trace_id` and `span_id` onto every Sentry event captured from the webapp, so engineers can copy a `trace_id` from a Sentry issue and search for the corresponding trace in any OTel-aware backend. Also adds an `otel_sampled` tag to indicate whether the trace was head-sampled — a cheap signal for whether the link will resolve to span data or hit a missing trace. ## Why Sentry and OTel were OTel-disconnected: `apps/webapp/sentry.server.ts` initialised Sentry with `skipOpenTelemetrySetup: true`, and no error-capture site (`logger.server.ts`, the Remix-wrapped `handleError`, the root `ErrorBoundary`) attached OTel context to the event. With many spans/sec across services, getting from a Sentry issue to its trace was guesswork. ## Approach Single global Sentry event processor, registered immediately after `Sentry.init`. On each event it reads `trace.getActiveSpan()?.spanContext()` via `@opentelemetry/api`, then writes: - `event.contexts.trace.trace_id` and `event.contexts.trace.span_id` (Sentry's native trace context fields) - `event.tags.otel_sampled` = `"true"` | `"false"` (derived from `traceFlags`) If no active span (module-load errors, scheduled timers without a context, primary cluster process), the processor returns the event unmodified — Sentry's default propagation context fills in. Implementation is co-located in `apps/webapp/sentry.server.ts` (no separate helper module — `sentry.server.ts` is built standalone by esbuild and a separate import would have required a new bundling step). Helper functions are exported so the unit tests can reach them without re-running `Sentry.init`. ## Non-goals (deliberate) - No sample rate change. ~95% of Sentry events will carry a `trace_id` that returns no spans in the tracing backend (head-sampled out). The `otel_sampled` tag makes that obvious at a glance. Raising find-rate is a separate conversation with cost trade-offs. - No user/org tags or `Sentry.setUser` (would need auth-helper + per-request scope wiring across multiple worker entrypoints — separate ticket). - Webapp image only. No changes to supervisor or CLI workers. ## Test plan - [x] Unit tests in `apps/webapp/test/sentryTraceContext.server.test.ts` — 9 tests covering: helper returns \`undefined\` with no active span; returns \`traceId\`/\`spanId\`/\`sampled=true\` for a recording span; returns \`sampled=false\` for a non-recording span; processor leaves the event unchanged with no active span; processor stamps \`trace_id\`/\`span_id\` onto \`contexts.trace\`; preserves existing \`contexts.trace\` fields; tags \`otel_sampled\` correctly for both sampled and non-sampled cases; never throws if \`@opentelemetry/api\` access throws. - [x] \`pnpm run typecheck --filter webapp\` passes. - [x] Manually verified end-to-end against a sandboxed Sentry project: confirmed both sampled and non-sampled traces correctly populate \`contexts.trace.trace_id\` matching the OTel ids logged from the loader, and the \`otel_sampled\` tag appears with the expected value. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
36 lines
1.2 KiB
TypeScript
36 lines
1.2 KiB
TypeScript
import * as Sentry from "@sentry/remix";
|
|
import { addOtelTraceContextToEvent } from "./app/utils/sentryTraceContext.server";
|
|
|
|
if (process.env.SENTRY_DSN) {
|
|
console.log("🔭 Initializing Sentry");
|
|
|
|
Sentry.init({
|
|
dsn: process.env.SENTRY_DSN,
|
|
release: process.env.BUILD_GIT_SHA,
|
|
|
|
// Adds request headers and IP for users, for more info visit: and captures action formData attributes
|
|
// https://docs.sentry.io/platforms/javascript/guides/remix/configuration/options/#sendDefaultPii
|
|
sendDefaultPii: false,
|
|
|
|
skipOpenTelemetrySetup: true,
|
|
registerEsmLoaderHooks: false,
|
|
disableInstrumentationWarnings: true,
|
|
|
|
maxBreadcrumbs: 0,
|
|
shutdownTimeout: 10,
|
|
|
|
serverName: process.env.SERVICE_NAME,
|
|
environment: process.env.APP_ENV,
|
|
|
|
// ServiceValidationError is thrown deliberately for user-facing
|
|
// validation failures (quota, parent run state, invalid input). Anchored
|
|
// regex matches the exception type exactly; subclasses
|
|
// (QueueSizeLimitExceededError, MetadataTooLargeError) override `.name`
|
|
// and stay visible.
|
|
ignoreErrors: ["queryRoute() call aborted", /^ServiceValidationError(?::|$)/],
|
|
includeLocalVariables: false,
|
|
});
|
|
|
|
Sentry.addEventProcessor(addOtelTraceContextToEvent);
|
|
}
|