Skip to content

feat(server-utils): Add otlpIntegration to connect Sentry to an existing OpenTelemetry setup - #23099

Merged
andreiborza merged 10 commits into
developfrom
ab/js-3291-otlp-integration
Aug 7, 2026
Merged

feat(server-utils): Add otlpIntegration to connect Sentry to an existing OpenTelemetry setup#23099
andreiborza merged 10 commits into
developfrom
ab/js-3291-otlp-integration

Conversation

@andreiborza

@andreiborza andreiborza commented Aug 6, 2026

Copy link
Copy Markdown
Member

What

Adds otlpIntegration() to @sentry/server-utils, re-exported from the server SDKs so it works with no extra install and no extra import.

  • Sentry.otlpIntegration() takes no options. Everything Sentry sends that carries trace information (errors, logs, metrics, check-ins) is attached to whatever OpenTelemetry span is active, so it lands on the same trace as the spans your own OpenTelemetry SDK exports.
  • Sentry.getOtlpTracesEndpoint(dsn) returns the URL and auth headers for Sentry's OTLP traces endpoint, to configure your own OTLPTraceExporter with.
  • Exported from node, bun, deno, cloudflare, vercel-edge, aws-serverless and google-cloud-serverless. The meta-framework SDKs inherit it through their existing export * from '@sentry/node'.
  • Adds a node-express-otlp E2E app covering a real OpenTelemetry Node SDK setup end to end.

The integration does not set up a span exporter, span processor, or tracer provider. Users keep full ownership of their OpenTelemetry pipeline. Outgoing request propagation is left to their OpenTelemetry propagator, and an active Sentry span still takes precedence, so this only changes behavior when Sentry has no span of its own.

import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import * as Sentry from '@sentry/node';

const provider = new NodeTracerProvider({
  spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter(Sentry.getOtlpTracesEndpoint('__DSN__')))],
});
provider.register();

Sentry.init({
  dsn: '__DSN__',
  integrations: [Sentry.otlpIntegration()],
});

Why

Users running their own OpenTelemetry setup had no supported way to correlate Sentry telemetry with their OpenTelemetry traces. The integration lives in @sentry/server-utils because it is the shared base for every server SDK and no browser SDK, so @opentelemetry/api reaches exactly the packages that can use it and never @sentry/browser. Putting it in @sentry/core was rejected: core is not a package users depend on directly, so @sentry/core/otlp would not resolve under pnpm, and core would own an @opentelemetry/api version range on behalf of every browser and edge user.

Closes: #23082

…sting OpenTelemetry setup

Adds `otlpIntegration()` and `getOtlpTracesEndpoint()` to `@sentry/server-utils`, re-exported
from the server SDKs so no extra install or import is needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 6, 2026

Copy link
Copy Markdown

JS-3291

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 30.34 kB +0.01% +2 B 🔺
@sentry/browser - with treeshaking flags 28.55 kB +0.03% +7 B 🔺
@sentry/browser - with treeshaking flags tracing without tracing 26.86 kB +0.01% +1 B 🔺
@sentry/browser (incl. Tracing) 47.81 kB +0.01% +2 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 47.82 kB +0.01% +2 B 🔺
@sentry/browser (incl. Tracing, Profiling) 52.55 kB - -
@sentry/browser (incl. Tracing, Replay) 87.19 kB +0.01% +1 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 76.62 kB +0.01% +1 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 91.91 kB +0.01% +1 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 104.56 kB +0.01% +1 B 🔺
@sentry/browser (incl. Feedback) 47.68 kB +0.01% +3 B 🔺
@sentry/browser (incl. sendFeedback) 35.17 kB +0.01% +2 B 🔺
@sentry/browser (incl. FeedbackAsync) 40.34 kB +0.01% +3 B 🔺
@sentry/browser (incl. Metrics) 31.41 kB -0.01% -1 B 🔽
@sentry/browser (incl. Logs) 31.66 kB +0.07% +22 B 🔺
@sentry/browser (incl. Metrics & Logs) 32.32 kB +0.02% +4 B 🔺
@sentry/react 32.15 kB +0.01% +2 B 🔺
@sentry/react (incl. Tracing) 50.06 kB +0.01% +1 B 🔺
@sentry/vue 35.45 kB +0.01% +2 B 🔺
@sentry/vue (incl. Tracing) 49.82 kB - -
@sentry/svelte 30.37 kB +0.01% +2 B 🔺
CDN Bundle 32.34 kB +0.01% +1 B 🔺
CDN Bundle (incl. Tracing) 48.13 kB +0.02% +5 B 🔺
CDN Bundle (incl. Logs, Metrics) 33.88 kB -0.01% -1 B 🔽
CDN Bundle (incl. Tracing, Logs, Metrics) 49.46 kB +0.02% +5 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 73.23 kB - -
CDN Bundle (incl. Tracing, Replay) 85.74 kB +0.01% +4 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 87.02 kB +0.01% +4 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 91.56 kB +0.01% +4 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.86 kB +0.01% +5 B 🔺
CDN Bundle - uncompressed 95.88 kB +0.01% +7 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 143.58 kB +0.01% +7 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 100.5 kB +0.01% +7 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 147.56 kB +0.01% +7 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 225.2 kB +0.01% +7 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 262.84 kB +0.01% +7 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 266.81 kB +0.01% +7 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 276.55 kB +0.01% +7 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 280.5 kB +0.01% +7 B 🔺
@sentry/nextjs (client) 52.62 kB +0.01% +1 B 🔺
@sentry/sveltekit (client) 48.26 kB -0.01% -1 B 🔽
@sentry/core/server 65.77 kB +0.02% +11 B 🔺
@sentry/core/browser 51.99 kB +0.02% +8 B 🔺
@sentry/node 119.41 kB +0.01% +3 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 0 B added added
@sentry/node - without tracing 83.39 kB +0.01% +4 B 🔺
@sentry/aws-serverless 92.72 kB +0.01% +1 B 🔺
@sentry/cloudflare (withSentry) - minified 214.86 kB +0.02% +25 B 🔺
@sentry/cloudflare (withSentry) 530.77 kB +0.03% +107 B 🔺

View base workflow run

Comment thread packages/server-utils/src/otlp.ts
Logs, metrics and check-ins already flow through the same trace context as errors. Assert it
end-to-end and say so in the integration's docs.
OpenTelemetry returns a span wrapping INVALID_SPAN_CONTEXT when tracing is suppressed or when a
span is started before a tracer provider is registered. Its all-zero ids were being stamped onto
everything Sentry sends, breaking trace linkage instead of falling back to the Sentry scope.
Astro's server entry cannot `export * from '@sentry/node'` (Vite moves the exports onto `default`
in prod builds), so it enumerates them. The node-exports-test-app E2E check caught the gap.
@andreiborza
andreiborza marked this pull request as ready for review August 6, 2026 14:41
@andreiborza
andreiborza requested review from a team as code owners August 6, 2026 14:41
@andreiborza
andreiborza requested review from JPeer264, Lms24, chargome, isaacs, mydea, nicohrubec and s1gr1d and removed request for a team, isaacs, mydea and s1gr1d August 6, 2026 14:41

@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.

nice

Fills the TODO left in the OpenTelemetry interoperability section, and covers the migration for
users of the v10 `@sentry/node-core/light/otlp` integration, whose options and built-in exporter
setup are gone.
Adds entries under Removed APIs for the dropped `@sentry/node-core/light/otlp` entry point and the
removed `setupOtlpTracesExporter` / `collectorUrl` options, and under Renames for the integration
name change. The narrative section now links to both instead of repeating them.

@Lms24 Lms24 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.

I like the clean implementation and user-facing setup of this. IMHO much nicer than the intertwined OTel setup users had to go through before.

Q: What happens to the DSC / trace envelope header? I believe it should still be constructed from the scope in combination with the external propagation context, correct? Maybe we can assert on this in the test?

@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.

clean

With an external propagation context active, events are stamped with the OpenTelemetry trace id
while the DSC was still built from the Sentry scope, so every envelope header named a trace that
appeared nowhere else. There is no transaction semantic to describe the OpenTelemetry trace with,
so send no sampling context at all, matching sentry-python.
@andreiborza

Copy link
Copy Markdown
Member Author

@Lms24 good question. I double checked with how our frens at the python sdk are doing this and they drop the DSC completely when there's an active OTel span because it's not possible to correctly reconcile. So I'm following suite here.

Changed in 5f3b902

@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 5f3b902. Configure here.

Comment thread packages/core/src/tracing/dynamicSamplingContext.ts
The TwP placeholder path spread the scope DSC, which is now undefined while an external propagation
context is active, so it collapsed to an empty object and emitted a `trace: {}` envelope header. A
Sentry span means we are head of its trace, so fall through and derive the DSC from the span.
@andreiborza
andreiborza requested a review from Lms24 August 7, 2026 09:22

@Lms24 Lms24 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.

Good to ship from my end, given there's precedence for the trace header omission. As discussed offline, a quick cross-check with Python/Ingest on omitting the trace header would be appreciated 🙏

Comment on lines +71 to +72
// OpenTelemetry span), the SDK is not the head of the trace and has no transaction semantics to
// describe it with, so there is no sampling context to send. The scope's own DSC would name a

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: I would probably rephrase this to something like

Suggested change
// OpenTelemetry span), the SDK is not the head of the trace and has no transaction semantics to
// describe it with, so there is no sampling context to send. The scope's own DSC would name a
// OpenTelemetry span), the SDK lacks most of the DSC information, like sampled, sample_rate, sample_rand and transaction.

but there might also be other reasons depending on Python's reasoning

@andreiborza
andreiborza merged commit 8622d2e into develop Aug 7, 2026
524 of 531 checks passed
@andreiborza
andreiborza deleted the ab/js-3291-otlp-integration branch August 7, 2026 12:28
andreiborza added a commit that referenced this pull request Aug 7, 2026
…#23149)

## What

Restructures the "Better OpenTelemetry interoperability" section of the
v11 migration guide around the three setups users can actually run, so
people can find the one that matches them instead of piecing it together
from prose.

- **Sentry only, OpenTelemetry ignored** (the new default): native
Sentry spans, no tracer provider, `@opentelemetry/api` spans ignored.
- **OpenTelemetry-compatible mode** (`skipOpenTelemetrySetup: false`): a
minimal tracer provider picks up `@opentelemetry/api` spans and turns
them into Sentry spans. Everything goes to Sentry and only to Sentry.
- **Your own OpenTelemetry, Sentry linked to it**: Sentry tracing off,
`otlpIntegration()` for trace linking, `getOtlpTracesEndpoint()` to
point your exporter at Sentry.

Also adds an "Avoiding duplicate spans" subsection covering what happens
when Sentry's instrumentation overlaps with your own, and spells out
that the v10 bridge built from `SentryContextManager`, `SentrySampler`
and `SentrySpanProcessor` is gone.

This replaces the prose that previously covered
`skipOpenTelemetrySetup`, the removed bridge and the HTTP/fetch example,
rather than adding alongside it. Everything unique from that block is
preserved: the flipped default and per-SDK lists, and the v10 change
where `skipOpenTelemetrySetup: true` no longer disables HTTP and fetch
spans.

## Why

The old section explained the mechanism but never said which
configuration a given user wants, so the common questions (does Sentry
pick up my OpenTelemetry spans, do I get duplicates, where did
`SentrySpanProcessor` go) had to be inferred. Naming the three setups
makes that a lookup.

Note this touches the same region as #23099, which adds its own OTLP
integration section, so one of the two will need a rebase.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Nicolas Hrubec <nicolas.hrubec@outlook.com>
Co-authored-by: Charly Gomez <charly.gomez1310@gmail.com>
Co-authored-by: Lukas Stracke <lukas.stracke@sentry.io>
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.

Add otlpIntegration to @sentry/server-utils

4 participants