usage.dart 15.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// 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
/// The collection of custom dimensions understood by the analytics backend.
/// When adding to this list, first ensure that the custom dimension is
11
/// defined in the backend, or will be defined shortly after the relevant PR
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
/// 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
  commandRunAndroidEmbeddingVersion, // cd45
  commandPackagesAndroidEmbeddingVersion, // cd46
60
}
61

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

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

69
Usage get flutterUsage => Usage.instance;
70

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

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

87 88 89 90
  /// 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));
91

92 93
  /// Whether this is the first run of the tool.
  bool get isFirstRun;
94

95
  /// Whether analytics reporting should be suppressed.
96
  bool get suppressAnalytics;
97

98 99
  /// Suppress analytics for this session.
  set suppressAnalytics(bool value);
100

101 102
  /// Whether analytics reporting is enabled.
  bool get enabled;
103

104 105 106 107 108 109 110 111 112 113 114
  /// 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.
115 116 117
  void sendCommand(
    String command, {
    Map<String, String> parameters,
118 119 120 121 122 123 124 125
  });

  /// 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
126 127 128
  void sendEvent(
    String category,
    String parameter, {
129
    String label,
130
    int value,
131
    Map<String, String> parameters,
132 133 134
  });

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

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

145 146 147 148 149 150 151 152 153 154 155 156 157
  /// 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();
}

158 159
class _DefaultUsage implements Usage {
  _DefaultUsage({
160 161
    String settingsName = 'flutter',
    String versionOverride,
162 163
    String configDirOverride,
    String logFile,
164
  }) {
165
    final FlutterVersion flutterVersion = globals.flutterVersion;
166
    final String version = versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true);
167 168
    final bool suppressEnvFlag = globals.platform.environment['FLUTTER_SUPPRESS_ANALYTICS'] == 'true';
    final String logFilePath = logFile ?? globals.platform.environment['FLUTTER_ANALYTICS_LOG_FILE'];
169 170 171 172 173 174 175 176 177 178
    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.
179
        isRunningOnBot(globals.platform) ||
180 181 182 183 184 185 186 187
        // 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;
    }
188

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

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

226
    // Record the host as the application installer ID - the context that flutter_tools is running in.
227 228
    if (globals.platform.environment.containsKey('FLUTTER_HOST')) {
      _analytics.setSessionValue('aiid', globals.platform.environment['FLUTTER_HOST']);
229
    }
230
    _analytics.analyticsOpt = AnalyticsOpt.optOut;
231 232 233 234
  }

  Analytics _analytics;

235
  bool _printedWelcome = false;
236
  bool _suppressAnalytics = false;
237

238
  @override
239 240
  bool get isFirstRun => _analytics.firstRun;

241
  @override
242 243
  bool get suppressAnalytics => _suppressAnalytics || _analytics.firstRun;

244
  @override
245 246 247 248
  set suppressAnalytics(bool value) {
    _suppressAnalytics = value;
  }

249 250 251 252
  @override
  bool get enabled => _analytics.enabled;

  @override
253 254 255 256
  set enabled(bool value) {
    _analytics.enabled = value;
  }

257
  @override
258 259
  String get clientId => _analytics.clientId;

260
  @override
261
  void sendCommand(String command, { Map<String, String> parameters }) {
262
    if (suppressAnalytics) {
263
      return;
264
    }
265

266 267
    final Map<String, String> paramsWithLocalTime = <String, String>{
      ...?parameters,
268
      cdKey(CustomDimensions.localTime): formatDateTime(systemClock.now()),
269 270
    };
    _analytics.sendScreenView(command, parameters: paramsWithLocalTime);
271 272
  }

273
  @override
274 275 276
  void sendEvent(
    String category,
    String parameter, {
277
    String label,
278
    int value,
279 280
    Map<String, String> parameters,
  }) {
281
    if (suppressAnalytics) {
282
      return;
283
    }
284

285 286
    final Map<String, String> paramsWithLocalTime = <String, String>{
      ...?parameters,
287
      cdKey(CustomDimensions.localTime): formatDateTime(systemClock.now()),
288
    };
289

290 291 292 293
    _analytics.sendEvent(
      category,
      parameter,
      label: label,
294
      value: value,
295 296
      parameters: paramsWithLocalTime,
    );
297 298
  }

299
  @override
300
  void sendTiming(
301 302
    String category,
    String variableName,
303 304
    Duration duration, {
    String label,
305
  }) {
306 307
    if (suppressAnalytics) {
      return;
308
    }
309 310 311 312 313 314
    _analytics.sendTiming(
      variableName,
      duration.inMilliseconds,
      category: category,
      label: label,
    );
315 316
  }

317
  @override
318
  void sendException(dynamic exception) {
319 320 321 322
    if (suppressAnalytics) {
      return;
    }
    _analytics.sendException(exception.runtimeType.toString());
323 324
  }

325
  @override
326 327
  Stream<Map<String, dynamic>> get onSend => _analytics.onSend;

328
  @override
329
  Future<void> ensureAnalyticsSent() async {
330 331 332
    // 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?
333
    await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
334
  }
335

336
  void _printWelcome() {
337 338
    globals.printStatus('');
    globals.printStatus('''
Seth Ladd's avatar
Seth Ladd committed
339
  ╔════════════════════════════════════════════════════════════════════════════╗
340
  ║                 Welcome to Flutter! - https://flutter.dev                  ║
Seth Ladd's avatar
Seth Ladd committed
341
  ║                                                                            ║
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
  ║ The Flutter tool uses Google Analytics to anonymously report feature usage ║
  ║ statistics and basic crash reports. This data is used to help improve      ║
  ║ Flutter tools over time.                                                   ║
  ║                                                                            ║
  ║ Flutter tool analytics are not sent on the very first run. To disable      ║
  ║ reporting, type 'flutter config --no-analytics'. To display the current    ║
  ║ setting, type 'flutter config'. If you opt out of analytics, an opt-out    ║
  ║ event will be sent, and then no further information will be sent by the    ║
  ║ Flutter tool.                                                              ║
  ║                                                                            ║
  ║ By downloading the Flutter SDK, you agree to the Google Terms of Service.  ║
  ║ Note: The Google Privacy Policy describes how data is handled in this      ║
  ║ service.                                                                   ║
  ║                                                                            ║
  ║ Moreover, Flutter includes the Dart SDK, which may send usage metrics and  ║
  ║ crash reports to Google.                                                   ║
358 359
  ║                                                                            ║
  ║ Read about data we send with crash reports:                                ║
360
  ║ https://flutter.dev/docs/reference/crash-reporting                         ║
361 362
  ║                                                                            ║
  ║ See Google's privacy policy:                                               
363
   https://policies.google.com/privacy                                        ║
Seth Ladd's avatar
Seth Ladd committed
364
  ╚════════════════════════════════════════════════════════════════════════════╝
365 366
  ''', emphasis: true);
  }
367 368 369 370 371 372 373 374 375 376 377

  @override
  void printWelcome() {
    // Only print once per run.
    if (_printedWelcome) {
      return;
    }
    if (// Display the welcome message if this is the first run of the tool.
        isFirstRun ||
        // Display the welcome message if we are not on master, and if the
        // persistent tool state instructs that we should.
378
        (!globals.flutterVersion.isMaster &&
379 380 381 382 383 384
        (persistentToolState.redisplayWelcomeMessage ?? true))) {
      _printWelcome();
      _printedWelcome = true;
      persistentToolState.redisplayWelcomeMessage = false;
    }
  }
385
}
386 387 388 389 390 391

// 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) :
392
    logFile = globals.fs.file(logFilePath)..createSync(recursive: true),
393 394 395
    super(true);

  final File logFile;
396
  final Map<String, String> _sessionValues = <String, String>{};
397

398 399 400 401 402 403
  final StreamController<Map<String, dynamic>> _sendController =
        StreamController<Map<String, dynamic>>.broadcast(sync: true);

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

404
  @override
405 406 407
  Future<void> sendScreenView(String viewName, {
    Map<String, String> parameters,
  }) {
408 409 410
    if (!enabled) {
      return Future<void>.value(null);
    }
411 412
    parameters ??= <String, String>{};
    parameters['viewName'] = viewName;
413
    parameters.addAll(_sessionValues);
414
    _sendController.add(parameters);
415 416 417 418 419 420 421
    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}) {
422 423 424
    if (!enabled) {
      return Future<void>.value(null);
    }
425 426 427
    parameters ??= <String, String>{};
    parameters['category'] = category;
    parameters['action'] = action;
428
    _sendController.add(parameters);
429
    logFile.writeAsStringSync('event $parameters\n', mode: FileMode.append);
430 431
    return Future<void>.value(null);
  }
432

433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
  @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);
  }

450 451 452 453
  @override
  void setSessionValue(String param, dynamic value) {
    _sessionValues[param] = value.toString();
  }
454
}