From 963165f8dcfeb67f0ff4f03ea9f644c4698ea7d2 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 17 Jun 2025 12:21:14 +0200 Subject: [PATCH 1/2] feat(crash-detection): allow matching on `frame.module` --- .../sdk_crashes/sdk_crash_detection_config.py | 54 ++++++++++++---- .../utils/sdk_crashes/sdk_crash_detector.py | 11 +++- tests/sentry/utils/sdk_crashes/conftest.py | 2 +- .../sdk_crashes/test_sdk_crash_detector.py | 63 +++++++++++++++++++ 4 files changed, 116 insertions(+), 14 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 8bf92ea351cde2..d1cafd9fa7704d 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py @@ -20,6 +20,21 @@ class FunctionAndPathPattern: path_pattern: str +@dataclass(frozen=True) +class FunctionAndModulePattern: + """Pattern for matching function and module to ignore SDK crashes. + + Use "*" as a wildcard to match any value. + Examples: + - FunctionAndModulePattern("specific.module", "invoke") - matches only "invoke" in "specific.module" + - FunctionAndModulePattern("*", "invoke") - matches "invoke" in any module + - FunctionAndModulePattern("specific.module", "*") - matches any function in "specific.module" + """ + + module_pattern: str + function_pattern: str + + @dataclass class SDKFrameConfig: function_patterns: set[str] @@ -65,8 +80,8 @@ class SDKCrashDetectionConfig: system_library_path_patterns: set[str] """The configuration for detecting SDK frames.""" sdk_frame_config: SDKFrameConfig - """The functions to ignore when detecting SDK crashes. For example, `**SentrySDK crash**`""" - sdk_crash_ignore_functions_matchers: set[str] + """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] class SDKCrashDetectionOptions(TypedDict): @@ -119,7 +134,12 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]: ), # [SentrySDK crash] is a testing function causing a crash. # Therefore, we don't want to mark it a as a SDK crash. - sdk_crash_ignore_functions_matchers={"**SentrySDK crash**"}, + sdk_crash_ignore_matchers={ + FunctionAndModulePattern( + module_pattern="*", + function_pattern="**SentrySDK crash**", + ), + }, ) configs.append(cocoa_config) @@ -172,7 +192,7 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]: fallback_path="sentry-react-native", ), ), - sdk_crash_ignore_functions_matchers=set(), + sdk_crash_ignore_matchers=set(), ) configs.append(react_native_config) @@ -252,7 +272,7 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]: ], path_replacer=KeepFieldPathReplacer(fields={"module", "filename", "package"}), ), - sdk_crash_ignore_functions_matchers=set(), + sdk_crash_ignore_matchers=set(), ) configs.append(java_config) @@ -308,7 +328,7 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]: fallback_path="sentry", ), ), - sdk_crash_ignore_functions_matchers=set(), + sdk_crash_ignore_matchers=set(), ) configs.append(native_config) @@ -355,19 +375,31 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]: }, path_replacer=KeepFieldPathReplacer(fields={"package", "filename", "abs_path"}), ), - sdk_crash_ignore_functions_matchers={ + sdk_crash_ignore_matchers={ # getCurrentStackTrace is always part of the stacktrace when the SDK captures the stacktrace, # and would cause false positives. Therefore, we ignore it. - "getCurrentStackTrace", + FunctionAndModulePattern( + module_pattern="*", + function_pattern="getCurrentStackTrace", + ), # Ignore handleDrawFrame and handleBeginFrame to avoid false positives. # In the Sentry Flutter SDK, we override the handleDrawFrame and handleBeginFrame methods, # add our custom implementation on top to instrument frame tracking and then forward the calls to Flutter. # However every custom implementation is try/catch guarded so no exception can be thrown. - "SentryWidgetsBindingMixin.handleDrawFrame", - "SentryWidgetsBindingMixin.handleBeginFrame", + FunctionAndModulePattern( + module_pattern="*", + function_pattern="SentryWidgetsBindingMixin.handleDrawFrame", + ), + FunctionAndModulePattern( + module_pattern="*", + function_pattern="SentryWidgetsBindingMixin.handleBeginFrame", + ), # This is the integration responsible for reporting unhandled errors. # For certain errors the frame is sometimes included in the stacktrace which leads to false positives. - "FlutterErrorIntegration.call.", + FunctionAndModulePattern( + module_pattern="*", + function_pattern="FlutterErrorIntegration.call.", + ), }, ) configs.append(dart_config) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py index d5130d8140b4a7..07c495b149a03b 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detector.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detector.py @@ -89,9 +89,16 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool: iter_frames = [f for f in reversed(frames) if f is not None] for frame in iter_frames: function = frame.get("function") + module = frame.get("module") + if function: - for matcher in self.config.sdk_crash_ignore_functions_matchers: - if glob_match(function, matcher, ignorecase=True): + 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 False if self.is_sdk_frame(frame): diff --git a/tests/sentry/utils/sdk_crashes/conftest.py b/tests/sentry/utils/sdk_crashes/conftest.py index 03872ff702250f..5b8c6aff4f67b4 100644 --- a/tests/sentry/utils/sdk_crashes/conftest.py +++ b/tests/sentry/utils/sdk_crashes/conftest.py @@ -33,5 +33,5 @@ def empty_cocoa_config() -> SDKCrashDetectionConfig: path_patterns=set(), path_replacer=FixedPathReplacer(path=""), ), - sdk_crash_ignore_functions_matchers=set(), + sdk_crash_ignore_matchers=set(), ) diff --git a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detector.py b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detector.py index 1d6c582a52255f..eb48cce091f9d1 100644 --- a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detector.py +++ b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detector.py @@ -1,5 +1,6 @@ import pytest +from sentry.utils.sdk_crashes.sdk_crash_detection_config import FunctionAndModulePattern from sentry.utils.sdk_crashes.sdk_crash_detector import SDKCrashDetector @@ -15,3 +16,65 @@ def test_build_sdk_crash_detection_configs(empty_cocoa_config, field_containing_ } assert detector.is_sdk_frame(frame) is True + + +@pytest.mark.parametrize( + "test_id,ignore_matchers,frames,is_crash,description", + [ + ( + "function_module_match", + [ + FunctionAndModulePattern( + module_pattern="kotlin.coroutines", function_pattern="invoke" + ) + ], + [{"function": "invoke", "module": "kotlin.coroutines", "package": "MyApp"}], + False, + "Should not report a crash when both module and function match pattern exactly", + ), + ( + "function_match_module_wildcard", + [FunctionAndModulePattern(module_pattern="*", function_pattern="getCurrentStackTrace")], + [{"function": "getCurrentStackTrace", "module": "some.module", "package": "MyApp"}], + False, + "Should not report a crash when function matches and module pattern is wildcard", + ), + ( + "function_match_module_wildcard_module_is_none", + [FunctionAndModulePattern(module_pattern="*", function_pattern="getCurrentStackTrace")], + [{"function": "getCurrentStackTrace", "package": "MyApp"}], + False, + "Should not report a crash when function matches and module pattern is wildcard, even when frames[0].module is None", + ), + ( + "function_mismatch_module_wildcard", + [FunctionAndModulePattern(module_pattern="*", function_pattern="getCurrentStackTrace")], + [{"function": "someOtherFunction", "module": "some.module", "package": "MyApp"}], + True, + "Should report a crash when module pattern is wildcard but function doesn't match", + ), + ( + "function_wildcard_module_match", + [FunctionAndModulePattern(module_pattern="test.module", function_pattern="*")], + [{"function": "anyFunction", "module": "test.module", "package": "MyApp"}], + False, + "Should not report a crash when function pattern is wildcard and module matches", + ), + ( + "function_wildcard_module_mismatch", + [FunctionAndModulePattern(module_pattern="test.module", function_pattern="*")], + [{"function": "anyFunction", "module": "other.module", "package": "MyApp"}], + True, + "Should report a crash when function pattern is wildcard but module doesn't match", + ), + ], +) +def test_sdk_crash_ignore_matchers( + empty_cocoa_config, test_id, ignore_matchers, frames, is_crash, description +): + empty_cocoa_config.sdk_crash_ignore_matchers = set(ignore_matchers) + empty_cocoa_config.sdk_frame_config.path_patterns = {"**"} + + detector = SDKCrashDetector(empty_cocoa_config) + + assert detector.is_sdk_crash(frames) is is_crash, description From 8b1fa487e8800ac83f369944e639ae9ca5ec837b Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 17 Jun 2025 12:28:47 +0200 Subject: [PATCH 2/2] update --- .../utils/sdk_crashes/sdk_crash_detection_config.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 d1cafd9fa7704d..0ad5b20e8616e0 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py @@ -192,7 +192,14 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]: fallback_path="sentry-react-native", ), ), - sdk_crash_ignore_matchers=set(), + sdk_crash_ignore_matchers={ + # sentryWrapped rethrows the original error + # https://github.com/getsentry/sentry-javascript/blob/a67ebc4f56fd20259bffbe194e8e92e968589c12/packages/browser/src/helpers.ts#L107 + FunctionAndModulePattern( + module_pattern="*", + function_pattern="sentryWrapped", + ), + }, ) configs.append(react_native_config)