@@ -88,33 +88,52 @@ export class SentrySampler implements Sampler {
8888 // Non-root-spans simply inherit the sampling decision from their parent.
8989 if ( ! isRootSpan ) {
9090 if ( this . _isSpanStreaming ) {
91+ // We only record client outcomes for child spans when streaming
92+ if ( ! parentSampled ) {
93+ const parentSegmentIgnored = parentContext ?. traceState ?. get ( SENTRY_TRACE_STATE_SEGMENT_IGNORED ) === '1' ;
94+ this . _client . recordDroppedEvent ( parentSegmentIgnored ? 'ignored' : 'sample_rate' , 'span' ) ;
95+ return wrapSamplingDecision ( {
96+ decision : SamplingDecision . NOT_RECORD ,
97+ context,
98+ spanAttributes,
99+ } ) ;
100+ }
101+
91102 // `ignoreSpans` is only applied at span start for streamed spans.
92103 // Static spans/transactions get filtered at transaction end.
93- // Likewise, we only record client outcomes for child spans when streaming
94- if ( parentSampled ) {
95- if ( ignoreSpans ?. length ) {
96- const { description : inferredChildName , op : childOp } = inferSpanData ( spanName , spanAttributes , spanKind ) ;
97- if (
98- shouldIgnoreSpan (
99- { description : inferredChildName , op : spanAttributes [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] ?? childOp } ,
100- ignoreSpans ,
101- )
102- ) {
103- this . _client . recordDroppedEvent ( 'ignored' , 'span' ) ;
104- return wrapSamplingDecision ( {
105- decision : SamplingDecision . NOT_RECORD ,
106- context,
107- spanAttributes,
108- ignoredChildSpan : true ,
109- } ) ;
110- }
104+ const { description : inferredChildName , op : childOp } = inferSpanData ( spanName , spanAttributes , spanKind ) ;
105+ if ( ignoreSpans ?. length ) {
106+ if (
107+ shouldIgnoreSpan (
108+ { description : inferredChildName , op : spanAttributes [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] ?? childOp } ,
109+ ignoreSpans ,
110+ )
111+ ) {
112+ this . _client . recordDroppedEvent ( 'ignored' , 'span' ) ;
113+ return wrapSamplingDecision ( {
114+ decision : SamplingDecision . NOT_RECORD ,
115+ context,
116+ spanAttributes,
117+ ignoredChildSpan : true ,
118+ } ) ;
111119 }
112120 }
113121
114- if ( ! parentSampled ) {
115- const parentSegmentIgnored = parentContext ?. traceState ?. get ( SENTRY_TRACE_STATE_SEGMENT_IGNORED ) === '1' ;
116- this . _client . recordDroppedEvent ( parentSegmentIgnored ? 'ignored' : 'sample_rate' , 'span' ) ;
117- }
122+ // For streamed spans, we need to propagate the inferred `sentry.op` via the SamplingResult attributes
123+ // so that OTel sets it on the span at creation time. This is necessary because the streaming serialization
124+ // path (`spanToStreamedSpanJSON`) passes raw OTel attributes through without inference, unlike the
125+ // non-streamed path where the `SentrySpanExporter` infers the op during export.
126+ const inferredOp = spanAttributes [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] ?? childOp ;
127+ return {
128+ ...wrapSamplingDecision ( {
129+ decision : SamplingDecision . RECORD_AND_SAMPLED ,
130+ context,
131+ spanAttributes,
132+ } ) ,
133+ attributes : {
134+ ...( inferredOp ? { [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : inferredOp } : { } ) ,
135+ } ,
136+ } ;
118137 }
119138
120139 return wrapSamplingDecision ( {
@@ -224,8 +243,7 @@ export class SentrySampler implements Sampler {
224243 attributes : {
225244 // We set the sample rate on the span when a local sample rate was applied to better understand how traces were sampled in Sentry
226245 [ SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE ] : localSampleRateWasApplied ? sampleRate : undefined ,
227- // For streamed spans, propagate the inferred op onto the span so it's included in the serialized attributes.
228- // Non-streamed spans get the op set during export in the SentrySpanExporter.
246+ // See comment above in the `!isRootSpan` branch for why we propagate the op for streamed spans.
229247 ...( this . _isSpanStreaming && op ? { [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : op } : { } ) ,
230248 } ,
231249 } ;
0 commit comments