From 95551127f24f31bd21cd8bb27b42e3ab4df504b2 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 21 Aug 2024 16:29:58 +0200 Subject: [PATCH] Provide a way to cause an example native crash from Flutter (#2239) * add SentryFlutter.nativeCrash() for Android and iOS * add changelog entry * remove unused variable * improved kotlin implementation * fix kotlin analysis warnings * Update CHANGELOG.md Co-authored-by: Giancarlo Buenaflor * fix kotlin linter errors * remove whitespace * add Description for nativeCrash --------- Co-authored-by: Giancarlo Buenaflor --- CHANGELOG.md | 2 ++ .../io/sentry/flutter/SentryFlutterPlugin.kt | 28 +++++++++++------ flutter/example/lib/main.dart | 12 +++++++ .../Classes/SentryFlutterPluginApple.swift | 7 +++++ .../lib/src/native/sentry_native_binding.dart | 2 ++ .../lib/src/native/sentry_native_channel.dart | 3 ++ flutter/lib/src/sentry_flutter.dart | 31 +++++++++++++------ flutter/test/sentry_native_channel_test.dart | 9 ++++++ 8 files changed, 75 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 672ed7fce9..ccdca3529d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features +- Add `SentryFlutter.nativeCrash()` using MethodChannels for Android and iOS ([#2239](https://github.com/getsentry/sentry-dart/pull/2239)) + - This can be used to test if native crash reporting works - Add `ignoreRoutes` parameter to `SentryNavigatorObserver`. ([#2218](https://github.com/getsentry/sentry-dart/pull/2218)) - This will ignore the Routes and prevent the Route from being pushed to the Sentry server. - Ignored routes will also create no TTID and TTFD spans. diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt index 4f154a2465..5bb1296a24 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt @@ -3,6 +3,7 @@ package io.sentry.flutter import android.app.Activity import android.content.Context import android.os.Build +import android.os.Looper import android.util.Log import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware @@ -49,8 +50,8 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { sentryFlutter = SentryFlutter( - androidSdk = androidSdk, - nativeSdk = nativeSdk, + androidSdk = ANDROID_SDK, + nativeSdk = NATIVE_SDK, ) } @@ -74,6 +75,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { "removeTag" -> removeTag(call.argument("key"), result) "loadContexts" -> loadContexts(result) "displayRefreshRate" -> displayRefreshRate(result) + "nativeCrash" -> crash() else -> result.notImplemented() } } @@ -413,16 +415,17 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } companion object { + private const val FLUTTER_SDK = "sentry.dart.flutter" + private const val ANDROID_SDK = "sentry.java.android.flutter" + private const val NATIVE_SDK = "sentry.native.android.flutter" + private const val NATIVE_CRASH_WAIT_TIME = 500L - private const val flutterSdk = "sentry.dart.flutter" - private const val androidSdk = "sentry.java.android.flutter" - private const val nativeSdk = "sentry.native.android.flutter" private fun setEventOriginTag(event: SentryEvent) { event.sdk?.let { when (it.name) { - flutterSdk -> setEventEnvironmentTag(event, "flutter", "dart") - androidSdk -> setEventEnvironmentTag(event, environment = "java") - nativeSdk -> setEventEnvironmentTag(event, environment = "native") + FLUTTER_SDK -> setEventEnvironmentTag(event, "flutter", "dart") + ANDROID_SDK -> setEventEnvironmentTag(event, environment = "java") + NATIVE_SDK -> setEventEnvironmentTag(event, environment = "native") else -> return } } @@ -439,7 +442,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private fun addPackages(event: SentryEvent, sdk: SdkVersion?) { event.sdk?.let { - if (it.name == flutterSdk) { + if (it.name == FLUTTER_SDK) { sdk?.packageSet?.forEach { sentryPackage -> it.addPackage(sentryPackage.name, sentryPackage.version) } @@ -449,6 +452,13 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } } + + private fun crash() { + val exception = RuntimeException("FlutterSentry Native Integration: Sample RuntimeException") + val mainThread = Looper.getMainLooper().thread + mainThread.uncaughtExceptionHandler.uncaughtException(mainThread, exception) + mainThread.join(NATIVE_CRASH_WAIT_TIME) + } } private fun loadContexts(result: Result) { diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 82bcd0b3c8..7c11d17cca 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -758,6 +758,12 @@ class AndroidExample extends StatelessWidget { }, child: const Text('Platform exception'), ), + ElevatedButton( + onPressed: () async { + SentryFlutter.nativeCrash(); + }, + child: const Text('Sentry.nativeCrash'), + ), ]); } } @@ -870,6 +876,12 @@ class CocoaExample extends StatelessWidget { }, child: const Text('Objective-C SEGFAULT'), ), + ElevatedButton( + onPressed: () async { + SentryFlutter.nativeCrash(); + }, + child: const Text('Sentry.nativeCrash'), + ), ], ); } diff --git a/flutter/ios/Classes/SentryFlutterPluginApple.swift b/flutter/ios/Classes/SentryFlutterPluginApple.swift index 35249ef5d1..c2024f3779 100644 --- a/flutter/ios/Classes/SentryFlutterPluginApple.swift +++ b/flutter/ios/Classes/SentryFlutterPluginApple.swift @@ -174,6 +174,9 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { case "resumeAppHangTracking": resumeAppHangTracking(result) + case "nativeCrash": + crash() + default: result(FlutterMethodNotImplemented) } @@ -729,6 +732,10 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { SentrySDK.resumeAppHangTracking() result("") } + + private func crash() { + SentrySDK.crash() + } } // swiftlint:enable function_body_length diff --git a/flutter/lib/src/native/sentry_native_binding.dart b/flutter/lib/src/native/sentry_native_binding.dart index 002790fc32..168326fcc9 100644 --- a/flutter/lib/src/native/sentry_native_binding.dart +++ b/flutter/lib/src/native/sentry_native_binding.dart @@ -57,4 +57,6 @@ abstract class SentryNativeBinding { Future pauseAppHangTracking(); Future resumeAppHangTracking(); + + Future nativeCrash(); } diff --git a/flutter/lib/src/native/sentry_native_channel.dart b/flutter/lib/src/native/sentry_native_channel.dart index 623111bb9e..97245870db 100644 --- a/flutter/lib/src/native/sentry_native_channel.dart +++ b/flutter/lib/src/native/sentry_native_channel.dart @@ -194,4 +194,7 @@ class SentryNativeChannel @override Future resumeAppHangTracking() => _channel.invokeMethod('resumeAppHangTracking'); + + @override + Future nativeCrash() => _channel.invokeMethod('nativeCrash'); } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index c688f9862b..3ff835284c 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -249,11 +249,7 @@ mixin SentryFlutter { /// Only for iOS and macOS. static Future pauseAppHangTracking() { if (_native == null) { - // ignore: invalid_use_of_internal_member - Sentry.currentHub.options.logger( - SentryLevel.debug, - 'Native integration is not available. Make sure SentryFlutter is initialized before accessing the pauseAppHangTracking API.', - ); + _logNativeIntegrationNotAvailable("pauseAppHangTracking"); return Future.value(); } return _native!.pauseAppHangTracking(); @@ -263,11 +259,7 @@ mixin SentryFlutter { /// Only for iOS and macOS static Future resumeAppHangTracking() { if (_native == null) { - // ignore: invalid_use_of_internal_member - Sentry.currentHub.options.logger( - SentryLevel.debug, - 'Native integration is not available. Make sure SentryFlutter is initialized before accessing the resumeAppHangTracking API.', - ); + _logNativeIntegrationNotAvailable("resumeAppHangTracking"); return Future.value(); } return _native!.resumeAppHangTracking(); @@ -280,4 +272,23 @@ mixin SentryFlutter { static set native(SentryNativeBinding? value) => _native = value; static SentryNativeBinding? _native; + + /// Use `nativeCrash()` to crash the native implementation and test/debug the crash reporting for native code. + /// This should not be used in production code. + /// Only for Android, iOS and macOS + static Future nativeCrash() { + if (_native == null) { + _logNativeIntegrationNotAvailable("nativeCrash"); + return Future.value(); + } + return _native!.nativeCrash(); + } + + static void _logNativeIntegrationNotAvailable(String methodName) { + // ignore: invalid_use_of_internal_member + Sentry.currentHub.options.logger( + SentryLevel.debug, + 'Native integration is not available. Make sure SentryFlutter is initialized before accessing the $methodName API.', + ); + } } diff --git a/flutter/test/sentry_native_channel_test.dart b/flutter/test/sentry_native_channel_test.dart index 5ad5eb2b3f..fb339b4682 100644 --- a/flutter/test/sentry_native_channel_test.dart +++ b/flutter/test/sentry_native_channel_test.dart @@ -302,6 +302,15 @@ void main() { verify(channel.invokeMethod('resumeAppHangTracking')); }); + + test('nativeCrash', () async { + when(channel.invokeMethod('nativeCrash')) + .thenAnswer((_) => Future.value()); + + await sut.nativeCrash(); + + verify(channel.invokeMethod('nativeCrash')); + }); }); } }