Skip to content

Use FFI/JNI for captureEnvelope on iOS and Android #3115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 33 commits into
base: deps/bump-ffigen
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
53e7a92
base impl for JNI
buenaflor Jul 29, 2025
a3b9557
base impl for cocoa -> maybe improve perf
buenaflor Jul 29, 2025
8ebbb99
update
buenaflor Jul 29, 2025
55e7802
update
buenaflor Jul 29, 2025
a048625
update
buenaflor Jul 29, 2025
6a637d6
update
buenaflor Jul 29, 2025
5fa2e5f
update
buenaflor Jul 29, 2025
fcdc0a0
Remove method channels for captureEnvelope
buenaflor Jul 29, 2025
a60b98b
Update
buenaflor Jul 29, 2025
8243b86
Update
buenaflor Jul 29, 2025
4d265af
Merge branch 'main' into enh/ffi-jni-capture-envelope
buenaflor Aug 5, 2025
2cc00d2
Merge branch 'main' into enh/ffi-jni-capture-envelope
buenaflor Aug 6, 2025
53412e6
Merge branch 'main' into enh/ffi-jni-capture-envelope
buenaflor Aug 7, 2025
e1b0fc1
Update
buenaflor Aug 7, 2025
42ee582
Update
buenaflor Aug 7, 2025
021872e
Update
buenaflor Aug 7, 2025
fe5f773
Update
buenaflor Aug 7, 2025
7ff132b
Update
buenaflor Aug 11, 2025
9d4755a
Update
buenaflor Aug 11, 2025
14b68d5
Update
buenaflor Aug 11, 2025
94e033c
Merge branch 'main' into enh/ffi-jni-capture-envelope
buenaflor Aug 11, 2025
01ae22f
Merge branch 'main' into enh/ffi-jni-capture-envelope
buenaflor Aug 11, 2025
3c78cc9
Update sentry_native_channel_test.dart
buenaflor Aug 11, 2025
9a84ffe
Merge branch 'deps/bump-ffigen' into enh/ffi-jni-capture-envelope
buenaflor Aug 12, 2025
6985094
Update
buenaflor Aug 13, 2025
162f88b
Update
buenaflor Aug 13, 2025
95dde11
Update
buenaflor Aug 13, 2025
f13ed42
Update tests
buenaflor Aug 13, 2025
2867ece
Fix analyze
buenaflor Aug 13, 2025
3da86cd
Fix tests
buenaflor Aug 13, 2025
dc051c6
Fix tests
buenaflor Aug 13, 2025
314c17b
Merge branch 'deps/bump-ffigen' into enh/ffi-jni-capture-envelope
buenaflor Aug 13, 2025
303cf57
Fix linux tests
buenaflor Aug 13, 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- Add `sentry.origin` to logs created by `LoggingIntegration` ([#3153](https://github.com/getsentry/sentry-dart/pull/3153))
- Tag all spans with thread info on non-web platforms ([#3101](https://github.com/getsentry/sentry-dart/pull/3101), [#3144](https://github.com/getsentry/sentry-dart/pull/3144))

### Enhancements

- Use FFI/JNI for `captureEnvelope` on iOS and Android ([#3115](https://github.com/getsentry/sentry-dart/pull/3115))

## 9.7.0-beta.1

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ class SentryFlutterPlugin :
) {
when (call.method) {
"initNativeSdk" -> initNativeSdk(call, result)
"captureEnvelope" -> captureEnvelope(call, result)
"loadImageList" -> loadImageList(call, result)
"closeNativeSdk" -> closeNativeSdk(result)
"fetchNativeAppStart" -> fetchNativeAppStart(result)
Expand Down Expand Up @@ -368,32 +367,6 @@ class SentryFlutterPlugin :

result.success("")
}

private fun captureEnvelope(
call: MethodCall,
result: Result,
) {
if (!Sentry.isEnabled()) {
result.error("1", "The Sentry Android SDK is disabled", null)
return
}
val args = call.arguments() as List<Any>? ?: listOf()
if (args.isNotEmpty()) {
val event = args.first() as ByteArray?
val containsUnhandledException = args[1] as Boolean
if (event != null && event.isNotEmpty()) {
val id = InternalSentrySdk.captureEnvelope(event, containsUnhandledException)
if (id != null) {
result.success("")
} else {
result.error("2", "Failed to capture envelope", null)
}
return
}
}
result.error("3", "Envelope is null or empty", null)
}

private fun loadImageList(
call: MethodCall,
result: Result,
Expand Down
1 change: 1 addition & 0 deletions packages/flutter/ffi-jni.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ output:
log_level: all

classes:
- io.sentry.android.core.InternalSentrySdk
- io.sentry.android.replay.ReplayIntegration
- io.sentry.flutter.SentryFlutterPlugin
- android.graphics.Bitmap
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
case "closeNativeSdk":
closeNativeSdk(call, result: result)

case "captureEnvelope":
captureEnvelope(call, result: result)

case "fetchNativeAppStart":
fetchNativeAppStart(result: result)

Expand Down Expand Up @@ -412,24 +409,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
return !name.isEmpty
}

private func captureEnvelope(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let arguments = call.arguments as? [Any],
!arguments.isEmpty,
let data = (arguments.first as? FlutterStandardTypedData)?.data else {
print("Envelope is null or empty!")
result(FlutterError(code: "2", message: "Envelope is null or empty", details: nil))
return
}
guard let envelope = PrivateSentrySDKOnly.envelope(with: data) else {
print("Cannot parse the envelope data")
result(FlutterError(code: "3", message: "Cannot parse the envelope data", details: nil))
return
}
PrivateSentrySDKOnly.capture(envelope)
result("")
return
}

struct TimeSpan {
var startTimestampMsSinceEpoch: NSNumber
var stopTimestampMsSinceEpoch: NSNumber
Expand Down
39 changes: 39 additions & 0 deletions packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:ffi';

import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';

import '../../../sentry_flutter.dart';
Expand Down Expand Up @@ -52,6 +54,43 @@ class SentryNativeCocoa extends SentryNativeChannel {
return super.init(hub);
}

@override
FutureOr<void> captureEnvelope(
Uint8List envelopeData, bool containsUnhandledException) {
// Use a safe copy-based conversion to avoid crashes due to memory issues observed
// when relying on `dataWithBytesNoCopy:length:freeWhenDone:`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you be more specific about what memory issues you've faced?

Copy link
Contributor Author

@buenaflor buenaflor Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vaind it always crashed when using withBytesNoCopy and freeWhenDone set to true (or set to false and manually freeing)

it seemed to be a bug with the current generated ffigen, the new bindings with ffigen 19.0.0 dont have that issue so we can safely use dataWithBytesNoCopy, from manual testing it works fine

final length = envelopeData.length;
final buffer = malloc<Uint8>(length);
cocoa.NSData? nsData;
cocoa.SentryEnvelope? envelope;
try {
buffer.asTypedList(length).setAll(0, envelopeData);
nsData = cocoa.NSData.dataWithBytes_length_(_lib, buffer.cast(), length);
envelope = cocoa.PrivateSentrySDKOnly.envelopeWithData_(_lib, nsData);
cocoa.PrivateSentrySDKOnly.captureEnvelope_(_lib, envelope);
} catch (exception, stackTrace) {
options.log(SentryLevel.error, 'Failed to capture envelope',
exception: exception, stackTrace: stackTrace);

if (options.automatedTestMode) {
rethrow;
}
} finally {
// Release ObjC wrappers promptly and free the temporary C buffer.
if (envelope != null) {
try {
envelope.release();
} catch (_) {}
}
if (nsData != null) {
try {
nsData.release();
} catch (_) {}
}
malloc.free(buffer);
}
}

@override
FutureOr<void> setReplayConfig(ReplayConfig config) {
// Note: unused on iOS.
Expand Down
Loading
Loading