[flutter_tool] Send the local time to analytics with screens and events (#36545)

diff --git a/packages/flutter_tools/lib/src/reporting/usage.dart b/packages/flutter_tools/lib/src/reporting/usage.dart
index 2cd2138..ea2c739 100644
--- a/packages/flutter_tools/lib/src/reporting/usage.dart
+++ b/packages/flutter_tools/lib/src/reporting/usage.dart
@@ -12,6 +12,7 @@
 import '../base/file_system.dart';
 import '../base/os.dart';
 import '../base/platform.dart';
+import '../base/time.dart';
 import '../base/utils.dart';
 import '../features.dart';
 import '../globals.dart';
@@ -19,6 +20,9 @@
 
 const String _kFlutterUA = 'UA-67589403-6';
 
+// Attached to all `Usage.sendCommand` and `Usage.sendEvent`.
+const String _kLocalTimeParameter = 'cd33';
+
 const String kSessionHostOsDetails = 'cd1';
 const String kSessionChannelName = 'cd2';
 
@@ -59,7 +63,7 @@
 const String reloadExceptionFullRestart = 'cd30';
 
 const String enabledFlutterFeatures = 'cd32';
-// Next ID: cd33
+// Next ID: cd34
 
 Usage get flutterUsage => Usage.instance;
 
@@ -141,12 +145,16 @@
   String get clientId => _analytics.clientId;
 
   void sendCommand(String command, { Map<String, String> parameters }) {
-    if (suppressAnalytics)
+    if (suppressAnalytics) {
       return;
+    }
 
-    parameters ??= const <String, String>{};
+    final Map<String, String> paramsWithLocalTime = <String, String>{
+      ...?parameters,
+      _kLocalTimeParameter: systemClock.now().toString(),
+    };
 
-    _analytics.sendScreenView(command, parameters: parameters);
+    _analytics.sendScreenView(command, parameters: paramsWithLocalTime);
   }
 
   void sendEvent(
@@ -154,12 +162,16 @@
     String parameter, {
     Map<String, String> parameters,
   }) {
-    if (suppressAnalytics)
+    if (suppressAnalytics) {
       return;
+    }
 
-    parameters ??= const <String, String>{};
+    final Map<String, String> paramsWithLocalTime = <String, String>{
+      ...?parameters,
+      _kLocalTimeParameter: systemClock.now().toString(),
+    };
 
-    _analytics.sendEvent(category, parameter, parameters: parameters);
+    _analytics.sendEvent(category, parameter, parameters: paramsWithLocalTime);
   }
 
   void sendTiming(
@@ -168,19 +180,22 @@
     Duration duration, {
     String label,
   }) {
-    if (!suppressAnalytics) {
-      _analytics.sendTiming(
-        variableName,
-        duration.inMilliseconds,
-        category: category,
-        label: label,
-      );
+    if (suppressAnalytics) {
+      return;
     }
+    _analytics.sendTiming(
+      variableName,
+      duration.inMilliseconds,
+      category: category,
+      label: label,
+    );
   }
 
   void sendException(dynamic exception) {
-    if (!suppressAnalytics)
-      _analytics.sendException(exception.runtimeType.toString());
+    if (suppressAnalytics) {
+      return;
+    }
+    _analytics.sendException(exception.runtimeType.toString());
   }
 
   /// Fires whenever analytics data is sent over the network.
@@ -199,8 +214,9 @@
   void printWelcome() {
     // This gets called if it's the first run by the selected command, if any,
     // and on exit, in case there was no command.
-    if (_printedWelcome)
+    if (_printedWelcome) {
       return;
+    }
     _printedWelcome = true;
 
     printStatus('');
@@ -239,7 +255,17 @@
   Future<void> sendScreenView(String viewName, {Map<String, String> parameters}) {
     parameters ??= <String, String>{};
     parameters['viewName'] = viewName;
-    logFile.writeAsStringSync('screenView $parameters\n');
+    logFile.writeAsStringSync('screenView $parameters\n', mode: FileMode.append);
+    return Future<void>.value(null);
+  }
+
+  @override
+  Future<void> sendEvent(String category, String action,
+      {String label, int value, Map<String, String> parameters}) {
+    parameters ??= <String, String>{};
+    parameters['category'] = category;
+    parameters['action'] = action;
+    logFile.writeAsStringSync('event $parameters\n', mode: FileMode.append);
     return Future<void>.value(null);
   }
 }
diff --git a/packages/flutter_tools/test/general.shard/analytics_test.dart b/packages/flutter_tools/test/general.shard/analytics_test.dart
index 754ace0..d4b75b5 100644
--- a/packages/flutter_tools/test/general.shard/analytics_test.dart
+++ b/packages/flutter_tools/test/general.shard/analytics_test.dart
@@ -3,23 +3,26 @@
 // found in the LICENSE file.
 
 import 'package:args/command_runner.dart';
+import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/config.dart';
-import 'package:flutter_tools/src/base/time.dart';
-import 'package:flutter_tools/src/features.dart';
-import 'package:mockito/mockito.dart';
-
 import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/time.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/build.dart';
 import 'package:flutter_tools/src/commands/config.dart';
 import 'package:flutter_tools/src/commands/doctor.dart';
 import 'package:flutter_tools/src/doctor.dart';
-import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:flutter_tools/src/features.dart';
 import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
 import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
 
 import '../src/common.dart';
 import '../src/context.dart';
+import '../src/mocks.dart';
 
 void main() {
   group('analytics', () {
@@ -123,12 +126,16 @@
   });
 
   group('analytics with mocks', () {
+    MemoryFileSystem memoryFileSystem;
+    MockStdio mockStdio;
     Usage mockUsage;
     SystemClock mockClock;
     Doctor mockDoctor;
     List<int> mockTimes;
 
     setUp(() {
+      memoryFileSystem = MemoryFileSystem();
+      mockStdio = MockStdio();
       mockUsage = MockUsage();
       when(mockUsage.isFirstRun).thenReturn(false);
       mockClock = MockClock();
@@ -190,6 +197,56 @@
     }, overrides: <Type, Generator>{
       Usage: () => mockUsage,
     });
+
+    testUsingContext('command sends localtime', () async {
+      const int kMillis = 1000;
+      mockTimes = <int>[kMillis];
+      // Since FLUTTER_ANALYTICS_LOG_FILE is set in the environment, analytics
+      // will be written to a file.
+      final Usage usage = Usage(versionOverride: 'test');
+      usage.suppressAnalytics = false;
+      usage.enabled = true;
+
+      usage.sendCommand('test');
+
+      final String log = fs.file('analytics.log').readAsStringSync();
+      final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
+      expect(log.contains(dateTime.toString()), isTrue);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      SystemClock: () => mockClock,
+      Platform: () => FakePlatform(
+        environment: <String, String>{
+          'FLUTTER_ANALYTICS_LOG_FILE': 'analytics.log',
+        },
+      ),
+      Stdio: () => mockStdio,
+    });
+
+    testUsingContext('event sends localtime', () async {
+      const int kMillis = 1000;
+      mockTimes = <int>[kMillis];
+      // Since FLUTTER_ANALYTICS_LOG_FILE is set in the environment, analytics
+      // will be written to a file.
+      final Usage usage = Usage(versionOverride: 'test');
+      usage.suppressAnalytics = false;
+      usage.enabled = true;
+
+      usage.sendEvent('test', 'test');
+
+      final String log = fs.file('analytics.log').readAsStringSync();
+      final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
+      expect(log.contains(dateTime.toString()), isTrue);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      SystemClock: () => mockClock,
+      Platform: () => FakePlatform(
+        environment: <String, String>{
+          'FLUTTER_ANALYTICS_LOG_FILE': 'analytics.log',
+        },
+      ),
+      Stdio: () => mockStdio,
+    });
   });
 
   group('analytics bots', () {