diff --git a/packages/flutter_tools/lib/src/reporting/usage.dart b/packages/flutter_tools/lib/src/reporting/usage.dart index 0d62132bcf7..de48d6526f3 100644 --- a/packages/flutter_tools/lib/src/reporting/usage.dart +++ b/packages/flutter_tools/lib/src/reporting/usage.dart @@ -60,13 +60,17 @@ Map _useCdKeys(Map parameters) { Usage get flutterUsage => Usage.instance; abstract class Usage { + /// Create a new Usage instance; [versionOverride], [configDirOverride], and + /// [logFile] are used for testing. factory Usage({ String settingsName = 'flutter', String versionOverride, - String configDirOverride - }) => _UsageImpl(settingsName: settingsName, - versionOverride: versionOverride, - configDirOverride: configDirOverride); + String configDirOverride, + String logFile, + }) => _DefaultUsage(settingsName: settingsName, + versionOverride: versionOverride, + configDirOverride: configDirOverride, + logFile: logFile); /// Returns [Usage] active in the current app context. static Usage get instance => context.get(); @@ -134,28 +138,49 @@ abstract class Usage { void printWelcome(); } -class _UsageImpl implements Usage { - /// Create a new Usage instance; [versionOverride] and [configDirOverride] are - /// used for testing. - _UsageImpl({ +class _DefaultUsage implements Usage { + _DefaultUsage({ String settingsName = 'flutter', String versionOverride, - String configDirOverride + String configDirOverride, + String logFile, }) { final FlutterVersion flutterVersion = FlutterVersion.instance; final String version = versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true); + final bool suppressEnvFlag = platform.environment['FLUTTER_SUPPRESS_ANALYTICS'] == 'true'; + final String logFilePath = logFile ?? platform.environment['FLUTTER_ANALYTICS_LOG_FILE']; + final bool usingLogFile = logFilePath != null && logFilePath.isNotEmpty; - final String logFilePath = platform.environment['FLUTTER_ANALYTICS_LOG_FILE']; + if (// To support testing, only allow other signals to supress analytics + // when analytics are not being shunted to a file. + !usingLogFile && ( + // Ignore local user branches. + version.startsWith('[user-branch]') || + // Many CI systems don't do a full git checkout. + version.endsWith('/unknown') || + // Ignore bots. + isRunningOnBot || + // Ignore when suppressed by FLUTTER_SUPPRESS_ANALYTICS. + suppressEnvFlag + )) { + // If we think we're running on a CI system, suppress sending analytics. + suppressAnalytics = true; + _analytics = AnalyticsMock(); + return; + } - _analytics = logFilePath == null || logFilePath.isEmpty ? - AnalyticsIO( - _kFlutterUA, - settingsName, - version, - documentDirectory: configDirOverride != null ? fs.directory(configDirOverride) : null, - ) : - // Used for testing. - LogToFileAnalytics(logFilePath); + if (usingLogFile) { + _analytics = LogToFileAnalytics(logFilePath); + } else { + _analytics = AnalyticsIO( + _kFlutterUA, + settingsName, + version, + documentDirectory: + configDirOverride != null ? fs.directory(configDirOverride) : null, + ); + } + assert(_analytics != null); // Report a more detailed OS version string than package:usage does by default. _analytics.setSessionValue(cdKey(CustomDimensions.sessionHostOsDetails), os.name); @@ -178,14 +203,6 @@ class _UsageImpl implements Usage { _analytics.setSessionValue('aiid', platform.environment['FLUTTER_HOST']); } _analytics.analyticsOpt = AnalyticsOpt.optOut; - - final bool suppressEnvFlag = platform.environment['FLUTTER_SUPPRESS_ANALYTICS'] == 'true'; - _analytics.sendScreenView('version is $version, is bot $isRunningOnBot, suppressed $suppressEnvFlag'); - // Many CI systems don't do a full git checkout. - if (version.endsWith('/unknown') || isRunningOnBot || suppressEnvFlag) { - // If we think we're running on a CI system, suppress sending analytics. - suppressAnalytics = true; - } } Analytics _analytics; @@ -325,13 +342,23 @@ class LogToFileAnalytics extends AnalyticsMock { final File logFile; final Map _sessionValues = {}; + final StreamController> _sendController = + StreamController>.broadcast(sync: true); + + @override + Stream> get onSend => _sendController.stream; + @override Future sendScreenView(String viewName, { Map parameters, }) { + if (!enabled) { + return Future.value(null); + } parameters ??= {}; parameters['viewName'] = viewName; parameters.addAll(_sessionValues); + _sendController.add(parameters); logFile.writeAsStringSync('screenView $parameters\n', mode: FileMode.append); return Future.value(null); } @@ -339,13 +366,34 @@ class LogToFileAnalytics extends AnalyticsMock { @override Future sendEvent(String category, String action, {String label, int value, Map parameters}) { + if (!enabled) { + return Future.value(null); + } parameters ??= {}; parameters['category'] = category; parameters['action'] = action; + _sendController.add(parameters); logFile.writeAsStringSync('event $parameters\n', mode: FileMode.append); return Future.value(null); } + @override + Future sendTiming(String variableName, int time, + {String category, String label}) { + if (!enabled) { + return Future.value(null); + } + final Map parameters = { + 'variableName': variableName, + 'time': '$time', + if (category != null) 'category': category, + if (label != null) 'label': label, + }; + _sendController.add(parameters); + logFile.writeAsStringSync('timing $parameters\n', mode: FileMode.append); + return Future.value(null); + } + @override void setSessionValue(String param, dynamic value) { _sessionValues[param] = value.toString(); diff --git a/packages/flutter_tools/test/general.shard/analytics_test.dart b/packages/flutter_tools/test/general.shard/analytics_test.dart index f210783b93a..753607654d6 100644 --- a/packages/flutter_tools/test/general.shard/analytics_test.dart +++ b/packages/flutter_tools/test/general.shard/analytics_test.dart @@ -55,7 +55,7 @@ void main() { flutterUsage.enabled = true; await createProject(tempDir); - expect(count, flutterUsage.isFirstRun ? 0 : 2); + expect(count, flutterUsage.isFirstRun ? 0 : 3); count = 0; flutterUsage.enabled = false; @@ -65,7 +65,10 @@ void main() { expect(count, 0); }, overrides: { FlutterVersion: () => FlutterVersion(const SystemClock()), - Usage: () => Usage(configDirOverride: tempDir.path), + Usage: () => Usage( + configDirOverride: tempDir.path, + logFile: tempDir.childFile('analytics.log').path + ), }); // Ensure we don't send for the 'flutter config' command. @@ -84,7 +87,10 @@ void main() { expect(count, 0); }, overrides: { FlutterVersion: () => FlutterVersion(const SystemClock()), - Usage: () => Usage(configDirOverride: tempDir.path), + Usage: () => Usage( + configDirOverride: tempDir.path, + logFile: tempDir.childFile('analytics.log').path + ), }); testUsingContext('Usage records one feature in experiment setting', () async {