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).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 195ff46. Configure here.
The provider assembles a transaction synchronously from the live span tree the instant the root span ends. A child whose async instrumentation closes it one tick later (e.g. a diagnostics-channel `asyncEnd` callback that runs right after the user callback which ended the root) is not finished yet, so `getSpanDescendants` drops it. This dropped the trailing `SELECT NOW()` span in the orchestrion mysql callback scenarios. Defer the segment span's transaction capture by a microtask so those synchronously-following span ends land first. Tracked per client (a `WeakSet` the provider opts its client into during setup) rather than a process-wide flag, so the setting cannot leak across `Sentry.init()` calls and every other setup keeps its synchronous capture. Never deferred in the browser, where there is no such pattern and a deferred capture could be lost on page unload. The `spanEnd`/`afterSpanEnd` hooks still fire synchronously, so per-span data (status, OTel attributes) is finalized before the deferred snapshot.
195ff46 to
eee3e5c
Compare
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.

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.