From 56e1d05acdcdc62d1ff0d6eebadc22f2aaa85f41 Mon Sep 17 00:00:00 2001 From: calm329 Date: Mon, 5 Jan 2026 16:40:35 -0800 Subject: [PATCH 01/12] fix(sdk-crashes): Ignore SentrySwizzleWrapper false positives (#105625) --- .../sdk_crashes/sdk_crash_detection_config.py | 18 ++ .../utils/sdk_crashes/sdk_crash_detector.py | 32 +++- .../test_sdk_crash_detection_cocoa.py | 169 +++++++++++++++++- 3 files changed, 217 insertions(+), 2 deletions(-) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py b/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py index 797447a05f61ec..74b5980825ff67 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py @@ -83,6 +83,14 @@ class SDKCrashDetectionConfig: sdk_frame_config: SDKFrameConfig """The function and module patterns to ignore when detecting SDK crashes. For example, FunctionAndModulePattern("*", "**SentrySDK crash**") for any module with that function""" sdk_crash_ignore_matchers: set[FunctionAndModulePattern] + """The function patterns to ignore when they are the only SDK frames in the stacktrace. + These frames are typically SDK instrumentation frames that intercept calls but don't cause crashes themselves. + If there are other SDK frames above these frames, the crash is still reported as an SDK crash. + For example, SentrySwizzleWrapper is used for method swizzling and shouldn't be reported as an SDK crash + when it's the only SDK frame, since the actual crash originates from system libraries.""" + sdk_crash_ignore_when_only_sdk_frame_matchers: set[FunctionAndModulePattern] = field( + default_factory=set + ) class SDKCrashDetectionOptions(TypedDict): @@ -152,6 +160,16 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]: function_pattern="**SentryCrashExceptionApplicationHelper _crashOnException**", ), }, + sdk_crash_ignore_when_only_sdk_frame_matchers={ + # SentrySwizzleWrapper is used for method swizzling to intercept UI events. + # When it's the only SDK frame, the crash originates from system libraries, + # not from the SDK. Only report as SDK crash if there are other SDK frames + # above SentrySwizzleWrapper in the call stack. + FunctionAndModulePattern( + module_pattern="*", + function_pattern="**SentrySwizzleWrapper**", + ), + }, ) configs.append(cocoa_config) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index 07c495b149a03b..ae5ae3e41399cb 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -102,13 +102,43 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: return False if self.is_sdk_frame(frame): - return True + # Check if this SDK frame matches the "ignore when only SDK frame" pattern. + # These are instrumentation frames that don't cause crashes themselves. + if self._matches_ignore_when_only_sdk_frame(frame): + # Found a conditional SDK frame (like SentrySwizzleWrapper). + # Since we iterate from youngest to oldest, we've already checked all + # frames above this one (closer to the crash). If there were any + # non-conditional SDK frames above, we would have returned True already. + # Since there weren't, the crash originates from system libraries, + # not the SDK. + return False + else: + # Found a non-conditional SDK frame, this is definitely an SDK crash + return True if not self.is_system_library_frame(frame): return False return False + def _matches_ignore_when_only_sdk_frame(self, frame: Mapping[str, Any]) -> bool: + """ + Returns true if the frame matches the sdk_crash_ignore_when_only_sdk_frame_matchers pattern. + """ + function = frame.get("function") + if not function: + return False + + module = frame.get("module") + for matcher in self.config.sdk_crash_ignore_when_only_sdk_frame_matchers: + function_matches = glob_match(function, matcher.function_pattern, ignorecase=True) + module_matches = glob_match(module, matcher.module_pattern, ignorecase=True) + + if function_matches and module_matches: + return True + + return False + def is_sdk_frame(self, frame: Mapping[str, Any]) -> bool: """ Returns true if frame is an SDK frame. diff --git a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py index 887c0b3b62c484..1b00acb5063fb3 100644 --- a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py +++ b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py @@ -653,8 +653,175 @@ def test_sentrycrash_exception_application_helper_not_reported( ) +@patch("sentry.utils.sdk_crashes.sdk_crash_detection.sdk_crash_detection.sdk_crash_reporter") +class CocoaSDKSwizzleWrapperTestMixin(BaseSDKCrashDetectionMixin): + """Tests for SentrySwizzleWrapper conditional SDK crash detection. + + SentrySwizzleWrapper is used for method swizzling to intercept UI events. + When it's the only SDK frame, the crash originates from system libraries, + not from the SDK. Only report as SDK crash if there are other SDK frames + above SentrySwizzleWrapper in the call stack. + + Note: Frames are ordered from oldest (caller) to youngest (exception). + "Above" means closer to the exception (younger frames). + """ + + def test_swizzle_wrapper_only_sdk_frame_not_reported( + self, mock_sdk_crash_reporter: MagicMock + ) -> None: + """ + SentrySwizzleWrapper is the only SDK frame in the stack. + This is a false positive - the crash originates from system libraries. + """ + # Frames ordered from oldest (caller) to youngest (exception) + frames = [ + { + "function": "-[UIApplication sendEvent:]", + "package": "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", + "in_app": False, + }, + { + "function": "-[UIGestureRecognizer _updateGestureForActiveEvents]", + "package": "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", + "in_app": False, + }, + { + "function": "-[UITextMultiTapRecognizer onStateUpdate:]", + "package": "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", + "in_app": False, + }, + { + "function": "__49-[SentrySwizzleWrapper swizzleSendAction:forKey:]_block_invoke_2", + "package": "/private/var/containers/Bundle/Application/59E988EF-46DB-4C75-8E08-10C27DC3E90E/iOS-Swift.app/Frameworks/Sentry.framework/Sentry", + "in_app": False, + }, + { + "function": "-[UITextInputController insertDictationResult:withCorrectionIdentifier:]", + "package": "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", + "in_app": False, + }, + { + "function": "-[NSString substringWithRange:]", + "package": "/System/Library/Frameworks/Foundation.framework/Foundation", + "in_app": False, + }, + { + "function": "objc_exception_throw", + "package": "/usr/lib/libobjc.A.dylib", + "in_app": False, + }, + { + "function": "__exceptionPreprocess", + "package": "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", + "in_app": False, + }, + ] + + self.execute_test( + get_crash_event_with_frames(frames), + False, # Should NOT be reported + mock_sdk_crash_reporter, + ) + + def test_swizzle_wrapper_with_other_sdk_frames_above_reported( + self, mock_sdk_crash_reporter: MagicMock + ) -> None: + """ + SentrySwizzleWrapper is in the stack, but there are other SDK frames above it + (closer to the crash origin). This IS an SDK crash. + """ + # Frames ordered from oldest (caller) to youngest (exception) + frames = [ + { + "function": "-[UIApplication sendEvent:]", + "package": "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", + "in_app": False, + }, + { + "function": "__49-[SentrySwizzleWrapper swizzleSendAction:forKey:]_block_invoke_2", + "package": "/private/var/containers/Bundle/Application/59E988EF-46DB-4C75-8E08-10C27DC3E90E/iOS-Swift.app/Frameworks/Sentry.framework/Sentry", + "in_app": False, + }, + { + "function": "-[UITextInputController insertDictationResult:withCorrectionIdentifier:]", + "package": "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", + "in_app": False, + }, + { + # This is another SDK frame ABOVE SentrySwizzleWrapper (closer to crash) + "function": "-[SentryHub captureEvent:]", + "package": "/private/var/containers/Bundle/Application/59E988EF-46DB-4C75-8E08-10C27DC3E90E/iOS-Swift.app/Frameworks/Sentry.framework/Sentry", + "in_app": False, + }, + { + "function": "objc_exception_throw", + "package": "/usr/lib/libobjc.A.dylib", + "in_app": False, + }, + { + "function": "__exceptionPreprocess", + "package": "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", + "in_app": False, + }, + ] + + self.execute_test( + get_crash_event_with_frames(frames), + True, # Should be reported + mock_sdk_crash_reporter, + ) + + def test_swizzle_wrapper_with_sdk_frame_below_not_reported( + self, mock_sdk_crash_reporter: MagicMock + ) -> None: + """ + SentrySwizzleWrapper is in the stack, with another SDK frame below it (further from crash). + The SDK frame below doesn't count - we only care about SDK frames ABOVE (closer to crash). + Since SentrySwizzleWrapper is the only SDK frame above, this is NOT an SDK crash. + """ + # Frames ordered from oldest (caller) to youngest (exception) + frames = [ + { + # This SDK frame is BELOW SentrySwizzleWrapper (further from crash origin) + "function": "-[SentryHub captureEvent:]", + "package": "/private/var/containers/Bundle/Application/59E988EF-46DB-4C75-8E08-10C27DC3E90E/iOS-Swift.app/Frameworks/Sentry.framework/Sentry", + "in_app": False, + }, + { + "function": "-[UIApplication sendEvent:]", + "package": "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", + "in_app": False, + }, + { + "function": "__49-[SentrySwizzleWrapper swizzleSendAction:forKey:]_block_invoke_2", + "package": "/private/var/containers/Bundle/Application/59E988EF-46DB-4C75-8E08-10C27DC3E90E/iOS-Swift.app/Frameworks/Sentry.framework/Sentry", + "in_app": False, + }, + { + "function": "-[NSString substringWithRange:]", + "package": "/System/Library/Frameworks/Foundation.framework/Foundation", + "in_app": False, + }, + { + "function": "__exceptionPreprocess", + "package": "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", + "in_app": False, + }, + ] + + self.execute_test( + get_crash_event_with_frames(frames), + False, # Should NOT be reported - SDK frame below doesn't count + mock_sdk_crash_reporter, + ) + + class SDKCrashDetectionCocoaTest( - TestCase, CococaSDKFilenameTestMixin, CococaSDKFramesTestMixin, CococaSDKFunctionTestMixin + TestCase, + CococaSDKFilenameTestMixin, + CococaSDKFramesTestMixin, + CococaSDKFunctionTestMixin, + CocoaSDKSwizzleWrapperTestMixin, ): def create_event(self, data, project_id, assert_no_errors=True): return self.store_event(data=data, project_id=project_id, assert_no_errors=assert_no_errors) From e8f03b7692d60dc7c8c0354bade1804b69f156ad Mon Sep 17 00:00:00 2001 From: calm329 Date: Fri, 9 Jan 2026 06:06:16 -0800 Subject: [PATCH 02/12] fix(sdk-crashes): Address review feedback for SentrySwizzleWrapper handling --- .../sdk_crashes/sdk_crash_detection_config.py | 13 +++++----- .../utils/sdk_crashes/sdk_crash_detector.py | 25 +++++++++++++------ .../test_sdk_crash_detection_cocoa.py | 17 ++++++------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py b/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py index 74b5980825ff67..810e40d8a69afe 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py @@ -84,10 +84,10 @@ class SDKCrashDetectionConfig: """The function and module patterns to ignore when detecting SDK crashes. For example, FunctionAndModulePattern("*", "**SentrySDK crash**") for any module with that function""" sdk_crash_ignore_matchers: set[FunctionAndModulePattern] """The function patterns to ignore when they are the only SDK frames in the stacktrace. - These frames are typically SDK instrumentation frames that intercept calls but don't cause crashes themselves. - If there are other SDK frames above these frames, the crash is still reported as an SDK crash. - For example, SentrySwizzleWrapper is used for method swizzling and shouldn't be reported as an SDK crash - when it's the only SDK frame, since the actual crash originates from system libraries.""" + These frames are typically SDK instrumentation frames that intercept calls, such as swizzling or monkey patching, + but don't cause crashes themselves. If there are other SDK frames anywhere in the stacktrace, the crash is still + reported as an SDK crash. For example, SentrySwizzleWrapper is used for method swizzling and shouldn't be reported + as an SDK crash when it's the only SDK frame, since it's highly unlikely the crash stems from that code.""" sdk_crash_ignore_when_only_sdk_frame_matchers: set[FunctionAndModulePattern] = field( default_factory=set ) @@ -162,9 +162,8 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]: }, sdk_crash_ignore_when_only_sdk_frame_matchers={ # SentrySwizzleWrapper is used for method swizzling to intercept UI events. - # When it's the only SDK frame, the crash originates from system libraries, - # not from the SDK. Only report as SDK crash if there are other SDK frames - # above SentrySwizzleWrapper in the call stack. + # When it's the only SDK frame, it's highly unlikely the crash stems from the SDK. + # Only report as SDK crash if there are other SDK frames anywhere in the stacktrace. FunctionAndModulePattern( module_pattern="*", function_pattern="**SentrySwizzleWrapper**", diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index ae5ae3e41399cb..5ac4cf588c7477 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -87,6 +87,16 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: # Cocoa SDK frames can be marked as in_app. Therefore, the algorithm only checks if frames # are SDK frames or from system libraries. iter_frames = [f for f in reversed(frames) if f is not None] + + # First pass: Check if we have any SDK frames that are NOT conditional (like SentrySwizzleWrapper). + # We prefer to overreport rather than underreport SDK crashes, so if there's any other + # SDK frame anywhere in the stacktrace, we should report it. + has_non_conditional_sdk_frame = False + for frame in iter_frames: + if self.is_sdk_frame(frame) and not self._matches_ignore_when_only_sdk_frame(frame): + has_non_conditional_sdk_frame = True + break + for frame in iter_frames: function = frame.get("function") module = frame.get("module") @@ -103,15 +113,16 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: if self.is_sdk_frame(frame): # Check if this SDK frame matches the "ignore when only SDK frame" pattern. - # These are instrumentation frames that don't cause crashes themselves. + # These are instrumentation frames (like swizzling or monkey patching) that + # don't cause crashes themselves. if self._matches_ignore_when_only_sdk_frame(frame): # Found a conditional SDK frame (like SentrySwizzleWrapper). - # Since we iterate from youngest to oldest, we've already checked all - # frames above this one (closer to the crash). If there were any - # non-conditional SDK frames above, we would have returned True already. - # Since there weren't, the crash originates from system libraries, - # not the SDK. - return False + # Only ignore it if there are no other SDK frames anywhere in the stacktrace. + # We prefer to overreport rather than underreport SDK crashes. + if has_non_conditional_sdk_frame: + return True + else: + return False else: # Found a non-conditional SDK frame, this is definitely an SDK crash return True diff --git a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py index 1b00acb5063fb3..6738ca4c6596a4 100644 --- a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py +++ b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py @@ -658,12 +658,11 @@ class CocoaSDKSwizzleWrapperTestMixin(BaseSDKCrashDetectionMixin): """Tests for SentrySwizzleWrapper conditional SDK crash detection. SentrySwizzleWrapper is used for method swizzling to intercept UI events. - When it's the only SDK frame, the crash originates from system libraries, - not from the SDK. Only report as SDK crash if there are other SDK frames - above SentrySwizzleWrapper in the call stack. + When it's the only SDK frame, it's highly unlikely the crash stems from the SDK. + Only report as SDK crash if there are other SDK frames anywhere in the stacktrace. + We prefer to overreport rather than underreport SDK crashes. Note: Frames are ordered from oldest (caller) to youngest (exception). - "Above" means closer to the exception (younger frames). """ def test_swizzle_wrapper_only_sdk_frame_not_reported( @@ -671,7 +670,7 @@ def test_swizzle_wrapper_only_sdk_frame_not_reported( ) -> None: """ SentrySwizzleWrapper is the only SDK frame in the stack. - This is a false positive - the crash originates from system libraries. + It's highly unlikely the crash stems from SentrySwizzleWrapper. """ # Frames ordered from oldest (caller) to youngest (exception) frames = [ @@ -771,13 +770,13 @@ def test_swizzle_wrapper_with_other_sdk_frames_above_reported( mock_sdk_crash_reporter, ) - def test_swizzle_wrapper_with_sdk_frame_below_not_reported( + def test_swizzle_wrapper_with_sdk_frame_below_reported( self, mock_sdk_crash_reporter: MagicMock ) -> None: """ SentrySwizzleWrapper is in the stack, with another SDK frame below it (further from crash). - The SDK frame below doesn't count - we only care about SDK frames ABOVE (closer to crash). - Since SentrySwizzleWrapper is the only SDK frame above, this is NOT an SDK crash. + Since there's another SDK frame anywhere in the stacktrace, this IS an SDK crash. + We prefer to overreport rather than underreport SDK crashes. """ # Frames ordered from oldest (caller) to youngest (exception) frames = [ @@ -811,7 +810,7 @@ def test_swizzle_wrapper_with_sdk_frame_below_not_reported( self.execute_test( get_crash_event_with_frames(frames), - False, # Should NOT be reported - SDK frame below doesn't count + True, # Should be reported - there's another SDK frame in the stack mock_sdk_crash_reporter, ) From a203ae8bc7a926e4153d2c2848097d3727020e0b Mon Sep 17 00:00:00 2001 From: calm329 Date: Fri, 9 Jan 2026 06:37:30 -0800 Subject: [PATCH 03/12] fix(sdk-crashes): Address review feedback for SentrySwizzleWrapper handling --- .../utils/sdk_crashes/sdk_crash_detector.py | 39 ++++++++++-------- .../test_sdk_crash_detection_cocoa.py | 41 +++++++++++++++++++ 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index 5ac4cf588c7477..d8a6f0b430297c 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -88,12 +88,13 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: # are SDK frames or from system libraries. iter_frames = [f for f in reversed(frames) if f is not None] - # First pass: Check if we have any SDK frames that are NOT conditional (like SentrySwizzleWrapper). - # We prefer to overreport rather than underreport SDK crashes, so if there's any other - # SDK frame anywhere in the stacktrace, we should report it. has_non_conditional_sdk_frame = False for frame in iter_frames: - if self.is_sdk_frame(frame) and not self._matches_ignore_when_only_sdk_frame(frame): + if ( + self.is_sdk_frame(frame) + and not self._matches_ignore_when_only_sdk_frame(frame) + and not self._matches_sdk_crash_ignore(frame) + ): has_non_conditional_sdk_frame = True break @@ -112,20 +113,9 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: return False if self.is_sdk_frame(frame): - # Check if this SDK frame matches the "ignore when only SDK frame" pattern. - # These are instrumentation frames (like swizzling or monkey patching) that - # don't cause crashes themselves. if self._matches_ignore_when_only_sdk_frame(frame): - # Found a conditional SDK frame (like SentrySwizzleWrapper). - # Only ignore it if there are no other SDK frames anywhere in the stacktrace. - # We prefer to overreport rather than underreport SDK crashes. - if has_non_conditional_sdk_frame: - return True - else: - return False - else: - # Found a non-conditional SDK frame, this is definitely an SDK crash - return True + return has_non_conditional_sdk_frame + return True if not self.is_system_library_frame(frame): return False @@ -150,6 +140,21 @@ def _matches_ignore_when_only_sdk_frame(self, frame: Mapping[str, Any]) -> bool: return False + def _matches_sdk_crash_ignore(self, frame: Mapping[str, Any]) -> bool: + function = frame.get("function") + if not function: + return False + + module = frame.get("module") + for matcher in self.config.sdk_crash_ignore_matchers: + function_matches = glob_match(function, matcher.function_pattern, ignorecase=True) + module_matches = glob_match(module, matcher.module_pattern, ignorecase=True) + + if function_matches and module_matches: + return True + + return False + def is_sdk_frame(self, frame: Mapping[str, Any]) -> bool: """ Returns true if frame is an SDK frame. diff --git a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py index 6738ca4c6596a4..f08e2378127567 100644 --- a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py +++ b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py @@ -814,6 +814,47 @@ def test_swizzle_wrapper_with_sdk_frame_below_reported( mock_sdk_crash_reporter, ) + def test_swizzle_wrapper_with_always_ignored_sdk_frame_not_reported( + self, mock_sdk_crash_reporter: MagicMock + ) -> None: + """ + SentrySwizzleWrapper with an always-ignored SDK frame ([SentrySDK crash]). + The always-ignored frame should not count as another SDK frame. + """ + frames = [ + { + "function": "+[SentrySDK crash]", + "package": "/private/var/containers/Bundle/Application/59E988EF-46DB-4C75-8E08-10C27DC3E90E/iOS-Swift.app/Frameworks/Sentry.framework/Sentry", + "in_app": False, + }, + { + "function": "-[UIApplication sendEvent:]", + "package": "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", + "in_app": False, + }, + { + "function": "__49-[SentrySwizzleWrapper swizzleSendAction:forKey:]_block_invoke_2", + "package": "/private/var/containers/Bundle/Application/59E988EF-46DB-4C75-8E08-10C27DC3E90E/iOS-Swift.app/Frameworks/Sentry.framework/Sentry", + "in_app": False, + }, + { + "function": "-[NSString substringWithRange:]", + "package": "/System/Library/Frameworks/Foundation.framework/Foundation", + "in_app": False, + }, + { + "function": "__exceptionPreprocess", + "package": "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", + "in_app": False, + }, + ] + + self.execute_test( + get_crash_event_with_frames(frames), + False, + mock_sdk_crash_reporter, + ) + class SDKCrashDetectionCocoaTest( TestCase, From f1ac0285bc46fe566d5113e0cf05632d5122ed0f Mon Sep 17 00:00:00 2001 From: calm329 Date: Mon, 12 Jan 2026 08:07:46 -0800 Subject: [PATCH 04/12] fix(sdk-crashes): Address review feedback for SentrySwizzleWrapper handling --- .../utils/sdk_crashes/sdk_crash_detector.py | 71 ++++++++----------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index d8a6f0b430297c..c8cdd2e4cbbfeb 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -6,7 +6,10 @@ from sentry.db.models import NodeData from sentry.utils.glob import glob_match from sentry.utils.safe import get_path -from sentry.utils.sdk_crashes.sdk_crash_detection_config import SDKCrashDetectionConfig +from sentry.utils.sdk_crashes.sdk_crash_detection_config import ( + FunctionAndModulePattern, + SDKCrashDetectionConfig, +) class SDKCrashDetector: @@ -88,72 +91,56 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: # are SDK frames or from system libraries. iter_frames = [f for f in reversed(frames) if f is not None] - has_non_conditional_sdk_frame = False + # First: check if we should ignore the crash because a frame matches sdk_crash_ignore_matchers for frame in iter_frames: - if ( - self.is_sdk_frame(frame) - and not self._matches_ignore_when_only_sdk_frame(frame) - and not self._matches_sdk_crash_ignore(frame) - ): - has_non_conditional_sdk_frame = True - break + if self._matches_sdk_crash_ignore(frame): + return False + # Second: check for conditional SDK frames (like SentrySwizzleWrapper) and track if there are other SDK frames + has_conditional_sdk_frame = False + has_other_sdk_frame = False for frame in iter_frames: - function = frame.get("function") - module = frame.get("module") - - if function: - for matcher in self.config.sdk_crash_ignore_matchers: - function_matches = glob_match( - function, matcher.function_pattern, ignorecase=True - ) - module_matches = glob_match(module, matcher.module_pattern, ignorecase=True) + if self.is_sdk_frame(frame): + if self._matches_ignore_when_only_sdk_frame(frame): + has_conditional_sdk_frame = True + else: + has_other_sdk_frame = True - if function_matches and module_matches: - return False + if has_conditional_sdk_frame and not has_other_sdk_frame: + return False + # Third: determine if this is an SDK crash + for frame in iter_frames: if self.is_sdk_frame(frame): - if self._matches_ignore_when_only_sdk_frame(frame): - return has_non_conditional_sdk_frame return True - if not self.is_system_library_frame(frame): return False return False - def _matches_ignore_when_only_sdk_frame(self, frame: Mapping[str, Any]) -> bool: - """ - Returns true if the frame matches the sdk_crash_ignore_when_only_sdk_frame_matchers pattern. - """ + def _matches_frame_pattern( + self, frame: Mapping[str, Any], matchers: set[FunctionAndModulePattern] + ) -> bool: function = frame.get("function") if not function: return False module = frame.get("module") - for matcher in self.config.sdk_crash_ignore_when_only_sdk_frame_matchers: + for matcher in matchers: function_matches = glob_match(function, matcher.function_pattern, ignorecase=True) module_matches = glob_match(module, matcher.module_pattern, ignorecase=True) - if function_matches and module_matches: return True return False - def _matches_sdk_crash_ignore(self, frame: Mapping[str, Any]) -> bool: - function = frame.get("function") - if not function: - return False - - module = frame.get("module") - for matcher in self.config.sdk_crash_ignore_matchers: - function_matches = glob_match(function, matcher.function_pattern, ignorecase=True) - module_matches = glob_match(module, matcher.module_pattern, ignorecase=True) - - if function_matches and module_matches: - return True + def _matches_ignore_when_only_sdk_frame(self, frame: Mapping[str, Any]) -> bool: + return self._matches_frame_pattern( + frame, self.config.sdk_crash_ignore_when_only_sdk_frame_matchers + ) - return False + def _matches_sdk_crash_ignore(self, frame: Mapping[str, Any]) -> bool: + return self._matches_frame_pattern(frame, self.config.sdk_crash_ignore_matchers) def is_sdk_frame(self, frame: Mapping[str, Any]) -> bool: """ From 4050e6b0491e11f794145a3756d9351df0944cb4 Mon Sep 17 00:00:00 2001 From: calm329 Date: Mon, 12 Jan 2026 08:25:12 -0800 Subject: [PATCH 05/12] fix(sdk-crashes): Refactor pattern matching to reduce code duplication --- .../utils/sdk_crashes/sdk_crash_detector.py | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index c8cdd2e4cbbfeb..5d03f9c486c5db 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -91,28 +91,23 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: # are SDK frames or from system libraries. iter_frames = [f for f in reversed(frames) if f is not None] - # First: check if we should ignore the crash because a frame matches sdk_crash_ignore_matchers + # First pass: check if there are non-conditional SDK frames anywhere + has_non_conditional_sdk_frame = False + for frame in iter_frames: + if self.is_sdk_frame(frame) and not self._matches_ignore_when_only_sdk_frame(frame): + has_non_conditional_sdk_frame = True + break + + # Main loop: determine if this is an SDK crash for frame in iter_frames: if self._matches_sdk_crash_ignore(frame): return False - # Second: check for conditional SDK frames (like SentrySwizzleWrapper) and track if there are other SDK frames - has_conditional_sdk_frame = False - has_other_sdk_frame = False - for frame in iter_frames: if self.is_sdk_frame(frame): if self._matches_ignore_when_only_sdk_frame(frame): - has_conditional_sdk_frame = True - else: - has_other_sdk_frame = True - - if has_conditional_sdk_frame and not has_other_sdk_frame: - return False - - # Third: determine if this is an SDK crash - for frame in iter_frames: - if self.is_sdk_frame(frame): + return has_non_conditional_sdk_frame return True + if not self.is_system_library_frame(frame): return False From 0c13941fa84083341476691b5dcbb96188db4ae2 Mon Sep 17 00:00:00 2001 From: calm329 Date: Mon, 12 Jan 2026 08:35:34 -0800 Subject: [PATCH 06/12] fix(sdk-crashes): Exclude always-ignored frames from non-conditional check --- src/sentry/utils/sdk_crashes/sdk_crash_detector.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index 5d03f9c486c5db..fe6b4d1d7540fb 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -94,7 +94,11 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: # First pass: check if there are non-conditional SDK frames anywhere has_non_conditional_sdk_frame = False for frame in iter_frames: - if self.is_sdk_frame(frame) and not self._matches_ignore_when_only_sdk_frame(frame): + if ( + self.is_sdk_frame(frame) + and not self._matches_ignore_when_only_sdk_frame(frame) + and not self._matches_sdk_crash_ignore(frame) + ): has_non_conditional_sdk_frame = True break From 4178ff33f236a19488cc7b8a7b9521367626c5e9 Mon Sep 17 00:00:00 2001 From: calm329 Date: Mon, 12 Jan 2026 08:50:41 -0800 Subject: [PATCH 07/12] fix(sdk-crashes): Skip always-ignored frames instead of early return --- src/sentry/utils/sdk_crashes/sdk_crash_detector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index fe6b4d1d7540fb..6b2615e354383c 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -105,7 +105,7 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: # Main loop: determine if this is an SDK crash for frame in iter_frames: if self._matches_sdk_crash_ignore(frame): - return False + continue if self.is_sdk_frame(frame): if self._matches_ignore_when_only_sdk_frame(frame): From 7fa02b88d8d1319a9c8fcfbff1aae9ed675547ac Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 13 Jan 2026 15:47:13 +0100 Subject: [PATCH 08/12] code review and one edge case --- .../utils/sdk_crashes/sdk_crash_detector.py | 37 ++++++++------ .../test_sdk_crash_detection_cocoa.py | 49 +++++++++++++++++++ 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index 6b2615e354383c..63c6ebc11e1cd2 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -91,25 +91,34 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: # are SDK frames or from system libraries. iter_frames = [f for f in reversed(frames) if f is not None] - # First pass: check if there are non-conditional SDK frames anywhere - has_non_conditional_sdk_frame = False - for frame in iter_frames: - if ( - self.is_sdk_frame(frame) - and not self._matches_ignore_when_only_sdk_frame(frame) - and not self._matches_sdk_crash_ignore(frame) - ): - has_non_conditional_sdk_frame = True - break - - # Main loop: determine if this is an SDK crash + # Loop 1: Check if we should ignore the crash altogether because a frame matches + # sdk_crash_ignore_matchers. These are typically SDK test methods (like +[SentrySDK crash]) + # that intentionally crash for testing purposes. for frame in iter_frames: if self._matches_sdk_crash_ignore(frame): - continue + return False + # Loop 2: Check if the only SDK frame is a conditional one (like SentrySwizzleWrapper). + # These frames are SDK instrumentation frames that intercept calls but don't cause crashes + # themselves. If there's exactly one SDK frame and it's conditional, it's not an SDK crash. + # Multiple SDK frames (even if all conditional) should still be detected as SDK crashes. + conditional_sdk_frame_count = 0 + has_non_conditional_sdk_frame = False + for frame in iter_frames: if self.is_sdk_frame(frame): if self._matches_ignore_when_only_sdk_frame(frame): - return has_non_conditional_sdk_frame + conditional_sdk_frame_count += 1 + else: + has_non_conditional_sdk_frame = True + break + + if conditional_sdk_frame_count == 1 and not has_non_conditional_sdk_frame: + return False + + # Loop 3: Determine if this is an SDK crash based on frame ordering. + # If the first non-system frame (closest to crash origin) is an SDK frame, it's an SDK crash. + for frame in iter_frames: + if self.is_sdk_frame(frame): return True if not self.is_system_library_frame(frame): diff --git a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py index f08e2378127567..a3054e641919bb 100644 --- a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py +++ b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py @@ -855,6 +855,55 @@ def test_swizzle_wrapper_with_always_ignored_sdk_frame_not_reported( mock_sdk_crash_reporter, ) + def test_multiple_swizzle_wrapper_frames_reported( + self, mock_sdk_crash_reporter: MagicMock + ) -> None: + """ + Multiple SentrySwizzleWrapper frames in the stack. + Even though each individual frame would be ignored when it's the only SDK frame, + having multiple SDK frames (even if all conditional) should be reported. + """ + # Frames ordered from oldest (caller) to youngest (exception) + frames = [ + { + "function": "-[UIApplication sendEvent:]", + "package": "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", + "in_app": False, + }, + { + "function": "__49-[SentrySwizzleWrapper swizzleSendAction:forKey:]_block_invoke_2", + "package": "/private/var/containers/Bundle/Application/59E988EF-46DB-4C75-8E08-10C27DC3E90E/iOS-Swift.app/Frameworks/Sentry.framework/Sentry", + "in_app": False, + }, + { + "function": "-[UIGestureRecognizer _updateGestureForActiveEvents]", + "package": "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", + "in_app": False, + }, + { + # Second SentrySwizzleWrapper frame + "function": "__49-[SentrySwizzleWrapper swizzleSendAction:forKey:]_block_invoke_3", + "package": "/private/var/containers/Bundle/Application/59E988EF-46DB-4C75-8E08-10C27DC3E90E/iOS-Swift.app/Frameworks/Sentry.framework/Sentry", + "in_app": False, + }, + { + "function": "-[NSString substringWithRange:]", + "package": "/System/Library/Frameworks/Foundation.framework/Foundation", + "in_app": False, + }, + { + "function": "__exceptionPreprocess", + "package": "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", + "in_app": False, + }, + ] + + self.execute_test( + get_crash_event_with_frames(frames), + True, # Should be reported - multiple SDK frames even if all conditional + mock_sdk_crash_reporter, + ) + class SDKCrashDetectionCocoaTest( TestCase, From 2609e22e752c7688e464accbf3ed63d4f5cf0c1c Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 13 Jan 2026 15:57:34 +0100 Subject: [PATCH 09/12] efficiency --- .../utils/sdk_crashes/sdk_crash_detector.py | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index 63c6ebc11e1cd2..78a20104564ccd 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -91,17 +91,36 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: # are SDK frames or from system libraries. iter_frames = [f for f in reversed(frames) if f is not None] - # Loop 1: Check if we should ignore the crash altogether because a frame matches - # sdk_crash_ignore_matchers. These are typically SDK test methods (like +[SentrySDK crash]) - # that intentionally crash for testing purposes. + # For efficiency, we first check if an SDK frame appears before any non-system frame + # (Loop 1). Most crashes are not SDK crashes, so we avoid the overhead of the ignore + # checks in the common case. Only if we detect a potential SDK crash do we run the + # additional validation loops (Loop 2 and Loop 3). + + # Loop 1: Check if the first non-system frame (closest to crash origin) is an SDK frame. + potential_sdk_crash = False + for frame in iter_frames: + if self.is_sdk_frame(frame): + potential_sdk_crash = True + break + + if not self.is_system_library_frame(frame): + # A non-SDK, non-system frame (e.g., app code) appeared first. + return False + + if not potential_sdk_crash: + return False + + # Loop 2: Check if any frame matches sdk_crash_ignore_matchers. + # These are SDK methods used for testing (e.g., +[SentrySDK crash]) that intentionally + # trigger crashes and should not be reported as SDK crashes. for frame in iter_frames: if self._matches_sdk_crash_ignore(frame): return False - # Loop 2: Check if the only SDK frame is a conditional one (like SentrySwizzleWrapper). - # These frames are SDK instrumentation frames that intercept calls but don't cause crashes - # themselves. If there's exactly one SDK frame and it's conditional, it's not an SDK crash. - # Multiple SDK frames (even if all conditional) should still be detected as SDK crashes. + # Loop 3: Check if the only SDK frame is a "conditional" one (e.g., SentrySwizzleWrapper). + # These are SDK instrumentation frames that intercept calls but are unlikely to cause + # crashes themselves. A single conditional frame is not reported, but multiple SDK frames + # (even if all conditional) are reported to prefer over-reporting over under-reporting. conditional_sdk_frame_count = 0 has_non_conditional_sdk_frame = False for frame in iter_frames: @@ -115,16 +134,16 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: if conditional_sdk_frame_count == 1 and not has_non_conditional_sdk_frame: return False - # Loop 3: Determine if this is an SDK crash based on frame ordering. - # If the first non-system frame (closest to crash origin) is an SDK frame, it's an SDK crash. - for frame in iter_frames: - if self.is_sdk_frame(frame): - return True + # Passed all ignore checks: this is an SDK crash. + return True - if not self.is_system_library_frame(frame): - return False + def _matches_ignore_when_only_sdk_frame(self, frame: Mapping[str, Any]) -> bool: + return self._matches_frame_pattern( + frame, self.config.sdk_crash_ignore_when_only_sdk_frame_matchers + ) - return False + def _matches_sdk_crash_ignore(self, frame: Mapping[str, Any]) -> bool: + return self._matches_frame_pattern(frame, self.config.sdk_crash_ignore_matchers) def _matches_frame_pattern( self, frame: Mapping[str, Any], matchers: set[FunctionAndModulePattern] @@ -142,14 +161,6 @@ def _matches_frame_pattern( return False - def _matches_ignore_when_only_sdk_frame(self, frame: Mapping[str, Any]) -> bool: - return self._matches_frame_pattern( - frame, self.config.sdk_crash_ignore_when_only_sdk_frame_matchers - ) - - def _matches_sdk_crash_ignore(self, frame: Mapping[str, Any]) -> bool: - return self._matches_frame_pattern(frame, self.config.sdk_crash_ignore_matchers) - def is_sdk_frame(self, frame: Mapping[str, Any]) -> bool: """ Returns true if frame is an SDK frame. From 02ad5f3db1d06e7aa910778a223e1b293dd39a79 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 13 Jan 2026 16:44:41 +0100 Subject: [PATCH 10/12] fix failing test --- src/sentry/utils/sdk_crashes/sdk_crash_detector.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index 78a20104564ccd..a28efd470fb9f5 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -110,12 +110,16 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: if not potential_sdk_crash: return False - # Loop 2: Check if any frame matches sdk_crash_ignore_matchers. + # Loop 2: Check if any frame (up to the first SDK frame) matches sdk_crash_ignore_matchers. # These are SDK methods used for testing (e.g., +[SentrySDK crash]) that intentionally - # trigger crashes and should not be reported as SDK crashes. + # trigger crashes and should not be reported as SDK crashes. We only check frames up to + # the first SDK frame to match the original single-loop algorithm behavior. for frame in iter_frames: if self._matches_sdk_crash_ignore(frame): return False + if self.is_sdk_frame(frame): + # Stop at the first SDK frame; don't check older frames in the call stack. + break # Loop 3: Check if the only SDK frame is a "conditional" one (e.g., SentrySwizzleWrapper). # These are SDK instrumentation frames that intercept calls but are unlikely to cause From db9196a683ca900eae06ad1fca7f380f3c3c4fb7 Mon Sep 17 00:00:00 2001 From: calm329 Date: Wed, 14 Jan 2026 04:51:58 -0800 Subject: [PATCH 11/12] fix(sdk-crashes): Skip always-ignored frames in conditional check --- src/sentry/utils/sdk_crashes/sdk_crash_detector.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index a28efd470fb9f5..1e59caf203c91a 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -129,6 +129,8 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: has_non_conditional_sdk_frame = False for frame in iter_frames: if self.is_sdk_frame(frame): + if self._matches_sdk_crash_ignore(frame): + continue if self._matches_ignore_when_only_sdk_frame(frame): conditional_sdk_frame_count += 1 else: From 64eaf5f59f125c2b24fa2d73005de3e96e06ab60 Mon Sep 17 00:00:00 2001 From: calm329 Date: Wed, 14 Jan 2026 05:19:25 -0800 Subject: [PATCH 12/12] Trigger CI