[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', () {