usage.dart 14.1 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
part of reporting;
6

7
const String _kFlutterUA = 'UA-67589403-6';
8

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
/// The collection of custom dimensions understood by the analytics backend.
/// When adding to this list, first ensure that the custom dimension is
/// defined in the backend, or will be defined shortly after the relevent PR
/// lands.
enum CustomDimensions {
  sessionHostOsDetails,  // cd1
  sessionChannelName,  // cd2
  commandRunIsEmulator, // cd3
  commandRunTargetName, // cd4
  hotEventReason,  // cd5
  hotEventFinalLibraryCount,  // cd6
  hotEventSyncedLibraryCount,  // cd7
  hotEventSyncedClassesCount,  // cd8
  hotEventSyncedProceduresCount,  // cd9
  hotEventSyncedBytes,  // cd10
  hotEventInvalidatedSourcesCount,  // cd11
  hotEventTransferTimeInMs,  // cd12
  hotEventOverallTimeInMs,  // cd13
  commandRunProjectType,  // cd14
  commandRunProjectHostLanguage,  // cd15
  commandCreateAndroidLanguage,  // cd16
  commandCreateIosLanguage,  // cd17
  commandRunProjectModule,  // cd18
  commandCreateProjectType,  // cd19
  commandPackagesNumberPlugins,  // cd20
  commandPackagesProjectModule,  // cd21
  commandRunTargetOsVersion,  // cd22
  commandRunModeName,  // cd23
  commandBuildBundleTargetPlatform,  // cd24
  commandBuildBundleIsModule,  // cd25
  commandResult,  // cd26
  hotEventTargetPlatform,  // cd27
  hotEventSdkName,  // cd28
  hotEventEmulator,  // cd29
  hotEventFullRestart,  // cd30
  commandHasTerminal,  // cd31
  enabledFlutterFeatures,  // cd32
  localTime,  // cd33
  commandBuildAarTargetPlatform,  // cd34
  commandBuildAarProjectType,  // cd35
  buildEventCommand,  // cd36
  buildEventSettings,  // cd37
51 52 53 54 55
  commandBuildApkTargetPlatform, // cd38
  commandBuildApkBuildMode, // cd39
  commandBuildApkSplitPerAbi, // cd40
  commandBuildAppBundleTargetPlatform, // cd41
  commandBuildAppBundleBuildMode, // cd42
56
  buildEventError,  // cd43
57
  commandResultEventMaxRss,  // cd44
58
}
59

60
String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}';
61

62 63 64 65
Map<String, String> _useCdKeys(Map<CustomDimensions, String> parameters) {
  return parameters.map((CustomDimensions k, String v) =>
      MapEntry<String, String>(cdKey(k), v));
}
66

67
Usage get flutterUsage => Usage.instance;
68

69
abstract class Usage {
70 71
  /// Create a new Usage instance; [versionOverride], [configDirOverride], and
  /// [logFile] are used for testing.
72 73 74
  factory Usage({
    String settingsName = 'flutter',
    String versionOverride,
75 76 77 78 79 80
    String configDirOverride,
    String logFile,
  }) => _DefaultUsage(settingsName: settingsName,
                      versionOverride: versionOverride,
                      configDirOverride: configDirOverride,
                      logFile: logFile);
81

82 83
  /// Returns [Usage] active in the current app context.
  static Usage get instance => context.get<Usage>();
84

85 86 87 88
  /// Uses the global [Usage] instance to send a 'command' to analytics.
  static void command(String command, {
    Map<CustomDimensions, String> parameters,
  }) => flutterUsage.sendCommand(command, parameters: _useCdKeys(parameters));
89

90 91
  /// Whether this is the first run of the tool.
  bool get isFirstRun;
92

93 94
  /// Whether analytics reporting should be supressed.
  bool get suppressAnalytics;
95

96 97
  /// Suppress analytics for this session.
  set suppressAnalytics(bool value);
98

99 100
  /// Whether analytics reporting is enabled.
  bool get enabled;
101

102 103 104 105 106 107 108 109 110 111 112
  /// Enable or disable reporting analytics.
  set enabled(bool value);

  /// A stable randomly generated UUID used to deduplicate multiple identical
  /// reports coming from the same computer.
  String get clientId;

  /// Sends a 'command' to the underlying analytics implementation.
  ///
  /// Note that using [command] above is preferred to ensure that the parameter
  /// keys are well-defined in [CustomDimensions] above.
113 114 115
  void sendCommand(
    String command, {
    Map<String, String> parameters,
116 117 118 119 120 121 122 123
  });

  /// Sends an 'event' to the underlying analytics implementation.
  ///
  /// Note that this method should not be used directly, instead see the
  /// event types defined in this directory in events.dart.
  @visibleForOverriding
  @visibleForTesting
124 125 126
  void sendEvent(
    String category,
    String parameter, {
127
    String label,
128
    Map<String, String> parameters,
129 130 131
  });

  /// Sends timing information to the underlying analytics implementation.
132 133 134 135 136
  void sendTiming(
    String category,
    String variableName,
    Duration duration, {
    String label,
137 138 139 140
  });

  /// Sends an exception to the underlying analytics implementation.
  void sendException(dynamic exception);
141

142 143 144 145 146 147 148 149 150 151 152 153 154
  /// Fires whenever analytics data is sent over the network.
  @visibleForTesting
  Stream<Map<String, dynamic>> get onSend;

  /// Returns when the last analytics event has been sent, or after a fixed
  /// (short) delay, whichever is less.
  Future<void> ensureAnalyticsSent();

  /// Prints a welcome message that informs the tool user about the collection
  /// of anonymous usage information.
  void printWelcome();
}

155 156
class _DefaultUsage implements Usage {
  _DefaultUsage({
157 158
    String settingsName = 'flutter',
    String versionOverride,
159 160
    String configDirOverride,
    String logFile,
161
  }) {
162
    final FlutterVersion flutterVersion = FlutterVersion.instance;
163
    final String version = versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true);
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
    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;

    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;
    }
185

186 187 188 189 190 191 192 193 194 195 196 197
    if (usingLogFile) {
      _analytics = LogToFileAnalytics(logFilePath);
    } else {
      _analytics = AnalyticsIO(
            _kFlutterUA,
            settingsName,
            version,
            documentDirectory:
                configDirOverride != null ? fs.directory(configDirOverride) : null,
          );
    }
    assert(_analytics != null);
198

199
    // Report a more detailed OS version string than package:usage does by default.
200
    _analytics.setSessionValue(cdKey(CustomDimensions.sessionHostOsDetails), os.name);
201
    // Send the branch name as the "channel".
202 203
    _analytics.setSessionValue(cdKey(CustomDimensions.sessionChannelName),
                               flutterVersion.getBranchName(redactUnknownBranches: true));
204 205 206 207 208 209 210 211 212
    // For each flutter experimental feature, record a session value in a comma
    // separated list.
    final String enabledFeatures = allFeatures
        .where((Feature feature) {
          return feature.configSetting != null &&
                 Config.instance.getValue(feature.configSetting) == true;
        })
        .map((Feature feature) => feature.configSetting)
        .join(',');
213
    _analytics.setSessionValue(cdKey(CustomDimensions.enabledFlutterFeatures), enabledFeatures);
214

215 216 217 218
    // Record the host as the application installer ID - the context that flutter_tools is running in.
    if (platform.environment.containsKey('FLUTTER_HOST')) {
      _analytics.setSessionValue('aiid', platform.environment['FLUTTER_HOST']);
    }
219
    _analytics.analyticsOpt = AnalyticsOpt.optOut;
220 221 222 223
  }

  Analytics _analytics;

224
  bool _printedWelcome = false;
225
  bool _suppressAnalytics = false;
226

227
  @override
228 229
  bool get isFirstRun => _analytics.firstRun;

230
  @override
231 232
  bool get suppressAnalytics => _suppressAnalytics || _analytics.firstRun;

233
  @override
234 235 236 237
  set suppressAnalytics(bool value) {
    _suppressAnalytics = value;
  }

238 239 240 241
  @override
  bool get enabled => _analytics.enabled;

  @override
242 243 244 245
  set enabled(bool value) {
    _analytics.enabled = value;
  }

246
  @override
247 248
  String get clientId => _analytics.clientId;

249
  @override
250
  void sendCommand(String command, { Map<String, String> parameters }) {
251
    if (suppressAnalytics) {
252
      return;
253
    }
254

255 256
    final Map<String, String> paramsWithLocalTime = <String, String>{
      ...?parameters,
257
      cdKey(CustomDimensions.localTime): formatDateTime(systemClock.now()),
258 259
    };
    _analytics.sendScreenView(command, parameters: paramsWithLocalTime);
260 261
  }

262
  @override
263 264 265
  void sendEvent(
    String category,
    String parameter, {
266
    String label,
267 268
    Map<String, String> parameters,
  }) {
269
    if (suppressAnalytics) {
270
      return;
271
    }
272

273 274
    final Map<String, String> paramsWithLocalTime = <String, String>{
      ...?parameters,
275
      cdKey(CustomDimensions.localTime): formatDateTime(systemClock.now()),
276
    };
277

278 279 280 281 282 283
    _analytics.sendEvent(
      category,
      parameter,
      label: label,
      parameters: paramsWithLocalTime,
    );
284 285
  }

286
  @override
287
  void sendTiming(
288 289
    String category,
    String variableName,
290 291
    Duration duration, {
    String label,
292
  }) {
293 294
    if (suppressAnalytics) {
      return;
295
    }
296 297 298 299 300 301
    _analytics.sendTiming(
      variableName,
      duration.inMilliseconds,
      category: category,
      label: label,
    );
302 303
  }

304
  @override
305
  void sendException(dynamic exception) {
306 307 308 309
    if (suppressAnalytics) {
      return;
    }
    _analytics.sendException(exception.runtimeType.toString());
310 311
  }

312
  @override
313 314
  Stream<Map<String, dynamic>> get onSend => _analytics.onSend;

315
  @override
316
  Future<void> ensureAnalyticsSent() async {
317 318 319
    // TODO(devoncarew): This may delay tool exit and could cause some analytics
    // events to not be reported. Perhaps we could send the analytics pings
    // out-of-process from flutter_tools?
320
    await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
321
  }
322

323
  @override
324 325 326
  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.
327
    if (_printedWelcome) {
328
      return;
329
    }
330
    _printedWelcome = true;
331 332 333

    printStatus('');
    printStatus('''
Seth Ladd's avatar
Seth Ladd committed
334
  ╔════════════════════════════════════════════════════════════════════════════╗
335
  ║                 Welcome to Flutter! - https://flutter.dev                  ║
Seth Ladd's avatar
Seth Ladd committed
336
  ║                                                                            ║
337 338 339 340 341 342 343 344
  ║ The Flutter tool anonymously reports feature usage statistics and crash    ║
  ║ reports to Google in order to help Google contribute improvements to       ║
  ║ Flutter over time.                                                         ║
  ║                                                                            ║
  ║ Read about data we send with crash reports:                                ║
  ║ https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting        ║
  ║                                                                            ║
  ║ See Google's privacy policy:                                               
Seth Ladd's avatar
Seth Ladd committed
345 346
   https://www.google.com/intl/en/policies/privacy/                           ║
                                                                              
347 348
   Use "flutter config --no-analytics" to disable analytics and crash         
   reporting.                                                                 
Seth Ladd's avatar
Seth Ladd committed
349
  ╚════════════════════════════════════════════════════════════════════════════╝
350 351
  ''', emphasis: true);
  }
352
}
353 354 355 356 357 358 359 360 361 362

// An Analytics mock that logs to file. Unimplemented methods goes to stdout.
// But stdout can't be used for testing since wrapper scripts like
// xcode_backend.sh etc manipulates them.
class LogToFileAnalytics extends AnalyticsMock {
  LogToFileAnalytics(String logFilePath) :
    logFile = fs.file(logFilePath)..createSync(recursive: true),
    super(true);

  final File logFile;
363
  final Map<String, String> _sessionValues = <String, String>{};
364

365 366 367 368 369 370
  final StreamController<Map<String, dynamic>> _sendController =
        StreamController<Map<String, dynamic>>.broadcast(sync: true);

  @override
  Stream<Map<String, dynamic>> get onSend => _sendController.stream;

371
  @override
372 373 374
  Future<void> sendScreenView(String viewName, {
    Map<String, String> parameters,
  }) {
375 376 377
    if (!enabled) {
      return Future<void>.value(null);
    }
378 379
    parameters ??= <String, String>{};
    parameters['viewName'] = viewName;
380
    parameters.addAll(_sessionValues);
381
    _sendController.add(parameters);
382 383 384 385 386 387 388
    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}) {
389 390 391
    if (!enabled) {
      return Future<void>.value(null);
    }
392 393 394
    parameters ??= <String, String>{};
    parameters['category'] = category;
    parameters['action'] = action;
395
    _sendController.add(parameters);
396
    logFile.writeAsStringSync('event $parameters\n', mode: FileMode.append);
397 398
    return Future<void>.value(null);
  }
399

400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
  @override
  Future<void> sendTiming(String variableName, int time,
      {String category, String label}) {
    if (!enabled) {
      return Future<void>.value(null);
    }
    final Map<String, String> parameters = <String, String>{
      '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<void>.value(null);
  }

417 418 419 420
  @override
  void setSessionValue(String param, dynamic value) {
    _sessionValues[param] = value.toString();
  }
421
}