Просмотр исходного кода

feat(sdk-crash): Add Java V2 (#65968)

Enable the SDK Crash detection for Java / Android.
Philipp Hofmann 1 год назад
Родитель
Сommit
3a4807917a

+ 97 - 0
fixtures/sdk_crash_detection/crash_event_android.py

@@ -0,0 +1,97 @@
+import time
+from collections.abc import Mapping, MutableMapping, Sequence
+
+
+def get_frames(
+    sdk_frame_module: str, system_frame_module: str
+) -> Sequence[MutableMapping[str, str]]:
+    frames = [
+        {
+            "function": "main",
+            "module": "android.app.ActivityThread",
+            "filename": "ActivityThread.java",
+            "abs_path": "ActivityThread.java",
+        },
+        {
+            "function": "handleCallback",
+            "module": "android.os.Handler",
+            "filename": "Handler.java",
+            "abs_path": "Handler.java",
+        },
+        {
+            "function": "performClickInternal",
+            "module": "android.view.View",
+            "filename": "View.java",
+            "abs_path": "View.java",
+        },
+        {
+            "function": "onClick",
+            "module": "com.some.samples.android.MainActivity$$ExternalSyntheticLambda8",
+        },
+        {
+            "function": "captureMessage",
+            "module": sdk_frame_module,
+            "filename": "Hub.java",
+            "abs_path": "Hub.java",
+        },
+        {
+            "function": "invoke",
+            "module": system_frame_module,
+            "filename": "Method.java",
+        },
+    ]
+    return frames
+
+
+def get_crash_event(
+    sdk_frame_module="io.sentry.Hub", system_frame_module="java.lang.reflect.Method", **kwargs
+) -> dict[str, object]:
+    return get_crash_event_with_frames(
+        get_frames(sdk_frame_module, system_frame_module),
+        **kwargs,
+    )
+
+
+def get_crash_event_with_frames(frames: Sequence[Mapping[str, str]], **kwargs) -> dict[str, object]:
+    result = {
+        "event_id": "0a52a8331d3b45089ebd74f8118d4fa1",
+        "release": "io.sentry.samples.android@7.4.0+2",
+        "dist": "2",
+        "platform": "java",
+        "environment": "debug",
+        "exception": {
+            "values": [
+                {
+                    "type": "IllegalArgumentException",
+                    "value": "SDK Crash",
+                    "module": "java.lang",
+                    "stacktrace": {"frames": frames},
+                    "mechanism": {"type": "onerror", "handled": False},
+                }
+            ]
+        },
+        "key_id": "1336851",
+        "level": "fatal",
+        "contexts": {
+            "device": {
+                "name": "sdk_gphone64_arm64",
+                "family": "sdk_gphone64_arm64",
+                "model": "sdk_gphone64_arm64",
+                "simulator": True,
+            },
+            "os": {
+                "name": "Android",
+                "version": "13",
+                "build": "sdk_gphone64_arm64-userdebug UpsideDownCake UPB2.230407.019 10170211 dev-keys",
+                "kernel_version": "6.1.21-android14-3-01811-g9e35a21ec03f-ab9850788",
+                "rooted": False,
+                "type": "os",
+            },
+        },
+        "sdk": {"name": "sentry.java.android", "version": "7.4.0"},
+        "timestamp": time.time(),
+        "type": "error",
+    }
+
+    result.update(kwargs)
+    return result

+ 22 - 0
src/sentry/options/defaults.py

@@ -1918,6 +1918,28 @@ register(
     default=0.0,
     flags=FLAG_AUTOMATOR_MODIFIABLE,
 )
+
+register(
+    "issues.sdk_crash_detection.java.project_id",
+    default=0,
+    type=Int,
+    flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE,
+)
+
+# The allowlist of org IDs that the java crash detection is enabled for.
+register(
+    "issues.sdk_crash_detection.java.organization_allowlist",
+    type=Sequence,
+    default=[],
+    flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE,
+)
+
+register(
+    "issues.sdk_crash_detection.java.sample_rate",
+    default=0.0,
+    flags=FLAG_AUTOMATOR_MODIFIABLE,
+)
+
 # END: SDK Crash Detection
 
 register(

+ 52 - 3
src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py

@@ -3,12 +3,11 @@ from dataclasses import dataclass
 from enum import Enum, unique
 from typing import TypedDict
 
-import sentry_sdk
-
 from sentry import options
 from sentry.utils.sdk_crashes.path_replacer import (
     FixedPathReplacer,
     KeepAfterPatternMatchPathReplacer,
+    KeepFieldPathReplacer,
     PathReplacer,
 )
 
@@ -26,6 +25,7 @@ class SDKFrameConfig:
 class SdkName(Enum):
     Cocoa = "cocoa"
     ReactNative = "react-native"
+    Java = "java"
 
 
 @dataclass
@@ -150,6 +150,56 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]:
         )
         configs.append(react_native_config)
 
+    java_options = _get_options(sdk_name=SdkName.Java, has_organization_allowlist=True)
+    if java_options:
+        java_config = SDKCrashDetectionConfig(
+            sdk_name=SdkName.Java,
+            project_id=java_options["project_id"],
+            sample_rate=java_options["sample_rate"],
+            organization_allowlist=java_options["organization_allowlist"],
+            sdk_names=[
+                "sentry.java.android",
+                "sentry.java.android.capacitor",
+                "sentry.java.android.dotnet",
+                "sentry.java.android.flutter",
+                "sentry.java.android.kmp",
+                "sentry.java.android.react-native",
+                "sentry.java.android.timber",
+                "sentry.java.android.unity",
+                "sentry.java.android.unreal",
+                "sentry.java.jul",
+                "sentry.java.kmp",
+                "sentry.java.log4j2",
+                "sentry.java.logback",
+                "sentry.java.opentelemetry.agent",
+                "sentry.java.spring",
+                "sentry.java.spring-boot",
+                "sentry.java.spring-boot.jakarta",
+                "sentry.java.spring.jakarta",
+            ],
+            # The sentry-java SDK sends SDK frames for uncaught exceptions since 7.0.0, which is required for detecting SDK crashes.
+            # 7.0.0 was released in Nov 2023, see https://github.com/getsentry/sentry-java/releases/tag/7.0.0
+            min_sdk_version="7.0.0",
+            system_library_path_patterns={
+                r"java.**",
+                r"javax.**",
+                r"android.**",
+                r"androidx.**",
+                r"com.android.internal.**",
+                r"kotlin.**",
+                r"dalvik.**",
+            },
+            sdk_frame_config=SDKFrameConfig(
+                function_patterns=set(),
+                path_patterns={
+                    r"io.sentry.**",
+                },
+                path_replacer=KeepFieldPathReplacer(fields={"module", "filename"}),
+            ),
+            sdk_crash_ignore_functions_matchers=set(),
+        )
+        configs.append(java_config)
+
     return configs
 
 
@@ -160,7 +210,6 @@ def _get_options(
 
     project_id = options.get(f"{options_prefix}.project_id")
     if not project_id:
-        sentry_sdk.capture_message(f"{sdk_name.value} project_id is not set.")
         return None
 
     sample_rate = options.get(f"{options_prefix}.sample_rate")

+ 28 - 10
tests/sentry/utils/sdk_crashes/test_build_sdk_crash_detection_configs.py

@@ -12,12 +12,15 @@ from sentry.utils.sdk_crashes.sdk_crash_detection_config import (
         "issues.sdk_crash_detection.react-native.project_id": 2,
         "issues.sdk_crash_detection.react-native.sample_rate": 0.2,
         "issues.sdk_crash_detection.react-native.organization_allowlist": [1],
+        "issues.sdk_crash_detection.java.project_id": 3,
+        "issues.sdk_crash_detection.java.sample_rate": 0.3,
+        "issues.sdk_crash_detection.java.organization_allowlist": [2],
     }
 )
 def test_build_sdk_crash_detection_configs():
     configs = build_sdk_crash_detection_configs()
 
-    assert len(configs) == 2
+    assert len(configs) == 3
 
     cocoa_config = configs[0]
     assert cocoa_config.sdk_name == SdkName.Cocoa
@@ -31,14 +34,23 @@ def test_build_sdk_crash_detection_configs():
     assert react_native_config.sample_rate == 0.2
     assert react_native_config.organization_allowlist == [1]
 
+    java_config = configs[2]
+    assert java_config.sdk_name == SdkName.Java
+    assert java_config.project_id == 3
+    assert java_config.sample_rate == 0.3
+    assert java_config.organization_allowlist == [2]
+
 
 @override_options(
     {
-        "issues.sdk_crash_detection.cocoa.project_id": None,
-        "issues.sdk_crash_detection.cocoa.sample_rate": None,
+        "issues.sdk_crash_detection.cocoa.project_id": 0,
+        "issues.sdk_crash_detection.cocoa.sample_rate": 0.0,
         "issues.sdk_crash_detection.react-native.project_id": 2,
         "issues.sdk_crash_detection.react-native.sample_rate": 0.2,
         "issues.sdk_crash_detection.react-native.organization_allowlist": [1],
+        "issues.sdk_crash_detection.java.project_id": 0,
+        "issues.sdk_crash_detection.java.sample_rate": 0.0,
+        "issues.sdk_crash_detection.java.organization_allowlist": [],
     }
 )
 def test_build_sdk_crash_detection_configs_only_react_native():
@@ -55,10 +67,13 @@ def test_build_sdk_crash_detection_configs_only_react_native():
 @override_options(
     {
         "issues.sdk_crash_detection.cocoa.project_id": 1.0,
-        "issues.sdk_crash_detection.cocoa.sample_rate": None,
+        "issues.sdk_crash_detection.cocoa.sample_rate": 0.0,
         "issues.sdk_crash_detection.react-native.project_id": 2,
         "issues.sdk_crash_detection.react-native.sample_rate": 0.2,
         "issues.sdk_crash_detection.react-native.organization_allowlist": [1],
+        "issues.sdk_crash_detection.java.project_id": 3,
+        "issues.sdk_crash_detection.java.sample_rate": 0.0,
+        "issues.sdk_crash_detection.java.organization_allowlist": [2],
     }
 )
 def test_build_sdk_crash_detection_configs_no_sample_rate():
@@ -74,12 +89,15 @@ def test_build_sdk_crash_detection_configs_no_sample_rate():
 
 @override_options(
     {
-        "issues.sdk_crash_detection.cocoa.project_id": None,
-        "issues.sdk_crash_detection.cocoa.sample_rate": None,
-        "issues.sdk_crash_detection.react-native.project_id": None,
-        "issues.sdk_crash_detection.react-native.sample_rate": None,
-        "issues.sdk_crash_detection.react-native.organization_allowlist": [],
+        "issues.sdk_crash_detection.cocoa.project_id": 0,
+        "issues.sdk_crash_detection.cocoa.sample_rate": 0.0,
+        "issues.sdk_crash_detection.react-native.project_id": 0,
+        "issues.sdk_crash_detection.react-native.sample_rate": 0.0,
+        "issues.sdk_crash_detection.react-native.organization_allowlist": [1],
+        "issues.sdk_crash_detection.java.project_id": 0,
+        "issues.sdk_crash_detection.java.sample_rate": 0.0,
+        "issues.sdk_crash_detection.java.organization_allowlist": [1],
     }
 )
-def test_build_sdk_crash_detection_configs_no_configs():
+def test_build_sdk_crash_detection_default_configs():
     assert len(build_sdk_crash_detection_configs()) == 0

+ 34 - 0
tests/sentry/utils/sdk_crashes/test_event_stripper.py

@@ -35,6 +35,9 @@ def configs() -> Sequence[SDKCrashDetectionConfig]:
             "issues.sdk_crash_detection.react-native.project_id": 2,
             "issues.sdk_crash_detection.react-native.sample_rate": 0.2,
             "issues.sdk_crash_detection.react-native.organization_allowlist": [1],
+            "issues.sdk_crash_detection.java.project_id": 3,
+            "issues.sdk_crash_detection.java.sample_rate": 0.3,
+            "issues.sdk_crash_detection.java.organization_allowlist": [2],
         }
     ):
         return build_sdk_crash_detection_configs()
@@ -354,6 +357,37 @@ def test_strip_frames_sdk_frames_keep_after_matcher(store_and_strip_event, confi
     }
 
 
+@django_db_all
+@pytest.mark.snuba
+def test_strip_frames_with_keep_for_fields_path_replacer(store_and_strip_event, configs):
+    frames = get_frames("register", sentry_frame_in_app=False)
+
+    sentry_sdk_frame = frames[-1]
+
+    sentry_sdk_frame["module"] = "io.sentry.android.core.SentryAndroidOptions"
+    sentry_sdk_frame["filename"] = "SentryAndroidOptions.java"
+    sentry_sdk_frame["abs_path"] = "remove_me"
+    sentry_sdk_frame["package"] = "remove_me"
+
+    event_data = get_crash_event_with_frames(frames)
+
+    java_config = configs[2]
+    stripped_event_data = store_and_strip_event(data=event_data, config=java_config)
+
+    stripped_frames = get_path(
+        stripped_event_data, "exception", "values", -1, "stacktrace", "frames"
+    )
+
+    cocoa_sdk_frame = stripped_frames[-1]
+    assert cocoa_sdk_frame == {
+        "function": "register",
+        "module": "io.sentry.android.core.SentryAndroidOptions",
+        "filename": "SentryAndroidOptions.java",
+        "in_app": True,
+        "image_addr": "0x100304000",
+    }
+
+
 @django_db_all
 @pytest.mark.snuba
 def test_strip_event_without_data_returns_empty_dict(store_and_strip_event):

+ 177 - 0
tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_java.py

@@ -0,0 +1,177 @@
+from collections.abc import Sequence
+from functools import wraps
+from unittest.mock import patch
+
+import pytest
+
+from fixtures.sdk_crash_detection.crash_event_android import get_crash_event
+from sentry.testutils.helpers.options import override_options
+from sentry.testutils.pytest.fixtures import django_db_all
+from sentry.utils.safe import get_path, set_path
+from sentry.utils.sdk_crashes.sdk_crash_detection import sdk_crash_detection
+from sentry.utils.sdk_crashes.sdk_crash_detection_config import (
+    SDKCrashDetectionConfig,
+    build_sdk_crash_detection_configs,
+)
+
+
+def decorators(func):
+    @wraps(func)
+    @django_db_all
+    @pytest.mark.snuba
+    @patch("random.random", return_value=0.1)
+    @patch("sentry.utils.sdk_crashes.sdk_crash_detection.sdk_crash_detection.sdk_crash_reporter")
+    def wrapper(*args, **kwargs):
+        return func(*args, **kwargs)
+
+    return wrapper
+
+
+@pytest.fixture
+def configs() -> Sequence[SDKCrashDetectionConfig]:
+    with override_options(
+        {
+            "issues.sdk_crash_detection.java.project_id": 3,
+            "issues.sdk_crash_detection.java.sample_rate": 0.3,
+            "issues.sdk_crash_detection.java.organization_allowlist": [2],
+        }
+    ):
+        return build_sdk_crash_detection_configs()
+
+
+@pytest.mark.parametrize(
+    ["sdk_frame_module", "system_frame_module", "detected"],
+    [
+        (
+            "io.sentry.Hub",
+            "java.lang.reflect.Method",
+            True,
+        ),
+        (
+            "io.sentry.Client",
+            "javax.some.Method",
+            True,
+        ),
+        (
+            "io.sentry.Hub",
+            "android.app.ActivityThread",
+            True,
+        ),
+        (
+            "io.sentry.Hub",
+            "com.android.internal.os.RuntimeInit$MethodAndArgsCaller",
+            True,
+        ),
+        (
+            "io.sentry.Hub",
+            "androidx.app.ActivityThread",
+            True,
+        ),
+        (
+            "io.sentry.Hub",
+            "kotlinn.str",
+            False,
+        ),
+        (
+            "io.sentry.Hub",
+            "dalvik.system.ApplicationRuntime",
+            True,
+        ),
+        (
+            "io.sentr.Hub",
+            "java.lang.reflect.Method",
+            False,
+        ),
+        (
+            "io.sentry.Hub",
+            "jav.lang.reflect.Method",
+            False,
+        ),
+    ],
+)
+@decorators
+def test_sdk_crash_is_reported_with_android_paths(
+    mock_sdk_crash_reporter,
+    mock_random,
+    store_event,
+    configs,
+    sdk_frame_module,
+    system_frame_module,
+    detected,
+):
+    event = store_event(
+        data=get_crash_event(
+            sdk_frame_module=sdk_frame_module, system_frame_module=system_frame_module
+        )
+    )
+
+    configs[1].organization_allowlist = [event.project.organization_id]
+
+    sdk_crash_detection.detect_sdk_crash(event=event, configs=configs)
+
+    if detected:
+        assert mock_sdk_crash_reporter.report.call_count == 1
+        reported_event_data = mock_sdk_crash_reporter.report.call_args.args[0]
+
+        stripped_frames = get_path(
+            reported_event_data, "exception", "values", -1, "stacktrace", "frames"
+        )
+
+        assert len(stripped_frames) == 5
+
+        system_frame1 = stripped_frames[0]
+        assert system_frame1["function"] == "main"
+        assert system_frame1["module"] == "android.app.ActivityThread"
+        assert system_frame1["filename"] == "ActivityThread.java"
+        assert system_frame1["abs_path"] == "ActivityThread.java"
+        assert system_frame1["in_app"] is False
+
+        sdk_frame = stripped_frames[3]
+        assert sdk_frame["function"] == "captureMessage"
+        assert sdk_frame["module"] == sdk_frame_module
+        assert sdk_frame["filename"] == "Hub.java"
+        assert "abs_path" not in sdk_frame
+        assert sdk_frame["in_app"] is True
+
+        system_frame2 = stripped_frames[4]
+        assert system_frame2["function"] == "invoke"
+        assert system_frame2["module"] == system_frame_module
+        assert system_frame2["filename"] == "Method.java"
+        assert system_frame2["in_app"] is False
+
+    else:
+        assert mock_sdk_crash_reporter.report.call_count == 0
+
+
+@decorators
+def test_beta_sdk_version_detected(mock_sdk_crash_reporter, mock_random, store_event, configs):
+    event_data = get_crash_event()
+    set_path(event_data, "sdk", "version", value="7.0.1-beta.0")
+    event = store_event(data=event_data)
+
+    configs[1].organization_allowlist = [event.project.organization_id]
+
+    sdk_crash_detection.detect_sdk_crash(
+        event=event,
+        configs=configs,
+    )
+
+    assert mock_sdk_crash_reporter.report.call_count == 1
+
+
+@decorators
+def test_too_low_min_sdk_version_not_detected(
+    mock_sdk_crash_reporter, mock_random, store_event, configs
+):
+    event_data = get_crash_event()
+    set_path(event_data, "sdk", "version", value="6.9.9")
+    event = store_event(data=event_data)
+
+    configs[1].organization_allowlist = [event.project.organization_id]
+
+    sdk_crash_detection.detect_sdk_crash(
+        event=event,
+        configs=configs,
+    )
+
+    assert mock_sdk_crash_reporter.report.call_count == 0