Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk-crash-detection): Store event to dedicated project #49593

Merged
merged 48 commits into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
938bfcf
backup
philipphofmann Apr 3, 2023
46335e8
Merge branch 'feat/sdk-crash-monitoring' into feat/add-to-post-proces…
philipphofmann Apr 3, 2023
06c9711
Merge branch 'feat/sdk-crash-monitoring' into feat/add-to-post-proces…
philipphofmann Apr 4, 2023
bbe8ee4
import via local scope
philipphofmann Apr 4, 2023
22beae8
Merge branch 'feat/sdk-crash-monitoring' into feat/add-to-post-proces…
philipphofmann Apr 6, 2023
625c045
add test
philipphofmann Apr 6, 2023
7a8115e
todo note
philipphofmann Apr 6, 2023
d376867
Merge branch 'feat/sdk-crash-monitoring' into feat/add-to-post-proces…
philipphofmann May 8, 2023
e1ee353
refactor tests to use silos
philipphofmann May 11, 2023
d9a167b
convert test_detect_sdk_crash
philipphofmann May 15, 2023
ca1bde8
add CococaSDKFunctionTestMixin
philipphofmann May 15, 2023
6b2a4c7
finish converting sdk_crash_detection tests
philipphofmann May 15, 2023
f358fd5
fix debug meta
philipphofmann May 15, 2023
5c6c0b6
Convert event stripper tests
philipphofmann May 16, 2023
296ba23
fix tests
philipphofmann May 16, 2023
995aa4e
fix post processing test
philipphofmann May 16, 2023
9b7265f
Merge branch 'feat/sdk-crash-monitoring' into feat/add-to-post-proces…
philipphofmann May 16, 2023
6a21f54
backup
philipphofmann May 19, 2023
ae7fef2
Merge branch 'feat/sdk-crash-monitoring' into feat/add-to-post-proces…
philipphofmann May 19, 2023
e33ae28
Merge branch 'feat/add-to-post-processing' into feat/sdk-crash-featur…
philipphofmann May 22, 2023
fdf61bc
add test
philipphofmann May 22, 2023
ebb759a
fix feature comment
philipphofmann May 22, 2023
8c08c4e
add project ID setting
philipphofmann May 22, 2023
5dc5420
first version
philipphofmann May 22, 2023
48f7f6f
rename flag to crash-reporting
philipphofmann May 22, 2023
419ccf0
Merge branch 'feat/sdk-crash-feature-flag' into feat/sdk-crash-save-e…
philipphofmann May 22, 2023
17e6e56
Update src/sentry/features/__init__.py
philipphofmann May 23, 2023
018caa5
fix failing test
philipphofmann May 23, 2023
7c52fa9
Merge branch 'feat/sdk-crash-feature-flag' into feat/sdk-crash-save-e…
philipphofmann May 23, 2023
79d3b40
fix test
philipphofmann May 23, 2023
cf566c8
fix test
philipphofmann May 23, 2023
9f791fc
Code review
philipphofmann May 25, 2023
a84d982
Merge branch 'feat/sdk-crash-monitoring' into feat/add-to-post-proces…
philipphofmann May 25, 2023
c683c1f
Merge branch 'feat/add-to-post-processing' into feat/sdk-crash-featur…
philipphofmann May 25, 2023
4f167bf
Merge branch 'feat/sdk-crash-feature-flag' into feat/sdk-crash-save-e…
philipphofmann May 25, 2023
dbbab1c
fix test_sdk_crash_monitoring_is_called_with_event
philipphofmann May 25, 2023
f3720c0
Move feature check to post_process
philipphofmann May 30, 2023
04d112f
rename feature to detection
philipphofmann May 30, 2023
172e89d
Update src/sentry/utils/sdk_crashes/sdk_crash_detection.py
philipphofmann May 30, 2023
a7bde15
turn event_stripper into a function
philipphofmann May 30, 2023
26d66c7
Merge branch 'feat/sdk-crash-monitoring' into feat/add-to-post-proces…
philipphofmann May 30, 2023
512add9
Merge branch 'feat/add-to-post-processing' into feat/sdk-crash-featur…
philipphofmann May 30, 2023
ebb7391
Merge branch 'feat/sdk-crash-feature-flag' into feat/sdk-crash-save-e…
philipphofmann May 30, 2023
4cf6560
move project id check to post processing
philipphofmann May 30, 2023
5418204
Update src/sentry/utils/sdk_crashes/sdk_crash_detection.py
philipphofmann May 30, 2023
c10affe
cleanup
philipphofmann May 30, 2023
f2e3777
Merge branch 'feat/sdk-crash-monitoring' into feat/sdk-crash-save-event
philipphofmann Jun 5, 2023
426710b
fix merge
philipphofmann Jun 5, 2023
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
5 changes: 5 additions & 0 deletions src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1519,6 +1519,8 @@ def SOCIAL_AUTH_DEFAULT_USERNAME():
"organizations:ds-sliding-window": False,
# If true certain Slack messages will be escaped to prevent rendering markdown
"organizations:slack-escape-messages": False,
# Enable detecting SDK crashes during event processing
"organizations:sdk-crash-detection": False,
# Adds additional filters and a new section to issue alert rules.
"projects:alert-filters": True,
# Enable functionality to specify custom inbound filters on events.
Expand Down Expand Up @@ -3351,3 +3353,6 @@ def build_cdc_postgres_init_db_volume(settings):
# Raise schema validation errors and make the indexer crash (only useful in
# tests)
SENTRY_METRICS_INDEXER_RAISE_VALIDATION_ERRORS = False

# The project ID for SDK Crash Monitoring to save the detected SDK crashed to.
SDK_CRASH_DETECTION_PROJECT_ID = None
1 change: 1 addition & 0 deletions src/sentry/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
default_manager.add("organizations:set-grouping-config", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
default_manager.add("organizations:slack-escape-messages", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
default_manager.add("organizations:slack-overage-notifications", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
default_manager.add("organizations:sdk-crash-detection", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
default_manager.add("organizations:starfish-view", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
default_manager.add("organizations:starfish-test-endpoint", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
default_manager.add("organizations:streamline-targeting-context", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
Expand Down
23 changes: 23 additions & 0 deletions src/sentry/tasks/post_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,28 @@ def fire_error_processed(job: PostProcessJob):
)


def sdk_crash_monitoring(job: PostProcessJob):
from sentry.utils.sdk_crashes.sdk_crash_detection import sdk_crash_detection

if job["is_reprocessed"]:
return

event = job["event"]

if not features.has("organizations:sdk-crash-detection", event.project.organization):
return

if settings.SDK_CRASH_DETECTION_PROJECT_ID is None:
logger.warning(
"SDK crash detection is enabled but SDK_CRASH_DETECTION_PROJECT_ID is not set."
)
return None

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)


def plugin_post_process_group(plugin_slug, event, **kwargs):
"""
Fires post processing hooks for a group.
Expand Down Expand Up @@ -1044,6 +1066,7 @@ def plugin_post_process_group(plugin_slug, event, **kwargs):
process_similarity,
update_existing_attachments,
fire_error_processed,
sdk_crash_monitoring,
],
GroupCategory.PERFORMANCE: [
process_snoozes,
Expand Down
140 changes: 68 additions & 72 deletions src/sentry/utils/sdk_crashes/event_stripper.py
Original file line number Diff line number Diff line change
@@ -1,78 +1,74 @@
from typing import Any, Mapping, Sequence
from typing import Any, Dict, Mapping, Sequence

from sentry.eventstore.models import Event
from sentry.utils.safe import get_path
from sentry.utils.sdk_crashes.sdk_crash_detector import SDKCrashDetector

ALLOWED_EVENT_KEYS = {
"type",
"datetime",
"timestamp",
"platform",
"sdk",
"level",
"logger",
"exception",
"debug_meta",
"contexts",
}

class EventStripper:
def __init__(
self,
sdk_crash_detector: SDKCrashDetector,
):
self
self.sdk_crash_detector = sdk_crash_detector

ALLOWED_EVENT_KEYS = {
"type",
"datetime",
"timestamp",
"platform",
"sdk",
"level",
"logger",
"exception",
"debug_meta",
"contexts",
}

def strip_event_data(self, event: Event) -> Event:
new_event = dict(filter(self._filter_event, event.items()))
new_event["contexts"] = dict(filter(self._filter_contexts, new_event["contexts"].items()))

stripped_frames = []
frames = get_path(event, "exception", "values", -1, "stacktrace", "frames")

if frames is not None:
stripped_frames = self._strip_frames(frames)
new_event["exception"]["values"][0]["stacktrace"]["frames"] = stripped_frames

debug_meta_images = get_path(event, "debug_meta", "images")
if debug_meta_images is not None:
stripped_debug_meta_images = self._strip_debug_meta(debug_meta_images, stripped_frames)
new_event["debug_meta"]["images"] = stripped_debug_meta_images

return new_event

def _filter_event(self, pair):
key, _ = pair
if key in self.ALLOWED_EVENT_KEYS:
return True

return False

def _filter_contexts(self, pair):
key, _ = pair
if key in {"os", "device"}:
return True
return False

def _strip_debug_meta(
self, debug_meta_images: Sequence[Mapping[str, Any]], frames: Sequence[Mapping[str, Any]]
) -> Sequence[Mapping[str, Any]]:

frame_image_addresses = {frame["image_addr"] for frame in frames}

return [
image for image in debug_meta_images if image["image_addr"] in frame_image_addresses
]

def _strip_frames(self, frames: Sequence[Mapping[str, Any]]) -> Sequence[Mapping[str, Any]]:
"""
Only keep SDK frames or non in app frames.
"""
return [
frame
for frame in frames
if self.sdk_crash_detector.is_sdk_frame(frame) or frame.get("in_app", True) is False
]

def strip_event_data(event: Event, sdk_crash_detector: SDKCrashDetector) -> Dict[str, Any]:
new_event_data = dict(filter(_filter_event, event.data.items()))
new_event_data["contexts"] = dict(filter(_filter_contexts, new_event_data["contexts"].items()))

stripped_frames = []
frames = get_path(new_event_data, "exception", "values", -1, "stacktrace", "frames")

if frames is not None:
stripped_frames = _strip_frames(frames, sdk_crash_detector)
new_event_data["exception"]["values"][0]["stacktrace"]["frames"] = stripped_frames

debug_meta_images = get_path(new_event_data, "debug_meta", "images")
if debug_meta_images is not None:
stripped_debug_meta_images = _strip_debug_meta(debug_meta_images, stripped_frames)
new_event_data["debug_meta"]["images"] = stripped_debug_meta_images

return new_event_data


def _filter_event(pair):
key, _ = pair
if key in ALLOWED_EVENT_KEYS:
return True

return False


def _filter_contexts(pair):
key, _ = pair
if key in {"os", "device"}:
return True
return False


def _strip_debug_meta(
debug_meta_images: Sequence[Mapping[str, Any]], frames: Sequence[Mapping[str, Any]]
) -> Sequence[Mapping[str, Any]]:

frame_image_addresses = {frame["image_addr"] for frame in frames}

return [image for image in debug_meta_images if image["image_addr"] in frame_image_addresses]


def _strip_frames(
frames: Sequence[Mapping[str, Any]], sdk_crash_detector: SDKCrashDetector
) -> Sequence[Mapping[str, Any]]:
"""
Only keep SDK frames or non in app frames.
"""
return [
frame
for frame in frames
if sdk_crash_detector.is_sdk_frame(frame) or frame.get("in_app", None) is False
]
58 changes: 42 additions & 16 deletions src/sentry/utils/sdk_crashes/sdk_crash_detection.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,75 @@
from __future__ import annotations

from typing import Any, Dict, Optional

from django.conf import settings

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.event_stripper import EventStripper
from sentry.utils.sdk_crashes.cocoa_sdk_crash_detector import CocoaSDKCrashDetector
from sentry.utils.sdk_crashes.event_stripper import strip_event_data
from sentry.utils.sdk_crashes.sdk_crash_detector import SDKCrashDetector


class SDKCrashReporter:
def __init__(self):
self

def report(self, event: Event) -> None:
pass
def report(self, event_data: Dict[str, Any], event_project_id: int) -> Event:
from sentry.event_manager import EventManager

manager = EventManager(event_data)
philipphofmann marked this conversation as resolved.
Show resolved Hide resolved
manager.normalize()
return manager.save(project_id=event_project_id)


class SDKCrashDetection:
def __init__(
self,
sdk_crash_reporter: SDKCrashReporter,
sdk_crash_detector: SDKCrashDetector,
event_stripper: EventStripper,
):
self
self.sdk_crash_reporter = sdk_crash_reporter
self.cocoa_sdk_crash_detector = sdk_crash_detector
self.event_stripper = event_stripper

def detect_sdk_crash(self, event: Event) -> None:
if event.get("type", None) != "error" or event.get("platform") != "cocoa":
def detect_sdk_crash(self, event: Event) -> 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

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

is_unhandled = get_path(event, "exception", "values", -1, "mechanism", "handled") is False
is_unhandled = (
get_path(event.data, "exception", "values", -1, "mechanism", "data", "handled") is False
)
if is_unhandled is False:
return
return None

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

if self.cocoa_sdk_crash_detector.is_sdk_crash(frames):
sdk_crash_event = self.event_stripper.strip_event_data(event)
sdk_crash_event_data = strip_event_data(event, self.cocoa_sdk_crash_detector)

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

return self.sdk_crash_reporter.report(
sdk_crash_event_data, settings.SDK_CRASH_DETECTION_PROJECT_ID
)


_crash_reporter = SDKCrashReporter()
_cocoa_sdk_crash_detector = CocoaSDKCrashDetector()

set_path(sdk_crash_event, "contexts", "sdk_crash_detection", value={"detected": True})
self.sdk_crash_reporter.report(sdk_crash_event)
sdk_crash_detection = SDKCrashDetection(_crash_reporter, _cocoa_sdk_crash_detector)
57 changes: 57 additions & 0 deletions tests/sentry/tasks/test_post_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
from sentry.types.group import GroupSubStatus
from sentry.utils.cache import cache
from tests.sentry.issues.test_utils import OccurrenceTestMixin
from tests.sentry.utils.sdk_crashes.test_fixture import get_crash_event


class EventMatcher:
Expand Down Expand Up @@ -1481,6 +1482,61 @@ def test_forecast_in_activity(self, mock_is_escalating):
).exists()


class SDKCrashMonitoringTestMixin(BasePostProgressGroupMixin):
@with_feature("organizations:sdk-crash-detection")
@patch("sentry.utils.sdk_crashes.sdk_crash_detection.sdk_crash_detection.sdk_crash_reporter")
@override_settings(SDK_CRASH_DETECTION_PROJECT_ID=1234)
def test_sdk_crash_monitoring_is_called_with_event(self, mock_sdk_crash_reporter):
event = self.create_event(
data=get_crash_event(),
project_id=self.project.id,
)

self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)

mock_sdk_crash_reporter.report.assert_called_once()

@patch("sentry.utils.sdk_crashes.sdk_crash_detection.sdk_crash_detection")
def test_sdk_crash_monitoring_is_not_called_with_disabled_feature(
self, mock_sdk_crash_detection
):
event = self.create_event(
data=get_crash_event(),
project_id=self.project.id,
)

self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)

mock_sdk_crash_detection.detect_sdk_crash.assert_not_called()

@with_feature("organizations:sdk-crash-detection")
@patch("sentry.utils.sdk_crashes.sdk_crash_detection.sdk_crash_detection")
def test_sdk_crash_monitoring_is_not_called_without_project_id(self, mock_sdk_crash_detection):
event = self.create_event(
data=get_crash_event(),
project_id=self.project.id,
)

self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)

mock_sdk_crash_detection.detect_sdk_crash.assert_not_called()


@region_silo_test
class PostProcessGroupErrorTest(
TestCase,
Expand All @@ -1493,6 +1549,7 @@ class PostProcessGroupErrorTest(
RuleProcessorTestMixin,
ServiceHooksTestMixin,
SnoozeTestMixin,
SDKCrashMonitoringTestMixin,
):
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)
Expand Down
Loading