From 624d5c22ea9b699cad1b593d2890abce87b26670 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Tue, 26 May 2026 15:52:22 -0700 Subject: [PATCH 1/2] test(snuba): demonstrate dropped rate-limit fields when policy throttles Co-authored-by: Claude --- tests/sentry/utils/test_snuba.py | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/sentry/utils/test_snuba.py b/tests/sentry/utils/test_snuba.py index a275d59d35e5ba..d656d66ca78978 100644 --- a/tests/sentry/utils/test_snuba.py +++ b/tests/sentry/utils/test_snuba.py @@ -628,3 +628,41 @@ def test_rate_limit_error_handling_with_stats_but_no_quota_details( assert ( str(exc_info.value) == "Query on could not be run due to allocation policies, info: ..." ) + + @mock.patch("sentry.utils.snuba._snuba_query") + def test_rate_limit_error_handling_throttle_only(self, mock_snuba_query) -> None: + """ + Test that policy metadata propagates when the 429 came from a throttle: + rejected_by is empty and throttled_by carries throttle_threshold, with no rejection_threshold + """ + mock_response = mock.Mock(spec=HTTPResponse) + mock_response.status = 429 + mock_response.data = json.dumps( + { + "error": { + "message": "Query scanned more than the allocated amount of bytes", + }, + "quota_allowance": { + "summary": { + "rejected_by": {}, + "throttled_by": { + "policy": "BytesScannedRejectingPolicy", + "quota_used": 1500000000000, + "throttle_threshold": 1000000000000, + "quota_unit": "bytes", + "storage_key": "errors_ro", + }, + } + }, + } + ).encode() + + mock_snuba_query.return_value = ("test_referrer", mock_response, lambda x: x, lambda x: x) + + with pytest.raises(RateLimitExceeded) as exc_info: + _bulk_snuba_query([self.snuba_request]) + + assert exc_info.value.policy == "BytesScannedRejectingPolicy" + assert exc_info.value.storage_key == "errors_ro" + assert exc_info.value.quota_used == 1500000000000 + assert exc_info.value.quota_unit == "bytes" From 1bc1597fccb194928cf062c64671479b6859c048 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Tue, 26 May 2026 15:52:36 -0700 Subject: [PATCH 2/2] fix(snuba): preserve policy metadata when 429 came from a throttle, not a rejection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When snuba returns 429 from a throttle path (e.g. TOO_MANY_BYTES from BytesScannedRejectingPolicy's limit_bytes_instead_of_rejecting), the response's quota_allowance.summary.throttled_by carries throttle_threshold rather than rejection_threshold. Subscripting policy_info["rejection_threshold"] raised KeyError, which was swallowed and fell through to a bare RateLimitExceeded with no policy/storage_key — wiping the SnubaRateLimitMeta downstream and leaving api.access logs with rate_limit_type=snuba but blank snuba_policy / snuba_storage_key. Co-authored-by: Claude --- src/sentry/utils/snuba.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sentry/utils/snuba.py b/src/sentry/utils/snuba.py index 04ebdae2d34738..528451597d45b7 100644 --- a/src/sentry/utils/snuba.py +++ b/src/sentry/utils/snuba.py @@ -1351,7 +1351,8 @@ def _bulk_snuba_query(snuba_requests: Sequence[SnubaRequest]) -> ResultSet: quota_unit=policy_info["quota_unit"], storage_key=policy_info["storage_key"], quota_used=policy_info["quota_used"], - rejection_threshold=policy_info["rejection_threshold"], + # We won't have rejection_threshold for throttled_by errors + rejection_threshold=policy_info.get("rejection_threshold"), ) except KeyError: logger.warning(