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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixes

- Normalize data properties of `SentryUser` and `Breadcrumb` before sending over method channel ([#1591](https://github.com/getsentry/sentry-dart/pull/1591))

## 7.9.0

### Features
Expand Down
44 changes: 44 additions & 0 deletions flutter/lib/src/method_channel_helper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:meta/meta.dart';

/// Makes sure no invalid data is sent over method channels.
@internal
class MethodChannelHelper {
static Map<String, dynamic>? normalizeMap(Map<String, dynamic>? data) {
if (data == null) {
return null;
}
final mapToReturn = <String, dynamic>{};
data.forEach((key, value) {
if (_isPrimitive(value)) {
mapToReturn[key] = value;
} else if (value is List<dynamic>) {
mapToReturn[key] = _normalizeList(value);
} else if (value is Map<String, dynamic>) {
mapToReturn[key] = normalizeMap(value);
} else if (value is Object) {
mapToReturn[key] = value.toString();
}
});
return mapToReturn;
}

static List<dynamic> _normalizeList(List<dynamic> data) {
final listToReturn = <dynamic>[];
for (var element in data) {
denrase marked this conversation as resolved.
Show resolved Hide resolved
if (_isPrimitive(element)) {
listToReturn.add(element);
} else if (element is List<dynamic>) {
listToReturn.add(_normalizeList(element));
} else if (element is Map<String, dynamic>) {
listToReturn.add(normalizeMap(element));
} else if (element is Object) {
listToReturn.add(element.toString());
}
denrase marked this conversation as resolved.
Show resolved Hide resolved
}
return listToReturn;
}

static bool _isPrimitive(dynamic value) {
return value == null || value is String || value is num || value is bool;
}
}
18 changes: 15 additions & 3 deletions flutter/lib/src/sentry_native_channel.dart
Original file line number Diff line number Diff line change
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 Down
1 change: 1 addition & 0 deletions flutter/pubspec.yaml
Original file line number Diff line number Diff line change
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
99 changes: 99 additions & 0 deletions flutter/test/method_channel_helper_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:sentry_flutter/src/method_channel_helper.dart';
import 'package:collection/collection.dart';

void main() {
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()';
}
}
29 changes: 22 additions & 7 deletions flutter/test/sentry_native_channel_test.dart
Original file line number Diff line number Diff line change
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 Down
Loading