Skip to content

Commit 09f2a69

Browse files
authored
fix(opentelemetry): Ensure DSC propagation works correctly (#10904)
This updates the propagation handling in OTEL to the following: 1. If there is an active (local) span, we always pick up propagation context/DSC data from it. 2. Else, we try to pick it from the current scope 3. If we can't find this (e.g. a detached otel context is used), we try to pick it from the remote span context 4. Finally, if that also fails, we have no DSC Closes #10899
1 parent ff918f0 commit 09f2a69

File tree

9 files changed

+607
-428
lines changed

9 files changed

+607
-428
lines changed

packages/node-experimental/test/integration/transactions.test.ts

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { TraceFlags, context, trace } from '@opentelemetry/api';
22
import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
33
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core';
4-
import { SentrySpanProcessor, setPropagationContextOnContext } from '@sentry/opentelemetry';
5-
import type { PropagationContext, TransactionEvent } from '@sentry/types';
4+
import { SentrySpanProcessor } from '@sentry/opentelemetry';
5+
import type { TransactionEvent } from '@sentry/types';
66
import { logger } from '@sentry/utils';
77

88
import * as Sentry from '../../src';
@@ -488,37 +488,27 @@ describe('Integration | Transactions', () => {
488488
traceFlags: TraceFlags.SAMPLED,
489489
};
490490

491-
const propagationContext: PropagationContext = {
492-
traceId,
493-
parentSpanId,
494-
spanId: '6e0c63257de34c93',
495-
sampled: true,
496-
};
497-
498491
mockSdkInit({ enableTracing: true, beforeSendTransaction });
499492

500493
const client = Sentry.getClient()!;
501494

502495
// We simulate the correct context we'd normally get from the SentryPropagator
503-
context.with(
504-
trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext),
505-
() => {
506-
Sentry.startSpan(
507-
{
508-
op: 'test op',
509-
name: 'test name',
510-
origin: 'auto.test',
511-
attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task' },
512-
},
513-
() => {
514-
const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' });
515-
subSpan.end();
496+
context.with(trace.setSpanContext(context.active(), spanContext), () => {
497+
Sentry.startSpan(
498+
{
499+
op: 'test op',
500+
name: 'test name',
501+
origin: 'auto.test',
502+
attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task' },
503+
},
504+
() => {
505+
const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' });
506+
subSpan.end();
516507

517-
Sentry.startSpan({ name: 'inner span 2' }, () => {});
518-
},
519-
);
520-
},
521-
);
508+
Sentry.startSpan({ name: 'inner span 2' }, () => {});
509+
},
510+
);
511+
});
522512

523513
await client.flush();
524514

packages/opentelemetry/src/constants.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import { createContextKey } from '@opentelemetry/api';
33
export const SENTRY_TRACE_HEADER = 'sentry-trace';
44
export const SENTRY_BAGGAGE_HEADER = 'baggage';
55
export const SENTRY_TRACE_STATE_DSC = 'sentry.trace';
6-
7-
/** Context Key to hold a PropagationContext. */
8-
export const SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY = createContextKey('SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY');
6+
export const SENTRY_TRACE_STATE_PARENT_SPAN_ID = 'sentry.parent_span_id';
97

108
/** Context Key to hold a Hub. */
119
export const SENTRY_HUB_CONTEXT_KEY = createContextKey('sentry_hub');

packages/opentelemetry/src/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@ export {
1414
getSpanScopes,
1515
} from './utils/spanData';
1616

17-
export {
18-
getPropagationContextFromContext,
19-
setPropagationContextOnContext,
20-
getScopesFromContext,
21-
} from './utils/contextData';
17+
export { getScopesFromContext } from './utils/contextData';
2218

2319
export {
2420
spanHasAttributes,

packages/opentelemetry/src/propagator.ts

Lines changed: 122 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Baggage, Context, SpanContext, TextMapGetter, TextMapSetter } from '@opentelemetry/api';
22
import { TraceFlags, propagation, trace } from '@opentelemetry/api';
33
import { TraceState, W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core';
4-
import { getClient, getDynamicSamplingContextFromClient } from '@sentry/core';
4+
import { getClient, getCurrentScope, getDynamicSamplingContextFromClient, getIsolationScope } from '@sentry/core';
55
import type { DynamicSamplingContext, PropagationContext } from '@sentry/types';
66
import {
77
SENTRY_BAGGAGE_KEY_PREFIX,
@@ -11,28 +11,30 @@ import {
1111
propagationContextFromHeaders,
1212
} from '@sentry/utils';
1313

14-
import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER, SENTRY_TRACE_STATE_DSC } from './constants';
15-
import { getPropagationContextFromContext, setPropagationContextOnContext } from './utils/contextData';
16-
17-
function getDynamicSamplingContextFromContext(context: Context): Partial<DynamicSamplingContext> | undefined {
18-
// If possible, we want to take the DSC from the active span
19-
// That should take precedence over the DSC from the propagation context
20-
const activeSpan = trace.getSpan(context);
21-
const traceStateDsc = activeSpan?.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC);
22-
const dscOnSpan = traceStateDsc ? baggageHeaderToDynamicSamplingContext(traceStateDsc) : undefined;
23-
24-
if (dscOnSpan) {
25-
return dscOnSpan;
26-
}
27-
28-
const propagationContext = getPropagationContextFromContext(context);
29-
30-
if (propagationContext) {
31-
const { traceId } = getSentryTraceData(context, propagationContext);
32-
return getDynamicSamplingContext(propagationContext, traceId);
33-
}
34-
35-
return undefined;
14+
import {
15+
SENTRY_BAGGAGE_HEADER,
16+
SENTRY_TRACE_HEADER,
17+
SENTRY_TRACE_STATE_DSC,
18+
SENTRY_TRACE_STATE_PARENT_SPAN_ID,
19+
} from './constants';
20+
import { getScopesFromContext, setScopesOnContext } from './utils/contextData';
21+
22+
/** Get the Sentry propagation context from a span context. */
23+
export function getPropagationContextFromSpanContext(spanContext: SpanContext): PropagationContext {
24+
const { traceId, spanId, traceFlags, traceState } = spanContext;
25+
26+
const dscString = traceState ? traceState.get(SENTRY_TRACE_STATE_DSC) : undefined;
27+
const dsc = dscString ? baggageHeaderToDynamicSamplingContext(dscString) : undefined;
28+
const parentSpanId = traceState ? traceState.get(SENTRY_TRACE_STATE_PARENT_SPAN_ID) : undefined;
29+
const sampled = traceFlags === TraceFlags.SAMPLED;
30+
31+
return {
32+
traceId,
33+
spanId,
34+
sampled,
35+
parentSpanId,
36+
dsc,
37+
};
3638
}
3739

3840
/**
@@ -49,10 +51,7 @@ export class SentryPropagator extends W3CBaggagePropagator {
4951

5052
let baggage = propagation.getBaggage(context) || propagation.createBaggage({});
5153

52-
const propagationContext = getPropagationContextFromContext(context);
53-
const { spanId, traceId, sampled } = getSentryTraceData(context, propagationContext);
54-
55-
const dynamicSamplingContext = getDynamicSamplingContextFromContext(context);
54+
const { dynamicSamplingContext, traceId, spanId, sampled } = getInjectionData(context);
5655

5756
if (dynamicSamplingContext) {
5857
baggage = Object.entries(dynamicSamplingContext).reduce<Baggage>((b, [dscKey, dscValue]) => {
@@ -83,15 +82,11 @@ export class SentryPropagator extends W3CBaggagePropagator {
8382

8483
const propagationContext = propagationContextFromHeaders(sentryTraceHeader, maybeBaggageHeader);
8584

86-
// Add propagation context to context
87-
const contextWithPropagationContext = setPropagationContextOnContext(context, propagationContext);
88-
8985
// We store the DSC as OTEL trace state on the span context
90-
const dscString = propagationContext.dsc
91-
? dynamicSamplingContextToSentryBaggageHeader(propagationContext.dsc)
92-
: undefined;
93-
94-
const traceState = dscString ? new TraceState().set(SENTRY_TRACE_STATE_DSC, dscString) : undefined;
86+
const traceState = makeTraceState({
87+
parentSpanId: propagationContext.parentSpanId,
88+
dsc: propagationContext.dsc,
89+
});
9590

9691
const spanContext: SpanContext = {
9792
traceId: propagationContext.traceId,
@@ -101,8 +96,18 @@ export class SentryPropagator extends W3CBaggagePropagator {
10196
traceState,
10297
};
10398

104-
// Add remote parent span context
105-
return trace.setSpanContext(contextWithPropagationContext, spanContext);
99+
// Add remote parent span context,
100+
const ctxWithSpanContext = trace.setSpanContext(context, spanContext);
101+
102+
// Also update the scope on the context (to be sure this is picked up everywhere)
103+
const scopes = getScopesFromContext(ctxWithSpanContext);
104+
const newScopes = {
105+
scope: scopes ? scopes.scope.clone() : getCurrentScope().clone(),
106+
isolationScope: scopes ? scopes.isolationScope : getIsolationScope(),
107+
};
108+
newScopes.scope.setPropagationContext(propagationContext);
109+
110+
return setScopesOnContext(ctxWithSpanContext, newScopes);
106111
}
107112

108113
/**
@@ -113,13 +118,91 @@ export class SentryPropagator extends W3CBaggagePropagator {
113118
}
114119
}
115120

116-
/** Get the DSC. */
121+
/** Exported for tests. */
122+
export function makeTraceState({
123+
parentSpanId,
124+
dsc,
125+
}: { parentSpanId?: string; dsc?: Partial<DynamicSamplingContext> }): TraceState | undefined {
126+
if (!parentSpanId && !dsc) {
127+
return undefined;
128+
}
129+
130+
// We store the DSC as OTEL trace state on the span context
131+
const dscString = dsc ? dynamicSamplingContextToSentryBaggageHeader(dsc) : undefined;
132+
133+
const traceStateBase = parentSpanId
134+
? new TraceState().set(SENTRY_TRACE_STATE_PARENT_SPAN_ID, parentSpanId)
135+
: new TraceState();
136+
137+
return dscString ? traceStateBase.set(SENTRY_TRACE_STATE_DSC, dscString) : traceStateBase;
138+
}
139+
140+
function getInjectionData(context: Context): {
141+
dynamicSamplingContext: Partial<DynamicSamplingContext> | undefined;
142+
traceId: string | undefined;
143+
spanId: string | undefined;
144+
sampled: boolean | undefined;
145+
} {
146+
const span = trace.getSpan(context);
147+
const spanIsRemote = span?.spanContext().isRemote;
148+
149+
// If we have a local span, we can just pick everything from it
150+
if (span && !spanIsRemote) {
151+
const spanContext = span.spanContext();
152+
const propagationContext = getPropagationContextFromSpanContext(spanContext);
153+
const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, spanContext.traceId);
154+
return {
155+
dynamicSamplingContext,
156+
traceId: spanContext.traceId,
157+
spanId: spanContext.spanId,
158+
sampled: spanContext.traceFlags === TraceFlags.SAMPLED,
159+
};
160+
}
161+
162+
// Else we try to use the propagation context from the scope
163+
const scope = getScopesFromContext(context)?.scope;
164+
if (scope) {
165+
const propagationContext = scope.getPropagationContext();
166+
const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, propagationContext.traceId);
167+
return {
168+
dynamicSamplingContext,
169+
traceId: propagationContext.traceId,
170+
spanId: propagationContext.spanId,
171+
sampled: propagationContext.sampled,
172+
};
173+
}
174+
175+
// Else, we look at the remote span context
176+
const spanContext = trace.getSpanContext(context);
177+
if (spanContext) {
178+
const propagationContext = getPropagationContextFromSpanContext(spanContext);
179+
const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, spanContext.traceId);
180+
181+
return {
182+
dynamicSamplingContext,
183+
traceId: spanContext.traceId,
184+
spanId: spanContext.spanId,
185+
sampled: spanContext.traceFlags === TraceFlags.SAMPLED,
186+
};
187+
}
188+
189+
// If we have neither, there is nothing much we can do, but that should not happen usually
190+
// Unless there is a detached OTEL context being passed around
191+
return {
192+
dynamicSamplingContext: undefined,
193+
traceId: undefined,
194+
spanId: undefined,
195+
sampled: undefined,
196+
};
197+
}
198+
199+
/** Get the DSC from a context, or fall back to use the one from the client. */
117200
function getDynamicSamplingContext(
118201
propagationContext: PropagationContext,
119202
traceId: string | undefined,
120203
): Partial<DynamicSamplingContext> | undefined {
121204
// If we have a DSC on the propagation context, we just use it
122-
if (propagationContext.dsc) {
205+
if (propagationContext?.dsc) {
123206
return propagationContext.dsc;
124207
}
125208

@@ -132,30 +215,3 @@ function getDynamicSamplingContext(
132215

133216
return undefined;
134217
}
135-
136-
/** Get the trace data for propagation. */
137-
function getSentryTraceData(
138-
context: Context,
139-
propagationContext: PropagationContext | undefined,
140-
): {
141-
spanId: string | undefined;
142-
traceId: string | undefined;
143-
sampled: boolean | undefined;
144-
} {
145-
const span = trace.getSpan(context);
146-
const spanContext = span && span.spanContext();
147-
148-
const traceId = spanContext ? spanContext.traceId : propagationContext?.traceId;
149-
150-
// We have a few scenarios here:
151-
// If we have an active span, and it is _not_ remote, we just use the span's ID
152-
// If we have an active span that is remote, we do not want to use the spanId, as we don't want to attach it to the parent span
153-
// If `isRemote === true`, the span is bascially virtual
154-
// If we don't have a local active span, we use the generated spanId from the propagationContext
155-
const spanId = spanContext && !spanContext.isRemote ? spanContext.spanId : propagationContext?.spanId;
156-
157-
// eslint-disable-next-line no-bitwise
158-
const sampled = spanContext ? Boolean(spanContext.traceFlags & TraceFlags.SAMPLED) : propagationContext?.sampled;
159-
160-
return { traceId, spanId, sampled };
161-
}

packages/opentelemetry/src/sampler.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import type { Client, ClientOptions, SamplingContext } from '@sentry/types';
88
import { isNaN, logger } from '@sentry/utils';
99

1010
import { DEBUG_BUILD } from './debug-build';
11+
import { getPropagationContextFromSpanContext } from './propagator';
1112
import { InternalSentrySemanticAttributes } from './semanticAttributes';
12-
import { getPropagationContextFromContext } from './utils/contextData';
1313

1414
/**
1515
* A custom OTEL sampler that uses Sentry sampling rates to make it's decision
@@ -44,7 +44,7 @@ export class SentrySampler implements Sampler {
4444
// Note for testing: `isSpanContextValid()` checks the format of the traceId/spanId, so we need to pass valid ones
4545
if (parentContext && isSpanContextValid(parentContext) && parentContext.traceId === traceId) {
4646
if (parentContext.isRemote) {
47-
parentSampled = getParentRemoteSampled(parentContext, context);
47+
parentSampled = getParentRemoteSampled(parentContext);
4848
DEBUG_BUILD &&
4949
logger.log(`[Tracing] Inheriting remote parent's sampled decision for ${spanName}: ${parentSampled}`);
5050
} else {
@@ -178,10 +178,10 @@ function isValidSampleRate(rate: unknown): boolean {
178178
return true;
179179
}
180180

181-
function getParentRemoteSampled(spanContext: SpanContext, context: Context): boolean | undefined {
181+
function getParentRemoteSampled(spanContext: SpanContext): boolean | undefined {
182182
const traceId = spanContext.traceId;
183-
const traceparentData = getPropagationContextFromContext(context);
183+
const traceparentData = getPropagationContextFromSpanContext(spanContext);
184184

185-
// Only inherit sample rate if `traceId` is the same
185+
// Only inherit sampled if `traceId` is the same
186186
return traceparentData && traceId === traceparentData.traceId ? traceparentData.sampled : undefined;
187187
}

packages/opentelemetry/src/utils/contextData.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,11 @@
11
import type { Context } from '@opentelemetry/api';
2-
import type { Hub, PropagationContext, Scope } from '@sentry/types';
2+
import type { Hub, Scope } from '@sentry/types';
33

4-
import {
5-
SENTRY_HUB_CONTEXT_KEY,
6-
SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY,
7-
SENTRY_SCOPES_CONTEXT_KEY,
8-
} from '../constants';
4+
import { SENTRY_HUB_CONTEXT_KEY, SENTRY_SCOPES_CONTEXT_KEY } from '../constants';
95
import type { CurrentScopes } from '../types';
106

117
const SCOPE_CONTEXT_MAP = new WeakMap<Scope, Context>();
128

13-
/**
14-
* Try to get the Propagation Context from the given OTEL context.
15-
* This requires the SentryPropagator to be registered.
16-
*/
17-
export function getPropagationContextFromContext(context: Context): PropagationContext | undefined {
18-
return context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext | undefined;
19-
}
20-
21-
/**
22-
* Set a Propagation Context on an OTEL context..
23-
* This will return a forked context with the Propagation Context set.
24-
*/
25-
export function setPropagationContextOnContext(context: Context, propagationContext: PropagationContext): Context {
26-
return context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext);
27-
}
28-
299
/**
3010
* Try to get the Hub from the given OTEL context.
3111
* This requires a Context Manager that was wrapped with getWrappedContextManager.

0 commit comments

Comments
 (0)