From ffdd6ec83aede12a442aa550c94a84a1b506557a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 3 Jul 2026 09:12:53 +0200 Subject: [PATCH 01/15] . --- sentry_sdk/traces.py | 6 ++++++ tests/tracing/test_http_headers.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 5ee7e8460b..ec3b8978b0 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -718,10 +718,16 @@ def active(self) -> bool: @property def span_id(self) -> str: + # Note: this dummy span ID should never actually surface in anything + # user-facing. If a no-op span is active, we use the propagation context + # to get trace propagation data. return "0000000000000000" @property def trace_id(self) -> str: + # Note: this dummy trace ID should never actually surface in anything + # user-facing. If a no-op span is active, we use the propagation context + # to get trace propagation data. return "00000000000000000000000000000000" @property diff --git a/tests/tracing/test_http_headers.py b/tests/tracing/test_http_headers.py index 1914347b2b..ac00d99726 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,34 @@ 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"): + traceparent = sentry_sdk.get_traceparent() + + parts = traceparent.split("-") + assert parts[0] == trace_id + assert parts[1] != "0000000000000000" + if sampled == "": + assert len(parts) == 2 + else: + assert parts[2] == sampled + + @pytest.mark.parametrize("sampling_decision", [True, False]) def test_sentrytrace_extraction(sampling_decision): sentrytrace_header = "12312012123120121231201212312012-0415201309082013-{}".format( From ac59d4347c7f38bbe904d6e3c4bbfd72b52d153e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 10:22:04 +0200 Subject: [PATCH 02/15] fix(tracing): Fix unsampled/deferred trace propagation in span streaming --- sentry_sdk/scope.py | 35 ++++- sentry_sdk/traces.py | 41 +++--- sentry_sdk/tracing_utils.py | 13 +- tests/tracing/test_http_headers.py | 28 ---- tests/tracing/test_span_streaming.py | 212 +++++++++++++++++++++++---- 5 files changed, 238 insertions(+), 91 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 4fd22714cf..a78ab24c83 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -597,7 +597,7 @@ def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return traceparent from there - if span_streaming and type(self.streamed_span) is StreamedSpan: + if span_streaming and self.streamed_span is not None: return self.streamed_span._to_traceparent() elif not span_streaming and self.span is not None: return self.span._to_traceparent() @@ -617,7 +617,7 @@ def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return baggage from there - if span_streaming and type(self.streamed_span) is StreamedSpan: + if span_streaming and self.streamed_span is not None: return self.streamed_span._to_baggage() elif not span_streaming and self.span is not None: return self.span._to_baggage() @@ -632,7 +632,7 @@ def get_trace_context(self) -> "Dict[str, Any]": if ( has_tracing_enabled(self.get_client().options) and self._span is not None - and not isinstance(self._span, (NoOpStreamedSpan, NoOpSpan)) + and not isinstance(self._span, NoOpSpan) ): return self._span._get_trace_context() @@ -703,7 +703,7 @@ def iter_trace_propagation_headers( if ( has_tracing_enabled(client.options) and span is not None - and not isinstance(span, (NoOpStreamedSpan, NoOpSpan)) + and not isinstance(span, NoOpSpan) ): for header in span._iter_headers(): yield header @@ -1295,13 +1295,18 @@ def start_streamed_span( parent_span = self.streamed_span # If no eligible parent_span was provided and there is no currently - # active span, this is a segment + # active span, this is a new segment if parent_span is None: propagation_context = self.get_active_propagation_context() if is_ignored_span(name, attributes): return NoOpStreamedSpan( scope=self, + segment=None, + trace_id=propagation_context.trace_id, + parent_span_id=propagation_context.parent_span_id, + parent_sampled=propagation_context.parent_sampled, + baggage=propagation_context.baggage, unsampled_reason="ignored", ) @@ -1314,9 +1319,15 @@ def start_streamed_span( if sample_rate is not None: self._update_sample_rate(sample_rate) - if sampled is False: + if sampled is False or sampled is None: return NoOpStreamedSpan( scope=self, + segment=None, + trace_id=propagation_context.trace_id, + parent_span_id=propagation_context.parent_span_id, + parent_sampled=propagation_context.parent_sampled, + baggage=propagation_context.baggage, + sampled=sampled, unsampled_reason=outcome, ) @@ -1338,11 +1349,21 @@ def start_streamed_span( with new_scope(): if is_ignored_span(name, attributes): return NoOpStreamedSpan( + segment=parent_span._segment, + trace_id=parent_span.trace_id, + parent_span_id=parent_span.span_id, + parent_sampled=parent_span.sampled, unsampled_reason="ignored", ) if isinstance(parent_span, NoOpStreamedSpan): - return NoOpStreamedSpan(unsampled_reason=parent_span._unsampled_reason) + return NoOpStreamedSpan( + segment=parent_span._segment, + trace_id=parent_span.trace_id, + parent_span_id=parent_span.span_id, + parent_sampled=parent_span.sampled, + unsampled_reason=parent_span._unsampled_reason + ) return StreamedSpan( name=name, diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index ec3b8978b0..85392eb14b 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -610,15 +610,37 @@ def _to_json(self) -> "SpanJSON": class NoOpStreamedSpan(StreamedSpan): __slots__ = ( + "_trace_id", + "_span_id", + "_sampled", + "_segment", "_finished", "_unsampled_reason", ) def __init__( self, + segment: "Optional[StreamedSpan]" = None, + trace_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + parent_sampled: "Optional[bool]" = None, + baggage: "Optional[Baggage]" = None, + sampled: "Optional[bool]" = False, unsampled_reason: "Optional[str]" = None, scope: "Optional[sentry_sdk.Scope]" = None, ) -> None: + self._span_id: "Optional[str]" = None + + self._sampled = sampled + self._segment = segment or self + + self._trace_id: "Optional[str]" = trace_id + self._parent_span_id = parent_span_id + self._parent_sampled = parent_sampled + self._baggage = baggage + self._sample_rand = None + self._sample_rate = None + self._scope = scope # type: ignore[assignment] self._unsampled_reason = unsampled_reason @@ -693,9 +715,6 @@ def set_attributes(self, attributes: "Attributes") -> None: def remove_attribute(self, key: str) -> None: pass - def _is_segment(self) -> bool: - return self._scope is not None - @property def status(self) -> "str": return SpanStatus.OK.value @@ -716,23 +735,9 @@ def name(self, value: str) -> None: def active(self) -> bool: return True - @property - def span_id(self) -> str: - # Note: this dummy span ID should never actually surface in anything - # user-facing. If a no-op span is active, we use the propagation context - # to get trace propagation data. - return "0000000000000000" - - @property - def trace_id(self) -> str: - # Note: this dummy trace ID should never actually surface in anything - # user-facing. If a no-op span is active, we use the propagation context - # to get trace propagation data. - return "00000000000000000000000000000000" - @property def sampled(self) -> "Optional[bool]": - return False + return self._sampled @property def start_timestamp(self) -> "Optional[datetime]": diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index c2e34a795b..cfcec66949 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -857,7 +857,8 @@ def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": options = client.options or {} sentry_items["trace_id"] = segment.trace_id - sentry_items["sample_rand"] = f"{segment._sample_rand:.6f}" # noqa: E231 + if segment._sample_rand is not None: + sentry_items["sample_rand"] = f"{segment._sample_rand:.6f}" if options.get("environment"): sentry_items["environment"] = options["environment"] @@ -873,8 +874,8 @@ def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": if ( segment.get_attributes().get("sentry.span.source") not in LOW_QUALITY_SEGMENT_SOURCES - ) and segment._name: - sentry_items["transaction"] = segment._name + ) and segment.name: + sentry_items["transaction"] = segment.name if segment._sample_rate is not None: sentry_items["sample_rate"] = str(segment._sample_rate) @@ -1542,11 +1543,11 @@ def _make_sampling_decision( """ client = sentry_sdk.get_client() - if not has_tracing_enabled(client.options): - return False, None, None, None - propagation_context = scope.get_active_propagation_context() + if not has_tracing_enabled(client.options): + return propagation_context.parent_sampled, None, None, None + sample_rand = None if propagation_context.baggage is not None: sample_rand = propagation_context.baggage._sample_rand() diff --git a/tests/tracing/test_http_headers.py b/tests/tracing/test_http_headers.py index ac00d99726..3530f6f895 100644 --- a/tests/tracing/test_http_headers.py +++ b/tests/tracing/test_http_headers.py @@ -27,34 +27,6 @@ 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"): - traceparent = sentry_sdk.get_traceparent() - - parts = traceparent.split("-") - assert parts[0] == trace_id - assert parts[1] != "0000000000000000" - if sampled == "": - assert len(parts) == 2 - else: - assert parts[2] == sampled - - @pytest.mark.parametrize("sampling_decision", [True, False]) def test_sentrytrace_extraction(sampling_decision): sentrytrace_header = "12312012123120121231201212312012-0415201309082013-{}".format( diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index e90e56d779..62bcb6cf60 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -744,8 +744,8 @@ def test_continue_trace_unsampled(sentry_init, capture_items): assert span.sampled is False assert span.name == "" - assert span.trace_id == "00000000000000000000000000000000" - assert span.span_id == "0000000000000000" + assert span.trace_id == trace_id + assert span.span_id != "0000000000000000" sentry_sdk.get_client().flush() spans = [item.payload for item in items] @@ -908,65 +908,213 @@ def test_continue_trace_no_sample_rand(sentry_init, capture_items): assert segment["trace_id"] == trace_id -def test_outgoing_traceparent_and_baggage(sentry_init, capture_envelopes): +@pytest.mark.parametrize( + "traces_sample_rate", + ( + 0.0, + 1.0, + # traces_sample_rate=None means tracing without performance: don't make + # any sampling decisions; defer downstream + None, + ) +) +def test_outgoing_traceparent_and_baggage_head_sdk(sentry_init, traces_sample_rate): sentry_init( - traces_sample_rate=1.0, + traces_sample_rate=traces_sample_rate, _experiments={"trace_lifecycle": "stream"}, ) - sentry_sdk.traces.new_trace() + if traces_sample_rate == 0.0: + expected_sampled = False + elif traces_sample_rate == 1.0: + expected_sampled = True + elif traces_sample_rate is None: + expected_sampled = None with sentry_sdk.traces.start_span(name="span") as span: - assert span.sampled is True + assert span.sampled is expected_sampled trace_id = span.trace_id - span_id = span.span_id traceparent = sentry_sdk.get_traceparent() - assert traceparent == f"{trace_id}-{span_id}-1" + + if expected_sampled is True: + span_id = span.span_id + assert traceparent == f"{trace_id}-{span_id}-1" + elif expected_sampled is False: + span_id = span.span_id + assert traceparent == f"{trace_id}-{span_id}-0" + elif expected_sampled is None: + span_id = sentry_sdk.get_isolation_scope().get_active_propagation_context().span_id + assert traceparent == f"{trace_id}-{span_id}" baggage = sentry_sdk.get_baggage() baggage_items = dict(tuple(item.split("=")) for item in baggage.split(",")) + assert "sentry-trace_id" in baggage_items assert baggage_items["sentry-trace_id"] == trace_id - assert "sentry-sampled" in baggage_items - assert baggage_items["sentry-sampled"] == "true" + if expected_sampled is None: + assert "sentry-sampled" not in baggage_items + else: + assert "sentry-sampled" in baggage_items -def test_outgoing_traceparent_and_baggage_when_noop_span_is_active( - sentry_init, capture_envelopes -): + if expected_sampled is True: + assert baggage_items["sentry-sampled"] == "true" + elif expected_sampled is False: + assert baggage_items["sentry-sampled"] == "false" + + +@pytest.mark.parametrize( + "traces_sample_rate,parent_sampled", + ( + (0.0, False), + (0.0, True), + (1.0, False), + (1.0, True), + # traces_sample_rate=None means tracing without performance: don't make + # any sampling decisions on our end, propagate existing ones + (None, False), + (None, True), + ) +) +def test_outgoing_traceparent_and_baggage_incoming_trace(sentry_init, traces_sample_rate, parent_sampled): + """The SDK respects a positive/negative incoming sampling decision.""" sentry_init( - traces_sample_rate=1.0, - _experiments={ - "trace_lifecycle": "stream", - "ignore_spans": ["ignored"], - }, + traces_sample_rate=traces_sample_rate, + _experiments={"trace_lifecycle": "stream"}, ) - sentry_sdk.traces.new_trace() + trace_id = "0af7651916cd43dd8448eb211c80319c" + parent_span_id = "b7ad6b7169203331" - propagation_context = ( - sentry_sdk.get_current_scope().get_active_propagation_context() + incoming_baggage = { + "sentry-trace_id": trace_id, + } + + if parent_sampled is True: + incoming_sentry_trace = f"{trace_id}-{parent_span_id}-1" + incoming_baggage.update({ + "sentry-sample_rate": "0.75", + "sentry-sample_rand": "0.5", + "sentry-sampled": "true", + }) + elif parent_sampled is False: + incoming_sentry_trace = f"{trace_id}-{parent_span_id}-0" + incoming_baggage.update({ + "sentry-sample_rate": "0.75", + "sentry-sample_rand": "0.8", + "sentry-sampled": "false", + }) + + sentry_sdk.traces.continue_trace( + { + "sentry-trace": incoming_sentry_trace, + "baggage": ",".join(sorted([f"{k}={v}" for k,v in incoming_baggage.items()])), + } ) - propagation_trace_id = propagation_context.trace_id - propagation_span_id = propagation_context.span_id - with sentry_sdk.traces.start_span(name="ignored") as span: - assert span.sampled is False + with sentry_sdk.traces.start_span(name="span") as span: + assert span.sampled is parent_sampled - noop_trace_id = span.trace_id - noop_span_id = span.span_id + traceparent = sentry_sdk.get_traceparent() + + if traces_sample_rate is None: + # TWP: Span ID will come from the propagation context in this case + # (it doesn't even make sense to start a span explicitly as we do in + # this test since tracing is turned off, but nothing should break + # either) + span_id = sentry_sdk.get_current_scope().get_active_propagation_context().span_id + assert traceparent == f"{trace_id}-{span_id}" + else: + span_id = span.span_id + if parent_sampled is True: + assert traceparent == f"{trace_id}-{span_id}-1" + elif parent_sampled is False: + assert traceparent == f"{trace_id}-{span_id}-0" + + # As we've received incoming baggage, we mustn't modify it ourselves and + # have to propagate it as-is, unless we + baggage = sentry_sdk.get_baggage() + baggage_items = dict(tuple(item.split("=")) for item in baggage.split(",")) + assert baggage_items == incoming_baggage + + +@pytest.mark.parametrize( + "traces_sample_rate", + ( + 0.0, + 1.0, + # traces_sample_rate=None means tracing without performance: don't make + # any sampling decisions on our end, propagate existing ones + None, + ) +) +def test_outgoing_traceparent_and_baggage_incoming_trace_deferred(sentry_init, traces_sample_rate): + """The SDK handles a deferred incoming sampling decision correctly.""" + sentry_init( + traces_sample_rate=traces_sample_rate, + _experiments={"trace_lifecycle": "stream"}, + ) + + if traces_sample_rate == 0.0: + expected_sampled = False + if traces_sample_rate == 1.0: + expected_sampled = True + if traces_sample_rate is None: + expected_sampled = None + + trace_id = "0af7651916cd43dd8448eb211c80319c" + parent_span_id = "b7ad6b7169203331" + + incoming_baggage = { + "sentry-trace_id": trace_id, + "sentry-sample_rand": "0.500000" + } + + sentry_sdk.traces.continue_trace( + { + "sentry-trace": f"{trace_id}-{parent_span_id}-", + "baggage": ",".join(sorted([f"{k}={v}" for k,v in incoming_baggage.items()])), + } + ) + + with sentry_sdk.traces.start_span(name="span") as span: + assert span.sampled is expected_sampled traceparent = sentry_sdk.get_traceparent() - assert traceparent != f"{noop_trace_id}-{noop_span_id}" - assert traceparent == f"{propagation_trace_id}-{propagation_span_id}" + + if traces_sample_rate is None: + # TWP: Span ID will come from the propagation context in this case + # (it doesn't even make sense to start a span explicitly as we do in + # this test since tracing is turned off, but nothing should break + # either) + span_id = sentry_sdk.get_current_scope().get_active_propagation_context().span_id + else: + span_id = span.span_id + + if expected_sampled is True: + assert traceparent == f"{trace_id}-{span_id}-1" + elif expected_sampled is False: + assert traceparent == f"{trace_id}-{span_id}-0" + elif expected_sampled is None: + assert traceparent == f"{trace_id}-{span_id}" baggage = sentry_sdk.get_baggage() baggage_items = dict(tuple(item.split("=")) for item in baggage.split(",")) - assert "sentry-trace_id" in baggage_items - assert baggage_items["sentry-trace_id"] != noop_trace_id - assert baggage_items["sentry-trace_id"] == propagation_trace_id + + if traces_sample_rate is not None: + # If our sample rate is not None, we're expected to have made + # a sampling decision + expected_baggage = incoming_baggage + expected_baggage.update({ + "sentry-sample_rate": str(traces_sample_rate), + "sentry-sampled": "true" if expected_sampled else "false", + }) + assert baggage_items == expected_baggage + else: + # If tracing is off, we should have deferred the decision further + assert baggage_items == incoming_baggage def test_set_span_status(sentry_init, capture_items): From 60c7f817573e49d9ba9feaa804bbf94d56d813b6 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 11:22:49 +0200 Subject: [PATCH 03/15] . --- sentry_sdk/tracing_utils.py | 12 +++++++++--- tests/tracing/test_span_streaming.py | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index cfcec66949..69a78cfffb 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -664,8 +664,7 @@ def _fill_sample_rand(self) -> None: # Get the sample rate and compute the transformation that will map the random value # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). - sample_rate = try_convert(float, self.baggage.sentry_items.get("sample_rate")) - lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) + lower, upper = _sample_rand_range(self.parent_sampled, self._sample_rate()) try: sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) @@ -689,6 +688,13 @@ def _sample_rand(self) -> "Optional[str]": return self.baggage.sentry_items.get("sample_rand") + def _sample_rate(self) -> "Optional[float]": + """Convenience method to get the sample_ value from the baggage.""" + if self.baggage is None: + return None + + return try_convert(float, self.baggage.sentry_items.get("sample_rate")) + class Baggage: """ @@ -1573,7 +1579,7 @@ def _make_sampling_decision( sample_rate = client.options["traces_sampler"](sampling_context) else: if propagation_context.parent_sampled is not None: - sample_rate = propagation_context.parent_sampled + sample_rate = propagation_context._sample_rate() else: sample_rate = client.options["traces_sample_rate"] diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index 62bcb6cf60..ab99885c4b 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -996,14 +996,14 @@ def test_outgoing_traceparent_and_baggage_incoming_trace(sentry_init, traces_sam incoming_sentry_trace = f"{trace_id}-{parent_span_id}-1" incoming_baggage.update({ "sentry-sample_rate": "0.75", - "sentry-sample_rand": "0.5", + "sentry-sample_rand": "0.500000", "sentry-sampled": "true", }) elif parent_sampled is False: incoming_sentry_trace = f"{trace_id}-{parent_span_id}-0" incoming_baggage.update({ "sentry-sample_rate": "0.75", - "sentry-sample_rand": "0.8", + "sentry-sample_rand": "0.800000", "sentry-sampled": "false", }) From 5a46d50b9b03cc2182e4fe72cb091e8998e41189 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 11:36:51 +0200 Subject: [PATCH 04/15] update sampled --- sentry_sdk/scope.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index a78ab24c83..d77ddca9bd 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1317,7 +1317,7 @@ def start_streamed_span( ) if sample_rate is not None: - self._update_sample_rate(sample_rate) + self._update_sample_rate(sample_rate, sampled) if sampled is False or sampled is None: return NoOpStreamedSpan( @@ -1376,7 +1376,7 @@ def start_streamed_span( parent_sampled=parent_span.sampled, ) - def _update_sample_rate(self, sample_rate: float) -> None: + def _update_sample_rate(self, sample_rate: float, sampled: "Optional[bool]") -> None: # If we had to adjust the sample rate when setting the sampling decision # for a span, it needs to be updated in the propagation context too propagation_context = self.get_active_propagation_context() @@ -1384,6 +1384,8 @@ def _update_sample_rate(self, sample_rate: float) -> None: if baggage is not None: baggage.sentry_items["sample_rate"] = str(sample_rate) + if sampled is not None: + baggage.sentry_items["sampled"] = "true" if sampled else "false" def continue_trace( self, From d6f117871f4d740c411ebc9b7968bcc3dc9aa215 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 11:43:10 +0200 Subject: [PATCH 05/15] . --- sentry_sdk/tracing_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 69a78cfffb..d278ebc26f 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1579,7 +1579,7 @@ def _make_sampling_decision( sample_rate = client.options["traces_sampler"](sampling_context) else: if propagation_context.parent_sampled is not None: - sample_rate = propagation_context._sample_rate() + sample_rate = propagation_context._sample_rate() or propagation_context.parent_sampled else: sample_rate = client.options["traces_sample_rate"] From 751cd69185493aadba26463b9330e598d6c63efc Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 11:45:23 +0200 Subject: [PATCH 06/15] . --- sentry_sdk/traces.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 85392eb14b..12daadb14e 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -747,6 +747,14 @@ def start_timestamp(self) -> "Optional[datetime]": def end_timestamp(self) -> "Optional[datetime]": return None + def _get_trace_context(self) -> "dict[str, Any]": + return { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self._parent_span_id, + "dynamic_sampling_context": self._dynamic_sampling_context(), + } + if TYPE_CHECKING: From d00c0f6c04cb775dee5bbfebf7c04ccfb3ca1a3f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 11:48:06 +0200 Subject: [PATCH 07/15] fixes --- sentry_sdk/tracing_utils.py | 3 ++- tests/tracing/test_http_headers.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index d278ebc26f..780ee8bbad 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -664,7 +664,8 @@ def _fill_sample_rand(self) -> None: # Get the sample rate and compute the transformation that will map the random value # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). - lower, upper = _sample_rand_range(self.parent_sampled, self._sample_rate()) + sample_rate = self._sample_rate() + lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) try: sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) diff --git a/tests/tracing/test_http_headers.py b/tests/tracing/test_http_headers.py index 3530f6f895..1914347b2b 100644 --- a/tests/tracing/test_http_headers.py +++ b/tests/tracing/test_http_headers.py @@ -2,7 +2,6 @@ import pytest -import sentry_sdk from sentry_sdk.tracing import Transaction from sentry_sdk.tracing_utils import extract_sentrytrace_data From 1a35a5e7b0a9fad77b7a718821c5b31f4fa4c0b9 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 11:49:51 +0200 Subject: [PATCH 08/15] ruff --- sentry_sdk/scope.py | 6 ++- sentry_sdk/tracing_utils.py | 4 +- tests/tracing/test_span_streaming.py | 75 +++++++++++++++++----------- 3 files changed, 54 insertions(+), 31 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index d77ddca9bd..9b795825df 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1362,7 +1362,7 @@ def start_streamed_span( trace_id=parent_span.trace_id, parent_span_id=parent_span.span_id, parent_sampled=parent_span.sampled, - unsampled_reason=parent_span._unsampled_reason + unsampled_reason=parent_span._unsampled_reason, ) return StreamedSpan( @@ -1376,7 +1376,9 @@ def start_streamed_span( parent_sampled=parent_span.sampled, ) - def _update_sample_rate(self, sample_rate: float, sampled: "Optional[bool]") -> None: + def _update_sample_rate( + self, sample_rate: float, sampled: "Optional[bool]" + ) -> None: # If we had to adjust the sample rate when setting the sampling decision # for a span, it needs to be updated in the propagation context too propagation_context = self.get_active_propagation_context() diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 780ee8bbad..c4b8c17ef9 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1580,7 +1580,9 @@ def _make_sampling_decision( sample_rate = client.options["traces_sampler"](sampling_context) else: if propagation_context.parent_sampled is not None: - sample_rate = propagation_context._sample_rate() or propagation_context.parent_sampled + sample_rate = ( + propagation_context._sample_rate() or propagation_context.parent_sampled + ) else: sample_rate = client.options["traces_sample_rate"] diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index ab99885c4b..ca5db0178d 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -916,7 +916,7 @@ def test_continue_trace_no_sample_rand(sentry_init, capture_items): # traces_sample_rate=None means tracing without performance: don't make # any sampling decisions; defer downstream None, - ) + ), ) def test_outgoing_traceparent_and_baggage_head_sdk(sentry_init, traces_sample_rate): sentry_init( @@ -945,7 +945,11 @@ def test_outgoing_traceparent_and_baggage_head_sdk(sentry_init, traces_sample_ra span_id = span.span_id assert traceparent == f"{trace_id}-{span_id}-0" elif expected_sampled is None: - span_id = sentry_sdk.get_isolation_scope().get_active_propagation_context().span_id + span_id = ( + sentry_sdk.get_isolation_scope() + .get_active_propagation_context() + .span_id + ) assert traceparent == f"{trace_id}-{span_id}" baggage = sentry_sdk.get_baggage() @@ -976,9 +980,11 @@ def test_outgoing_traceparent_and_baggage_head_sdk(sentry_init, traces_sample_ra # any sampling decisions on our end, propagate existing ones (None, False), (None, True), - ) + ), ) -def test_outgoing_traceparent_and_baggage_incoming_trace(sentry_init, traces_sample_rate, parent_sampled): +def test_outgoing_traceparent_and_baggage_incoming_trace( + sentry_init, traces_sample_rate, parent_sampled +): """The SDK respects a positive/negative incoming sampling decision.""" sentry_init( traces_sample_rate=traces_sample_rate, @@ -994,23 +1000,29 @@ def test_outgoing_traceparent_and_baggage_incoming_trace(sentry_init, traces_sam if parent_sampled is True: incoming_sentry_trace = f"{trace_id}-{parent_span_id}-1" - incoming_baggage.update({ - "sentry-sample_rate": "0.75", - "sentry-sample_rand": "0.500000", - "sentry-sampled": "true", - }) + incoming_baggage.update( + { + "sentry-sample_rate": "0.75", + "sentry-sample_rand": "0.500000", + "sentry-sampled": "true", + } + ) elif parent_sampled is False: incoming_sentry_trace = f"{trace_id}-{parent_span_id}-0" - incoming_baggage.update({ - "sentry-sample_rate": "0.75", - "sentry-sample_rand": "0.800000", - "sentry-sampled": "false", - }) + incoming_baggage.update( + { + "sentry-sample_rate": "0.75", + "sentry-sample_rand": "0.800000", + "sentry-sampled": "false", + } + ) sentry_sdk.traces.continue_trace( { "sentry-trace": incoming_sentry_trace, - "baggage": ",".join(sorted([f"{k}={v}" for k,v in incoming_baggage.items()])), + "baggage": ",".join( + sorted([f"{k}={v}" for k, v in incoming_baggage.items()]) + ), } ) @@ -1024,7 +1036,9 @@ def test_outgoing_traceparent_and_baggage_incoming_trace(sentry_init, traces_sam # (it doesn't even make sense to start a span explicitly as we do in # this test since tracing is turned off, but nothing should break # either) - span_id = sentry_sdk.get_current_scope().get_active_propagation_context().span_id + span_id = ( + sentry_sdk.get_current_scope().get_active_propagation_context().span_id + ) assert traceparent == f"{trace_id}-{span_id}" else: span_id = span.span_id @@ -1048,9 +1062,11 @@ def test_outgoing_traceparent_and_baggage_incoming_trace(sentry_init, traces_sam # traces_sample_rate=None means tracing without performance: don't make # any sampling decisions on our end, propagate existing ones None, - ) + ), ) -def test_outgoing_traceparent_and_baggage_incoming_trace_deferred(sentry_init, traces_sample_rate): +def test_outgoing_traceparent_and_baggage_incoming_trace_deferred( + sentry_init, traces_sample_rate +): """The SDK handles a deferred incoming sampling decision correctly.""" sentry_init( traces_sample_rate=traces_sample_rate, @@ -1067,15 +1083,14 @@ def test_outgoing_traceparent_and_baggage_incoming_trace_deferred(sentry_init, t trace_id = "0af7651916cd43dd8448eb211c80319c" parent_span_id = "b7ad6b7169203331" - incoming_baggage = { - "sentry-trace_id": trace_id, - "sentry-sample_rand": "0.500000" - } + incoming_baggage = {"sentry-trace_id": trace_id, "sentry-sample_rand": "0.500000"} sentry_sdk.traces.continue_trace( { "sentry-trace": f"{trace_id}-{parent_span_id}-", - "baggage": ",".join(sorted([f"{k}={v}" for k,v in incoming_baggage.items()])), + "baggage": ",".join( + sorted([f"{k}={v}" for k, v in incoming_baggage.items()]) + ), } ) @@ -1089,7 +1104,9 @@ def test_outgoing_traceparent_and_baggage_incoming_trace_deferred(sentry_init, t # (it doesn't even make sense to start a span explicitly as we do in # this test since tracing is turned off, but nothing should break # either) - span_id = sentry_sdk.get_current_scope().get_active_propagation_context().span_id + span_id = ( + sentry_sdk.get_current_scope().get_active_propagation_context().span_id + ) else: span_id = span.span_id @@ -1107,10 +1124,12 @@ def test_outgoing_traceparent_and_baggage_incoming_trace_deferred(sentry_init, t # If our sample rate is not None, we're expected to have made # a sampling decision expected_baggage = incoming_baggage - expected_baggage.update({ - "sentry-sample_rate": str(traces_sample_rate), - "sentry-sampled": "true" if expected_sampled else "false", - }) + expected_baggage.update( + { + "sentry-sample_rate": str(traces_sample_rate), + "sentry-sampled": "true" if expected_sampled else "false", + } + ) assert baggage_items == expected_baggage else: # If tracing is off, we should have deferred the decision further From 259605b7b6f0585b893f7e0985a748b08b92045b Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 12:18:13 +0200 Subject: [PATCH 09/15] . --- sentry_sdk/scope.py | 8 ++------ sentry_sdk/tracing_utils.py | 7 ++++--- tests/integrations/stdlib/test_httplib.py | 16 +++++++++++----- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 9b795825df..febf2fc9c7 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1317,7 +1317,7 @@ def start_streamed_span( ) if sample_rate is not None: - self._update_sample_rate(sample_rate, sampled) + self._update_sample_rate(sample_rate) if sampled is False or sampled is None: return NoOpStreamedSpan( @@ -1376,9 +1376,7 @@ def start_streamed_span( parent_sampled=parent_span.sampled, ) - def _update_sample_rate( - self, sample_rate: float, sampled: "Optional[bool]" - ) -> None: + def _update_sample_rate(self, sample_rate: float) -> None: # If we had to adjust the sample rate when setting the sampling decision # for a span, it needs to be updated in the propagation context too propagation_context = self.get_active_propagation_context() @@ -1386,8 +1384,6 @@ def _update_sample_rate( if baggage is not None: baggage.sentry_items["sample_rate"] = str(sample_rate) - if sampled is not None: - baggage.sentry_items["sampled"] = "true" if sampled else "false" def continue_trace( self, diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index c4b8c17ef9..899c99f141 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1580,9 +1580,10 @@ def _make_sampling_decision( sample_rate = client.options["traces_sampler"](sampling_context) else: if propagation_context.parent_sampled is not None: - sample_rate = ( - propagation_context._sample_rate() or propagation_context.parent_sampled - ) + if propagation_context._sample_rate() is not None: + sample_rate = propagation_context._sample_rate() + else: + sample_rate = propagation_context.parent_sampled else: sample_rate = client.options["traces_sample_rate"] diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 1723830f4e..87b6e90009 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -282,9 +282,14 @@ def getresponse(self, *args, **kwargs): headers = { "sentry-trace": "771a43a4192642f0b136d5159a501700-1234567890abcdef-1", "baggage": ( - "other-vendor-value-1=foo;bar;baz, sentry-trace_id=771a43a4192642f0b136d5159a501700, " - "sentry-public_key=49d0f7386ad645858ae85020e393bef3, sentry-sample_rate=0.01337, " - "sentry-user_id=Am%C3%A9lie, sentry-sample_rand=0.132521102938283, other-vendor-value-2=foo;bar;" + "other-vendor-value-1=foo;bar;baz," + "sentry-trace_id=771a43a4192642f0b136d5159a501700," + "sentry-public_key=49d0f7386ad645858ae85020e393bef3," + "sentry-sample_rate=0.01337," + "sentry-user_id=Am%C3%A9lie," + "sentry-sample_rand=0.000005," + "sentry-sampled=true," + "other-vendor-value-2=foo;bar;" ), } @@ -336,9 +341,10 @@ def getresponse(self, *args, **kwargs): expected_outgoing_baggage = ( "sentry-trace_id=771a43a4192642f0b136d5159a501700," "sentry-public_key=49d0f7386ad645858ae85020e393bef3," - "sentry-sample_rate=1.0," + "sentry-sample_rate=0.01337," "sentry-user_id=Am%C3%A9lie," - "sentry-sample_rand=0.132521102938283" + "sentry-sample_rand=0.000005," + "sentry-sampled=true" ) assert request_headers["baggage"] == expected_outgoing_baggage From c923e093e9ae85a17d8f75a08964263dd1d94002 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 13:04:10 +0200 Subject: [PATCH 10/15] fix in legacy too --- sentry_sdk/tracing.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 1790e13fbf..d8854e38ff 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1200,16 +1200,20 @@ def _set_initial_sampling_decision( # we would have bailed already if neither `traces_sampler` nor # `traces_sample_rate` were defined, so one of these should work; prefer # the hook if so - sample_rate = ( - client.options["traces_sampler"](sampling_context) - if callable(client.options.get("traces_sampler")) + if callable(client.options.get("traces_sampler")): + sample_rate = client.options["traces_sampler"](sampling_context) + else: # default inheritance behavior - else ( - sampling_context["parent_sampled"] - if sampling_context["parent_sampled"] is not None - else client.options["traces_sample_rate"] - ) - ) + if sampling_context["parent_sampled"] is not None: + propagation_context = ( + sentry_sdk.get_current_scope().get_active_propagation_context() + ) + if propagation_context._sample_rate() is not None: + sample_rate = propagation_context._sample_rate() + else: + sample_rate = sampling_context["parent_sampled"] + else: + sample_rate = client.options["traces_sample_rate"] # Since this is coming from the user (or from a function provided by the # user), who knows what we might get. (The only valid values are From a4ddd47f2baffd83eded61a6b9b5dccddecca1c1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 13:30:00 +0200 Subject: [PATCH 11/15] Revert "fix in legacy too" This reverts commit c923e093e9ae85a17d8f75a08964263dd1d94002. --- sentry_sdk/tracing.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index d8854e38ff..1790e13fbf 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1200,20 +1200,16 @@ def _set_initial_sampling_decision( # we would have bailed already if neither `traces_sampler` nor # `traces_sample_rate` were defined, so one of these should work; prefer # the hook if so - if callable(client.options.get("traces_sampler")): - sample_rate = client.options["traces_sampler"](sampling_context) - else: + sample_rate = ( + client.options["traces_sampler"](sampling_context) + if callable(client.options.get("traces_sampler")) # default inheritance behavior - if sampling_context["parent_sampled"] is not None: - propagation_context = ( - sentry_sdk.get_current_scope().get_active_propagation_context() - ) - if propagation_context._sample_rate() is not None: - sample_rate = propagation_context._sample_rate() - else: - sample_rate = sampling_context["parent_sampled"] - else: - sample_rate = client.options["traces_sample_rate"] + else ( + sampling_context["parent_sampled"] + if sampling_context["parent_sampled"] is not None + else client.options["traces_sample_rate"] + ) + ) # Since this is coming from the user (or from a function provided by the # user), who knows what we might get. (The only valid values are From 23a9e78babad6645b2416e0c1e5a0c187be7ff58 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 13:32:38 +0200 Subject: [PATCH 12/15] . --- tests/integrations/stdlib/test_httplib.py | 30 +++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 87b6e90009..64e059fc4c 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -314,6 +314,15 @@ def getresponse(self, *args, **kwargs): parent_span_id=request_span["span_id"], sampled=1, ) + + expected_outgoing_baggage = ( + "sentry-trace_id=771a43a4192642f0b136d5159a501700," + "sentry-public_key=49d0f7386ad645858ae85020e393bef3," + "sentry-sample_rate=0.01337," + "sentry-user_id=Am%C3%A9lie," + "sentry-sample_rand=0.000005," + "sentry-sampled=true" + ) else: events = capture_events() transaction = continue_trace(headers) @@ -336,17 +345,18 @@ def getresponse(self, *args, **kwargs): sampled=1, ) - assert request_headers["sentry-trace"] == expected_sentry_trace - - expected_outgoing_baggage = ( - "sentry-trace_id=771a43a4192642f0b136d5159a501700," - "sentry-public_key=49d0f7386ad645858ae85020e393bef3," - "sentry-sample_rate=0.01337," - "sentry-user_id=Am%C3%A9lie," - "sentry-sample_rand=0.000005," - "sentry-sampled=true" - ) + # Note: the sample rate here is actually wrong. It's fixed in the + # streaming path + expected_outgoing_baggage = ( + "sentry-trace_id=771a43a4192642f0b136d5159a501700," + "sentry-public_key=49d0f7386ad645858ae85020e393bef3," + "sentry-sample_rate=1.0," + "sentry-user_id=Am%C3%A9lie," + "sentry-sample_rand=0.000005," + "sentry-sampled=true" + ) + assert request_headers["sentry-trace"] == expected_sentry_trace assert request_headers["baggage"] == expected_outgoing_baggage From cd7357197bc06f43577a270d2f29af936b5691f1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 13:40:20 +0200 Subject: [PATCH 13/15] sure mypy --- sentry_sdk/tracing_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 899c99f141..3ea1809b8c 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1538,12 +1538,12 @@ def _make_sampling_decision( name: str, attributes: "Optional[Attributes]", scope: "sentry_sdk.Scope", -) -> "tuple[bool, Optional[float], Optional[float], Optional[str]]": +) -> "tuple[Optional[bool], Optional[float], Optional[float], Optional[str]]": """ Decide whether a span should be sampled. Returns a tuple with: - 1. the sampling decision + 1. the sampling decision (sampled, unsampled, deferred) 2. the effective sample rate 3. the sample rand 4. the reason for not sampling the span, if unsampled @@ -1593,7 +1593,7 @@ def _make_sampling_decision( logger.warning(f"[Tracing] Discarding {name} because of invalid sample rate.") return False, None, None, "sample_rate" - sample_rate = float(sample_rate) + sample_rate = float(sample_rate) # type: ignore[arg-type] if not sample_rate: if traces_sampler_defined: reason = "traces_sampler returned 0 or False" From 46b410ab0d7d1b50028a2186dfb47f913daf2223 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 6 Jul 2026 13:42:01 +0200 Subject: [PATCH 14/15] . --- tests/tracing/test_span_streaming.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index ca5db0178d..004dd2d975 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -1127,7 +1127,6 @@ def test_outgoing_traceparent_and_baggage_incoming_trace_deferred( expected_baggage.update( { "sentry-sample_rate": str(traces_sample_rate), - "sentry-sampled": "true" if expected_sampled else "false", } ) assert baggage_items == expected_baggage From f2127eae084777b9ae5053c1553510be9e8b3184 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 7 Jul 2026 11:50:11 +0200 Subject: [PATCH 15/15] . --- sentry_sdk/scope.py | 2 ++ sentry_sdk/traces.py | 10 ++++---- sentry_sdk/tracing_utils.py | 2 +- tests/tracing/test_span_streaming.py | 36 ++++++++++++++++------------ 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index febf2fc9c7..2538eb482d 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1329,6 +1329,8 @@ def start_streamed_span( baggage=propagation_context.baggage, sampled=sampled, unsampled_reason=outcome, + sample_rand=sample_rand, + sample_rate=sample_rate, ) return StreamedSpan( diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 12daadb14e..4911a8c6f5 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -501,9 +501,9 @@ def _dynamic_sampling_context(self) -> "dict[str, str]": return self._segment._get_baggage().dynamic_sampling_context() def _to_traceparent(self) -> str: - if self.sampled is True: + if self._segment.sampled is True: sampled = "1" - elif self.sampled is False: + elif self._segment.sampled is False: sampled = "0" else: sampled = None @@ -628,6 +628,8 @@ def __init__( sampled: "Optional[bool]" = False, unsampled_reason: "Optional[str]" = None, scope: "Optional[sentry_sdk.Scope]" = None, + sample_rand: "Optional[float]" = None, + sample_rate: "Optional[float]" = None, ) -> None: self._span_id: "Optional[str]" = None @@ -638,8 +640,8 @@ def __init__( self._parent_span_id = parent_span_id self._parent_sampled = parent_sampled self._baggage = baggage - self._sample_rand = None - self._sample_rate = None + self._sample_rand = sample_rand + self._sample_rate = sample_rate self._scope = scope # type: ignore[assignment] self._unsampled_reason = unsampled_reason diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 3ea1809b8c..c49f8c7054 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1601,7 +1601,7 @@ def _make_sampling_decision( reason = "traces_sample_rate is set to 0" logger.debug(f"[Tracing] Discarding {name} because {reason}") - return False, 0.0, None, "sample_rate" + return False, 0.0, sample_rand, "sample_rate" # Adjust sample rate if we're under backpressure sample_rate_before_backpressure = sample_rate diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index 004dd2d975..bce5997a1a 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -919,11 +919,6 @@ def test_continue_trace_no_sample_rand(sentry_init, capture_items): ), ) def test_outgoing_traceparent_and_baggage_head_sdk(sentry_init, traces_sample_rate): - sentry_init( - traces_sample_rate=traces_sample_rate, - _experiments={"trace_lifecycle": "stream"}, - ) - if traces_sample_rate == 0.0: expected_sampled = False elif traces_sample_rate == 1.0: @@ -931,6 +926,13 @@ def test_outgoing_traceparent_and_baggage_head_sdk(sentry_init, traces_sample_ra elif traces_sample_rate is None: expected_sampled = None + sentry_init( + traces_sample_rate=traces_sample_rate, + _experiments={"trace_lifecycle": "stream"}, + ) + + sentry_sdk.traces.new_trace() + with sentry_sdk.traces.start_span(name="span") as span: assert span.sampled is expected_sampled @@ -945,10 +947,10 @@ def test_outgoing_traceparent_and_baggage_head_sdk(sentry_init, traces_sample_ra span_id = span.span_id assert traceparent == f"{trace_id}-{span_id}-0" elif expected_sampled is None: + # Tracing without performance, span ID comes from the propagation + # context on the scope span_id = ( - sentry_sdk.get_isolation_scope() - .get_active_propagation_context() - .span_id + sentry_sdk.get_current_scope().get_active_propagation_context().span_id ) assert traceparent == f"{trace_id}-{span_id}" @@ -961,8 +963,10 @@ def test_outgoing_traceparent_and_baggage_head_sdk(sentry_init, traces_sample_ra if expected_sampled is None: assert "sentry-sampled" not in baggage_items else: - assert "sentry-sampled" in baggage_items + assert baggage_items["sentry-sample_rate"] == str(traces_sample_rate) + assert "sentry-sample_rand" in baggage_items + assert "sentry-sampled" in baggage_items if expected_sampled is True: assert baggage_items["sentry-sampled"] == "true" elif expected_sampled is False: @@ -986,6 +990,9 @@ def test_outgoing_traceparent_and_baggage_incoming_trace( sentry_init, traces_sample_rate, parent_sampled ): """The SDK respects a positive/negative incoming sampling decision.""" + # The case where the incoming trace has a deferred sampling decision is + # tested separately in + # test_outgoing_traceparent_and_baggage_incoming_trace_deferred sentry_init( traces_sample_rate=traces_sample_rate, _experiments={"trace_lifecycle": "stream"}, @@ -1042,10 +1049,9 @@ def test_outgoing_traceparent_and_baggage_incoming_trace( assert traceparent == f"{trace_id}-{span_id}" else: span_id = span.span_id - if parent_sampled is True: - assert traceparent == f"{trace_id}-{span_id}-1" - elif parent_sampled is False: - assert traceparent == f"{trace_id}-{span_id}-0" + assert ( + traceparent == f"{trace_id}-{span_id}-{'1' if parent_sampled else '0'}" + ) # As we've received incoming baggage, we mustn't modify it ourselves and # have to propagate it as-is, unless we @@ -1075,9 +1081,9 @@ def test_outgoing_traceparent_and_baggage_incoming_trace_deferred( if traces_sample_rate == 0.0: expected_sampled = False - if traces_sample_rate == 1.0: + elif traces_sample_rate == 1.0: expected_sampled = True - if traces_sample_rate is None: + elif traces_sample_rate is None: expected_sampled = None trace_id = "0af7651916cd43dd8448eb211c80319c"