blob: cdff5ae512ef24c2c9472a1e651b6b795a6e7136 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of reporting;
const String _kFlutterUA = 'UA-67589403-6';
/// 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 relevant 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
commandBuildApkTargetPlatform, // cd38
commandBuildApkBuildMode, // cd39
commandBuildApkSplitPerAbi, // cd40
commandBuildAppBundleTargetPlatform, // cd41
commandBuildAppBundleBuildMode, // cd42
buildEventError, // cd43
commandResultEventMaxRss, // cd44
commandRunAndroidEmbeddingVersion, // cd45
commandPackagesAndroidEmbeddingVersion, // cd46
nullSafety, // cd47
fastReassemble, // cd48
}
String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}';
Map<String, String> _useCdKeys(Map<CustomDimensions, Object> parameters) {
return parameters.map((CustomDimensions k, Object v) =>
MapEntry<String, String>(cdKey(k), v.toString()));
}
abstract class Usage {
/// Create a new Usage instance; [versionOverride], [configDirOverride], and
/// [logFile] are used for testing.
factory Usage({
String settingsName = 'flutter',
String versionOverride,
String configDirOverride,
String logFile,
AnalyticsFactory analyticsIOFactory,
@required bool runningOnBot,
}) => _DefaultUsage(settingsName: settingsName,
versionOverride: versionOverride,
configDirOverride: configDirOverride,
logFile: logFile,
analyticsIOFactory: analyticsIOFactory,
runningOnBot: runningOnBot);
factory Usage.test() => _DefaultUsage.test();
/// Uses the global [Usage] instance to send a 'command' to analytics.
static void command(String command, {
Map<CustomDimensions, Object> parameters,
}) => globals.flutterUsage.sendCommand(command, parameters: _useCdKeys(parameters));
/// Whether this is the first run of the tool.
bool get isFirstRun;
/// Whether analytics reporting should be suppressed.
bool get suppressAnalytics;
/// Suppress analytics for this session.
set suppressAnalytics(bool value);
/// Whether analytics reporting is enabled.
bool get enabled;
/// 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.
void sendCommand(
String command, {
Map<String, String> parameters,
});
/// 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
void sendEvent(
String category,
String parameter, {
String label,
int value,
Map<String, String> parameters,
});
/// Sends timing information to the underlying analytics implementation.
void sendTiming(
String category,
String variableName,
Duration duration, {
String label,
});
/// Sends an exception to the underlying analytics implementation.
void sendException(dynamic exception);
/// 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();
}
typedef AnalyticsFactory = Analytics Function(
String trackingId,
String applicationName,
String applicationVersion, {
String analyticsUrl,
Directory documentDirectory,
});
Analytics _defaultAnalyticsIOFactory(
String trackingId,
String applicationName,
String applicationVersion, {
String analyticsUrl,
Directory documentDirectory,
}) {
return AnalyticsIO(
trackingId,
applicationName,
applicationVersion,
analyticsUrl: analyticsUrl,
documentDirectory: documentDirectory,
);
}
class _DefaultUsage implements Usage {
_DefaultUsage({
String settingsName = 'flutter',
String versionOverride,
String configDirOverride,
String logFile,
AnalyticsFactory analyticsIOFactory,
@required bool runningOnBot,
}) {
final FlutterVersion flutterVersion = globals.flutterVersion;
final String version = versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true);
final bool suppressEnvFlag = globals.platform.environment['FLUTTER_SUPPRESS_ANALYTICS'] == 'true';
final String logFilePath = logFile ?? globals.platform.environment['FLUTTER_ANALYTICS_LOG_FILE'];
final bool usingLogFile = logFilePath != null && logFilePath.isNotEmpty;
analyticsIOFactory ??= _defaultAnalyticsIOFactory;
_clock = globals.systemClock;
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.
runningOnBot ||
// 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;
}
if (usingLogFile) {
_analytics = LogToFileAnalytics(logFilePath);
} else {
try {
ErrorHandlingFileSystem.noExitOnFailure(() {
_analytics = analyticsIOFactory(
_kFlutterUA,
settingsName,
version,
documentDirectory: configDirOverride != null
? globals.fs.directory(configDirOverride)
: null,
);
});
} on Exception catch (e) {
globals.printTrace('Failed to initialize analytics reporting: $e');
suppressAnalytics = true;
_analytics = AnalyticsMock();
return;
}
}
assert(_analytics != null);
// Report a more detailed OS version string than package:usage does by default.
_analytics.setSessionValue(
cdKey(CustomDimensions.sessionHostOsDetails),
globals.os.name,
);
// Send the branch name as the "channel".
_analytics.setSessionValue(
cdKey(CustomDimensions.sessionChannelName),
flutterVersion.getBranchName(redactUnknownBranches: true),
);
// 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 &&
globals.config.getValue(feature.configSetting) == true;
})
.map((Feature feature) => feature.configSetting)
.join(',');
_analytics.setSessionValue(
cdKey(CustomDimensions.enabledFlutterFeatures),
enabledFeatures,
);
// Record the host as the application installer ID - the context that flutter_tools is running in.
if (globals.platform.environment.containsKey('FLUTTER_HOST')) {
_analytics.setSessionValue('aiid', globals.platform.environment['FLUTTER_HOST']);
}
_analytics.analyticsOpt = AnalyticsOpt.optOut;
}
_DefaultUsage.test() :
_suppressAnalytics = false,
_analytics = AnalyticsMock(true),
_clock = SystemClock.fixed(DateTime(2020, 10, 8));
Analytics _analytics;
bool _printedWelcome = false;
bool _suppressAnalytics = false;
SystemClock _clock;
@override
bool get isFirstRun => _analytics.firstRun;
@override
bool get suppressAnalytics => _suppressAnalytics || _analytics.firstRun;
@override
set suppressAnalytics(bool value) {
_suppressAnalytics = value;
}
@override
bool get enabled => _analytics.enabled;
@override
set enabled(bool value) {
_analytics.enabled = value;
}
@override
String get clientId => _analytics.clientId;
@override
void sendCommand(String command, { Map<String, String> parameters }) {
if (suppressAnalytics) {
return;
}
final Map<String, String> paramsWithLocalTime = <String, String>{
...?parameters,
cdKey(CustomDimensions.localTime): formatDateTime(_clock.now()),
};
_analytics.sendScreenView(command, parameters: paramsWithLocalTime);
}
@override
void sendEvent(
String category,
String parameter, {
String label,
int value,
Map<String, String> parameters,
}) {
if (suppressAnalytics) {
return;
}
final Map<String, String> paramsWithLocalTime = <String, String>{
...?parameters,
cdKey(CustomDimensions.localTime): formatDateTime(_clock.now()),
};
_analytics.sendEvent(
category,
parameter,
label: label,
value: value,
parameters: paramsWithLocalTime,
);
}
@override
void sendTiming(
String category,
String variableName,
Duration duration, {
String label,
}) {
if (suppressAnalytics) {
return;
}
_analytics.sendTiming(
variableName,
duration.inMilliseconds,
category: category,
label: label,
);
}
@override
void sendException(dynamic exception) {
if (suppressAnalytics) {
return;
}
_analytics.sendException(exception.runtimeType.toString());
}
@override
Stream<Map<String, dynamic>> get onSend => _analytics.onSend;
@override
Future<void> ensureAnalyticsSent() async {
// 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?
await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
}
void _printWelcome() {
globals.printStatus('');
globals.printStatus('''
╔════════════════════════════════════════════════════════════════════════════╗
║ Welcome to Flutter! - https://flutter.dev ║
║ ║
║ 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. ║
║ ║
║ Read about data we send with crash reports: ║
║ https://flutter.dev/docs/reference/crash-reporting ║
║ ║
║ See Google's privacy policy: ║
║ https://policies.google.com/privacy ║
╚════════════════════════════════════════════════════════════════════════════╝
''', emphasis: true);
}
@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.
(!globals.flutterVersion.isMaster &&
(globals.persistentToolState.redisplayWelcomeMessage ?? true))) {
_printWelcome();
_printedWelcome = true;
globals.persistentToolState.redisplayWelcomeMessage = false;
}
}
}
// 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 = globals.fs.file(logFilePath)..createSync(recursive: true),
super(true);
final File logFile;
final Map<String, String> _sessionValues = <String, String>{};
final StreamController<Map<String, dynamic>> _sendController =
StreamController<Map<String, dynamic>>.broadcast(sync: true);
@override
Stream<Map<String, dynamic>> get onSend => _sendController.stream;
@override
Future<void> sendScreenView(String viewName, {
Map<String, String> parameters,
}) {
if (!enabled) {
return Future<void>.value(null);
}
parameters ??= <String, String>{};
parameters['viewName'] = viewName;
parameters.addAll(_sessionValues);
_sendController.add(parameters);
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}) {
if (!enabled) {
return Future<void>.value(null);
}
parameters ??= <String, String>{};
parameters['category'] = category;
parameters['action'] = action;
_sendController.add(parameters);
logFile.writeAsStringSync('event $parameters\n', mode: FileMode.append);
return Future<void>.value(null);
}
@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);
}
@override
void setSessionValue(String param, dynamic value) {
_sessionValues[param] = value.toString();
}
}