Skip to content

Commit e1b27fd

Browse files
Ensure complex / dynamic values are properly serializable on web (#197)
* convert props for web * remove JSAny case check * rename to ensureSerializable* * uncomment * add tests * add test ffile
1 parent 9755a99 commit e1b27fd

File tree

5 files changed

+474
-60
lines changed

5 files changed

+474
-60
lines changed

example/pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ packages:
126126
path: ".."
127127
relative: true
128128
source: path
129-
version: "2.4.1"
129+
version: "2.4.3"
130130
path:
131131
dependency: transitive
132132
description:

lib/mixpanel_flutter.dart

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import 'package:mixpanel_flutter/codec/mixpanel_message_codec.dart';
88

99
/// The primary class for integrating Mixpanel with your app.
1010
class Mixpanel {
11-
static const MethodChannel _channel = const MethodChannel(
12-
'mixpanel_flutter', StandardMethodCodec(MixpanelMessageCodec()));
11+
static final MethodChannel _channel = kIsWeb
12+
? const MethodChannel('mixpanel_flutter')
13+
: const MethodChannel(
14+
'mixpanel_flutter', StandardMethodCodec(MixpanelMessageCodec()));
1315
static Map<String, String> _mixpanelProperties = {
1416
'\$lib_version': '2.4.3',
1517
'mp_lib': 'flutter',
@@ -42,8 +44,8 @@ class Mixpanel {
4244
allProperties['optOutTrackingDefault'] = optOutTrackingDefault;
4345
allProperties['trackAutomaticEvents'] = trackAutomaticEvents;
4446
allProperties['mixpanelProperties'] = _mixpanelProperties;
45-
allProperties['superProperties'] = superProperties;
46-
allProperties['config'] = config;
47+
allProperties['superProperties'] = _MixpanelHelper.ensureSerializableProperties(superProperties);
48+
allProperties['config'] = _MixpanelHelper.ensureSerializableProperties(config);
4749
await _channel.invokeMethod<void>('initialize', allProperties);
4850
return Mixpanel(token);
4951
}
@@ -196,7 +198,7 @@ class Mixpanel {
196198
}) async {
197199
if (_MixpanelHelper.isValidString(eventName)) {
198200
await _channel.invokeMethod<void>('track',
199-
<String, dynamic>{'eventName': eventName, 'properties': properties});
201+
<String, dynamic>{'eventName': eventName, 'properties': _MixpanelHelper.ensureSerializableProperties(properties)});
200202
} else {
201203
developer.log('`track` failed: eventName cannot be blank',
202204
name: 'Mixpanel');
@@ -229,8 +231,8 @@ class Mixpanel {
229231
if (_MixpanelHelper.isValidString(eventName)) {
230232
await _channel.invokeMethod<void>('trackWithGroups', <String, dynamic>{
231233
'eventName': eventName,
232-
'properties': properties,
233-
'groups': groups
234+
'properties': _MixpanelHelper.ensureSerializableProperties(properties),
235+
'groups': _MixpanelHelper.ensureSerializableProperties(groups)
234236
});
235237
} else {
236238
developer.log('`trackWithGroups` failed: eventName cannot be blank',
@@ -245,7 +247,7 @@ class Mixpanel {
245247
void setGroup(String groupKey, dynamic groupID) {
246248
if (_MixpanelHelper.isValidString(groupKey)) {
247249
_channel.invokeMethod<void>('setGroup',
248-
<String, dynamic>{'groupKey': groupKey, 'groupID': groupID});
250+
<String, dynamic>{'groupKey': groupKey, 'groupID': _MixpanelHelper.ensureSerializableValue(groupID)});
249251
} else {
250252
developer.log('`setGroup` failed: groupKey cannot be blank',
251253
name: 'Mixpanel');
@@ -260,7 +262,7 @@ class Mixpanel {
260262
/// return an instance of MixpanelGroup that you can use to update
261263
/// records in Mixpanel Group Analytics
262264
MixpanelGroup getGroup(String groupKey, dynamic groupID) {
263-
return new MixpanelGroup(this._token, groupKey, groupID);
265+
return new MixpanelGroup(this._token, groupKey, _MixpanelHelper.ensureSerializableValue(groupID));
264266
}
265267

266268
/// Add a group to this user's membership for a particular group key
@@ -270,7 +272,7 @@ class Mixpanel {
270272
void addGroup(String groupKey, dynamic groupID) {
271273
if (_MixpanelHelper.isValidString(groupKey)) {
272274
_channel.invokeMethod<void>('addGroup',
273-
<String, dynamic>{'groupKey': groupKey, 'groupID': groupID});
275+
<String, dynamic>{'groupKey': groupKey, 'groupID': _MixpanelHelper.ensureSerializableValue(groupID)});
274276
} else {
275277
developer.log('`addGroup` failed: groupKey cannot be blank',
276278
name: 'Mixpanel');
@@ -284,7 +286,7 @@ class Mixpanel {
284286
void removeGroup(String groupKey, dynamic groupID) {
285287
if (_MixpanelHelper.isValidString(groupKey)) {
286288
_channel.invokeMethod<void>('removeGroup',
287-
<String, dynamic>{'groupKey': groupKey, 'groupID': groupID});
289+
<String, dynamic>{'groupKey': groupKey, 'groupID': _MixpanelHelper.ensureSerializableValue(groupID)});
288290
} else {
289291
developer.log('`removeGroup` failed: groupKey cannot be blank',
290292
name: 'Mixpanel');
@@ -301,7 +303,7 @@ class Mixpanel {
301303
void deleteGroup(String groupKey, dynamic groupID) {
302304
if (_MixpanelHelper.isValidString(groupKey)) {
303305
_channel.invokeMethod<void>('deleteGroup',
304-
<String, dynamic>{'groupKey': groupKey, 'groupID': groupID});
306+
<String, dynamic>{'groupKey': groupKey, 'groupID': _MixpanelHelper.ensureSerializableValue(groupID)});
305307
} else {
306308
developer.log('`deleteGroup` failed: groupKey cannot be blank',
307309
name: 'Mixpanel');
@@ -323,7 +325,7 @@ class Mixpanel {
323325
/// * [properties] A Map containing super properties to register
324326
Future<void> registerSuperProperties(Map<String, dynamic> properties) async {
325327
await _channel.invokeMethod<void>(
326-
'registerSuperProperties', <String, dynamic>{'properties': properties});
328+
'registerSuperProperties', <String, dynamic>{'properties': _MixpanelHelper.ensureSerializableProperties(properties)});
327329
}
328330

329331
/// Register super properties for events, only if no other super property with the
@@ -336,7 +338,7 @@ class Mixpanel {
336338
Map<String, dynamic> properties,
337339
) async {
338340
await _channel.invokeMethod<void>('registerSuperPropertiesOnce',
339-
<String, dynamic>{'properties': properties});
341+
<String, dynamic>{'properties': _MixpanelHelper.ensureSerializableProperties(properties)});
340342
}
341343

342344
/// Remove a single superProperty, so that it will not be sent with future calls to track().
@@ -450,7 +452,10 @@ class Mixpanel {
450452
/// persist across stops and starts of your application, until you make another
451453
/// call to identify using a different id.
452454
class People {
453-
static const MethodChannel _channel = const MethodChannel('mixpanel_flutter');
455+
static final MethodChannel _channel = kIsWeb
456+
? const MethodChannel('mixpanel_flutter')
457+
: const MethodChannel(
458+
'mixpanel_flutter', StandardMethodCodec(MixpanelMessageCodec()));
454459

455460
final String _token;
456461

@@ -467,7 +472,7 @@ class People {
467472
if (_MixpanelHelper.isValidString(prop)) {
468473
Map<String, dynamic> properties = {prop: to};
469474
_channel.invokeMethod<void>('set',
470-
<String, dynamic>{'token': this._token, 'properties': properties});
475+
<String, dynamic>{'token': this._token, 'properties': _MixpanelHelper.ensureSerializableProperties(properties)});
471476
} else {
472477
developer.log('`people set` failed: prop cannot be blank',
473478
name: 'Mixpanel');
@@ -482,7 +487,7 @@ class People {
482487
if (_MixpanelHelper.isValidString(prop)) {
483488
Map<String, dynamic> properties = {prop: to};
484489
_channel.invokeMethod<void>('setOnce',
485-
<String, dynamic>{'token': this._token, 'properties': properties});
490+
<String, dynamic>{'token': this._token, 'properties': _MixpanelHelper.ensureSerializableProperties(properties)});
486491
} else {
487492
developer.log('`people setOnce` failed: prop cannot be blank',
488493
name: 'Mixpanel');
@@ -499,7 +504,7 @@ class People {
499504
Map<String, dynamic> properties = {prop: by};
500505
if (_MixpanelHelper.isValidString(prop)) {
501506
_channel.invokeMethod<void>('increment',
502-
<String, dynamic>{'token': this._token, 'properties': properties});
507+
<String, dynamic>{'token': this._token, 'properties': _MixpanelHelper.ensureSerializableProperties(properties)});
503508
} else {
504509
developer.log('`people increment` failed: prop cannot be blank',
505510
name: 'Mixpanel');
@@ -516,12 +521,12 @@ class People {
516521
if (kIsWeb || Platform.isIOS) {
517522
Map<String, dynamic> properties = {name: value};
518523
_channel.invokeMethod<void>('append',
519-
<String, dynamic>{'token': this._token, 'properties': properties});
524+
<String, dynamic>{'token': this._token, 'properties': _MixpanelHelper.ensureSerializableProperties(properties)});
520525
} else {
521526
_channel.invokeMethod<void>('append', <String, dynamic>{
522527
'token': this._token,
523528
'name': name,
524-
'value': value
529+
'value': _MixpanelHelper.ensureSerializableValue(value)
525530
});
526531
}
527532
} else {
@@ -541,12 +546,12 @@ class People {
541546
if (kIsWeb || Platform.isIOS) {
542547
Map<String, dynamic> properties = {name: value};
543548
_channel.invokeMethod<void>('union',
544-
<String, dynamic>{'token': this._token, 'properties': properties});
549+
<String, dynamic>{'token': this._token, 'properties': _MixpanelHelper.ensureSerializableProperties(properties)});
545550
} else {
546551
_channel.invokeMethod<void>('union', <String, dynamic>{
547552
'token': this._token,
548553
'name': name,
549-
'value': value
554+
'value': _MixpanelHelper.ensureSerializableValue(value)
550555
});
551556
}
552557
} else {
@@ -566,12 +571,12 @@ class People {
566571
if (kIsWeb || Platform.isIOS) {
567572
Map<String, dynamic> properties = {name: value};
568573
_channel.invokeMethod<void>('remove',
569-
<String, dynamic>{'token': this._token, 'properties': properties});
574+
<String, dynamic>{'token': this._token, 'properties': _MixpanelHelper.ensureSerializableProperties(properties)});
570575
} else {
571576
_channel.invokeMethod<void>('remove', <String, dynamic>{
572577
'token': this._token,
573578
'name': name,
574-
'value': value
579+
'value': _MixpanelHelper.ensureSerializableValue(value)
575580
});
576581
}
577582
} else {
@@ -603,7 +608,7 @@ class People {
603608
_channel.invokeMethod<void>('trackCharge', <String, dynamic>{
604609
'token': this._token,
605610
'amount': amount,
606-
'properties': properties
611+
'properties': _MixpanelHelper.ensureSerializableProperties(properties)
607612
});
608613
} else {
609614
developer.log('`people trackCharge` failed: amount cannot be blank',
@@ -631,7 +636,10 @@ class People {
631636
///
632637
/// The MixpanelGroup object is used to update properties in a group's Group Analytics record.
633638
class MixpanelGroup {
634-
static const MethodChannel _channel = const MethodChannel('mixpanel_flutter');
639+
static final MethodChannel _channel = kIsWeb
640+
? const MethodChannel('mixpanel_flutter')
641+
: const MethodChannel(
642+
'mixpanel_flutter', StandardMethodCodec(MixpanelMessageCodec()));
635643

636644
final String _token;
637645
final String _groupKey;
@@ -656,7 +664,7 @@ class MixpanelGroup {
656664
'token': this._token,
657665
'groupKey': this._groupKey,
658666
'groupID': this._groupID,
659-
'properties': properties
667+
'properties': _MixpanelHelper.ensureSerializableProperties(properties)
660668
});
661669
} else {
662670
developer.log('`group set` failed: prop cannot be blank',
@@ -676,7 +684,7 @@ class MixpanelGroup {
676684
'token': this._token,
677685
'groupKey': this._groupKey,
678686
'groupID': this._groupID,
679-
'properties': properties
687+
'properties': _MixpanelHelper.ensureSerializableProperties(properties)
680688
});
681689
} else {
682690
developer.log('`group setOnce` failed: prop cannot be blank',
@@ -714,7 +722,7 @@ class MixpanelGroup {
714722
'groupKey': this._groupKey,
715723
'groupID': this._groupID,
716724
'name': name,
717-
'value': value
725+
'value': _MixpanelHelper.ensureSerializableValue(value)
718726
});
719727
} else {
720728
developer.log('`group remove` failed: name cannot be blank',
@@ -745,7 +753,7 @@ class MixpanelGroup {
745753
'groupKey': this._groupKey,
746754
'groupID': this._groupID,
747755
'name': name,
748-
'value': value
756+
'value': _MixpanelHelper.ensureSerializableValue(value)
749757
});
750758
}
751759
}
@@ -755,4 +763,32 @@ class _MixpanelHelper {
755763
// ignore: unnecessary_null_comparison
756764
return input != null && input.isNotEmpty;
757765
}
766+
767+
/// Converts complex types to basic types for web platform
768+
static dynamic ensureSerializableValue(dynamic value) {
769+
if (!kIsWeb) {
770+
return value;
771+
}
772+
if (value == null) {
773+
return null;
774+
} else if (value is DateTime) {
775+
return value.toIso8601String();
776+
} else if (value is Uri) {
777+
return value.toString();
778+
} else if (value is Map) {
779+
return value.map((k, v) => MapEntry(k, ensureSerializableValue(v)));
780+
} else if (value is List) {
781+
return value.map((v) => ensureSerializableValue(v)).toList();
782+
} else {
783+
return value;
784+
}
785+
}
786+
787+
/// Converts properties map for web platform
788+
static Map<String, dynamic>? ensureSerializableProperties(Map<String, dynamic>? properties) {
789+
if (!kIsWeb || properties == null) {
790+
return properties;
791+
}
792+
return properties.map((k, v) => MapEntry(k, ensureSerializableValue(v)));
793+
}
758794
}

lib/mixpanel_flutter_web.dart

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,28 +42,27 @@ import 'package:mixpanel_flutter/web/mixpanel_js_bindings.dart';
4242
/// var jsNull = safeJsify(null); // Returns null
4343
/// ```
4444
JSAny? safeJsify(dynamic value) {
45-
if (value == null) {
46-
return null;
47-
} else if (value is JSAny) {
48-
return value;
49-
} else if (value is Map) {
50-
return value.jsify();
51-
} else if (value is List) {
52-
return value.jsify();
53-
} else if (value is DateTime) {
54-
return value.jsify();
55-
} else if (value is bool) {
56-
return value.toJS;
57-
} else if (value is num) {
58-
return value.toJS;
59-
} else if (value is String) {
60-
return value.toJS;
61-
} else {
62-
debugPrint('[Mixpanel] Warning: Unsupported type for JS conversion: ${value.runtimeType}. '
63-
'Value will be ignored. Supported types are: Map, List, DateTime, bool, num, String, JSAny, and null.');
64-
return null;
65-
}
45+
if (value == null) {
46+
return null;
47+
} else if (value is Map) {
48+
return value.jsify();
49+
} else if (value is List) {
50+
return value.jsify();
51+
} else if (value is DateTime) {
52+
return value.jsify();
53+
} else if (value is bool) {
54+
return value.toJS;
55+
} else if (value is num) {
56+
return value.toJS;
57+
} else if (value is String) {
58+
return value.toJS;
59+
} else {
60+
debugPrint(
61+
'[Mixpanel] Warning: Unsupported type for JS conversion: ${value.runtimeType}. '
62+
'Value will be ignored. Supported types are: Map, List, DateTime, bool, num, String, JSAny, and null.');
63+
return null;
6664
}
65+
}
6766

6867
/// A web implementation of the MixpanelFlutter plugin.
6968
class MixpanelFlutterPlugin {
@@ -73,6 +72,7 @@ class MixpanelFlutterPlugin {
7372
};
7473

7574
static void registerWith(Registrar registrar) {
75+
// Web platform doesn't need the custom codec since safeJsify handles type conversions
7676
final MethodChannel channel = MethodChannel(
7777
'mixpanel_flutter',
7878
const StandardMethodCodec(),
@@ -368,8 +368,7 @@ class MixpanelFlutterPlugin {
368368
dynamic groupID = args['groupID'];
369369

370370
dynamic properties = args['properties'];
371-
get_group(groupKey, safeJsify(groupID))
372-
.set(safeJsify(properties));
371+
get_group(groupKey, safeJsify(groupID)).set(safeJsify(properties));
373372
}
374373

375374
void handleGroupSetPropertyOnce(MethodCall call) {
@@ -389,8 +388,7 @@ class MixpanelFlutterPlugin {
389388
dynamic groupID = args['groupID'];
390389

391390
String propertyName = args['propertyName'] as String;
392-
get_group(groupKey, safeJsify(groupID))
393-
.unset(propertyName);
391+
get_group(groupKey, safeJsify(groupID)).unset(propertyName);
394392
}
395393

396394
void handleGroupRemove(MethodCall call) {
@@ -400,8 +398,7 @@ class MixpanelFlutterPlugin {
400398

401399
String name = args['name'] as String;
402400
dynamic value = args['value'];
403-
get_group(groupKey, safeJsify(groupID))
404-
.remove(name, safeJsify(value));
401+
get_group(groupKey, safeJsify(groupID)).remove(name, safeJsify(value));
405402
}
406403

407404
void handleGroupUnion(MethodCall call) {

0 commit comments

Comments
 (0)