Skip to content

Commit 19bd02d

Browse files
committed
Refactor setExtra and removeExtra to FFI/JNI
1 parent 8dcc1d0 commit 19bd02d

File tree

9 files changed

+153
-86
lines changed

9 files changed

+153
-86
lines changed

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: 52 additions & 3 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';
@@ -823,6 +821,57 @@ void main() {
823821
}
824822
});
825823

824+
testWidgets('setExtra and removeExtra sync to native', (tester) async {
825+
await restoreFlutterOnErrorAfter(() async {
826+
await setupSentryAndApp(tester);
827+
});
828+
829+
await Sentry.configureScope((scope) async {
830+
scope.setExtra('key1', 'randomValue');
831+
scope.setExtra('key2',
832+
{'String': 'Value', 'Bool': true, 'Int': 123, 'Double': 12.3});
833+
scope.setExtra('key3', true);
834+
scope.setExtra('key4', 12);
835+
scope.setExtra('key5', 12.3);
836+
});
837+
838+
var contexts = await SentryFlutter.native?.loadContexts();
839+
840+
final extras = Platform.isIOS ? contexts!['extra'] : contexts!['extras'];
841+
expect(extras, isNotNull, reason: 'Extras are null');
842+
843+
if (Platform.isIOS) {
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+
expect(contexts!['extra'], isNull, reason: 'Extra are not null');
873+
});
874+
826875
group('e2e', () {
827876
var output = find.byKey(const Key('output'));
828877
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,26 @@ 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+
cocoa.SentrySDK.configureScope(
292+
cocoa.ObjCBlock_ffiVoid_SentryScope.fromFunction(
293+
(cocoa.SentryScope scope) {
294+
scope.setExtraValue(_dartToNSObject(value as Object),
295+
forKey: key.toNSString());
296+
}));
297+
});
298+
299+
@override
300+
void removeExtra(String key) => tryCatchSync('removeExtra', () {
301+
cocoa.SentrySDK.configureScope(
302+
cocoa.ObjCBlock_ffiVoid_SentryScope.fromFunction(
303+
(cocoa.SentryScope scope) {
304+
scope.removeExtraForKey(key.toNSString());
305+
}));
306+
});
287307
}
288308

289309
// 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: 41 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,46 @@ class SentryNativeJava extends SentryNativeChannel {
336337
native.Sentry.removeTag(jKey);
337338
});
338339
});
340+
341+
@override
342+
void setExtra(String key, dynamic value) => tryCatchSync('setExtra', () {
343+
native.Sentry.configureScope(
344+
native.ScopeCallback.implement(
345+
native.$ScopeCallback(
346+
run: (iScope) {
347+
using((arena) {
348+
final jKey = key.toJString()..releasedBy(arena);
349+
final jVal = normalize(value).toString().toJString()
350+
..releasedBy(arena);
351+
352+
final scope = iScope.as(const native.$Scope$Type())
353+
..releasedBy(arena);
354+
scope.setExtra(jKey, jVal);
355+
});
356+
},
357+
),
358+
),
359+
);
360+
});
361+
362+
@override
363+
void removeExtra(String key) => tryCatchSync('removeExtra', () {
364+
native.Sentry.configureScope(
365+
native.ScopeCallback.implement(
366+
native.$ScopeCallback(
367+
run: (iScope) {
368+
using((arena) {
369+
final jKey = key.toJString()..releasedBy(arena);
370+
371+
final scope = iScope.as(const native.$Scope$Type())
372+
..releasedBy(arena);
373+
scope.removeExtra(jKey);
374+
});
375+
},
376+
),
377+
),
378+
);
379+
});
339380
}
340381

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

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,14 @@ class SentryNativeChannel
148148
}
149149

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

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

160160
@override
161161
FutureOr<void> setTag(String key, String value) {

packages/flutter/test/sentry_native_channel_test.dart

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -119,26 +119,29 @@ void main() {
119119
verifyZeroInteractions(channel);
120120
});
121121

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

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

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

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

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

141-
verify(channel.invokeMethod('removeExtra', {'key': 'fixture-key'}));
144+
verifyZeroInteractions(channel);
142145
});
143146

144147
test('setTag', () async {

0 commit comments

Comments
 (0)