1
1
import type { Baggage , Context , SpanContext , TextMapGetter , TextMapSetter } from '@opentelemetry/api' ;
2
+ import { context } from '@opentelemetry/api' ;
2
3
import { TraceFlags , propagation , trace } from '@opentelemetry/api' ;
3
4
import { TraceState , W3CBaggagePropagator , isTracingSuppressed } from '@opentelemetry/core' ;
5
+ import type { continueTrace } from '@sentry/core' ;
4
6
import { getClient , getCurrentScope , getDynamicSamplingContextFromClient , getIsolationScope } from '@sentry/core' ;
5
7
import type { DynamicSamplingContext , PropagationContext } from '@sentry/types' ;
6
8
import {
@@ -16,18 +18,20 @@ import {
16
18
SENTRY_TRACE_HEADER ,
17
19
SENTRY_TRACE_STATE_DSC ,
18
20
SENTRY_TRACE_STATE_PARENT_SPAN_ID ,
21
+ SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING ,
19
22
} from './constants' ;
20
23
import { getScopesFromContext , setScopesOnContext } from './utils/contextData' ;
21
24
import { setIsSetup } from './utils/setupCheck' ;
22
25
23
26
/** Get the Sentry propagation context from a span context. */
24
27
export function getPropagationContextFromSpanContext ( spanContext : SpanContext ) : PropagationContext {
25
- const { traceId, spanId, traceFlags , traceState } = spanContext ;
28
+ const { traceId, spanId, traceState } = spanContext ;
26
29
27
30
const dscString = traceState ? traceState . get ( SENTRY_TRACE_STATE_DSC ) : undefined ;
28
31
const dsc = dscString ? baggageHeaderToDynamicSamplingContext ( dscString ) : undefined ;
29
32
const parentSpanId = traceState ? traceState . get ( SENTRY_TRACE_STATE_PARENT_SPAN_ID ) : undefined ;
30
- const sampled = traceFlags === TraceFlags . SAMPLED ;
33
+
34
+ const sampled = getSamplingDecision ( spanContext ) ;
31
35
32
36
return {
33
37
traceId,
@@ -78,32 +82,18 @@ export class SentryPropagator extends W3CBaggagePropagator {
78
82
*/
79
83
public extract ( context : Context , carrier : unknown , getter : TextMapGetter ) : Context {
80
84
const maybeSentryTraceHeader : string | string [ ] | undefined = getter . get ( carrier , SENTRY_TRACE_HEADER ) ;
81
- const maybeBaggageHeader = getter . get ( carrier , SENTRY_BAGGAGE_HEADER ) ;
85
+ const baggage = getter . get ( carrier , SENTRY_BAGGAGE_HEADER ) ;
82
86
83
- const sentryTraceHeader = maybeSentryTraceHeader
87
+ const sentryTrace = maybeSentryTraceHeader
84
88
? Array . isArray ( maybeSentryTraceHeader )
85
89
? maybeSentryTraceHeader [ 0 ]
86
90
: maybeSentryTraceHeader
87
91
: undefined ;
88
92
89
- const propagationContext = propagationContextFromHeaders ( sentryTraceHeader , maybeBaggageHeader ) ;
90
-
91
- // We store the DSC as OTEL trace state on the span context
92
- const traceState = makeTraceState ( {
93
- parentSpanId : propagationContext . parentSpanId ,
94
- dsc : propagationContext . dsc ,
95
- } ) ;
96
-
97
- const spanContext : SpanContext = {
98
- traceId : propagationContext . traceId ,
99
- spanId : propagationContext . parentSpanId || '' ,
100
- isRemote : true ,
101
- traceFlags : propagationContext . sampled === true ? TraceFlags . SAMPLED : TraceFlags . NONE ,
102
- traceState,
103
- } ;
93
+ const propagationContext = propagationContextFromHeaders ( sentryTrace , baggage ) ;
104
94
105
95
// Add remote parent span context,
106
- const ctxWithSpanContext = trace . setSpanContext ( context , spanContext ) ;
96
+ const ctxWithSpanContext = getContextWithRemoteActiveSpan ( context , { sentryTrace , baggage } ) ;
107
97
108
98
// Also update the scope on the context (to be sure this is picked up everywhere)
109
99
const scopes = getScopesFromContext ( ctxWithSpanContext ) ;
@@ -128,8 +118,13 @@ export class SentryPropagator extends W3CBaggagePropagator {
128
118
export function makeTraceState ( {
129
119
parentSpanId,
130
120
dsc,
131
- } : { parentSpanId ?: string ; dsc ?: Partial < DynamicSamplingContext > } ) : TraceState | undefined {
132
- if ( ! parentSpanId && ! dsc ) {
121
+ sampled,
122
+ } : {
123
+ parentSpanId ?: string ;
124
+ dsc ?: Partial < DynamicSamplingContext > ;
125
+ sampled ?: boolean ;
126
+ } ) : TraceState | undefined {
127
+ if ( ! parentSpanId && ! dsc && sampled !== false ) {
133
128
return undefined ;
134
129
}
135
130
@@ -140,7 +135,11 @@ export function makeTraceState({
140
135
? new TraceState ( ) . set ( SENTRY_TRACE_STATE_PARENT_SPAN_ID , parentSpanId )
141
136
: new TraceState ( ) ;
142
137
143
- return dscString ? traceStateBase . set ( SENTRY_TRACE_STATE_DSC , dscString ) : traceStateBase ;
138
+ const traceStateWithDsc = dscString ? traceStateBase . set ( SENTRY_TRACE_STATE_DSC , dscString ) : traceStateBase ;
139
+
140
+ // We also specifically want to store if this is sampled to be not recording,
141
+ // or unsampled (=could be either sampled or not)
142
+ return sampled === false ? traceStateWithDsc . set ( SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING , '1' ) : traceStateWithDsc ;
144
143
}
145
144
146
145
function getInjectionData ( context : Context ) : {
@@ -161,7 +160,7 @@ function getInjectionData(context: Context): {
161
160
dynamicSamplingContext,
162
161
traceId : spanContext . traceId ,
163
162
spanId : spanContext . spanId ,
164
- sampled : spanContext . traceFlags === TraceFlags . SAMPLED ,
163
+ sampled : getSamplingDecision ( spanContext ) ,
165
164
} ;
166
165
}
167
166
@@ -188,7 +187,7 @@ function getInjectionData(context: Context): {
188
187
dynamicSamplingContext,
189
188
traceId : spanContext . traceId ,
190
189
spanId : spanContext . spanId ,
191
- sampled : spanContext . traceFlags === TraceFlags . SAMPLED ,
190
+ sampled : getSamplingDecision ( spanContext ) ,
192
191
} ;
193
192
}
194
193
@@ -221,3 +220,79 @@ function getDynamicSamplingContext(
221
220
222
221
return undefined ;
223
222
}
223
+
224
+ function getContextWithRemoteActiveSpan (
225
+ ctx : Context ,
226
+ { sentryTrace, baggage } : Parameters < typeof continueTrace > [ 0 ] ,
227
+ ) : Context {
228
+ const propagationContext = propagationContextFromHeaders ( sentryTrace , baggage ) ;
229
+
230
+ // We store the DSC as OTEL trace state on the span context
231
+ const traceState = makeTraceState ( {
232
+ parentSpanId : propagationContext . parentSpanId ,
233
+ dsc : propagationContext . dsc ,
234
+ sampled : propagationContext . sampled ,
235
+ } ) ;
236
+
237
+ const spanContext : SpanContext = {
238
+ traceId : propagationContext . traceId ,
239
+ spanId : propagationContext . parentSpanId || '' ,
240
+ isRemote : true ,
241
+ traceFlags : propagationContext . sampled ? TraceFlags . SAMPLED : TraceFlags . NONE ,
242
+ traceState,
243
+ } ;
244
+
245
+ return trace . setSpanContext ( ctx , spanContext ) ;
246
+ }
247
+
248
+ /**
249
+ * Takes trace strings and propagates them as a remote active span.
250
+ * This should be used in addition to `continueTrace` in OTEL-powered environments.
251
+ */
252
+ export function continueTraceAsRemoteSpan < T > (
253
+ ctx : Context ,
254
+ options : Parameters < typeof continueTrace > [ 0 ] ,
255
+ callback : ( ) => T ,
256
+ ) : T {
257
+ const ctxWithSpanContext = getContextWithRemoteActiveSpan ( ctx , options ) ;
258
+
259
+ return context . with ( ctxWithSpanContext , callback ) ;
260
+ }
261
+
262
+ /**
263
+ * OpenTelemetry only knows about SAMPLED or NONE decision,
264
+ * but for us it is important to differentiate between unset and unsampled.
265
+ *
266
+ * Both of these are identified as `traceFlags === TracegFlags.NONE`,
267
+ * but we additionally look at a special trace state to differentiate between them.
268
+ */
269
+ export function getSamplingDecision ( spanContext : SpanContext ) : boolean | undefined {
270
+ const { traceFlags, traceState } = spanContext ;
271
+
272
+ const sampledNotRecording = traceState ? traceState . get ( SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING ) === '1' : false ;
273
+
274
+ // If trace flag is `SAMPLED`, we interpret this as sampled
275
+ // If it is `NONE`, it could mean either it was sampled to be not recorder, or that it was not sampled at all
276
+ // For us this is an important difference, sow e look at the SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING
277
+ // to identify which it is
278
+ if ( traceFlags === TraceFlags . SAMPLED ) {
279
+ return true ;
280
+ }
281
+
282
+ if ( sampledNotRecording ) {
283
+ return false ;
284
+ }
285
+
286
+ // Fall back to DSC as a last resort, that may also contain `sampled`...
287
+ const dscString = traceState ? traceState . get ( SENTRY_TRACE_STATE_DSC ) : undefined ;
288
+ const dsc = dscString ? baggageHeaderToDynamicSamplingContext ( dscString ) : undefined ;
289
+
290
+ if ( dsc ?. sampled === 'true' ) {
291
+ return true ;
292
+ }
293
+ if ( dsc ?. sampled === 'false' ) {
294
+ return false ;
295
+ }
296
+
297
+ return undefined ;
298
+ }
0 commit comments