Skip to content

feat(opentelemetry): Add SentryTracerProvider#21666

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

feat(opentelemetry): Add SentryTracerProvider#21666
andreiborza wants to merge 11 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 Outdated
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 27.59 kB - -
@sentry/browser - with treeshaking flags 26.03 kB - -
@sentry/browser (incl. Tracing) 46.07 kB +0.06% +27 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 47.81 kB +0.06% +25 B 🔺
@sentry/browser (incl. Tracing, Profiling) 50.86 kB +0.1% +50 B 🔺
@sentry/browser (incl. Tracing, Replay) 85.32 kB +0.05% +37 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 74.93 kB +0.06% +42 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 90.02 kB +0.06% +45 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 102.68 kB +0.04% +38 B 🔺
@sentry/browser (incl. Feedback) 44.77 kB - -
@sentry/browser (incl. sendFeedback) 32.39 kB - -
@sentry/browser (incl. FeedbackAsync) 37.52 kB - -
@sentry/browser (incl. Metrics) 28.67 kB - -
@sentry/browser (incl. Logs) 28.91 kB - -
@sentry/browser (incl. Metrics & Logs) 29.6 kB - -
@sentry/react 29.38 kB - -
@sentry/react (incl. Tracing) 48.37 kB +0.06% +25 B 🔺
@sentry/vue 32.85 kB +0.11% +35 B 🔺
@sentry/vue (incl. Tracing) 47.95 kB +0.1% +45 B 🔺
@sentry/svelte 27.61 kB - -
CDN Bundle 30 kB - -
CDN Bundle (incl. Tracing) 48.02 kB +0.08% +37 B 🔺
CDN Bundle (incl. Logs, Metrics) 31.57 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.36 kB +0.1% +45 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 70.77 kB - -
CDN Bundle (incl. Tracing, Replay) 85.51 kB +0.04% +26 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.78 kB +0.03% +26 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 91.33 kB +0.05% +38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.58 kB +0.06% +49 B 🔺
CDN Bundle - uncompressed 89.35 kB - -
CDN Bundle (incl. Tracing) - uncompressed 145.41 kB +0.1% +135 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 94.05 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 149.39 kB +0.1% +135 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 218.59 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 264.43 kB +0.06% +135 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 268.39 kB +0.06% +135 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 278.13 kB +0.05% +135 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 282.08 kB +0.05% +135 B 🔺
@sentry/nextjs (client) 50.78 kB +0.09% +45 B 🔺
@sentry/sveltekit (client) 46.47 kB +0.08% +36 B 🔺
@sentry/core/server 77.88 kB +0.17% +125 B 🔺
@sentry/core/browser 64.19 kB +0.22% +138 B 🔺
@sentry/node-core 62.51 kB +0.22% +136 B 🔺
@sentry/node 121.39 kB +0.12% +141 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 69.95 kB - -
@sentry/node/light 50.5 kB +0.08% +37 B 🔺
@sentry/node - without tracing 72.83 kB +0.21% +152 B 🔺
@sentry/aws-serverless 83.67 kB +0.19% +155 B 🔺
@sentry/cloudflare (withSentry) - minified 180.82 kB +0.11% +193 B 🔺
@sentry/cloudflare (withSentry) 447.51 kB +0.14% +582 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
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
Comment thread packages/core/src/tracing/sentrySpan.ts
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch 2 times, most recently from 924ce11 to 630bb67 Compare June 30, 2026 08:44
@andreiborza andreiborza marked this pull request as ready for review June 30, 2026 09:07
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch from 630bb67 to 15154ed Compare June 30, 2026 09:56

@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 15154ed. Configure here.

Comment thread packages/opentelemetry/src/applyOtelSpanData.ts
@andreiborza andreiborza force-pushed the ab/sentry-trace-provider-otel branch 3 times, most recently from 97e8266 to cfbfb8b Compare July 1, 2026 14:02

@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 didn't go over every edge cases I guess, but I tried it in Cloudflare SDK and replaced it with the current mocked trace provider. And it works like a charm

*/
export function backfillStreamedSpanDataFromOtel(spanJSON: StreamedSpanJSON, hint?: { spanKind?: number }): void {
const attributes = spanJSON.attributes;
if (!attributes) {

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/m: (I know it moved 1:1, but just saw this now) Should this guard against undefined? If this is undefined then it wouldn't set otel.kind nor the 'custom' source. Just want to clarify if that is intended. And if it is intended what would be the difference between attributes being undefined vs. an empty object {}

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.

In both paths (SentryTracerProvider with SentrySpans and OTel's BasicTracerProvider with ReadableSpans) this basically never hits.

SentrySpan's attributes init to {} and ReadableSpan's attributes are always an object so this is basically always at least {}.

Nonetheless, I updated it because "this shouldn't happen" are famous last words and it costs us nothing to fallback to {}.

Updated in 98255e6

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 cfbfb8b to 1c93e34 Compare July 1, 2026 18:41
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