blob: e5e10de0bb6326b0c51f30f91da75cfb677f0140 [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. 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:convert';
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:telemetry/telemetry.dart' as telemetry show isRunningOnBot;
import 'package:usage/src/usage_impl.dart';
import 'package:usage/src/usage_impl_io.dart';
import 'package:usage/usage_io.dart';
const String analyticsNoticeOnFirstRunMessage = '''
╔════════════════════════════════════════════════════════════════════════════╗
║ The Dart tool uses Google Analytics to report feature usage statistics ║
║ and to send basic crash reports. This data is used to help improve the ║
║ Dart platform and tools over time. ║
║ ║
║ To disable reporting of analytics, run: ║
║ ║
║ dart --disable-analytics ║
║ ║
╚════════════════════════════════════════════════════════════════════════════╝
''';
const String analyticsDisabledNoticeMessage = '''
╔════════════════════════════════════════════════════════════════════════════╗
║ Analytics reporting disabled. In order to enable it, run: ║
║ ║
║ dart --enable-analytics ║
║ ║
╚════════════════════════════════════════════════════════════════════════════╝
''';
const String _appName = 'dartdev';
const String _dartDirectoryName = '.dart';
const String _settingsFileName = 'dartdev.json';
const String _trackingId = 'UA-26406144-37';
const String _readmeFileName = 'README.txt';
const String _readmeFileContents = '''
This directory contains user-level settings for the Dart programming language
(https://dart.dev).
''';
const String eventCategory = 'dartdev';
const String exitCodeParam = 'exitCode';
/// Disabled [Analytics], exposed for testing only
@visibleForTesting
Analytics get disabledAnalytics => DisabledAnalytics(_trackingId, _appName);
/// Create and return an [Analytics] instance.
Analytics createAnalyticsInstance(bool disableAnalytics) {
if (Platform.environment['_DARTDEV_LOG_ANALYTICS'] != null) {
// Used for testing what analytics messages are sent.
return _LoggingAnalytics();
}
if (disableAnalytics) {
// Dartdev tests pass a hidden 'no-analytics' flag which is
// handled here.
//
// Also, stdout.hasTerminal is checked; if there is no terminal we infer
// that a machine is running dartdev so we return that analytics shouldn't
// be set enabled.
return DisabledAnalytics(_trackingId, _appName);
}
final settingsDir = getDartStorageDirectory();
if (settingsDir == null) {
// Some systems don't support user home directories; for those, fail
// gracefully by returning a disabled analytics object.
return DisabledAnalytics(_trackingId, _appName);
}
if (!settingsDir.existsSync()) {
try {
settingsDir.createSync();
} catch (e) {
// If we can't create the directory for the analytics settings, fail
// gracefully by returning a disabled analytics object.
return DisabledAnalytics(_trackingId, _appName);
}
}
final readmeFile =
File('${settingsDir.absolute.path}${path.separator}$_readmeFileName');
if (!readmeFile.existsSync()) {
readmeFile.createSync();
readmeFile.writeAsStringSync(_readmeFileContents);
}
final settingsFile = File(path.join(settingsDir.path, _settingsFileName));
return DartdevAnalytics(_trackingId, settingsFile, _appName);
}
/// The directory used to store the analytics settings file.
///
/// Typically, the directory is `~/.dart/` (and the settings file is
/// `dartdev.json`).
///
/// This can return null under some conditions, including when the user's home
/// directory does not exist.
Directory getDartStorageDirectory() {
var homeDir = Directory(userHomeDir());
if (!homeDir.existsSync()) {
return null;
}
return Directory(path.join(homeDir.path, _dartDirectoryName));
}
/// The method used by dartdev to determine if this machine is a bot such as a
/// CI machine.
bool isBot() => telemetry.isRunningOnBot();
class DartdevAnalytics extends AnalyticsImpl {
DartdevAnalytics(String trackingId, File settingsFile, String appName)
: super(
trackingId,
IOPersistentProperties.fromFile(settingsFile),
IOPostHandler(),
applicationName: appName,
applicationVersion: getDartVersion(),
);
@override
bool get enabled {
// Don't enable if the user hasn't been shown the disclosure or if this
// machine is bot.
if (!disclosureShownOnTerminal || isBot()) {
return false;
}
// If there's no explicit setting (enabled or disabled) then we don't send.
return (properties['enabled'] as bool) ?? false;
}
bool get disclosureShownOnTerminal =>
(properties['disclosureShown'] as bool) ?? false;
set disclosureShownOnTerminal(bool value) {
properties['disclosureShown'] = value;
}
}
@visibleForTesting
class DisabledAnalytics extends AnalyticsMock {
@override
final String trackingId;
@override
final String applicationName;
DisabledAnalytics(this.trackingId, this.applicationName);
@override
bool get enabled => false;
@override
bool get firstRun => false;
}
class _LoggingAnalytics extends AnalyticsMock {
_LoggingAnalytics() {
onSend.listen((event) {
stderr.writeln('[analytics]${json.encode(event)}');
});
}
@override
bool get firstRun => false;
@override
Future sendScreenView(String viewName, {Map<String, String> parameters}) {
parameters ??= <String, String>{};
parameters['viewName'] = viewName;
return _log('screenView', parameters);
}
@override
Future sendEvent(String category, String action,
{String label, int value, Map<String, String> parameters}) {
parameters ??= <String, String>{};
return _log(
'event',
{'category': category, 'action': action, 'label': label, 'value': value}
..addAll(parameters));
}
@override
Future sendSocial(String network, String action, String target) =>
_log('social', {'network': network, 'action': action, 'target': target});
@override
Future sendTiming(String variableName, int time,
{String category, String label}) {
return _log('timing', {
'variableName': variableName,
'time': time,
'category': category,
'label': label
});
}
Future<void> _log(String hitType, Map message) async {
final encoded = json.encode({'hitType': hitType, 'message': message});
stderr.writeln('[analytics]: $encoded');
}
}