From c40ff0cb43691ab50b7041472944459e511758b9 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 15:15:07 +0200 Subject: [PATCH 1/2] Update --- .../io/sentry/flutter/SentryFlutterPlugin.kt | 17 +++---- .../sentry_flutter/SentryFlutterPlugin.swift | 35 +++++--------- .../sentry_flutter_objc/SentryFlutterPlugin.h | 3 ++ .../flutter/lib/src/native/cocoa/binding.dart | 30 ++++++++++++ .../src/native/cocoa/sentry_native_cocoa.dart | 19 ++++++++ .../flutter/lib/src/native/java/binding.dart | 47 +++++++++++++++++++ .../src/native/java/sentry_native_java.dart | 15 ++++++ .../lib/src/native/sentry_native_channel.dart | 16 +++++-- .../test/sentry_native_channel_test.dart | 47 +++++++++++++------ 9 files changed, 179 insertions(+), 50 deletions(-) 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 09e8d34122..13151ea1a0 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 @@ -71,7 +71,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,6 +288,15 @@ class SentryFlutterPlugin : @JvmStatic fun privateSentryGetReplayIntegration(): ReplayIntegration? = replay + @Suppress("unused") // Used by native/jni bindings + @JvmStatic + fun nativeCrash() { + 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") // Used by native/jni bindings @JvmStatic fun getDisplayRefreshRate(): Int? { @@ -457,13 +465,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/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift index 6629269211..44bb1eaff0 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`. @@ -541,6 +518,18 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { #endif } + @objc public class func nativeCrash() { + SentrySDK.crash() + } + + @objc public class func pauseAppHangTracking() { + SentrySDK.pauseAppHangTracking() + } + + @objc public class func resumeAppHangTracking() { + SentrySDK.resumeAppHangTracking() + } + @objc(loadDebugImagesAsBytes:) public class func loadDebugImagesAsBytes(instructionAddresses: Set) -> NSData? { var debugImages: [DebugMeta] = [] diff --git a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterPlugin.h b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterPlugin.h index 6f2e25eb54..61af58310d 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterPlugin.h +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterPlugin.h @@ -8,5 +8,8 @@ + (nullable NSData *)fetchNativeAppStartAsBytes; + (nullable NSData *)loadContextsAsBytes; + (nullable NSData *)loadDebugImagesAsBytes:(NSSet *)instructionAddresses; ++ (void)nativeCrash; ++ (void)pauseAppHangTracking; ++ (void)resumeAppHangTracking; @end #endif diff --git a/packages/flutter/lib/src/native/cocoa/binding.dart b/packages/flutter/lib/src/native/cocoa/binding.dart index 903062aa10..73d693b44d 100644 --- a/packages/flutter/lib/src/native/cocoa/binding.dart +++ b/packages/flutter/lib/src/native/cocoa/binding.dart @@ -1128,6 +1128,19 @@ late final _sel_fetchNativeAppStartAsBytes = late final _sel_loadContextsAsBytes = objc.registerName("loadContextsAsBytes"); late final _sel_loadDebugImagesAsBytes_ = objc.registerName("loadDebugImagesAsBytes:"); +late final _sel_nativeCrash = objc.registerName("nativeCrash"); +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"); /// SentryFlutterPlugin class SentryFlutterPlugin extends objc.NSObject { @@ -1186,6 +1199,23 @@ class SentryFlutterPlugin extends objc.NSObject { : objc.NSData.castFromPointer(_ret, retain: true, release: true); } + /// nativeCrash + static void nativeCrash() { + _objc_msgSend_1pl9qdv(_class_SentryFlutterPlugin, _sel_nativeCrash); + } + + /// pauseAppHangTracking + static void pauseAppHangTracking() { + _objc_msgSend_1pl9qdv( + _class_SentryFlutterPlugin, _sel_pauseAppHangTracking); + } + + /// resumeAppHangTracking + static void resumeAppHangTracking() { + _objc_msgSend_1pl9qdv( + _class_SentryFlutterPlugin, _sel_resumeAppHangTracking); + } + /// init SentryFlutterPlugin init() { objc.checkOsVersionInternal('SentryFlutterPlugin.init', 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 0a8dc2ee4a..097b26bc30 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -178,4 +178,23 @@ class SentryNativeCocoa extends SentryNativeChannel { return NativeAppStart.fromJson(json); }, ); + + @override + void nativeCrash() { + cocoa.SentryFlutterPlugin.nativeCrash(); + } + + @override + void pauseAppHangTracking() { + tryCatchSync('pauseAppHangTracking', () { + cocoa.SentryFlutterPlugin.pauseAppHangTracking(); + }); + } + + @override + void resumeAppHangTracking() { + tryCatchSync('resumeAppHangTracking', () { + cocoa.SentryFlutterPlugin.resumeAppHangTracking(); + }); + } } diff --git a/packages/flutter/lib/src/native/java/binding.dart b/packages/flutter/lib/src/native/java/binding.dart index feae89319b..f9730b691a 100644 --- a/packages/flutter/lib/src/native/java/binding.dart +++ b/packages/flutter/lib/src/native/java/binding.dart @@ -1305,6 +1305,29 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject { .object(const $ReplayIntegration$NullableType()); } + static final _id_nativeCrash = _class.instanceMethodId( + r'nativeCrash', + r'()V', + ); + + static final _nativeCrash = 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 nativeCrash()` + void nativeCrash() { + _nativeCrash(reference.pointer, _id_nativeCrash as jni$_.JMethodIDPtr) + .check(); + } + static final _id_getDisplayRefreshRate = _class.instanceMethodId( r'getDisplayRefreshRate', r'()Ljava/lang/Integer;', @@ -1816,6 +1839,30 @@ class SentryFlutterPlugin extends jni$_.JObject { .object(const $ReplayIntegration$NullableType()); } + static final _id_nativeCrash = _class.staticMethodId( + r'nativeCrash', + r'()V', + ); + + static final _nativeCrash = 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 nativeCrash()` + static void nativeCrash() { + _nativeCrash( + _class.reference.pointer, _id_nativeCrash 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 b79d8b9db4..817d51c00d 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -199,6 +199,21 @@ class SentryNativeJava extends SentryNativeChannel { }); } + @override + void nativeCrash() { + native.SentryFlutterPlugin.Companion.nativeCrash(); + } + + @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 384bdcaca7..b1f2f20ab4 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 { From fce96c708a6c5c14770596941286c8b777c11c25 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Oct 2025 14:31:08 +0200 Subject: [PATCH 2/2] Update --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fefedbdabe..751275ef2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ # Changelog -## 9.7.0 +## Unreleased + +### Enhancements +- Move app hang and crash apis to use FFI/JNI ([#3289](https://github.com/getsentry/sentry-dart/pull/3289/)) + +## 9.7.0 ### Features