Skip to content

Commit e1ab497

Browse files
authored
enh: refactor setExtra and removeExtra to FFI/JNI (#3314)
* Refactor setContext and removeContext to FFI/JNI * Update scope * Integration test * Fix swift lint * CHANGELOG * Improve objc conversion * Fix tests * Remove native channel from coverage * Refactor setTag and removeTag to FFI/JNI * Refactor setExtra and removeExtra to FFI/JNI * Update CHANGELOG * Update CHANGELOG * Fix analyze * Null handling on Android side * Unused import * Unused import * Fix test * Fix test * Update * Update
1 parent bfabaf2 commit e1ab497

File tree

10 files changed

+143
-90
lines changed

10 files changed

+143
-90
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
### Enhancements
1111

12+
- Refactor `setExtra` and `removeExtra` to use FFI/JNI ([#3314](https://github.com/getsentry/sentry-dart/pull/3314))
1213
- Refactor `setTag` and `removeTag` to use FFI/JNI ([#3313](https://github.com/getsentry/sentry-dart/pull/3313))
1314
- Refactor `setContexts` and `removeContexts` to use FFI/JNI ([#3312](https://github.com/getsentry/sentry-dart/pull/3312))
1415
- Refactor `setUser` to use FFI/JNI ([#3295](https://github.com/getsentry/sentry-dart/pull/3295/))

packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ class SentryFlutterPlugin :
6161
when (call.method) {
6262
"initNativeSdk" -> initNativeSdk(call, result)
6363
"closeNativeSdk" -> closeNativeSdk(result)
64-
"setExtra" -> setExtra(call.argument("key"), call.argument("value"), result)
65-
"removeExtra" -> removeExtra(call.argument("key"), result)
6664
"setReplayConfig" -> setReplayConfig(call, result)
6765
"captureReplay" -> captureReplay(result)
6866
else -> result.notImplemented()
@@ -137,33 +135,6 @@ class SentryFlutterPlugin :
137135
}
138136
}
139137

140-
private fun setExtra(
141-
key: String?,
142-
value: String?,
143-
result: Result,
144-
) {
145-
if (key == null || value == null) {
146-
result.success("")
147-
return
148-
}
149-
Sentry.setExtra(key, value)
150-
151-
result.success("")
152-
}
153-
154-
private fun removeExtra(
155-
key: String?,
156-
result: Result,
157-
) {
158-
if (key == null) {
159-
result.success("")
160-
return
161-
}
162-
Sentry.removeExtra(key)
163-
164-
result.success("")
165-
}
166-
167138
private fun closeNativeSdk(result: Result) {
168139
ScopesAdapter.getInstance().close()
169140

packages/flutter/example/integration_test/integration_test.dart

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
// ignore_for_file: avoid_print
2-
// ignore_for_file: invalid_use_of_internal_member
3-
// ignore_for_file: unused_local_variable
1+
// ignore_for_file: avoid_print, invalid_use_of_internal_member, unused_local_variable, deprecated_member_use
42

53
import 'dart:async';
64
import 'dart:convert';
@@ -130,9 +128,7 @@ void main() {
130128
await scope.addBreadcrumb(breadcrumb);
131129
await scope.clearBreadcrumbs();
132130

133-
// ignore: deprecated_member_use
134131
await scope.setExtra('extra-key', 'extra-value');
135-
// ignore: deprecated_member_use
136132
await scope.removeExtra('extra-key');
137133

138134
await scope.setTag('tag-key', 'tag-value');
@@ -823,6 +819,64 @@ void main() {
823819
}
824820
});
825821

822+
testWidgets('setExtra and removeExtra sync to native', (tester) async {
823+
await restoreFlutterOnErrorAfter(() async {
824+
await setupSentryAndApp(tester);
825+
});
826+
827+
await Sentry.configureScope((scope) async {
828+
scope.setExtra('key1', 'randomValue');
829+
scope.setExtra('key2',
830+
{'String': 'Value', 'Bool': true, 'Int': 123, 'Double': 12.3});
831+
scope.setExtra('key3', true);
832+
scope.setExtra('key4', 12);
833+
scope.setExtra('key5', 12.3);
834+
});
835+
836+
var contexts = await SentryFlutter.native?.loadContexts();
837+
838+
final extras = (Platform.isIOS || Platform.isMacOS)
839+
? contexts!['extra']
840+
: contexts!['extras'];
841+
expect(extras, isNotNull, reason: 'Extras are null');
842+
843+
if (Platform.isIOS || Platform.isMacOS) {
844+
expect(extras['key1'], 'randomValue', reason: 'key1 mismatch');
845+
expect(extras['key2'],
846+
{'String': 'Value', 'Bool': 1, 'Int': 123, 'Double': 12.3},
847+
reason: 'key2 mismatch');
848+
// bool values are mapped to num values of 1 or 0 during objc conversion
849+
expect(extras['key3'], 1, reason: 'key3 mismatch');
850+
expect(extras['key4'], 12, reason: 'key4 mismatch');
851+
expect(extras['key5'], 12.3, reason: 'key5 mismatch');
852+
} else if (Platform.isAndroid) {
853+
// Sentry Java's setExtra only allows String values so this is after normalization
854+
expect(extras['key1'], 'randomValue', reason: 'key1 mismatch');
855+
expect(
856+
extras['key2'], '{String: Value, Bool: true, Int: 123, Double: 12.3}',
857+
reason: 'key2 mismatch');
858+
expect(extras['key3'], 'true', reason: 'key3 mismatch');
859+
expect(extras['key4'], '12', reason: 'key4 mismatch');
860+
expect(extras['key5'], '12.3', reason: 'key5 mismatch');
861+
}
862+
863+
await Sentry.configureScope((scope) async {
864+
scope.removeExtra('key1');
865+
scope.removeExtra('key2');
866+
scope.removeExtra('key3');
867+
scope.removeExtra('key4');
868+
scope.removeExtra('key5');
869+
});
870+
871+
contexts = await SentryFlutter.native?.loadContexts();
872+
final extraKey = (Platform.isIOS || Platform.isMacOS) ? 'extra' : 'extras';
873+
if (Platform.isIOS || Platform.isMacOS) {
874+
expect(contexts![extraKey], isNull, reason: 'Extra are not null');
875+
} else {
876+
expect(contexts![extraKey], {}, reason: 'Extra are not empty');
877+
}
878+
});
879+
826880
group('e2e', () {
827881
var output = find.byKey(const Key('output'));
828882
late Fixture fixture;

packages/flutter/ffi-cocoa.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ objc-interfaces:
4242
- 'removeContextForKey:'
4343
- 'setTagValue:forKey:'
4444
- 'removeTagForKey:'
45+
- 'setExtraValue:forKey:'
46+
- 'removeExtraForKey:'
4547
preamble: |
4648
// ignore_for_file: type=lint, unused_element
4749

packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
7575
case "closeNativeSdk":
7676
closeNativeSdk(call, result: result)
7777

78-
case "setExtra":
79-
let arguments = call.arguments as? [String: Any?]
80-
let key = arguments?["key"] as? String
81-
let value = arguments?["value"] as? Any
82-
setExtra(key: key, value: value, result: result)
83-
84-
case "removeExtra":
85-
let arguments = call.arguments as? [String: Any?]
86-
let key = arguments?["key"] as? String
87-
removeExtra(key: key, result: result)
88-
8978
#if !os(tvOS) && !os(watchOS)
9079
case "discardProfiler":
9180
discardProfiler(call, result)
@@ -243,30 +232,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
243232
return !name.isEmpty
244233
}
245234

246-
private func setExtra(key: String?, value: Any?, result: @escaping FlutterResult) {
247-
guard let key = key else {
248-
result("")
249-
return
250-
}
251-
SentrySDK.configureScope { scope in
252-
scope.setExtra(value: value, key: key)
253-
254-
result("")
255-
}
256-
}
257-
258-
private func removeExtra(key: String?, result: @escaping FlutterResult) {
259-
guard let key = key else {
260-
result("")
261-
return
262-
}
263-
SentrySDK.configureScope { scope in
264-
scope.removeExtra(key: key)
265-
266-
result("")
267-
}
268-
}
269-
270235
private func collectProfile(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
271236
guard let arguments = call.arguments as? [String: Any],
272237
let traceId = arguments["traceId"] as? String else {

packages/flutter/lib/src/native/cocoa/binding.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,9 @@ interface class SentrySerializable extends objc.ObjCProtocolBase
11501150

11511151
late final _sel_setTagValue_forKey_ = objc.registerName("setTagValue:forKey:");
11521152
late final _sel_removeTagForKey_ = objc.registerName("removeTagForKey:");
1153+
late final _sel_setExtraValue_forKey_ =
1154+
objc.registerName("setExtraValue:forKey:");
1155+
late final _sel_removeExtraForKey_ = objc.registerName("removeExtraForKey:");
11531156
late final _sel_clearBreadcrumbs = objc.registerName("clearBreadcrumbs");
11541157
final _objc_msgSend_1pl9qdv = objc.msgSendPointer
11551158
.cast<
@@ -1198,6 +1201,19 @@ class SentryScope extends objc.NSObject implements SentrySerializable {
11981201
this.ref.pointer, _sel_removeTagForKey_, key.ref.pointer);
11991202
}
12001203

1204+
/// Set global extra -> these will be sent with every event
1205+
void setExtraValue(objc.ObjCObjectBase? value,
1206+
{required objc.NSString forKey}) {
1207+
_objc_msgSend_pfv6jd(this.ref.pointer, _sel_setExtraValue_forKey_,
1208+
value?.ref.pointer ?? ffi.nullptr, forKey.ref.pointer);
1209+
}
1210+
1211+
/// Remove the extra for the specified key.
1212+
void removeExtraForKey(objc.NSString key) {
1213+
_objc_msgSend_xtuoz7(
1214+
this.ref.pointer, _sel_removeExtraForKey_, key.ref.pointer);
1215+
}
1216+
12011217
/// Clears all breadcrumbs in the scope
12021218
void clearBreadcrumbs() {
12031219
_objc_msgSend_1pl9qdv(this.ref.pointer, _sel_clearBreadcrumbs);

packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,27 @@ class SentryNativeCocoa extends SentryNativeChannel {
284284
scope.removeTagForKey(key.toNSString());
285285
}));
286286
});
287+
288+
@override
289+
void setExtra(String key, dynamic value) => tryCatchSync('setExtra', () {
290+
if (value == null) return;
291+
292+
cocoa.SentrySDK.configureScope(
293+
cocoa.ObjCBlock_ffiVoid_SentryScope.fromFunction(
294+
(cocoa.SentryScope scope) {
295+
scope.setExtraValue(_dartToNSObject(value as Object),
296+
forKey: key.toNSString());
297+
}));
298+
});
299+
300+
@override
301+
void removeExtra(String key) => tryCatchSync('removeExtra', () {
302+
cocoa.SentrySDK.configureScope(
303+
cocoa.ObjCBlock_ffiVoid_SentryScope.fromFunction(
304+
(cocoa.SentryScope scope) {
305+
scope.removeExtraForKey(key.toNSString());
306+
}));
307+
});
287308
}
288309

289310
// The default conversion does not handle bool so we will add it ourselves

packages/flutter/lib/src/native/java/sentry_native_java.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../../../sentry_flutter.dart';
88
import '../../replay/scheduled_recorder_config.dart';
99
import '../native_app_start.dart';
1010
import '../sentry_native_channel.dart';
11+
import '../utils/data_normalizer.dart';
1112
import '../utils/utf8_json.dart';
1213
import 'android_envelope_sender.dart';
1314
import 'android_replay_recorder.dart';
@@ -336,6 +337,27 @@ class SentryNativeJava extends SentryNativeChannel {
336337
native.Sentry.removeTag(jKey);
337338
});
338339
});
340+
341+
@override
342+
void setExtra(String key, dynamic value) => tryCatchSync('setExtra', () {
343+
if (value == null) return;
344+
345+
using((arena) {
346+
final jKey = key.toJString()..releasedBy(arena);
347+
final jVal = normalize(value).toString().toJString()
348+
..releasedBy(arena);
349+
350+
native.Sentry.setExtra(jKey, jVal);
351+
});
352+
});
353+
354+
@override
355+
void removeExtra(String key) => tryCatchSync('removeExtra', () {
356+
using((arena) {
357+
final jKey = key.toJString()..releasedBy(arena);
358+
native.Sentry.removeExtra(jKey);
359+
});
360+
});
339361
}
340362

341363
JObject? _dartToJObject(Object? value, Arena arena) => switch (value) {

packages/flutter/lib/src/native/sentry_native_channel.dart

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import 'native_app_start.dart';
1212
import 'sentry_native_binding.dart';
1313
import 'sentry_native_invoker.dart';
1414
import 'sentry_safe_method_channel.dart';
15-
import 'utils/data_normalizer.dart';
1615

1716
/// Provide typed methods to access native layer via MethodChannel.
1817
@internal
@@ -148,14 +147,14 @@ class SentryNativeChannel
148147
}
149148

150149
@override
151-
Future<void> setExtra(String key, dynamic value) => channel.invokeMethod(
152-
'setExtra',
153-
{'key': key, 'value': normalize(value)},
154-
);
150+
FutureOr<void> setExtra(String key, dynamic value) {
151+
assert(false, 'setExtra should not be used through method channels.');
152+
}
155153

156154
@override
157-
Future<void> removeExtra(String key) =>
158-
channel.invokeMethod('removeExtra', {'key': key});
155+
FutureOr<void> removeExtra(String key) {
156+
assert(false, 'removeExtra should not be used through method channels.');
157+
}
159158

160159
@override
161160
FutureOr<void> setTag(String key, String value) {

packages/flutter/test/sentry_native_channel_test.dart

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import 'package:sentry/src/platform/mock_platform.dart';
1111
import 'package:sentry_flutter/sentry_flutter.dart';
1212
import 'package:sentry_flutter/src/native/factory.dart';
1313
import 'package:sentry_flutter/src/native/sentry_native_binding.dart';
14-
import 'package:sentry_flutter/src/native/utils/data_normalizer.dart';
1514
import 'package:sentry_flutter/src/replay/replay_config.dart';
1615

1716
import 'mocks.dart';
@@ -119,26 +118,29 @@ void main() {
119118
verifyZeroInteractions(channel);
120119
});
121120

122-
test('setExtra', () async {
121+
test('setExtra', () {
123122
final value = {'object': Object()};
124-
final normalizedValue = normalize(value);
125-
when(channel.invokeMethod(
126-
'setExtra', {'key': 'fixture-key', 'value': normalizedValue}))
127-
.thenAnswer((_) => Future.value());
123+
final matcher = _nativeUnavailableMatcher(
124+
mockPlatform,
125+
includeLookupSymbol: true,
126+
includeFailedToLoadClassException: true,
127+
);
128128

129-
await sut.setExtra('fixture-key', value);
129+
expect(() => sut.setExtra('fixture-key', value), matcher);
130130

131-
verify(channel.invokeMethod(
132-
'setExtra', {'key': 'fixture-key', 'value': normalizedValue}));
131+
verifyZeroInteractions(channel);
133132
});
134133

135-
test('removeExtra', () async {
136-
when(channel.invokeMethod('removeExtra', {'key': 'fixture-key'}))
137-
.thenAnswer((_) => Future.value());
134+
test('removeExtra', () {
135+
final matcher = _nativeUnavailableMatcher(
136+
mockPlatform,
137+
includeLookupSymbol: true,
138+
includeFailedToLoadClassException: true,
139+
);
138140

139-
await sut.removeExtra('fixture-key');
141+
expect(() => sut.removeExtra('fixture-key'), matcher);
140142

141-
verify(channel.invokeMethod('removeExtra', {'key': 'fixture-key'}));
143+
verifyZeroInteractions(channel);
142144
});
143145

144146
test('setTag', () async {

0 commit comments

Comments
 (0)