Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

### Enhancements

- Move 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/)
- Offload `captureEnvelope` to background isolate for Cocoa and Android [#3232](https://github.com/getsentry/sentry-dart/pull/3232)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -289,6 +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") // Used by native/jni bindings
@JvmStatic
fun getDisplayRefreshRate(): Int? {
Expand Down Expand Up @@ -463,13 +471,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) {
Expand Down
8 changes: 8 additions & 0 deletions packages/flutter/ffi-cocoa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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`.
Expand Down
59 changes: 59 additions & 0 deletions packages/flutter/lib/src/native/cocoa/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<objc.ObjCObject>,
ffi.Pointer<objc.ObjCSelector>)>>()
.asFunction<
void Function(
ffi.Pointer<objc.ObjCObject>, ffi.Pointer<objc.ObjCSelector>)>();
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 <code>start(configureOptions:)</code> to initialize Sentry.
class SentrySDK extends objc.NSObject {
SentrySDK._(ffi.Pointer<objc.ObjCObject> 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<objc.ObjCObject> 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 <code>SentryCrash</code> 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 <code>resumeAppHangTracking</code>.
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");
Expand Down
13 changes: 13 additions & 0 deletions packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,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();
});
}
44 changes: 44 additions & 0 deletions packages/flutter/lib/src/native/java/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,28 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject {
.object<ReplayIntegration?>(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$_.Void>,
jni$_.JMethodIDPtr,
)>>('globalEnv_CallVoidMethod')
.asFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
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;',
Expand Down Expand Up @@ -1816,6 +1838,28 @@ class SentryFlutterPlugin extends jni$_.JObject {
.object<ReplayIntegration?>(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$_.Void>,
jni$_.JMethodIDPtr,
)>>('globalEnv_CallStaticVoidMethod')
.asFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
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;',
Expand Down
15 changes: 15 additions & 0 deletions packages/flutter/lib/src/native/java/sentry_native_java.dart
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,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<void> close() async {
await _replayRecorder?.stop();
Expand Down
16 changes: 11 additions & 5 deletions packages/flutter/lib/src/native/sentry_native_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,21 @@ class SentryNativeChannel
}

@override
Future<void> pauseAppHangTracking() =>
channel.invokeMethod('pauseAppHangTracking');
FutureOr<void> pauseAppHangTracking() {
assert(false,
'pauseAppHangTracking should not be used through method channels.');
}

@override
Future<void> resumeAppHangTracking() =>
channel.invokeMethod('resumeAppHangTracking');
FutureOr<void> resumeAppHangTracking() {
assert(false,
'resumeAppHangTracking should not be used through method channels.');
}

@override
Future<void> nativeCrash() => channel.invokeMethod('nativeCrash');
FutureOr<void> nativeCrash() {
assert(false, 'nativeCrash should not be used through method channels.');
}

@override
bool get supportsReplay => false;
Expand Down
47 changes: 33 additions & 14 deletions packages/flutter/test/sentry_native_channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading