diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bca3cf824..cfd9e36296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ - Pin `ffigen` to `19.0.0` and add `objective_c` version `8.0.0` package used in `ffigen` on iOS and macOS ([#3163](https://github.com/getsentry/sentry-dart/pull/3163)) +### 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 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 3975ac978c..3a5647478d 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 @@ -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) @@ -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? ?: 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, diff --git a/packages/flutter/ffi-jni.yaml b/packages/flutter/ffi-jni.yaml index 1e4969aa2a..cf7f4e0e6e 100644 --- a/packages/flutter/ffi-jni.yaml +++ b/packages/flutter/ffi-jni.yaml @@ -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 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 ac079f9c8a..8013f03dc7 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -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) @@ -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 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 d7094d838e..66579f3144 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -1,4 +1,8 @@ import 'dart:async'; +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; import 'package:meta/meta.dart'; import 'package:objective_c/objective_c.dart'; @@ -50,6 +54,32 @@ class SentryNativeCocoa extends SentryNativeChannel { return super.init(hub); } + @override + FutureOr captureEnvelope( + Uint8List envelopeData, bool containsUnhandledException) { + try { + final length = envelopeData.length; + final buffer = malloc(length); + buffer.asTypedList(length).setAll(0, envelopeData); + final nsData = NSData.dataWithBytesNoCopy$1(buffer.cast(), + length: length, freeWhenDone: true); + final envelope = cocoa.PrivateSentrySDKOnly.envelopeWithData(nsData); + if (envelope != null) { + cocoa.PrivateSentrySDKOnly.captureEnvelope(envelope); + } else { + options.log( + SentryLevel.error, 'Failed to capture envelope: envelope is null'); + } + } catch (exception, stackTrace) { + options.log(SentryLevel.error, 'Failed to capture envelope', + exception: exception, stackTrace: stackTrace); + + if (options.automatedTestMode) { + rethrow; + } + } + } + @override FutureOr setReplayConfig(ReplayConfig config) { // Note: unused on iOS. diff --git a/packages/flutter/lib/src/native/java/binding.dart b/packages/flutter/lib/src/native/java/binding.dart index a33dec0759..fc0836e50a 100644 --- a/packages/flutter/lib/src/native/java/binding.dart +++ b/packages/flutter/lib/src/native/java/binding.dart @@ -36,6 +36,298 @@ import 'dart:core' as core$_; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; +/// from: `io.sentry.android.core.InternalSentrySdk` +class InternalSentrySdk extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JObjType $type; + + @jni$_.internal + InternalSentrySdk.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = + jni$_.JClass.forName(r'io/sentry/android/core/InternalSentrySdk'); + + /// The type which includes information such as the signature of this class. + static const nullableType = $InternalSentrySdk$NullableType(); + static const type = $InternalSentrySdk$Type(); + static final _id_new$ = _class.constructorId( + r'()V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory InternalSentrySdk() { + return InternalSentrySdk.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr) + .reference); + } + + static final _id_getCurrentScope = _class.staticMethodId( + r'getCurrentScope', + r'()Lio/sentry/IScope;', + ); + + static final _getCurrentScope = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `static public io.sentry.IScope getCurrentScope()` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getCurrentScope() { + return _getCurrentScope( + _class.reference.pointer, _id_getCurrentScope as jni$_.JMethodIDPtr) + .object(const jni$_.JObjectNullableType()); + } + + static final _id_serializeScope = _class.staticMethodId( + r'serializeScope', + r'(Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/IScope;)Ljava/util/Map;', + ); + + static final _serializeScope = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer + )>)>>('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer)>(); + + /// from: `static public java.util.Map serializeScope(android.content.Context context, io.sentry.android.core.SentryAndroidOptions sentryAndroidOptions, io.sentry.IScope iScope)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JMap serializeScope( + jni$_.JObject context, + jni$_.JObject sentryAndroidOptions, + jni$_.JObject? iScope, + ) { + final _$context = context.reference; + final _$sentryAndroidOptions = sentryAndroidOptions.reference; + final _$iScope = iScope?.reference ?? jni$_.jNullReference; + return _serializeScope( + _class.reference.pointer, + _id_serializeScope as jni$_.JMethodIDPtr, + _$context.pointer, + _$sentryAndroidOptions.pointer, + _$iScope.pointer) + .object>( + const jni$_.JMapType( + jni$_.JStringNullableType(), jni$_.JObjectNullableType())); + } + + static final _id_captureEnvelope = _class.staticMethodId( + r'captureEnvelope', + r'([BZ)Lio/sentry/protocol/SentryId;', + ); + + static final _captureEnvelope = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_ + .VarArgs<(jni$_.Pointer, jni$_.Int32)>)>>( + 'globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer, int)>(); + + /// from: `static public io.sentry.protocol.SentryId captureEnvelope(byte[] bs, boolean z)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? captureEnvelope( + jni$_.JByteArray bs, + bool z, + ) { + final _$bs = bs.reference; + return _captureEnvelope(_class.reference.pointer, + _id_captureEnvelope as jni$_.JMethodIDPtr, _$bs.pointer, z ? 1 : 0) + .object(const jni$_.JObjectNullableType()); + } + + static final _id_getAppStartMeasurement = _class.staticMethodId( + r'getAppStartMeasurement', + r'()Ljava/util/Map;', + ); + + static final _getAppStartMeasurement = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `static public java.util.Map getAppStartMeasurement()` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JMap? getAppStartMeasurement() { + return _getAppStartMeasurement(_class.reference.pointer, + _id_getAppStartMeasurement as jni$_.JMethodIDPtr) + .object?>( + const jni$_.JMapNullableType( + jni$_.JStringNullableType(), jni$_.JObjectNullableType())); + } + + static final _id_setTrace = _class.staticMethodId( + r'setTrace', + r'(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V', + ); + + static final _setTrace = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer + )>)>>('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer)>(); + + /// from: `static public void setTrace(java.lang.String string, java.lang.String string1, java.lang.Double double, java.lang.Double double1)` + static void setTrace( + jni$_.JString string, + jni$_.JString string1, + jni$_.JDouble? double, + jni$_.JDouble? double1, + ) { + final _$string = string.reference; + final _$string1 = string1.reference; + final _$double = double?.reference ?? jni$_.jNullReference; + final _$double1 = double1?.reference ?? jni$_.jNullReference; + _setTrace( + _class.reference.pointer, + _id_setTrace as jni$_.JMethodIDPtr, + _$string.pointer, + _$string1.pointer, + _$double.pointer, + _$double1.pointer) + .check(); + } +} + +final class $InternalSentrySdk$NullableType + extends jni$_.JObjType { + @jni$_.internal + const $InternalSentrySdk$NullableType(); + + @jni$_.internal + @core$_.override + String get signature => r'Lio/sentry/android/core/InternalSentrySdk;'; + + @jni$_.internal + @core$_.override + InternalSentrySdk? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : InternalSentrySdk.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($InternalSentrySdk$NullableType).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($InternalSentrySdk$NullableType) && + other is $InternalSentrySdk$NullableType; + } +} + +final class $InternalSentrySdk$Type extends jni$_.JObjType { + @jni$_.internal + const $InternalSentrySdk$Type(); + + @jni$_.internal + @core$_.override + String get signature => r'Lio/sentry/android/core/InternalSentrySdk;'; + + @jni$_.internal + @core$_.override + InternalSentrySdk fromReference(jni$_.JReference reference) => + InternalSentrySdk.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => + const $InternalSentrySdk$NullableType(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($InternalSentrySdk$Type).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($InternalSentrySdk$Type) && + other is $InternalSentrySdk$Type; + } +} + /// from: `io.sentry.android.replay.ReplayIntegration` class ReplayIntegration extends jni$_.JObject { @jni$_.internal 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 37fb1085fd..afe21658be 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -1,9 +1,14 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:jni/jni.dart'; import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; import '../../replay/scheduled_recorder_config.dart'; import '../sentry_native_channel.dart'; import 'android_replay_recorder.dart'; +import 'binding.dart' as native; @internal class SentryNativeJava extends SentryNativeChannel { @@ -68,6 +73,33 @@ class SentryNativeJava extends SentryNativeChannel { return super.init(hub); } + @override + FutureOr captureEnvelope( + Uint8List envelopeData, bool containsUnhandledException) { + JObject? id; + JByteArray? byteArray; + try { + byteArray = JByteArray.from(envelopeData); + id = native.InternalSentrySdk.captureEnvelope( + byteArray, containsUnhandledException); + + if (id == null) { + options.log(SentryLevel.error, + 'Native Android SDK returned null id when capturing envelope'); + } + } catch (exception, stackTrace) { + options.log(SentryLevel.error, 'Failed to capture envelope', + exception: exception, stackTrace: stackTrace); + + if (options.automatedTestMode) { + rethrow; + } + } finally { + byteArray?.release(); + id?.release(); + } + } + @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 7e4450c547..7397e44e6e 100644 --- a/packages/flutter/lib/src/native/sentry_native_channel.dart +++ b/packages/flutter/lib/src/native/sentry_native_channel.dart @@ -101,7 +101,7 @@ class SentryNativeChannel bool get supportsCaptureEnvelope => true; @override - Future captureEnvelope( + FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { return channel.invokeMethod( 'captureEnvelope', [envelopeData, containsUnhandledException]); diff --git a/packages/flutter/test/sentry_native_channel_test.dart b/packages/flutter/test/sentry_native_channel_test.dart index d0b5caff77..4a3535851a 100644 --- a/packages/flutter/test/sentry_native_channel_test.dart +++ b/packages/flutter/test/sentry_native_channel_test.dart @@ -8,7 +8,6 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry/src/platform/mock_platform.dart'; -import 'package:sentry/src/platform/platform.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/native/factory.dart'; import 'package:sentry_flutter/src/native/method_channel_helper.dart'; @@ -199,16 +198,27 @@ void main() { if (mockPlatform.isAndroid) { matcher = throwsUnsupportedError; } else if (mockPlatform.isIOS || mockPlatform.isMacOS) { - if (Platform().isMacOS) { + if (mockPlatform.isMacOS) { matcher = throwsA(predicate((e) => - e is Exception && - e.toString().contains('Failed to load Objective-C class'))); + (e is Exception && + e + .toString() + .contains('Failed to load Objective-C class')) || + (e is ArgumentError && + e + .toString() + .contains('Couldn\'t resolve native function')))); } else { matcher = throwsA(predicate((e) => - e is ArgumentError && - (e.toString().contains('undefined symbol: objc_msgSend') || - e.toString().contains( - 'Couldn\'t resolve native function \'objc_msgSend\'')))); + (e is ArgumentError && + (e.toString().contains('undefined symbol: objc_msgSend') || + e + .toString() + .contains('Couldn\'t resolve native function'))) || + (e is Exception && + e + .toString() + .contains('Failed to load Objective-C class')))); } } expect(() => sut.startProfiler(SentryId.newId()), matcher); @@ -245,18 +255,45 @@ void main() { })); }); - test('captureEnvelope', () async { - final data = Uint8List.fromList([1, 2, 3]); + test( + 'captureEnvelope', + () { + when(channel.invokeMethod('captureEnvelope', any)) + .thenAnswer((_) async => {}); - late Uint8List captured; - when(channel.invokeMethod('captureEnvelope', any)).thenAnswer( - (invocation) async => - {captured = invocation.positionalArguments[1][0] as Uint8List}); + late Matcher matcher; + if (mockPlatform.isAndroid) { + matcher = throwsA(predicate((e) => + e is Error && + e.toString().contains('Unable to locate the helper library'))); + } else if (mockPlatform.isIOS || mockPlatform.isMacOS) { + if (mockPlatform.isMacOS) { + matcher = throwsA(predicate((e) => + (e is Exception && + (e + .toString() + .contains('Failed to load Objective-C class'))) || + (e is ArgumentError && + e + .toString() + .contains('Couldn\'t resolve native function')))); + } else { + matcher = throwsA(predicate((e) => + e is ArgumentError && + (e.toString().contains('undefined symbol: objc_msgSend') || + e + .toString() + .contains('Couldn\'t resolve native function') || + e.toString().contains('Failed to lookup symbol')))); + } + } - await sut.captureEnvelope(data, false); + final data = Uint8List.fromList([1, 2, 3]); + expect(() => sut.captureEnvelope(data, false), matcher); - expect(captured, data); - }); + verifyZeroInteractions(channel); + }, + ); test('loadContexts', () async { when(channel.invokeMethod('loadContexts'))