From e29c09d178a309a3e47008ff951767ed739c2e7b Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 17 Sep 2025 13:57:58 +0200 Subject: [PATCH 01/29] dedupe with a fingerprint instead of exception object --- sentry_sdk/integrations/dedupe.py | 97 ++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index eab2764fcd..4dbc153027 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -1,22 +1,87 @@ +import hashlib import sentry_sdk -from sentry_sdk.utils import ContextVar, logger +from sentry_sdk.utils import ( + ContextVar, + logger, + get_type_name, + get_type_module, + get_error_message, + iter_stacks, +) from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Optional + from typing import Any, Optional from sentry_sdk._types import Event, Hint +def _create_exception_fingerprint(exc_info): + # type: (Any) -> str + """ + Creates a unique fingerprint for an exception based on type, message, and traceback. + + This replaces object identity comparison to prevent memory leaks while maintaining + accurate deduplication for the same exception (same type+message+traceback). + + Memory usage: 64 bytes (SHA256 hex string) for the last seen exception fingerprint. + """ + exc_type, exc_value, tb = exc_info + + if exc_type is None or exc_value is None: + return "" + + # Get exception type information + type_module = get_type_module(exc_type) or "" + type_name = get_type_name(exc_type) or "" + + # Get exception message + message = get_error_message(exc_value) + + # Create traceback fingerprint from top frames (limit to avoid excessive memory usage) + tb_parts = [] + frame_count = 0 + max_frames = 10 # Limit frames to keep memory usage low + + for tb_frame in iter_stacks(tb): + if frame_count >= max_frames: + break + + # Extract key frame information for fingerprint + filename = tb_frame.tb_frame.f_code.co_filename or "" + function_name = tb_frame.tb_frame.f_code.co_name or "" + line_number = str(tb_frame.tb_lineno) + + # Create a compact frame fingerprint + frame_fingerprint = "{}:{}:{}".format( + ( + filename.split("/")[-1] if "/" in filename else filename + ), # Just filename, not full path + function_name, + line_number, + ) + tb_parts.append(frame_fingerprint) + frame_count += 1 + + # Combine all parts for the complete fingerprint + fingerprint_parts = [type_module, type_name, message, "|".join(tb_parts)] + + # Create SHA256 hash of the combined fingerprint + fingerprint_data = "||".join(fingerprint_parts).encode("utf-8", errors="replace") + return hashlib.sha256(fingerprint_data).hexdigest() + + class DedupeIntegration(Integration): identifier = "dedupe" def __init__(self): # type: () -> None - self._last_seen = ContextVar("last-seen") + # Store fingerprint of the last seen exception instead of the exception object + # This prevents memory leaks by not holding references to exception objects + self._last_fingerprint = ContextVar("last-fingerprint", default=None) @staticmethod def setup_once(): @@ -35,19 +100,35 @@ def processor(event, hint): if exc_info is None: return event - exc = exc_info[1] - if integration._last_seen.get(None) is exc: - logger.info("DedupeIntegration dropped duplicated error event %s", exc) + # Create fingerprint from exception instead of storing the object + fingerprint = _create_exception_fingerprint(exc_info) + if not fingerprint: + return event + + # Check if this fingerprint matches the last seen one + last_fingerprint = integration._last_fingerprint.get() + if last_fingerprint == fingerprint: + logger.info( + "DedupeIntegration dropped duplicated error event with fingerprint %s", + fingerprint[:16], + ) return None - integration._last_seen.set(exc) + # Store this fingerprint as the last seen one + integration._last_fingerprint.set(fingerprint) return event @staticmethod def reset_last_seen(): # type: () -> None + """ + Resets the deduplication state, clearing the last seen exception fingerprint. + + This maintains the existing public API while working with the new + fingerprint-based implementation. + """ integration = sentry_sdk.get_client().get_integration(DedupeIntegration) if integration is None: return - integration._last_seen.set(None) + integration._last_fingerprint.set(None) From 2f66cd41fdbdf46a8e8d51a604e97798b8c809a3 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 17 Sep 2025 15:16:20 +0200 Subject: [PATCH 02/29] cleanup --- sentry_sdk/integrations/dedupe.py | 54 ++++++++++++++++++------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 4dbc153027..3247b24a7f 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -8,6 +8,7 @@ get_error_message, iter_stacks, ) +from sentry_sdk.tracing_utils import _should_be_included from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor @@ -19,58 +20,65 @@ from sentry_sdk._types import Event, Hint +def _is_frame_in_app(tb_frame): + # type: (Any) -> bool + client = sentry_sdk.get_client() + if not client.is_active(): + return True + + in_app_include = client.options.get("in_app_include") + in_app_exclude = client.options.get("in_app_exclude") + project_root = client.options.get("project_root") + + abs_path = tb_frame.tb_frame.f_code.co_filename + namespace = tb_frame.tb_frame.f_globals.get("__name__") + + return _should_be_included( + is_sentry_sdk_frame=False, + namespace=namespace, + in_app_include=in_app_include, + in_app_exclude=in_app_exclude, + abs_path=abs_path, + project_root=project_root, + ) + + def _create_exception_fingerprint(exc_info): # type: (Any) -> str """ - Creates a unique fingerprint for an exception based on type, message, and traceback. - - This replaces object identity comparison to prevent memory leaks while maintaining - accurate deduplication for the same exception (same type+message+traceback). - - Memory usage: 64 bytes (SHA256 hex string) for the last seen exception fingerprint. + Creates a unique fingerprint for an exception based on type, message, and in-app traceback. """ exc_type, exc_value, tb = exc_info if exc_type is None or exc_value is None: return "" - # Get exception type information type_module = get_type_module(exc_type) or "" type_name = get_type_name(exc_type) or "" - - # Get exception message message = get_error_message(exc_value) - # Create traceback fingerprint from top frames (limit to avoid excessive memory usage) tb_parts = [] frame_count = 0 - max_frames = 10 # Limit frames to keep memory usage low for tb_frame in iter_stacks(tb): - if frame_count >= max_frames: - break + if not _is_frame_in_app(tb_frame): + continue - # Extract key frame information for fingerprint - filename = tb_frame.tb_frame.f_code.co_filename or "" + file_path = tb_frame.tb_frame.f_code.co_filename or "" + file_name = file_path.split("/")[-1] if "/" in file_path else file_path function_name = tb_frame.tb_frame.f_code.co_name or "" line_number = str(tb_frame.tb_lineno) - - # Create a compact frame fingerprint frame_fingerprint = "{}:{}:{}".format( - ( - filename.split("/")[-1] if "/" in filename else filename - ), # Just filename, not full path + file_name, function_name, line_number, ) tb_parts.append(frame_fingerprint) frame_count += 1 - # Combine all parts for the complete fingerprint fingerprint_parts = [type_module, type_name, message, "|".join(tb_parts)] - - # Create SHA256 hash of the combined fingerprint fingerprint_data = "||".join(fingerprint_parts).encode("utf-8", errors="replace") + return hashlib.sha256(fingerprint_data).hexdigest() From 4e15ba1e4a0b9435538f9bc01c03a1c6b7f4d6c1 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 17 Sep 2025 15:23:52 +0200 Subject: [PATCH 03/29] refactor --- sentry_sdk/integrations/dedupe.py | 42 ++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 3247b24a7f..52f90760e2 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -15,21 +15,13 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Optional + from typing import Any, Optional, List from sentry_sdk._types import Event, Hint -def _is_frame_in_app(tb_frame): - # type: (Any) -> bool - client = sentry_sdk.get_client() - if not client.is_active(): - return True - - in_app_include = client.options.get("in_app_include") - in_app_exclude = client.options.get("in_app_exclude") - project_root = client.options.get("project_root") - +def _is_frame_in_app(tb_frame, in_app_include, in_app_exclude, project_root): + # type: (Any, Optional[List[str]], Optional[List[str]], Optional[str]) -> bool abs_path = tb_frame.tb_frame.f_code.co_filename namespace = tb_frame.tb_frame.f_globals.get("__name__") @@ -43,8 +35,10 @@ def _is_frame_in_app(tb_frame): ) -def _create_exception_fingerprint(exc_info): - # type: (Any) -> str +def _create_exception_fingerprint( + exc_info, in_app_include, in_app_exclude, project_root +): + # type: (Any, Optional[List[str]], Optional[List[str]], Optional[str]) -> str """ Creates a unique fingerprint for an exception based on type, message, and in-app traceback. """ @@ -61,7 +55,7 @@ def _create_exception_fingerprint(exc_info): frame_count = 0 for tb_frame in iter_stacks(tb): - if not _is_frame_in_app(tb_frame): + if not _is_frame_in_app(tb_frame, in_app_include, in_app_exclude, project_root): continue file_path = tb_frame.tb_frame.f_code.co_filename or "" @@ -87,13 +81,22 @@ class DedupeIntegration(Integration): def __init__(self): # type: () -> None - # Store fingerprint of the last seen exception instead of the exception object - # This prevents memory leaks by not holding references to exception objects self._last_fingerprint = ContextVar("last-fingerprint", default=None) + self.in_app_include = None # type: Optional[List[str]] + self.in_app_exclude = None # type: Optional[List[str]] + self.project_root = None # type: Optional[str] + @staticmethod def setup_once(): # type: () -> None + client = sentry_sdk.get_client() + integration = client.get_integration(DedupeIntegration) + if integration is not None: + integration.in_app_include = client.options.get("in_app_include") + integration.in_app_exclude = client.options.get("in_app_exclude") + integration.project_root = client.options.get("project_root") + @add_global_event_processor def processor(event, hint): # type: (Event, Optional[Hint]) -> Optional[Event] @@ -109,7 +112,12 @@ def processor(event, hint): return event # Create fingerprint from exception instead of storing the object - fingerprint = _create_exception_fingerprint(exc_info) + fingerprint = _create_exception_fingerprint( + exc_info, + integration.in_app_include, + integration.in_app_exclude, + integration.project_root, + ) if not fingerprint: return event From 9ad77e0f39d6386899361cf63e3bff5279027d66 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 17 Sep 2025 15:32:57 +0200 Subject: [PATCH 04/29] refactor --- sentry_sdk/integrations/dedupe.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 52f90760e2..62f72f933e 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -8,7 +8,7 @@ get_error_message, iter_stacks, ) -from sentry_sdk.tracing_utils import _should_be_included +from sentry_sdk.tracing_utils import _should_be_included, _get_frame_module_abs_path from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor @@ -20,11 +20,8 @@ from sentry_sdk._types import Event, Hint -def _is_frame_in_app(tb_frame, in_app_include, in_app_exclude, project_root): - # type: (Any, Optional[List[str]], Optional[List[str]], Optional[str]) -> bool - abs_path = tb_frame.tb_frame.f_code.co_filename - namespace = tb_frame.tb_frame.f_globals.get("__name__") - +def _is_frame_in_app(namespace, abs_path, in_app_include, in_app_exclude, project_root): + # type: (Any, Optional[str], Optional[List[str]], Optional[List[str]], Optional[str]) -> bool return _should_be_included( is_sentry_sdk_frame=False, namespace=namespace, @@ -55,13 +52,18 @@ def _create_exception_fingerprint( frame_count = 0 for tb_frame in iter_stacks(tb): - if not _is_frame_in_app(tb_frame, in_app_include, in_app_exclude, project_root): + abs_path = _get_frame_module_abs_path(tb_frame.tb_frame) or "" + namespace = tb_frame.tb_frame.f_globals.get("__name__") + + if not _is_frame_in_app( + namespace, abs_path, in_app_include, in_app_exclude, project_root + ): continue - file_path = tb_frame.tb_frame.f_code.co_filename or "" - file_name = file_path.split("/")[-1] if "/" in file_path else file_path + file_name = abs_path.split("/")[-1] if "/" in abs_path else abs_path function_name = tb_frame.tb_frame.f_code.co_name or "" line_number = str(tb_frame.tb_lineno) + frame_fingerprint = "{}:{}:{}".format( file_name, function_name, From 2b1de90c013e55a3a49c4ba2eee441b9735f13df Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 17 Sep 2025 15:40:56 +0200 Subject: [PATCH 05/29] Cleanup --- sentry_sdk/integrations/dedupe.py | 138 ++++++++++++++---------------- 1 file changed, 62 insertions(+), 76 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 62f72f933e..97faa77a04 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -20,84 +20,79 @@ from sentry_sdk._types import Event, Hint -def _is_frame_in_app(namespace, abs_path, in_app_include, in_app_exclude, project_root): - # type: (Any, Optional[str], Optional[List[str]], Optional[List[str]], Optional[str]) -> bool - return _should_be_included( - is_sentry_sdk_frame=False, - namespace=namespace, - in_app_include=in_app_include, - in_app_exclude=in_app_exclude, - abs_path=abs_path, - project_root=project_root, - ) - - -def _create_exception_fingerprint( - exc_info, in_app_include, in_app_exclude, project_root -): - # type: (Any, Optional[List[str]], Optional[List[str]], Optional[str]) -> str - """ - Creates a unique fingerprint for an exception based on type, message, and in-app traceback. - """ - exc_type, exc_value, tb = exc_info - - if exc_type is None or exc_value is None: - return "" - - type_module = get_type_module(exc_type) or "" - type_name = get_type_name(exc_type) or "" - message = get_error_message(exc_value) - - tb_parts = [] - frame_count = 0 - - for tb_frame in iter_stacks(tb): - abs_path = _get_frame_module_abs_path(tb_frame.tb_frame) or "" - namespace = tb_frame.tb_frame.f_globals.get("__name__") - - if not _is_frame_in_app( - namespace, abs_path, in_app_include, in_app_exclude, project_root - ): - continue - - file_name = abs_path.split("/")[-1] if "/" in abs_path else abs_path - function_name = tb_frame.tb_frame.f_code.co_name or "" - line_number = str(tb_frame.tb_lineno) - - frame_fingerprint = "{}:{}:{}".format( - file_name, - function_name, - line_number, - ) - tb_parts.append(frame_fingerprint) - frame_count += 1 - - fingerprint_parts = [type_module, type_name, message, "|".join(tb_parts)] - fingerprint_data = "||".join(fingerprint_parts).encode("utf-8", errors="replace") - - return hashlib.sha256(fingerprint_data).hexdigest() - - class DedupeIntegration(Integration): identifier = "dedupe" def __init__(self): # type: () -> None self._last_fingerprint = ContextVar("last-fingerprint", default=None) - - self.in_app_include = None # type: Optional[List[str]] - self.in_app_exclude = None # type: Optional[List[str]] + self.in_app_include = [] # type: List[str] + self.in_app_exclude = [] # type: List[str] self.project_root = None # type: Optional[str] + def _is_frame_in_app(self, namespace, abs_path): + # type: (Any, Optional[str], Optional[str]) -> bool + return _should_be_included( + is_sentry_sdk_frame=False, + namespace=namespace, + in_app_include=self.in_app_include, + in_app_exclude=self.in_app_exclude, + abs_path=abs_path, + project_root=self.project_root, + ) + + def _create_exception_fingerprint(self, exc_info): + # type: (Any) -> str + """ + Creates a unique fingerprint for an exception based on type, message, and in-app traceback. + """ + exc_type, exc_value, tb = exc_info + + if exc_type is None or exc_value is None: + return "" + + type_module = get_type_module(exc_type) or "" + type_name = get_type_name(exc_type) or "" + message = get_error_message(exc_value) + + tb_parts = [] + frame_count = 0 + + for tb_frame in iter_stacks(tb): + abs_path = _get_frame_module_abs_path(tb_frame.tb_frame) or "" + namespace = tb_frame.tb_frame.f_globals.get("__name__") + + if not self._is_frame_in_app(namespace, abs_path): + continue + + file_name = abs_path.split("/")[-1] if "/" in abs_path else abs_path + function_name = tb_frame.tb_frame.f_code.co_name or "" + line_number = str(tb_frame.tb_lineno) + + frame_fingerprint = "{}:{}:{}".format( + file_name, + function_name, + line_number, + ) + tb_parts.append(frame_fingerprint) + frame_count += 1 + + fingerprint_parts = [type_module, type_name, message, "|".join(tb_parts)] + fingerprint_data = "||".join(fingerprint_parts).encode( + "utf-8", errors="replace" + ) + + return hashlib.sha256(fingerprint_data).hexdigest() + @staticmethod def setup_once(): # type: () -> None client = sentry_sdk.get_client() integration = client.get_integration(DedupeIntegration) if integration is not None: - integration.in_app_include = client.options.get("in_app_include") - integration.in_app_exclude = client.options.get("in_app_exclude") - integration.project_root = client.options.get("project_root") + integration.in_app_include = client.options.get("in_app_include") or [] + integration.in_app_exclude = client.options.get("in_app_exclude") or [] + integration.project_root = client.options.get("project_root") or None @add_global_event_processor def processor(event, hint): @@ -113,19 +108,10 @@ def processor(event, hint): if exc_info is None: return event - # Create fingerprint from exception instead of storing the object - fingerprint = _create_exception_fingerprint( - exc_info, - integration.in_app_include, - integration.in_app_exclude, - integration.project_root, - ) - if not fingerprint: - return event - - # Check if this fingerprint matches the last seen one + fingerprint = integration._create_exception_fingerprint(exc_info) last_fingerprint = integration._last_fingerprint.get() - if last_fingerprint == fingerprint: + + if fingerprint == last_fingerprint: logger.info( "DedupeIntegration dropped duplicated error event with fingerprint %s", fingerprint[:16], From 6256765e29a94416ce39ce59d97a2b7f7f78a058 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 17 Sep 2025 15:55:46 +0200 Subject: [PATCH 06/29] updated test --- tests/integrations/launchdarkly/test_launchdarkly.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 20bb4d031f..9fa6d4c2e3 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -85,7 +85,9 @@ def task(flag_key): client.variation(flag_key, context, False) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) - sentry_sdk.capture_exception(Exception("something wrong!")) + sentry_sdk.capture_exception( + Exception(f"something wrong with task {flag_key}!") + ) # Capture an eval before we split isolation scopes. client.variation("hello", context, False) From 98c0bf6635f1e336fa9e3b5e352e4bea41793ff6 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 17 Sep 2025 15:57:14 +0200 Subject: [PATCH 07/29] updated test --- tests/integrations/openfeature/test_openfeature.py | 4 +++- tests/integrations/statsig/test_statsig.py | 4 +++- tests/integrations/unleash/test_unleash.py | 4 +++- tests/test_feature_flags.py | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index 46acc61ae7..460f1a89b0 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -63,7 +63,9 @@ def task(flag): client.get_boolean_value(flag, default_value=False) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag) - sentry_sdk.capture_exception(Exception("something wrong!")) + sentry_sdk.capture_exception( + Exception(f"something wrong with task {flag}!") + ) # Run tasks in separate threads with cf.ThreadPoolExecutor(max_workers=2) as pool: diff --git a/tests/integrations/statsig/test_statsig.py b/tests/integrations/statsig/test_statsig.py index 5eb2cf39f3..3c451de5c1 100644 --- a/tests/integrations/statsig/test_statsig.py +++ b/tests/integrations/statsig/test_statsig.py @@ -70,7 +70,9 @@ def task(flag_key): statsig.check_gate(user, flag_key) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) - sentry_sdk.capture_exception(Exception("something wrong!")) + sentry_sdk.capture_exception( + Exception(f"something wrong with task {flag_key}!") + ) with cf.ThreadPoolExecutor(max_workers=2) as pool: pool.map(task, ["world", "other"]) diff --git a/tests/integrations/unleash/test_unleash.py b/tests/integrations/unleash/test_unleash.py index 98a6188181..b456bf55ab 100644 --- a/tests/integrations/unleash/test_unleash.py +++ b/tests/integrations/unleash/test_unleash.py @@ -51,7 +51,9 @@ def task(flag_key): client.is_enabled(flag_key) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) - sentry_sdk.capture_exception(Exception("something wrong!")) + sentry_sdk.capture_exception( + Exception(f"something wrong with task {flag_key}!") + ) # Capture an eval before we split isolation scopes. client.is_enabled("hello") diff --git a/tests/test_feature_flags.py b/tests/test_feature_flags.py index e0ab1e254e..dd5b6462b2 100644 --- a/tests/test_feature_flags.py +++ b/tests/test_feature_flags.py @@ -104,7 +104,9 @@ def task(flag_key): add_feature_flag(flag_key, False) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) - sentry_sdk.capture_exception(Exception("something wrong!")) + sentry_sdk.capture_exception( + Exception(f"something wrong with task {flag_key}!") + ) # Run tasks in separate threads with cf.ThreadPoolExecutor(max_workers=2) as pool: From 0c810d9d74d360ca60147d93ce506cbedf2182a0 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 17 Sep 2025 16:05:25 +0200 Subject: [PATCH 08/29] cleanup --- sentry_sdk/integrations/dedupe.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 97faa77a04..286f55232d 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -25,7 +25,7 @@ class DedupeIntegration(Integration): def __init__(self): # type: () -> None - self._last_fingerprint = ContextVar("last-fingerprint", default=None) + self._last_seen = ContextVar("last-seen", default=None) self.in_app_include = [] # type: List[str] self.in_app_exclude = [] # type: List[str] self.project_root = None # type: Optional[str] @@ -109,7 +109,7 @@ def processor(event, hint): return event fingerprint = integration._create_exception_fingerprint(exc_info) - last_fingerprint = integration._last_fingerprint.get() + last_fingerprint = integration._last_seen.get() if fingerprint == last_fingerprint: logger.info( @@ -118,8 +118,7 @@ def processor(event, hint): ) return None - # Store this fingerprint as the last seen one - integration._last_fingerprint.set(fingerprint) + integration._last_seen.set(fingerprint) return event @staticmethod @@ -127,12 +126,9 @@ def reset_last_seen(): # type: () -> None """ Resets the deduplication state, clearing the last seen exception fingerprint. - - This maintains the existing public API while working with the new - fingerprint-based implementation. """ integration = sentry_sdk.get_client().get_integration(DedupeIntegration) if integration is None: return - integration._last_fingerprint.set(None) + integration._last_seen.set(None) From 72c76c83377ba6048da794aee51495179d50d479 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 17 Sep 2025 16:07:26 +0200 Subject: [PATCH 09/29] log message --- 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 286f55232d..449f69d3fd 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -113,7 +113,7 @@ def processor(event, hint): if fingerprint == last_fingerprint: logger.info( - "DedupeIntegration dropped duplicated error event with fingerprint %s", + "DedupeIntegration dropped duplicated error event %s (fingerprint)", fingerprint[:16], ) return None From 0271dc7259793bfe08df7335e767a0e600199fc1 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 17 Sep 2025 17:01:42 +0200 Subject: [PATCH 10/29] updated tests --- sentry_sdk/integrations/dedupe.py | 20 ++++++++++---------- tests/test_basics.py | 20 +++++++++++++++----- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 449f69d3fd..d64b1b9e74 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -26,8 +26,8 @@ class DedupeIntegration(Integration): def __init__(self): # type: () -> None self._last_seen = ContextVar("last-seen", default=None) - self.in_app_include = [] # type: List[str] - self.in_app_exclude = [] # type: List[str] + self.in_app_include = None # type: Optional[List[str]] + self.in_app_exclude = None # type: Optional[List[str]] self.project_root = None # type: Optional[str] def _is_frame_in_app(self, namespace, abs_path): @@ -87,23 +87,23 @@ def _create_exception_fingerprint(self, exc_info): @staticmethod def setup_once(): # type: () -> None - client = sentry_sdk.get_client() - integration = client.get_integration(DedupeIntegration) - if integration is not None: - integration.in_app_include = client.options.get("in_app_include") or [] - integration.in_app_exclude = client.options.get("in_app_exclude") or [] - integration.project_root = client.options.get("project_root") or None - @add_global_event_processor def processor(event, hint): # type: (Event, Optional[Hint]) -> Optional[Event] if hint is None: return event - integration = sentry_sdk.get_client().get_integration(DedupeIntegration) + client = sentry_sdk.get_client() + integration = client.get_integration(DedupeIntegration) + if integration is None: return event + if integration.in_app_include is None: + integration.in_app_include = client.options.get("in_app_include") or [] + integration.in_app_exclude = client.options.get("in_app_exclude") or [] + integration.project_root = client.options.get("project_root") or None + exc_info = hint.get("exc_info", None) if exc_info is None: return event diff --git a/tests/test_basics.py b/tests/test_basics.py index 45303c9a59..bc5cd46f1e 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -220,7 +220,9 @@ def before_breadcrumb(crumb, hint): crumb["data"] = {"foo": "bar"} return crumb - sentry_init(before_send=before_send, before_breadcrumb=before_breadcrumb) + sentry_init( + before_send=before_send, before_breadcrumb=before_breadcrumb + ) # , disabled_integrations=[DedupeIntegration]) events = capture_events() monkeypatch.setattr( @@ -230,7 +232,7 @@ def before_breadcrumb(crumb, hint): def do_this(): add_breadcrumb(message="Hello", hint={"foo": 42}) try: - raise ValueError("aha!") + raise ValueError(f"aha! {time.time()}") except Exception: capture_exception() @@ -368,7 +370,7 @@ def _(scope): def test_breadcrumbs(sentry_init, capture_events): - sentry_init(max_breadcrumbs=10) + sentry_init(max_breadcrumbs=10, in_app_include=["tests"]) events = capture_events() for i in range(20): @@ -376,7 +378,11 @@ def test_breadcrumbs(sentry_init, capture_events): category="auth", message="Authenticated user %s" % i, level="info" ) - capture_exception(ValueError()) + try: + raise ValueError() + except Exception: + capture_exception() + (event,) = events assert len(event["breadcrumbs"]["values"]) == 10 @@ -392,7 +398,11 @@ def test_breadcrumbs(sentry_init, capture_events): sentry_sdk.get_isolation_scope().clear() - capture_exception(ValueError()) + try: + raise ValueError() + except Exception: + capture_exception() + (event,) = events assert len(event["breadcrumbs"]["values"]) == 0 From cbf0fc0996518b48f73246069e8ec5bb80c21f77 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 09:33:44 +0200 Subject: [PATCH 11/29] update tests --- tests/test_api.py | 13 ++++++++----- tests/test_basics.py | 17 +++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index acc33cdf4c..f3f6cbf0b2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -137,13 +137,16 @@ def test_get_client(): assert not client.is_active() -def raise_and_capture(): +def raise_and_capture(message): """Raise an exception and capture it. This is a utility function for test_set_tags. """ try: - 1 / 0 + if message: + raise ZeroDivisionError(message) + else: + 1 / 0 except ZeroDivisionError: capture_exception() @@ -153,13 +156,13 @@ def test_set_tags(sentry_init, capture_events): events = capture_events() set_tags({"tag1": "value1", "tag2": "value2"}) - raise_and_capture() + raise_and_capture("one") (*_, event) = events assert event["tags"] == {"tag1": "value1", "tag2": "value2"}, "Setting tags failed" set_tags({"tag2": "updated", "tag3": "new"}) - raise_and_capture() + raise_and_capture("two") (*_, event) = events assert event["tags"] == { @@ -169,7 +172,7 @@ def test_set_tags(sentry_init, capture_events): }, "Updating tags failed" set_tags({}) - raise_and_capture() + raise_and_capture("three") (*_, event) = events assert event["tags"] == { diff --git a/tests/test_basics.py b/tests/test_basics.py index bc5cd46f1e..b651491a51 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -35,7 +35,7 @@ from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.stdlib import StdlibIntegration from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import get_sdk_name, reraise +from sentry_sdk.utils import get_sdk_name from sentry_sdk.tracing_utils import has_tracing_enabled @@ -762,14 +762,15 @@ def test_dedupe_event_processor_drop_records_client_report( events = capture_events() record_lost_event_calls = capture_record_lost_event_calls() - try: - raise ValueError("aha!") - except Exception: + for x in range(3): try: - capture_exception() - reraise(*sys.exc_info()) - except Exception: - capture_exception() + if x < 2: + div = x # fails for x = 0 + else: + div = x - 2 # fails for x = 2 + 1 / div # fails twice in the loop + except Exception as e: + capture_exception(e) (event,) = events (lost_event_call,) = record_lost_event_calls From 10564dc27941ac73d2f8be9b08ae1f2ba186d9e2 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 10:16:29 +0200 Subject: [PATCH 12/29] new test --- tests/integrations/bottle/test_bottle.py | 6 ++++- tests/test_basics.py | 31 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/integrations/bottle/test_bottle.py b/tests/integrations/bottle/test_bottle.py index 1965691d6c..98de7fb184 100644 --- a/tests/integrations/bottle/test_bottle.py +++ b/tests/integrations/bottle/test_bottle.py @@ -344,7 +344,11 @@ def index(): logger.exception(e) raise e - assert len(events) == 1 + # With the new fingerprint-based deduplication, the bottle integration + # and logging integration capture the exception with different tracebacks, + # so they are no longer deduplicated. This is arguably more correct behavior. + expected_events = 2 if len(integrations) > 1 else 1 + assert len(events) == expected_events def test_mount(app, capture_exceptions, capture_events, sentry_init, get_client): diff --git a/tests/test_basics.py b/tests/test_basics.py index b651491a51..fcb94ad3ac 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -780,6 +780,37 @@ def test_dedupe_event_processor_drop_records_client_report( assert lost_event_call == ("event_processor", "error", None, 1) +def test_dedupe_with_logging_integration( + sentry_init, capture_events, capture_record_lost_event_calls +): + logger = logging.Logger("some-logger") + + sentry_init(integrations=[LoggingIntegration(event_level="ERROR")]) + events = capture_events() + record_lost_event_calls = capture_record_lost_event_calls() + + try: + 1 / 0 + except Exception as e: + logger.exception(e) # captures an error + capture_exception() # captures an error + + (event,) = events + (lost_event_call,) = record_lost_event_calls + + assert len(events) == 1 + assert event["exception"]["values"][0]["type"] == "ZeroDivisionError" + assert ( + event["exception"]["values"][0]["mechanism"]["type"] == "logging" + ) # the exception captured by LoggingIntegration + assert lost_event_call == ( + "event_processor", + "error", + None, + 1, + ) # one exception was dropped by DedupeIntegration + + def test_dedupe_doesnt_take_into_account_dropped_exception(sentry_init, capture_events): # Two exceptions happen one after another. The first one is dropped in the # user's before_send. The second one isn't. From 500d91a8272a5ebcb8bdf4a47ac24daa81293949 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:05:55 +0200 Subject: [PATCH 13/29] udpate tests --- tests/integrations/bottle/test_bottle.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/integrations/bottle/test_bottle.py b/tests/integrations/bottle/test_bottle.py index 98de7fb184..d0f77dcb0b 100644 --- a/tests/integrations/bottle/test_bottle.py +++ b/tests/integrations/bottle/test_bottle.py @@ -342,13 +342,9 @@ def index(): client.get("/") except ZeroDivisionError as e: logger.exception(e) - raise e + raise e from None - # With the new fingerprint-based deduplication, the bottle integration - # and logging integration capture the exception with different tracebacks, - # so they are no longer deduplicated. This is arguably more correct behavior. - expected_events = 2 if len(integrations) > 1 else 1 - assert len(events) == expected_events + assert len(events) == 1 def test_mount(app, capture_exceptions, capture_events, sentry_init, get_client): From 253e8ba31a13f4355e323e14f709c7a875b4ad8c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:43:59 +0200 Subject: [PATCH 14/29] use object id --- sentry_sdk/integrations/dedupe.py | 52 +++---------------------------- 1 file changed, 4 insertions(+), 48 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index d64b1b9e74..acdc67cc71 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -5,17 +5,14 @@ logger, get_type_name, get_type_module, - get_error_message, - iter_stacks, ) -from sentry_sdk.tracing_utils import _should_be_included, _get_frame_module_abs_path from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Optional, List + from typing import Any, Optional from sentry_sdk._types import Event, Hint @@ -26,25 +23,11 @@ class DedupeIntegration(Integration): def __init__(self): # type: () -> None self._last_seen = ContextVar("last-seen", default=None) - self.in_app_include = None # type: Optional[List[str]] - self.in_app_exclude = None # type: Optional[List[str]] - self.project_root = None # type: Optional[str] - - def _is_frame_in_app(self, namespace, abs_path): - # type: (Any, Optional[str], Optional[str]) -> bool - return _should_be_included( - is_sentry_sdk_frame=False, - namespace=namespace, - in_app_include=self.in_app_include, - in_app_exclude=self.in_app_exclude, - abs_path=abs_path, - project_root=self.project_root, - ) def _create_exception_fingerprint(self, exc_info): # type: (Any) -> str """ - Creates a unique fingerprint for an exception based on type, message, and in-app traceback. + Creates a unique fingerprint for an exception based on type, message, and object id. """ exc_type, exc_value, tb = exc_info @@ -53,31 +36,9 @@ def _create_exception_fingerprint(self, exc_info): type_module = get_type_module(exc_type) or "" type_name = get_type_name(exc_type) or "" - message = get_error_message(exc_value) - - tb_parts = [] - frame_count = 0 - - for tb_frame in iter_stacks(tb): - abs_path = _get_frame_module_abs_path(tb_frame.tb_frame) or "" - namespace = tb_frame.tb_frame.f_globals.get("__name__") - - if not self._is_frame_in_app(namespace, abs_path): - continue + exception_id = str(id(exc_value)) - file_name = abs_path.split("/")[-1] if "/" in abs_path else abs_path - function_name = tb_frame.tb_frame.f_code.co_name or "" - line_number = str(tb_frame.tb_lineno) - - frame_fingerprint = "{}:{}:{}".format( - file_name, - function_name, - line_number, - ) - tb_parts.append(frame_fingerprint) - frame_count += 1 - - fingerprint_parts = [type_module, type_name, message, "|".join(tb_parts)] + fingerprint_parts = [type_module, type_name, exception_id] fingerprint_data = "||".join(fingerprint_parts).encode( "utf-8", errors="replace" ) @@ -99,11 +60,6 @@ def processor(event, hint): if integration is None: return event - if integration.in_app_include is None: - integration.in_app_include = client.options.get("in_app_include") or [] - integration.in_app_exclude = client.options.get("in_app_exclude") or [] - integration.project_root = client.options.get("project_root") or None - exc_info = hint.get("exc_info", None) if exc_info is None: return event From 560cc41d40b7b1f0e93a4b1fc9469ed6b6b243e5 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:44:26 +0200 Subject: [PATCH 15/29] Revert "udpate tests" This reverts commit 500d91a8272a5ebcb8bdf4a47ac24daa81293949. --- tests/integrations/bottle/test_bottle.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/integrations/bottle/test_bottle.py b/tests/integrations/bottle/test_bottle.py index d0f77dcb0b..98de7fb184 100644 --- a/tests/integrations/bottle/test_bottle.py +++ b/tests/integrations/bottle/test_bottle.py @@ -342,9 +342,13 @@ def index(): client.get("/") except ZeroDivisionError as e: logger.exception(e) - raise e from None + raise e - assert len(events) == 1 + # With the new fingerprint-based deduplication, the bottle integration + # and logging integration capture the exception with different tracebacks, + # so they are no longer deduplicated. This is arguably more correct behavior. + expected_events = 2 if len(integrations) > 1 else 1 + assert len(events) == expected_events def test_mount(app, capture_exceptions, capture_events, sentry_init, get_client): From 0c729ec0f754bef76b7a2b77488ee8cc13215e30 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:44:29 +0200 Subject: [PATCH 16/29] Revert "new test" This reverts commit 10564dc27941ac73d2f8be9b08ae1f2ba186d9e2. --- tests/integrations/bottle/test_bottle.py | 6 +---- tests/test_basics.py | 31 ------------------------ 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/tests/integrations/bottle/test_bottle.py b/tests/integrations/bottle/test_bottle.py index 98de7fb184..1965691d6c 100644 --- a/tests/integrations/bottle/test_bottle.py +++ b/tests/integrations/bottle/test_bottle.py @@ -344,11 +344,7 @@ def index(): logger.exception(e) raise e - # With the new fingerprint-based deduplication, the bottle integration - # and logging integration capture the exception with different tracebacks, - # so they are no longer deduplicated. This is arguably more correct behavior. - expected_events = 2 if len(integrations) > 1 else 1 - assert len(events) == expected_events + assert len(events) == 1 def test_mount(app, capture_exceptions, capture_events, sentry_init, get_client): diff --git a/tests/test_basics.py b/tests/test_basics.py index fcb94ad3ac..b651491a51 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -780,37 +780,6 @@ def test_dedupe_event_processor_drop_records_client_report( assert lost_event_call == ("event_processor", "error", None, 1) -def test_dedupe_with_logging_integration( - sentry_init, capture_events, capture_record_lost_event_calls -): - logger = logging.Logger("some-logger") - - sentry_init(integrations=[LoggingIntegration(event_level="ERROR")]) - events = capture_events() - record_lost_event_calls = capture_record_lost_event_calls() - - try: - 1 / 0 - except Exception as e: - logger.exception(e) # captures an error - capture_exception() # captures an error - - (event,) = events - (lost_event_call,) = record_lost_event_calls - - assert len(events) == 1 - assert event["exception"]["values"][0]["type"] == "ZeroDivisionError" - assert ( - event["exception"]["values"][0]["mechanism"]["type"] == "logging" - ) # the exception captured by LoggingIntegration - assert lost_event_call == ( - "event_processor", - "error", - None, - 1, - ) # one exception was dropped by DedupeIntegration - - def test_dedupe_doesnt_take_into_account_dropped_exception(sentry_init, capture_events): # Two exceptions happen one after another. The first one is dropped in the # user's before_send. The second one isn't. From 149ac81011bb67b32b482ff8d24f976947f06865 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:44:31 +0200 Subject: [PATCH 17/29] Revert "update tests" This reverts commit cbf0fc0996518b48f73246069e8ec5bb80c21f77. --- tests/test_api.py | 13 +++++-------- tests/test_basics.py | 17 ++++++++--------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index f3f6cbf0b2..acc33cdf4c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -137,16 +137,13 @@ def test_get_client(): assert not client.is_active() -def raise_and_capture(message): +def raise_and_capture(): """Raise an exception and capture it. This is a utility function for test_set_tags. """ try: - if message: - raise ZeroDivisionError(message) - else: - 1 / 0 + 1 / 0 except ZeroDivisionError: capture_exception() @@ -156,13 +153,13 @@ def test_set_tags(sentry_init, capture_events): events = capture_events() set_tags({"tag1": "value1", "tag2": "value2"}) - raise_and_capture("one") + raise_and_capture() (*_, event) = events assert event["tags"] == {"tag1": "value1", "tag2": "value2"}, "Setting tags failed" set_tags({"tag2": "updated", "tag3": "new"}) - raise_and_capture("two") + raise_and_capture() (*_, event) = events assert event["tags"] == { @@ -172,7 +169,7 @@ def test_set_tags(sentry_init, capture_events): }, "Updating tags failed" set_tags({}) - raise_and_capture("three") + raise_and_capture() (*_, event) = events assert event["tags"] == { diff --git a/tests/test_basics.py b/tests/test_basics.py index b651491a51..bc5cd46f1e 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -35,7 +35,7 @@ from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.stdlib import StdlibIntegration from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import get_sdk_name +from sentry_sdk.utils import get_sdk_name, reraise from sentry_sdk.tracing_utils import has_tracing_enabled @@ -762,15 +762,14 @@ def test_dedupe_event_processor_drop_records_client_report( events = capture_events() record_lost_event_calls = capture_record_lost_event_calls() - for x in range(3): + try: + raise ValueError("aha!") + except Exception: try: - if x < 2: - div = x # fails for x = 0 - else: - div = x - 2 # fails for x = 2 - 1 / div # fails twice in the loop - except Exception as e: - capture_exception(e) + capture_exception() + reraise(*sys.exc_info()) + except Exception: + capture_exception() (event,) = events (lost_event_call,) = record_lost_event_calls From 1a82df5ab0a72bb0d7a4abd3d3d030014d6d6f09 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:45:16 +0200 Subject: [PATCH 18/29] Revert "updated tests" This reverts commit 0271dc7259793bfe08df7335e767a0e600199fc1. --- sentry_sdk/integrations/dedupe.py | 11 ++++++++--- tests/test_basics.py | 20 +++++--------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index acdc67cc71..c46142af97 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -48,15 +48,20 @@ def _create_exception_fingerprint(self, exc_info): @staticmethod def setup_once(): # type: () -> None + client = sentry_sdk.get_client() + integration = client.get_integration(DedupeIntegration) + if integration is not None: + integration.in_app_include = client.options.get("in_app_include") or [] + integration.in_app_exclude = client.options.get("in_app_exclude") or [] + integration.project_root = client.options.get("project_root") or None + @add_global_event_processor def processor(event, hint): # type: (Event, Optional[Hint]) -> Optional[Event] if hint is None: return event - client = sentry_sdk.get_client() - integration = client.get_integration(DedupeIntegration) - + integration = sentry_sdk.get_client().get_integration(DedupeIntegration) if integration is None: return event diff --git a/tests/test_basics.py b/tests/test_basics.py index bc5cd46f1e..45303c9a59 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -220,9 +220,7 @@ def before_breadcrumb(crumb, hint): crumb["data"] = {"foo": "bar"} return crumb - sentry_init( - before_send=before_send, before_breadcrumb=before_breadcrumb - ) # , disabled_integrations=[DedupeIntegration]) + sentry_init(before_send=before_send, before_breadcrumb=before_breadcrumb) events = capture_events() monkeypatch.setattr( @@ -232,7 +230,7 @@ def before_breadcrumb(crumb, hint): def do_this(): add_breadcrumb(message="Hello", hint={"foo": 42}) try: - raise ValueError(f"aha! {time.time()}") + raise ValueError("aha!") except Exception: capture_exception() @@ -370,7 +368,7 @@ def _(scope): def test_breadcrumbs(sentry_init, capture_events): - sentry_init(max_breadcrumbs=10, in_app_include=["tests"]) + sentry_init(max_breadcrumbs=10) events = capture_events() for i in range(20): @@ -378,11 +376,7 @@ def test_breadcrumbs(sentry_init, capture_events): category="auth", message="Authenticated user %s" % i, level="info" ) - try: - raise ValueError() - except Exception: - capture_exception() - + capture_exception(ValueError()) (event,) = events assert len(event["breadcrumbs"]["values"]) == 10 @@ -398,11 +392,7 @@ def test_breadcrumbs(sentry_init, capture_events): sentry_sdk.get_isolation_scope().clear() - try: - raise ValueError() - except Exception: - capture_exception() - + capture_exception(ValueError()) (event,) = events assert len(event["breadcrumbs"]["values"]) == 0 From d7568d5429a1095b361a656bd879d14d69a69239 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:45:19 +0200 Subject: [PATCH 19/29] Revert "log message" This reverts commit 72c76c83377ba6048da794aee51495179d50d479. --- 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 c46142af97..6983643298 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -74,7 +74,7 @@ def processor(event, hint): if fingerprint == last_fingerprint: logger.info( - "DedupeIntegration dropped duplicated error event %s (fingerprint)", + "DedupeIntegration dropped duplicated error event with fingerprint %s", fingerprint[:16], ) return None From 0b91f44a7e6f8c87e5ed6811afcc50e179831f7c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:45:42 +0200 Subject: [PATCH 20/29] Revert "cleanup" This reverts commit 0c810d9d74d360ca60147d93ce506cbedf2182a0. --- sentry_sdk/integrations/dedupe.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 6983643298..e4c05e06f5 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -70,7 +70,7 @@ def processor(event, hint): return event fingerprint = integration._create_exception_fingerprint(exc_info) - last_fingerprint = integration._last_seen.get() + last_fingerprint = integration._last_fingerprint.get() if fingerprint == last_fingerprint: logger.info( @@ -79,7 +79,8 @@ def processor(event, hint): ) return None - integration._last_seen.set(fingerprint) + # Store this fingerprint as the last seen one + integration._last_fingerprint.set(fingerprint) return event @staticmethod @@ -87,9 +88,12 @@ def reset_last_seen(): # type: () -> None """ Resets the deduplication state, clearing the last seen exception fingerprint. + + This maintains the existing public API while working with the new + fingerprint-based implementation. """ integration = sentry_sdk.get_client().get_integration(DedupeIntegration) if integration is None: return - integration._last_seen.set(None) + integration._last_fingerprint.set(None) From c10fb5c8b71d72c661c0a03999636ec967def616 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:45:44 +0200 Subject: [PATCH 21/29] Revert "updated test" This reverts commit 98c0bf6635f1e336fa9e3b5e352e4bea41793ff6. --- tests/integrations/openfeature/test_openfeature.py | 4 +--- tests/integrations/statsig/test_statsig.py | 4 +--- tests/integrations/unleash/test_unleash.py | 4 +--- tests/test_feature_flags.py | 4 +--- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index 460f1a89b0..46acc61ae7 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -63,9 +63,7 @@ def task(flag): client.get_boolean_value(flag, default_value=False) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag) - sentry_sdk.capture_exception( - Exception(f"something wrong with task {flag}!") - ) + sentry_sdk.capture_exception(Exception("something wrong!")) # Run tasks in separate threads with cf.ThreadPoolExecutor(max_workers=2) as pool: diff --git a/tests/integrations/statsig/test_statsig.py b/tests/integrations/statsig/test_statsig.py index 3c451de5c1..5eb2cf39f3 100644 --- a/tests/integrations/statsig/test_statsig.py +++ b/tests/integrations/statsig/test_statsig.py @@ -70,9 +70,7 @@ def task(flag_key): statsig.check_gate(user, flag_key) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) - sentry_sdk.capture_exception( - Exception(f"something wrong with task {flag_key}!") - ) + sentry_sdk.capture_exception(Exception("something wrong!")) with cf.ThreadPoolExecutor(max_workers=2) as pool: pool.map(task, ["world", "other"]) diff --git a/tests/integrations/unleash/test_unleash.py b/tests/integrations/unleash/test_unleash.py index b456bf55ab..98a6188181 100644 --- a/tests/integrations/unleash/test_unleash.py +++ b/tests/integrations/unleash/test_unleash.py @@ -51,9 +51,7 @@ def task(flag_key): client.is_enabled(flag_key) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) - sentry_sdk.capture_exception( - Exception(f"something wrong with task {flag_key}!") - ) + sentry_sdk.capture_exception(Exception("something wrong!")) # Capture an eval before we split isolation scopes. client.is_enabled("hello") diff --git a/tests/test_feature_flags.py b/tests/test_feature_flags.py index dd5b6462b2..e0ab1e254e 100644 --- a/tests/test_feature_flags.py +++ b/tests/test_feature_flags.py @@ -104,9 +104,7 @@ def task(flag_key): add_feature_flag(flag_key, False) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) - sentry_sdk.capture_exception( - Exception(f"something wrong with task {flag_key}!") - ) + sentry_sdk.capture_exception(Exception("something wrong!")) # Run tasks in separate threads with cf.ThreadPoolExecutor(max_workers=2) as pool: From 8c4a127318fcfcedf83646e9c59cc59a50edf5de Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:46:40 +0200 Subject: [PATCH 22/29] cleanup --- sentry_sdk/integrations/dedupe.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index e4c05e06f5..5c3e1dca61 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -48,13 +48,6 @@ def _create_exception_fingerprint(self, exc_info): @staticmethod def setup_once(): # type: () -> None - client = sentry_sdk.get_client() - integration = client.get_integration(DedupeIntegration) - if integration is not None: - integration.in_app_include = client.options.get("in_app_include") or [] - integration.in_app_exclude = client.options.get("in_app_exclude") or [] - integration.project_root = client.options.get("project_root") or None - @add_global_event_processor def processor(event, hint): # type: (Event, Optional[Hint]) -> Optional[Event] From ea4a29ef472641b819879346a57ea235acb3f8ae Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:47:20 +0200 Subject: [PATCH 23/29] cleanup --- tests/integrations/launchdarkly/test_launchdarkly.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 9fa6d4c2e3..20bb4d031f 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -85,9 +85,7 @@ def task(flag_key): client.variation(flag_key, context, False) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) - sentry_sdk.capture_exception( - Exception(f"something wrong with task {flag_key}!") - ) + sentry_sdk.capture_exception(Exception("something wrong!")) # Capture an eval before we split isolation scopes. client.variation("hello", context, False) From cf2377649786e94e9e6fdf23148c7cc793dffc64 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:47:59 +0200 Subject: [PATCH 24/29] cleanup --- 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 5c3e1dca61..c4744b9c9e 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -89,4 +89,4 @@ def reset_last_seen(): if integration is None: return - integration._last_fingerprint.set(None) + integration._last_seen.set(None) From 75c1ecd4f343890f242646722303c2ff8b2d8694 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:48:14 +0200 Subject: [PATCH 25/29] cleanup --- sentry_sdk/integrations/dedupe.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index c4744b9c9e..2193ced8f9 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -81,9 +81,6 @@ def reset_last_seen(): # type: () -> None """ Resets the deduplication state, clearing the last seen exception fingerprint. - - This maintains the existing public API while working with the new - fingerprint-based implementation. """ integration = sentry_sdk.get_client().get_integration(DedupeIntegration) if integration is None: From 04fe8ff17f2ba70452eec79d3dbb2b01e0d96a09 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:48:45 +0200 Subject: [PATCH 26/29] cleanup --- 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 2193ced8f9..3ff9fbed39 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -73,7 +73,7 @@ def processor(event, hint): return None # Store this fingerprint as the last seen one - integration._last_fingerprint.set(fingerprint) + integration._last_seen.set(fingerprint) return event @staticmethod From ce6296e3c02460edd25f4ef9f652269a7eac5986 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:49:27 +0200 Subject: [PATCH 27/29] cleanup --- 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 3ff9fbed39..5087241e70 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -63,7 +63,7 @@ def processor(event, hint): return event fingerprint = integration._create_exception_fingerprint(exc_info) - last_fingerprint = integration._last_fingerprint.get() + last_fingerprint = integration._last_seen.get() if fingerprint == last_fingerprint: logger.info( From a1d0c3982e234482582b9869c8a9667689566c06 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:50:58 +0200 Subject: [PATCH 28/29] cleanup --- sentry_sdk/integrations/dedupe.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 5087241e70..1d913db586 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -62,9 +62,11 @@ def processor(event, hint): if exc_info is None: return event - fingerprint = integration._create_exception_fingerprint(exc_info) last_fingerprint = integration._last_seen.get() + if not last_fingerprint: + return event + fingerprint = integration._create_exception_fingerprint(exc_info) if fingerprint == last_fingerprint: logger.info( "DedupeIntegration dropped duplicated error event with fingerprint %s", From 9ad51334be6c028265c9d237d9a29f69aab5b92c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 14:51:53 +0200 Subject: [PATCH 29/29] cleanup --- sentry_sdk/integrations/dedupe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 1d913db586..c60972541c 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -74,7 +74,6 @@ def processor(event, hint): ) return None - # Store this fingerprint as the last seen one integration._last_seen.set(fingerprint) return event