Skip to content

Commit 2312ed3

Browse files
committed
ensure ignoring works on orchestrion async context strategy
1 parent 19235b6 commit 2312ed3

3 files changed

Lines changed: 81 additions & 6 deletions

File tree

dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ describe('ignoring a child of a continued server segment (streaming)', () => {
1212
const testPropagation = async (path: string): Promise<void> => {
1313
const [SERVER_URL, closeTestServer] = await createTestServer()
1414
.get('/outgoing', headers => {
15-
expect(headers['sentry-trace']).toEqual(
16-
expect.stringMatching(/^12345678901234567890123456789012-[\da-f]{16}-1$/),
17-
);
15+
expect(headers['sentry-trace']).toMatch(/^12345678901234567890123456789012-[\da-f]{16}-1$/);
1816

1917
expect(headers['baggage']).toBe(
2018
'sentry-trace_id=12345678901234567890123456789012,sentry-sample_rate=1,sentry-sampled=true,sentry-public_key=public,sentry-sample_rand=0.5',

packages/opentelemetry/src/nodeAsyncContextStrategy.ts

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

67
interface ContextApi {
78
_getContextManager():
@@ -31,7 +32,7 @@ function getDefaultAsyncLocalStorageFactory(): () => TracingChannelBinding {
3132
return () => {
3233
return {
3334
asyncLocalStorage: defaultAsyncLocalStorage,
34-
getStoreWithActiveSpan: span => api.trace.setSpan(api.context.active(), span),
35+
getStoreWithActiveSpan,
3536
} satisfies TracingChannelBinding;
3637
};
3738
}
@@ -50,11 +51,24 @@ function getCustomAsyncLocalStorageFactory(): () => TracingChannelBinding | unde
5051
return asyncLocalStorage
5152
? ({
5253
asyncLocalStorage,
53-
getStoreWithActiveSpan: span => api.trace.setSpan(api.context.active(), span as api.Span),
54+
getStoreWithActiveSpan,
5455
} satisfies TracingChannelBinding)
5556
: undefined;
5657
} catch {
5758
return undefined;
5859
}
5960
};
6061
}
62+
63+
function getStoreWithActiveSpan(span: Parameters<TracingChannelBinding['getStoreWithActiveSpan']>[0]): api.Context {
64+
const activeContext = api.context.active();
65+
const activeSpan = api.trace.getSpan(activeContext);
66+
67+
// Tracing channels bind directly to the context manager's AsyncLocalStorage and bypass
68+
// SentryContextManager.with(), so ignored children must restore their parent here as well.
69+
const isIgnoredChild =
70+
(!!activeSpan && spanIsIgnored(span)) ||
71+
span.spanContext().traceState?.get(SENTRY_TRACE_STATE_CHILD_IGNORED) === '1';
72+
73+
return isIgnoredChild ? activeContext : api.trace.setSpan(activeContext, span);
74+
}

packages/opentelemetry/test/asyncContextStrategy.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1+
import { context, trace, TraceFlags, type Context } from '@opentelemetry/api';
12
import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
23
import type { Scope } from '@sentry/core';
34
import {
5+
getAsyncContextStrategy,
46
getCurrentScope,
57
getIsolationScope,
8+
getMainCarrier,
69
Scope as ScopeClass,
10+
SentryNonRecordingSpan,
711
setAsyncContextStrategy,
812
withIsolationScope,
913
withScope,
1014
} from '@sentry/core';
1115
import { afterAll, afterEach, beforeEach, describe, expect, it, test } from 'vitest';
1216
import { setOpenTelemetryContextAsyncContextStrategy } from '../src/asyncContextStrategy';
17+
import { SENTRY_TRACE_STATE_CHILD_IGNORED } from '../src/constants';
18+
import { setNodeOpenTelemetryContextAsyncContextStrategy } from '../src/nodeAsyncContextStrategy';
19+
import { TraceState } from '../src/utils/TraceState';
1320
import { setupOtel } from './helpers/initOtel';
1421
import { cleanupOtel } from './helpers/mockSdkInit';
1522
import { getDefaultTestClientOptions, TestClient } from './helpers/TestClient';
@@ -81,6 +88,62 @@ describe('asyncContextStrategy', () => {
8188
});
8289
});
8390

91+
test('tracing channel binding keeps the parent active for an ignored child span', () => {
92+
setNodeOpenTelemetryContextAsyncContextStrategy();
93+
94+
const parentSpan = trace.getTracer('test').startSpan('parent');
95+
const ignoredSpan = trace.wrapSpanContext({
96+
traceId: parentSpan.spanContext().traceId,
97+
spanId: '1234567890123456',
98+
traceFlags: TraceFlags.NONE,
99+
traceState: new TraceState().set(SENTRY_TRACE_STATE_CHILD_IGNORED, '1'),
100+
});
101+
102+
context.with(trace.setSpan(context.active(), parentSpan), () => {
103+
const binding = getAsyncContextStrategy(getMainCarrier()).getTracingChannelBinding?.();
104+
const store = binding?.getStoreWithActiveSpan(ignoredSpan);
105+
106+
expect(store).toBeDefined();
107+
expect(trace.getSpan(store as Context)).toBe(parentSpan);
108+
});
109+
110+
parentSpan.end();
111+
});
112+
113+
test('tracing channel binding keeps the parent active for a native ignored child span', () => {
114+
setNodeOpenTelemetryContextAsyncContextStrategy();
115+
116+
const parentSpan = trace.getTracer('test').startSpan('parent');
117+
const ignoredSpan = new SentryNonRecordingSpan({
118+
dropReason: 'ignored',
119+
traceId: parentSpan.spanContext().traceId,
120+
});
121+
122+
context.with(trace.setSpan(context.active(), parentSpan), () => {
123+
const binding = getAsyncContextStrategy(getMainCarrier()).getTracingChannelBinding?.();
124+
const store = binding?.getStoreWithActiveSpan(ignoredSpan);
125+
126+
expect(store).toBeDefined();
127+
expect(trace.getSpan(store as Context)).toBe(parentSpan);
128+
});
129+
130+
parentSpan.end();
131+
});
132+
133+
test('tracing channel binding activates a native ignored root span', () => {
134+
setNodeOpenTelemetryContextAsyncContextStrategy();
135+
136+
const ignoredSpan = new SentryNonRecordingSpan({
137+
dropReason: 'ignored',
138+
traceId: '12345678901234567890123456789012',
139+
});
140+
const binding = getAsyncContextStrategy(getMainCarrier()).getTracingChannelBinding?.();
141+
const store = binding?.getStoreWithActiveSpan(ignoredSpan);
142+
143+
expect(store).toBeDefined();
144+
expect(trace.getSpan(store as Context)).toBe(ignoredSpan);
145+
});
146+
84147
test('async scope inheritance', async () => {
85148
const initialScope = getCurrentScope();
86149
const initialIsolationScope = getIsolationScope();

0 commit comments

Comments
 (0)