From 0ea034a6fd17cc5a9323b07fbbf04f8cfc38cfed Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 27 Nov 2025 15:38:48 +0100 Subject: [PATCH 01/13] Update --- .../flutter/example/integration_test/all.dart | 2 + .../integration_test/integration_test.dart | 48 ++- .../native_ffi_jni_utils_test.dart | 283 ++++++++++++++++++ .../example/integration_test/utils.dart | 3 + .../src/native/cocoa/sentry_native_cocoa.dart | 68 ++--- .../cocoa/sentry_native_cocoa_init.dart | 8 +- .../src/native/java/sentry_native_java.dart | 39 +-- .../native/java/sentry_native_java_init.dart | 2 +- 8 files changed, 380 insertions(+), 73 deletions(-) create mode 100644 packages/flutter/example/integration_test/native_ffi_jni_utils_test.dart diff --git a/packages/flutter/example/integration_test/all.dart b/packages/flutter/example/integration_test/all.dart index e1b57dd2ec..6a5b582b38 100644 --- a/packages/flutter/example/integration_test/all.dart +++ b/packages/flutter/example/integration_test/all.dart @@ -3,10 +3,12 @@ import 'integration_test.dart' as a; import 'profiling_test.dart' as b; import 'replay_test.dart' as c; import 'platform_integrations_test.dart' as d; +import 'native_ffi_jni_utils_test.dart' as e; void main() { a.main(); b.main(); c.main(); d.main(); + e.main(); } diff --git a/packages/flutter/example/integration_test/integration_test.dart b/packages/flutter/example/integration_test/integration_test.dart index 0e0ecc058a..b07079b4c4 100644 --- a/packages/flutter/example/integration_test/integration_test.dart +++ b/packages/flutter/example/integration_test/integration_test.dart @@ -769,11 +769,20 @@ void main() { }); // 1. Add a breadcrumb via Dart + final customObject = CustomObject(); final testBreadcrumb = Breadcrumb( - message: 'test-breadcrumb-message', - category: 'test-category', - level: SentryLevel.info, - ); + message: 'test-breadcrumb-message', + category: 'test-category', + level: SentryLevel.info, + data: { + 'string': 'data', + 'int': 12, + 'bool': true, + 'double': 12.34, + 'map': {'nested': 'data', 'custom object': customObject}, + 'list': [1, customObject, 3], + 'custom object': customObject + }); await Sentry.addBreadcrumb(testBreadcrumb); // 2. Verify it appears in native via loadContexts @@ -794,6 +803,17 @@ void main() { expect(testCrumb, isNotNull, reason: 'Test breadcrumb should exist in native breadcrumbs'); expect(testCrumb['category'], equals('test-category')); + expect(testCrumb['level'], equals('info')); + expect(testCrumb['data'], isNotNull); + expect(testCrumb['data']['map'], isNotNull); + expect(testCrumb['data']['map']['nested'], equals('data')); + expect(testCrumb['data']['map']['custom object'], + equals(customObject.toString())); + expect(testCrumb['data']['list'], isNotNull); + expect(testCrumb['data']['list'][0], equals(1)); + expect(testCrumb['data']['list'][1], equals(customObject.toString())); + expect(testCrumb['data']['list'][2], equals(3)); + expect(testCrumb['data']['custom object'], equals(customObject.toString())); // 3. Clear breadcrumbs await Sentry.configureScope((scope) async { @@ -813,10 +833,20 @@ void main() { }); // 1. Set a user via Dart + final customObject = CustomObject(); final testUser = SentryUser( id: 'test-user-id', email: 'test@example.com', username: 'test-username', + data: { + 'string': 'data', + 'int': 12, + 'bool': true, + 'double': 12.34, + 'map': {'nested': 'data', 'custom object': customObject}, + 'list': [1, customObject, 3], + 'custom object': customObject + }, ); await Sentry.configureScope((scope) async { await scope.setUser(testUser); @@ -844,6 +874,16 @@ void main() { expect(user['username'], isNull); expect(user['id'], isNotNull); expect(user['id'], isNotEmpty); + expect(user['data'], isNotNull); + expect(user['data']['map'], isNotNull); + expect(user['data']['map']['nested'], equals('data')); + expect( + user['data']['map']['custom object'], equals(customObject.toString())); + expect(user['data']['list'], isNotNull); + expect(user['data']['list'][0], equals(1)); + expect(user['data']['list'][1], equals(customObject.toString())); + expect(user['data']['list'][2], equals(3)); + expect(user['data']['custom object'], equals(customObject.toString())); }); testWidgets('loads debug images through loadDebugImages', (tester) async { diff --git a/packages/flutter/example/integration_test/native_ffi_jni_utils_test.dart b/packages/flutter/example/integration_test/native_ffi_jni_utils_test.dart new file mode 100644 index 0000000000..9b4472e101 --- /dev/null +++ b/packages/flutter/example/integration_test/native_ffi_jni_utils_test.dart @@ -0,0 +1,283 @@ +// ignore_for_file: depend_on_referenced_packages + +@TestOn('vm') + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:jni/jni.dart'; +import 'package:objective_c/objective_c.dart'; +import 'package:sentry_flutter/src/native/cocoa/sentry_native_cocoa.dart'; +import 'package:sentry_flutter/src/native/java/sentry_native_java.dart'; + +import 'utils.dart'; + +final _customObject = CustomObject(); + +final _nestedMap = { + 'innerString': 'nested', + 'innerList': [1, null, 2], + 'innerNull': null, +}; + +final _testList = [ + 'value', + 1, + 1.1, + true, + _customObject, + ['nestedList', 2], + _nestedMap, + null, +]; + +final _testMap = { + 'key': 'value', + 'key2': 1, + 'key3': 1.1, + 'key4': true, + 'key5': _customObject, + 'list': _testList, + 'nestedMap': _nestedMap, + 'nullEntry': null, +}; + +const _expectedListLength = 7; +const _expectedNestedListLength = 2; +const _expectedMapLength = 7; + +void main() { + group('JNI (Android)', () { + test('dartToJObject converts primitives', () { + _expectJniString(dartToJObject('value'), 'value'); + _expectJniInt(dartToJObject(1), 1); + _expectJniDouble(dartToJObject(1.1), 1.1); + _expectJniBool(dartToJObject(true), true); + _expectJniString(dartToJObject(_customObject), _customObject.toString()); + }); + + test('dartToJObject converts list (drops nulls)', () { + final jList = dartToJObject(_testList).as(JList.type(JObject.type)); + addTearDown(jList.release); + _verifyJniList(jList); + }); + + test('dartToJObject converts map (drops null values)', () { + final jMap = + dartToJObject(_testMap).as(JMap.type(JString.type, JObject.type)); + addTearDown(jMap.release); + _verifyJniMap(jMap); + }); + + test('dartToJList', () { + final jList = dartToJList(_testList); + addTearDown(jList.release); + _verifyJniList(jList); + }); + + test('dartToJMap', () { + final jMap = dartToJMap(_testMap); + addTearDown(jMap.release); + _verifyJniMap(jMap); + }); + }, skip: !Platform.isAndroid); + + group('FFI (iOS/macOS)', () { + test('dartToNSObject converts primitives', () { + _expectNSString(dartToNSObject('value'), 'value'); + _expectNSInt(dartToNSObject(1), 1); + _expectNSDouble(dartToNSObject(1.1), 1.1); + _expectNSBool(dartToNSObject(true), true); + _expectNSString(dartToNSObject(_customObject), _customObject.toString()); + }); + + test('dartToNSObject converts list (drops nulls)', () { + final nsArray = NSArray.castFrom(dartToNSObject(_testList)); + _verifyNSArray(nsArray); + }); + + test('dartToNSObject converts map (drops null values)', () { + final nsDict = NSDictionary.castFrom(dartToNSObject(_testMap)); + _verifyNSDictionary(nsDict); + }); + + test('dartToNSArray', () { + _verifyNSArray(dartToNSArray(_testList)); + }); + + test('dartToNSDictionary', () { + _verifyNSDictionary(dartToNSDictionary(_testMap)); + }); + }, skip: !(Platform.isIOS || Platform.isMacOS)); +} + +void _expectJniString(JObject obj, String expected) { + expect(obj, isA()); + expect((obj as JString).toDartString(releaseOriginal: true), expected); +} + +void _expectJniInt(JObject obj, int expected) { + expect(obj, isA()); + expect((obj as JLong).longValue(releaseOriginal: true), expected); +} + +void _expectJniDouble(JObject obj, double expected) { + expect(obj, isA()); + expect((obj as JDouble).doubleValue(releaseOriginal: true), expected); +} + +void _expectJniBool(JObject obj, bool expected) { + expect(obj, isA()); + expect((obj as JBoolean).booleanValue(releaseOriginal: true), expected); +} + +JObject? _jniGet(JMap map, String key) { + final jKey = key.toJString(); + final value = map[jKey]; + jKey.release(); + return value; +} + +bool _jniIsNull(JObject? obj) => obj == null || obj.toString() == 'null'; + +void _verifyJniList(JList list) { + expect(list.length, _expectedListLength); + + // Verify primitives + expect(list[0].as(JString.type).toDartString(), 'value'); + expect(list[1].as(JLong.type).longValue(), 1); + expect(list[2].as(JDouble.type).doubleValue(), 1.1); + expect(list[3].as(JBoolean.type).booleanValue(), isTrue); + expect(list[4].as(JString.type).toDartString(), _customObject.toString()); + + // Verify nested list + final nestedList = list[5].as(JList.type(JObject.type)); + expect(nestedList.length, 2); + expect(nestedList[0].as(JString.type).toDartString(), 'nestedList'); + expect(nestedList[1].as(JLong.type).longValue(), 2); + nestedList.release(); + + // Verify nested map + final nestedMap = list[6].as(JMap.type(JString.type, JObject.type)); + _verifyJniNestedMap(nestedMap); + nestedMap.release(); +} + +void _verifyJniMap(JMap map) { + expect(map.length, _expectedMapLength); + + // Verify primitives + expect(_jniGet(map, 'key')!.as(JString.type).toDartString(), 'value'); + expect(_jniGet(map, 'key2')!.as(JLong.type).longValue(), 1); + expect(_jniGet(map, 'key3')!.as(JDouble.type).doubleValue(), 1.1); + expect(_jniGet(map, 'key4')!.as(JBoolean.type).booleanValue(), isTrue); + expect(_jniGet(map, 'key5')!.as(JString.type).toDartString(), + _customObject.toString()); + + // Verify nested list + final nestedList = _jniGet(map, 'list')!.as(JList.type(JObject.type)); + _verifyJniList(nestedList); + nestedList.release(); + + // Verify nested map + final nestedMap = + _jniGet(map, 'nestedMap')!.as(JMap.type(JString.type, JObject.type)); + _verifyJniNestedMap(nestedMap); + nestedMap.release(); + + // Verify null was dropped + expect(_jniIsNull(_jniGet(map, 'nullEntry')), isTrue); +} + +void _verifyJniNestedMap(JMap map) { + expect( + _jniGet(map, 'innerString')!.as(JString.type).toDartString(), 'nested'); + + final innerList = _jniGet(map, 'innerList')!.as(JList.type(JObject.type)); + expect(innerList.length, _expectedNestedListLength); + expect(innerList[0].as(JLong.type).longValue(), 1); + expect(innerList[1].as(JLong.type).longValue(), 2); + innerList.release(); + + // Verify null was dropped + expect(_jniIsNull(_jniGet(map, 'innerNull')), isTrue); +} + +void _expectNSString(ObjCObjectBase obj, String expected) { + expect(NSString.isInstance(obj), isTrue); + expect(NSString.castFrom(obj).toDartString(), expected); +} + +void _expectNSInt(ObjCObjectBase obj, int expected) { + expect(NSNumber.isInstance(obj), isTrue); + expect(NSNumber.castFrom(obj).longLongValue, expected); +} + +void _expectNSDouble(ObjCObjectBase obj, double expected) { + expect(NSNumber.isInstance(obj), isTrue); + expect(NSNumber.castFrom(obj).doubleValue, expected); +} + +void _expectNSBool(ObjCObjectBase obj, bool expected) { + expect(NSNumber.isInstance(obj), isTrue); + expect(NSNumber.castFrom(obj).boolValue, expected); +} + +ObjCObjectBase? _nsGet(NSDictionary dict, String key) => + dict.objectForKey(key.toNSString()); + +void _verifyNSArray(NSArray array) { + expect(array.count, _expectedListLength); + + // Verify primitives + expect(NSString.castFrom(array.objectAtIndex(0)).toDartString(), 'value'); + expect(NSNumber.castFrom(array.objectAtIndex(1)).longLongValue, 1); + expect(NSNumber.castFrom(array.objectAtIndex(2)).doubleValue, 1.1); + expect(NSNumber.castFrom(array.objectAtIndex(3)).boolValue, isTrue); + expect(NSString.castFrom(array.objectAtIndex(4)).toDartString(), + _customObject.toString()); + + // Verify nested list + final nestedList = NSArray.castFrom(array.objectAtIndex(5)); + expect(nestedList.count, 2); + expect(NSString.castFrom(nestedList.objectAtIndex(0)).toDartString(), + 'nestedList'); + expect(NSNumber.castFrom(nestedList.objectAtIndex(1)).longLongValue, 2); + + // Verify nested map + _verifyNSNestedDict(NSDictionary.castFrom(array.objectAtIndex(6))); +} + +void _verifyNSDictionary(NSDictionary dict) { + expect(dict.count, _expectedMapLength); + + // Verify primitives + expect(NSString.castFrom(_nsGet(dict, 'key')!).toDartString(), 'value'); + expect(NSNumber.castFrom(_nsGet(dict, 'key2')!).longLongValue, 1); + expect(NSNumber.castFrom(_nsGet(dict, 'key3')!).doubleValue, 1.1); + expect(NSNumber.castFrom(_nsGet(dict, 'key4')!).boolValue, isTrue); + expect(NSString.castFrom(_nsGet(dict, 'key5')!).toDartString(), + _customObject.toString()); + + // Verify nested list + _verifyNSArray(NSArray.castFrom(_nsGet(dict, 'list')!)); + + // Verify nested map + _verifyNSNestedDict(NSDictionary.castFrom(_nsGet(dict, 'nestedMap')!)); + + // Verify null was dropped + expect(_nsGet(dict, 'nullEntry'), isNull); +} + +void _verifyNSNestedDict(NSDictionary dict) { + expect( + NSString.castFrom(_nsGet(dict, 'innerString')!).toDartString(), 'nested'); + + final innerList = NSArray.castFrom(_nsGet(dict, 'innerList')!); + expect(innerList.count, _expectedNestedListLength); + expect(NSNumber.castFrom(innerList.objectAtIndex(0)).longLongValue, 1); + expect(NSNumber.castFrom(innerList.objectAtIndex(1)).longLongValue, 2); + + // Verify null was dropped + expect(_nsGet(dict, 'innerNull'), isNull); +} diff --git a/packages/flutter/example/integration_test/utils.dart b/packages/flutter/example/integration_test/utils.dart index 3e1b42d2d7..89ba185c10 100644 --- a/packages/flutter/example/integration_test/utils.dart +++ b/packages/flutter/example/integration_test/utils.dart @@ -22,3 +22,6 @@ FutureOr restoreFlutterOnErrorAfter(FutureOr Function() fn) async { } const fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; + +// Used to test for correct serialization of custom object in attributes / data. +class CustomObject {} 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 856eef3b95..0e176b2611 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -167,7 +167,7 @@ class SentryNativeCocoa extends SentryNativeChannel { tryCatchSync('addBreadcrumb', () { final nativeBreadcrumb = cocoa.PrivateSentrySDKOnly.breadcrumbWithDictionary( - _dartToNSDictionary(breadcrumb.toJson())); + dartToNSDictionary(breadcrumb.toJson())); cocoa.SentrySDK.addBreadcrumb(nativeBreadcrumb); }); @@ -198,7 +198,7 @@ class SentryNativeCocoa extends SentryNativeChannel { if (user == null) { cocoa.SentrySDK.setUser(null); } else { - final dictionary = _dartToNSDictionary(user.toJson()); + final dictionary = dartToNSDictionary(user.toJson()); final cUser = cocoa.PrivateSentrySDKOnly.userWithDictionary(dictionary); cocoa.SentrySDK.setUser(cUser); @@ -212,9 +212,9 @@ class SentryNativeCocoa extends SentryNativeChannel { final normalizedValue = normalize(value); dictionary = switch (normalizedValue) { - Map m => _dartToNSDictionary(m), + Map m => dartToNSDictionary(m), Object o => NSDictionary.fromEntries( - [MapEntry('value'.toNSString(), _dartToNSObject(o))]), + [MapEntry('value'.toNSString(), dartToNSObject(o))]), _ => null }; @@ -261,7 +261,7 @@ class SentryNativeCocoa extends SentryNativeChannel { cocoa.SentrySDK.configureScope( cocoa.ObjCBlock_ffiVoid_SentryScope.fromFunction( (cocoa.SentryScope scope) { - scope.setExtraValue(_dartToNSObject(value as Object), + scope.setExtraValue(dartToNSObject(value as Object), forKey: key.toNSString()); })); }); @@ -295,53 +295,31 @@ class SentryNativeCocoa extends SentryNativeChannel { final ObjCObjectBase Function(Object) _defaultObjcConverter = (obj) { return switch (obj) { bool b => NSNumberCreation.numberWithBool(b), - _ => toObjCObject(obj) + _ => toObjCObject(obj.toString()), }; }; -NSDictionary _dartToNSDictionary(Map json) { - return _deepConvertMapNonNull(json) - .toNSDictionary(convertOther: _defaultObjcConverter); -} - -NSArray _dartToNSArray(List list) { - return _deepConvertListNonNull(list) - .toNSArray(convertOther: _defaultObjcConverter); -} - -ObjCObjectBase _dartToNSObject(Object value) { +@visibleForTesting +ObjCObjectBase dartToNSObject(Object value) { return switch (value) { - Map m => _dartToNSDictionary(m), - List l => _dartToNSArray(l), + Map m => dartToNSDictionary(m), + List l => dartToNSArray(l), _ => toObjCObject(value, convertOther: _defaultObjcConverter) }; } -List _deepConvertListNonNull(List list) => [ - for (final e in list) - if (e case Map m) - _deepConvertMapNonNull(m) - else if (e case List l) - _deepConvertListNonNull(l) - else if (e case Object o) - o, - ]; - -/// This map conversion is needed so we can use the toNSDictionary extension function -/// provided by the objective_c package. -Map _deepConvertMapNonNull(Map input) { - final out = {}; - - for (final entry in input.entries) { - final value = entry.value; - if (value == null) continue; - - out[entry.key] = switch (value) { - Map m => _deepConvertMapNonNull(m), - List l => _deepConvertListNonNull(l), - _ => value as Object, - }; - } +@visibleForTesting +NSDictionary dartToNSDictionary(Map json) { + return NSDictionary.fromEntries( + json.entries.where((e) => e.value != null).map((e) => MapEntry( + e.key.toNSString() as NSCopying, + dartToNSObject(e.value as Object), + ))); +} - return out; +@visibleForTesting +NSArray dartToNSArray(List list) { + return NSArray.of(list.nonNulls.map((element) { + return dartToNSObject(element); + })); } diff --git a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa_init.dart b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa_init.dart index 68285a254d..a7051d3163 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa_init.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa_init.dart @@ -29,7 +29,7 @@ void initSentryCocoa({ hub: hub, owner: owner, ), - tags: _dartToNSDictionary(options.privacy.toJson()), + tags: dartToNSDictionary(options.privacy.toJson()), ); } @@ -138,8 +138,8 @@ void configureCocoaOptions({ options.sdk.packages.map((e) => e.toJson()).toList(growable: false); cocoa.SentryFlutterPlugin.setBeforeSend( cocoaOptions, - packages: _dartToNSArray(packages), - integrations: _dartToNSArray(options.sdk.integrations), + packages: dartToNSArray(packages), + integrations: dartToNSArray(options.sdk.integrations), ); } @@ -176,7 +176,7 @@ cocoa.DartSentryReplayCaptureCallback createReplayCaptureCallback({ result(null); return; } - final nsDict = _dartToNSDictionary(Map.from(data)); + final nsDict = dartToNSDictionary(Map.from(data)); result(nsDict); }).catchError((Object exception, StackTrace stackTrace) { options.log( 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 3697c6e618..544b7ca6fb 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -192,7 +192,7 @@ class SentryNativeJava extends SentryNativeChannel { final nativeOptions = native.ScopesAdapter.getInstance()?.getOptions() ?..releasedBy(arena); if (nativeOptions == null) return; - final jMap = _dartToJMap(breadcrumb.toJson()); + final jMap = dartToJMap(breadcrumb.toJson()); final nativeBreadcrumb = native.Breadcrumb.fromMap(jMap, nativeOptions) ?..releasedBy(arena); @@ -219,7 +219,7 @@ class SentryNativeJava extends SentryNativeChannel { ?..releasedBy(arena); if (nativeOptions == null) return; - final jMap = _dartToJMap(user.toJson()); + final jMap = dartToJMap(user.toJson()); final nativeUser = native.User.fromMap(jMap, nativeOptions) ?..releasedBy(arena); // release jMap directly after use @@ -239,9 +239,7 @@ class SentryNativeJava extends SentryNativeChannel { run: (iScope) { using((arena) { final jKey = key.toJString()..releasedBy(arena); - final jVal = _dartToJObject(value)?..releasedBy(arena); - - if (jVal == null) return; + final jVal = dartToJObject(value)..releasedBy(arena); final scope = iScope.as(const native.$Scope$Type()) ..releasedBy(arena); @@ -390,35 +388,38 @@ class SentryNativeJava extends SentryNativeChannel { }); } -JObject? _dartToJObject(Object? value) => switch (value) { - null => null, +@visibleForTesting +JObject dartToJObject(Object? value) => switch (value) { String s => s.toJString(), bool b => b.toJBoolean(), int i => i.toJLong(), double d => d.toJDouble(), - List l => _dartToJList(l), - Map m => _dartToJMap(m), - _ => null + List l => dartToJList(l), + Map m => dartToJMap(m), + _ => value.toString().toJString() }; -JList _dartToJList(List values) { - final jList = JList.array(JObject.nullableType); - for (final v in values) { - final j = _dartToJObject(v); +@visibleForTesting +JList dartToJList(List values) { + final jList = JList.array(JObject.type); + for (final v in values.nonNulls) { + final j = dartToJObject(v); jList.add(j); - j?.release(); + j.release(); } return jList; } -JMap _dartToJMap(Map json) { - final jMap = JMap.hash(JString.type, JObject.nullableType); +@visibleForTesting +JMap dartToJMap(Map json) { + final jMap = JMap.hash(JString.type, JObject.type); for (final entry in json.entries) { + if (entry.value == null) continue; final jk = entry.key.toJString(); - final jv = _dartToJObject(entry.value); + final jv = dartToJObject(entry.value); jMap[jk] = jv; jk.release(); - jv?.release(); + jv.release(); } return jMap; } diff --git a/packages/flutter/lib/src/native/java/sentry_native_java_init.dart b/packages/flutter/lib/src/native/java/sentry_native_java_init.dart index 36642754be..98af364019 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java_init.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java_init.dart @@ -125,7 +125,7 @@ native.SentryOptions$BeforeSendReplayCallback createBeforeSendReplayCallback( return shouldRemove; }); - final jMap = _dartToJMap(options.privacy.toJson()); + final jMap = dartToJMap(options.privacy.toJson()); payload?.addAll(jMap); jMap.release(); } From 9e0ccbeb6e1faa2366d5c2d6a2ed37531d2220a6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 27 Nov 2025 15:45:24 +0100 Subject: [PATCH 02/13] Update --- packages/flutter/lib/src/native/java/sentry_native_java.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 544b7ca6fb..05ba9a7a16 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -413,8 +413,7 @@ JList dartToJList(List values) { @visibleForTesting JMap dartToJMap(Map json) { final jMap = JMap.hash(JString.type, JObject.type); - for (final entry in json.entries) { - if (entry.value == null) continue; + for (final entry in json.entries.where((e) => e.value != null)) { final jk = entry.key.toJString(); final jv = dartToJObject(entry.value); jMap[jk] = jv; From 7dc837d5901adbc14eca583484f5fa3ebd7dd6b5 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 27 Nov 2025 16:05:38 +0100 Subject: [PATCH 03/13] Fix test --- .../integration_test/integration_test.dart | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/flutter/example/integration_test/integration_test.dart b/packages/flutter/example/integration_test/integration_test.dart index b07079b4c4..b962dd41ba 100644 --- a/packages/flutter/example/integration_test/integration_test.dart +++ b/packages/flutter/example/integration_test/integration_test.dart @@ -861,6 +861,15 @@ void main() { expect(user!['id'], equals('test-user-id')); expect(user['email'], equals('test@example.com')); expect(user['username'], equals('test-username')); + expect(user['data']['map'], isNotNull); + expect(user['data']['map']['nested'], equals('data')); + expect( + user['data']['map']['custom object'], equals(customObject.toString())); + expect(user['data']['list'], isNotNull); + expect(user['data']['list'][0], equals(1)); + expect(user['data']['list'][1], equals(customObject.toString())); + expect(user['data']['list'][2], equals(3)); + expect(user['data']['custom object'], equals(customObject.toString())); // 3. Clear user (after clearing the id should remain) await Sentry.configureScope((scope) async { @@ -874,16 +883,6 @@ void main() { expect(user['username'], isNull); expect(user['id'], isNotNull); expect(user['id'], isNotEmpty); - expect(user['data'], isNotNull); - expect(user['data']['map'], isNotNull); - expect(user['data']['map']['nested'], equals('data')); - expect( - user['data']['map']['custom object'], equals(customObject.toString())); - expect(user['data']['list'], isNotNull); - expect(user['data']['list'][0], equals(1)); - expect(user['data']['list'][1], equals(customObject.toString())); - expect(user['data']['list'][2], equals(3)); - expect(user['data']['custom object'], equals(customObject.toString())); }); testWidgets('loads debug images through loadDebugImages', (tester) async { From 07d2248f378cb48e677045f7156ced7bd7484747 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 1 Dec 2025 16:41:52 +0100 Subject: [PATCH 04/13] Update --- packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart | 2 ++ packages/flutter/lib/src/native/java/sentry_native_java.dart | 1 + 2 files changed, 3 insertions(+) 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 0e176b2611..a2e3834b38 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -208,6 +208,8 @@ class SentryNativeCocoa extends SentryNativeChannel { @override void setContexts(String key, dynamic value) => tryCatchSync('setContexts', () { + if (value == null) return; + NSDictionary? dictionary; final normalizedValue = normalize(value); 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 05ba9a7a16..4b34674218 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -233,6 +233,7 @@ class SentryNativeJava extends SentryNativeChannel { @override void setContexts(String key, value) => tryCatchSync('setContexts', () { + if (value == null) return; native.Sentry.configureScope( native.ScopeCallback.implement( native.$ScopeCallback( From 2d053297bb22fcd8fd5d8f03f1a832fa11da070a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 1 Dec 2025 23:26:02 +0100 Subject: [PATCH 05/13] Update --- .../integration_test/integration_test.dart | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/flutter/example/integration_test/integration_test.dart b/packages/flutter/example/integration_test/integration_test.dart index b962dd41ba..cf04dbbc7d 100644 --- a/packages/flutter/example/integration_test/integration_test.dart +++ b/packages/flutter/example/integration_test/integration_test.dart @@ -843,7 +843,7 @@ void main() { 'int': 12, 'bool': true, 'double': 12.34, - 'map': {'nested': 'data', 'custom object': customObject}, + 'map': {'nested': 'data', 'custom object': 'customObject'}, 'list': [1, customObject, 3], 'custom object': customObject }, @@ -862,15 +862,26 @@ void main() { expect(user['email'], equals('test@example.com')); expect(user['username'], equals('test-username')); expect(user['data']['map'], isNotNull); - expect(user['data']['map']['nested'], equals('data')); - expect( - user['data']['map']['custom object'], equals(customObject.toString())); expect(user['data']['list'], isNotNull); - expect(user['data']['list'][0], equals(1)); - expect(user['data']['list'][1], equals(customObject.toString())); - expect(user['data']['list'][2], equals(3)); expect(user['data']['custom object'], equals(customObject.toString())); + if (Platform.isAndroid) { + // On Android, the Java SDK's User.data field only supports Map. + // Nested Maps and Lists are converted to Java's HashMap/ArrayList toString() + // format (e.g., {key=value} instead of {"key":"value"}). + expect(user['data']['map'], + equals('{nested=data, custom object=customObject}')); + expect( + user['data']['list'], equals('[1, ${customObject.toString()}, 3]')); + } else { + expect(user['data']['map']['nested'], equals('data')); + expect(user['data']['map']['custom object'], + equals(customObject.toString())); + expect(user['data']['list'][0], equals(1)); + expect(user['data']['list'][1], equals(customObject.toString())); + expect(user['data']['list'][2], equals(3)); + } + // 3. Clear user (after clearing the id should remain) await Sentry.configureScope((scope) async { await scope.setUser(null); From 330ac1aecbb46be7f99ef9d571b4a9f46bf59f04 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 1 Dec 2025 23:27:19 +0100 Subject: [PATCH 06/13] Update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d963158de..5a29ebf988 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Dart to native type conversion ([#3372](https://github.com/getsentry/sentry-dart/pull/3372)) + ## 9.9.0-beta.3 ### Features From 5f689b3305e5176b15f0a2c3e56217a23a521977 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 00:19:40 +0100 Subject: [PATCH 07/13] Update --- .../flutter/example/integration_test/integration_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter/example/integration_test/integration_test.dart b/packages/flutter/example/integration_test/integration_test.dart index cf04dbbc7d..6590730db9 100644 --- a/packages/flutter/example/integration_test/integration_test.dart +++ b/packages/flutter/example/integration_test/integration_test.dart @@ -843,7 +843,7 @@ void main() { 'int': 12, 'bool': true, 'double': 12.34, - 'map': {'nested': 'data', 'custom object': 'customObject'}, + 'map': {'nested': 'data', 'custom object': customObject}, 'list': [1, customObject, 3], 'custom object': customObject }, @@ -870,7 +870,7 @@ void main() { // Nested Maps and Lists are converted to Java's HashMap/ArrayList toString() // format (e.g., {key=value} instead of {"key":"value"}). expect(user['data']['map'], - equals('{nested=data, custom object=customObject}')); + equals('{nested=data, custom object=${customObject.toString()}}')); expect( user['data']['list'], equals('[1, ${customObject.toString()}, 3]')); } else { From d3251d0cfe8c3780b0e6e0f05f400297d2123bc4 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 9 Dec 2025 23:04:06 +0100 Subject: [PATCH 08/13] Add JNI utility tests for Dart to Java object conversion This commit introduces a new test file for verifying the conversion of Dart objects to JNI types, including primitives, lists, and maps. The tests ensure that null values are dropped appropriately and that nested structures are handled correctly. The tests are designed to run on the Android platform only. --- ...s_test.dart => native_jni_utils_test.dart} | 109 ------------------ 1 file changed, 109 deletions(-) rename packages/flutter/example/integration_test/{native_ffi_jni_utils_test.dart => native_jni_utils_test.dart} (56%) diff --git a/packages/flutter/example/integration_test/native_ffi_jni_utils_test.dart b/packages/flutter/example/integration_test/native_jni_utils_test.dart similarity index 56% rename from packages/flutter/example/integration_test/native_ffi_jni_utils_test.dart rename to packages/flutter/example/integration_test/native_jni_utils_test.dart index 9b4472e101..1b4ebce7b3 100644 --- a/packages/flutter/example/integration_test/native_ffi_jni_utils_test.dart +++ b/packages/flutter/example/integration_test/native_jni_utils_test.dart @@ -6,8 +6,6 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:jni/jni.dart'; -import 'package:objective_c/objective_c.dart'; -import 'package:sentry_flutter/src/native/cocoa/sentry_native_cocoa.dart'; import 'package:sentry_flutter/src/native/java/sentry_native_java.dart'; import 'utils.dart'; @@ -81,34 +79,6 @@ void main() { _verifyJniMap(jMap); }); }, skip: !Platform.isAndroid); - - group('FFI (iOS/macOS)', () { - test('dartToNSObject converts primitives', () { - _expectNSString(dartToNSObject('value'), 'value'); - _expectNSInt(dartToNSObject(1), 1); - _expectNSDouble(dartToNSObject(1.1), 1.1); - _expectNSBool(dartToNSObject(true), true); - _expectNSString(dartToNSObject(_customObject), _customObject.toString()); - }); - - test('dartToNSObject converts list (drops nulls)', () { - final nsArray = NSArray.castFrom(dartToNSObject(_testList)); - _verifyNSArray(nsArray); - }); - - test('dartToNSObject converts map (drops null values)', () { - final nsDict = NSDictionary.castFrom(dartToNSObject(_testMap)); - _verifyNSDictionary(nsDict); - }); - - test('dartToNSArray', () { - _verifyNSArray(dartToNSArray(_testList)); - }); - - test('dartToNSDictionary', () { - _verifyNSDictionary(dartToNSDictionary(_testMap)); - }); - }, skip: !(Platform.isIOS || Platform.isMacOS)); } void _expectJniString(JObject obj, String expected) { @@ -202,82 +172,3 @@ void _verifyJniNestedMap(JMap map) { // Verify null was dropped expect(_jniIsNull(_jniGet(map, 'innerNull')), isTrue); } - -void _expectNSString(ObjCObjectBase obj, String expected) { - expect(NSString.isInstance(obj), isTrue); - expect(NSString.castFrom(obj).toDartString(), expected); -} - -void _expectNSInt(ObjCObjectBase obj, int expected) { - expect(NSNumber.isInstance(obj), isTrue); - expect(NSNumber.castFrom(obj).longLongValue, expected); -} - -void _expectNSDouble(ObjCObjectBase obj, double expected) { - expect(NSNumber.isInstance(obj), isTrue); - expect(NSNumber.castFrom(obj).doubleValue, expected); -} - -void _expectNSBool(ObjCObjectBase obj, bool expected) { - expect(NSNumber.isInstance(obj), isTrue); - expect(NSNumber.castFrom(obj).boolValue, expected); -} - -ObjCObjectBase? _nsGet(NSDictionary dict, String key) => - dict.objectForKey(key.toNSString()); - -void _verifyNSArray(NSArray array) { - expect(array.count, _expectedListLength); - - // Verify primitives - expect(NSString.castFrom(array.objectAtIndex(0)).toDartString(), 'value'); - expect(NSNumber.castFrom(array.objectAtIndex(1)).longLongValue, 1); - expect(NSNumber.castFrom(array.objectAtIndex(2)).doubleValue, 1.1); - expect(NSNumber.castFrom(array.objectAtIndex(3)).boolValue, isTrue); - expect(NSString.castFrom(array.objectAtIndex(4)).toDartString(), - _customObject.toString()); - - // Verify nested list - final nestedList = NSArray.castFrom(array.objectAtIndex(5)); - expect(nestedList.count, 2); - expect(NSString.castFrom(nestedList.objectAtIndex(0)).toDartString(), - 'nestedList'); - expect(NSNumber.castFrom(nestedList.objectAtIndex(1)).longLongValue, 2); - - // Verify nested map - _verifyNSNestedDict(NSDictionary.castFrom(array.objectAtIndex(6))); -} - -void _verifyNSDictionary(NSDictionary dict) { - expect(dict.count, _expectedMapLength); - - // Verify primitives - expect(NSString.castFrom(_nsGet(dict, 'key')!).toDartString(), 'value'); - expect(NSNumber.castFrom(_nsGet(dict, 'key2')!).longLongValue, 1); - expect(NSNumber.castFrom(_nsGet(dict, 'key3')!).doubleValue, 1.1); - expect(NSNumber.castFrom(_nsGet(dict, 'key4')!).boolValue, isTrue); - expect(NSString.castFrom(_nsGet(dict, 'key5')!).toDartString(), - _customObject.toString()); - - // Verify nested list - _verifyNSArray(NSArray.castFrom(_nsGet(dict, 'list')!)); - - // Verify nested map - _verifyNSNestedDict(NSDictionary.castFrom(_nsGet(dict, 'nestedMap')!)); - - // Verify null was dropped - expect(_nsGet(dict, 'nullEntry'), isNull); -} - -void _verifyNSNestedDict(NSDictionary dict) { - expect( - NSString.castFrom(_nsGet(dict, 'innerString')!).toDartString(), 'nested'); - - final innerList = NSArray.castFrom(_nsGet(dict, 'innerList')!); - expect(innerList.count, _expectedNestedListLength); - expect(NSNumber.castFrom(innerList.objectAtIndex(0)).longLongValue, 1); - expect(NSNumber.castFrom(innerList.objectAtIndex(1)).longLongValue, 2); - - // Verify null was dropped - expect(_nsGet(dict, 'innerNull'), isNull); -} From 735ff6e9c99bdf2b216962ff09e526cdd382ec7f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 9 Dec 2025 23:09:25 +0100 Subject: [PATCH 09/13] Rename native FFI JNI utility test file to native JNI utility test for clarity --- packages/flutter/example/integration_test/all.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/example/integration_test/all.dart b/packages/flutter/example/integration_test/all.dart index 6a5b582b38..c91f875af0 100644 --- a/packages/flutter/example/integration_test/all.dart +++ b/packages/flutter/example/integration_test/all.dart @@ -3,7 +3,7 @@ import 'integration_test.dart' as a; import 'profiling_test.dart' as b; import 'replay_test.dart' as c; import 'platform_integrations_test.dart' as d; -import 'native_ffi_jni_utils_test.dart' as e; +import 'native_jni_utils_test.dart' as e; void main() { a.main(); From 9135ca4b65ab24650880da77b725d88180d0625f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 10 Dec 2025 15:05:50 +0100 Subject: [PATCH 10/13] Refactor JNI utility tests to improve clarity and consistency This commit updates the JNI utility tests by renaming assertion functions for better readability and consistency. The changes include replacing direct assertions with dedicated helper functions that check for equality, ensuring null checks are performed, and enhancing the overall structure of the test cases. This refactor aims to improve maintainability and clarity in the test suite. --- .../native_jni_utils_test.dart | 102 +++++++++--------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/packages/flutter/example/integration_test/native_jni_utils_test.dart b/packages/flutter/example/integration_test/native_jni_utils_test.dart index 1b4ebce7b3..df83a4e0d3 100644 --- a/packages/flutter/example/integration_test/native_jni_utils_test.dart +++ b/packages/flutter/example/integration_test/native_jni_utils_test.dart @@ -47,11 +47,12 @@ const _expectedMapLength = 7; void main() { group('JNI (Android)', () { test('dartToJObject converts primitives', () { - _expectJniString(dartToJObject('value'), 'value'); - _expectJniInt(dartToJObject(1), 1); - _expectJniDouble(dartToJObject(1.1), 1.1); - _expectJniBool(dartToJObject(true), true); - _expectJniString(dartToJObject(_customObject), _customObject.toString()); + _expectJniStringEquals(dartToJObject('value'), 'value'); + _expectJniLongEquals(dartToJObject(1), 1); + _expectJniDoubleEquals(dartToJObject(1.1), 1.1); + _expectJniBoolEquals(dartToJObject(true), true); + _expectJniStringEquals( + dartToJObject(_customObject), _customObject.toString()); }); test('dartToJObject converts list (drops nulls)', () { @@ -81,31 +82,37 @@ void main() { }, skip: !Platform.isAndroid); } -void _expectJniString(JObject obj, String expected) { - expect(obj, isA()); - expect((obj as JString).toDartString(releaseOriginal: true), expected); +void _expectJniStringEquals(JObject? obj, String expected) { + expect(obj, isNotNull); + final jString = obj!.as(JString.type); + expect(jString.toDartString(releaseOriginal: true), expected); } -void _expectJniInt(JObject obj, int expected) { - expect(obj, isA()); - expect((obj as JLong).longValue(releaseOriginal: true), expected); +void _expectJniLongEquals(JObject? obj, int expected) { + expect(obj, isNotNull); + final jLong = obj!.as(JLong.type); + expect(jLong.longValue(releaseOriginal: true), expected); } -void _expectJniDouble(JObject obj, double expected) { - expect(obj, isA()); - expect((obj as JDouble).doubleValue(releaseOriginal: true), expected); +void _expectJniDoubleEquals(JObject? obj, double expected) { + expect(obj, isNotNull); + final jDouble = obj!.as(JDouble.type); + expect(jDouble.doubleValue(releaseOriginal: true), expected); } -void _expectJniBool(JObject obj, bool expected) { - expect(obj, isA()); - expect((obj as JBoolean).booleanValue(releaseOriginal: true), expected); +void _expectJniBoolEquals(JObject? obj, bool expected) { + expect(obj, isNotNull); + final jBoolean = obj!.as(JBoolean.type); + expect(jBoolean.booleanValue(releaseOriginal: true), expected); } -JObject? _jniGet(JMap map, String key) { +JObject? _jniGetValue(JMap map, String key) { final jKey = key.toJString(); - final value = map[jKey]; - jKey.release(); - return value; + try { + return map[jKey]; + } finally { + jKey.release(); + } } bool _jniIsNull(JObject? obj) => obj == null || obj.toString() == 'null'; @@ -113,21 +120,18 @@ bool _jniIsNull(JObject? obj) => obj == null || obj.toString() == 'null'; void _verifyJniList(JList list) { expect(list.length, _expectedListLength); - // Verify primitives - expect(list[0].as(JString.type).toDartString(), 'value'); - expect(list[1].as(JLong.type).longValue(), 1); - expect(list[2].as(JDouble.type).doubleValue(), 1.1); - expect(list[3].as(JBoolean.type).booleanValue(), isTrue); - expect(list[4].as(JString.type).toDartString(), _customObject.toString()); + _expectJniStringEquals(list[0], 'value'); + _expectJniLongEquals(list[1], 1); + _expectJniDoubleEquals(list[2], 1.1); + _expectJniBoolEquals(list[3], true); + _expectJniStringEquals(list[4], _customObject.toString()); - // Verify nested list final nestedList = list[5].as(JList.type(JObject.type)); - expect(nestedList.length, 2); - expect(nestedList[0].as(JString.type).toDartString(), 'nestedList'); - expect(nestedList[1].as(JLong.type).longValue(), 2); + expect(nestedList.length, _expectedNestedListLength); + _expectJniStringEquals(nestedList[0], 'nestedList'); + _expectJniLongEquals(nestedList[1], 2); nestedList.release(); - // Verify nested map final nestedMap = list[6].as(JMap.type(JString.type, JObject.type)); _verifyJniNestedMap(nestedMap); nestedMap.release(); @@ -136,39 +140,33 @@ void _verifyJniList(JList list) { void _verifyJniMap(JMap map) { expect(map.length, _expectedMapLength); - // Verify primitives - expect(_jniGet(map, 'key')!.as(JString.type).toDartString(), 'value'); - expect(_jniGet(map, 'key2')!.as(JLong.type).longValue(), 1); - expect(_jniGet(map, 'key3')!.as(JDouble.type).doubleValue(), 1.1); - expect(_jniGet(map, 'key4')!.as(JBoolean.type).booleanValue(), isTrue); - expect(_jniGet(map, 'key5')!.as(JString.type).toDartString(), - _customObject.toString()); + _expectJniStringEquals(_jniGetValue(map, 'key'), 'value'); + _expectJniLongEquals(_jniGetValue(map, 'key2'), 1); + _expectJniDoubleEquals(_jniGetValue(map, 'key3'), 1.1); + _expectJniBoolEquals(_jniGetValue(map, 'key4'), true); + _expectJniStringEquals(_jniGetValue(map, 'key5'), _customObject.toString()); - // Verify nested list - final nestedList = _jniGet(map, 'list')!.as(JList.type(JObject.type)); + final nestedList = _jniGetValue(map, 'list')!.as(JList.type(JObject.type)); _verifyJniList(nestedList); nestedList.release(); - // Verify nested map final nestedMap = - _jniGet(map, 'nestedMap')!.as(JMap.type(JString.type, JObject.type)); + _jniGetValue(map, 'nestedMap')!.as(JMap.type(JString.type, JObject.type)); _verifyJniNestedMap(nestedMap); nestedMap.release(); - // Verify null was dropped - expect(_jniIsNull(_jniGet(map, 'nullEntry')), isTrue); + expect(_jniIsNull(_jniGetValue(map, 'nullEntry')), isTrue); } void _verifyJniNestedMap(JMap map) { - expect( - _jniGet(map, 'innerString')!.as(JString.type).toDartString(), 'nested'); + _expectJniStringEquals(_jniGetValue(map, 'innerString'), 'nested'); - final innerList = _jniGet(map, 'innerList')!.as(JList.type(JObject.type)); + final innerList = + _jniGetValue(map, 'innerList')!.as(JList.type(JObject.type)); expect(innerList.length, _expectedNestedListLength); - expect(innerList[0].as(JLong.type).longValue(), 1); - expect(innerList[1].as(JLong.type).longValue(), 2); + _expectJniLongEquals(innerList[0], 1); + _expectJniLongEquals(innerList[1], 2); innerList.release(); - // Verify null was dropped - expect(_jniIsNull(_jniGet(map, 'innerNull')), isTrue); + expect(_jniIsNull(_jniGetValue(map, 'innerNull')), isTrue); } From 927702d0c5f2af43f9869cbd558ce2c3daffe1a9 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 10 Dec 2025 22:01:20 +0100 Subject: [PATCH 11/13] Remove null checks from setContexts and setExtra methods in SentryNativeJava class for cleaner code. --- packages/flutter/lib/src/native/java/sentry_native_java.dart | 3 --- 1 file changed, 3 deletions(-) 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 4b34674218..62d88b4df1 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -233,7 +233,6 @@ class SentryNativeJava extends SentryNativeChannel { @override void setContexts(String key, value) => tryCatchSync('setContexts', () { - if (value == null) return; native.Sentry.configureScope( native.ScopeCallback.implement( native.$ScopeCallback( @@ -284,8 +283,6 @@ class SentryNativeJava extends SentryNativeChannel { @override void setExtra(String key, dynamic value) => tryCatchSync('setExtra', () { - if (value == null) return; - using((arena) { final jKey = key.toJString()..releasedBy(arena); final jVal = normalize(value).toString().toJString() From 3510c6f2f60f4b552f1e94ee784f5246e19aed08 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 10 Dec 2025 23:33:16 +0100 Subject: [PATCH 12/13] Refactor JNI utility tests to enhance structure and readability This commit updates the JNI utility tests by restructuring the test cases for better clarity and maintainability. Key changes include the introduction of local variables for input data, improved assertion handling with arena management, and the renaming of helper functions for consistency. These modifications aim to streamline the testing process and ensure accurate validation of Dart to JNI object conversions. --- .../native_jni_utils_test.dart | 316 +++++++++++------- 1 file changed, 186 insertions(+), 130 deletions(-) diff --git a/packages/flutter/example/integration_test/native_jni_utils_test.dart b/packages/flutter/example/integration_test/native_jni_utils_test.dart index df83a4e0d3..3f08aef5ed 100644 --- a/packages/flutter/example/integration_test/native_jni_utils_test.dart +++ b/packages/flutter/example/integration_test/native_jni_utils_test.dart @@ -1,172 +1,228 @@ // ignore_for_file: depend_on_referenced_packages - @TestOn('vm') import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:jni/jni.dart'; import 'package:sentry_flutter/src/native/java/sentry_native_java.dart'; import 'utils.dart'; -final _customObject = CustomObject(); - -final _nestedMap = { - 'innerString': 'nested', - 'innerList': [1, null, 2], - 'innerNull': null, -}; - -final _testList = [ - 'value', - 1, - 1.1, - true, - _customObject, - ['nestedList', 2], - _nestedMap, - null, -]; - -final _testMap = { - 'key': 'value', - 'key2': 1, - 'key3': 1.1, - 'key4': true, - 'key5': _customObject, - 'list': _testList, - 'nestedMap': _nestedMap, - 'nullEntry': null, -}; - -const _expectedListLength = 7; -const _expectedNestedListLength = 2; -const _expectedMapLength = 7; - void main() { + final customObject = CustomObject(); + + final inputNestedMap = { + 'innerString': 'nested', + 'innerList': [1, null, 2], + 'innerNull': null, + }; + + final inputList = [ + 'value', + 1, + 1.1, + true, + customObject, + ['nestedList', 2], + inputNestedMap, + null, + ]; + + final inputMap = { + 'key': 'value', + 'key2': 1, + 'key3': 1.1, + 'key4': true, + 'key5': customObject, + 'list': inputList, + 'nestedMap': inputNestedMap, + 'nullEntry': null, + }; + + final expectedNestedList = ['nestedList', 2]; + final expectedNestedMap = { + 'innerString': 'nested', + 'innerList': [1, 2], + 'innerNull': null, + }; + final expectedList = [ + 'value', + 1, + 1.1, + true, + customObject.toString(), + expectedNestedList, + expectedNestedMap, + ]; + final expectedMap = { + 'key': 'value', + 'key2': 1, + 'key3': 1.1, + 'key4': true, + 'key5': customObject.toString(), + 'list': expectedList, + 'nestedMap': expectedNestedMap, + }; + group('JNI (Android)', () { test('dartToJObject converts primitives', () { - _expectJniStringEquals(dartToJObject('value'), 'value'); - _expectJniLongEquals(dartToJObject(1), 1); - _expectJniDoubleEquals(dartToJObject(1.1), 1.1); - _expectJniBoolEquals(dartToJObject(true), true); - _expectJniStringEquals( - dartToJObject(_customObject), _customObject.toString()); + using((arena) { + _expectJniStringEquals( + dartToJObject('value')..releasedBy(arena), 'value'); + _expectJniLongEquals(dartToJObject(1)..releasedBy(arena), 1); + _expectJniDoubleEquals(dartToJObject(1.1)..releasedBy(arena), 1.1); + _expectJniBoolEquals(dartToJObject(true)..releasedBy(arena), true); + _expectJniStringEquals( + dartToJObject(customObject)..releasedBy(arena), + customObject.toString(), + ); + }); }); test('dartToJObject converts list (drops nulls)', () { - final jList = dartToJObject(_testList).as(JList.type(JObject.type)); - addTearDown(jList.release); - _verifyJniList(jList); + using((arena) { + final javaList = dartToJObject(inputList).as(JList.type(JObject.type)) + ..releasedBy(arena); + _expectJniList(javaList, expectedList, arena); + }); }); test('dartToJObject converts map (drops null values)', () { - final jMap = - dartToJObject(_testMap).as(JMap.type(JString.type, JObject.type)); - addTearDown(jMap.release); - _verifyJniMap(jMap); + using((arena) { + final javaMap = dartToJObject(inputMap) + .as(JMap.type(JString.type, JObject.type)) + ..releasedBy(arena); + _expectJniMap(javaMap, expectedMap, arena); + }); }); test('dartToJList', () { - final jList = dartToJList(_testList); - addTearDown(jList.release); - _verifyJniList(jList); + using((arena) { + final javaList = dartToJList(inputList)..releasedBy(arena); + _expectJniList(javaList, expectedList, arena); + }); }); test('dartToJMap', () { - final jMap = dartToJMap(_testMap); - addTearDown(jMap.release); - _verifyJniMap(jMap); + using((arena) { + final javaMap = dartToJMap(inputMap)..releasedBy(arena); + _expectJniMap(javaMap, expectedMap, arena); + }); }); }, skip: !Platform.isAndroid); } -void _expectJniStringEquals(JObject? obj, String expected) { - expect(obj, isNotNull); - final jString = obj!.as(JString.type); - expect(jString.toDartString(releaseOriginal: true), expected); +void _expectJniStringEquals(JObject? javaObject, String expected) { + expect(javaObject, isNotNull); + final javaString = javaObject!.as(JString.type); + expect(javaString.toDartString(releaseOriginal: true), expected); } -void _expectJniLongEquals(JObject? obj, int expected) { - expect(obj, isNotNull); - final jLong = obj!.as(JLong.type); - expect(jLong.longValue(releaseOriginal: true), expected); +void _expectJniLongEquals(JObject? javaObject, int expected) { + expect(javaObject, isNotNull); + final javaLong = javaObject!.as(JLong.type); + expect(javaLong.longValue(releaseOriginal: true), expected); } -void _expectJniDoubleEquals(JObject? obj, double expected) { - expect(obj, isNotNull); - final jDouble = obj!.as(JDouble.type); - expect(jDouble.doubleValue(releaseOriginal: true), expected); +void _expectJniDoubleEquals(JObject? javaObject, double expected) { + expect(javaObject, isNotNull); + final javaDouble = javaObject!.as(JDouble.type); + expect(javaDouble.doubleValue(releaseOriginal: true), expected); } -void _expectJniBoolEquals(JObject? obj, bool expected) { - expect(obj, isNotNull); - final jBoolean = obj!.as(JBoolean.type); - expect(jBoolean.booleanValue(releaseOriginal: true), expected); +void _expectJniBoolEquals(JObject? javaObject, bool expected) { + expect(javaObject, isNotNull); + final javaBoolean = javaObject!.as(JBoolean.type); + expect(javaBoolean.booleanValue(releaseOriginal: true), expected); } -JObject? _jniGetValue(JMap map, String key) { - final jKey = key.toJString(); - try { - return map[jKey]; - } finally { - jKey.release(); - } +JObject? _get(JMap javaMap, String key, Arena arena) => + javaMap[key.toJString()..releasedBy(arena)]; + +void _expectJniList( + JList javaList, + List expectedListValues, + Arena arena, +) { + expect(javaList.length, expectedListValues.length); + + _expectJniStringEquals(javaList[0], expectedListValues[0] as String); + _expectJniLongEquals(javaList[1], expectedListValues[1] as int); + _expectJniDoubleEquals(javaList[2], expectedListValues[2] as double); + _expectJniBoolEquals(javaList[3], expectedListValues[3] as bool); + _expectJniStringEquals(javaList[4], expectedListValues[4] as String); + + final nestedList = javaList[5].as(JList.type(JObject.type)) + ..releasedBy(arena); + final expectedNestedList = expectedListValues[5] as List; + expect(nestedList.length, expectedNestedList.length); + _expectJniStringEquals(nestedList[0], expectedNestedList[0] as String); + _expectJniLongEquals(nestedList[1], expectedNestedList[1] as int); + + final nestedMap = javaList[6].as(JMap.type(JString.type, JObject.type)) + ..releasedBy(arena); + _expectJniNestedMap( + nestedMap, + expectedListValues[6] as Map, + expectedNestedList.length, + arena, + ); } -bool _jniIsNull(JObject? obj) => obj == null || obj.toString() == 'null'; - -void _verifyJniList(JList list) { - expect(list.length, _expectedListLength); - - _expectJniStringEquals(list[0], 'value'); - _expectJniLongEquals(list[1], 1); - _expectJniDoubleEquals(list[2], 1.1); - _expectJniBoolEquals(list[3], true); - _expectJniStringEquals(list[4], _customObject.toString()); - - final nestedList = list[5].as(JList.type(JObject.type)); - expect(nestedList.length, _expectedNestedListLength); - _expectJniStringEquals(nestedList[0], 'nestedList'); - _expectJniLongEquals(nestedList[1], 2); - nestedList.release(); - - final nestedMap = list[6].as(JMap.type(JString.type, JObject.type)); - _verifyJniNestedMap(nestedMap); - nestedMap.release(); +void _expectJniMap( + JMap javaMap, + Map expectedMapValues, + Arena arena, +) { + expect(javaMap.length, expectedMapValues.length); + + final expectedList = expectedMapValues['list']! as List; + final expectedNestedList = expectedList[5] as List; + final expectedNestedMap = + expectedMapValues['nestedMap']! as Map; + + _expectJniStringEquals( + _get(javaMap, 'key', arena), expectedMapValues['key'] as String); + _expectJniLongEquals( + _get(javaMap, 'key2', arena), expectedMapValues['key2'] as int); + _expectJniDoubleEquals( + _get(javaMap, 'key3', arena), expectedMapValues['key3'] as double); + _expectJniBoolEquals( + _get(javaMap, 'key4', arena), expectedMapValues['key4'] as bool); + _expectJniStringEquals( + _get(javaMap, 'key5', arena), expectedMapValues['key5'] as String); + + final nestedList = _get(javaMap, 'list', arena)!.as(JList.type(JObject.type)) + ..releasedBy(arena); + _expectJniList(nestedList, expectedList, arena); + + final nestedMap = _get(javaMap, 'nestedMap', arena)! + .as(JMap.type(JString.type, JObject.type)) + ..releasedBy(arena); + _expectJniNestedMap( + nestedMap, expectedNestedMap, expectedNestedList.length, arena); + + expect(_get(javaMap, 'nullEntry', arena), isNull); } -void _verifyJniMap(JMap map) { - expect(map.length, _expectedMapLength); - - _expectJniStringEquals(_jniGetValue(map, 'key'), 'value'); - _expectJniLongEquals(_jniGetValue(map, 'key2'), 1); - _expectJniDoubleEquals(_jniGetValue(map, 'key3'), 1.1); - _expectJniBoolEquals(_jniGetValue(map, 'key4'), true); - _expectJniStringEquals(_jniGetValue(map, 'key5'), _customObject.toString()); - - final nestedList = _jniGetValue(map, 'list')!.as(JList.type(JObject.type)); - _verifyJniList(nestedList); - nestedList.release(); - - final nestedMap = - _jniGetValue(map, 'nestedMap')!.as(JMap.type(JString.type, JObject.type)); - _verifyJniNestedMap(nestedMap); - nestedMap.release(); - - expect(_jniIsNull(_jniGetValue(map, 'nullEntry')), isTrue); -} - -void _verifyJniNestedMap(JMap map) { - _expectJniStringEquals(_jniGetValue(map, 'innerString'), 'nested'); - - final innerList = - _jniGetValue(map, 'innerList')!.as(JList.type(JObject.type)); - expect(innerList.length, _expectedNestedListLength); - _expectJniLongEquals(innerList[0], 1); - _expectJniLongEquals(innerList[1], 2); - innerList.release(); - - expect(_jniIsNull(_jniGetValue(map, 'innerNull')), isTrue); +void _expectJniNestedMap( + JMap javaNestedMap, + Map expectedNestedMapValues, + int expectedNestedListLength, + Arena arena, +) { + _expectJniStringEquals(_get(javaNestedMap, 'innerString', arena), + expectedNestedMapValues['innerString'] as String); + + final innerList = _get(javaNestedMap, 'innerList', arena)! + .as(JList.type(JObject.type)) + ..releasedBy(arena); + expect(innerList.length, expectedNestedListLength); + _expectJniLongEquals(innerList[0], + (expectedNestedMapValues['innerList']! as List)[0] as int); + _expectJniLongEquals(innerList[1], + (expectedNestedMapValues['innerList']! as List)[1] as int); + + expect(_get(javaNestedMap, 'innerNull', arena), isNull); } From 681cc43c8c59b80afd768dbb3514f8c2135972a3 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 10 Dec 2025 23:34:17 +0100 Subject: [PATCH 13/13] Enhance JNI utility tests by adding missing line breaks for improved readability This commit introduces line breaks in the JNI utility test file to enhance the overall readability of the code. The changes aim to improve the visual structure of the test cases, making it easier to follow the logic and organization of the tests. --- .../flutter/example/integration_test/native_jni_utils_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/flutter/example/integration_test/native_jni_utils_test.dart b/packages/flutter/example/integration_test/native_jni_utils_test.dart index 3f08aef5ed..21c5bdbccf 100644 --- a/packages/flutter/example/integration_test/native_jni_utils_test.dart +++ b/packages/flutter/example/integration_test/native_jni_utils_test.dart @@ -41,6 +41,7 @@ void main() { }; final expectedNestedList = ['nestedList', 2]; + final expectedNestedMap = { 'innerString': 'nested', 'innerList': [1, 2], @@ -55,6 +56,7 @@ void main() { expectedNestedList, expectedNestedMap, ]; + final expectedMap = { 'key': 'value', 'key2': 1,