fix(tracing): Fix unsampled/deferred trace propagation in span streaming#6757
fix(tracing): Fix unsampled/deferred trace propagation in span streaming#6757sentrivana wants to merge 14 commits into
Conversation
Codecov Results 📊✅ 90184 passed | ⏭️ 6302 skipped | Total: 96486 | Pass Rate: 93.47% | Execution Time: 313m 36s 📊 Comparison with Base Branch
All tests are passing successfully. ✅ Patch coverage is 96.67%. Project has 2443 uncovered lines. Files with missing lines (1)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
- Coverage 89.76% 89.75% -0.01%
==========================================
Files 192 192 —
Lines 23823 23833 +10
Branches 8226 8232 +6
==========================================
+ Hits 21384 21390 +6
- Misses 2439 2443 +4
- Partials 1351 1348 -3Generated by Codecov Action |
| parent_span_id: "Optional[str]" = None, | ||
| parent_sampled: "Optional[bool]" = None, | ||
| baggage: "Optional[Baggage]" = None, | ||
| sampled: "Optional[bool]" = False, |
There was a problem hiding this comment.
Default sampled=False produces contradictory trace propagation headers for ignored child spans of sampled parents
When an ignored child span (is_ignored_span=True) is created inside a sampled real StreamedSpan, sampled is not passed and defaults to False. The outgoing sentry-trace header then carries -0 (explicitly unsampled) while the baggage header, derived from _segment._get_baggage() on the real sampled segment, carries sentry-sampled=true. Downstream services receiving these contradictory headers will typically honor the traceparent's -0 and drop the trace, breaking continuity of a sampled trace.
Evidence
scope.pycreatesNoOpStreamedSpan(segment=parent_span._segment, ..., unsampled_reason="ignored")without passingsampledwhenis_ignored_spanis true for a child of a realStreamedSpan(scope.py ~line 1350).- The default
sampled=Falseis stored asself._sampled(line 634 of traces.py). stdlib.pypasses the ignored HTTP span explicitly toiter_trace_propagation_headers(span=span)(stdlib.py ~line 160).iter_trace_propagation_headerscallsspan._iter_headers()becauseNoOpStreamedSpanis not an instance ofNoOpSpanandhas_tracing_enabledis True (scope.py ~line 703).- Inside
_iter_headers(),self._to_traceparent()usesself.sampled = False→ appends-0, butself._segment._get_baggage()uses the real sampledStreamedSpansegment whosesampledisTrue→ baggage includessentry-sampled=true.
Identified by Warden code-review · RXZ-PUR
There was a problem hiding this comment.
I will verify but we might need to take the segment's sampled flag into account here, instead of the child. It used to not make a difference, but now it does.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 46b410a. Configure here.
| trace_id=propagation_context.trace_id, | ||
| parent_span_id=propagation_context.parent_span_id, | ||
| parent_sampled=propagation_context.parent_sampled, | ||
| baggage=propagation_context.baggage, |
There was a problem hiding this comment.
Ignored spans wrong trace propagation
Medium Severity
Segment-level ignored spans create an active NoOpStreamedSpan without a propagation sampled value, so it defaults to False. Outgoing headers now come from that noop via _to_traceparent, which uses sampled, not parent_sampled. Downstream traces can be marked unsampled even when the incoming trace was sampled or should stay deferred.
Reviewed by Cursor Bugbot for commit 46b410a. Configure here.
|
Converting to draft to address bot comments |


Description
The SDK didn't behave correctly in some trace propagation scenarios in stream mode.
The current SDK design was emitting headers with a deferred sampling decision if the sampling decision was negative (whether inherited or not), because it was falling back to TWP behavior too eagerly.
Here's how it should be working. In the following, tracing enabled means either a
traces_sample_rateor atraces_sampleris defined. Tracing disabled means both areNoneand tracing without performance is active.There are three kinds of sampling decisions: sampled (
1), unsampled (0), and deferred (``), meaning no decision.The fix:
NoOpStreamedSpansnow carry their own trace and span ID information as well as the current propagation data (a snapshot of the propagation context, likeStreamedSpans) and use it to emit the correct headers.Issues
Closes #6755
Reminders
uv run ruff.feat:,fix:,ref:,meta:)