Skip to content

Commit ad121c0

Browse files
authored
Refactor app hang and native crash apis to FFI/JNI (#3289)
* Update * Update * Update * Update * Update * Update * Configure diagnostic log * Update log messages * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Fix test * Update * Update * Update * Add automatedTestMode option * Update * Fix web tests * Update * Update * Add close * Review * Review * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Fix tests * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update
1 parent 29e8ebe commit ad121c0

File tree

10 files changed

+196
-54
lines changed

10 files changed

+196
-54
lines changed

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99

1010
### Enhancements
1111

12-
- Refactor `AndroidReplayRecorder` to use the new worker isolate api [#3296](https://github.com/getsentry/sentry-dart/pull/3296/)
13-
- Refactor fetching app start and display refresh rate to use FFI and JNI [#3288](https://github.com/getsentry/sentry-dart/pull/3288/)
14-
- Offload `captureEnvelope` to background isolate for Cocoa and Android [#3232](https://github.com/getsentry/sentry-dart/pull/3232)
12+
- Refactor app hang and crash apis to use FFI/JNI ([#3289](https://github.com/getsentry/sentry-dart/pull/3289/))
13+
- Refactor `AndroidReplayRecorder` to use the new worker isolate api ([#3296](https://github.com/getsentry/sentry-dart/pull/3296/))
14+
- Refactor fetching app start and display refresh rate to use FFI and JNI ([#3288](https://github.com/getsentry/sentry-dart/pull/3288/))
15+
- Offload `captureEnvelope` to background isolate for Cocoa and Android ([#3232](https://github.com/getsentry/sentry-dart/pull/3232))
1516
- Add `sentry.replay_id` to flutter logs ([#3257](https://github.com/getsentry/sentry-dart/pull/3257))
1617

1718
## 9.7.0

packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ class SentryFlutterPlugin :
7070
"removeExtra" -> removeExtra(call.argument("key"), result)
7171
"setTag" -> setTag(call.argument("key"), call.argument("value"), result)
7272
"removeTag" -> removeTag(call.argument("key"), result)
73-
"nativeCrash" -> crash()
7473
"setReplayConfig" -> setReplayConfig(call, result)
7574
"captureReplay" -> captureReplay(result)
7675
else -> result.notImplemented()
@@ -289,7 +288,15 @@ class SentryFlutterPlugin :
289288
@JvmStatic
290289
fun privateSentryGetReplayIntegration(): ReplayIntegration? = replay
291290

292-
@Suppress("unused") // Used by native/jni bindings
291+
@JvmStatic
292+
fun crash() {
293+
val exception = RuntimeException("FlutterSentry Native Integration: Sample RuntimeException")
294+
val mainThread = Looper.getMainLooper().thread
295+
mainThread.uncaughtExceptionHandler?.uncaughtException(mainThread, exception)
296+
mainThread.join(NATIVE_CRASH_WAIT_TIME)
297+
}
298+
299+
@Suppress("unused", "ReturnCount", "TooGenericExceptionCaught") // Used by native/jni bindings
293300
@JvmStatic
294301
fun getDisplayRefreshRate(): Int? {
295302
var refreshRate: Int? = null
@@ -463,13 +470,6 @@ class SentryFlutterPlugin :
463470
"debug_file" to debugFile,
464471
)
465472

466-
private fun crash() {
467-
val exception = RuntimeException("FlutterSentry Native Integration: Sample RuntimeException")
468-
val mainThread = Looper.getMainLooper().thread
469-
mainThread.uncaughtExceptionHandler?.uncaughtException(mainThread, exception)
470-
mainThread.join(NATIVE_CRASH_WAIT_TIME)
471-
}
472-
473473
private fun Double.adjustReplaySizeToBlockSize(): Double {
474474
val remainder = this % VIDEO_BLOCK_SIZE
475475
return if (remainder <= VIDEO_BLOCK_SIZE / 2) {

packages/flutter/ffi-cocoa.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,16 @@ objc-interfaces:
1919
- PrivateSentrySDKOnly
2020
- SentryId
2121
- SentryFlutterPlugin
22+
- SentrySDK
2223
module:
2324
'SentryId': 'Sentry'
25+
'SentrySDK': 'Sentry'
26+
member-filter:
27+
SentrySDK:
28+
include:
29+
- 'crash'
30+
- 'pauseAppHangTracking'
31+
- 'resumeAppHangTracking'
2432
preamble: |
2533
// ignore_for_file: type=lint, unused_element
2634

packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
130130
collectProfile(call, result)
131131
#endif
132132

133-
case "pauseAppHangTracking":
134-
pauseAppHangTracking(result)
135-
136-
case "resumeAppHangTracking":
137-
resumeAppHangTracking(result)
138-
139-
case "nativeCrash":
140-
crash()
141-
142133
case "captureReplay":
143134
#if canImport(UIKit) && !SENTRY_NO_UIKIT && (os(iOS) || os(tvOS))
144135
PrivateSentrySDKOnly.captureReplay()
@@ -431,20 +422,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
431422
result(nil)
432423
}
433424

434-
private func pauseAppHangTracking(_ result: @escaping FlutterResult) {
435-
SentrySDK.pauseAppHangTracking()
436-
result("")
437-
}
438-
439-
private func resumeAppHangTracking(_ result: @escaping FlutterResult) {
440-
SentrySDK.resumeAppHangTracking()
441-
result("")
442-
}
443-
444-
private func crash() {
445-
SentrySDK.crash()
446-
}
447-
448425
// MARK: - Objective-C interoperability
449426
//
450427
// Group of methods exposed to the Objective-C runtime via `@objc`.

packages/flutter/lib/src/native/cocoa/binding.dart

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,65 @@ class SentryId$1 extends objc.NSObject {
11201120
factory SentryId$1() => new$();
11211121
}
11221122

1123+
late final _class_SentrySDK = objc.getClass("Sentry.SentrySDK");
1124+
late final _sel_crash = objc.registerName("crash");
1125+
final _objc_msgSend_1pl9qdv = objc.msgSendPointer
1126+
.cast<
1127+
ffi.NativeFunction<
1128+
ffi.Void Function(ffi.Pointer<objc.ObjCObject>,
1129+
ffi.Pointer<objc.ObjCSelector>)>>()
1130+
.asFunction<
1131+
void Function(
1132+
ffi.Pointer<objc.ObjCObject>, ffi.Pointer<objc.ObjCSelector>)>();
1133+
late final _sel_pauseAppHangTracking =
1134+
objc.registerName("pauseAppHangTracking");
1135+
late final _sel_resumeAppHangTracking =
1136+
objc.registerName("resumeAppHangTracking");
1137+
1138+
/// The main entry point for the Sentry SDK.
1139+
/// We recommend using <code>start(configureOptions:)</code> to initialize Sentry.
1140+
class SentrySDK extends objc.NSObject {
1141+
SentrySDK._(ffi.Pointer<objc.ObjCObject> pointer,
1142+
{bool retain = false, bool release = false})
1143+
: super.castFromPointer(pointer, retain: retain, release: release);
1144+
1145+
/// Constructs a [SentrySDK] that points to the same underlying object as [other].
1146+
SentrySDK.castFrom(objc.ObjCObjectBase other)
1147+
: this._(other.ref.pointer, retain: true, release: true);
1148+
1149+
/// Constructs a [SentrySDK] that wraps the given raw object pointer.
1150+
SentrySDK.castFromPointer(ffi.Pointer<objc.ObjCObject> other,
1151+
{bool retain = false, bool release = false})
1152+
: this._(other, retain: retain, release: release);
1153+
1154+
/// Returns whether [obj] is an instance of [SentrySDK].
1155+
static bool isInstance(objc.ObjCObjectBase obj) {
1156+
return _objc_msgSend_19nvye5(
1157+
obj.ref.pointer, _sel_isKindOfClass_, _class_SentrySDK);
1158+
}
1159+
1160+
/// This forces a crash, useful to test the <code>SentryCrash</code> integration.
1161+
/// note:
1162+
/// The SDK can’t report a crash when a debugger is attached. Your application needs to run
1163+
/// without a debugger attached to capture the crash and send it to Sentry the next time you launch
1164+
/// your application.
1165+
static void crash() {
1166+
_objc_msgSend_1pl9qdv(_class_SentrySDK, _sel_crash);
1167+
}
1168+
1169+
/// Pauses sending detected app hangs to Sentry.
1170+
/// This method doesn’t close the detection of app hangs. Instead, the app hang detection
1171+
/// will ignore detected app hangs until you call <code>resumeAppHangTracking</code>.
1172+
static void pauseAppHangTracking() {
1173+
_objc_msgSend_1pl9qdv(_class_SentrySDK, _sel_pauseAppHangTracking);
1174+
}
1175+
1176+
/// Resumes sending detected app hangs to Sentry.
1177+
static void resumeAppHangTracking() {
1178+
_objc_msgSend_1pl9qdv(_class_SentrySDK, _sel_resumeAppHangTracking);
1179+
}
1180+
}
1181+
11231182
late final _class_SentryFlutterPlugin = objc.getClass("SentryFlutterPlugin");
11241183
late final _sel_getDisplayRefreshRate =
11251184
objc.registerName("getDisplayRefreshRate");

packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,17 @@ class SentryNativeCocoa extends SentryNativeChannel {
191191
return NativeAppStart.fromJson(json);
192192
},
193193
);
194+
195+
@override
196+
void nativeCrash() => cocoa.SentrySDK.crash();
197+
198+
@override
199+
void pauseAppHangTracking() => tryCatchSync('pauseAppHangTracking', () {
200+
cocoa.SentrySDK.pauseAppHangTracking();
201+
});
202+
203+
@override
204+
void resumeAppHangTracking() => tryCatchSync('resumeAppHangTracking', () {
205+
cocoa.SentrySDK.resumeAppHangTracking();
206+
});
194207
}

packages/flutter/lib/src/native/java/binding.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,28 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject {
13051305
.object<ReplayIntegration?>(const $ReplayIntegration$NullableType());
13061306
}
13071307

1308+
static final _id_crash = _class.instanceMethodId(
1309+
r'crash',
1310+
r'()V',
1311+
);
1312+
1313+
static final _crash = jni$_.ProtectedJniExtensions.lookup<
1314+
jni$_.NativeFunction<
1315+
jni$_.JThrowablePtr Function(
1316+
jni$_.Pointer<jni$_.Void>,
1317+
jni$_.JMethodIDPtr,
1318+
)>>('globalEnv_CallVoidMethod')
1319+
.asFunction<
1320+
jni$_.JThrowablePtr Function(
1321+
jni$_.Pointer<jni$_.Void>,
1322+
jni$_.JMethodIDPtr,
1323+
)>();
1324+
1325+
/// from: `public final void crash()`
1326+
void crash() {
1327+
_crash(reference.pointer, _id_crash as jni$_.JMethodIDPtr).check();
1328+
}
1329+
13081330
static final _id_getDisplayRefreshRate = _class.instanceMethodId(
13091331
r'getDisplayRefreshRate',
13101332
r'()Ljava/lang/Integer;',
@@ -1816,6 +1838,28 @@ class SentryFlutterPlugin extends jni$_.JObject {
18161838
.object<ReplayIntegration?>(const $ReplayIntegration$NullableType());
18171839
}
18181840

1841+
static final _id_crash = _class.staticMethodId(
1842+
r'crash',
1843+
r'()V',
1844+
);
1845+
1846+
static final _crash = jni$_.ProtectedJniExtensions.lookup<
1847+
jni$_.NativeFunction<
1848+
jni$_.JThrowablePtr Function(
1849+
jni$_.Pointer<jni$_.Void>,
1850+
jni$_.JMethodIDPtr,
1851+
)>>('globalEnv_CallStaticVoidMethod')
1852+
.asFunction<
1853+
jni$_.JThrowablePtr Function(
1854+
jni$_.Pointer<jni$_.Void>,
1855+
jni$_.JMethodIDPtr,
1856+
)>();
1857+
1858+
/// from: `static public final void crash()`
1859+
static void crash() {
1860+
_crash(_class.reference.pointer, _id_crash as jni$_.JMethodIDPtr).check();
1861+
}
1862+
18191863
static final _id_getDisplayRefreshRate = _class.staticMethodId(
18201864
r'getDisplayRefreshRate',
18211865
r'()Ljava/lang/Integer;',

packages/flutter/lib/src/native/java/sentry_native_java.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,21 @@ class SentryNativeJava extends SentryNativeChannel {
221221
});
222222
}
223223

224+
@override
225+
void nativeCrash() {
226+
native.SentryFlutterPlugin.Companion.crash();
227+
}
228+
229+
@override
230+
void pauseAppHangTracking() {
231+
assert(false, 'pauseAppHangTracking is not supported on Android.');
232+
}
233+
234+
@override
235+
void resumeAppHangTracking() {
236+
assert(false, 'resumeAppHangTracking is not supported on Android.');
237+
}
238+
224239
@override
225240
Future<void> close() async {
226241
await _replayRecorder?.stop();

packages/flutter/lib/src/native/sentry_native_channel.dart

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,15 +230,21 @@ class SentryNativeChannel
230230
}
231231

232232
@override
233-
Future<void> pauseAppHangTracking() =>
234-
channel.invokeMethod('pauseAppHangTracking');
233+
FutureOr<void> pauseAppHangTracking() {
234+
assert(false,
235+
'pauseAppHangTracking should not be used through method channels.');
236+
}
235237

236238
@override
237-
Future<void> resumeAppHangTracking() =>
238-
channel.invokeMethod('resumeAppHangTracking');
239+
FutureOr<void> resumeAppHangTracking() {
240+
assert(false,
241+
'resumeAppHangTracking should not be used through method channels.');
242+
}
239243

240244
@override
241-
Future<void> nativeCrash() => channel.invokeMethod('nativeCrash');
245+
FutureOr<void> nativeCrash() {
246+
assert(false, 'nativeCrash should not be used through method channels.');
247+
}
242248

243249
@override
244250
bool get supportsReplay => false;

packages/flutter/test/sentry_native_channel_test.dart

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -263,30 +263,49 @@ void main() {
263263
});
264264

265265
test('pauseAppHangTracking', () async {
266-
when(channel.invokeMethod('pauseAppHangTracking'))
267-
.thenAnswer((_) => Future.value());
268-
269-
await sut.pauseAppHangTracking();
266+
if (mockPlatform.isAndroid) {
267+
// Android doesn't support app hang tracking, so it should hit the assertion
268+
expect(() => sut.pauseAppHangTracking(), throwsAssertionError);
269+
} else {
270+
// iOS/macOS should throw FFI exceptions in tests
271+
final matcher = _nativeUnavailableMatcher(
272+
mockPlatform,
273+
includeLookupSymbol: true,
274+
includeFailedToLoadClassException: true,
275+
);
276+
expect(() => sut.pauseAppHangTracking(), matcher);
277+
}
270278

271-
verify(channel.invokeMethod('pauseAppHangTracking'));
279+
verifyZeroInteractions(channel);
272280
});
273281

274282
test('resumeAppHangTracking', () async {
275-
when(channel.invokeMethod('resumeAppHangTracking'))
276-
.thenAnswer((_) => Future.value());
277-
278-
await sut.resumeAppHangTracking();
283+
if (mockPlatform.isAndroid) {
284+
// Android doesn't support app hang tracking, so it should hit the assertion
285+
expect(() => sut.resumeAppHangTracking(), throwsAssertionError);
286+
} else {
287+
// iOS/macOS should throw FFI exceptions in tests
288+
final matcher = _nativeUnavailableMatcher(
289+
mockPlatform,
290+
includeLookupSymbol: true,
291+
includeFailedToLoadClassException: true,
292+
);
293+
expect(() => sut.resumeAppHangTracking(), matcher);
294+
}
279295

280-
verify(channel.invokeMethod('resumeAppHangTracking'));
296+
verifyZeroInteractions(channel);
281297
});
282298

283299
test('nativeCrash', () async {
284-
when(channel.invokeMethod('nativeCrash'))
285-
.thenAnswer((_) => Future.value());
300+
final matcher = _nativeUnavailableMatcher(
301+
mockPlatform,
302+
includeLookupSymbol: true,
303+
includeFailedToLoadClassException: true,
304+
);
286305

287-
await sut.nativeCrash();
306+
expect(() => sut.nativeCrash(), matcher);
288307

289-
verify(channel.invokeMethod('nativeCrash'));
308+
verifyZeroInteractions(channel);
290309
});
291310

292311
test('setReplayConfig', () async {

0 commit comments

Comments
 (0)