Skip to content

Commit ddeccf6

Browse files
authored
Handling malformed log items in log file (#175)
* Catch errors for each line and ignore * Adding tests * Additional test with valid json, but missing keys * dart format fix * Additional test for malformed record getting phased out * Update documentation * Added TODO + add error handling for casting errors * Fix format error * Clean up
1 parent 92c5c15 commit ddeccf6

File tree

3 files changed

+203
-4
lines changed

3 files changed

+203
-4
lines changed

pkgs/unified_analytics/lib/src/event.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ final class Event {
158158
/// [transitiveFileUniqueCount] - the number of unique files reachable from
159159
/// the files in each analysis context.
160160
///
161-
/// [transitiveFileUniqueLineCount] - the number of lines in the unique.
162-
/// transitive files
161+
/// [transitiveFileUniqueLineCount] - the number of lines in the unique
162+
/// transitive files.
163163
Event.contextStructure({
164164
required int contextsFromBothFiles,
165165
required int contextsFromOptionsFiles,

pkgs/unified_analytics/lib/src/log_handler.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,19 @@ class LogHandler {
183183
// removed later through `whereType<LogItem>`
184184
final records = logFile
185185
.readAsLinesSync()
186-
.map((String e) =>
187-
LogItem.fromRecord(jsonDecode(e) as Map<String, Object?>))
186+
.map((String e) {
187+
// TODO: eliasyishak, once https://github.com/dart-lang/tools/issues/167
188+
// has landed ensure we are sending an event for each error
189+
// with helpful messages
190+
try {
191+
return LogItem.fromRecord(jsonDecode(e) as Map<String, Object?>);
192+
} on FormatException {
193+
return null;
194+
// ignore: avoid_catching_errors
195+
} on TypeError {
196+
return null;
197+
}
198+
})
188199
.whereType<LogItem>()
189200
.toList();
190201

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:file/file.dart';
6+
import 'package:file/memory.dart';
7+
import 'package:path/path.dart' as p;
8+
import 'package:test/test.dart';
9+
10+
import 'package:unified_analytics/src/constants.dart';
11+
import 'package:unified_analytics/src/enums.dart';
12+
import 'package:unified_analytics/unified_analytics.dart';
13+
14+
void main() {
15+
late Analytics analytics;
16+
late Directory homeDirectory;
17+
late FileSystem fs;
18+
late File logFile;
19+
20+
final testEvent = Event.hotReloadTime(timeMs: 10);
21+
22+
setUp(() {
23+
fs = MemoryFileSystem.test(style: FileSystemStyle.posix);
24+
homeDirectory = fs.directory('home');
25+
logFile = fs.file(p.join(
26+
homeDirectory.path,
27+
kDartToolDirectoryName,
28+
kLogFileName,
29+
));
30+
31+
// Create the initialization analytics instance to onboard the tool
32+
final initializationAnalytics = Analytics.test(
33+
tool: DashTool.flutterTool,
34+
homeDirectory: homeDirectory,
35+
measurementId: 'measurementId',
36+
apiSecret: 'apiSecret',
37+
dartVersion: 'dartVersion',
38+
fs: fs,
39+
platform: DevicePlatform.macos,
40+
);
41+
initializationAnalytics.clientShowedMessage();
42+
43+
// This instance is free to send events since the instance above
44+
// has confirmed that the client has shown the message
45+
analytics = Analytics.test(
46+
tool: DashTool.flutterTool,
47+
homeDirectory: homeDirectory,
48+
measurementId: 'measurementId',
49+
apiSecret: 'apiSecret',
50+
dartVersion: 'dartVersion',
51+
fs: fs,
52+
platform: DevicePlatform.macos,
53+
);
54+
});
55+
56+
test('Ensure that log file is created', () {
57+
expect(logFile.existsSync(), true);
58+
});
59+
60+
test('LogFileStats is null before events are sent', () {
61+
expect(analytics.logFileStats(), isNull);
62+
});
63+
64+
test('LogFileStats returns valid response after sent events', () async {
65+
final countOfEventsToSend = 10;
66+
67+
for (var i = 0; i < countOfEventsToSend; i++) {
68+
await analytics.send(testEvent);
69+
}
70+
71+
expect(analytics.logFileStats(), isNotNull);
72+
expect(logFile.readAsLinesSync().length, countOfEventsToSend);
73+
expect(analytics.logFileStats()!.recordCount, countOfEventsToSend);
74+
});
75+
76+
test('The only record in the log file is malformed', () {
77+
// Write invalid json for the only log record
78+
logFile.writeAsStringSync('{{\n');
79+
80+
final logFileStats = analytics.logFileStats();
81+
expect(logFile.readAsLinesSync().length, 1);
82+
expect(logFileStats, isNull,
83+
reason: 'Null should be returned since only '
84+
'one record is in there and it is malformed');
85+
});
86+
87+
test('The first record is malformed, but rest are valid', () async {
88+
// Write invalid json for the only log record
89+
logFile.writeAsStringSync('{{\n');
90+
91+
final countOfEventsToSend = 10;
92+
93+
for (var i = 0; i < countOfEventsToSend; i++) {
94+
await analytics.send(testEvent);
95+
}
96+
final logFileStats = analytics.logFileStats();
97+
98+
expect(logFile.readAsLinesSync().length, countOfEventsToSend + 1);
99+
expect(logFileStats, isNotNull);
100+
expect(logFileStats!.recordCount, countOfEventsToSend);
101+
});
102+
103+
test('Several records are malformed', () async {
104+
final countOfMalformedRecords = 4;
105+
for (var i = 0; i < countOfMalformedRecords; i++) {
106+
final currentContents = logFile.readAsStringSync();
107+
logFile.writeAsStringSync('$currentContents{{\n');
108+
}
109+
110+
final countOfEventsToSend = 10;
111+
112+
for (var i = 0; i < countOfEventsToSend; i++) {
113+
await analytics.send(testEvent);
114+
}
115+
final logFileStats = analytics.logFileStats();
116+
117+
expect(logFile.readAsLinesSync().length,
118+
countOfEventsToSend + countOfMalformedRecords);
119+
expect(logFileStats, isNotNull);
120+
expect(logFileStats!.recordCount, countOfEventsToSend);
121+
});
122+
123+
test('Valid json but invalid keys', () {
124+
// The second line here is missing the "events" top level
125+
// key which should cause an error for that record only
126+
//
127+
// Important to note that this won't actually cause a FormatException
128+
// like the other malformed records, instead the LogItem.fromRecord
129+
// constructor will return null if all the keys are not available
130+
final contents = '''
131+
{"client_id":"fe4a035b-bba8-4d4b-a651-ea213e9b8a2c","events":[{"name":"lint_usage_count","params":{"count":1,"name":"prefer_final_fields"}}],"user_properties":{"session_id":{"value":1695147041117},"flutter_channel":{"value":null},"host":{"value":"macOS"},"flutter_version":{"value":"3.14.0-14.0.pre.303"},"dart_version":{"value":"3.2.0-140.0.dev"},"analytics_pkg_version":{"value":"3.1.0"},"tool":{"value":"vscode-plugins"},"local_time":{"value":"2023-09-19 14:44:11.528153 -0400"}}}
132+
{"client_id":"fe4a035b-bba8-4d4b-a651-ea213e9b8a2c","WRONG_EVENT_KEY":[{"name":"lint_usage_count","params":{"count":1,"name":"prefer_for_elements_to_map_fromIterable"}}],"user_properties":{"session_id":{"value":1695147041117},"flutter_channel":{"value":null},"host":{"value":"macOS"},"flutter_version":{"value":"3.14.0-14.0.pre.303"},"dart_version":{"value":"3.2.0-140.0.dev"},"analytics_pkg_version":{"value":"3.1.0"},"tool":{"value":"vscode-plugins"},"local_time":{"value":"2023-09-19 14:44:11.565549 -0400"}}}
133+
{"client_id":"fe4a035b-bba8-4d4b-a651-ea213e9b8a2c","events":[{"name":"lint_usage_count","params":{"count":1,"name":"prefer_function_declarations_over_variables"}}],"user_properties":{"session_id":{"value":1695147041117},"flutter_channel":{"value":null},"host":{"value":"macOS"},"flutter_version":{"value":"3.14.0-14.0.pre.303"},"dart_version":{"value":"3.2.0-140.0.dev"},"analytics_pkg_version":{"value":"3.1.0"},"tool":{"value":"vscode-plugins"},"local_time":{"value":"2023-09-19 14:44:11.589338 -0400"}}}
134+
''';
135+
logFile.writeAsStringSync(contents);
136+
137+
final logFileStats = analytics.logFileStats();
138+
139+
expect(logFile.readAsLinesSync().length, 3);
140+
expect(logFileStats, isNotNull);
141+
expect(logFileStats!.recordCount, 2);
142+
});
143+
144+
test('Malformed record gets phased out after several events', () async {
145+
// Write invalid json for the only log record
146+
logFile.writeAsStringSync('{{\n');
147+
148+
// Send the max number of events minus one so that we have
149+
// one malformed record on top of the logs and the rest
150+
// are valid log records
151+
for (var i = 0; i < kLogFileLength - 1; i++) {
152+
await analytics.send(testEvent);
153+
}
154+
final logFileStats = analytics.logFileStats();
155+
expect(logFile.readAsLinesSync().length, kLogFileLength);
156+
expect(logFileStats, isNotNull);
157+
expect(logFileStats!.recordCount, kLogFileLength - 1,
158+
reason: 'The first record should be malformed');
159+
expect(logFile.readAsLinesSync()[0].trim(), '{{');
160+
161+
// Sending one more event should flush out the malformed record
162+
await analytics.send(testEvent);
163+
164+
final secondLogFileStats = analytics.logFileStats();
165+
expect(secondLogFileStats, isNotNull);
166+
expect(secondLogFileStats!.recordCount, kLogFileLength);
167+
expect(logFile.readAsLinesSync()[0].trim(), isNot('{{'));
168+
});
169+
170+
test('Catching cast errors for each log record silently', () async {
171+
// Write a json array to the log file which will cause
172+
// a cast error when parsing each line
173+
logFile.writeAsStringSync('[{}, 1, 2, 3]\n');
174+
175+
final logFileStats = analytics.logFileStats();
176+
expect(logFileStats, isNull);
177+
178+
// Ensure it will work as expected after writing correct logs
179+
final countOfEventsToSend = 10;
180+
for (var i = 0; i < countOfEventsToSend; i++) {
181+
await analytics.send(testEvent);
182+
}
183+
final secondLogFileStats = analytics.logFileStats();
184+
185+
expect(secondLogFileStats, isNotNull);
186+
expect(secondLogFileStats!.recordCount, countOfEventsToSend);
187+
});
188+
}

0 commit comments

Comments
 (0)