Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
03f8118
Update
buenaflor Sep 2, 2025
6928f3a
Update
buenaflor Sep 3, 2025
a91cbae
Update
buenaflor Sep 3, 2025
a6bd3cc
Update
buenaflor Sep 3, 2025
b9269c7
Update
buenaflor Sep 3, 2025
6ef9960
Update
buenaflor Sep 4, 2025
a43f2e1
Configure diagnostic log
buenaflor Sep 4, 2025
e334269
Update log messages
buenaflor Sep 4, 2025
aa728e7
Update
buenaflor Sep 4, 2025
45cc8c3
Update
buenaflor Sep 4, 2025
a603960
Update
buenaflor Sep 4, 2025
147da01
Update
buenaflor Sep 4, 2025
2b11149
Update
buenaflor Sep 4, 2025
8325952
Update
buenaflor Sep 4, 2025
3dbe751
Update
buenaflor Sep 4, 2025
71ba593
Update
buenaflor Sep 4, 2025
fe7f6df
Update
buenaflor Sep 4, 2025
39a951e
Update
buenaflor Oct 7, 2025
884642f
Fix test
buenaflor Oct 7, 2025
f5f5401
Update
buenaflor Oct 7, 2025
de232c6
Update
buenaflor Oct 7, 2025
69d5111
Update
buenaflor Oct 7, 2025
79d9ff9
Merge branch 'main' into enh/long-lived-envelope-worker
buenaflor Oct 7, 2025
62bb12b
Add automatedTestMode option
buenaflor Oct 7, 2025
532d2b3
Merge branch 'main' into enh/long-lived-envelope-worker
buenaflor Oct 7, 2025
53c6036
Update
buenaflor Oct 7, 2025
06ee227
Fix web tests
buenaflor Oct 7, 2025
10d9419
Update
buenaflor Oct 7, 2025
e6771bb
Update
buenaflor Oct 7, 2025
e2ae6a3
Add close
buenaflor Oct 7, 2025
ae9b24c
Review
buenaflor Oct 8, 2025
7efb747
Merge branch 'main' into enh/long-lived-envelope-worker
buenaflor Oct 8, 2025
4b440d0
Review
buenaflor Oct 8, 2025
91b0298
Update
buenaflor Oct 8, 2025
2cd6dd8
Update
buenaflor Oct 8, 2025
2f1840d
Update
buenaflor Oct 8, 2025
7750213
Update
buenaflor Oct 9, 2025
cd554e4
Update
buenaflor Oct 9, 2025
9708168
Update
buenaflor Oct 9, 2025
99b5584
Update
buenaflor Oct 9, 2025
eead0ce
Update
buenaflor Oct 9, 2025
780a59c
Update
buenaflor Oct 9, 2025
143414f
Update
buenaflor Oct 9, 2025
60f9674
Update
buenaflor Oct 9, 2025
3eeda60
Fix tests
buenaflor Oct 9, 2025
a40112f
Update
buenaflor Oct 9, 2025
83f8ea4
Update
buenaflor Oct 9, 2025
f9edebc
Update
buenaflor Oct 9, 2025
c40ff0c
Update
buenaflor Oct 9, 2025
e80304a
Merge branch 'enh/app-start-refresh-rate-jni-ffi' into enh/app-hang-f…
buenaflor Oct 9, 2025
895068c
Merge branch 'main' into enh/app-start-refresh-rate-jni-ffi
buenaflor Oct 13, 2025
1ffba6f
Update
buenaflor Oct 14, 2025
f881241
Merge branch 'enh/app-start-refresh-rate-jni-ffi' into enh/app-hang-f…
buenaflor Oct 14, 2025
9733bc8
Merge branch 'main' into enh/app-start-refresh-rate-jni-ffi
buenaflor Oct 14, 2025
fce96c7
Update
buenaflor Oct 16, 2025
958fa0d
Update
buenaflor Oct 16, 2025
84542ab
Update
buenaflor Oct 16, 2025
e744a5e
Update
buenaflor Oct 16, 2025
9912628
Update
buenaflor Oct 16, 2025
dee9256
Update
buenaflor Oct 16, 2025
ab2f4e9
Update
buenaflor Oct 16, 2025
ed51094
Update
buenaflor Oct 16, 2025
0d378b3
Update
buenaflor Oct 16, 2025
5abe802
Merge branch 'enh/app-start-refresh-rate-jni-ffi' into enh/app-hang-f…
buenaflor Oct 16, 2025
aacf1ff
Update
buenaflor Oct 20, 2025
714279e
Update
buenaflor Oct 20, 2025
ab8fd7f
Merge branch 'main' into enh/app-hang-ffi-jni-refactor
buenaflor Oct 23, 2025
dc580bb
Update
buenaflor Oct 23, 2025
3b4c2d3
Update
buenaflor Oct 23, 2025
e82e153
Update
buenaflor Oct 23, 2025
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
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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,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
Expand Down Expand Up @@ -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) {
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 @@ -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();
});
}
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 @@ -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<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