Skip to content

feat(node): Wire up SentryTracerProvider#21680

Draft
andreiborza wants to merge 18 commits into
ab/sentry-trace-provider-otelfrom
ab/sentry-trace-provider-node
Draft

feat(node): Wire up SentryTracerProvider#21680
andreiborza wants to merge 18 commits into
ab/sentry-trace-provider-otelfrom
ab/sentry-trace-provider-node

Conversation

@andreiborza

@andreiborza andreiborza commented Jun 22, 2026

Copy link
Copy Markdown
Member

Wires up the node SDK to use SentryTracerProvider when _experiments.useSentryTracerProvider is 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.

@andreiborza andreiborza requested a review from a team as a code owner June 22, 2026 09:35
@andreiborza andreiborza requested review from JPeer264 and mydea and removed request for a team June 22, 2026 09:35
@nicohrubec nicohrubec self-requested a review June 22, 2026 09:46
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 27.47 kB -0.01% -1 B 🔽
@sentry/browser - with treeshaking flags 25.91 kB - -
@sentry/browser (incl. Tracing) 46.05 kB +0.19% +83 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 47.8 kB +0.16% +73 B 🔺
@sentry/browser (incl. Tracing, Profiling) 50.82 kB +0.13% +64 B 🔺
@sentry/browser (incl. Tracing, Replay) 85.29 kB +0.1% +77 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 74.91 kB +0.14% +100 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 89.98 kB +0.08% +69 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 102.65 kB +0.08% +80 B 🔺
@sentry/browser (incl. Feedback) 44.66 kB - -
@sentry/browser (incl. sendFeedback) 32.26 kB -0.01% -1 B 🔽
@sentry/browser (incl. FeedbackAsync) 37.4 kB - -
@sentry/browser (incl. Metrics) 28.54 kB - -
@sentry/browser (incl. Logs) 28.78 kB - -
@sentry/browser (incl. Metrics & Logs) 29.47 kB - -
@sentry/react 29.27 kB - -
@sentry/react (incl. Tracing) 48.36 kB +0.18% +84 B 🔺
@sentry/vue 32.72 kB +0.3% +95 B 🔺
@sentry/vue (incl. Tracing) 47.93 kB +0.21% +96 B 🔺
@sentry/svelte 27.5 kB - -
CDN Bundle 29.89 kB +0.03% +6 B 🔺
CDN Bundle (incl. Tracing) 47.98 kB +0.2% +95 B 🔺
CDN Bundle (incl. Logs, Metrics) 31.44 kB +0.01% +2 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 49.32 kB +0.16% +78 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 70.78 kB - -
CDN Bundle (incl. Tracing, Replay) 85.49 kB +0.11% +89 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.76 kB +0.1% +83 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 91.29 kB +0.12% +105 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.53 kB +0.09% +80 B 🔺
CDN Bundle - uncompressed 88.94 kB - -
CDN Bundle (incl. Tracing) - uncompressed 145.31 kB +0.2% +287 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 93.65 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 149.28 kB +0.2% +287 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 218.62 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 264.33 kB +0.11% +287 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 268.29 kB +0.11% +287 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 278.03 kB +0.11% +287 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 281.98 kB +0.11% +287 B 🔺
@sentry/nextjs (client) 50.76 kB +0.19% +93 B 🔺
@sentry/sveltekit (client) 46.45 kB +0.19% +85 B 🔺
@sentry/core/server 77.67 kB +0.18% +133 B 🔺
@sentry/core/browser 64 kB +0.19% +120 B 🔺
@sentry/node-core 61.7 kB +0.32% +195 B 🔺
@sentry/node 125.14 kB +1.35% +1.66 kB 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 69.95 kB - -
@sentry/node/light 50.52 kB +0.23% +114 B 🔺
@sentry/node - without tracing 74.61 kB +1.36% +997 B 🔺
@sentry/aws-serverless 85.45 kB +1.25% +1.05 kB 🔺
@sentry/cloudflare (withSentry) - minified 180.67 kB +0.21% +365 B 🔺
@sentry/cloudflare (withSentry) 447.27 kB +0.24% +1.03 kB 🔺

View base workflow run

@andreiborza andreiborza changed the title feat(node): Wire up _experiments.useSentryTracerProvider feat(node): Wire up SentryTracerProvider Jun 22, 2026
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from e05567e to dc3cd9d Compare June 22, 2026 11:32
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from 7f2f88d to 172dd9f Compare June 22, 2026 11:49
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from dc3cd9d to f3c0c65 Compare June 22, 2026 11:49
Comment thread packages/node/src/sdk/initOtel.ts Outdated
coreDebug.warn(
'Could not register SentryTracerProvider because another OpenTelemetry tracer provider is already registered.',
);
return [undefined, undefined];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/node/src/sdk/initOtel.ts Outdated
): [BasicTracerProvider, AsyncLocalStorageLookup] {
): [OpenTelemetryTraceProvider | undefined, AsyncLocalStorageLookup | undefined] {
if (client.getOptions()._experiments?.useSentryTracerProvider) {
setOpenTelemetryContextAsyncContextStrategy();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

setOpenTelemetryContextAsyncContextStrategy();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 324c8e8

@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from 172dd9f to afb77ef Compare June 22, 2026 18:20
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from e200c8f to 502dca9 Compare June 22, 2026 18:20
Comment thread packages/node/src/sdk/initOtel.ts
Comment thread packages/node/src/sdk/initOtel.ts
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from afb77ef to fcdf2df Compare June 22, 2026 18:30
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from 502dca9 to 6ae8302 Compare June 22, 2026 18:30
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch 2 times, most recently from 2c670f3 to 308e560 Compare June 22, 2026 18:46
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from 6ae8302 to e2f91c0 Compare June 22, 2026 18:46
Comment thread packages/node/test/sdk/init.test.ts
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch 2 times, most recently from 4a3010c to d2384e8 Compare June 22, 2026 22:34
Comment thread packages/node/src/sdk/initOtel.ts
Comment thread packages/core/src/types/options.ts Outdated
* @default false
* @experimental
*/
useSentryTracerProvider?: boolean;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this live in core? is this not a node-specific option?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be a breaking change? I thought the opt-out would be rather for v11 and in v10 it's opt-in

@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch 2 times, most recently from cc82764 to 400be89 Compare June 23, 2026 15:50
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from 06f6f64 to 6759aa8 Compare June 23, 2026 19:13
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch 3 times, most recently from 796a13e to eba9cdb Compare June 25, 2026 09:15
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from ec74013 to 6826f8a Compare June 25, 2026 09:18
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from eba9cdb to b9a14f9 Compare June 25, 2026 09:18
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from 6826f8a to ccf5b72 Compare June 25, 2026 12:38
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from b9a14f9 to 81705d0 Compare June 25, 2026 12:39
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.
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from 81705d0 to 2f784bc Compare June 25, 2026 13:37
`.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).
Comment thread packages/node/src/sdk/initOtel.ts Outdated
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from 195ff46 to eee3e5c Compare June 25, 2026 23:46
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.
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-node branch from b84dc4d to 6a1e177 Compare June 26, 2026 20:34

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ 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);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6a1e177. Configure here.

},
1,
{ maxWait: 100 },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 6a1e177. Configure here.

type: mechanismType,
},
});
throw error;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 6a1e177. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants