Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
dcefe76
feat(node)!: Default most SDKs to a no-tracer-provider setup
andreiborza Aug 3, 2026
29e8adc
Merge remote-tracking branch 'origin/develop' into ab/default-no-trac…
andreiborza Aug 3, 2026
2db921c
Gate setupEventContextTrace on the tracer-provider mode
andreiborza Aug 3, 2026
13d89f3
Replace event-trace spy tests with a behavioral test
andreiborza Aug 3, 2026
6f6f1d0
Backfill Sentry span data in no-provider mode
andreiborza Aug 3, 2026
1a2fdb0
Pin OTel-tracer integration tests to skipOpenTelemetrySetup: false
andreiborza Aug 3, 2026
35ec3af
Propagate sample_rand from a continued trace's frozen empty DSC
andreiborza Aug 3, 2026
e7a39a6
Do not activate ignored spans in the tracing-channel binding
andreiborza Aug 3, 2026
2a7b5b0
Only fold sample_rand into an empty continued DSC at freeze time
andreiborza Aug 3, 2026
2cc5797
Drop otel context assertion from astro tracing e2e tests
andreiborza Aug 3, 2026
aaec895
Expose the ALS scope store to node-native in no-provider mode
andreiborza Aug 3, 2026
6d64b01
Update DSC continuation assertions for propagated sample_rand
andreiborza Aug 3, 2026
842a0c0
Key react-router middleware counter by request, not OTel context
andreiborza Aug 3, 2026
8e12acb
Key react-router middleware counter by root span
andreiborza Aug 3, 2026
c55fad4
Merge remote-tracking branch 'origin/develop' into ab/default-no-trac…
andreiborza Aug 4, 2026
8f5fb88
Always run span-data backfill, independent of the tracer provider
andreiborza Aug 4, 2026
acc3fe9
Opt SvelteKit Cloudflare into the tracer provider
andreiborza Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,29 @@ Only `@sentry/nextjs` and `@sentry/sveltekit` still set up an OpenTelemetry comp

This means you can run your own OpenTelemetry setup cleanly alongside Sentry without having Sentry spans leak into your pipeline anymore. Your OpenTelemetry setup will no longer be required to use Sentry components for exporting, context management and trace propagation.

This behavior is controlled by the existing `skipOpenTelemetrySetup` option, whose default was flipped in v11. It now defaults to `true` for most server SDKs (including `@sentry/node`, `@sentry/bun`, the serverless SDKs, and `@sentry/cloudflare`) and to `false` for `@sentry/nextjs` and `@sentry/sveltekit`. When `true`, the SDK skips the tracer provider and isolates scopes with a native AsyncLocalStorage strategy; it still emits its own spans, but does not pick up spans created through `@opentelemetry/api`. Set it to `false` to have Sentry register its tracer provider and surface your OpenTelemetry spans:

```js
Sentry.init({
dsn: '__DSN__',
// Register the Sentry OpenTelemetry tracer provider to surface OTel spans in Sentry
skipOpenTelemetrySetup: false,
});
```

In v10, setting `skipOpenTelemetrySetup: true` also turned Sentry's own HTTP and fetch spans off by default, on the assumption that your own OpenTelemetry `HttpInstrumentation` would emit them instead. That is no longer the case: Sentry now emits HTTP and fetch spans whenever tracing is enabled, regardless of `skipOpenTelemetrySetup`. If you run your own OpenTelemetry HTTP instrumentation alongside Sentry, disable Sentry's spans to avoid duplicates:

```js
Sentry.init({
dsn: '__DSN__',
integrations: [
// Let your own OpenTelemetry HttpInstrumentation own HTTP & fetch spans
Sentry.httpIntegration({ spans: false }),
Sentry.nativeNodeFetchIntegration({ spans: false }),
],
});
```

With this, we also heavily reduced our OpenTelemetry dependencies, with `@opentelemetry/api` being the only remaining package we abide by. These changes also mean `@sentry/node-core` no longer serves any purpose and was [merged back into `@sentry/node`](#sentrynode-core-was-merged-back-into-sentrynode).

For most users, day-to-day tracing is **unchanged**.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1,
// The Vercel AI SDK emits its spans through `@opentelemetry/api`, so they are only picked up when
// the Cloudflare OpenTelemetry tracer provider is set up.
skipOpenTelemetrySetup: false,
}),
{
async fetch(_request, _env, _ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Sentry.init({
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
// The user-owned OTel HttpInstrumentation's spans reach Sentry through the tracer provider.
skipOpenTelemetrySetup: false,
integrations: [
// Disable Sentry's span creation so that OTel HttpInstrumentation
// is the only source of http.client spans. Breadcrumbs and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Sentry.init({
tracesSampleRate: 1.0,
transport: loggingTransport,
debug: true,
// This suite exercises coexistence with a user-owned OTel HttpInstrumentation whose spans reach
// Sentry through the tracer provider, so it must run with the provider enabled.
skipOpenTelemetrySetup: false,
});

// Simulate a user who independently sets up OTel HttpInstrumentation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1.0,
transport: loggingTransport,
// This suite drives the raw OpenTelemetry tracer (`client.tracer.startActiveSpan`), which only
// produces spans when Sentry owns the tracer provider.
skipOpenTelemetrySetup: false,
});
14 changes: 6 additions & 8 deletions packages/cloudflare/src/baseSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export function initWithDefaultIntegrations(
stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser),
integrations: getIntegrationsToSetup(options),
transport: options.transport || makeCloudflareTransport,
// Like most Node-based SDKs, Cloudflare defaults to running without a Sentry OpenTelemetry tracer
// provider. Scope isolation is handled by the entrypoint wrappers' AsyncLocalStorage strategy.
skipOpenTelemetrySetup: options.skipOpenTelemetrySetup ?? true,
flushLock,
};

Expand All @@ -109,14 +112,9 @@ export function initWithDefaultIntegrations(
}
/*! rollup-include-development-only-end */

/**
* The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility
* via a custom trace provider.
* This ensures that any spans emitted via `@opentelemetry/api` will be captured by Sentry.
* HOWEVER, big caveat: This does not handle custom context handling, it will always work off the current scope.
* This should be good enough for many, but not all integrations.
*/
if (!options.skipOpenTelemetrySetup) {
// Opt-in only: when `skipOpenTelemetrySetup` is `false`, set up a custom trace provider so spans
// emitted via `@opentelemetry/api` are captured by Sentry. See the option's docs for the caveats.
if (!clientOptions.skipOpenTelemetrySetup) {
setupOpenTelemetryTracer();
}

Expand Down
14 changes: 7 additions & 7 deletions packages/cloudflare/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,15 @@ interface BaseCloudflareOptions {
enableDedupe?: boolean;

/**
* The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility
* via a custom trace provider.
* This ensures that any spans emitted via `@opentelemetry/api` will be captured by Sentry.
* HOWEVER, big caveat: This does not handle custom context handling, it will always work off the current scope.
* This should be good enough for many, but not all integrations.
* The Cloudflare SDK is not OpenTelemetry native. By default (`true`) it does not set up a tracer
* provider; spans are emitted via the SDK's own instrumentation and scopes are isolated with
* AsyncLocalStorage.
*
* If you want to opt-out of setting up the OpenTelemetry compatibility tracer, set this to `true`.
* Set this to `false` to opt into the OpenTelemetry compatibility tracer, which captures spans
* emitted via `@opentelemetry/api`. Big caveat: it does not handle custom context, always working
* off the current scope. This is good enough for many, but not all, integrations.
*
* @default false
* @default true
*/
skipOpenTelemetrySetup?: boolean;

Expand Down
4 changes: 4 additions & 0 deletions packages/cloudflare/test/opentelemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe('opentelemetry compatibility', () => {
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
traceLifecycle: 'static',
skipOpenTelemetrySetup: false,
beforeSendTransaction: event => {
transactionEvents.push(event);
return null;
Expand Down Expand Up @@ -109,6 +110,7 @@ describe('opentelemetry compatibility', () => {
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
traceLifecycle: 'static',
skipOpenTelemetrySetup: false,
beforeSendTransaction: event => {
transactionEvents.push(event);
return null;
Expand Down Expand Up @@ -153,6 +155,7 @@ describe('opentelemetry compatibility', () => {
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
traceLifecycle: 'static',
skipOpenTelemetrySetup: false,
beforeSendTransaction: event => {
transactionEvents.push(event);
return null;
Expand Down Expand Up @@ -181,6 +184,7 @@ describe('opentelemetry compatibility', () => {
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
traceLifecycle: 'static',
skipOpenTelemetrySetup: false,
beforeSendTransaction: event => {
transactionEvents.push(event);
return null;
Expand Down
3 changes: 3 additions & 0 deletions packages/nextjs/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ export function init(options: NodeOptions): NodeClient | undefined {
environment: options.environment || process.env.SENTRY_ENVIRONMENT || getVercelEnv(false) || process.env.NODE_ENV,
release: process.env._sentryRelease || globalWithInjectedValues._sentryRelease,
defaultIntegrations: customDefaultIntegrations,
// Next.js emits its own OpenTelemetry spans, so it defaults to registering the Sentry tracer
// provider (unlike most Node-based SDKs). A user-provided value still overrides this via `...options`.
skipOpenTelemetrySetup: false,
...options,
// Override runtime to 'cloudflare' when running on OpenNext/Cloudflare
...cloudflareConfig,
Expand Down
4 changes: 2 additions & 2 deletions packages/node-native/src/event-loop-block-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ function startPolling(
integrationOptions: Partial<ThreadBlockedIntegrationOptions>,
): IntegrationInternal | undefined {
if (client.asyncLocalStorageLookup) {
const { asyncLocalStorage, contextSymbol } = client.asyncLocalStorageLookup;
registerThread({ asyncLocalStorage, stateLookup: ['_currentContext', contextSymbol] });
const { asyncLocalStorage, stateLookup } = client.asyncLocalStorageLookup;
registerThread({ asyncLocalStorage, stateLookup });
} else {
registerThread();
}
Expand Down
6 changes: 3 additions & 3 deletions packages/node/src/integrations/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface HttpOptions {
* This will ensure that the default HttpInstrumentation from OpenTelemetry is not setup,
* only the Sentry-specific instrumentation for request isolation is applied.
*
* If `skipOpenTelemetrySetup: true` is configured, this defaults to `false`, otherwise it defaults to `true`.
* Defaults to `true` when tracing is enabled.
*/
spans?: boolean;

Expand All @@ -55,8 +55,8 @@ interface HttpOptions {
* Whether to inject trace propagation headers (sentry-trace, baggage, traceparent) into outgoing HTTP requests.
*
* When set to `false`, Sentry will not inject any trace propagation headers, but will still create breadcrumbs
* (if `breadcrumbs` is enabled). This is useful when `skipOpenTelemetrySetup: true` is configured and you want
* to avoid duplicate trace headers being injected by both Sentry and OpenTelemetry's HttpInstrumentation.
* (if `breadcrumbs` is enabled). This is useful when you run your own OpenTelemetry `HttpInstrumentation` and
* want to avoid duplicate trace headers being injected by both Sentry and OpenTelemetry.
*
* @default `true`
*/
Expand Down
6 changes: 3 additions & 3 deletions packages/node/src/integrations/node-fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {
export const nativeNodeFetchIntegration = defineIntegration(_nativeNodeFetchIntegration);

function _shouldInstrumentSpans(options: NodeFetchOptions, clientOptions: Partial<NodeClientOptions> = {}): boolean {
// If `spans` is passed in, it takes precedence
// Else, we by default emit spans, unless `skipOpenTelemetrySetup` is set to `true` or spans are not enabled
return options.spans ?? (!clientOptions.skipOpenTelemetrySetup && hasSpansEnabled(clientOptions));
// If `spans` is passed in, it takes precedence. Otherwise emit spans whenever tracing is enabled;
// fetch instrumentation is channel-based and does not depend on a Sentry OpenTelemetry tracer provider.
return options.spans ?? hasSpansEnabled(clientOptions);
}
2 changes: 1 addition & 1 deletion packages/node/src/integrations/node-fetch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export interface NodeFetchOptions extends UndiciInstrumentationConfig {
* If set to false, do not emit any spans.
* Breadcrumbs and trace propagation for outgoing fetch requests are still applied.
*
* If `skipOpenTelemetrySetup: true` is configured, this defaults to `false`, otherwise it defaults to `true`.
* Defaults to `true` when tracing is enabled.
*/
spans?: boolean;

Expand Down
31 changes: 26 additions & 5 deletions packages/node/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
stackParserFromStackParserOptions,
} from '@sentry/core';
import { setOpenTelemetryContextAsyncContextStrategy, setupEventContextTrace } from '@sentry/opentelemetry';
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils';
import { isMainThread, parentPort } from 'node:worker_threads';
import { detectOrchestrionSetup } from '@sentry/server-utils/orchestrion';
import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register';
Expand All @@ -40,7 +41,7 @@ import { getEntryPointType } from '../utils/entry-point';
import { getSpotlightConfig } from '../utils/spotlight';
import { defaultStackParser, getSentryRelease } from './api';
import { NodeClient } from './client';
import { initOpenTelemetry } from './initOtel';
import { initOpenTelemetry, setupSpanDataBackfill } from './initOtel';

/**
* Get the base default integrations shared by all Node SDK default-integration sets.
Expand Down Expand Up @@ -171,7 +172,18 @@ function _init(

const clientOptions = getClientOptions({ ...options, defaultIntegrations }, getDefaultIntegrationsImpl);

const asyncLocalStorageLookup = setOpenTelemetryContextAsyncContextStrategy();
// When Sentry does not own an OpenTelemetry tracer provider, scope isolation runs on a pure
// AsyncLocalStorage strategy instead of the OpenTelemetry context strategy. Instrumentation still
// emits spans via core `startSpan`; there is just no OTel provider or propagator behind them.
let asyncLocalStorageLookup: ReturnType<typeof setOpenTelemetryContextAsyncContextStrategy> | undefined;
if (clientOptions.skipOpenTelemetrySetup) {
// The ALS store already is the `{ scope, isolationScope }` object, so no key path is needed to
// reach it (unlike the OTel context strategy, where it is nested under the OTel context).
const asyncLocalStorage = setAsyncLocalStorageAsyncContextStrategy();
asyncLocalStorageLookup = { asyncLocalStorage, stateLookup: [] };
} else {
asyncLocalStorageLookup = setOpenTelemetryContextAsyncContextStrategy();
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

const scope = getCurrentScope();
scope.update(clientOptions.initialScope);
Expand Down Expand Up @@ -202,8 +214,6 @@ function _init(

updateScopeFromEnvVariables();

setupEventContextTrace(client);

// Ensure we flush events when vercel functions are ended
// See: https://vercel.com/docs/functions/functions-api-reference#sigterm-signal
if (process.env.VERCEL) {
Expand All @@ -213,8 +223,16 @@ function _init(
});
}

// Add Node SDK specific OpenTelemetry setup
// Channel-based instrumentation emits spans via core `startSpan` in every mode, so always backfill
// the Sentry-convention span data (e.g. `sentry.op`) the OTel provider pipeline would otherwise
// derive. It is idempotent, so it is a no-op for spans the provider already enriches.
setupSpanDataBackfill(client);

// Add Node SDK specific OpenTelemetry setup. `setupEventContextTrace` reads the active span from the
// OpenTelemetry context, so it only belongs here: without a Sentry tracer provider a foreign OTel
// span could otherwise override the Sentry trace on error events.
if (!clientOptions.skipOpenTelemetrySetup) {
setupEventContextTrace(client);
initOpenTelemetry(client);
}

Expand Down Expand Up @@ -249,6 +267,9 @@ function getClientOptions(
tracesSampleRate,
spotlight,
traceLifecycle,
// Most Node-based SDKs default to running without a Sentry OpenTelemetry tracer provider. SDKs
// that need OTel spans surfaced in Sentry (nextjs, sveltekit) opt back in by passing `false`.
skipOpenTelemetrySetup: options.skipOpenTelemetrySetup ?? true,
debug: envToBool(options.debug ?? process.env.SENTRY_DEBUG),
};

Expand Down
30 changes: 20 additions & 10 deletions packages/node/src/sdk/initOtel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function initOpenTelemetry(client: NodeClient): void {
setupOpenTelemetryLogger();
}

const provider = setupOtel(client);
const provider = setupOtel();
client.traceProvider = provider;
}

Expand Down Expand Up @@ -120,8 +120,26 @@ function getPreloadMethods(integrationNames?: string[]): ((() => void) & { id: s
});
}

/**
* Backfill Sentry span data (op, source, name, status) from OpenTelemetry semantic attributes.
*
* Channel-based instrumentation stamps OTel semantic attributes on native Sentry spans but leaves the
* Sentry-convention fields (e.g. `sentry.op`) to be inferred. On the OTel SDK provider that inference
* runs in the span processor/exporter; here it runs via client hooks so it happens whether or not a
* Sentry tracer provider is set up.
*/
export function setupSpanDataBackfill(client: NodeClient): void {
client.on('spanEnd', span => {
applyOtelSpanData(span, { finalizeStatus: true });
});

if (hasSpanStreamingEnabled(client)) {
client.on('preprocessSpan', backfillStreamedSpanDataFromOtel);
}
}

/** Just exported for tests. */
export function setupOtel(client: NodeClient): SentryTracerProvider | undefined {
export function setupOtel(): SentryTracerProvider | undefined {
const provider = new SentryTracerProvider();

if (!registerGlobalTracerProvider(provider)) {
Expand All @@ -134,13 +152,5 @@ export function setupOtel(client: NodeClient): SentryTracerProvider | undefined

propagation.setGlobalPropagator(new SentryPropagator());

client.on('spanEnd', span => {
applyOtelSpanData(span, { finalizeStatus: true });
});

if (hasSpanStreamingEnabled(client)) {
client.on('preprocessSpan', backfillStreamedSpanDataFromOtel);
}

return provider;
}
14 changes: 10 additions & 4 deletions packages/node/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ import type { NodeTransportOptions } from './transports';
*/
export interface OpenTelemetryServerRuntimeOptions extends ServerRuntimeOptions {
/**
* If this is set to true, the SDK will not set up OpenTelemetry automatically.
* In this case, you _have_ to ensure to set it up correctly yourself, including:
* * The `SentryPropagator`
* * The `SentryContextManager`
* Controls whether the SDK registers a Sentry OpenTelemetry tracer provider.
*
* When `true` (the default for most SDKs), no tracer provider is set up. The SDK isolates scopes
* with a native AsyncLocalStorage context strategy and still emits spans via its own
* instrumentation, but OpenTelemetry spans created through `@opentelemetry/api` are not picked up.
*
* When `false`, the SDK registers the `SentryTracerProvider` and `SentryPropagator` so that
* OpenTelemetry spans are surfaced in Sentry. This is the default for the Next.js and SvelteKit SDKs.
*
* @default true
*/
skipOpenTelemetrySetup?: boolean;
}
Expand Down
Loading
Loading