Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
52 changes: 40 additions & 12 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,28 +90,53 @@ 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)
# 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

if function_matches and module_matches:
return False
# Main loop: determine if this is an SDK crash
for frame in iter_frames:
if self._matches_sdk_crash_ignore(frame):
return False

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_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

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.
Expand Down
209 changes: 208 additions & 1 deletion tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_cocoa.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,8 +653,215 @@ 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, 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).
"""

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.
It's highly unlikely the crash stems from SentrySwizzleWrapper.
"""
# 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_reported(
self, mock_sdk_crash_reporter: MagicMock
) -> None:
"""
SentrySwizzleWrapper is in the stack, with another SDK frame below it (further from 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 = [
{
# 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),
True, # Should be reported - there's another SDK frame in the stack
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, 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)
Loading