diff --git a/CHANGELOG.md b/CHANGELOG.md index fc15ae0ce8..ed0208bf9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,10 @@ ### Enhancements -- Refactor `AndroidReplayRecorder` to use the new worker isolate api [#3296](https://github.com/getsentry/sentry-dart/pull/3296/) -- Refactor fetching app start and display refresh rate to use FFI and JNI [#3288](https://github.com/getsentry/sentry-dart/pull/3288/) -- Offload `captureEnvelope` to background isolate for Cocoa and Android [#3232](https://github.com/getsentry/sentry-dart/pull/3232) +- Refactor app hang and crash apis to use FFI/JNI ([#3289](https://github.com/getsentry/sentry-dart/pull/3289/)) +- Refactor `AndroidReplayRecorder` to use the new worker isolate api ([#3296](https://github.com/getsentry/sentry-dart/pull/3296/)) +- Refactor fetching app start and display refresh rate to use FFI and JNI ([#3288](https://github.com/getsentry/sentry-dart/pull/3288/)) +- Offload `captureEnvelope` to background isolate for Cocoa and Android ([#3232](https://github.com/getsentry/sentry-dart/pull/3232)) - Add `sentry.replay_id` to flutter logs ([#3257](https://github.com/getsentry/sentry-dart/pull/3257)) ## 9.7.0 diff --git a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt index da755ecd10..bd0843006c 100644 --- a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt +++ b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt @@ -70,7 +70,6 @@ class SentryFlutterPlugin : "removeExtra" -> removeExtra(call.argument("key"), result) "setTag" -> setTag(call.argument("key"), call.argument("value"), result) "removeTag" -> removeTag(call.argument("key"), result) - "nativeCrash" -> crash() "setReplayConfig" -> setReplayConfig(call, result) "captureReplay" -> captureReplay(result) else -> result.notImplemented() @@ -289,7 +288,15 @@ class SentryFlutterPlugin : @JvmStatic fun privateSentryGetReplayIntegration(): ReplayIntegration? = replay - @Suppress("unused") // Used by native/jni bindings + @JvmStatic + 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) + } + + @Suppress("unused", "ReturnCount", "TooGenericExceptionCaught") // Used by native/jni bindings @JvmStatic fun getDisplayRefreshRate(): Int? { var refreshRate: Int? = null @@ -463,13 +470,6 @@ class SentryFlutterPlugin : "debug_file" to debugFile, ) - 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 Double.adjustReplaySizeToBlockSize(): Double { val remainder = this % VIDEO_BLOCK_SIZE return if (remainder <= VIDEO_BLOCK_SIZE / 2) { diff --git a/packages/flutter/ffi-cocoa.yaml b/packages/flutter/ffi-cocoa.yaml index 72cf5a88e0..bc1a60cdcf 100644 --- a/packages/flutter/ffi-cocoa.yaml +++ b/packages/flutter/ffi-cocoa.yaml @@ -19,8 +19,16 @@ objc-interfaces: - PrivateSentrySDKOnly - SentryId - SentryFlutterPlugin + - SentrySDK module: 'SentryId': 'Sentry' + 'SentrySDK': 'Sentry' + member-filter: + SentrySDK: + include: + - 'crash' + - 'pauseAppHangTracking' + - 'resumeAppHangTracking' preamble: | // ignore_for_file: type=lint, unused_element diff --git a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift index 7d275bcca8..ca10f8140a 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -130,15 +130,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { collectProfile(call, result) #endif - case "pauseAppHangTracking": - pauseAppHangTracking(result) - - case "resumeAppHangTracking": - resumeAppHangTracking(result) - - case "nativeCrash": - crash() - case "captureReplay": #if canImport(UIKit) && !SENTRY_NO_UIKIT && (os(iOS) || os(tvOS)) PrivateSentrySDKOnly.captureReplay() @@ -431,20 +422,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { result(nil) } - private func pauseAppHangTracking(_ result: @escaping FlutterResult) { - SentrySDK.pauseAppHangTracking() - result("") - } - - private func resumeAppHangTracking(_ result: @escaping FlutterResult) { - SentrySDK.resumeAppHangTracking() - result("") - } - - private func crash() { - SentrySDK.crash() - } - // MARK: - Objective-C interoperability // // Group of methods exposed to the Objective-C runtime via `@objc`. diff --git a/packages/flutter/lib/src/native/cocoa/binding.dart b/packages/flutter/lib/src/native/cocoa/binding.dart index 903062aa10..d39050185c 100644 --- a/packages/flutter/lib/src/native/cocoa/binding.dart +++ b/packages/flutter/lib/src/native/cocoa/binding.dart @@ -1120,6 +1120,65 @@ class SentryId$1 extends objc.NSObject { factory SentryId$1() => new$(); } +late final _class_SentrySDK = objc.getClass("Sentry.SentrySDK"); +late final _sel_crash = objc.registerName("crash"); +final _objc_msgSend_1pl9qdv = objc.msgSendPointer + .cast< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>() + .asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); +late final _sel_pauseAppHangTracking = + objc.registerName("pauseAppHangTracking"); +late final _sel_resumeAppHangTracking = + objc.registerName("resumeAppHangTracking"); + +/// The main entry point for the Sentry SDK. +/// We recommend using start(configureOptions:) to initialize Sentry. +class SentrySDK extends objc.NSObject { + SentrySDK._(ffi.Pointer pointer, + {bool retain = false, bool release = false}) + : super.castFromPointer(pointer, retain: retain, release: release); + + /// Constructs a [SentrySDK] that points to the same underlying object as [other]. + SentrySDK.castFrom(objc.ObjCObjectBase other) + : this._(other.ref.pointer, retain: true, release: true); + + /// Constructs a [SentrySDK] that wraps the given raw object pointer. + SentrySDK.castFromPointer(ffi.Pointer other, + {bool retain = false, bool release = false}) + : this._(other, retain: retain, release: release); + + /// Returns whether [obj] is an instance of [SentrySDK]. + static bool isInstance(objc.ObjCObjectBase obj) { + return _objc_msgSend_19nvye5( + obj.ref.pointer, _sel_isKindOfClass_, _class_SentrySDK); + } + + /// This forces a crash, useful to test the SentryCrash integration. + /// note: + /// The SDK can’t report a crash when a debugger is attached. Your application needs to run + /// without a debugger attached to capture the crash and send it to Sentry the next time you launch + /// your application. + static void crash() { + _objc_msgSend_1pl9qdv(_class_SentrySDK, _sel_crash); + } + + /// Pauses sending detected app hangs to Sentry. + /// This method doesn’t close the detection of app hangs. Instead, the app hang detection + /// will ignore detected app hangs until you call resumeAppHangTracking. + static void pauseAppHangTracking() { + _objc_msgSend_1pl9qdv(_class_SentrySDK, _sel_pauseAppHangTracking); + } + + /// Resumes sending detected app hangs to Sentry. + static void resumeAppHangTracking() { + _objc_msgSend_1pl9qdv(_class_SentrySDK, _sel_resumeAppHangTracking); + } +} + late final _class_SentryFlutterPlugin = objc.getClass("SentryFlutterPlugin"); late final _sel_getDisplayRefreshRate = objc.registerName("getDisplayRefreshRate"); diff --git a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart index 1a9cbadd21..35f1cf4144 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -191,4 +191,17 @@ class SentryNativeCocoa extends SentryNativeChannel { return NativeAppStart.fromJson(json); }, ); + + @override + void nativeCrash() => cocoa.SentrySDK.crash(); + + @override + void pauseAppHangTracking() => tryCatchSync('pauseAppHangTracking', () { + cocoa.SentrySDK.pauseAppHangTracking(); + }); + + @override + void resumeAppHangTracking() => tryCatchSync('resumeAppHangTracking', () { + cocoa.SentrySDK.resumeAppHangTracking(); + }); } diff --git a/packages/flutter/lib/src/native/java/binding.dart b/packages/flutter/lib/src/native/java/binding.dart index feae89319b..9b59a003b3 100644 --- a/packages/flutter/lib/src/native/java/binding.dart +++ b/packages/flutter/lib/src/native/java/binding.dart @@ -1305,6 +1305,28 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject { .object(const $ReplayIntegration$NullableType()); } + static final _id_crash = _class.instanceMethodId( + r'crash', + r'()V', + ); + + static final _crash = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final void crash()` + void crash() { + _crash(reference.pointer, _id_crash as jni$_.JMethodIDPtr).check(); + } + static final _id_getDisplayRefreshRate = _class.instanceMethodId( r'getDisplayRefreshRate', r'()Ljava/lang/Integer;', @@ -1816,6 +1838,28 @@ class SentryFlutterPlugin extends jni$_.JObject { .object(const $ReplayIntegration$NullableType()); } + static final _id_crash = _class.staticMethodId( + r'crash', + r'()V', + ); + + static final _crash = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `static public final void crash()` + static void crash() { + _crash(_class.reference.pointer, _id_crash as jni$_.JMethodIDPtr).check(); + } + static final _id_getDisplayRefreshRate = _class.staticMethodId( r'getDisplayRefreshRate', r'()Ljava/lang/Integer;', diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index 9f62ceacca..83c3f1a098 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -221,6 +221,21 @@ class SentryNativeJava extends SentryNativeChannel { }); } + @override + void nativeCrash() { + native.SentryFlutterPlugin.Companion.crash(); + } + + @override + void pauseAppHangTracking() { + assert(false, 'pauseAppHangTracking is not supported on Android.'); + } + + @override + void resumeAppHangTracking() { + assert(false, 'resumeAppHangTracking is not supported on Android.'); + } + @override Future close() async { await _replayRecorder?.stop(); diff --git a/packages/flutter/lib/src/native/sentry_native_channel.dart b/packages/flutter/lib/src/native/sentry_native_channel.dart index 7ff6ee2591..fd6c05c1be 100644 --- a/packages/flutter/lib/src/native/sentry_native_channel.dart +++ b/packages/flutter/lib/src/native/sentry_native_channel.dart @@ -230,15 +230,21 @@ class SentryNativeChannel } @override - Future pauseAppHangTracking() => - channel.invokeMethod('pauseAppHangTracking'); + FutureOr pauseAppHangTracking() { + assert(false, + 'pauseAppHangTracking should not be used through method channels.'); + } @override - Future resumeAppHangTracking() => - channel.invokeMethod('resumeAppHangTracking'); + FutureOr resumeAppHangTracking() { + assert(false, + 'resumeAppHangTracking should not be used through method channels.'); + } @override - Future nativeCrash() => channel.invokeMethod('nativeCrash'); + FutureOr nativeCrash() { + assert(false, 'nativeCrash should not be used through method channels.'); + } @override bool get supportsReplay => false; diff --git a/packages/flutter/test/sentry_native_channel_test.dart b/packages/flutter/test/sentry_native_channel_test.dart index f35d68ea8b..7288d00971 100644 --- a/packages/flutter/test/sentry_native_channel_test.dart +++ b/packages/flutter/test/sentry_native_channel_test.dart @@ -263,30 +263,49 @@ void main() { }); test('pauseAppHangTracking', () async { - when(channel.invokeMethod('pauseAppHangTracking')) - .thenAnswer((_) => Future.value()); - - await sut.pauseAppHangTracking(); + if (mockPlatform.isAndroid) { + // Android doesn't support app hang tracking, so it should hit the assertion + expect(() => sut.pauseAppHangTracking(), throwsAssertionError); + } else { + // iOS/macOS should throw FFI exceptions in tests + final matcher = _nativeUnavailableMatcher( + mockPlatform, + includeLookupSymbol: true, + includeFailedToLoadClassException: true, + ); + expect(() => sut.pauseAppHangTracking(), matcher); + } - verify(channel.invokeMethod('pauseAppHangTracking')); + verifyZeroInteractions(channel); }); test('resumeAppHangTracking', () async { - when(channel.invokeMethod('resumeAppHangTracking')) - .thenAnswer((_) => Future.value()); - - await sut.resumeAppHangTracking(); + if (mockPlatform.isAndroid) { + // Android doesn't support app hang tracking, so it should hit the assertion + expect(() => sut.resumeAppHangTracking(), throwsAssertionError); + } else { + // iOS/macOS should throw FFI exceptions in tests + final matcher = _nativeUnavailableMatcher( + mockPlatform, + includeLookupSymbol: true, + includeFailedToLoadClassException: true, + ); + expect(() => sut.resumeAppHangTracking(), matcher); + } - verify(channel.invokeMethod('resumeAppHangTracking')); + verifyZeroInteractions(channel); }); test('nativeCrash', () async { - when(channel.invokeMethod('nativeCrash')) - .thenAnswer((_) => Future.value()); + final matcher = _nativeUnavailableMatcher( + mockPlatform, + includeLookupSymbol: true, + includeFailedToLoadClassException: true, + ); - await sut.nativeCrash(); + expect(() => sut.nativeCrash(), matcher); - verify(channel.invokeMethod('nativeCrash')); + verifyZeroInteractions(channel); }); test('setReplayConfig', () async {