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
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
    String configDirOverride,
    String logFile,
77
    @required bool runningOnBot,
78 79 80
  }) => _DefaultUsage(settingsName: settingsName,
                      versionOverride: versionOverride,
                      configDirOverride: configDirOverride,
81 82
                      logFile: logFile,
                      runningOnBot: runningOnBot);
83

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

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

92
  /// Whether analytics reporting should be suppressed.
93
  bool get suppressAnalytics;
94

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

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

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

  /// 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
123 124 125
  void sendEvent(
    String category,
    String parameter, {
126
    String label,
127
    int value,
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
    @required bool runningOnBot,
162
  }) {
163
    final FlutterVersion flutterVersion = globals.flutterVersion;
164
    final String version = versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true);
165 166
    final bool suppressEnvFlag = globals.platform.environment['FLUTTER_SUPPRESS_ANALYTICS'] == 'true';
    final String logFilePath = logFile ?? globals.platform.environment['FLUTTER_ANALYTICS_LOG_FILE'];
167 168 169 170 171 172 173 174 175 176
    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.
177
        runningOnBot ||
178 179 180 181 182 183 184 185
        // 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;
    }
186

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

200
    // Report a more detailed OS version string than package:usage does by default.
201 202 203 204
    _analytics.setSessionValue(
      cdKey(CustomDimensions.sessionHostOsDetails),
      globals.os.name,
    );
205
    // Send the branch name as the "channel".
206 207 208 209
    _analytics.setSessionValue(
      cdKey(CustomDimensions.sessionChannelName),
      flutterVersion.getBranchName(redactUnknownBranches: true),
    );
210 211 212
    // For each flutter experimental feature, record a session value in a comma
    // separated list.
    final String enabledFeatures = allFeatures
213 214 215 216 217 218 219 220 221 222
      .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,
    );
223

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

  Analytics _analytics;

233
  bool _printedWelcome = false;
234
  bool _suppressAnalytics = false;
235

236
  @override
237 238
  bool get isFirstRun => _analytics.firstRun;

239
  @override
240 241
  bool get suppressAnalytics => _suppressAnalytics || _analytics.firstRun;

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

247 248 249 250
  @override
  bool get enabled => _analytics.enabled;

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

255
  @override
256 257
  String get clientId => _analytics.clientId;

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

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

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

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

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

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

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

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

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

334
  void _printWelcome() {
335 336
    globals.printStatus('');
    globals.printStatus('''
Seth Ladd's avatar
Seth Ladd committed
337
  ╔════════════════════════════════════════════════════════════════════════════╗
338
  ║                 Welcome to Flutter! - https://flutter.dev                  ║
Seth Ladd's avatar
Seth Ladd committed
339
  ║                                                                            ║
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
  ║ 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.                                                   ║
356 357
  ║                                                                            ║
  ║ Read about data we send with crash reports:                                ║
358
  ║ https://flutter.dev/docs/reference/crash-reporting                         ║
359 360
  ║                                                                            ║
  ║ See Google's privacy policy:                                               
361
   https://policies.google.com/privacy                                        ║
Seth Ladd's avatar
Seth Ladd committed
362
  ╚════════════════════════════════════════════════════════════════════════════╝
363 364
  ''', emphasis: true);
  }
365 366 367 368 369 370 371 372 373 374 375

  @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.
376
        (!globals.flutterVersion.isMaster &&
377
        (globals.persistentToolState.redisplayWelcomeMessage ?? true))) {
378 379
      _printWelcome();
      _printedWelcome = true;
380
      globals.persistentToolState.redisplayWelcomeMessage = false;
381 382
    }
  }
383
}
384 385 386 387 388 389

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

  final File logFile;
394
  final Map<String, String> _sessionValues = <String, String>{};
395

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

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

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

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

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