Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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
)


class SDKCrashDetectionOptions(TypedDict):
Expand Down Expand Up @@ -152,6 +160,15 @@ 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, 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**",
),
},
)
configs.append(cocoa_config)

Expand Down
86 changes: 72 additions & 14 deletions src/sentry/utils/sdk_crashes/sdk_crash_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -87,25 +90,80 @@ 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]
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 function_matches and module_matches:
return False
# 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):
return True
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 (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. 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
# 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:
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:
has_non_conditional_sdk_frame = True
break

if conditional_sdk_frame_count == 1 and not has_non_conditional_sdk_frame:
return False

# Passed all ignore checks: this is an SDK crash.
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
)

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]
) -> bool:
function = frame.get("function")
if not function:
return False

module = frame.get("module")
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

Expand Down
Loading
Loading