Adding more information to `LogFileStats` + minor updates to tests (#31)
* Adding duration for start and end times + record count
* Update USAGE_GUIDE.md
* Update to include counts for each event in LogFileStats
* Update CHANGELOG.md
* Use package:clock for LogFileStats calculations
* Update to test to include newly added data points
* Added test to check CHANGELOG for matching versions
* Prep for publishing
* Remove redundant expect statement
* Ensure that no events are being sent for the first run + test
* Fixing tests to account for no events sent on tool first run
* Clean up from dart format
* dart format on test directory
* Use one clock for `now`
* Including formatDateTime to include timezone offset
* Updating documentation of `local_time`
diff --git a/pkgs/unified_analytics/CHANGELOG.md b/pkgs/unified_analytics/CHANGELOG.md
index 22bc528..347a36a 100644
--- a/pkgs/unified_analytics/CHANGELOG.md
+++ b/pkgs/unified_analytics/CHANGELOG.md
@@ -1,6 +1,7 @@
-## 0.1.1-dev
+## 0.1.1
- Bumping intl package to 0.18.0 to fix version solving issue with flutter_tools
+- LogFileStats includes more information about how many events are persisted and total count of how many times each event was sent
## 0.1.0
diff --git a/pkgs/unified_analytics/USAGE_GUIDE.md b/pkgs/unified_analytics/USAGE_GUIDE.md
index 1040637..ae1fca1 100644
--- a/pkgs/unified_analytics/USAGE_GUIDE.md
+++ b/pkgs/unified_analytics/USAGE_GUIDE.md
@@ -156,18 +156,30 @@
// Prints out the below
// {
-// "startDateTime": "2023-02-08 15:07:10.293728",
-// "endDateTime": "2023-02-08 15:07:10.299678",
-// "sessionCount": 1,
-// "flutterChannelCount": 1,
-// "toolCount": 1
+// "startDateTime": "2023-02-22 15:23:24.410921",
+// "minsFromStartDateTime": 20319,
+// "endDateTime": "2023-03-08 15:46:36.318211",
+// "minsFromEndDateTime": 136,
+// "sessionCount": 7,
+// "flutterChannelCount": 2,
+// "toolCount": 1,
+// "recordCount": 23,
+// "eventCount": {
+// "hot_reload_time": 16,
+// "analytics_collection_enabled": 7,
+// ... scales up with number of events
+// }
// }
```
Explanation of the each key above
- startDateTime: the earliest event that was sent
+- minsFromStartDateTime: the number of minutes elapsed since the earliest message
- endDateTime: the latest, most recent event that was sent
+- minsFromEndDateTime: the number of minutes elapsed since the latest message
- sessionCount: count of sessions; sessions have a minimum time of 30 minutes
- flutterChannelCount: count of flutter channels (can be 0 if developer is a Dart dev only)
- toolCount: count of the Dart and Flutter tools sending analytics
+- recordCount: count of the total number of events in the log file
+- eventCount: counts each unique event and how many times they occurred in the log file
\ No newline at end of file
diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart
index 67a782c..fa1f510 100644
--- a/pkgs/unified_analytics/lib/src/analytics.dart
+++ b/pkgs/unified_analytics/lib/src/analytics.dart
@@ -246,7 +246,14 @@
required DashEvent eventName,
Map<String, Object?> eventData = const {},
}) {
- if (!telemetryEnabled) return null;
+ // Checking the [telemetryEnabled] boolean reflects what the
+ // config file reflects
+ //
+ // Checking the [_showMessage] boolean indicates if this the first
+ // time the tool is using analytics or if there has been an update
+ // the messaging found in constants.dart - in both cases, analytics
+ // will not be sent until the second time the tool is used
+ if (!telemetryEnabled || _showMessage) return null;
// Construct the body of the request
final Map<String, Object?> body = generateRequestBody(
@@ -311,7 +318,7 @@
required DashEvent eventName,
Map<String, Object?> eventData = const {},
}) {
- if (!telemetryEnabled) return null;
+ if (!telemetryEnabled || _showMessage) return null;
// Calling the [generateRequestBody] method will ensure that the
// session file is getting updated without actually making any
diff --git a/pkgs/unified_analytics/lib/src/constants.dart b/pkgs/unified_analytics/lib/src/constants.dart
index b1006f8..51155d0 100644
--- a/pkgs/unified_analytics/lib/src/constants.dart
+++ b/pkgs/unified_analytics/lib/src/constants.dart
@@ -70,7 +70,7 @@
const String kLogFileName = 'dash-analytics.log';
/// The current version of the package, should be in line with pubspec version.
-const String kPackageVersion = '0.1.1-dev';
+const String kPackageVersion = '0.1.1';
/// The minimum length for a session
const int kSessionDurationMinutes = 30;
diff --git a/pkgs/unified_analytics/lib/src/log_handler.dart b/pkgs/unified_analytics/lib/src/log_handler.dart
index 872fe31..2caf2b5 100644
--- a/pkgs/unified_analytics/lib/src/log_handler.dart
+++ b/pkgs/unified_analytics/lib/src/log_handler.dart
@@ -4,6 +4,7 @@
import 'dart:convert';
+import 'package:clock/clock.dart';
import 'package:file/file.dart';
import 'package:path/path.dart' as p;
@@ -16,9 +17,15 @@
/// The oldest timestamp in the log file
final DateTime startDateTime;
+ /// Number of minutes from [startDateTime] to [clock.now()]
+ final int minsFromStartDateTime;
+
/// The latest timestamp in the log file
final DateTime endDateTime;
+ /// Number of minutes from [endDateTime] to [clock.now()]
+ final int minsFromEndDateTime;
+
/// The number of unique session ids found in the log file
final int sessionCount;
@@ -28,22 +35,37 @@
/// The number of unique tools found in the log file
final int toolCount;
+ /// The map containing all of the events in the file along with
+ /// how many times they have occured
+ final Map<String, int> eventCount;
+
+ /// Total number of records in the log file
+ final int recordCount;
+
/// Contains the data from the [LogHandler.logFileStats] method
const LogFileStats({
required this.startDateTime,
+ required this.minsFromStartDateTime,
required this.endDateTime,
+ required this.minsFromEndDateTime,
required this.sessionCount,
required this.flutterChannelCount,
required this.toolCount,
+ required this.recordCount,
+ required this.eventCount,
});
@override
String toString() => jsonEncode(<String, Object?>{
'startDateTime': startDateTime.toString(),
+ 'minsFromStartDateTime': minsFromStartDateTime,
'endDateTime': endDateTime.toString(),
+ 'minsFromEndDateTime': minsFromEndDateTime,
'sessionCount': sessionCount,
'flutterChannelCount': flutterChannelCount,
'toolCount': toolCount,
+ 'recordCount': recordCount,
+ 'eventCount': eventCount,
});
}
@@ -89,26 +111,42 @@
final DateTime startDateTime = records.first.localTime;
final DateTime endDateTime = records.last.localTime;
- // Collection of unique sessions
+ // Map with counters for user properties
final Map<String, Set<Object>> counter = <String, Set<Object>>{
'sessions': <int>{},
'flutter_channel': <String>{},
'tool': <String>{},
};
+
+ // Map of counters for each event
+ final Map<String, int> eventCount = <String, int>{};
for (LogItem record in records) {
counter['sessions']!.add(record.sessionId);
counter['tool']!.add(record.tool);
if (record.flutterChannel != null) {
counter['flutter_channel']!.add(record.flutterChannel!);
}
+
+ // Count each event, if it doesn't exist in the [eventCount]
+ // it will be added first
+ if (!eventCount.containsKey(record.eventName)) {
+ eventCount[record.eventName] = 0;
+ }
+ eventCount[record.eventName] = eventCount[record.eventName]! + 1;
}
+ final DateTime now = clock.now();
+
return LogFileStats(
startDateTime: startDateTime,
+ minsFromStartDateTime: now.difference(startDateTime).inMinutes,
endDateTime: endDateTime,
+ minsFromEndDateTime: now.difference(endDateTime).inMinutes,
sessionCount: counter['sessions']!.length,
flutterChannelCount: counter['flutter_channel']!.length,
toolCount: counter['tool']!.length,
+ eventCount: eventCount,
+ recordCount: records.length,
);
}
@@ -135,6 +173,7 @@
/// Data class for each record persisted on the client's machine
class LogItem {
+ final String eventName;
final int sessionId;
final String? flutterChannel;
final String host;
@@ -144,6 +183,7 @@
final DateTime localTime;
LogItem({
+ required this.eventName,
required this.sessionId,
this.flutterChannel,
required this.host,
@@ -194,18 +234,27 @@
/// "value": "flutter-tools"
/// },
/// "local_time": {
- /// "value": "2023-01-31 14:32:14.592898"
+ /// "value": "2023-01-31 14:32:14.592898 -0500"
/// }
/// }
/// }
/// ```
static LogItem? fromRecord(Map<String, Object?> record) {
- if (!record.containsKey('user_properties')) return null;
+ if (!record.containsKey('user_properties') ||
+ !record.containsKey('events')) {
+ return null;
+ }
// Using a try/except here to parse out the fields if possible,
// if not, it will quietly return null and won't get processed
// downstream
try {
+ // Parse out values from the top level key = 'events' and return
+ // a map for the one event in the value
+ final Map<String, Object?> eventProp =
+ ((record['events']! as List<Object?>).first as Map<String, Object?>);
+ final String eventName = eventProp['name'] as String;
+
// Parse the data out of the `user_properties` value
final Map<String, Object?> userProps =
record['user_properties'] as Map<String, Object?>;
@@ -230,6 +279,10 @@
// indicates the record is malformed; note that `flutter_version`
// and `flutter_channel` are nullable fields in the log file
final List<Object?> values = <Object?>[
+ // Values associated with the top level key = 'events'
+ eventName,
+
+ // Values associated with the top level key = 'events'
sessionId,
host,
dartVersion,
@@ -241,9 +294,10 @@
}
// Parse the local time from the string extracted
- final DateTime localTime = DateTime.parse(localTimeString!);
+ final DateTime localTime = DateTime.parse(localTimeString!).toLocal();
return LogItem(
+ eventName: eventName,
sessionId: sessionId!,
flutterChannel: flutterChannel,
host: host!,
diff --git a/pkgs/unified_analytics/lib/src/user_property.dart b/pkgs/unified_analytics/lib/src/user_property.dart
index ee82e62..b85870e 100644
--- a/pkgs/unified_analytics/lib/src/user_property.dart
+++ b/pkgs/unified_analytics/lib/src/user_property.dart
@@ -8,6 +8,7 @@
import 'constants.dart';
import 'session.dart';
+import 'utils.dart';
class UserProperty {
final Session session;
@@ -58,6 +59,6 @@
'dart_version': dartVersion,
'analytics_pkg_version': kPackageVersion,
'tool': tool,
- 'local_time': '${clock.now()}',
+ 'local_time': formatDateTime(clock.now()),
};
}
diff --git a/pkgs/unified_analytics/lib/src/utils.dart b/pkgs/unified_analytics/lib/src/utils.dart
index f007ecb..e9b5562 100644
--- a/pkgs/unified_analytics/lib/src/utils.dart
+++ b/pkgs/unified_analytics/lib/src/utils.dart
@@ -10,6 +10,21 @@
import 'enums.dart';
import 'user_property.dart';
+/// Format time as 'yyyy-MM-dd HH:mm:ss Z' where Z is the difference between the
+/// timezone of t and UTC formatted according to RFC 822.
+String formatDateTime(DateTime t) {
+ final String sign = t.timeZoneOffset.isNegative ? '-' : '+';
+ final Duration tzOffset = t.timeZoneOffset.abs();
+ final int hoursOffset = tzOffset.inHours;
+ final int minutesOffset =
+ tzOffset.inMinutes - (Duration.minutesPerHour * hoursOffset);
+ assert(hoursOffset < 24);
+ assert(minutesOffset < 60);
+
+ String twoDigits(int n) => (n >= 10) ? '$n' : '0$n';
+ return '$t $sign${twoDigits(hoursOffset)}${twoDigits(minutesOffset)}';
+}
+
/// Construct the Map that will be converted to json for the
/// body of the request
///
@@ -26,7 +41,7 @@
/// "flutter_version": { "value": "Flutter 3.6.0-7.0.pre.47" },
/// "dart_version": { "value": "Dart 2.19.0" },
/// "tool": { "value": "flutter-tools" },
-/// "local_time": { "value": "2023-01-11 14:53:31.471816" }
+/// "local_time": { "value": "2023-01-11 14:53:31.471816 -0500" }
/// }
/// }
/// ```
diff --git a/pkgs/unified_analytics/pubspec.yaml b/pkgs/unified_analytics/pubspec.yaml
index e5d0a16..7f14902 100644
--- a/pkgs/unified_analytics/pubspec.yaml
+++ b/pkgs/unified_analytics/pubspec.yaml
@@ -4,7 +4,7 @@
to Google Analytics.
# When updating this, keep the version consistent with the changelog and the
# value in lib/src/constants.dart.
-version: 0.1.1-dev
+version: 0.1.1
repository: https://github.com/dart-lang/tools/tree/main/pkgs/unified_analytics
environment:
diff --git a/pkgs/unified_analytics/test/unified_analytics_test.dart b/pkgs/unified_analytics/test/unified_analytics_test.dart
index 95ef536..ed2bd9a 100644
--- a/pkgs/unified_analytics/test/unified_analytics_test.dart
+++ b/pkgs/unified_analytics/test/unified_analytics_test.dart
@@ -22,6 +22,7 @@
late FileSystem fs;
late Directory home;
late Directory dartToolDirectory;
+ late Analytics initializationAnalytics;
late Analytics analytics;
late File clientIdFile;
late File sessionFile;
@@ -49,8 +50,27 @@
home = fs.directory(homeDirName);
dartToolDirectory = home.childDirectory(kDartToolDirectoryName);
+ // This is the first analytics instance that will be used to demonstrate
+ // that events will not be sent with the first run of analytics
+ initializationAnalytics = Analytics.test(
+ tool: initialToolName,
+ homeDirectory: home,
+ measurementId: measurementId,
+ apiSecret: apiSecret,
+ flutterChannel: flutterChannel,
+ toolsMessageVersion: toolsMessageVersion,
+ toolsMessage: toolsMessage,
+ flutterVersion: flutterVersion,
+ dartVersion: dartVersion,
+ fs: fs,
+ platform: platform,
+ );
+
// The main analytics instance, other instances can be spawned within tests
// to test how to instances running together work
+ //
+ // This instance should have the same parameters as the one above for
+ // [initializationAnalytics]
analytics = Analytics.test(
tool: initialToolName,
homeDirectory: home,
@@ -102,7 +122,7 @@
expect(dartToolDirectory.listSync().length, equals(4),
reason:
'There should only be 4 files in the $kDartToolDirectoryName directory');
- expect(analytics.shouldShowMessage, true,
+ expect(initializationAnalytics.shouldShowMessage, true,
reason: 'For the first run, analytics should default to being enabled');
expect(configFile.readAsLinesSync().length,
kConfigString.split('\n').length + 1,
@@ -138,6 +158,24 @@
'The config file should have the same message from the constants file');
});
+ test('First time analytics run will not send events, second time will', () {
+ // Send an event with the first analytics class; this should result
+ // in no logs in the log file which keeps track of all the events
+ // that have been sent
+ initializationAnalytics.sendEvent(
+ eventName: DashEvent.hotReloadTime, eventData: <String, dynamic>{});
+ initializationAnalytics.sendEvent(
+ eventName: DashEvent.hotReloadTime, eventData: <String, dynamic>{});
+
+ // Use the second instance of analytics defined in setUp() to send the actual
+ // events to simulate the second time the tool ran
+ analytics.sendEvent(
+ eventName: DashEvent.hotReloadTime, eventData: <String, dynamic>{});
+
+ expect(logFile.readAsLinesSync().length, 1,
+ reason: 'The second analytics instance should have logged an event');
+ });
+
test('Toggling telemetry boolean through Analytics class api', () async {
expect(analytics.telemetryEnabled, true,
reason: 'Telemetry should be enabled by default '
@@ -699,55 +737,96 @@
});
test('Check the query on the log file works as expected', () {
- expect(analytics.logFileStats(), isNull,
- reason: 'The result for the log file stats should be null when '
- 'there are no logs');
- analytics.sendEvent(
- eventName: DashEvent.hotReloadTime, eventData: <String, dynamic>{});
+ // Define a new clock so that we can check the output of the
+ // log file stats method explicitly
+ final DateTime start = DateTime(1995, 3, 3, 12, 0);
+ final Clock firstClock = Clock.fixed(start);
- final LogFileStats firstQuery = analytics.logFileStats()!;
- expect(firstQuery.sessionCount, 1,
- reason:
- 'There should only be one session after the initial send event');
- expect(firstQuery.flutterChannelCount, 1,
- reason: 'There should only be one flutter channel logged');
- expect(firstQuery.toolCount, 1,
- reason: 'There should only be one tool logged');
-
- // Define a new clock that is outside of the session duration
- final DateTime firstClock =
- clock.now().add(Duration(minutes: kSessionDurationMinutes + 1));
-
- // Use the new clock to send an event that will change the session identifier
- withClock(Clock.fixed(firstClock), () {
+ // Run with the simulated clock for the initial events
+ withClock(firstClock, () {
+ expect(analytics.logFileStats(), isNull,
+ reason: 'The result for the log file stats should be null when '
+ 'there are no logs');
analytics.sendEvent(
eventName: DashEvent.hotReloadTime, eventData: <String, dynamic>{});
+
+ final LogFileStats firstQuery = analytics.logFileStats()!;
+ expect(firstQuery.sessionCount, 1,
+ reason:
+ 'There should only be one session after the initial send event');
+ expect(firstQuery.flutterChannelCount, 1,
+ reason: 'There should only be one flutter channel logged');
+ expect(firstQuery.toolCount, 1,
+ reason: 'There should only be one tool logged');
});
- final LogFileStats secondQuery = analytics.logFileStats()!;
- expect(secondQuery.sessionCount, 2,
- reason: 'There should be 2 sessions after the second event');
+ // Define a new clock that is outside of the session duration
+ final DateTime secondClock =
+ start.add(Duration(minutes: kSessionDurationMinutes + 1));
+
+ // Use the new clock to send an event that will change the session identifier
+ withClock(Clock.fixed(secondClock), () {
+ analytics.sendEvent(
+ eventName: DashEvent.hotReloadTime, eventData: <String, dynamic>{});
+
+ final LogFileStats secondQuery = analytics.logFileStats()!;
+
+ // Construct the expected response for the second query
+ //
+ // This will need to be updated as the output for [LogFileStats]
+ // changes in the future
+ //
+ // Expecting the below returned
+ // {
+ // "startDateTime": "1995-03-03 12:00:00.000",
+ // "minsFromStartDateTime": 31,
+ // "endDateTime": "1995-03-03 12:31:00.000",
+ // "minsFromEndDateTime": 0,
+ // "sessionCount": 2,
+ // "flutterChannelCount": 1,
+ // "toolCount": 1,
+ // "recordCount": 2,
+ // "eventCount": {
+ // "hot_reload_time": 2
+ // }
+ // }
+ expect(secondQuery.startDateTime, DateTime(1995, 3, 3, 12, 0));
+ expect(secondQuery.minsFromStartDateTime, 31);
+ expect(secondQuery.endDateTime, DateTime(1995, 3, 3, 12, 31));
+ expect(secondQuery.minsFromEndDateTime, 0);
+ expect(secondQuery.sessionCount, 2);
+ expect(secondQuery.flutterChannelCount, 1);
+ expect(secondQuery.toolCount, 1);
+ expect(secondQuery.recordCount, 2);
+ expect(secondQuery.eventCount, <String, int>{'hot_reload_time': 2});
+ });
});
test('Check that the log file shows two different tools being used', () {
- final Analytics secondAnalytics = Analytics.test(
- tool: secondTool,
- homeDirectory: home,
- measurementId: 'measurementId',
- apiSecret: 'apiSecret',
- flutterChannel: flutterChannel,
- toolsMessageVersion: toolsMessageVersion,
- toolsMessage: toolsMessage,
- flutterVersion: 'Flutter 3.6.0-7.0.pre.47',
- dartVersion: 'Dart 2.19.0',
- fs: fs,
- platform: platform,
- );
+ // Use a for loop two initialize the second analytics instance
+ // twice to account for no events being sent on the first instance
+ // run for a given tool
+ Analytics? secondAnalytics;
+ for (int i = 0; i < 2; i++) {
+ secondAnalytics = Analytics.test(
+ tool: secondTool,
+ homeDirectory: home,
+ measurementId: 'measurementId',
+ apiSecret: 'apiSecret',
+ flutterChannel: flutterChannel,
+ toolsMessageVersion: toolsMessageVersion,
+ toolsMessage: toolsMessage,
+ flutterVersion: 'Flutter 3.6.0-7.0.pre.47',
+ dartVersion: 'Dart 2.19.0',
+ fs: fs,
+ platform: platform,
+ );
+ }
// Send events with both instances of the classes
analytics.sendEvent(
eventName: DashEvent.hotReloadTime, eventData: <String, dynamic>{});
- secondAnalytics.sendEvent(
+ secondAnalytics!.sendEvent(
eventName: DashEvent.hotReloadTime, eventData: <String, dynamic>{});
// Query the log file stats to verify that there are two tools
@@ -769,7 +848,7 @@
'"flutter_version":{"value":"Flutter 3.6.0-7.0.pre.47"},'
'"dart_version":{"value":"Dart 2.19.0"},'
// '"tool":{"value":"flutter-tools"},' NEEDS REMAIN REMOVED
- '"local_time":{"value":"2023-01-31 14:32:14.592898"}}}';
+ '"local_time":{"value":"2023-01-31 14:32:14.592898 -0500"}}}';
logFile.writeAsStringSync(malformedLog);
final LogFileStats? query = analytics.logFileStats();
@@ -791,7 +870,7 @@
'"flutter_version":{"value":"Flutter 3.6.0-7.0.pre.47"},'
'"dart_version":{"value":"Dart 2.19.0"},'
'"tool":{"value":"flutter-tools"},'
- '"local_time":{"value":"2023-xx-31 14:32:14.592898"}}}'; // PURPOSEFULLY MALFORMED
+ '"local_time":{"value":"2023-xx-31 14:32:14.592898 -0500"}}}'; // PURPOSEFULLY MALFORMED
logFile.writeAsStringSync(malformedLog);
final LogFileStats? query = analytics.logFileStats();
@@ -801,7 +880,8 @@
'The query should be null because the `local_time` value is malformed');
});
- test('Check that the constant kPackageVersion matches pubspec version', () {
+ test('Version is the same in the change log, pubspec, and constants.dart',
+ () {
// Parse the contents of the pubspec.yaml
final String pubspecYamlString = io.File('pubspec.yaml').readAsStringSync();
@@ -814,26 +894,39 @@
'constants.dart need to match\n'
'Pubspec: $version && constants.dart: $kPackageVersion\n\n'
'Make sure both are the same');
+
+ // Parse the contents of the change log file
+ final String changeLogFirstLineString =
+ io.File('CHANGELOG.md').readAsLinesSync().first;
+ expect(changeLogFirstLineString.substring(3), kPackageVersion,
+ reason: 'The CHANGELOG.md file needs the first line to '
+ 'be the same version as the pubspec and constants.dart');
});
test('Null values for flutter parameters is reflected properly in log file',
() {
- final Analytics secondAnalytics = Analytics.test(
- tool: secondTool,
- homeDirectory: home,
- measurementId: 'measurementId',
- apiSecret: 'apiSecret',
- // flutterChannel: flutterChannel, THIS NEEDS TO REMAIN REMOVED
- // toolsMessageVersion: toolsMessageVersion, THIS NEEDS TO REMAIN REMOVED
- toolsMessage: toolsMessage,
- flutterVersion: 'Flutter 3.6.0-7.0.pre.47',
- dartVersion: 'Dart 2.19.0',
- fs: fs,
- platform: platform,
- );
+ // Use a for loop two initialize the second analytics instance
+ // twice to account for no events being sent on the first instance
+ // run for a given tool
+ Analytics? secondAnalytics;
+ for (int i = 0; i < 2; i++) {
+ secondAnalytics = Analytics.test(
+ tool: secondTool,
+ homeDirectory: home,
+ measurementId: 'measurementId',
+ apiSecret: 'apiSecret',
+ // flutterChannel: flutterChannel, THIS NEEDS TO REMAIN REMOVED
+ toolsMessageVersion: toolsMessageVersion,
+ toolsMessage: toolsMessage,
+ flutterVersion: 'Flutter 3.6.0-7.0.pre.47',
+ dartVersion: 'Dart 2.19.0',
+ fs: fs,
+ platform: platform,
+ );
+ }
// Send an event and check that the query stats reflects what is expected
- secondAnalytics.sendEvent(
+ secondAnalytics!.sendEvent(
eventName: DashEvent.hotReloadTime, eventData: <String, dynamic>{});
// Query the log file stats to verify that there are two tools