• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.dev316  ║                                                                            ║
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:                                ║
322https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting323  ║                                                                            ║
324  ║ See Google's privacy policy:                                               ║
325https://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