Skip to content

Commit c13f7d1

Browse files
committed
point ignored child span to its parent
1 parent b099301 commit c13f7d1

4 files changed

Lines changed: 35 additions & 12 deletions

File tree

packages/core/src/tracing/trace.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,11 @@ function createChildOrRootSpan({
407407
dropReason: 'ignored',
408408
traceId: parentSpan?.spanContext().traceId ?? scope.getPropagationContext().traceId,
409409
});
410+
if (parentSpan && !forceTransaction) {
411+
// Preserve the root relationship so async context strategies can distinguish
412+
// ignored children from ignored segments and keep the parent active.
413+
addChildSpanToSpan(parentSpan, ignoredSpan);
414+
}
410415
setCapturedScopesOnSpan(ignoredSpan, scope, isolationScope);
411416

412417
return ignoredSpan;

packages/core/test/lib/tracing/trace.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2707,9 +2707,16 @@ describe('ignoreSpans (core path, streaming)', () => {
27072707
client.init();
27082708
const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
27092709

2710-
startSpan({ name: 'root' }, () => {
2711-
startSpan({ name: 'ignored-child' }, span => {
2712-
expect(span).toBeInstanceOf(SentryNonRecordingSpan);
2710+
startSpan({ name: 'root' }, rootSpan => {
2711+
startSpan({ name: 'ignored-child' }, ignoredSpan1 => {
2712+
expect(ignoredSpan1).toBeInstanceOf(SentryNonRecordingSpan);
2713+
expect(getRootSpan(ignoredSpan1)).toBe(rootSpan);
2714+
2715+
startSpan({ name: 'ignored-child' }, ignoredSpan2 => {
2716+
expect(ignoredSpan2).toBeInstanceOf(SentryNonRecordingSpan);
2717+
// since ignoredSpan1 is not active, ignoredSpan2 also has root as its parent
2718+
expect(getRootSpan(ignoredSpan2)).toBe(rootSpan);
2719+
});
27132720
});
27142721
});
27152722

packages/opentelemetry/src/nodeAsyncContextStrategy.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as api from '@opentelemetry/api';
22
import { setOpenTelemetryContextAsyncContextStrategy } from './asyncContextStrategy';
33
import { AsyncLocalStorage } from 'node:async_hooks';
4-
import { spanIsIgnored, type TracingChannelBinding } from '@sentry/core';
4+
import { getRootSpan, spanIsIgnored, type TracingChannelBinding } from '@sentry/core';
55
import { SENTRY_TRACE_STATE_CHILD_IGNORED } from './constants';
66

77
interface ContextApi {
@@ -62,12 +62,11 @@ function getCustomAsyncLocalStorageFactory(): () => TracingChannelBinding | unde
6262

6363
function getStoreWithActiveSpan(span: Parameters<TracingChannelBinding['getStoreWithActiveSpan']>[0]): api.Context {
6464
const activeContext = api.context.active();
65-
const activeSpan = api.trace.getSpan(activeContext);
6665

6766
// Tracing channels bind directly to the context manager's AsyncLocalStorage and bypass
6867
// SentryContextManager.with(), so ignored children must restore their parent here as well.
6968
const isIgnoredChild =
70-
(!!activeSpan && spanIsIgnored(span)) ||
69+
(spanIsIgnored(span) && getRootSpan(span) !== span) ||
7170
span.spanContext().traceState?.get(SENTRY_TRACE_STATE_CHILD_IGNORED) === '1';
7271

7372
return isIgnoredChild ? activeContext : api.trace.setSpan(activeContext, span);

packages/opentelemetry/test/asyncContextStrategy.test.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { context, trace, TraceFlags, type Context } from '@opentelemetry/api';
22
import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
33
import type { Scope } from '@sentry/core';
44
import {
5+
addChildSpanToSpan,
56
getAsyncContextStrategy,
67
getCurrentScope,
78
getIsolationScope,
@@ -118,6 +119,7 @@ describe('asyncContextStrategy', () => {
118119
dropReason: 'ignored',
119120
traceId: parentSpan.spanContext().traceId,
120121
});
122+
addChildSpanToSpan(parentSpan, ignoredSpan);
121123

122124
context.with(trace.setSpan(context.active(), parentSpan), () => {
123125
const binding = getAsyncContextStrategy(getMainCarrier()).getTracingChannelBinding?.();
@@ -130,18 +132,28 @@ describe('asyncContextStrategy', () => {
130132
parentSpan.end();
131133
});
132134

133-
test('tracing channel binding activates a native ignored root span', () => {
135+
test('tracing channel binding activates a native ignored root span with a remote parent', () => {
134136
setNodeOpenTelemetryContextAsyncContextStrategy();
135137

138+
const traceId = '12345678901234567890123456789012';
139+
const remoteParent = trace.wrapSpanContext({
140+
traceId,
141+
spanId: '1234567890123456',
142+
traceFlags: TraceFlags.SAMPLED,
143+
isRemote: true,
144+
});
136145
const ignoredSpan = new SentryNonRecordingSpan({
137146
dropReason: 'ignored',
138-
traceId: '12345678901234567890123456789012',
147+
traceId,
139148
});
140-
const binding = getAsyncContextStrategy(getMainCarrier()).getTracingChannelBinding?.();
141-
const store = binding?.getStoreWithActiveSpan(ignoredSpan);
142149

143-
expect(store).toBeDefined();
144-
expect(trace.getSpan(store as Context)).toBe(ignoredSpan);
150+
context.with(trace.setSpan(context.active(), remoteParent), () => {
151+
const binding = getAsyncContextStrategy(getMainCarrier()).getTracingChannelBinding?.();
152+
const store = binding?.getStoreWithActiveSpan(ignoredSpan);
153+
154+
expect(store).toBeDefined();
155+
expect(trace.getSpan(store as Context)).toBe(ignoredSpan);
156+
});
145157
});
146158

147159
test('async scope inheritance', async () => {

0 commit comments

Comments
 (0)