Skip to content

Commit 2dcce5b

Browse files
authored
fix(cli_tools): Prevent analytics send failures from crashing the host process (#125)
Make both providers' `sendEvent` properly async so they return reified `Future<void>`s, and harden `Analytics.track` to swallow errors with try/catch instead of `catchError`, so implementations returning foreign futures cannot crash the process.
1 parent 539e21f commit 2dcce5b

4 files changed

Lines changed: 55 additions & 6 deletions

File tree

‎packages/cli_tools/lib/src/analytics/analytics.dart‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,21 @@ abstract class Analytics {
2121
final Map<String, dynamic> properties = const {},
2222
}) {
2323
late final Future<void> pendingTrack;
24-
pendingTrack = sendEvent(event: event, properties: properties)
25-
.catchError((final _) {})
24+
pendingTrack = _quietSendEvent(event: event, properties: properties)
2625
.whenComplete(() => _pendingTracks.remove(pendingTrack));
2726
_pendingTracks.add(pendingTrack);
2827
}
2928

29+
/// Sends an event, swallowing any errors.
30+
Future<void> _quietSendEvent({
31+
required final String event,
32+
required final Map<String, dynamic> properties,
33+
}) async {
34+
try {
35+
await sendEvent(event: event, properties: properties);
36+
} catch (_) {}
37+
}
38+
3039
/// Send an event to the analytics service.
3140
Future<void> sendEvent({
3241
required final String event,

‎packages/cli_tools/lib/src/analytics/mixpanel.dart‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class MixPanelAnalytics extends Analytics {
5555
Future<void> sendEvent({
5656
required final String event,
5757
final Map<String, dynamic> properties = const {},
58-
}) {
58+
}) async {
5959
final payload = jsonEncode({
6060
'event': event,
6161
'properties': {
@@ -68,7 +68,7 @@ class MixPanelAnalytics extends Analytics {
6868
},
6969
});
7070

71-
return http.post(
71+
await http.post(
7272
_endpoint,
7373
body: 'data=$payload',
7474
headers: {

‎packages/cli_tools/lib/src/analytics/posthog.dart‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class PostHogAnalytics extends Analytics {
3939
Future<void> sendEvent({
4040
required final String event,
4141
final Map<String, dynamic> properties = const {},
42-
}) {
42+
}) async {
4343
final eventData = {
4444
'api_key': _projectApiKey,
4545
'event': event,
@@ -54,7 +54,7 @@ class PostHogAnalytics extends Analytics {
5454
},
5555
};
5656

57-
return http
57+
await http
5858
.post(
5959
_endpoint,
6060
headers: {'Content-Type': 'application/json'},

‎packages/cli_tools/test/analytics/analytics_test.dart‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,26 @@ void main() {
4848
},
4949
);
5050

51+
test(
52+
'Given an analytics implementation whose sendEvent returns a future with a non-void reified type, '
53+
'when the event send fails, '
54+
'then the failure is ignored and flush completes.',
55+
() async {
56+
final analytics = NonVoidFutureAnalytics();
57+
58+
analytics.track(event: 'test');
59+
60+
final flush = expectLater(analytics.flush(), completes);
61+
await flushEventQueue();
62+
63+
analytics.completers.single.completeError(
64+
StateError('Failed to send event.'),
65+
);
66+
67+
await expectLater(flush, completes);
68+
},
69+
);
70+
5171
test(
5272
'Given compound analytics with a provider that fails to flush, '
5373
'when flushing the compound analytics, '
@@ -79,6 +99,26 @@ class PendingAnalytics extends Analytics {
7999
}
80100
}
81101

102+
/// Returns futures from [sendEvent] whose reified type is not `Future<void>`.
103+
///
104+
/// This an example of how NOT to do it. In general don't try to be smart and
105+
/// skip the async on a method returning `Future<void>`. This is just one pitfall
106+
/// of many.
107+
class NonVoidFutureAnalytics extends Analytics {
108+
final completers = <Completer<String>>[];
109+
110+
@override
111+
Future<void> sendEvent /* no async */ ({
112+
required final String event,
113+
final Map<String, dynamic> properties = const {},
114+
}) {
115+
// note the mismatch Future<void> vs Future<String>
116+
final completer = Completer<String>();
117+
completers.add(completer);
118+
return completer.future; // no await
119+
}
120+
}
121+
82122
class FailingFlushAnalytics extends Analytics {
83123
var didFlush = false;
84124

0 commit comments

Comments
 (0)