From 1cf1491bb220e7e6285637cfd4402e956407d4ae Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 29 May 2026 14:04:43 +0200 Subject: [PATCH 1/2] fix(sampling): Attribute backpressure as unsampling reason --- sentry_sdk/tracing_utils.py | 24 ++++++++++-- tests/tracing/test_span_streaming.py | 56 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index bf74dd6de6..ae449341b9 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1577,6 +1577,7 @@ def _make_sampling_decision( return False, 0.0, None, "sample_rate" # Adjust sample rate if we're under backpressure + sample_rate_before_backpressure = sample_rate if client.monitor: sample_rate /= 2**client.monitor.downsample_factor @@ -1584,16 +1585,31 @@ def _make_sampling_decision( logger.debug(f"[Tracing] Discarding {name} because backpressure") return False, 0.0, None, "backpressure" + # Make the actual decision sampled = sample_rand < sample_rate if sampled: logger.debug(f"[Tracing] Starting {name}") outcome = None + else: - logger.debug( - f"[Tracing] Discarding {name} because it's not included in the random sample (sampling rate = {sample_rate})" - ) - outcome = "sample_rate" + # Determine why exactly the span will not be sampled. If we've lowered + # the effective sample_rate because of backpressure, check whether the + # span would've been sampled if backpressure wasn't active. If that's the + # case, backpressure is the actual reason, otherwise just pure sampling + # rate. + if ( + sample_rate_before_backpressure != sample_rate + and sample_rand < sample_rate_before_backpressure + ): + logger.debug(f"[Tracing] Discarding {name} because backpressure") + outcome = "backpressure" + + else: + logger.debug( + f"[Tracing] Discarding {name} because it's not included in the random sample (sampling rate = {sample_rate})" + ) + outcome = "sample_rate" return sampled, sample_rate, sample_rand, outcome diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index c861616b70..a91b545fb2 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -754,6 +754,62 @@ def test_continue_trace_unsampled(sentry_init, capture_items): assert len(spans) == 0 +@pytest.mark.parametrize( + ("sample_rand", "expected_sampled", "expected_outcome"), + [ + ("0.100000", True, None), + ("0.300000", False, "backpressure"), + ("0.700000", False, "sample_rate"), + ], +) +def test_backpressure_outcome( + sentry_init, + capture_items, + capture_record_lost_event_calls, + sample_rand, + expected_sampled, + expected_outcome, +): + sentry_init( + traces_sample_rate=0.5, + enable_backpressure_handling=True, + _experiments={"trace_lifecycle": "stream"}, + ) + + items = capture_items("span") + record_lost_event_calls = capture_record_lost_event_calls() + + client = sentry_sdk.get_client() + client.monitor._downsample_factor = 1 + + trace_id = "0af7651916cd43dd8448eb211c80319c" + parent_span_id = "b7ad6b7169203331" + + sentry_sdk.traces.continue_trace( + { + "sentry-trace": f"{trace_id}-{parent_span_id}", + "baggage": f"sentry-trace_id={trace_id},sentry-sample_rand={sample_rand}", + } + ) + + with sentry_sdk.traces.start_span(name="span") as span: + pass + + sentry_sdk.get_client().flush() + spans = [item.payload for item in items] + + assert span.sampled is expected_sampled + + if expected_sampled: + assert len(spans) == 1 + assert not record_lost_event_calls + else: + assert len(spans) == 0 + assert record_lost_event_calls == [ + (expected_outcome, "span", None, 1), + ] + + def test_unsampled_spans_produce_client_report( sentry_init, capture_items, capture_record_lost_event_calls ): From b7c0197c87fb06bd52e3b75c9df2278d48eecd21 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 29 May 2026 14:41:31 +0200 Subject: [PATCH 2/2] better test --- tests/tracing/test_span_streaming.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index a91b545fb2..d8537ea779 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -758,7 +758,9 @@ def test_continue_trace_unsampled(sentry_init, capture_items): ("sample_rand", "expected_sampled", "expected_outcome"), [ ("0.100000", True, None), + ("0.250000", False, "backpressure"), ("0.300000", False, "backpressure"), + ("0.500000", False, "sample_rate"), ("0.700000", False, "sample_rate"), ], ) @@ -798,6 +800,11 @@ def test_backpressure_outcome( sentry_sdk.get_client().flush() spans = [item.payload for item in items] + # Original traces_sample_rate is 0.5, downsampled sample rate is 0.25, so: + # - sample_rand < 0.25 -> sampled + # - 0.25 < sample_rand < 0.5 -> unsampled because of backpressure (would've been sampled if no backpressure) + # - 0.5 < sample_rand -> unsampled because of sampling rate + assert span.sampled is expected_sampled if expected_sampled: