Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/flutter-test/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ runs:
# Remove native binding/bridge files from coverage.
# These FFI/JNI bindings are currently not unit tested due to limitations of FFI/JNI mocking.
dart run remove_from_coverage -f coverage/lcov.info -r 'binding.dart'
dart run remove_from_coverage -f coverage/lcov.info -r 'lib/src/native/sentry_native_channel.dart'
Copy link
Contributor Author

@buenaflor buenaflor Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this from the coverage so CI doesn't complain.

We will eventually remove the file anyway as soon as we have moved away from method channels completely

dart run remove_from_coverage -f coverage/lcov.info -r 'lib/src/native/java/sentry_native_java.dart'
dart run remove_from_coverage -f coverage/lcov.info -r 'lib/src/native/cocoa/sentry_native_cocoa.dart'
fi
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

### Enhancements

- Refactor `setContexts` and `removeContexts` to use FFI/JNI ([#3312](https://github.com/getsentry/sentry-dart/pull/3312))
- Refactor `setUser` to use FFI/JNI ([#3295](https://github.com/getsentry/sentry-dart/pull/3295/))
- Refactor native breadcrumbs sync to use FFI/JNI ([#3293](https://github.com/getsentry/sentry-dart/pull/3293/))
- Refactor app hang and crash apis to use FFI/JNI ([#3289](https://github.com/getsentry/sentry-dart/pull/3289/))
Expand Down
8 changes: 4 additions & 4 deletions packages/dart/lib/src/scope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,17 @@ class Scope {
}

/// add an entry to the Scope's contexts
Future<void> setContexts(String key, dynamic value) async {
FutureOr<void> setContexts(String key, dynamic value) {
_setContextsSync(key, value);
await _callScopeObservers(
return _callScopeObservers(
(scopeObserver) async => await scopeObserver.setContexts(key, value));
}

/// Removes a value from the Scope's contexts
Future<void> removeContexts(String key) async {
FutureOr<void> removeContexts(String key) {
_contexts.remove(key);

await _callScopeObservers(
return _callScopeObservers(
(scopeObserver) async => await scopeObserver.removeContexts(key));
}

Expand Down
4 changes: 2 additions & 2 deletions packages/dart/lib/src/scope_observer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import 'protocol/breadcrumb.dart';
import 'protocol/sentry_user.dart';

abstract class ScopeObserver {
Future<void> setContexts(String key, dynamic value);
Future<void> removeContexts(String key);
FutureOr<void> setContexts(String key, dynamic value);
FutureOr<void> removeContexts(String key);
FutureOr<void> setUser(SentryUser? user);
FutureOr<void> addBreadcrumb(Breadcrumb breadcrumb);
FutureOr<void> clearBreadcrumbs();
Expand Down
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)
"setContexts" -> setContexts(call.argument("key"), call.argument("value"), result)
"removeContexts" -> removeContexts(call.argument("key"), 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)
Expand Down Expand Up @@ -141,37 +139,6 @@ class SentryFlutterPlugin :
}
}

private fun setContexts(
key: String?,
value: Any?,
result: Result,
) {
if (key == null || value == null) {
result.success("")
return
}
Sentry.configureScope { scope ->
scope.setContexts(key, value)

result.success("")
}
}

private fun removeContexts(
key: String?,
result: Result,
) {
if (key == null) {
result.success("")
return
}
Sentry.configureScope { scope ->
scope.removeContexts(key)

result.success("")
}
}

private fun setExtra(
key: String?,
value: String?,
Expand Down
56 changes: 56 additions & 0 deletions packages/flutter/example/integration_test/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,62 @@ void main() {
}
});

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

await Sentry.configureScope((scope) async {
scope.setContexts('key1', 'randomValue');
scope.setContexts('key2',
{'String': 'Value', 'Bool': true, 'Int': 123, 'Double': 12.3});
scope.setContexts('key3', true);
scope.setContexts('key4', 12);
scope.setContexts('key5', 12.3);
});

var contexts = await SentryFlutter.native?.loadContexts();
final values = contexts!['contexts'];
expect(values, isNotNull, reason: 'Contexts are null');

if (Platform.isIOS) {
expect(values['key1'], {'value': 'randomValue'}, reason: 'key1 mismatch');
expect(values['key2'],
{'String': 'Value', 'Bool': 1, 'Int': 123, 'Double': 12.3},
reason: 'key2 mismatch');
// bool values are mapped to num values of 1 or 0 during objc conversion
expect(values['key3'], {'value': 1}, reason: 'key3 mismatch');
expect(values['key4'], {'value': 12}, reason: 'key4 mismatch');
expect(values['key5'], {'value': 12.3}, reason: 'key5 mismatch');
} else if (Platform.isAndroid) {
expect(values['key1'], 'randomValue', reason: 'key1 mismatch');
expect(values['key2'],
{'String': 'Value', 'Bool': true, 'Int': 123, 'Double': 12.3},
reason: 'key2 mismatch');
expect(values['key3'], true, reason: 'key3 mismatch');
expect(values['key4'], 12, reason: 'key4 mismatch');
expect(values['key5'], 12.3, reason: 'key5 mismatch');
}

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

contexts = await SentryFlutter.native?.loadContexts();
final removedValues = contexts!['contexts'];
expect(removedValues, isNotNull, reason: 'Contexts are null');

expect(removedValues['key1'], isNull, reason: 'key1 should be removed');
expect(removedValues['key2'], isNull, reason: 'key2 should be removed');
expect(removedValues['key3'], isNull, reason: 'key3 should be removed');
expect(removedValues['key4'], isNull, reason: 'key4 should be removed');
expect(removedValues['key5'], isNull, reason: 'key5 should be removed');
});

group('e2e', () {
var output = find.byKey(const Key('output'));
late Fixture fixture;
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 @@ -38,6 +38,8 @@ objc-interfaces:
SentryScope:
include:
- 'clearBreadcrumbs'
- 'setContextValue:forKey:'
- 'removeContextForKey:'
preamble: |
// ignore_for_file: type=lint, unused_element

2 changes: 2 additions & 0 deletions packages/flutter/ffi-jni.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ classes:
- io.sentry.Sentry
- io.sentry.Breadcrumb
- io.sentry.ScopesAdapter
- io.sentry.Scope
- io.sentry.ScopeCallback
- io.sentry.protocol.User
- android.graphics.Bitmap
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
?? iso8601Formatter.date(from: iso8601String) // Parse date with low precision formatter for backward compatible
}

// swiftlint:disable:next cyclomatic_complexity
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method as String {
case "initNativeSdk":
Expand All @@ -76,17 +75,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
case "closeNativeSdk":
closeNativeSdk(call, result: result)

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

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

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

private func setContexts(key: String?, value: Any?, result: @escaping FlutterResult) {
guard let key = key else {
result("")
return
}

SentrySDK.configureScope { scope in
if let dictionary = value as? [String: Any] {
scope.setContext(value: dictionary, key: key)
} else if let string = value as? String {
scope.setContext(value: ["value": string], key: key)
} else if let int = value as? Int {
scope.setContext(value: ["value": int], key: key)
} else if let double = value as? Double {
scope.setContext(value: ["value": double], key: key)
} else if let bool = value as? Bool {
scope.setContext(value: ["value": bool], key: key)
}
result("")
}
}

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

private func setExtra(key: String?, value: Any?, result: @escaping FlutterResult) {
guard let key = key else {
result("")
Expand Down
18 changes: 18 additions & 0 deletions packages/flutter/lib/src/native/cocoa/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,10 @@ final _objc_msgSend_1pl9qdv = objc.msgSendPointer
.asFunction<
void Function(
ffi.Pointer<objc.ObjCObject>, ffi.Pointer<objc.ObjCSelector>)>();
late final _sel_setContextValue_forKey_ =
objc.registerName("setContextValue:forKey:");
late final _sel_removeContextForKey_ =
objc.registerName("removeContextForKey:");

/// SentryScope
class SentryScope extends objc.NSObject implements SentrySerializable {
Expand All @@ -1183,6 +1187,20 @@ class SentryScope extends objc.NSObject implements SentrySerializable {
void clearBreadcrumbs() {
_objc_msgSend_1pl9qdv(this.ref.pointer, _sel_clearBreadcrumbs);
}

/// Sets context values which will overwrite SentryEvent.context when event is
/// "enriched" with scope before sending event.
void setContextValue(objc.NSDictionary value,
{required objc.NSString forKey}) {
_objc_msgSend_pfv6jd(this.ref.pointer, _sel_setContextValue_forKey_,
value.ref.pointer, forKey.ref.pointer);
}

/// Remove the context for the specified key.
void removeContextForKey(objc.NSString key) {
_objc_msgSend_xtuoz7(
this.ref.pointer, _sel_removeContextForKey_, key.ref.pointer);
}
}

void _ObjCBlock_ffiVoid_SentryScope_fnPtrTrampoline(
Expand Down
57 changes: 54 additions & 3 deletions packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import '../../../sentry_flutter.dart';
import '../../replay/replay_config.dart';
import '../native_app_start.dart';
import '../sentry_native_channel.dart';
import '../utils/data_normalizer.dart';
import '../utils/utf8_json.dart';
import 'binding.dart' as cocoa;
import 'cocoa_replay_recorder.dart';
Expand Down Expand Up @@ -197,7 +198,7 @@ class SentryNativeCocoa extends SentryNativeChannel {
tryCatchSync('addBreadcrumb', () {
final nativeBreadcrumb =
cocoa.PrivateSentrySDKOnly.breadcrumbWithDictionary(
_deepConvertMapNonNull(breadcrumb.toJson()).toNSDictionary());
_dartToNSDictionary(breadcrumb.toJson()));
cocoa.SentrySDK.addBreadcrumb(nativeBreadcrumb);
});

Expand Down Expand Up @@ -228,13 +229,63 @@ class SentryNativeCocoa extends SentryNativeChannel {
if (user == null) {
cocoa.SentrySDK.setUser(null);
} else {
final dictionary =
_deepConvertMapNonNull(user.toJson()).toNSDictionary();
final dictionary = _dartToNSDictionary(user.toJson());
final cUser =
cocoa.PrivateSentrySDKOnly.userWithDictionary(dictionary);
cocoa.SentrySDK.setUser(cUser);
}
});

@override
void setContexts(String key, dynamic value) =>
tryCatchSync('setContexts', () {
NSDictionary? dictionary;

final normalizedValue = normalize(value);
dictionary = switch (normalizedValue) {
Map<String, dynamic> m => _dartToNSDictionary(m),
Object o => NSDictionary.fromEntries(
[MapEntry('value'.toNSString(), _dartToNSObject(o))]),
_ => null
};

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

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

// The default conversion does not handle bool so we will add it ourselves
final ObjCObjectBase Function(Object) _defaultObjcConverter = (obj) {
return switch (obj) {
bool b => b ? 1.toNSNumber() : 0.toNSNumber(),
_ => toObjCObject(obj)
};
};

NSDictionary _dartToNSDictionary(Map<String, dynamic> json) {
return _deepConvertMapNonNull(json)
.toNSDictionary(convertOther: _defaultObjcConverter);
}

ObjCObjectBase _dartToNSObject(Object value) {
return switch (value) {
Map<String, dynamic> m => _dartToNSDictionary(m),
_ => toObjCObject(value, convertOther: _defaultObjcConverter)
};
}

/// This map conversion is needed so we can use the toNSDictionary extension function
Expand Down
Loading
Loading