Skip to content

Commit cabe3dc

Browse files
andreiborzaclaudeisaacss1gr1d
authored
feat(opentelemetry)!: Simplify propagation and remove custom OTel tracing utils (#22702)
Simplifies the OpenTelemetry `SentryPropagator` down to a thin `TextMapPropagator` that delegates all trace-data serialization to core's `getTraceData()`, and removes the custom OTel-specific tracing/propagation overrides (`continueTrace`, `startNewTrace`, `getTraceData`, `getTraceContextForScope`) in favor of core's implementations. The net effect is that OTel-powered SDKs (Node, Next.js, SvelteKit, etc.) now share the same propagation and trace-continuation code paths as the rest of the SDK instead of maintaining a parallel OTel-only variant. ### What changed - **`SentryPropagator`** no longer extends `W3CBaggagePropagator`. It now implements `TextMapPropagator` directly. `inject()` reads headers from `getTraceData()` and only operates on the active context — if called with a non-active context it warns and skips. - Injecting with a non-active context _was_ supported before, but we intentionally drop it. The main remaining consumers of this propagator (Next.js and SvelteKit) don't need it, so rather than carry the complexity of handling arbitrary non-active contexts, we focus the propagator on the main use case and skip the edge cases that would drastically increase its complexity. - **URL / `tracePropagationTargets` filtering and `propagateTraceparent`** are no longer the propagator's concern — outgoing-request filtering already lives in the Node HTTP layer (`inject-trace-propagation-headers.ts`), so the duplicated logic, the `_urlMatchesTargetsMap` LRU cache, and the `sentry.url` trace-state (`SENTRY_TRACE_STATE_URL`) are removed. - **Removed the OTel-specific `getTraceData`, `continueTrace`, and `startNewTrace`** overrides from the async context strategy. Core's implementations are now used directly. To make this work, `continueTrace`/`startNewTrace` in core were reworked to route through `withActiveSpan(null, …)`, and OTel's non-recording (TwP) spans fall back to the scope's propagation-context trace id. - **Removed `getTraceContextForScope`** (and `NodeClient._getTraceInfoFromScope`); scope→trace-context resolution now goes through the shared core path. - `_startSpan` now runs the callback with the started span set active on the unsuppressed context, fixing event trace-context attaching to a stale ancestor span across `startNewTrace`/`continueTrace` boundaries. ### Aligning Node and Browser behavior Previously, the OTel/Node path and the browser (core) path had subtly diverging propagation behavior because they ran through _different_ implementations of `getTraceData`, `continueTrace`, and `startNewTrace`. The OTel variants derived trace data from the OTel context/span graph, while the browser used core's scope- and span-based logic — so edge cases (TwP sampling, DSC freezing, which trace id a fresh root span lands on) could resolve differently between the two. By deleting the OTel-specific overrides and routing everything through core, Node and Browser now produce trace headers and continue/start traces via a single common code path. There is one source of truth for how a `sentry-trace`/`baggage` pair is derived from the current scope and span, which removes a class of Node-vs-Browser discrepancies and makes future changes apply uniformly to both. ### Test adjustments Because this aligns Node's propagation/trace-continuation behavior with the common core path, a number of Node integration tests had to be updated to match the new (now shared) behavior — primarily expectations around trace ids for parallel/root spans and outgoing-request propagation (`parallel-root-spans`, `parallel-spans-in-scope`, `fetch-sampled-no-active-span`, etc.). These changes reflect the corrected/unified behavior, not regressions. ### Breaking changes - `getTraceContextForScope` is no longer exported from `@sentry/opentelemetry`. - The `sentry.url` trace-state and the `getInjectionData` / OTel-specific `getTraceData` internals of the propagator are removed. - The `SentryPropagator` no longer injects when called with a non-active context. Closes #22281 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: isaacs <i@izs.me> Co-authored-by: Andrei Borza <andrei.borza@sentry.io> Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> Co-authored-by: Sigrid <32902192+s1gr1d@users.noreply.github.com>
1 parent 33725cd commit cabe3dc

26 files changed

Lines changed: 249 additions & 1624 deletions

File tree

MIGRATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ Sentry.init({
479479

480480
### `@sentry/opentelemetry`
481481

482-
- `SentryPropagator` was removed. It is no longer needed now that Sentry does not manage OpenTelemetry trace propagation by default.
482+
- `getTraceContextForScope` was removed. Scope-to-trace-context resolution now goes through the shared core implementation.
483483
- `OpenTelemetryServerRuntimeOptions` was removed.
484484
- The `@opentelemetry/core` peer dependency was removed; its APIs are now vendored internally.
485485
- OpenTelemetry resources are no longer collected, and `contexts.otel.resource` was dropped from events.

dev-packages/node-integration-tests/suites/pino/scenario.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ Sentry.pinoIntegration.untrackLogger(ignoredLogger);
88

99
ignoredLogger.info('this will not be tracked');
1010

11-
Sentry.withIsolationScope(() => {
11+
// Each operation runs in its own trace, so logs emitted within them carry distinct trace ids.
12+
Sentry.startNewTrace(() => {
1213
Sentry.startSpan({ name: 'startup' }, () => {
1314
logger.info({ user: 'user-id', something: { more: 3, complex: 'nope' } }, 'hello world');
1415
});
1516
});
1617

1718
setTimeout(() => {
18-
Sentry.withIsolationScope(() => {
19+
Sentry.startNewTrace(() => {
1920
Sentry.startSpan({ name: 'later' }, () => {
2021
const child = logger.child({ module: 'authentication' });
2122
child.error(new Error('oh no'));

dev-packages/node-integration-tests/suites/public-api/bindScopeToEmitter/test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ test('bindScopeToEmitter preserves the active span for listeners firing in a dif
3434
expect(childBound?.parent_span_id).toBe(parentSpanId);
3535
expect(childBound?.trace_id).toBe(parentTraceId);
3636

37-
// The unbound emitter's listener ran without the parent active -> its own, separate trace.
37+
// The unbound emitter's listener ran without the parent active -> its own root transaction,
38+
// not nested under the parent span. It still shares the isolation scope's propagation context
39+
// trace, matching the core SDK behavior for root spans.
3840
expect(childUnbound?.spans).toEqual([]);
39-
expect(childUnbound?.contexts?.trace?.trace_id).not.toBe(parentTraceId);
41+
expect(childUnbound?.contexts?.trace?.parent_span_id).toBeUndefined();
4042
});

dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,24 @@ afterAll(() => {
66
});
77

88
test('sends manually started streamed parallel root spans in root context', async () => {
9-
expect.assertions(7);
9+
expect.assertions(6);
1010

1111
await createRunner(__dirname, 'scenario.ts')
12-
.expect({ span: { items: [{ name: 'test_span_1' }] } })
1312
.expect({
1413
span: spanContainer => {
1514
expect(spanContainer).toBeDefined();
16-
const traceId = spanContainer.items[0]!.trace_id;
17-
expect(traceId).toMatch(/^[0-9a-f]{32}$/);
1815

19-
// It ignores propagation context of the root context
20-
expect(traceId).not.toBe('12345678901234567890123456789012');
21-
expect(spanContainer.items[0]!.parent_span_id).toBeUndefined();
16+
const span1 = spanContainer.items.find(item => item.name === 'test_span_1');
17+
const span2 = spanContainer.items.find(item => item.name === 'test_span_2');
18+
expect(span1).toBeDefined();
19+
expect(span2).toBeDefined();
2220

23-
// Different trace ID than the first span
24-
const trace1Id = spanContainer.items[0]!.attributes.spanIdTraceId?.value;
25-
expect(trace1Id).toMatch(/^[0-9a-f]{32}$/);
21+
// Both root spans continue the scope's propagation context, matching the core SDK behavior.
22+
expect(span1!.trace_id).toBe('12345678901234567890123456789012');
23+
expect(span1!.parent_span_id).toBe('1234567890123456');
2624

27-
expect(trace1Id).not.toBe(traceId);
25+
// Same trace ID for both spans
26+
expect(span2!.trace_id).toBe(span1!.trace_id);
2827
},
2928
})
3029
.start()

dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,23 @@ afterAll(() => {
66
});
77

88
test('should send manually started parallel root spans in root context', async () => {
9-
expect.assertions(7);
9+
expect.assertions(6);
1010

1111
await createRunner(__dirname, 'scenario.ts')
1212
.expect({ transaction: { transaction: 'test_span_1' } })
1313
.expect({
1414
transaction: transaction => {
1515
expect(transaction).toBeDefined();
1616
const traceId = transaction.contexts?.trace?.trace_id;
17-
expect(traceId).toBeDefined();
1817

19-
// It ignores propagation context of the root context
20-
expect(traceId).not.toBe('12345678901234567890123456789012');
21-
expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined();
18+
// Both root spans continue the scope's propagation context, matching the core SDK behavior.
19+
expect(traceId).toBe('12345678901234567890123456789012');
20+
expect(transaction.contexts?.trace?.parent_span_id).toBe('1234567890123456');
2221

23-
// Different trace ID than the first span
22+
// Same trace ID as the first span
2423
const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
25-
expect(trace1Id).toBeDefined();
26-
expect(trace1Id).not.toBe(traceId);
24+
expect(trace1Id).toBe('12345678901234567890123456789012');
25+
expect(trace1Id).toBe(traceId);
2726
},
2827
})
2928
.start()

dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,20 @@ test('sends manually started streamed parallel root spans outside of root contex
99
expect.assertions(6);
1010

1111
await createRunner(__dirname, 'scenario.ts')
12-
.expect({ span: { items: [{ name: 'test_span_1' }] } })
1312
.expect({
1413
span: spanContainer => {
1514
expect(spanContainer).toBeDefined();
16-
const traceId = spanContainer.items[0]!.trace_id;
17-
expect(traceId).toMatch(/^[0-9a-f]{32}$/);
18-
expect(spanContainer.items[0]!.parent_span_id).toBeUndefined();
1915

20-
const trace1Id = spanContainer.items[0]!.attributes.spanIdTraceId?.value;
21-
expect(trace1Id).toMatch(/^[0-9a-f]{32}$/);
16+
const span1 = spanContainer.items.find(item => item.name === 'test_span_1');
17+
const span2 = spanContainer.items.find(item => item.name === 'test_span_2');
18+
expect(span1).toBeDefined();
19+
expect(span2).toBeDefined();
2220

23-
// Different trace ID as the first span
24-
expect(trace1Id).not.toBe(traceId);
21+
expect(span1!.trace_id).toMatch(/^[0-9a-f]{32}$/);
22+
expect(span1!.parent_span_id).toBeUndefined();
23+
24+
// Same trace ID for both spans - both root spans share the scope's propagation context
25+
expect(span2!.trace_id).toBe(span1!.trace_id);
2526
},
2627
})
2728
.start()

dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,22 @@ test('sends manually started streamed parallel root spans outside of root contex
99
expect.assertions(6);
1010

1111
await createRunner(__dirname, 'scenario.ts')
12-
.expect({ span: { items: [{ name: 'test_span_1' }] } })
1312
.expect({
1413
span: spanContainer => {
1514
expect(spanContainer).toBeDefined();
16-
const traceId = spanContainer.items[0]!.trace_id;
17-
expect(traceId).toMatch(/^[0-9a-f]{32}$/);
18-
expect(spanContainer.items[0]!.parent_span_id).toBeUndefined();
1915

20-
const trace1Id = spanContainer.items[0]!.attributes.spanIdTraceId?.value;
21-
expect(trace1Id).toMatch(/^[0-9a-f]{32}$/);
16+
const span1 = spanContainer.items.find(item => item.name === 'test_span_1');
17+
const span2 = spanContainer.items.find(item => item.name === 'test_span_2');
18+
expect(span1).toBeDefined();
19+
expect(span2).toBeDefined();
2220

23-
// Different trace ID as the first span
24-
expect(trace1Id).not.toBe(traceId);
21+
// Both root spans continue the scope's propagation context, including the parentSpanId,
22+
// matching the core SDK behavior.
23+
expect(span1!.trace_id).toBe('12345678901234567890123456789012');
24+
expect(span1!.parent_span_id).toBe('1234567890123456');
25+
26+
// Same trace ID for both spans
27+
expect(span2!.trace_id).toBe(span1!.trace_id);
2528
},
2629
})
2730
.start()

dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ test('should send manually started parallel root spans outside of root context w
1212
transaction: transaction => {
1313
expect(transaction).toBeDefined();
1414
const traceId = transaction.contexts?.trace?.trace_id;
15-
expect(traceId).toBeDefined();
16-
expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined();
1715

18-
const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
19-
expect(trace1Id).toBeDefined();
16+
// Both root spans continue the scope's propagation context, including the parentSpanId,
17+
// matching the core SDK behavior.
18+
expect(traceId).toBe('12345678901234567890123456789012');
19+
expect(transaction.contexts?.trace?.parent_span_id).toBe('1234567890123456');
2020

21-
// Different trace ID as the first span
22-
expect(trace1Id).not.toBe(traceId);
21+
// Same trace ID as the first span
22+
const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
23+
expect(trace1Id).toBe(traceId);
2324
},
2425
})
2526
.start()

dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ test('should send manually started parallel root spans outside of root context',
2020
const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
2121
expect(trace1Id).toBeDefined();
2222

23-
// Different trace ID as the first span
24-
expect(trace1Id).not.toBe(traceId);
23+
// Same trace ID as the first span - both root spans share the scope's propagation context
24+
expect(trace1Id).toBe(traceId);
2525
},
2626
})
2727
.start()

dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ describe('outgoing fetch', () => {
1010
const [SERVER_URL, closeTestServer] = await createTestServer()
1111
.get('/api/v0', headers => {
1212
expect(headers['baggage']).toEqual(expect.any(String));
13-
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})$/));
14-
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
13+
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-0$/));
14+
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0');
1515
})
1616
.get('/api/v1', headers => {
1717
expect(headers['baggage']).toEqual(expect.any(String));
18-
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})$/));
19-
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
18+
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-0$/));
19+
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0');
2020
})
2121
.get('/api/v2', headers => {
2222
expect(headers['baggage']).toBeUndefined();

0 commit comments

Comments
 (0)