feat(node): Wire up SentryTracerProvider#21680
Conversation
size-limit report 📦
|
e05567e to
dc3cd9d
Compare
7f2f88d to
172dd9f
Compare
dc3cd9d to
f3c0c65
Compare
| coreDebug.warn( | ||
| 'Could not register SentryTracerProvider because another OpenTelemetry tracer provider is already registered.', | ||
| ); | ||
| return [undefined, undefined]; |
There was a problem hiding this comment.
m: is this in a good state if the setup fails here? we won't install the sentry propagator and context manager in here and then we also bail early in the main setupOtel method but we already set up the otel async context strategy. might be fine just checking
There was a problem hiding this comment.
There is no good state if the tracer provider registration fails users won't get proper traces, but there's no way to recover from this because you can only ever register one global tracer provider and if one is already registered you can't do anything.
That's why we log here, I think it's fine as is.
| ): [BasicTracerProvider, AsyncLocalStorageLookup] { | ||
| ): [OpenTelemetryTraceProvider | undefined, AsyncLocalStorageLookup | undefined] { | ||
| if (client.getOptions()._experiments?.useSentryTracerProvider) { | ||
| setOpenTelemetryContextAsyncContextStrategy(); |
There was a problem hiding this comment.
m: do we even need to call this here? setupOtel is called from initOpenTelemetry, which is called in the node init after we make the node-core init which also makes this same call:
172dd9f to
afb77ef
Compare
e200c8f to
502dca9
Compare
afb77ef to
fcdf2df
Compare
502dca9 to
6ae8302
Compare
2c670f3 to
308e560
Compare
6ae8302 to
e2f91c0
Compare
4a3010c to
d2384e8
Compare
| * @default false | ||
| * @experimental | ||
| */ | ||
| useSentryTracerProvider?: boolean; |
There was a problem hiding this comment.
should this live in core? is this not a node-specific option?
There was a problem hiding this comment.
also, why experimental? this can just be a regular option, and as discussed I'd actually make it opt-out (or more specifically, make the default dynamic based on if any options are set that require the more fully features tracer, e.g. spanProcessors)
There was a problem hiding this comment.
Wouldn't this be a breaking change? I thought the opt-out would be rather for v11 and in v10 it's opt-in
cc82764 to
400be89
Compare
06f6f64 to
6759aa8
Compare
796a13e to
eba9cdb
Compare
ec74013 to
6826f8a
Compare
eba9cdb to
b9a14f9
Compare
6826f8a to
ccf5b72
Compare
b9a14f9 to
81705d0
Compare
Add the SentryTracerProvider under an experimental `useSentryTracerProvider` flag and update the node setup path to register the new TracerProvider and its async context strategy instead of the full OTel SDK tracer provider when enabled.
Outside of span streaming, an outgoing fetch (`http.client`) span with no local parent is no longer recorded as a standalone transaction — the downstream sampling decision is left to the server. This is enforced via `onlyIfParent`, which still creates a non-recording span so trace propagation headers are injected. This rule already lives in `SentrySampler`, but that only runs when an OpenTelemetry SDK tracer provider is set up. Enforcing it in the instrumentation makes it hold for the `SentryTracerProvider` and for SDKs that don't use an OpenTelemetry tracer provider at all. The sampler rule is kept for OpenTelemetry SDK / custom OpenTelemetry setups.
81705d0 to
2f784bc
Compare
`.withResponse()` awaits the instrumented promise (which ends the gen_ai span) before returning, but `.asResponse()` routed straight to the raw `APIPromise.asResponse()` and never waited for it. The span then ended on the instrumentation's own parse schedule, which can be one microtask after the enclosing transaction has already been assembled. The SentryTracerProvider assembles transactions synchronously on root-span end (no debounced span flush), so the unfinished gen_ai span was dropped from the transaction, orphaning its child `http.client` span. Mirror the `.withResponse()` handling: await the instrumented promise before returning the raw `Response`, so the span ends before the caller continues, deterministically on both the provider and SDK paths. Applies to both the OpenAI and Anthropic instrumentations (shared util).
195ff46 to
eee3e5c
Compare
The transaction is assembled synchronously from the live span tree when the root span ends, dropping child spans whose instrumentation closes them after it - in the same tick (diagnostics-channel `asyncEnd`) or on a later tick (e.g. prisma engine spans). A per-client debounced timer (the one the OpenTelemetry span exporter uses) delays the snapshot so those children land first, and drains on the client `flush` hook so `Sentry.flush()` / `close()` stays safe. Enabled on the NodeClient rather than the SentryTracerProvider so it applies with or without a tracer provider; the browser keeps its synchronous capture.
Under the SentryTracerProvider, streamed spans carry `sentry.origin` as a first-class attribute including the default `manual` value, whereas the OpenTelemetry SDK path omits the `manual` default. The `mysql` (v1) db spans and the `pg.connect` span set no explicit origin, so they surface as `manual` here. Assert it for now. When those instrumentations are reworked to set an explicit `auto.db.otel.*` origin (e.g. #21568 for mysql), these expectations will be updated to the real origin then.
These assert prisma's engine spans (replayed asynchronously by `@prisma/instrumentation`), which the SentryTracerProvider drops because it assembles transactions synchronously on root-span end with no SpanExporter buffer to wait for late children. They pass on the OpenTelemetry SDK (`BasicTracerProvider`) path. Skip them here until the general "complete span-tree capture without a SpanExporter" follow-up lands; v7 is left enabled as it currently captures the engine spans in time.
b84dc4d to
6a1e177
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6a1e177. Configure here.
| // client reference can be reassigned. Only the snapshot is deferred, so late children land. | ||
| client.captureEvent(transactionEvent, undefined, scope); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Deferred capture lost on exit
Medium Severity
Root transaction capture is deferred through a debounced timer instead of running when the segment span ends. If the process terminates with process.exit() (or otherwise exits) before that timer runs and flush/close never drains pending captures, the transaction event is never created or sent.
Reviewed by Cursor Bugbot for commit 6a1e177. Configure here.
| }, | ||
| 1, | ||
| { maxWait: 100 }, | ||
| ); |
There was a problem hiding this comment.
Defer timer blocks process exit
Low Severity
The debounced timer used to defer segment-span transaction capture is created with plain setTimeout and is not unref'd, so it can keep short-lived Node scripts and CLI processes from exiting until the debounce fires.
Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit 6a1e177. Configure here.
| type: mechanismType, | ||
| }, | ||
| }); | ||
| throw error; |
There was a problem hiding this comment.
asResponse double exception capture
Low Severity
The new .asResponse() wrapper calls captureException and then rethrows, so failures from the raw response promise can be reported twice when the caller does not handle them and the SDK global handlers also capture the rejection.
Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit 6a1e177. Configure here.


Wires up the node SDK to use
SentryTracerProviderwhen_experiments.useSentryTracerProvideris enabled (default opt-in).When enabled, it uses our SentryTracerProvider and async context strategy instead of the Otel SDK's.
For simplicity, a flag is used, but we could change this to be an explicit call (+ a flag to opt out of the current tracer provider) to save some bundle size.