Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Normalize data properties of SentryUser and Breadcrumb before sending over method channel #1591

Merged
merged 10 commits into from Sep 4, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@

### Fixes

- Normalize data properties of `SentryUser` and `Breadcrumb` before sending over method channel ([#1591](https://github.com/getsentry/sentry-dart/pull/1591))
- Fixing memory leak issue in SentryFlutterPlugin (Android Plugin) ([#1588](https://github.com/getsentry/sentry-dart/pull/1588))

### Dependencies
Expand Down
38 changes: 38 additions & 0 deletions flutter/lib/src/method_channel_helper.dart
@@ -0,0 +1,38 @@
import 'package:meta/meta.dart';

/// Makes sure no invalid data is sent over method channels.
@internal
class MethodChannelHelper {
static dynamic normalize(dynamic data) {
if (data == null) {
return null;
}
if (_isPrimitive(data)) {
return data;
} else if (data is List<dynamic>) {
return _normalizeList(data);
} else if (data is Map<String, dynamic>) {
return normalizeMap(data);
} else {
return data.toString();
}
}

static Map<String, dynamic>? normalizeMap(Map<String, dynamic>? data) {
if (data == null) {
return null;
}
return data.map((key, value) => MapEntry(key, normalize(value)));
}

static List<dynamic>? _normalizeList(List<dynamic>? data) {
if (data == null) {
return null;
}
return data.map((e) => normalize(e)).toList();
}

static bool _isPrimitive(dynamic value) {
return value == null || value is String || value is num || value is bool;
}
}
32 changes: 26 additions & 6 deletions flutter/lib/src/sentry_native_channel.dart
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:meta/meta.dart';

import '../sentry_flutter.dart';
import 'method_channel_helper.dart';

/// Provide typed methods to access native layer.
@internal
Expand Down Expand Up @@ -47,16 +48,27 @@ class SentryNativeChannel {

Future<void> setUser(SentryUser? user) async {
try {
await _channel.invokeMethod('setUser', {'user': user?.toJson()});
final normalizedUser = user?.copyWith(
data: MethodChannelHelper.normalizeMap(user.data),
denrase marked this conversation as resolved.
Show resolved Hide resolved
);
await _channel.invokeMethod(
'setUser',
{'user': normalizedUser?.toJson()},
);
} catch (error, stackTrace) {
_logError('setUser', error, stackTrace);
}
}

Future<void> addBreadcrumb(Breadcrumb breadcrumb) async {
try {
await _channel
.invokeMethod('addBreadcrumb', {'breadcrumb': breadcrumb.toJson()});
final normalizedBreadcrumb = breadcrumb.copyWith(
data: MethodChannelHelper.normalizeMap(breadcrumb.data),
);
await _channel.invokeMethod(
'addBreadcrumb',
{'breadcrumb': normalizedBreadcrumb.toJson()},
);
} catch (error, stackTrace) {
_logError('addBreadcrumb', error, stackTrace);
}
Expand All @@ -72,7 +84,11 @@ class SentryNativeChannel {

Future<void> setContexts(String key, dynamic value) async {
try {
await _channel.invokeMethod('setContexts', {'key': key, 'value': value});
final normalizedValue = MethodChannelHelper.normalize(value);
await _channel.invokeMethod(
'setContexts',
{'key': key, 'value': normalizedValue},
);
} catch (error, stackTrace) {
_logError('setContexts', error, stackTrace);
}
Expand All @@ -88,7 +104,11 @@ class SentryNativeChannel {

Future<void> setExtra(String key, dynamic value) async {
try {
await _channel.invokeMethod('setExtra', {'key': key, 'value': value});
final normalizedValue = MethodChannelHelper.normalize(value);
await _channel.invokeMethod(
'setExtra',
{'key': key, 'value': normalizedValue},
);
} catch (error, stackTrace) {
_logError('setExtra', error, stackTrace);
}
Expand All @@ -102,7 +122,7 @@ class SentryNativeChannel {
}
}

Future<void> setTag(String key, dynamic value) async {
Future<void> setTag(String key, String value) async {
try {
await _channel.invokeMethod('setTag', {'key': key, 'value': value});
} catch (error, stackTrace) {
Expand Down
1 change: 1 addition & 0 deletions flutter/pubspec.yaml
Expand Up @@ -26,6 +26,7 @@ dev_dependencies:
mockito: ^5.1.0
yaml: ^3.1.0 # needed for version match (code and pubspec)
flutter_lints: ^2.0.0
collection: ^1.16.0

flutter:
plugin:
Expand Down
159 changes: 159 additions & 0 deletions flutter/test/method_channel_helper_test.dart
@@ -0,0 +1,159 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:sentry_flutter/src/method_channel_helper.dart';
import 'package:collection/collection.dart';

void main() {
group('normalize', () {
test('primitives', () {
var expected = <String, dynamic>{
'null': null,
'int': 1,
'float': 1.1,
'bool': true,
'string': 'Foo',
};

var actual = MethodChannelHelper.normalizeMap(expected);
expect(
DeepCollectionEquality().equals(actual, expected),
true,
);

expect(MethodChannelHelper.normalize(null), null);
expect(MethodChannelHelper.normalize(1), 1);
expect(MethodChannelHelper.normalize(1.1), 1.1);
expect(MethodChannelHelper.normalize(true), true);
expect(MethodChannelHelper.normalize('Foo'), 'Foo');
});

test('object', () {
expect(MethodChannelHelper.normalize(_CustomObject()), 'CustomObject()');
});

test('object in list', () {
var input = <String, dynamic>{
'object': [_CustomObject()]
};
var expected = <String, dynamic>{
'object': ['CustomObject()']
};

var actual = MethodChannelHelper.normalize(input);
expect(
DeepCollectionEquality().equals(actual, expected),
true,
);
});

test('object in map', () {
var input = <String, dynamic>{
'object': <String, dynamic>{'object': _CustomObject()}
};
var expected = <String, dynamic>{
'object': <String, dynamic>{'object': 'CustomObject()'}
};

var actual = MethodChannelHelper.normalize(input);
expect(
DeepCollectionEquality().equals(actual, expected),
true,
);
});
});

group('normalizeMap', () {
test('primitives', () {
var expected = <String, dynamic>{
'null': null,
'int': 1,
'float': 1.1,
'bool': true,
'string': 'Foo',
};

var actual = MethodChannelHelper.normalizeMap(expected);
expect(
DeepCollectionEquality().equals(actual, expected),
true,
);
});

test('list with primitives', () {
var expected = <String, dynamic>{
'list': [null, 1, 1.1, true, 'Foo'],
};

var actual = MethodChannelHelper.normalizeMap(expected);
expect(
DeepCollectionEquality().equals(actual, expected),
true,
);
});

test('map with primitives', () {
var expected = <String, dynamic>{
'map': <String, dynamic>{
'null': null,
'int': 1,
'float': 1.1,
'bool': true,
'string': 'Foo',
},
};

var actual = MethodChannelHelper.normalizeMap(expected);
expect(
DeepCollectionEquality().equals(actual, expected),
true,
);
});

test('object', () {
var input = <String, dynamic>{'object': _CustomObject()};
var expected = <String, dynamic>{'object': 'CustomObject()'};

var actual = MethodChannelHelper.normalizeMap(input);
expect(
DeepCollectionEquality().equals(actual, expected),
true,
);
});

test('object in list', () {
var input = <String, dynamic>{
'object': [_CustomObject()]
};
var expected = <String, dynamic>{
'object': ['CustomObject()']
};

var actual = MethodChannelHelper.normalizeMap(input);
expect(
DeepCollectionEquality().equals(actual, expected),
true,
);
});

test('object in map', () {
var input = <String, dynamic>{
'object': <String, dynamic>{'object': _CustomObject()}
};
var expected = <String, dynamic>{
'object': <String, dynamic>{'object': 'CustomObject()'}
};

var actual = MethodChannelHelper.normalizeMap(input);
expect(
DeepCollectionEquality().equals(actual, expected),
true,
);
});
});
}

class _CustomObject {
@override
String toString() {
return 'CustomObject()';
}
}
45 changes: 32 additions & 13 deletions flutter/test/sentry_native_channel_test.dart
Expand Up @@ -5,6 +5,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_flutter/src/method_channel_helper.dart';
import 'package:sentry_flutter/src/sentry_native.dart';
import 'package:sentry_flutter/src/sentry_native_channel.dart';
import 'mocks.mocks.dart';
Expand Down Expand Up @@ -64,26 +65,40 @@ void main() {
});

test('setUser', () async {
when(fixture.methodChannel.invokeMethod('setUser', {'user': null}))
final user = SentryUser(
id: "fixture-id",
data: {'object': Object()},
);
final normalizedUser = user.copyWith(
data: MethodChannelHelper.normalizeMap(user.data),
);
when(fixture.methodChannel
.invokeMethod('setUser', {'user': normalizedUser.toJson()}))
.thenAnswer((_) => Future.value());

final sut = fixture.getSut();
await sut.setUser(null);
await sut.setUser(user);

verify(fixture.methodChannel.invokeMethod('setUser', {'user': null}));
verify(fixture.methodChannel
.invokeMethod('setUser', {'user': normalizedUser.toJson()}));
});

test('addBreadcrumb', () async {
final breadcrumb = Breadcrumb();
final breadcrumb = Breadcrumb(
data: {'object': Object()},
);
final normalizedBreadcrumb = breadcrumb.copyWith(
data: MethodChannelHelper.normalizeMap(breadcrumb.data));

when(fixture.methodChannel.invokeMethod(
'addBreadcrumb', {'breadcrumb': breadcrumb.toJson()}))
'addBreadcrumb', {'breadcrumb': normalizedBreadcrumb.toJson()}))
.thenAnswer((_) => Future.value());

final sut = fixture.getSut();
await sut.addBreadcrumb(breadcrumb);

verify(fixture.methodChannel
.invokeMethod('addBreadcrumb', {'breadcrumb': breadcrumb.toJson()}));
verify(fixture.methodChannel.invokeMethod(
'addBreadcrumb', {'breadcrumb': normalizedBreadcrumb.toJson()}));
});

test('clearBreadcrumbs', () async {
Expand All @@ -97,15 +112,17 @@ void main() {
});

test('setContexts', () async {
final value = {'object': Object()};
final normalizedValue = MethodChannelHelper.normalize(value);
when(fixture.methodChannel.invokeMethod(
'setContexts', {'key': 'fixture-key', 'value': 'fixture-value'}))
'setContexts', {'key': 'fixture-key', 'value': normalizedValue}))
.thenAnswer((_) => Future.value());

final sut = fixture.getSut();
await sut.setContexts('fixture-key', 'fixture-value');
await sut.setContexts('fixture-key', value);

verify(fixture.methodChannel.invokeMethod(
'setContexts', {'key': 'fixture-key', 'value': 'fixture-value'}));
'setContexts', {'key': 'fixture-key', 'value': normalizedValue}));
});

test('removeContexts', () async {
Expand All @@ -121,15 +138,17 @@ void main() {
});

test('setExtra', () async {
final value = {'object': Object()};
final normalizedValue = MethodChannelHelper.normalize(value);
when(fixture.methodChannel.invokeMethod(
'setExtra', {'key': 'fixture-key', 'value': 'fixture-value'}))
'setExtra', {'key': 'fixture-key', 'value': normalizedValue}))
.thenAnswer((_) => Future.value());

final sut = fixture.getSut();
await sut.setExtra('fixture-key', 'fixture-value');
await sut.setExtra('fixture-key', value);

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

test('removeExtra', () async {
Expand Down