Skip to content

Commit

Permalink
feat(sdk-crash-detection): Store events to project (#50796)
Browse files Browse the repository at this point in the history
Store all unhandled Cocoa SDK events to a dedicated project configurable
via the settings. Before enabling this feature in production we must
check if the unhandled event stems from the Cocoa SDK and we need to
strip most of the event data to prevent collecting PII.

This is the second PR of splitting up the POC for SDK crash detection
(#49928) into multiple PRs and
it is based on #50794.

---------

Co-authored-by: Joris Bayer <joris.bayer@sentry.io>
  • Loading branch information
philipphofmann and jjbayer authored Jun 14, 2023
1 parent f61ff85 commit 7a57134
Show file tree
Hide file tree
Showing 7 changed files with 483 additions and 9 deletions.
Empty file.
269 changes: 269 additions & 0 deletions fixtures/sdk_crash_detection/crash_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
from typing import Any, Collection, Dict, Mapping, Sequence

IN_APP_FRAME = {
"function": "LoginViewController.viewDidAppear",
"raw_function": "LoginViewController.viewDidAppear(Bool)",
"symbol": "$s8Sentry9LoginViewControllerC13viewDidAppearyySbF",
"package": "SentryApp",
"filename": "LoginViewController.swift",
"abs_path": "/Users/sentry/git/iOS/Sentry/LoggedOut/LoginViewController.swift",
"lineno": 196,
"in_app": True,
"image_addr": "0x100260000",
"instruction_addr": "0x102b16630",
"symbol_addr": "0x100260000",
}


def get_sentry_frame(function: str, in_app: bool = False) -> Mapping[str, Any]:
return {
"function": function,
"package": "Sentry",
"in_app": in_app,
"image_addr": "0x100304000",
}


def get_frames(function: str, sentry_frame_in_app: bool = False) -> Sequence[Mapping[str, Any]]:
frames = [
get_sentry_frame(function, sentry_frame_in_app),
{
"function": "LoginViewController.viewDidAppear",
"symbol": "$s8Sentry9LoginViewControllerC13viewDidAppearyySbF",
"package": "SentryApp",
"in_app": True,
"filename": "LoginViewController.swift",
"image_addr": "0x100260000",
},
IN_APP_FRAME,
{
"function": "-[UIViewController _setViewAppearState:isAnimating:]",
"symbol": "-[UIViewController _setViewAppearState:isAnimating:]",
"package": "UIKitCore",
"in_app": False,
"image_addr": "0x1a4e8f000",
},
{
"function": "-[UIViewController __viewDidAppear:]",
"symbol": "-[UIViewController __viewDidAppear:]",
"package": "UIKitCore",
"in_app": False,
"image_addr": "0x1a4e8f000",
},
{
"function": "-[UIViewController _endAppearanceTransition:]",
"symbol": "-[UIViewController _endAppearanceTransition:]",
"package": "UIKitCore",
"in_app": False,
"image_addr": "0x1a4e8f000",
},
{
"function": "-[UINavigationController navigationTransitionView:didEndTransition:fromView:toView:]",
"symbol": "-[UINavigationController navigationTransitionView:didEndTransition:fromView:toView:]",
"package": "UIKitCore",
"in_app": False,
"image_addr": "0x1a4e8f000",
},
{
"function": "__49-[UINavigationController _startCustomTransition:]_block_invoke",
"symbol": "__49-[UINavigationController _startCustomTransition:]_block_invoke",
"package": "UIKitCore",
"in_app": False,
"image_addr": "0x1a4e8f000",
},
]

# The frames have to be ordered from caller to callee, or oldest to youngest.
# The last frame is the one creating the exception.
# As we usually look at stacktraces from youngest to oldest, we reverse the order.
return frames[::-1]


def get_crash_event(handled=False, function="-[Sentry]", **kwargs) -> Dict[str, Collection[str]]:
return get_crash_event_with_frames(get_frames(function), handled=handled, **kwargs)


def get_crash_event_with_frames(
frames: Sequence[Mapping[str, Any]], handled=False, **kwargs
) -> Dict[str, Collection[str]]:
result = {
"event_id": "80e3496eff734ab0ac993167aaa0d1cd",
"release": "5.222.5",
"type": "error",
"level": "fatal",
"platform": "cocoa",
"tags": {"level": "fatal"},
"exception": {
"values": [
{
"stacktrace": {
"frames": frames,
},
"type": "SIGABRT",
"mechanism": {"handled": handled},
}
]
},
"breadcrumbs": {
"values": [
{
"timestamp": 1675900265.0,
"type": "debug",
"category": "started",
"level": "info",
"message": "Breadcrumb Tracking",
},
]
},
"contexts": {
"app": {
"app_start_time": "2023-02-08T23:51:05Z",
"device_app_hash": "8854fe9e3d4e4a66493baee798bfae0228efabf1",
"build_type": "app store",
"app_identifier": "com.some.company.io",
"app_name": "SomeCompany",
"app_version": "5.222.5",
"app_build": "21036",
"app_id": "397D4F75-6C01-32D1-BF46-62098979E470",
"type": "app",
},
"device": {
"family": "iOS",
"model": "iPhone14,8",
"model_id": "D28AP",
"arch": "arm64e",
"memory_size": 5944508416,
"free_memory": 102154240,
"usable_memory": 4125687808,
"storage_size": 127854202880,
"boot_time": "2023-02-01T05:21:23Z",
"timezone": "PST",
"type": "device",
},
"os": {
"name": "iOS",
"version": "16.3",
"build": "20D47",
"kernel_version": "Darwin Kernel Version 22.3.0: Wed Jan 4 21:25:19 PST 2023; root:xnu-8792.82.2~1/RELEASE_ARM64_T8110",
"rooted": False,
"type": "os",
},
},
"debug_meta": {
"images": [
{
"code_file": "/private/var/containers/Bundle/Application/895DA2DE-5FE3-44A0-8C0F-900519EA5516/iOS-Swift.app/iOS-Swift",
"debug_id": "aa8a3697-c88a-36f9-a687-3d3596568c8d",
"arch": "arm64",
"image_addr": "0x100260000",
"image_size": 180224,
"image_vmaddr": "0x100000000",
"type": "macho",
},
{
"code_file": "/private/var/containers/Bundle/Application/9EB557CD-D653-4F51-BFCE-AECE691D4347/iOS-Swift.app/Frameworks/Sentry.framework/Sentry",
"debug_id": "e2623c4d-79c5-3cdf-90ab-2cf44e026bdd",
"arch": "arm64",
"image_addr": "0x100304000",
"image_size": 802816,
"type": "macho",
},
{
"code_file": "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore",
"debug_id": "b0858d8e-7220-37bf-873f-ecc2b0a358c3",
"arch": "arm64e",
"image_addr": "0x1a4e8f000",
"image_size": 25309184,
"image_vmaddr": "0x188ff7000",
"type": "macho",
},
{
"code_file": "/System/Library/Frameworks/CFNetwork.framework/CFNetwork",
"debug_id": "b2273be9-538a-3f56-b9c7-801f39550f58",
"arch": "arm64e",
"image_addr": "0x1a3e32000",
"image_size": 3977216,
"image_vmaddr": "0x187f9a000",
"in_app": False,
"type": "macho",
},
]
},
"environment": "test-app",
"sdk": {
"name": "sentry.cocoa",
"version": "8.1.0",
"integrations": [
"Crash",
"PerformanceTracking",
"MetricKit",
"WatchdogTerminationTracking",
"ViewHierarchy",
"NetworkTracking",
"ANRTracking",
"AutoBreadcrumbTracking",
"FramesTracking",
"AppStartTracking",
"Screenshot",
"FileIOTracking",
"UIEventTracking",
"AutoSessionTracking",
"CoreDataTracking",
"PreWarmedAppStartTracing",
],
},
"threads": {
"values": [
{
"id": 0,
"stacktrace": {
"frames": [
{
"function": "<redacted>",
"in_app": False,
"data": {"symbolicator_status": "unknown_image"},
"image_addr": "0x0",
"instruction_addr": "0x1129be52e",
"symbol_addr": "0x0",
},
{
"function": "<redacted>",
"in_app": False,
"data": {"symbolicator_status": "unknown_image"},
"image_addr": "0x0",
"instruction_addr": "0x104405f21",
"symbol_addr": "0x0",
},
],
},
"raw_stacktrace": {
"frames": [
{
"function": "<redacted>",
"in_app": False,
"image_addr": "0x0",
"instruction_addr": "0x1129be52e",
"symbol_addr": "0x0",
},
{
"function": "<redacted>",
"in_app": False,
"image_addr": "0x0",
"instruction_addr": "0x104405f21",
"symbol_addr": "0x0",
},
],
},
"crashed": True,
}
]
},
"user": {
"id": "803F5C87-0F8B-41C7-8499-27BD71A92738",
"ip_address": "192.168.0.1",
"geo": {"country_code": "US", "region": "United States"},
},
"logger": "my.logger.name",
}
result.update(kwargs)
return result
9 changes: 5 additions & 4 deletions src/sentry/tasks/post_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,10 +1073,11 @@ def sdk_crash_monitoring(job: PostProcessJob):
)
return None

with metrics.timer("post_process.sdk_crash_monitoring.duration"), sentry_sdk.start_span(
op="tasks.post_process_group.sdk_crash_monitoring"
):
sdk_crash_detection.detect_sdk_crash()
with metrics.timer("post_process.sdk_crash_monitoring.duration"):
with sentry_sdk.start_span(op="tasks.post_process_group.sdk_crash_monitoring"):
sdk_crash_detection.detect_sdk_crash(
event=event, event_project_id=settings.SDK_CRASH_DETECTION_PROJECT_ID
)


def plugin_post_process_group(plugin_slug, event, **kwargs):
Expand Down
9 changes: 9 additions & 0 deletions src/sentry/utils/sdk_crashes/cocoa_sdk_crash_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from sentry.utils.sdk_crashes.sdk_crash_detector import SDKCrashDetector


class CocoaSDKCrashDetector(SDKCrashDetector):
def is_sdk_crash(self) -> bool:
return True

def is_sdk_frame(self) -> bool:
return True
70 changes: 65 additions & 5 deletions src/sentry/utils/sdk_crashes/sdk_crash_detection.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,73 @@
from __future__ import annotations

from typing import Any, Mapping, Optional

from sentry.eventstore.models import Event
from sentry.issues.grouptype import GroupCategory
from sentry.utils.safe import get_path, set_path
from sentry.utils.sdk_crashes.cocoa_sdk_crash_detector import CocoaSDKCrashDetector
from sentry.utils.sdk_crashes.sdk_crash_detector import SDKCrashDetector


class SDKCrashReporter:
def report(self, event_data: Mapping[str, Any], event_project_id: int) -> Event:
from sentry.event_manager import EventManager

manager = EventManager(dict(event_data))
manager.normalize()
return manager.save(project_id=event_project_id)


class SDKCrashDetection:
"""
Placeholder class for SDK crash detection.
"""
def __init__(
self,
sdk_crash_reporter: SDKCrashReporter,
sdk_crash_detector: SDKCrashDetector,
):
self.sdk_crash_reporter = sdk_crash_reporter
self.cocoa_sdk_crash_detector = sdk_crash_detector

def detect_sdk_crash(self, event: Event, event_project_id: int) -> Optional[Event]:
should_detect_sdk_crash = (
event.group
and event.group.issue_category == GroupCategory.ERROR
and event.group.platform == "cocoa"
)
if not should_detect_sdk_crash:
return None

context = get_path(event.data, "contexts", "sdk_crash_detection")
if context is not None and context.get("detected", False):
return None

# Getting the frames and checking if the event is unhandled might different per platform.
# We will change this once we implement this for more platforms.
is_unhandled = (
get_path(event.data, "exception", "values", -1, "mechanism", "data", "handled") is False
)
if is_unhandled is False:
return None

frames = get_path(event.data, "exception", "values", -1, "stacktrace", "frames")
if not frames:
return None

# We still need to pass in the frames to validate it's an unhandled event coming from the Cocoa SDK.
# We will do this in a separate PR.
if self.cocoa_sdk_crash_detector.is_sdk_crash():
# We still need to strip event data for to avoid collecting PII. We will do this in a separate PR.
sdk_crash_event_data = event.data

set_path(
sdk_crash_event_data, "contexts", "sdk_crash_detection", value={"detected": True}
)

return self.sdk_crash_reporter.report(sdk_crash_event_data, event_project_id)

def detect_sdk_crash(self):
return None


sdk_crash_detection = SDKCrashDetection()
_crash_reporter = SDKCrashReporter()
_cocoa_sdk_crash_detector = CocoaSDKCrashDetector()

sdk_crash_detection = SDKCrashDetection(_crash_reporter, _cocoa_sdk_crash_detector)
16 changes: 16 additions & 0 deletions src/sentry/utils/sdk_crashes/sdk_crash_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from abc import ABC, abstractmethod


class SDKCrashDetector(ABC):
@abstractmethod
def is_sdk_crash(self) -> bool:
"""
Returns true if the stacktrace stems from an SDK crash.
:param frames: The stacktrace frames ordered from newest to oldest.
"""
raise NotImplementedError

@abstractmethod
def is_sdk_frame(self) -> bool:
raise NotImplementedError
Loading

0 comments on commit 7a57134

Please sign in to comment.