Browse Source

feat(sdk-crash): Add Native (#68434)

Add the SDK Crash detection for Native.

The plan is to only test this now with the sentry and
[sentry-sdks](https://sentry.sentry.io/issues/?project=1&query=SENTRY-sdks)
organization before rolling it out to customers.
Philipp Hofmann 11 months ago
parent
commit
79453af5b3

+ 81 - 0
fixtures/sdk_crash_detection/crash_event_native.py

@@ -0,0 +1,81 @@
+import time
+from collections.abc import Mapping, MutableMapping, Sequence
+
+
+def get_frames(
+    sdk_frame_function: str, system_frame_package: str
+) -> Sequence[MutableMapping[str, str]]:
+    frames = [
+        {
+            "function": "RtlUserThreadStart",
+            "symbol": "RtlUserThreadStart",
+            "package": "C:\\WINDOWS\\SYSTEM32\\ntdll.dll",
+        },
+        {
+            "function": "BaseThreadInitThunk",
+            "symbol": "BaseThreadInitThunk",
+            "package": "C:\\WINDOWS\\System32\\KERNEL32.DLL",
+        },
+        {
+            "function": "snprintf",
+            "symbol": "snprintf",
+            "package": "D:\\Sentry\\Sentaurs\\Game\\Sentaurs.exe",
+        },
+        {
+            "function": sdk_frame_function,
+            "symbol": sdk_frame_function,
+            "package": "E:\\Sentry\\Sentaurs\\Game\\Sentaurs.exe",
+        },
+        {
+            "function": "boost::serialization::singleton<T>::singleton<T>",
+            "symbol": "??0?$singleton@V?$extended_type_info_typeid@T_E_SC_SI_OPT_IR_MODE_SELECTOR@@@serialization@boost@@@serialization@boost@@IEAA@XZ",
+            "package": system_frame_package,
+        },
+    ]
+    return frames
+
+
+def get_crash_event(
+    sdk_frame_function="sentry_value_to_msgpack",
+    system_frame_package="C:\\Windows\\System32\\DriverStore\\FileRepository\\u0398226.inf_amd64_c5d9587384e4b5ff\\B398182\\amdxx64.dll",
+    **kwargs,
+) -> dict[str, object]:
+    return get_crash_event_with_frames(
+        get_frames(sdk_frame_function, system_frame_package),
+        **kwargs,
+    )
+
+
+def get_crash_event_with_frames(frames: Sequence[Mapping[str, str]], **kwargs) -> dict[str, object]:
+    result = {
+        "event_id": "0a52a8331d3b45089ebd74f8118d4fa1",
+        "release": "14.7",
+        "platform": "native",
+        "exception": {
+            "values": [
+                {
+                    "type": "EXCEPTION_ACCESS_VIOLATION_READ / 0x65707980",
+                    "value": "Fatal Error: EXCEPTION_ACCESS_VIOLATION_READ / 0x65707980",
+                    "stacktrace": {"frames": frames},
+                    "mechanism": {"type": "minidump", "synthetic": True, "handled": False},
+                }
+            ]
+        },
+        "level": "fatal",
+        "contexts": {
+            "device": {"arch": "x86_64", "type": "device"},
+            "os": {
+                "name": "Windows",
+                "version": "10.0.22631",
+                "build": "3296",
+                "kernel_version": "10.0.22621.3296",
+                "type": "os",
+            },
+        },
+        "sdk": {"name": "sentry.native", "version": "0.6.0"},
+        "timestamp": time.time(),
+        "type": "error",
+    }
+
+    result.update(kwargs)
+    return result

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

@@ -2113,6 +2113,26 @@ register(
     flags=FLAG_AUTOMATOR_MODIFIABLE,
 )
 
+register(
+    "issues.sdk_crash_detection.native.project_id",
+    default=0,
+    type=Int,
+    flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE,
+)
+
+register(
+    "issues.sdk_crash_detection.native.organization_allowlist",
+    type=Sequence,
+    default=[],
+    flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE,
+)
+
+register(
+    "issues.sdk_crash_detection.native.sample_rate",
+    default=0.0,
+    flags=FLAG_AUTOMATOR_MODIFIABLE,
+)
+
 # END: SDK Crash Detection
 
 register(

+ 53 - 0
src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py

@@ -26,6 +26,7 @@ class SdkName(Enum):
     Cocoa = "cocoa"
     ReactNative = "react-native"
     Java = "java"
+    Native = "native"
 
 
 @dataclass
@@ -200,6 +201,58 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]:
         )
         configs.append(java_config)
 
+    native_options = _get_options(sdk_name=SdkName.Native, has_organization_allowlist=True)
+
+    if native_options:
+        native_config = SDKCrashDetectionConfig(
+            sdk_name=SdkName.Native,
+            project_id=native_options["project_id"],
+            sample_rate=native_options["sample_rate"],
+            organization_allowlist=native_options["organization_allowlist"],
+            sdk_names=[
+                "sentry.native",
+                "sentry.native.android",
+                "sentry.native.android.capacitor",
+                "sentry.native.android.flutter",
+                "sentry.native.android.react-native",
+                "sentry.native.android.unity",
+                "sentry.native.android.unreal",
+                "sentry.native.dotnet",
+                "sentry.native.unity",
+                "sentry.native.unreal",
+            ],
+            # 0.6.0 was released in Feb 2023, see https://github.com/getsentry/sentry-native/releases/tag/0.6.0.
+            min_sdk_version="0.6.0",
+            system_library_path_patterns={
+                # well known locations for unix paths
+                r"/lib/**",
+                r"/usr/lib/**",
+                r"/usr/local/lib/**",
+                r"/usr/local/Cellar/**",
+                r"linux-gate.so*",
+                # others
+                r"/System/Library/Frameworks/**",  # macOS
+                r"C:/Windows/**",
+                r"/system/**",
+                r"/vendor/**",
+                r"**/libart.so",
+                r"/apex/com.android.*/lib*/**",  # Android
+            },
+            sdk_frame_config=SDKFrameConfig(
+                function_patterns={
+                    r"sentry_*",  # public interface
+                    r"sentry__*",  # module level interface
+                },
+                # The native SDK usually has the same path as the application binary.
+                # Therefore, we can't rely on it. We set a fixed path of Sentry for
+                # the SDK frames are so it's not empty.
+                path_patterns=set(),
+                path_replacer=FixedPathReplacer(path="sentry"),
+            ),
+            sdk_crash_ignore_functions_matchers=set(),
+        )
+        configs.append(native_config)
+
     return configs
 
 

+ 1 - 1
src/sentry/utils/sdk_crashes/sdk_crash_detector.py

@@ -18,7 +18,7 @@ class SDKCrashDetector:
 
     @property
     def fields_containing_paths(self) -> set[str]:
-        return {"package", "module", "abs_path", "filename"}
+        return {"package", "module", "path", "abs_path", "filename"}
 
     def replace_sdk_frame_path(self, path_field: str, path_value: str) -> str | None:
         return self.config.sdk_frame_config.path_replacer.replace_path(path_field, path_value)

+ 19 - 1
tests/sentry/utils/sdk_crashes/test_build_sdk_crash_detection_configs.py

@@ -15,12 +15,15 @@ from sentry.utils.sdk_crashes.sdk_crash_detection_config import (
         "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],
+        "issues.sdk_crash_detection.native.project_id": 4,
+        "issues.sdk_crash_detection.native.sample_rate": 0.4,
+        "issues.sdk_crash_detection.native.organization_allowlist": [3],
     }
 )
 def test_build_sdk_crash_detection_configs():
     configs = build_sdk_crash_detection_configs()
 
-    assert len(configs) == 3
+    assert len(configs) == 4
 
     cocoa_config = configs[0]
     assert cocoa_config.sdk_name == SdkName.Cocoa
@@ -40,6 +43,12 @@ def test_build_sdk_crash_detection_configs():
     assert java_config.sample_rate == 0.3
     assert java_config.organization_allowlist == [2]
 
+    native_config = configs[3]
+    assert native_config.sdk_name == SdkName.Native
+    assert native_config.project_id == 4
+    assert native_config.sample_rate == 0.4
+    assert native_config.organization_allowlist == [3]
+
 
 @override_options(
     {
@@ -51,6 +60,9 @@ def test_build_sdk_crash_detection_configs():
         "issues.sdk_crash_detection.java.project_id": 0,
         "issues.sdk_crash_detection.java.sample_rate": 0.0,
         "issues.sdk_crash_detection.java.organization_allowlist": [],
+        "issues.sdk_crash_detection.native.project_id": 0,
+        "issues.sdk_crash_detection.native.sample_rate": 0.0,
+        "issues.sdk_crash_detection.javnativea.organization_allowlist": [],
     }
 )
 def test_build_sdk_crash_detection_configs_only_react_native():
@@ -74,6 +86,9 @@ def test_build_sdk_crash_detection_configs_only_react_native():
         "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],
+        "issues.sdk_crash_detection.native.project_id": 4,
+        "issues.sdk_crash_detection.native.sample_rate": 0.0,
+        "issues.sdk_crash_detection.native.organization_allowlist": [3],
     }
 )
 def test_build_sdk_crash_detection_configs_no_sample_rate():
@@ -97,6 +112,9 @@ def test_build_sdk_crash_detection_configs_no_sample_rate():
         "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],
+        "issues.sdk_crash_detection.native.project_id": 0,
+        "issues.sdk_crash_detection.native.sample_rate": 0.0,
+        "issues.sdk_crash_detection.native.organization_allowlist": [],
     }
 )
 def test_build_sdk_crash_detection_default_configs():

+ 203 - 0
tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_native.py

@@ -0,0 +1,203 @@
+from collections.abc import Sequence
+from functools import wraps
+from unittest.mock import patch
+
+import pytest
+
+from fixtures.sdk_crash_detection.crash_event_native 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.native.project_id": 4,
+            "issues.sdk_crash_detection.native.sample_rate": 0.11,
+            "issues.sdk_crash_detection.native.organization_allowlist": [3],
+        }
+    ):
+        return build_sdk_crash_detection_configs()
+
+
+@pytest.mark.parametrize(
+    ["sdk_frame_function", "system_frame_package", "detected"],
+    [
+        (
+            "sentry_value_to_msgpack",
+            "/lib/x86_64-linux-gnu/libc.so.6",
+            True,
+        ),
+        (
+            "sentry_value_to_msgpack",
+            "/usr/lib/x86_64-linux-gnu/libc.so.6",
+            True,
+        ),
+        (
+            "sentry_value_to_msgpack",
+            "/usr/local/lib/x86_64-linux-gnu/libc.so.6",
+            True,
+        ),
+        (
+            "sentry_value_to_msgpack",
+            "/usr/local/Cellar/x86_64-linux-gnu/libc.so.6",
+            True,
+        ),
+        (
+            "sentry_value_to_msgpack",
+            "linux-gate.so.1",
+            True,
+        ),
+        (
+            "sentry_value_to_msgpack",
+            "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation",
+            True,
+        ),
+        (
+            "sentry_value_to_msgpack",
+            "C:\\WINDOWS\\SYSTEM32\\ntdll.dll",
+            True,
+        ),
+        (
+            "sentry_value_to_msgpack",
+            "/system/somepath/libc.so.6",
+            True,
+        ),
+        (
+            "sentry_value_to_msgpack",
+            "/vendor/somepath/yes",
+            True,
+        ),
+        (
+            "sentr_value_to_msgpack",
+            "/lib/x86_64-linux-gnu/libc.so.6",
+            False,
+        ),
+        (
+            "sentry__module_level",
+            "/usr/lib/something.so",
+            True,
+        ),
+        (
+            "sentry_value_to_msgpack",
+            "/something/on/android/libart.so",
+            True,
+        ),
+        (
+            "sentry_value_to_msgpack",
+            "/apex/com.android.placeholder/libA/yes/yes",
+            True,
+        ),
+        (
+            "sentry_value_to_msgpack",
+            "/apex/com.android.placeholder/liA/yes/yes",
+            False,
+        ),
+    ],
+)
+@decorators
+def test_sdk_crash_is_reported_with_native_paths(
+    mock_sdk_crash_reporter,
+    mock_random,
+    store_event,
+    configs,
+    sdk_frame_function,
+    system_frame_package,
+    detected,
+):
+    event = store_event(
+        data=get_crash_event(
+            sdk_frame_function=sdk_frame_function, system_frame_package=system_frame_package
+        )
+    )
+
+    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) == 4
+
+        system_frame1 = stripped_frames[0]
+        assert system_frame1["function"] == "RtlUserThreadStart"
+        assert system_frame1["symbol"] == "RtlUserThreadStart"
+        assert system_frame1["package"] == "C:\\WINDOWS\\SYSTEM32\\ntdll.dll"
+        assert system_frame1["in_app"] is False
+
+        sdk_frame = stripped_frames[2]
+        assert sdk_frame["function"] == sdk_frame_function
+        assert sdk_frame["symbol"] == sdk_frame_function
+        assert sdk_frame["package"] == "sentry"
+        assert sdk_frame["in_app"] is True
+
+        system_frame2 = stripped_frames[3]
+        assert system_frame2["function"] == "boost::serialization::singleton<T>::singleton<T>"
+        assert (
+            system_frame2["symbol"]
+            == "??0?$singleton@V?$extended_type_info_typeid@T_E_SC_SI_OPT_IR_MODE_SELECTOR@@@serialization@boost@@@serialization@boost@@IEAA@XZ"
+        )
+        assert system_frame2["package"] == system_frame_package
+        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="0.6.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="0.5.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