Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ class SentryFlutterPlugin :
when (call.method) {
"initNativeSdk" -> initNativeSdk(call, result)
"closeNativeSdk" -> closeNativeSdk(result)
"setExtra" -> setExtra(call.argument("key"), call.argument("value"), result)
"removeExtra" -> removeExtra(call.argument("key"), result)
"setTag" -> setTag(call.argument("key"), call.argument("value"), result)
"removeTag" -> removeTag(call.argument("key"), result)
"setReplayConfig" -> setReplayConfig(call, result)
Expand Down Expand Up @@ -139,33 +137,6 @@ class SentryFlutterPlugin :
}
}

private fun setExtra(
key: String?,
value: String?,
result: Result,
) {
if (key == null || value == null) {
result.success("")
return
}
Sentry.setExtra(key, value)

result.success("")
}

private fun removeExtra(
key: String?,
result: Result,
) {
if (key == null) {
result.success("")
return
}
Sentry.removeExtra(key)

result.success("")
}

private fun setTag(
key: String?,
value: String?,
Expand Down
52 changes: 47 additions & 5 deletions packages/flutter/example/integration_test/integration_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// ignore_for_file: avoid_print
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: unused_local_variable
// ignore_for_file: avoid_print, invalid_use_of_internal_member, unused_local_variable, deprecated_member_use

import 'dart:async';
import 'dart:convert';
Expand Down Expand Up @@ -130,9 +128,7 @@ void main() {
await scope.addBreadcrumb(breadcrumb);
await scope.clearBreadcrumbs();

// ignore: deprecated_member_use
await scope.setExtra('extra-key', 'extra-value');
// ignore: deprecated_member_use
await scope.removeExtra('extra-key');

await scope.setTag('tag-key', 'tag-value');
Expand Down Expand Up @@ -615,6 +611,52 @@ void main() {
expect(removedValues['key5'], isNull, reason: 'key5 should be removed');
});

testWidgets('setExtra and removeExtra sync to native', (tester) async {
await restoreFlutterOnErrorAfter(() async {
await setupSentryAndApp(tester);
});

await Sentry.configureScope((scope) async {
scope.setExtra('key1', 'randomValue');
scope.setExtra('key2', {'Key': 'Value'});
scope.setExtra('key3', true);
scope.setExtra('key4', 12);
scope.setExtra('key5', 12.3);
});

var contexts = await SentryFlutter.native?.loadContexts();

final extras = Platform.isIOS ? contexts!['extra'] : contexts!['extras'];
expect(extras, isNotNull, reason: 'Extras are null');

if (Platform.isIOS) {
expect(extras['key1'], 'randomValue', reason: 'key1 mismatch');
expect(extras['key2'], {'Key': 'Value'}, reason: 'key2 mismatch');
// bool values are mapped to num values of 1 or 0 during objc conversion
expect(extras['key3'], 1, reason: 'key3 mismatch');
expect(extras['key4'], 12, reason: 'key4 mismatch');
expect(extras['key5'], 12.3, reason: 'key5 mismatch');
} else if (Platform.isAndroid) {
// Sentry Java's setExtra only allows String values so this is after normalization
expect(extras['key1'], 'randomValue', reason: 'key1 mismatch');
expect(extras['key2'], '{Key: Value}', reason: 'key2 mismatch');
expect(extras['key3'], 'true', reason: 'key3 mismatch');
expect(extras['key4'], '12', reason: 'key4 mismatch');
expect(extras['key5'], '12.3', reason: 'key5 mismatch');
}

await Sentry.configureScope((scope) async {
scope.removeExtra('key1');
scope.removeExtra('key2');
scope.removeExtra('key3');
scope.removeExtra('key4');
scope.removeExtra('key5');
});

contexts = await SentryFlutter.native?.loadContexts();
expect(contexts!['extra'], isNull, reason: 'Extra are not null');
});

testWidgets('addBreadcrumb and clearBreadcrumbs sync to native',
(tester) async {
await restoreFlutterOnErrorAfter(() async {
Expand Down
2 changes: 2 additions & 0 deletions packages/flutter/ffi-cocoa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ objc-interfaces:
- 'clearBreadcrumbs'
- 'setContextValue:forKey:'
- 'removeContextForKey:'
- 'setExtraValue:forKey:'
- 'removeExtraForKey:'
preamble: |
// ignore_for_file: type=lint, unused_element

Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
case "closeNativeSdk":
closeNativeSdk(call, result: result)

case "setExtra":
let arguments = call.arguments as? [String: Any?]
let key = arguments?["key"] as? String
let value = arguments?["value"] as? Any
setExtra(key: key, value: value, result: result)

case "removeExtra":
let arguments = call.arguments as? [String: Any?]
let key = arguments?["key"] as? String
removeExtra(key: key, result: result)

case "setTag":
let arguments = call.arguments as? [String: Any?]
let key = arguments?["key"] as? String
Expand Down Expand Up @@ -254,30 +243,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
return !name.isEmpty
}

private func setExtra(key: String?, value: Any?, result: @escaping FlutterResult) {
guard let key = key else {
result("")
return
}
SentrySDK.configureScope { scope in
scope.setExtra(value: value, key: key)

result("")
}
}

private func removeExtra(key: String?, result: @escaping FlutterResult) {
guard let key = key else {
result("")
return
}
SentrySDK.configureScope { scope in
scope.removeExtra(key: key)

result("")
}
}

private func setTag(key: String?, value: String?, result: @escaping FlutterResult) {
guard let key = key, let value = value else {
result("")
Expand Down
16 changes: 16 additions & 0 deletions packages/flutter/lib/src/native/cocoa/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,9 @@ interface class SentrySerializable extends objc.ObjCProtocolBase
: this._(other, retain: retain, release: release);
}

late final _sel_setExtraValue_forKey_ =
objc.registerName("setExtraValue:forKey:");
late final _sel_removeExtraForKey_ = objc.registerName("removeExtraForKey:");
late final _sel_clearBreadcrumbs = objc.registerName("clearBreadcrumbs");
final _objc_msgSend_1pl9qdv = objc.msgSendPointer
.cast<
Expand Down Expand Up @@ -1183,6 +1186,19 @@ class SentryScope extends objc.NSObject implements SentrySerializable {
obj.ref.pointer, _sel_isKindOfClass_, _class_SentryScope);
}

/// Set global extra -> these will be sent with every event
void setExtraValue(objc.ObjCObjectBase? value,
{required objc.NSString forKey}) {
_objc_msgSend_pfv6jd(this.ref.pointer, _sel_setExtraValue_forKey_,
value?.ref.pointer ?? ffi.nullptr, forKey.ref.pointer);
}

/// Remove the extra for the specified key.
void removeExtraForKey(objc.NSString key) {
_objc_msgSend_xtuoz7(
this.ref.pointer, _sel_removeExtraForKey_, key.ref.pointer);
}

/// Clears all breadcrumbs in the scope
void clearBreadcrumbs() {
_objc_msgSend_1pl9qdv(this.ref.pointer, _sel_clearBreadcrumbs);
Expand Down
27 changes: 27 additions & 0 deletions packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,33 @@ class SentryNativeCocoa extends SentryNativeChannel {
scope.removeContextForKey(key.toNSString());
}));
});

@override
void setExtra(String key, dynamic value) => tryCatchSync('setExtra', () {
ObjCObjectBase? cValue = switch (value) {
Map<String, dynamic> m => _deepConvertMapNonNull(m).toNSDictionary(),
bool b => b ? 1.toNSNumber() : 0.toNSNumber(),
Object o => toObjCObject(o),
_ => null
};

cocoa.SentrySDK.configureScope(
cocoa.ObjCBlock_ffiVoid_SentryScope.fromFunction(
(cocoa.SentryScope scope) {
if (cValue != null) {
scope.setExtraValue(cValue, forKey: key.toNSString());
}
}));
});

@override
void removeExtra(String key) => tryCatchSync('removeExtra', () {
cocoa.SentrySDK.configureScope(
cocoa.ObjCBlock_ffiVoid_SentryScope.fromFunction(
(cocoa.SentryScope scope) {
scope.removeExtraForKey(key.toNSString());
}));
});
}

/// This map conversion is needed so we can use the toNSDictionary extension function
Expand Down
38 changes: 38 additions & 0 deletions packages/flutter/lib/src/native/java/sentry_native_java.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../../../sentry_flutter.dart';
import '../../replay/scheduled_recorder_config.dart';
import '../native_app_start.dart';
import '../sentry_native_channel.dart';
import '../utils/data_normalizer.dart';
import '../utils/utf8_json.dart';
import 'android_envelope_sender.dart';
import 'android_replay_recorder.dart';
Expand Down Expand Up @@ -308,6 +309,43 @@ class SentryNativeJava extends SentryNativeChannel {
jKey.release();
});
}

@override
void setExtra(String key, dynamic value) {
JString jKey = key.toJString();
JString jVal = normalize(value).toString().toJString();

tryCatchSync('setExtra', () {
native.Sentry.configureScope(
native.ScopeCallback.implement(
native.$ScopeCallback(
run: (iScope) {
final scope = iScope.as(const native.$Scope$Type());
scope.setExtra(jKey, jVal);
},
),
),
);
}, finallyFn: () {
jKey.release();
jVal.release();
});
}

@override
FutureOr<void> removeExtra(String key) {
JString jKey = key.toJString();

tryCatchSync('removeExtra', () {
native.Sentry.configureScope(
native.ScopeCallback.implement(native.$ScopeCallback(run: (iScope) {
final scope = iScope.as(const native.$Scope$Type());
scope.removeExtra(jKey);
})));
}, finallyFn: () {
jKey.release();
});
}
}

JObject? _dartToJObject(Object? value) => switch (value) {
Expand Down
13 changes: 6 additions & 7 deletions packages/flutter/lib/src/native/sentry_native_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import 'native_app_start.dart';
import 'sentry_native_binding.dart';
import 'sentry_native_invoker.dart';
import 'sentry_safe_method_channel.dart';
import 'utils/data_normalizer.dart';

/// Provide typed methods to access native layer via MethodChannel.
@internal
Expand Down Expand Up @@ -148,14 +147,14 @@ class SentryNativeChannel
}

@override
Future<void> setExtra(String key, dynamic value) => channel.invokeMethod(
'setExtra',
{'key': key, 'value': normalize(value)},
);
FutureOr<void> setExtra(String key, dynamic value) {
assert(false, "setExtra should not be used through method channels.");
}

@override
Future<void> removeExtra(String key) =>
channel.invokeMethod('removeExtra', {'key': key});
FutureOr<void> removeExtra(String key) {
assert(false, "removeExtra should not be used through method channels.");
}

@override
Future<void> setTag(String key, String value) =>
Expand Down
30 changes: 16 additions & 14 deletions packages/flutter/test/sentry_native_channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import 'package:sentry/src/platform/mock_platform.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_flutter/src/native/factory.dart';
import 'package:sentry_flutter/src/native/sentry_native_binding.dart';
import 'package:sentry_flutter/src/native/utils/data_normalizer.dart';
import 'package:sentry_flutter/src/replay/replay_config.dart';

import 'mocks.dart';
Expand Down Expand Up @@ -119,26 +118,29 @@ void main() {
verifyZeroInteractions(channel);
});

test('setExtra', () async {
test('setExtra', () {
final value = {'object': Object()};
final normalizedValue = normalize(value);
when(channel.invokeMethod(
'setExtra', {'key': 'fixture-key', 'value': normalizedValue}))
.thenAnswer((_) => Future.value());
final matcher = _nativeUnavailableMatcher(
mockPlatform,
includeLookupSymbol: true,
includeFailedToLoadClassException: true,
);

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

verify(channel.invokeMethod(
'setExtra', {'key': 'fixture-key', 'value': normalizedValue}));
verifyZeroInteractions(channel);
});

test('removeExtra', () async {
when(channel.invokeMethod('removeExtra', {'key': 'fixture-key'}))
.thenAnswer((_) => Future.value());
test('removeExtra', () {
final matcher = _nativeUnavailableMatcher(
mockPlatform,
includeLookupSymbol: true,
includeFailedToLoadClassException: true,
);

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

verify(channel.invokeMethod('removeExtra', {'key': 'fixture-key'}));
verifyZeroInteractions(channel);
});

test('setTag', () async {
Expand Down
Loading