From 2853d335dcfdba2e78cf3c0547043226de7b6d2b Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 5 Nov 2025 09:44:31 +0100 Subject: [PATCH 1/6] fix: Add hard limit to metrics batcher --- sentry_sdk/_metrics_batcher.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry_sdk/_metrics_batcher.py b/sentry_sdk/_metrics_batcher.py index 27d27f2c72..fa04b65220 100644 --- a/sentry_sdk/_metrics_batcher.py +++ b/sentry_sdk/_metrics_batcher.py @@ -13,6 +13,7 @@ class MetricsBatcher: MAX_METRICS_BEFORE_FLUSH = 1000 + MAX_METRICS_BEFORE_DROP = 10_000 FLUSH_WAIT_TIME = 5.0 def __init__( @@ -72,6 +73,9 @@ def add( return None with self._lock: + if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_DROP: + return None + self._metric_buffer.append(metric) if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_FLUSH: self._flush_event.set() From a52d0fbcfa7e6f92cb051cb8760daf83fb4f6caf Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 5 Nov 2025 13:28:43 +0100 Subject: [PATCH 2/6] record dropping metrics and test --- sentry_sdk/_metrics_batcher.py | 7 +++++++ sentry_sdk/client.py | 5 ++++- tests/test_metrics.py | 26 ++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/_metrics_batcher.py b/sentry_sdk/_metrics_batcher.py index fa04b65220..0db424cfcb 100644 --- a/sentry_sdk/_metrics_batcher.py +++ b/sentry_sdk/_metrics_batcher.py @@ -19,10 +19,12 @@ class MetricsBatcher: def __init__( self, capture_func, # type: Callable[[Envelope], None] + record_lost_func, # type: Callable[..., None] ): # type: (...) -> None self._metric_buffer = [] # type: List[Metric] self._capture_func = capture_func + self._record_lost_func = record_lost_func self._running = True self._lock = threading.Lock() @@ -74,6 +76,11 @@ def add( with self._lock: if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_DROP: + self._record_lost_func( + reason="queue_overflow", + data_category="trace_metric", + quantity=1, + ) return None self._metric_buffer.append(metric) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 219f69b404..5501b68b0a 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -377,7 +377,10 @@ def _capture_envelope(envelope): self.metrics_batcher = None if has_metrics_enabled(self.options): - self.metrics_batcher = MetricsBatcher(capture_func=_capture_envelope) + self.metrics_batcher = MetricsBatcher( + capture_func=_capture_envelope, + record_lost_func=self.transport.record_lost_event, + ) max_request_body_size = ("always", "never", "small", "medium") if self.options["max_request_body_size"] not in max_request_body_size: diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 29b139c184..e58d06ddbe 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -8,6 +8,8 @@ from sentry_sdk.envelope import Envelope from sentry_sdk.types import Metric +from sentry_sdk._metrics_batcher import MetricsBatcher + def envelopes_to_metrics(envelopes): # type: (List[Envelope]) -> List[Metric] @@ -204,3 +206,27 @@ def _before_metric(record, hint): assert len(metrics) == 1 assert metrics[0]["name"] == "test.keep" assert before_metric_called + + +def test_batcher_drops_metrics(sentry_init, monkeypatch): + sentry_init() + client = sentry_sdk.get_client() + + def no_op_flush(): + pass + + monkeypatch.setattr(client.metrics_batcher, "_flush", no_op_flush) + + lost_event_calls = [] + + def record_lost_event(reason, data_category=None, item=None, *, quantity=1): + lost_event_calls.append((reason, data_category, item, quantity)) + + monkeypatch.setattr(client.metrics_batcher, "_record_lost_func", record_lost_event) + + for i in range(10_005): # 5 metrics over the hard limit + sentry_sdk.metrics.count("test.counter", 1) + + assert len(lost_event_calls) == 5 + for lost_event_call in lost_event_calls: + assert lost_event_call == ("queue_overflow", "trace_metric", None, 1) From 36932b0e9aa7d089c6e53a98aa498a3a1787ac82 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 5 Nov 2025 13:29:25 +0100 Subject: [PATCH 3/6] . --- tests/test_metrics.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_metrics.py b/tests/test_metrics.py index e58d06ddbe..ff5a6332a2 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -8,8 +8,6 @@ from sentry_sdk.envelope import Envelope from sentry_sdk.types import Metric -from sentry_sdk._metrics_batcher import MetricsBatcher - def envelopes_to_metrics(envelopes): # type: (List[Envelope]) -> List[Metric] From 7c7f3f0ca78dd1f6c2f486aedd6eb66543144d7c Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 5 Nov 2025 13:48:57 +0100 Subject: [PATCH 4/6] check for None transport --- sentry_sdk/_metrics_batcher.py | 1 - sentry_sdk/client.py | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/_metrics_batcher.py b/sentry_sdk/_metrics_batcher.py index 0db424cfcb..cf5763444d 100644 --- a/sentry_sdk/_metrics_batcher.py +++ b/sentry_sdk/_metrics_batcher.py @@ -77,7 +77,6 @@ def add( with self._lock: if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_DROP: self._record_lost_func( - reason="queue_overflow", data_category="trace_metric", quantity=1, ) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 5501b68b0a..4731097f9d 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -62,7 +62,7 @@ from typing import Union from typing import TypeVar - from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric + from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory from sentry_sdk.integrations import Integration from sentry_sdk.scope import Scope from sentry_sdk.session import Session @@ -357,6 +357,17 @@ def _capture_envelope(envelope): if self.transport is not None: self.transport.capture_envelope(envelope) + def _record_batcher_overflow_lost_event( + data_category, # type: EventDataCategory + quantity=1, # type: int + ): + if self.transport is not None: + self.transport.record_lost_event( + reason="queue_overflow", + data_category=data_category, + quantity=quantity, + ) + try: _client_init_debug.set(self.options["debug"]) self.transport = make_transport(self.options) @@ -379,7 +390,7 @@ def _capture_envelope(envelope): if has_metrics_enabled(self.options): self.metrics_batcher = MetricsBatcher( capture_func=_capture_envelope, - record_lost_func=self.transport.record_lost_event, + record_lost_func=_record_batcher_overflow_lost_event, ) max_request_body_size = ("always", "never", "small", "medium") From d67ee3734178f72eae262f6eb7508fb488b32394 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 5 Nov 2025 13:55:57 +0100 Subject: [PATCH 5/6] . --- sentry_sdk/_metrics_batcher.py | 1 + sentry_sdk/client.py | 7 ++++--- tests/test_metrics.py | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/_metrics_batcher.py b/sentry_sdk/_metrics_batcher.py index cf5763444d..0db424cfcb 100644 --- a/sentry_sdk/_metrics_batcher.py +++ b/sentry_sdk/_metrics_batcher.py @@ -77,6 +77,7 @@ def add( with self._lock: if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_DROP: self._record_lost_func( + reason="queue_overflow", data_category="trace_metric", quantity=1, ) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 4731097f9d..31fea55203 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -357,13 +357,14 @@ def _capture_envelope(envelope): if self.transport is not None: self.transport.capture_envelope(envelope) - def _record_batcher_overflow_lost_event( + def _record_lost_event( + reason, # type: str data_category, # type: EventDataCategory quantity=1, # type: int ): if self.transport is not None: self.transport.record_lost_event( - reason="queue_overflow", + reason=reason, data_category=data_category, quantity=quantity, ) @@ -390,7 +391,7 @@ def _record_batcher_overflow_lost_event( if has_metrics_enabled(self.options): self.metrics_batcher = MetricsBatcher( capture_func=_capture_envelope, - record_lost_func=_record_batcher_overflow_lost_event, + record_lost_func=_record_lost_event, ) max_request_body_size = ("always", "never", "small", "medium") diff --git a/tests/test_metrics.py b/tests/test_metrics.py index ff5a6332a2..8d2419a689 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -217,8 +217,8 @@ def no_op_flush(): lost_event_calls = [] - def record_lost_event(reason, data_category=None, item=None, *, quantity=1): - lost_event_calls.append((reason, data_category, item, quantity)) + def record_lost_event(reason, data_category, quantity): + lost_event_calls.append((reason, data_category, quantity)) monkeypatch.setattr(client.metrics_batcher, "_record_lost_func", record_lost_event) @@ -227,4 +227,4 @@ def record_lost_event(reason, data_category=None, item=None, *, quantity=1): assert len(lost_event_calls) == 5 for lost_event_call in lost_event_calls: - assert lost_event_call == ("queue_overflow", "trace_metric", None, 1) + assert lost_event_call == ("queue_overflow", "trace_metric", 1) From b6be9670c2bfe037a8d546739609c6b21a11eeeb Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 5 Nov 2025 14:01:35 +0100 Subject: [PATCH 6/6] type --- sentry_sdk/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 31fea55203..6cb5ca5826 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -362,6 +362,7 @@ def _record_lost_event( data_category, # type: EventDataCategory quantity=1, # type: int ): + # type: (...) -> None if self.transport is not None: self.transport.record_lost_event( reason=reason,