Skip to content

feat(opentelemetry): Add SentryTracerProvider#21666

Open
andreiborza wants to merge 10 commits into
developfrom
ab/sentry-trace-provider-otel
Open

feat(opentelemetry): Add SentryTracerProvider#21666
andreiborza wants to merge 10 commits into
developfrom
ab/sentry-trace-provider-otel

Conversation

@andreiborza

@andreiborza andreiborza commented Jun 19, 2026

Copy link
Copy Markdown
Member

Add a minimal OpenTelemetry TracerProvider that creates native Sentry spans instead of bridging through the full OTel SDK.

Hooking up the tracer provider and e2e tests are in #21680

@andreiborza andreiborza requested a review from a team as a code owner June 19, 2026 17:58
@andreiborza andreiborza requested review from JPeer264 and mydea and removed request for a team June 19, 2026 17:58
Comment thread packages/opentelemetry/src/tracer.ts
Comment thread packages/opentelemetry/src/tracer.ts
Comment thread packages/opentelemetry/src/applyOtelSpanData.ts
Comment thread packages/opentelemetry/src/applyOtelSpanData.ts
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 27.47 kB - -
@sentry/browser - with treeshaking flags 25.91 kB - -
@sentry/browser (incl. Tracing) 45.96 kB -0.01% -1 B 🔽
@sentry/browser (incl. Tracing + Span Streaming) 47.72 kB +0.01% +1 B 🔺
@sentry/browser (incl. Tracing, Profiling) 50.76 kB - -
@sentry/browser (incl. Tracing, Replay) 85.22 kB +0.01% +2 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 74.81 kB +0.01% +3 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 89.92 kB +0.01% +3 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 102.57 kB +0.01% +5 B 🔺
@sentry/browser (incl. Feedback) 44.66 kB - -
@sentry/browser (incl. sendFeedback) 32.26 kB - -
@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.28 kB - -
@sentry/vue 32.63 kB +0.01% +3 B 🔺
@sentry/vue (incl. Tracing) 47.84 kB - -
@sentry/svelte 27.5 kB - -
CDN Bundle 29.89 kB - -
CDN Bundle (incl. Tracing) 47.9 kB +0.02% +9 B 🔺
CDN Bundle (incl. Logs, Metrics) 31.44 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.25 kB +0.03% +13 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 70.78 kB - -
CDN Bundle (incl. Tracing, Replay) 85.41 kB +0.02% +10 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.69 kB +0.02% +9 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 91.2 kB +0.02% +12 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.46 kB +0.02% +10 B 🔺
CDN Bundle - uncompressed 88.94 kB - -
CDN Bundle (incl. Tracing) - uncompressed 145.12 kB +0.07% +91 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 93.65 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 149.09 kB +0.07% +91 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 218.62 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 264.14 kB +0.04% +91 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 268.1 kB +0.04% +91 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 277.84 kB +0.04% +91 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 281.78 kB +0.04% +91 B 🔺
@sentry/nextjs (client) 50.67 kB +0.01% +2 B 🔺
@sentry/sveltekit (client) 46.37 kB +0.02% +6 B 🔺
@sentry/core/server 76.53 kB +0.04% +27 B 🔺
@sentry/core/browser 63.65 kB +0.03% +15 B 🔺
@sentry/node-core 61.57 kB +0.11% +66 B 🔺
@sentry/node 122.72 kB +0.06% +66 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 69.95 kB - -
@sentry/node/light 50.42 kB +0.04% +20 B 🔺
@sentry/node - without tracing 73.61 kB +0.08% +58 B 🔺
@sentry/aws-serverless 84.78 kB +0.06% +49 B 🔺
@sentry/cloudflare (withSentry) - minified 176.13 kB +0.08% +124 B 🔺
@sentry/cloudflare (withSentry) 438.01 kB +0.06% +241 B 🔺

View base workflow run

@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch 2 times, most recently from 20bf510 to 7f2f88d Compare June 19, 2026 23:04
@nicohrubec nicohrubec self-requested a review June 21, 2026 17:12
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from 7f2f88d to 172dd9f Compare June 22, 2026 11:49
const { description } = spanHasName(rootSpan) ? parseSpanDescription(rootSpan) : { description: undefined };
if (source !== 'url' && description) {
dsc.transaction = description;
if (jsonSpan.description) {

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.

can you explain this, why do we guard this based on jsonSpan.description?

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.

The previous guard spanHasName looks at Otel ReadableSpan's name field which we don't have in the new tracer provider path.

The guard is so that we don't end up with <unknown> from parseSpanDescription in the case that no description was set.

inferred.source !== undefined &&
inferred.source !== 'custom' &&
(options.finalizeStatus || inferred.source !== 'url') &&
(spanJSON.parent_span_id === undefined || kind === SpanKind.SERVER);

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.

the comment is pretty good but does not really explain this part of the condition?

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.

Updated the comment to explain it in 40d8abc

inferred.description !== spanJSON.description &&
(attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom' || (mayInferSource && !hasCustomSpanName))
) {
addNonEnumerableProperty(span as Span & { _name?: string }, '_name', inferred.description);

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.

uhh, this looks dangerous? What does that do? is the idea to override the protected _name field on the sentry span? If so, this may break I think when stuff is minimized, as I doubt this would catch this being the same field?

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.

This is a left-over from when we wanted to avoid calling updateName as it sets the source to 'custom'. But we now landed exempts from tracer started spans from that so we no longer need this workaround. I updated to a normal invocation of updateName in 2c670f3

Comment on lines +73 to +76
const capturedIsolationScope = getCapturedScopesOnSpan(span as unknown as Span).isolationScope;
if (capturedIsolationScope) {
ctxWithSpan = ctxWithSpan.setValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, capturedIsolationScope);
}

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.

is this really necessary? I thought this should just be inherited anyhow from child contexts...?

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.

No not necessary. There's on possibility where the a passed context to startActiveSpan could come with a differently bound scope but I can't come up with any examples of where that might happen.

Removed in 4ef5072

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.

I had to reinstate this as nextjs e2e middleware tests broke on #21680.

Previously, in the Otel SDK path, we handled the isolation scope capturing via SentrySpanProcessor.onStart, but since we no longer have a span processor, we need to replicate that behavior here. So we pin the captured scope onto the context, otherwise work done inside the span (e.g. tags and breadcrumbs) would land on a different scope.

// @ts-expect-error We just assume that this is non-abstract, if you pass in an abstract class this would make it non-abstract
class OpenTelemetryClient extends ClientClass implements OpenTelemetryClientInterface {
public traceProvider: BasicTracerProvider | undefined;
public traceProvider: OpenTelemetryTraceProvider | 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: do we need to update this in node-core and vercel-edge as well?

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.

The plan was to only setup this tracer provider in the node sdk, others will then switch to either no tracer provider at all or the sentry tracer provider in v11.

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.

right so for node-core it's a noop since it won't exist anymore. are we tracking somewhere that we need to adjust the other SDKs?

const attributes = jsonSpan.data;
const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];

const { description } = spanHasName(rootSpan) ? parseSpanDescription(rootSpan) : { description: 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.

q: do we need to audit these at some point? as in check if we need these anymore

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.

Yeah, with v11 we should be able to drop these.

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.

do we have issues for these cleanups so we don't forget?

Comment thread packages/opentelemetry/src/custom/client.ts Outdated
* A minimal OpenTelemetry TracerProvider which creates native Sentry spans.
*/
export class SentryTracerProvider implements TracerProvider {
public readonly resource?: { attributes: SpanAttributes };

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.

l: is this used anywhere? if not maybe we should remove it

@andreiborza andreiborza Jun 22, 2026

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.

Technically no, we don't apply resources on spans like in the otel path so this could go, but I'd prefer to remove this in conjunction with the vendored sentry resource in v11.

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.

do we have issues for these cleanups so we don't forget?

Comment thread packages/opentelemetry/README.md Outdated
Comment thread packages/opentelemetry/src/types.ts Outdated
Comment thread packages/opentelemetry/src/tracerProvider.ts
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch 2 times, most recently from afb77ef to fcdf2df Compare June 22, 2026 18:30

@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 1 potential issue.

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 fcdf2df. Configure here.

Comment thread packages/opentelemetry/test/tracerProvider.test.ts
@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
Comment thread packages/opentelemetry/src/tracer.ts
@andreiborza andreiborza requested review from mydea and nicohrubec June 23, 2026 07:29

@nicohrubec nicohrubec left a comment

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.

lgtm

@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from 09f43b1 to cc82764 Compare June 23, 2026 12:24
Comment thread packages/opentelemetry/src/applyOtelSpanData.ts
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch 2 times, most recently from 400be89 to 0cb493b Compare June 24, 2026 08:44

@JPeer264 JPeer264 left a comment

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.

LGTM. I also tried this on Cloudflare, and it works like a charm (together with Vercel AI SDK: https://sentry-sdks.sentry.io/explore/traces/trace/5fc71ee683a2467b8c1dec34691afbc3)

Comment thread packages/opentelemetry/src/tracer.ts
Comment thread packages/opentelemetry/src/tracer.ts
Comment thread packages/opentelemetry/src/applyOtelSpanData.ts
Add a minimal OpenTelemetry `TracerProvider` that creates native Sentry spans
instead of bridging through the full OTel SDK.
A root span with no parent and no remote (incoming) parent previously continued
the scope's propagation context, so manually-started parallel root spans in the
same scope all collapsed into a single shared trace. The OpenTelemetry SDK
instead mints a fresh trace id per such root span.

Wrap the no-parent branch of `_startSentrySpan` in `startNewTrace` (matching the
existing `options.root` branch) so each parentless root span gets its own trace.
Incoming traces are unaffected, since `continueTrace` sets a remote parent and
takes the `_startRootSpanWithRemoteParent` branch instead.
…race

When `SentryTracer` continues a remote trace whose incoming headers carried no
baggage, `_startRootSpanWithRemoteParent` froze a derived-but-incomplete dynamic
sampling context (missing `sample_rand` and `transaction`) onto the span, which
then propagated downstream.

Only freeze the DSC when the remote parent actually carried one (its trace state
has the `sentry.dsc` key); otherwise leave it unset so it is derived dynamically
from the span, matching the OpenTelemetry SDK path, which never freezes the DSC
there and resolves it lazily (picking up `transaction` and `sample_rand`).
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from babe9a6 to 71ecd8d Compare June 24, 2026 15:43
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.

4 participants