Skip to content

Commit 691e695

Browse files
authored
feat(crash-detection): allow matching on frame.module (#93690)
Close getsentry/sentry#66689
1 parent 29f37a4 commit 691e695

File tree

4 files changed

+120
-15
lines changed

4 files changed

+120
-15
lines changed

src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ class FunctionAndPathPattern:
2020
path_pattern: str
2121

2222

23+
@dataclass(frozen=True)
24+
class FunctionAndModulePattern:
25+
"""Pattern for matching function and module to ignore SDK crashes.
26+
27+
Use "*" as a wildcard to match any value.
28+
Examples:
29+
- FunctionAndModulePattern("specific.module", "invoke") - matches only "invoke" in "specific.module"
30+
- FunctionAndModulePattern("*", "invoke") - matches "invoke" in any module
31+
- FunctionAndModulePattern("specific.module", "*") - matches any function in "specific.module"
32+
"""
33+
34+
module_pattern: str
35+
function_pattern: str
36+
37+
2338
@dataclass
2439
class SDKFrameConfig:
2540
function_patterns: set[str]
@@ -65,8 +80,8 @@ class SDKCrashDetectionConfig:
6580
system_library_path_patterns: set[str]
6681
"""The configuration for detecting SDK frames."""
6782
sdk_frame_config: SDKFrameConfig
68-
"""The functions to ignore when detecting SDK crashes. For example, `**SentrySDK crash**`"""
69-
sdk_crash_ignore_functions_matchers: set[str]
83+
"""The function and module patterns to ignore when detecting SDK crashes. For example, FunctionAndModulePattern("*", "**SentrySDK crash**") for any module with that function"""
84+
sdk_crash_ignore_matchers: set[FunctionAndModulePattern]
7085

7186

7287
class SDKCrashDetectionOptions(TypedDict):
@@ -119,7 +134,12 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]:
119134
),
120135
# [SentrySDK crash] is a testing function causing a crash.
121136
# Therefore, we don't want to mark it a as a SDK crash.
122-
sdk_crash_ignore_functions_matchers={"**SentrySDK crash**"},
137+
sdk_crash_ignore_matchers={
138+
FunctionAndModulePattern(
139+
module_pattern="*",
140+
function_pattern="**SentrySDK crash**",
141+
),
142+
},
123143
)
124144
configs.append(cocoa_config)
125145

@@ -172,10 +192,13 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]:
172192
fallback_path="sentry-react-native",
173193
),
174194
),
175-
sdk_crash_ignore_functions_matchers={
195+
sdk_crash_ignore_matchers={
176196
# sentryWrapped rethrows the original error
177197
# https://github.com/getsentry/sentry-javascript/blob/a67ebc4f56fd20259bffbe194e8e92e968589c12/packages/browser/src/helpers.ts#L107
178-
"sentryWrapped",
198+
FunctionAndModulePattern(
199+
module_pattern="*",
200+
function_pattern="sentryWrapped",
201+
),
179202
},
180203
)
181204
configs.append(react_native_config)
@@ -256,7 +279,7 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]:
256279
],
257280
path_replacer=KeepFieldPathReplacer(fields={"module", "filename", "package"}),
258281
),
259-
sdk_crash_ignore_functions_matchers=set(),
282+
sdk_crash_ignore_matchers=set(),
260283
)
261284
configs.append(java_config)
262285

@@ -312,7 +335,7 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]:
312335
fallback_path="sentry",
313336
),
314337
),
315-
sdk_crash_ignore_functions_matchers=set(),
338+
sdk_crash_ignore_matchers=set(),
316339
)
317340
configs.append(native_config)
318341

@@ -359,19 +382,31 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]:
359382
},
360383
path_replacer=KeepFieldPathReplacer(fields={"package", "filename", "abs_path"}),
361384
),
362-
sdk_crash_ignore_functions_matchers={
385+
sdk_crash_ignore_matchers={
363386
# getCurrentStackTrace is always part of the stacktrace when the SDK captures the stacktrace,
364387
# and would cause false positives. Therefore, we ignore it.
365-
"getCurrentStackTrace",
388+
FunctionAndModulePattern(
389+
module_pattern="*",
390+
function_pattern="getCurrentStackTrace",
391+
),
366392
# Ignore handleDrawFrame and handleBeginFrame to avoid false positives.
367393
# In the Sentry Flutter SDK, we override the handleDrawFrame and handleBeginFrame methods,
368394
# add our custom implementation on top to instrument frame tracking and then forward the calls to Flutter.
369395
# However every custom implementation is try/catch guarded so no exception can be thrown.
370-
"SentryWidgetsBindingMixin.handleDrawFrame",
371-
"SentryWidgetsBindingMixin.handleBeginFrame",
396+
FunctionAndModulePattern(
397+
module_pattern="*",
398+
function_pattern="SentryWidgetsBindingMixin.handleDrawFrame",
399+
),
400+
FunctionAndModulePattern(
401+
module_pattern="*",
402+
function_pattern="SentryWidgetsBindingMixin.handleBeginFrame",
403+
),
372404
# This is the integration responsible for reporting unhandled errors.
373405
# For certain errors the frame is sometimes included in the stacktrace which leads to false positives.
374-
"FlutterErrorIntegration.call.<fn>",
406+
FunctionAndModulePattern(
407+
module_pattern="*",
408+
function_pattern="FlutterErrorIntegration.call.<fn>",
409+
),
375410
},
376411
)
377412
configs.append(dart_config)

src/sentry/utils/sdk_crashes/sdk_crash_detector.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,16 @@ def is_sdk_crash(self, frames: Sequence[Mapping[str, Any]]) -> bool:
8989
iter_frames = [f for f in reversed(frames) if f is not None]
9090
for frame in iter_frames:
9191
function = frame.get("function")
92+
module = frame.get("module")
93+
9294
if function:
93-
for matcher in self.config.sdk_crash_ignore_functions_matchers:
94-
if glob_match(function, matcher, ignorecase=True):
95+
for matcher in self.config.sdk_crash_ignore_matchers:
96+
function_matches = glob_match(
97+
function, matcher.function_pattern, ignorecase=True
98+
)
99+
module_matches = glob_match(module, matcher.module_pattern, ignorecase=True)
100+
101+
if function_matches and module_matches:
95102
return False
96103

97104
if self.is_sdk_frame(frame):

tests/sentry/utils/sdk_crashes/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ def empty_cocoa_config() -> SDKCrashDetectionConfig:
3333
path_patterns=set(),
3434
path_replacer=FixedPathReplacer(path=""),
3535
),
36-
sdk_crash_ignore_functions_matchers=set(),
36+
sdk_crash_ignore_matchers=set(),
3737
)

tests/sentry/utils/sdk_crashes/test_sdk_crash_detector.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22

3+
from sentry.utils.sdk_crashes.sdk_crash_detection_config import FunctionAndModulePattern
34
from sentry.utils.sdk_crashes.sdk_crash_detector import SDKCrashDetector
45

56

@@ -15,3 +16,65 @@ def test_build_sdk_crash_detection_configs(empty_cocoa_config, field_containing_
1516
}
1617

1718
assert detector.is_sdk_frame(frame) is True
19+
20+
21+
@pytest.mark.parametrize(
22+
"test_id,ignore_matchers,frames,is_crash,description",
23+
[
24+
(
25+
"function_module_match",
26+
[
27+
FunctionAndModulePattern(
28+
module_pattern="kotlin.coroutines", function_pattern="invoke"
29+
)
30+
],
31+
[{"function": "invoke", "module": "kotlin.coroutines", "package": "MyApp"}],
32+
False,
33+
"Should not report a crash when both module and function match pattern exactly",
34+
),
35+
(
36+
"function_match_module_wildcard",
37+
[FunctionAndModulePattern(module_pattern="*", function_pattern="getCurrentStackTrace")],
38+
[{"function": "getCurrentStackTrace", "module": "some.module", "package": "MyApp"}],
39+
False,
40+
"Should not report a crash when function matches and module pattern is wildcard",
41+
),
42+
(
43+
"function_match_module_wildcard_module_is_none",
44+
[FunctionAndModulePattern(module_pattern="*", function_pattern="getCurrentStackTrace")],
45+
[{"function": "getCurrentStackTrace", "package": "MyApp"}],
46+
False,
47+
"Should not report a crash when function matches and module pattern is wildcard, even when frames[0].module is None",
48+
),
49+
(
50+
"function_mismatch_module_wildcard",
51+
[FunctionAndModulePattern(module_pattern="*", function_pattern="getCurrentStackTrace")],
52+
[{"function": "someOtherFunction", "module": "some.module", "package": "MyApp"}],
53+
True,
54+
"Should report a crash when module pattern is wildcard but function doesn't match",
55+
),
56+
(
57+
"function_wildcard_module_match",
58+
[FunctionAndModulePattern(module_pattern="test.module", function_pattern="*")],
59+
[{"function": "anyFunction", "module": "test.module", "package": "MyApp"}],
60+
False,
61+
"Should not report a crash when function pattern is wildcard and module matches",
62+
),
63+
(
64+
"function_wildcard_module_mismatch",
65+
[FunctionAndModulePattern(module_pattern="test.module", function_pattern="*")],
66+
[{"function": "anyFunction", "module": "other.module", "package": "MyApp"}],
67+
True,
68+
"Should report a crash when function pattern is wildcard but module doesn't match",
69+
),
70+
],
71+
)
72+
def test_sdk_crash_ignore_matchers(
73+
empty_cocoa_config, test_id, ignore_matchers, frames, is_crash, description
74+
):
75+
empty_cocoa_config.sdk_crash_ignore_matchers = set(ignore_matchers)
76+
empty_cocoa_config.sdk_frame_config.path_patterns = {"**"}
77+
78+
detector = SDKCrashDetector(empty_cocoa_config)
79+
80+
assert detector.is_sdk_crash(frames) is is_crash, description

0 commit comments

Comments
 (0)