Skip to content

Commit d312548

Browse files
authored
Normalize data properties of SentryUser and Breadcrumb before sending over method channel (#1591)
1 parent 78eeed5 commit d312548

6 files changed

+257
-19
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66

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

910
### Dependencies
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import 'package:meta/meta.dart';
2+
3+
/// Makes sure no invalid data is sent over method channels.
4+
@internal
5+
class MethodChannelHelper {
6+
static dynamic normalize(dynamic data) {
7+
if (data == null) {
8+
return null;
9+
}
10+
if (_isPrimitive(data)) {
11+
return data;
12+
} else if (data is List<dynamic>) {
13+
return _normalizeList(data);
14+
} else if (data is Map<String, dynamic>) {
15+
return normalizeMap(data);
16+
} else {
17+
return data.toString();
18+
}
19+
}
20+
21+
static Map<String, dynamic>? normalizeMap(Map<String, dynamic>? data) {
22+
if (data == null) {
23+
return null;
24+
}
25+
return data.map((key, value) => MapEntry(key, normalize(value)));
26+
}
27+
28+
static List<dynamic>? _normalizeList(List<dynamic>? data) {
29+
if (data == null) {
30+
return null;
31+
}
32+
return data.map((e) => normalize(e)).toList();
33+
}
34+
35+
static bool _isPrimitive(dynamic value) {
36+
return value == null || value is String || value is num || value is bool;
37+
}
38+
}

flutter/lib/src/sentry_native_channel.dart

+26-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
44
import 'package:meta/meta.dart';
55

66
import '../sentry_flutter.dart';
7+
import 'method_channel_helper.dart';
78

89
/// Provide typed methods to access native layer.
910
@internal
@@ -47,16 +48,27 @@ class SentryNativeChannel {
4748

4849
Future<void> setUser(SentryUser? user) async {
4950
try {
50-
await _channel.invokeMethod('setUser', {'user': user?.toJson()});
51+
final normalizedUser = user?.copyWith(
52+
data: MethodChannelHelper.normalizeMap(user.data),
53+
);
54+
await _channel.invokeMethod(
55+
'setUser',
56+
{'user': normalizedUser?.toJson()},
57+
);
5158
} catch (error, stackTrace) {
5259
_logError('setUser', error, stackTrace);
5360
}
5461
}
5562

5663
Future<void> addBreadcrumb(Breadcrumb breadcrumb) async {
5764
try {
58-
await _channel
59-
.invokeMethod('addBreadcrumb', {'breadcrumb': breadcrumb.toJson()});
65+
final normalizedBreadcrumb = breadcrumb.copyWith(
66+
data: MethodChannelHelper.normalizeMap(breadcrumb.data),
67+
);
68+
await _channel.invokeMethod(
69+
'addBreadcrumb',
70+
{'breadcrumb': normalizedBreadcrumb.toJson()},
71+
);
6072
} catch (error, stackTrace) {
6173
_logError('addBreadcrumb', error, stackTrace);
6274
}
@@ -72,7 +84,11 @@ class SentryNativeChannel {
7284

7385
Future<void> setContexts(String key, dynamic value) async {
7486
try {
75-
await _channel.invokeMethod('setContexts', {'key': key, 'value': value});
87+
final normalizedValue = MethodChannelHelper.normalize(value);
88+
await _channel.invokeMethod(
89+
'setContexts',
90+
{'key': key, 'value': normalizedValue},
91+
);
7692
} catch (error, stackTrace) {
7793
_logError('setContexts', error, stackTrace);
7894
}
@@ -88,7 +104,11 @@ class SentryNativeChannel {
88104

89105
Future<void> setExtra(String key, dynamic value) async {
90106
try {
91-
await _channel.invokeMethod('setExtra', {'key': key, 'value': value});
107+
final normalizedValue = MethodChannelHelper.normalize(value);
108+
await _channel.invokeMethod(
109+
'setExtra',
110+
{'key': key, 'value': normalizedValue},
111+
);
92112
} catch (error, stackTrace) {
93113
_logError('setExtra', error, stackTrace);
94114
}
@@ -102,7 +122,7 @@ class SentryNativeChannel {
102122
}
103123
}
104124

105-
Future<void> setTag(String key, dynamic value) async {
125+
Future<void> setTag(String key, String value) async {
106126
try {
107127
await _channel.invokeMethod('setTag', {'key': key, 'value': value});
108128
} catch (error, stackTrace) {

flutter/pubspec.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dev_dependencies:
2626
mockito: ^5.1.0
2727
yaml: ^3.1.0 # needed for version match (code and pubspec)
2828
flutter_lints: ^2.0.0
29+
collection: ^1.16.0
2930

3031
flutter:
3132
plugin:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:sentry_flutter/src/method_channel_helper.dart';
3+
import 'package:collection/collection.dart';
4+
5+
void main() {
6+
group('normalize', () {
7+
test('primitives', () {
8+
var expected = <String, dynamic>{
9+
'null': null,
10+
'int': 1,
11+
'float': 1.1,
12+
'bool': true,
13+
'string': 'Foo',
14+
};
15+
16+
var actual = MethodChannelHelper.normalizeMap(expected);
17+
expect(
18+
DeepCollectionEquality().equals(actual, expected),
19+
true,
20+
);
21+
22+
expect(MethodChannelHelper.normalize(null), null);
23+
expect(MethodChannelHelper.normalize(1), 1);
24+
expect(MethodChannelHelper.normalize(1.1), 1.1);
25+
expect(MethodChannelHelper.normalize(true), true);
26+
expect(MethodChannelHelper.normalize('Foo'), 'Foo');
27+
});
28+
29+
test('object', () {
30+
expect(MethodChannelHelper.normalize(_CustomObject()), 'CustomObject()');
31+
});
32+
33+
test('object in list', () {
34+
var input = <String, dynamic>{
35+
'object': [_CustomObject()]
36+
};
37+
var expected = <String, dynamic>{
38+
'object': ['CustomObject()']
39+
};
40+
41+
var actual = MethodChannelHelper.normalize(input);
42+
expect(
43+
DeepCollectionEquality().equals(actual, expected),
44+
true,
45+
);
46+
});
47+
48+
test('object in map', () {
49+
var input = <String, dynamic>{
50+
'object': <String, dynamic>{'object': _CustomObject()}
51+
};
52+
var expected = <String, dynamic>{
53+
'object': <String, dynamic>{'object': 'CustomObject()'}
54+
};
55+
56+
var actual = MethodChannelHelper.normalize(input);
57+
expect(
58+
DeepCollectionEquality().equals(actual, expected),
59+
true,
60+
);
61+
});
62+
});
63+
64+
group('normalizeMap', () {
65+
test('primitives', () {
66+
var expected = <String, dynamic>{
67+
'null': null,
68+
'int': 1,
69+
'float': 1.1,
70+
'bool': true,
71+
'string': 'Foo',
72+
};
73+
74+
var actual = MethodChannelHelper.normalizeMap(expected);
75+
expect(
76+
DeepCollectionEquality().equals(actual, expected),
77+
true,
78+
);
79+
});
80+
81+
test('list with primitives', () {
82+
var expected = <String, dynamic>{
83+
'list': [null, 1, 1.1, true, 'Foo'],
84+
};
85+
86+
var actual = MethodChannelHelper.normalizeMap(expected);
87+
expect(
88+
DeepCollectionEquality().equals(actual, expected),
89+
true,
90+
);
91+
});
92+
93+
test('map with primitives', () {
94+
var expected = <String, dynamic>{
95+
'map': <String, dynamic>{
96+
'null': null,
97+
'int': 1,
98+
'float': 1.1,
99+
'bool': true,
100+
'string': 'Foo',
101+
},
102+
};
103+
104+
var actual = MethodChannelHelper.normalizeMap(expected);
105+
expect(
106+
DeepCollectionEquality().equals(actual, expected),
107+
true,
108+
);
109+
});
110+
111+
test('object', () {
112+
var input = <String, dynamic>{'object': _CustomObject()};
113+
var expected = <String, dynamic>{'object': 'CustomObject()'};
114+
115+
var actual = MethodChannelHelper.normalizeMap(input);
116+
expect(
117+
DeepCollectionEquality().equals(actual, expected),
118+
true,
119+
);
120+
});
121+
122+
test('object in list', () {
123+
var input = <String, dynamic>{
124+
'object': [_CustomObject()]
125+
};
126+
var expected = <String, dynamic>{
127+
'object': ['CustomObject()']
128+
};
129+
130+
var actual = MethodChannelHelper.normalizeMap(input);
131+
expect(
132+
DeepCollectionEquality().equals(actual, expected),
133+
true,
134+
);
135+
});
136+
137+
test('object in map', () {
138+
var input = <String, dynamic>{
139+
'object': <String, dynamic>{'object': _CustomObject()}
140+
};
141+
var expected = <String, dynamic>{
142+
'object': <String, dynamic>{'object': 'CustomObject()'}
143+
};
144+
145+
var actual = MethodChannelHelper.normalizeMap(input);
146+
expect(
147+
DeepCollectionEquality().equals(actual, expected),
148+
true,
149+
);
150+
});
151+
});
152+
}
153+
154+
class _CustomObject {
155+
@override
156+
String toString() {
157+
return 'CustomObject()';
158+
}
159+
}

flutter/test/sentry_native_channel_test.dart

+32-13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:flutter_test/flutter_test.dart';
66
import 'package:mockito/mockito.dart';
77
import 'package:sentry_flutter/sentry_flutter.dart';
8+
import 'package:sentry_flutter/src/method_channel_helper.dart';
89
import 'package:sentry_flutter/src/sentry_native.dart';
910
import 'package:sentry_flutter/src/sentry_native_channel.dart';
1011
import 'mocks.mocks.dart';
@@ -64,26 +65,40 @@ void main() {
6465
});
6566

6667
test('setUser', () async {
67-
when(fixture.methodChannel.invokeMethod('setUser', {'user': null}))
68+
final user = SentryUser(
69+
id: "fixture-id",
70+
data: {'object': Object()},
71+
);
72+
final normalizedUser = user.copyWith(
73+
data: MethodChannelHelper.normalizeMap(user.data),
74+
);
75+
when(fixture.methodChannel
76+
.invokeMethod('setUser', {'user': normalizedUser.toJson()}))
6877
.thenAnswer((_) => Future.value());
6978

7079
final sut = fixture.getSut();
71-
await sut.setUser(null);
80+
await sut.setUser(user);
7281

73-
verify(fixture.methodChannel.invokeMethod('setUser', {'user': null}));
82+
verify(fixture.methodChannel
83+
.invokeMethod('setUser', {'user': normalizedUser.toJson()}));
7484
});
7585

7686
test('addBreadcrumb', () async {
77-
final breadcrumb = Breadcrumb();
87+
final breadcrumb = Breadcrumb(
88+
data: {'object': Object()},
89+
);
90+
final normalizedBreadcrumb = breadcrumb.copyWith(
91+
data: MethodChannelHelper.normalizeMap(breadcrumb.data));
92+
7893
when(fixture.methodChannel.invokeMethod(
79-
'addBreadcrumb', {'breadcrumb': breadcrumb.toJson()}))
94+
'addBreadcrumb', {'breadcrumb': normalizedBreadcrumb.toJson()}))
8095
.thenAnswer((_) => Future.value());
8196

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

85-
verify(fixture.methodChannel
86-
.invokeMethod('addBreadcrumb', {'breadcrumb': breadcrumb.toJson()}));
100+
verify(fixture.methodChannel.invokeMethod(
101+
'addBreadcrumb', {'breadcrumb': normalizedBreadcrumb.toJson()}));
87102
});
88103

89104
test('clearBreadcrumbs', () async {
@@ -97,15 +112,17 @@ void main() {
97112
});
98113

99114
test('setContexts', () async {
115+
final value = {'object': Object()};
116+
final normalizedValue = MethodChannelHelper.normalize(value);
100117
when(fixture.methodChannel.invokeMethod(
101-
'setContexts', {'key': 'fixture-key', 'value': 'fixture-value'}))
118+
'setContexts', {'key': 'fixture-key', 'value': normalizedValue}))
102119
.thenAnswer((_) => Future.value());
103120

104121
final sut = fixture.getSut();
105-
await sut.setContexts('fixture-key', 'fixture-value');
122+
await sut.setContexts('fixture-key', value);
106123

107124
verify(fixture.methodChannel.invokeMethod(
108-
'setContexts', {'key': 'fixture-key', 'value': 'fixture-value'}));
125+
'setContexts', {'key': 'fixture-key', 'value': normalizedValue}));
109126
});
110127

111128
test('removeContexts', () async {
@@ -121,15 +138,17 @@ void main() {
121138
});
122139

123140
test('setExtra', () async {
141+
final value = {'object': Object()};
142+
final normalizedValue = MethodChannelHelper.normalize(value);
124143
when(fixture.methodChannel.invokeMethod(
125-
'setExtra', {'key': 'fixture-key', 'value': 'fixture-value'}))
144+
'setExtra', {'key': 'fixture-key', 'value': normalizedValue}))
126145
.thenAnswer((_) => Future.value());
127146

128147
final sut = fixture.getSut();
129-
await sut.setExtra('fixture-key', 'fixture-value');
148+
await sut.setExtra('fixture-key', value);
130149

131150
verify(fixture.methodChannel.invokeMethod(
132-
'setExtra', {'key': 'fixture-key', 'value': 'fixture-value'}));
151+
'setExtra', {'key': 'fixture-key', 'value': normalizedValue}));
133152
});
134153

135154
test('removeExtra', () async {

0 commit comments

Comments
 (0)