// Copyright (c) 2017, 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:io'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:usage/src/usage_impl.dart'; import 'package:usage/src/usage_impl_io.dart'; import 'package:usage/src/usage_impl_io.dart' as usage_io show getDartVersion; import 'package:usage/usage.dart'; import 'package:usage/usage_io.dart'; export 'package:usage/usage.dart' show Analytics; // TODO(devoncarew): Don't show the UI until we're ready to ship. final bool SHOW_ANALYTICS_UI = false; final String _dartDirectoryName = '.dart'; final String _settingsFileName = 'analytics.json'; /// Dart SDK tools with analytics should display this notice. /// /// In addition, they should support displaying the analytics' status, and have /// a flag to toggle analytics. This may look something like: /// /// `Analytics are currently enabled (and can be disabled with --no-analytics).` final String analyticsNotice = "Dart SDK tools anonymously report feature usage statistics and basic crash\n" "reports to help improve Dart tools over time. See Google's privacy policy:\n" "https://www.google.com/intl/en/policies/privacy/."; /// Return a customized message for command-line tools to display about the /// state of analytics, and how users can enabled or disable analytics. /// /// An example return value might be `'Analytics are currently enabled (and can /// be disabled with --no-analytics).'` String createAnalyticsStatusMessage( bool enabled, { String command: 'analytics', }) { String currentState = enabled ? 'enabled' : 'disabled'; String toggleState = enabled ? 'disabled' : 'enabled'; String commandToggle = enabled ? 'no-$command' : command; return 'Analytics are currently $currentState ' '(and can be $toggleState with --$commandToggle).'; } /// Create an [Analytics] instance with the given trackingID and /// applicationName. /// /// This analytics instance will share a common enablement state with the rest /// of the Dart SDK tools. Analytics createAnalyticsInstance( String trackingId, String applicationName, { bool disableForSession = false, bool forceEnabled = false, }) { final dir = getDartStorageDirectory(); if (dir == null) { // Some systems don't support user home directories; for those, fail // gracefully by returning a disabled analytics object. return new _DisabledAnalytics(trackingId, applicationName); } if (!dir.existsSync()) { try { dir.createSync(); } catch (e) { // If we can't create the directory for the analytics settings, fail // gracefully by returning a disabled analytics object. return new _DisabledAnalytics(trackingId, applicationName); } } File settingsFile = new File(path.join(dir.path, _settingsFileName)); return new _TelemetryAnalytics( trackingId, applicationName, getDartVersion(), settingsFile, disableForSession: disableForSession, forceEnabled: forceEnabled, ); } /// The directory used to store the analytics settings file. /// /// Typically, the directory is `~/.dart/` (and the settings file is /// `analytics.json`). /// /// This can return null under some conditions, including when the user's home /// directory does not exist. @visibleForTesting Directory? getDartStorageDirectory() { Directory homeDirectory = new Directory(userHomeDir()); if (!homeDirectory.existsSync()) return null; return new Directory(path.join(homeDirectory.path, _dartDirectoryName)); } /// Return the version of the Dart SDK. String getDartVersion() => usage_io.getDartVersion(); class _TelemetryAnalytics extends AnalyticsImpl { final bool disableForSession; final bool forceEnabled; _TelemetryAnalytics( String trackingId, String applicationName, String applicationVersion, File settingsFile, { required this.disableForSession, required this.forceEnabled, }) : super( trackingId, new IOPersistentProperties.fromFile(settingsFile), new IOPostHandler(), applicationName: applicationName, applicationVersion: applicationVersion, ) { final locale = getPlatformLocale(); if (locale != null) { setSessionValue('ul', locale); } } @override bool get enabled { if (disableForSession || isRunningOnBot()) { return false; } // This is only used in special cases. if (forceEnabled) { return true; } // If there's no explicit setting (enabled or disabled) then we don't send. return (properties['enabled'] as bool?) ?? false; } } class _DisabledAnalytics extends AnalyticsMock { @override final String trackingId; @override final String applicationName; _DisabledAnalytics(this.trackingId, this.applicationName); @override bool get enabled => false; } /// Detect whether we're running on a bot or in a continuous testing /// environment. /// /// We should periodically keep this code up to date with: /// https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/base/bot_detector.dart#L30 /// and /// https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/reporting/usage.dart#L200. bool isRunningOnBot() { final Map env = Platform.environment; if ( // Explicitly stated to not be a bot. env['BOT'] == 'false' // Set by the IDEs to the IDE name, so a strong signal that this is not a bot. || env.containsKey('FLUTTER_HOST') // When set, GA logs to a local file (normally for tests) so we don't need to filter. || env.containsKey('FLUTTER_ANALYTICS_LOG_FILE')) { return false; } return env.containsKey('BOT') // https://docs.travis-ci.com/user/environment-variables/ // Example .travis.yml file: https://github.com/flutter/devtools/blob/master/.travis.yml || env['TRAVIS'] == 'true' || env['CONTINUOUS_INTEGRATION'] == 'true' || env.containsKey('CI') // Travis and AppVeyor // https://www.appveyor.com/docs/environment-variables/ || env.containsKey('APPVEYOR') // https://cirrus-ci.org/guide/writing-tasks/#environment-variables || env.containsKey('CIRRUS_CI') // https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html || (env.containsKey('AWS_REGION') && env.containsKey('CODEBUILD_INITIATOR')) // https://wiki.jenkins.io/display/JENKINS/Building+a+software+project#Buildingasoftwareproject-belowJenkinsSetEnvironmentVariables || env.containsKey('JENKINS_URL') // https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables || env.containsKey('GITHUB_ACTIONS') // Properties on Flutter's Chrome Infra bots. || env['CHROME_HEADLESS'] == '1' || env.containsKey('BUILDBOT_BUILDERNAME') || env.containsKey('SWARMING_TASK_ID') // Property when running on borg. || env.containsKey('BORG_ALLOC_DIR'); // TODO(jwren): Azure detection -- each call for this detection requires an // http connection, the flutter cli tool captures the result on the first // run, we should consider the same caching here. }