1- import type { Attributes , Context , Span } from '@opentelemetry/api' ;
1+ import type { Attributes , Context , Span , TraceState as TraceStateInterface } from '@opentelemetry/api' ;
22import { SpanKind } from '@opentelemetry/api' ;
33import { isSpanContextValid , trace } from '@opentelemetry/api' ;
44import { TraceState } from '@opentelemetry/core' ;
@@ -40,16 +40,8 @@ export class SentrySampler implements Sampler {
4040 const parentSpan = trace . getSpan ( context ) ;
4141 const parentContext = parentSpan ?. spanContext ( ) ;
4242
43- let traceState = parentContext ?. traceState || new TraceState ( ) ;
44-
45- // We always keep the URL on the trace state, so we can access it in the propagator
46- const url = spanAttributes [ SEMATTRS_HTTP_URL ] ;
47- if ( url && typeof url === 'string' ) {
48- traceState = traceState . set ( SENTRY_TRACE_STATE_URL , url ) ;
49- }
50-
5143 if ( ! hasTracingEnabled ( options ) ) {
52- return { decision : SamplingDecision . NOT_RECORD , traceState } ;
44+ return wrapSamplingDecision ( { decision : undefined , context , spanAttributes } ) ;
5345 }
5446
5547 // If we have a http.client span that has no local parent, we never want to sample it
@@ -59,7 +51,7 @@ export class SentrySampler implements Sampler {
5951 spanAttributes [ SEMATTRS_HTTP_METHOD ] &&
6052 ( ! parentSpan || parentContext ?. isRemote )
6153 ) {
62- return { decision : SamplingDecision . NOT_RECORD , traceState } ;
54+ return wrapSamplingDecision ( { decision : undefined , context , spanAttributes } ) ;
6355 }
6456
6557 const parentSampled = parentSpan ? getParentSampled ( parentSpan , traceId , spanName ) : undefined ;
@@ -76,7 +68,7 @@ export class SentrySampler implements Sampler {
7668 mutableSamplingDecision ,
7769 ) ;
7870 if ( ! mutableSamplingDecision . decision ) {
79- return { decision : SamplingDecision . NOT_RECORD , traceState : traceState } ;
71+ return wrapSamplingDecision ( { decision : undefined , context , spanAttributes } ) ;
8072 }
8173
8274 const [ sampled , sampleRate ] = sampleSpan ( options , {
@@ -96,25 +88,22 @@ export class SentrySampler implements Sampler {
9688 const method = `${ spanAttributes [ SEMATTRS_HTTP_METHOD ] } ` . toUpperCase ( ) ;
9789 if ( method === 'OPTIONS' || method === 'HEAD' ) {
9890 DEBUG_BUILD && logger . log ( `[Tracing] Not sampling span because HTTP method is '${ method } ' for ${ spanName } ` ) ;
91+
9992 return {
100- decision : SamplingDecision . NOT_RECORD ,
93+ ... wrapSamplingDecision ( { decision : SamplingDecision . NOT_RECORD , context , spanAttributes } ) ,
10194 attributes,
102- traceState : traceState . set ( SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING , '1' ) ,
10395 } ;
10496 }
10597
10698 if ( ! sampled ) {
10799 return {
108- decision : SamplingDecision . NOT_RECORD ,
100+ ... wrapSamplingDecision ( { decision : SamplingDecision . NOT_RECORD , context , spanAttributes } ) ,
109101 attributes,
110- traceState : traceState . set ( SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING , '1' ) ,
111102 } ;
112103 }
113-
114104 return {
115- decision : SamplingDecision . RECORD_AND_SAMPLED ,
105+ ... wrapSamplingDecision ( { decision : SamplingDecision . RECORD_AND_SAMPLED , context , spanAttributes } ) ,
116106 attributes,
117- traceState,
118107 } ;
119108 }
120109
@@ -152,3 +141,43 @@ function getParentSampled(parentSpan: Span, traceId: string, spanName: string):
152141
153142 return undefined ;
154143}
144+
145+ /**
146+ * Wrap a sampling decision with data that Sentry needs to work properly with it.
147+ * If you pass `decision: undefined`, it will be treated as `NOT_RECORDING`, but in contrast to passing `NOT_RECORDING`
148+ * it will not propagate this decision to downstream Sentry SDKs.
149+ */
150+ export function wrapSamplingDecision ( {
151+ decision,
152+ context,
153+ spanAttributes,
154+ } : { decision : SamplingDecision | undefined ; context : Context ; spanAttributes : SpanAttributes } ) : SamplingResult {
155+ const traceState = getBaseTraceState ( context , spanAttributes ) ;
156+
157+ // If the decision is undefined, we treat it as NOT_RECORDING, but we don't propagate this decision to downstream SDKs
158+ // Which is done by not setting `SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING` traceState
159+ if ( decision == undefined ) {
160+ return { decision : SamplingDecision . NOT_RECORD , traceState } ;
161+ }
162+
163+ if ( decision === SamplingDecision . NOT_RECORD ) {
164+ return { decision, traceState : traceState . set ( SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING , '1' ) } ;
165+ }
166+
167+ return { decision, traceState } ;
168+ }
169+
170+ function getBaseTraceState ( context : Context , spanAttributes : SpanAttributes ) : TraceStateInterface {
171+ const parentSpan = trace . getSpan ( context ) ;
172+ const parentContext = parentSpan ?. spanContext ( ) ;
173+
174+ let traceState = parentContext ?. traceState || new TraceState ( ) ;
175+
176+ // We always keep the URL on the trace state, so we can access it in the propagator
177+ const url = spanAttributes [ SEMATTRS_HTTP_URL ] ;
178+ if ( url && typeof url === 'string' ) {
179+ traceState = traceState . set ( SENTRY_TRACE_STATE_URL , url ) ;
180+ }
181+
182+ return traceState ;
183+ }
0 commit comments