From d3d30ddfb262fc20a4710bf4c7a895724e557907 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 17 Sep 2026 17:07:46 -0400 Subject: [PATCH 1/6] Attempt #1 --- sentry_sdk/integrations/dedupe.py | 43 +++++++++++++++++++------------ 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index a0cc88081f..0fafcf38f0 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -1,4 +1,4 @@ -import weakref +import time from contextvars import ContextVar from typing import TYPE_CHECKING @@ -34,25 +34,36 @@ def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": if exc_info is None: return event - last_seen = integration._last_seen.get(None) - if last_seen is not None: - # last_seen is either a weakref or the original instance - last_seen = ( - last_seen() if isinstance(last_seen, weakref.ref) else last_seen - ) + last_seen_entries = integration._last_seen.get(None) + updated_cache_entries = set() exc = exc_info[1] - if last_seen is exc: - logger.info("DedupeIntegration dropped duplicated error event %s", exc) - return None + now = time.time() + + if not last_seen_entries: + integration._last_seen.set([(exc, now)]) + return event + + found_duplicate = False + for cache_item in last_seen_entries: + exception_item, last_seen = cache_item + if last_seen < (now - 60): # 1 minute TTL + continue - # we can only weakref non builtin types - try: - integration._last_seen.set(weakref.ref(exc)) - except TypeError: - integration._last_seen.set(exc) + if exc is exception_item: + updated_cache_entries.add((exception_item, now)) + found_duplicate = True + continue - return event + updated_cache_entries.add((exception_item, last_seen)) + + integration._last_seen.set(updated_cache_entries) + + if found_duplicate: + logger.info("DedupeIntegration dropped duplicated error event %s", exc) + return None + else: + return event @staticmethod def reset_last_seen() -> None: From c2b64a75696ae3955af464fbb82f0f3cfa945e09 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 17 Sep 2026 17:20:58 -0400 Subject: [PATCH 2/6] attempt 1.5 --- sentry_sdk/integrations/dedupe.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 0fafcf38f0..404bf91868 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -57,6 +57,9 @@ def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": updated_cache_entries.add((exception_item, last_seen)) + if not found_duplicate: + updated_cache_entries.add((exc, now)) + integration._last_seen.set(updated_cache_entries) if found_duplicate: From 15a9ecdefe3a999ec8ee0f59661611c2992457dc Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 18 Sep 2026 14:53:30 -0400 Subject: [PATCH 3/6] set private variable --- sentry_sdk/client.py | 7 ----- sentry_sdk/integrations/dedupe.py | 43 +++---------------------------- tests/test_basics.py | 24 +++++++++++++++-- 3 files changed, 26 insertions(+), 48 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 9114cf6b67..dff8492146 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -607,13 +607,6 @@ def _prepare_event( "before_send", data_category="error" ) - # If this is an exception, reset the DedupeIntegration. It still - # remembers the dropped exception as the last exception, meaning - # that if the same exception happens again and is not dropped - # in before_send, it'd get dropped by DedupeIntegration. - if event.get("exception"): - DedupeIntegration.reset_last_seen() - event = new_event return event diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 404bf91868..27f0654c2e 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -5,7 +5,7 @@ import sentry_sdk from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import logger +from sentry_sdk.utils import capture_internal_exceptions, logger if TYPE_CHECKING: from typing import Any, Optional @@ -16,9 +16,6 @@ class DedupeIntegration(Integration): identifier = "dedupe" - def __init__(self) -> None: - self._last_seen: "ContextVar[Any]" = ContextVar("last-seen") - @staticmethod def setup_once() -> None: @add_global_event_processor @@ -34,44 +31,12 @@ def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": if exc_info is None: return event - last_seen_entries = integration._last_seen.get(None) - updated_cache_entries = set() - exc = exc_info[1] - now = time.time() - - if not last_seen_entries: - integration._last_seen.set([(exc, now)]) - return event - - found_duplicate = False - for cache_item in last_seen_entries: - exception_item, last_seen = cache_item - if last_seen < (now - 60): # 1 minute TTL - continue - - if exc is exception_item: - updated_cache_entries.add((exception_item, now)) - found_duplicate = True - continue - updated_cache_entries.add((exception_item, last_seen)) - - if not found_duplicate: - updated_cache_entries.add((exc, now)) - - integration._last_seen.set(updated_cache_entries) - - if found_duplicate: + if getattr(exc, "_handled_by_sentry", False): logger.info("DedupeIntegration dropped duplicated error event %s", exc) return None else: + with capture_internal_exceptions(): + exc._handled_by_sentry = True return event - - @staticmethod - def reset_last_seen() -> None: - integration = sentry_sdk.get_client().get_integration(DedupeIntegration) - if integration is None: - return - - integration._last_seen.set(None) diff --git a/tests/test_basics.py b/tests/test_basics.py index eb4eb516fe..2ca72b5091 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -614,13 +614,33 @@ def before_send(event, hint): sentry_init(before_send=before_send) events = capture_events() - exc = ValueError("aha!") for _ in range(2): # The first ValueError will be dropped by before_send. The second # ValueError will be accepted by before_send, and should be sent to # Sentry. try: - raise exc + raise ValueError("aha!") + except Exception: + capture_exception() + + assert len(events) == 1 + +def test_dedupe_drops_exception_when_seen_a_second_time(sentry_init, capture_events): + """ + This test is intended to emulate behavior seen in frameworks like Django, + where an exception is raised in a view and then is re-raised in middleware. + + In cases like that we don't want to send a second event for that exception. + """ + sentry_init() + events = capture_events() + + test = None + for _ in range(2): + try: + if test is None: + test = ValueError("foo") + raise test except Exception: capture_exception() From d0848088bb3ba1ccf0773744f532c403218955c2 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 18 Sep 2026 14:53:59 -0400 Subject: [PATCH 4/6] lint --- sentry_sdk/integrations/dedupe.py | 2 -- tests/test_basics.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 27f0654c2e..3e6206aa3c 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -1,5 +1,3 @@ -import time -from contextvars import ContextVar from typing import TYPE_CHECKING import sentry_sdk diff --git a/tests/test_basics.py b/tests/test_basics.py index 2ca72b5091..1b16257fe8 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -625,6 +625,7 @@ def before_send(event, hint): assert len(events) == 1 + def test_dedupe_drops_exception_when_seen_a_second_time(sentry_init, capture_events): """ This test is intended to emulate behavior seen in frameworks like Django, From b51dfd501e1b9a8d16d51a2d9aedb91480da2886 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 18 Sep 2026 14:56:50 -0400 Subject: [PATCH 5/6] lint --- sentry_sdk/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index dff8492146..4e3946c395 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -24,7 +24,6 @@ ) from sentry_sdk.envelope import Envelope, Item from sentry_sdk.integrations import setup_integrations -from sentry_sdk.integrations.dedupe import DedupeIntegration from sentry_sdk.monitor import Monitor from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler from sentry_sdk.scrubber import EventScrubber From 900ad90155e61d58bd58efc9e35c45940816d38b Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 18 Sep 2026 15:00:33 -0400 Subject: [PATCH 6/6] . --- sentry_sdk/integrations/dedupe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 3e6206aa3c..a79b05bd29 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -6,7 +6,7 @@ from sentry_sdk.utils import capture_internal_exceptions, logger if TYPE_CHECKING: - from typing import Any, Optional + from typing import Optional from sentry_sdk._types import Event, Hint