From 1d2519c02ad468c115b36ce62fe8915c97ba3b1e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 2 Jul 2026 10:00:51 +0200 Subject: [PATCH] fix(tracing): Correctly propagate from NoOpStreamedSpans --- sentry_sdk/scope.py | 8 ++++++-- sentry_sdk/traces.py | 16 +++++++-------- tests/tracing/test_http_headers.py | 32 ++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 4fd22714cf..191d5893c1 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1298,7 +1298,6 @@ def start_streamed_span( # active span, this is a segment if parent_span is None: propagation_context = self.get_active_propagation_context() - if is_ignored_span(name, attributes): return NoOpStreamedSpan( scope=self, @@ -1317,6 +1316,7 @@ def start_streamed_span( if sampled is False: return NoOpStreamedSpan( scope=self, + trace_id=propagation_context.trace_id, unsampled_reason=outcome, ) @@ -1338,11 +1338,15 @@ def start_streamed_span( with new_scope(): if is_ignored_span(name, attributes): return NoOpStreamedSpan( + trace_id=parent_span.trace_id, unsampled_reason="ignored", ) if isinstance(parent_span, NoOpStreamedSpan): - return NoOpStreamedSpan(unsampled_reason=parent_span._unsampled_reason) + return NoOpStreamedSpan( + trace_id=parent_span.trace_id, + unsampled_reason=parent_span._unsampled_reason, + ) return StreamedSpan( name=name, diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 5ee7e8460b..46f07127fa 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -610,18 +610,24 @@ def _to_json(self) -> "SpanJSON": class NoOpStreamedSpan(StreamedSpan): __slots__ = ( + "_trace_id", + "_span_id", "_finished", "_unsampled_reason", ) def __init__( self, + trace_id: "Optional[str]" = None, unsampled_reason: "Optional[str]" = None, scope: "Optional[sentry_sdk.Scope]" = None, ) -> None: self._scope = scope # type: ignore[assignment] - self._unsampled_reason = unsampled_reason + self._trace_id = trace_id + self._span_id = None + + self._unsampled_reason = unsampled_reason self._finished = False self._start() @@ -716,14 +722,6 @@ def name(self, value: str) -> None: def active(self) -> bool: return True - @property - def span_id(self) -> str: - return "0000000000000000" - - @property - def trace_id(self) -> str: - return "00000000000000000000000000000000" - @property def sampled(self) -> "Optional[bool]": return False diff --git a/tests/tracing/test_http_headers.py b/tests/tracing/test_http_headers.py index 6a8467101e..7957dd38cf 100644 --- a/tests/tracing/test_http_headers.py +++ b/tests/tracing/test_http_headers.py @@ -2,6 +2,7 @@ import pytest +import sentry_sdk from sentry_sdk.tracing import Transaction from sentry_sdk.tracing_utils import extract_sentrytrace_data @@ -26,6 +27,37 @@ def test_to_traceparent(sampled): assert parts[2] == "1" if sampled is True else "0" # sampled +@pytest.mark.parametrize("sampled", ["1", "0", ""]) +def test_to_traceparent_span_streaming(sentry_init, sampled): + sentry_init( + # parent sampling decision takes precedence over traces_sample_rate + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream"}, + ) + + trace_id = "12312012123120121231201212312012" + + sentry_sdk.traces.continue_trace( + { + "sentry-trace": f"{trace_id}-b7ad6b7169203331-{sampled}", + } + ) + + with sentry_sdk.traces.start_span(name="/interactions/other-dogs/new-dog") as span: + pass + + traceparent = span._to_traceparent() + + parts = traceparent.split("-") + assert parts[0] == trace_id + assert parts[1] == span.span_id + if sampled == "": + # we'll sample this since our traces_sample_rate kicks in + assert parts[2] == "1" + else: + assert parts[2] == sampled + + @pytest.mark.parametrize("sampling_decision", [True, False]) def test_sentrytrace_extraction(sampling_decision): sentrytrace_header = "12312012123120121231201212312012-0415201309082013-{}".format(