part of reporting;
const String _kFlutterUA = 'UA-67589403-6';
enum CustomDimensions {
sessionHostOsDetails,
sessionChannelName,
commandRunIsEmulator,
commandRunTargetName,
hotEventReason,
hotEventFinalLibraryCount,
hotEventSyncedLibraryCount,
hotEventSyncedClassesCount,
hotEventSyncedProceduresCount,
hotEventSyncedBytes,
hotEventInvalidatedSourcesCount,
hotEventTransferTimeInMs,
hotEventOverallTimeInMs,
commandRunProjectType,
commandRunProjectHostLanguage,
commandCreateAndroidLanguage,
commandCreateIosLanguage,
commandRunProjectModule,
commandCreateProjectType,
commandPackagesNumberPlugins,
commandPackagesProjectModule,
commandRunTargetOsVersion,
commandRunModeName,
commandBuildBundleTargetPlatform,
commandBuildBundleIsModule,
commandResult,
hotEventTargetPlatform,
hotEventSdkName,
hotEventEmulator,
hotEventFullRestart,
commandHasTerminal,
enabledFlutterFeatures,
localTime,
commandBuildAarTargetPlatform,
commandBuildAarProjectType,
buildEventCommand,
buildEventSettings,
}
String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}';
Map<String, String> _useCdKeys(Map<CustomDimensions, String> parameters) {
return parameters.map((CustomDimensions k, String v) =>
MapEntry<String, String>(cdKey(k), v));
}
Usage get flutterUsage => Usage.instance;
abstract class Usage {
factory Usage({
String settingsName = 'flutter',
String versionOverride,
String configDirOverride,
String logFile,
}) => _DefaultUsage(settingsName: settingsName,
versionOverride: versionOverride,
configDirOverride: configDirOverride,
logFile: logFile);
static Usage get instance => context.get<Usage>();
static void command(String command, {
Map<CustomDimensions, String> parameters,
}) => flutterUsage.sendCommand(command, parameters: _useCdKeys(parameters));
bool get isFirstRun;
bool get suppressAnalytics;
set suppressAnalytics(bool value);
bool get enabled;
set enabled(bool value);
String get clientId;
void sendCommand(String command, {
Map<String, String> parameters
});
@visibleForOverriding
@visibleForTesting
void sendEvent(String category, String parameter, {
Map<String, String> parameters
});
void sendTiming(String category, String variableName, Duration duration, {
String label
});
void sendException(dynamic exception);
@visibleForTesting
Stream<Map<String, dynamic>> get onSend;
Future<void> ensureAnalyticsSent();
void printWelcome();
}
class _DefaultUsage implements Usage {
_DefaultUsage({
String settingsName = 'flutter',
String versionOverride,
String configDirOverride,
String logFile,
}) {
final FlutterVersion flutterVersion = FlutterVersion.instance;
final String version = versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true);
final bool suppressEnvFlag = platform.environment['FLUTTER_SUPPRESS_ANALYTICS'] == 'true';
final String logFilePath = logFile ?? platform.environment['FLUTTER_ANALYTICS_LOG_FILE'];
final bool usingLogFile = logFilePath != null && logFilePath.isNotEmpty;
if (
!usingLogFile && (
version.startsWith('[user-branch]') ||
version.endsWith('/unknown') ||
isRunningOnBot ||
suppressEnvFlag
)) {
suppressAnalytics = true;
_analytics = AnalyticsMock();
return;
}
if (usingLogFile) {
_analytics = LogToFileAnalytics(logFilePath);
} else {
_analytics = AnalyticsIO(
_kFlutterUA,
settingsName,
version,
documentDirectory:
configDirOverride != null ? fs.directory(configDirOverride) : null,
);
}
assert(_analytics != null);
_analytics.setSessionValue(cdKey(CustomDimensions.sessionHostOsDetails), os.name);
_analytics.setSessionValue(cdKey(CustomDimensions.sessionChannelName),
flutterVersion.getBranchName(redactUnknownBranches: true));
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(cdKey(CustomDimensions.enabledFlutterFeatures), enabledFeatures);
if (platform.environment.containsKey('FLUTTER_HOST')) {
_analytics.setSessionValue('aiid', platform.environment['FLUTTER_HOST']);
}
_analytics.analyticsOpt = AnalyticsOpt.optOut;
}
Analytics _analytics;
bool _printedWelcome = false;
bool _suppressAnalytics = false;
@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(systemClock.now()),
};
_analytics.sendScreenView(command, parameters: paramsWithLocalTime);
}
@override
void sendEvent(
String category,
String parameter, {
Map<String, String> parameters,
}) {
if (suppressAnalytics) {
return;
}
final Map<String, String> paramsWithLocalTime = <String, String>{
...?parameters,
cdKey(CustomDimensions.localTime): formatDateTime(systemClock.now()),
};
_analytics.sendEvent(category, parameter, 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 {
await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
}
@override
void printWelcome() {
if (_printedWelcome) {
return;
}
_printedWelcome = true;
printStatus('');
printStatus('''
╔════════════════════════════════════════════════════════════════════════════╗
║ Welcome to Flutter! - https://flutter.dev ║
║ ║
║ 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: ║
║ https://www.google.com/intl/en/policies/privacy/ ║
║ ║
║ Use "flutter config --no-analytics" to disable analytics and crash ║
║ reporting. ║
╚════════════════════════════════════════════════════════════════════════════╝
''', emphasis: true);
}
}
class LogToFileAnalytics extends AnalyticsMock {
LogToFileAnalytics(String logFilePath) :
logFile = 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();
}
}