1// Copyright 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5part of reporting; 6 7const String _kFlutterUA = 'UA-67589403-6'; 8 9/// The collection of custom dimensions understood by the analytics backend. 10/// When adding to this list, first ensure that the custom dimension is 11/// defined in the backend, or will be defined shortly after the relevent PR 12/// lands. 13enum CustomDimensions { 14 sessionHostOsDetails, // cd1 15 sessionChannelName, // cd2 16 commandRunIsEmulator, // cd3 17 commandRunTargetName, // cd4 18 hotEventReason, // cd5 19 hotEventFinalLibraryCount, // cd6 20 hotEventSyncedLibraryCount, // cd7 21 hotEventSyncedClassesCount, // cd8 22 hotEventSyncedProceduresCount, // cd9 23 hotEventSyncedBytes, // cd10 24 hotEventInvalidatedSourcesCount, // cd11 25 hotEventTransferTimeInMs, // cd12 26 hotEventOverallTimeInMs, // cd13 27 commandRunProjectType, // cd14 28 commandRunProjectHostLanguage, // cd15 29 commandCreateAndroidLanguage, // cd16 30 commandCreateIosLanguage, // cd17 31 commandRunProjectModule, // cd18 32 commandCreateProjectType, // cd19 33 commandPackagesNumberPlugins, // cd20 34 commandPackagesProjectModule, // cd21 35 commandRunTargetOsVersion, // cd22 36 commandRunModeName, // cd23 37 commandBuildBundleTargetPlatform, // cd24 38 commandBuildBundleIsModule, // cd25 39 commandResult, // cd26 40 hotEventTargetPlatform, // cd27 41 hotEventSdkName, // cd28 42 hotEventEmulator, // cd29 43 hotEventFullRestart, // cd30 44 commandHasTerminal, // cd31 45 enabledFlutterFeatures, // cd32 46 localTime, // cd33 47 commandBuildAarTargetPlatform, // cd34 48 commandBuildAarProjectType, // cd35 49 buildEventCommand, // cd36 50 buildEventSettings, // cd37 51} 52 53String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}'; 54 55Map<String, String> _useCdKeys(Map<CustomDimensions, String> parameters) { 56 return parameters.map((CustomDimensions k, String v) => 57 MapEntry<String, String>(cdKey(k), v)); 58} 59 60Usage get flutterUsage => Usage.instance; 61 62abstract class Usage { 63 /// Create a new Usage instance; [versionOverride], [configDirOverride], and 64 /// [logFile] are used for testing. 65 factory Usage({ 66 String settingsName = 'flutter', 67 String versionOverride, 68 String configDirOverride, 69 String logFile, 70 }) => _DefaultUsage(settingsName: settingsName, 71 versionOverride: versionOverride, 72 configDirOverride: configDirOverride, 73 logFile: logFile); 74 75 /// Returns [Usage] active in the current app context. 76 static Usage get instance => context.get<Usage>(); 77 78 /// Uses the global [Usage] instance to send a 'command' to analytics. 79 static void command(String command, { 80 Map<CustomDimensions, String> parameters, 81 }) => flutterUsage.sendCommand(command, parameters: _useCdKeys(parameters)); 82 83 /// Whether this is the first run of the tool. 84 bool get isFirstRun; 85 86 /// Whether analytics reporting should be supressed. 87 bool get suppressAnalytics; 88 89 /// Suppress analytics for this session. 90 set suppressAnalytics(bool value); 91 92 /// Whether analytics reporting is enabled. 93 bool get enabled; 94 95 /// Enable or disable reporting analytics. 96 set enabled(bool value); 97 98 /// A stable randomly generated UUID used to deduplicate multiple identical 99 /// reports coming from the same computer. 100 String get clientId; 101 102 /// Sends a 'command' to the underlying analytics implementation. 103 /// 104 /// Note that using [command] above is preferred to ensure that the parameter 105 /// keys are well-defined in [CustomDimensions] above. 106 void sendCommand(String command, { 107 Map<String, String> parameters 108 }); 109 110 /// Sends an 'event' to the underlying analytics implementation. 111 /// 112 /// Note that this method should not be used directly, instead see the 113 /// event types defined in this directory in events.dart. 114 @visibleForOverriding 115 @visibleForTesting 116 void sendEvent(String category, String parameter, { 117 Map<String, String> parameters 118 }); 119 120 /// Sends timing information to the underlying analytics implementation. 121 void sendTiming(String category, String variableName, Duration duration, { 122 String label 123 }); 124 125 /// Sends an exception to the underlying analytics implementation. 126 void sendException(dynamic exception); 127 128 /// Fires whenever analytics data is sent over the network. 129 @visibleForTesting 130 Stream<Map<String, dynamic>> get onSend; 131 132 /// Returns when the last analytics event has been sent, or after a fixed 133 /// (short) delay, whichever is less. 134 Future<void> ensureAnalyticsSent(); 135 136 /// Prints a welcome message that informs the tool user about the collection 137 /// of anonymous usage information. 138 void printWelcome(); 139} 140 141class _DefaultUsage implements Usage { 142 _DefaultUsage({ 143 String settingsName = 'flutter', 144 String versionOverride, 145 String configDirOverride, 146 String logFile, 147 }) { 148 final FlutterVersion flutterVersion = FlutterVersion.instance; 149 final String version = versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true); 150 final bool suppressEnvFlag = platform.environment['FLUTTER_SUPPRESS_ANALYTICS'] == 'true'; 151 final String logFilePath = logFile ?? platform.environment['FLUTTER_ANALYTICS_LOG_FILE']; 152 final bool usingLogFile = logFilePath != null && logFilePath.isNotEmpty; 153 154 if (// To support testing, only allow other signals to supress analytics 155 // when analytics are not being shunted to a file. 156 !usingLogFile && ( 157 // Ignore local user branches. 158 version.startsWith('[user-branch]') || 159 // Many CI systems don't do a full git checkout. 160 version.endsWith('/unknown') || 161 // Ignore bots. 162 isRunningOnBot || 163 // Ignore when suppressed by FLUTTER_SUPPRESS_ANALYTICS. 164 suppressEnvFlag 165 )) { 166 // If we think we're running on a CI system, suppress sending analytics. 167 suppressAnalytics = true; 168 _analytics = AnalyticsMock(); 169 return; 170 } 171 172 if (usingLogFile) { 173 _analytics = LogToFileAnalytics(logFilePath); 174 } else { 175 _analytics = AnalyticsIO( 176 _kFlutterUA, 177 settingsName, 178 version, 179 documentDirectory: 180 configDirOverride != null ? fs.directory(configDirOverride) : null, 181 ); 182 } 183 assert(_analytics != null); 184 185 // Report a more detailed OS version string than package:usage does by default. 186 _analytics.setSessionValue(cdKey(CustomDimensions.sessionHostOsDetails), os.name); 187 // Send the branch name as the "channel". 188 _analytics.setSessionValue(cdKey(CustomDimensions.sessionChannelName), 189 flutterVersion.getBranchName(redactUnknownBranches: true)); 190 // For each flutter experimental feature, record a session value in a comma 191 // separated list. 192 final String enabledFeatures = allFeatures 193 .where((Feature feature) { 194 return feature.configSetting != null && 195 Config.instance.getValue(feature.configSetting) == true; 196 }) 197 .map((Feature feature) => feature.configSetting) 198 .join(','); 199 _analytics.setSessionValue(cdKey(CustomDimensions.enabledFlutterFeatures), enabledFeatures); 200 201 // Record the host as the application installer ID - the context that flutter_tools is running in. 202 if (platform.environment.containsKey('FLUTTER_HOST')) { 203 _analytics.setSessionValue('aiid', platform.environment['FLUTTER_HOST']); 204 } 205 _analytics.analyticsOpt = AnalyticsOpt.optOut; 206 } 207 208 Analytics _analytics; 209 210 bool _printedWelcome = false; 211 bool _suppressAnalytics = false; 212 213 @override 214 bool get isFirstRun => _analytics.firstRun; 215 216 @override 217 bool get suppressAnalytics => _suppressAnalytics || _analytics.firstRun; 218 219 @override 220 set suppressAnalytics(bool value) { 221 _suppressAnalytics = value; 222 } 223 224 @override 225 bool get enabled => _analytics.enabled; 226 227 @override 228 set enabled(bool value) { 229 _analytics.enabled = value; 230 } 231 232 @override 233 String get clientId => _analytics.clientId; 234 235 @override 236 void sendCommand(String command, { Map<String, String> parameters }) { 237 if (suppressAnalytics) { 238 return; 239 } 240 241 final Map<String, String> paramsWithLocalTime = <String, String>{ 242 ...?parameters, 243 cdKey(CustomDimensions.localTime): formatDateTime(systemClock.now()), 244 }; 245 _analytics.sendScreenView(command, parameters: paramsWithLocalTime); 246 } 247 248 @override 249 void sendEvent( 250 String category, 251 String parameter, { 252 Map<String, String> parameters, 253 }) { 254 if (suppressAnalytics) { 255 return; 256 } 257 258 final Map<String, String> paramsWithLocalTime = <String, String>{ 259 ...?parameters, 260 cdKey(CustomDimensions.localTime): formatDateTime(systemClock.now()), 261 }; 262 263 _analytics.sendEvent(category, parameter, parameters: paramsWithLocalTime); 264 } 265 266 @override 267 void sendTiming( 268 String category, 269 String variableName, 270 Duration duration, { 271 String label, 272 }) { 273 if (suppressAnalytics) { 274 return; 275 } 276 _analytics.sendTiming( 277 variableName, 278 duration.inMilliseconds, 279 category: category, 280 label: label, 281 ); 282 } 283 284 @override 285 void sendException(dynamic exception) { 286 if (suppressAnalytics) { 287 return; 288 } 289 _analytics.sendException(exception.runtimeType.toString()); 290 } 291 292 @override 293 Stream<Map<String, dynamic>> get onSend => _analytics.onSend; 294 295 @override 296 Future<void> ensureAnalyticsSent() async { 297 // TODO(devoncarew): This may delay tool exit and could cause some analytics 298 // events to not be reported. Perhaps we could send the analytics pings 299 // out-of-process from flutter_tools? 300 await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250)); 301 } 302 303 @override 304 void printWelcome() { 305 // This gets called if it's the first run by the selected command, if any, 306 // and on exit, in case there was no command. 307 if (_printedWelcome) { 308 return; 309 } 310 _printedWelcome = true; 311 312 printStatus(''); 313 printStatus(''' 314 ╔════════════════════════════════════════════════════════════════════════════╗ 315 ║ Welcome to Flutter! - https://flutter.dev ║ 316 ║ ║ 317 ║ The Flutter tool anonymously reports feature usage statistics and crash ║ 318 ║ reports to Google in order to help Google contribute improvements to ║ 319 ║ Flutter over time. ║ 320 ║ ║ 321 ║ Read about data we send with crash reports: ║ 322 ║ https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting ║ 323 ║ ║ 324 ║ See Google's privacy policy: ║ 325 ║ https://www.google.com/intl/en/policies/privacy/ ║ 326 ║ ║ 327 ║ Use "flutter config --no-analytics" to disable analytics and crash ║ 328 ║ reporting. ║ 329 ╚════════════════════════════════════════════════════════════════════════════╝ 330 ''', emphasis: true); 331 } 332} 333 334// An Analytics mock that logs to file. Unimplemented methods goes to stdout. 335// But stdout can't be used for testing since wrapper scripts like 336// xcode_backend.sh etc manipulates them. 337class LogToFileAnalytics extends AnalyticsMock { 338 LogToFileAnalytics(String logFilePath) : 339 logFile = fs.file(logFilePath)..createSync(recursive: true), 340 super(true); 341 342 final File logFile; 343 final Map<String, String> _sessionValues = <String, String>{}; 344 345 final StreamController<Map<String, dynamic>> _sendController = 346 StreamController<Map<String, dynamic>>.broadcast(sync: true); 347 348 @override 349 Stream<Map<String, dynamic>> get onSend => _sendController.stream; 350 351 @override 352 Future<void> sendScreenView(String viewName, { 353 Map<String, String> parameters, 354 }) { 355 if (!enabled) { 356 return Future<void>.value(null); 357 } 358 parameters ??= <String, String>{}; 359 parameters['viewName'] = viewName; 360 parameters.addAll(_sessionValues); 361 _sendController.add(parameters); 362 logFile.writeAsStringSync('screenView $parameters\n', mode: FileMode.append); 363 return Future<void>.value(null); 364 } 365 366 @override 367 Future<void> sendEvent(String category, String action, 368 {String label, int value, Map<String, String> parameters}) { 369 if (!enabled) { 370 return Future<void>.value(null); 371 } 372 parameters ??= <String, String>{}; 373 parameters['category'] = category; 374 parameters['action'] = action; 375 _sendController.add(parameters); 376 logFile.writeAsStringSync('event $parameters\n', mode: FileMode.append); 377 return Future<void>.value(null); 378 } 379 380 @override 381 Future<void> sendTiming(String variableName, int time, 382 {String category, String label}) { 383 if (!enabled) { 384 return Future<void>.value(null); 385 } 386 final Map<String, String> parameters = <String, String>{ 387 'variableName': variableName, 388 'time': '$time', 389 if (category != null) 'category': category, 390 if (label != null) 'label': label, 391 }; 392 _sendController.add(parameters); 393 logFile.writeAsStringSync('timing $parameters\n', mode: FileMode.append); 394 return Future<void>.value(null); 395 } 396 397 @override 398 void setSessionValue(String param, dynamic value) { 399 _sessionValues[param] = value.toString(); 400 } 401} 402