usage.dart 10 KB
Newer Older
1 2 3 4 5 6
// 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.

import 'dart:async';

7
import 'package:meta/meta.dart';
8
import 'package:usage/usage_io.dart';
9

10
import '../base/config.dart';
11 12 13 14
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/os.dart';
import '../base/platform.dart';
15
import '../base/time.dart';
16
import '../base/utils.dart';
17
import '../features.dart';
18 19
import '../globals.dart';
import '../version.dart';
20

21
const String _kFlutterUA = 'UA-67589403-6';
22

23 24 25
// Attached to all `Usage.sendCommand` and `Usage.sendEvent`.
const String _kLocalTimeParameter = 'cd33';

26 27 28
const String kSessionHostOsDetails = 'cd1';
const String kSessionChannelName = 'cd2';

29
const String kEventReloadReasonParameterName = 'cd5';
30 31 32 33 34 35
const String kEventReloadFinalLibraryCount = 'cd6';
const String kEventReloadSyncedLibraryCount = 'cd7';
const String kEventReloadSyncedClassesCount = 'cd8';
const String kEventReloadSyncedProceduresCount = 'cd9';
const String kEventReloadSyncedBytes = 'cd10';
const String kEventReloadInvalidatedSourcesCount = 'cd11';
36 37
const String kEventReloadTransferTimeInMs = 'cd12';
const String kEventReloadOverallTimeInMs = 'cd13';
38

39 40 41 42 43
const String kCommandRunIsEmulator = 'cd3';
const String kCommandRunTargetName = 'cd4';
const String kCommandRunProjectType = 'cd14';
const String kCommandRunProjectHostLanguage = 'cd15';
const String kCommandRunProjectModule = 'cd18';
44 45
const String kCommandRunTargetOsVersion = 'cd22';
const String kCommandRunModeName = 'cd23';
46 47 48 49 50 51 52 53

const String kCommandCreateAndroidLanguage = 'cd16';
const String kCommandCreateIosLanguage = 'cd17';
const String kCommandCreateProjectType = 'cd19';

const String kCommandPackagesNumberPlugins = 'cd20';
const String kCommandPackagesProjectModule = 'cd21';

54 55
const String kCommandBuildBundleTargetPlatform = 'cd24';
const String kCommandBuildBundleIsModule = 'cd25';
56 57

const String kCommandResult = 'cd26';
58
const String kCommandHasTerminal = 'cd31';
59 60 61 62 63

const String reloadExceptionTargetPlatform = 'cd27';
const String reloadExceptionSdkName = 'cd28';
const String reloadExceptionEmulator = 'cd29';
const String reloadExceptionFullRestart = 'cd30';
64 65

const String enabledFlutterFeatures = 'cd32';
66
// Next ID: cd34
67

68 69
Usage get flutterUsage => Usage.instance;

70
class Usage {
71 72
  /// Create a new Usage instance; [versionOverride] and [configDirOverride] are
  /// used for testing.
73
  Usage({ String settingsName = 'flutter', String versionOverride, String configDirOverride}) {
74
    final FlutterVersion flutterVersion = FlutterVersion.instance;
75
    final String version = versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true);
76 77 78 79 80 81 82 83 84 85 86 87

    final String logFilePath = platform.environment['FLUTTER_ANALYTICS_LOG_FILE'];

    _analytics = logFilePath == null || logFilePath.isEmpty ?
        AnalyticsIO(
          _kFlutterUA,
          settingsName,
          version,
          documentDirectory: configDirOverride != null ? fs.directory(configDirOverride) : null,
        ) :
        // Used for testing.
        LogToFileAnalytics(logFilePath);
88

89
    // Report a more detailed OS version string than package:usage does by default.
90
    _analytics.setSessionValue(kSessionHostOsDetails, os.name);
91
    // Send the branch name as the "channel".
92
    _analytics.setSessionValue(kSessionChannelName, flutterVersion.getBranchName(redactUnknownBranches: true));
93 94 95 96 97 98 99 100 101 102 103
    // 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(',');
    _analytics.setSessionValue(enabledFlutterFeatures, enabledFeatures);

104 105 106 107
    // 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']);
    }
108
    _analytics.analyticsOpt = AnalyticsOpt.optOut;
109

110
    final bool suppressEnvFlag = platform.environment['FLUTTER_SUPPRESS_ANALYTICS'] == 'true';
111
    _analytics.sendScreenView('version is $version, is bot $isRunningOnBot, suppressed $suppressEnvFlag');
112
    // Many CI systems don't do a full git checkout.
113
    if (version.endsWith('/unknown') || isRunningOnBot || suppressEnvFlag) {
114 115 116
      // If we think we're running on a CI system, suppress sending analytics.
      suppressAnalytics = true;
    }
117 118 119
  }

  /// Returns [Usage] active in the current app context.
120
  static Usage get instance => context.get<Usage>();
121 122 123

  Analytics _analytics;

124
  bool _printedWelcome = false;
125
  bool _suppressAnalytics = false;
126

127 128 129 130
  bool get isFirstRun => _analytics.firstRun;

  bool get enabled => _analytics.enabled;

131 132 133 134 135 136 137
  bool get suppressAnalytics => _suppressAnalytics || _analytics.firstRun;

  /// Suppress analytics for this session.
  set suppressAnalytics(bool value) {
    _suppressAnalytics = value;
  }

138 139 140 141 142
  /// Enable or disable reporting analytics.
  set enabled(bool value) {
    _analytics.enabled = value;
  }

143 144 145 146
  /// A stable randomly generated UUID used to deduplicate multiple identical
  /// reports coming from the same computer.
  String get clientId => _analytics.clientId;

147
  void sendCommand(String command, { Map<String, String> parameters }) {
148
    if (suppressAnalytics) {
149
      return;
150
    }
151

152 153 154 155
    final Map<String, String> paramsWithLocalTime = <String, String>{
      ...?parameters,
      _kLocalTimeParameter: systemClock.now().toString(),
    };
156

157
    _analytics.sendScreenView(command, parameters: paramsWithLocalTime);
158 159
  }

160 161 162 163 164
  void sendEvent(
    String category,
    String parameter, {
    Map<String, String> parameters,
  }) {
165
    if (suppressAnalytics) {
166
      return;
167
    }
168

169 170 171 172
    final Map<String, String> paramsWithLocalTime = <String, String>{
      ...?parameters,
      _kLocalTimeParameter: systemClock.now().toString(),
    };
173

174
    _analytics.sendEvent(category, parameter, parameters: paramsWithLocalTime);
175 176
  }

177
  void sendTiming(
178 179
    String category,
    String variableName,
180 181
    Duration duration, {
    String label,
182
  }) {
183 184
    if (suppressAnalytics) {
      return;
185
    }
186 187 188 189 190 191
    _analytics.sendTiming(
      variableName,
      duration.inMilliseconds,
      category: category,
      label: label,
    );
192 193
  }

194
  void sendException(dynamic exception) {
195 196 197 198
    if (suppressAnalytics) {
      return;
    }
    _analytics.sendException(exception.runtimeType.toString());
199 200
  }

201 202
  /// Fires whenever analytics data is sent over the network.
  @visibleForTesting
203 204 205 206
  Stream<Map<String, dynamic>> get onSend => _analytics.onSend;

  /// Returns when the last analytics event has been sent, or after a fixed
  /// (short) delay, whichever is less.
207
  Future<void> ensureAnalyticsSent() async {
208 209 210
    // 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?
211
    await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
212
  }
213

214 215 216
  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.
217
    if (_printedWelcome) {
218
      return;
219
    }
220
    _printedWelcome = true;
221 222 223

    printStatus('');
    printStatus('''
Seth Ladd's avatar
Seth Ladd committed
224
  ╔════════════════════════════════════════════════════════════════════════════╗
225
  ║                 Welcome to Flutter! - https://flutter.dev                  ║
Seth Ladd's avatar
Seth Ladd committed
226
  ║                                                                            ║
227 228 229 230 231 232 233 234
  ║ 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
235 236
   https://www.google.com/intl/en/policies/privacy/                           ║
                                                                              
237 238
   Use "flutter config --no-analytics" to disable analytics and crash         
   reporting.                                                                 
Seth Ladd's avatar
Seth Ladd committed
239
  ╚════════════════════════════════════════════════════════════════════════════╝
240 241
  ''', emphasis: true);
  }
242
}
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257

// 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;

  @override
  Future<void> sendScreenView(String viewName, {Map<String, String> parameters}) {
    parameters ??= <String, String>{};
    parameters['viewName'] = viewName;
258 259 260 261 262 263 264 265 266 267 268
    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}) {
    parameters ??= <String, String>{};
    parameters['category'] = category;
    parameters['action'] = action;
    logFile.writeAsStringSync('event $parameters\n', mode: FileMode.append);
269 270 271
    return Future<void>.value(null);
  }
}