Unverified Commit 372fe290 authored by Zachary Anderson's avatar Zachary Anderson Committed by GitHub

[flutter_tool] Update analytics policy, send event on disable (#43217)

parent f6eb1295
......@@ -215,9 +215,8 @@ Future<String> _doctorText() async {
}
Future<int> _exit(int code) async {
if (flutterUsage.isFirstRun) {
flutterUsage.printWelcome();
}
// Prints the welcome message if needed.
flutterUsage.printWelcome();
// Send any last analytics calls that are in progress without overly delaying
// the tool's exit (we wait a maximum of 250ms).
......
......@@ -5,11 +5,10 @@
import '../convert.dart';
import 'context.dart';
import 'file_system.dart';
import 'platform.dart';
class Config {
Config([File configFile]) {
_configFile = configFile ?? fs.file(fs.path.join(_userHomeDir(), '.flutter_settings'));
_configFile = configFile ?? fs.file(fs.path.join(userHomePath(), '.flutter_settings'));
if (_configFile.existsSync()) {
_values = json.decode(_configFile.readAsStringSync());
}
......@@ -44,8 +43,3 @@ class Config {
_configFile.writeAsStringSync(json);
}
}
String _userHomeDir() {
final String envKey = platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
return platform.environment[envKey] ?? '.';
}
......@@ -185,3 +185,11 @@ class FileNotFoundException implements IOException {
@override
String toString() => 'File not found: $path';
}
/// Reads the process environment to find the current user's home directory.
///
/// If the searched environment variables are not set, '.' is returned instead.
String userHomePath() {
final String envKey = platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
return platform.environment[envKey] ?? '.';
}
......@@ -117,6 +117,7 @@ class ConfigCommand extends FlutterCommand {
if (argResults.wasParsed('analytics')) {
final bool value = argResults['analytics'];
flutterUsage.enabled = value;
AnalyticsConfigEvent(enabled: value).send();
printStatus('Analytics reporting ${value ? 'enabled' : 'disabled'}.');
}
......
......@@ -15,12 +15,14 @@ import '../base/process.dart';
import '../cache.dart';
import '../dart/pub.dart';
import '../globals.dart';
import '../persistent_tool_state.dart';
import '../runner/flutter_command.dart';
import '../version.dart';
import 'channel.dart';
class UpgradeCommand extends FlutterCommand {
UpgradeCommand() {
UpgradeCommand([UpgradeCommandRunner commandRunner])
: _commandRunner = commandRunner ?? UpgradeCommandRunner() {
argParser
..addFlag(
'force',
......@@ -32,10 +34,14 @@ class UpgradeCommand extends FlutterCommand {
'continue',
hide: true,
negatable: false,
help: 'For the second half of the upgrade flow requiring the new version of Flutter. Should not be invoked manually, but re-entrantly by the standard upgrade command.',
help: 'For the second half of the upgrade flow requiring the new '
'version of Flutter. Should not be invoked manually, but '
're-entrantly by the standard upgrade command.',
);
}
final UpgradeCommandRunner _commandRunner;
@override
final String name = 'upgrade';
......@@ -52,8 +58,7 @@ class UpgradeCommand extends FlutterCommand {
@override
Future<FlutterCommandResult> runCommand() async {
final UpgradeCommandRunner upgradeCommandRunner = UpgradeCommandRunner();
await upgradeCommandRunner.runCommand(
await _commandRunner.runCommand(
argResults['force'],
argResults['continue'],
GitTagVersion.determine(),
......@@ -141,9 +146,13 @@ class UpgradeCommandRunner {
// This method should only be called if the upgrade command is invoked
// re-entrantly with the `--continue` flag
Future<void> runCommandSecondHalf(FlutterVersion flutterVersion) async {
// Make sure the welcome message re-display is delayed until the end.
persistentToolState.redisplayWelcomeMessage = false;
await precacheArtifacts();
await updatePackages(flutterVersion);
await runDoctor();
// Force the welcome message to re-display following the upgrade.
persistentToolState.redisplayWelcomeMessage = true;
}
Future<bool> hasUncomittedChanges() async {
......
......@@ -48,6 +48,7 @@ import 'macos/macos_workflow.dart';
import 'macos/xcode.dart';
import 'macos/xcode_validator.dart';
import 'mdns_discovery.dart';
import 'persistent_tool_state.dart';
import 'reporting/reporting.dart';
import 'run_hot.dart';
import 'version.dart';
......@@ -106,9 +107,10 @@ Future<T> runInContext<T>(
MacOSWorkflow: () => const MacOSWorkflow(),
MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery(),
OperatingSystemUtils: () => OperatingSystemUtils(),
Pub: () => const Pub(),
PersistentToolState: () => PersistentToolState(),
ProcessInfo: () => ProcessInfo(),
ProcessUtils: () => ProcessUtils(),
Pub: () => const Pub(),
Signals: () => Signals(),
SimControl: () => SimControl(),
Stdio: () => const Stdio(),
......@@ -121,8 +123,8 @@ Future<T> runInContext<T>(
WebWorkflow: () => const WebWorkflow(),
WindowsWorkflow: () => const WindowsWorkflow(),
Xcode: () => Xcode(),
XcodeValidator: () => const XcodeValidator(),
XcodeProjectInterpreter: () => XcodeProjectInterpreter(),
XcodeValidator: () => const XcodeValidator(),
},
);
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'base/config.dart';
import 'base/context.dart';
import 'base/file_system.dart';
PersistentToolState get persistentToolState => PersistentToolState.instance;
/// A class that represents global (non-project-specific) internal state that
/// must persist across tool invocations.
abstract class PersistentToolState {
factory PersistentToolState([File configFile]) =>
_DefaultPersistentToolState(configFile);
static PersistentToolState get instance => context.get<PersistentToolState>();
/// Whether the welcome message should be redisplayed.
///
/// May give null if the value has not been set.
bool redisplayWelcomeMessage;
}
class _DefaultPersistentToolState implements PersistentToolState {
_DefaultPersistentToolState([File configFile]) :
_config = Config(configFile ?? fs.file(fs.path.join(userHomePath(), _kFileName)));
static const String _kFileName = '.flutter_tool_state';
static const String _kRedisplayWelcomeMessage = 'redisplay-welcome-message';
final Config _config;
@override
bool get redisplayWelcomeMessage => _config.getValue(_kRedisplayWelcomeMessage);
@override
set redisplayWelcomeMessage(bool value) {
_config.setValue(_kRedisplayWelcomeMessage, value);
}
}
......@@ -199,3 +199,15 @@ class CommandResultEvent extends UsageEvent {
}
}
}
/// An event that reports on changes in the configuration of analytics.
class AnalyticsConfigEvent extends UsageEvent {
AnalyticsConfigEvent({
/// Whether analytics reporting is being enabled (true) or disabled (false).
@required bool enabled,
}) : super(
'analytics',
'enabled',
label: enabled ? 'true' : 'false',
);
}
......@@ -21,6 +21,7 @@ import '../base/utils.dart';
import '../doctor.dart';
import '../features.dart';
import '../globals.dart';
import '../persistent_tool_state.dart';
import '../runner/flutter_command.dart';
import '../version.dart';
......
......@@ -323,35 +323,55 @@ class _DefaultUsage implements Usage {
await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
}
@override
void printWelcome() {
// This gets called if it's the first run by the selected command, if any,
// and on exit, in case there was no command.
if (_printedWelcome) {
return;
}
_printedWelcome = true;
void _printWelcome() {
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. ║
║ 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://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);
}
@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.
(!FlutterVersion.instance.isMaster &&
(persistentToolState.redisplayWelcomeMessage ?? true))) {
_printWelcome();
_printedWelcome = true;
persistentToolState.redisplayWelcomeMessage = false;
}
}
}
// An Analytics mock that logs to file. Unimplemented methods goes to stdout.
......
......@@ -447,9 +447,8 @@ abstract class FlutterCommand extends Command<void> {
name: 'command',
overrides: <Type, Generator>{FlutterCommand: () => this},
body: () async {
if (flutterUsage.isFirstRun) {
flutterUsage.printWelcome();
}
// Prints the welcome message if needed.
flutterUsage.printWelcome();
final String commandPath = await usagePath;
_registerSignalHandlers(commandPath, startTime);
FlutterCommandResult commandResult;
......
......@@ -14,6 +14,7 @@ import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/config.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart';
......@@ -24,6 +25,7 @@ void main() {
MockAndroidStudio mockAndroidStudio;
MockAndroidSdk mockAndroidSdk;
MockFlutterVersion mockFlutterVersion;
MockUsage mockUsage;
setUpAll(() {
Cache.disableLocking();
......@@ -33,8 +35,31 @@ void main() {
mockAndroidStudio = MockAndroidStudio();
mockAndroidSdk = MockAndroidSdk();
mockFlutterVersion = MockFlutterVersion();
mockUsage = MockUsage();
when(mockUsage.isFirstRun).thenReturn(false);
});
void verifyNoAnalytics() {
verifyNever(mockUsage.sendCommand(
any,
parameters: anyNamed('parameters'),
));
verifyNever(mockUsage.sendEvent(
any,
any,
label: anyNamed('label'),
value: anyNamed('value'),
parameters: anyNamed('parameters'),
));
verifyNever(mockUsage.sendTiming(
any,
any,
any,
label: anyNamed('label'),
));
}
group('config', () {
testUsingContext('machine flag', () async {
final BufferLogger logger = context.get<Logger>();
......@@ -50,9 +75,11 @@ void main() {
expect(jsonObject.containsKey('android-sdk'), true);
expect(jsonObject['android-sdk'], isNotNull);
verifyNoAnalytics();
}, overrides: <Type, Generator>{
AndroidStudio: () => mockAndroidStudio,
AndroidSdk: () => mockAndroidSdk,
Usage: () => mockUsage,
});
testUsingContext('Can set build-dir', () async {
......@@ -65,6 +92,9 @@ void main() {
]);
expect(getBuildDirectory(), 'foo');
verifyNoAnalytics();
}, overrides: <Type, Generator>{
Usage: () => mockUsage,
});
testUsingContext('throws error on absolute path to build-dir', () async {
......@@ -75,6 +105,9 @@ void main() {
'config',
'--build-dir=/foo',
]), throwsA(isInstanceOf<ToolExit>()));
verifyNoAnalytics();
}, overrides: <Type, Generator>{
Usage: () => mockUsage,
});
testUsingContext('allows setting and removing feature flags', () async {
......@@ -115,9 +148,11 @@ void main() {
expect(Config.instance.getValue('enable-linux-desktop'), false);
expect(Config.instance.getValue('enable-windows-desktop'), false);
expect(Config.instance.getValue('enable-macos-desktop'), false);
verifyNoAnalytics();
}, overrides: <Type, Generator>{
AndroidStudio: () => mockAndroidStudio,
AndroidSdk: () => mockAndroidSdk,
Usage: () => mockUsage,
});
testUsingContext('displays which config settings are available on stable', () async {
......@@ -142,10 +177,86 @@ void main() {
expect(logger.statusText, contains('enable-linux-desktop: true (Unavailable)'));
expect(logger.statusText, contains('enable-windows-desktop: true (Unavailable)'));
expect(logger.statusText, contains('enable-macos-desktop: true (Unavailable)'));
verifyNoAnalytics();
}, overrides: <Type, Generator>{
AndroidStudio: () => mockAndroidStudio,
AndroidSdk: () => mockAndroidSdk,
FlutterVersion: () => mockFlutterVersion,
Usage: () => mockUsage,
});
testUsingContext('no-analytics flag flips usage flag and sends event', () async {
final ConfigCommand configCommand = ConfigCommand();
final CommandRunner<void> commandRunner = createTestCommandRunner(configCommand);
await commandRunner.run(<String>[
'config',
'--no-analytics',
]);
expect(mockUsage.enabled, false);
// Verify that we only send the analytics disable event, and no other
// info.
verifyNever(mockUsage.sendCommand(
any,
parameters: anyNamed('parameters'),
));
verifyNever(mockUsage.sendTiming(
any,
any,
any,
label: anyNamed('label'),
));
expect(verify(mockUsage.sendEvent(
captureAny,
captureAny,
label: captureAnyNamed('label'),
value: anyNamed('value'),
parameters: anyNamed('parameters'),
)).captured,
<dynamic>['analytics', 'enabled', 'false'],
);
}, overrides: <Type, Generator>{
Usage: () => mockUsage,
});
testUsingContext('analytics flag flips usage flag and sends event', () async {
final ConfigCommand configCommand = ConfigCommand();
final CommandRunner<void> commandRunner = createTestCommandRunner(configCommand);
await commandRunner.run(<String>[
'config',
'--analytics',
]);
expect(mockUsage.enabled, true);
// Verify that we only send the analytics disable event, and no other
// info.
verifyNever(mockUsage.sendCommand(
any,
parameters: anyNamed('parameters'),
));
verifyNever(mockUsage.sendTiming(
any,
any,
any,
label: anyNamed('label'),
));
expect(verify(mockUsage.sendEvent(
captureAny,
captureAny,
label: captureAnyNamed('label'),
value: anyNamed('value'),
parameters: anyNamed('parameters'),
)).captured,
<dynamic>['analytics', 'enabled', 'true'],
);
}, overrides: <Type, Generator>{
Usage: () => mockUsage,
});
});
}
......@@ -161,3 +272,8 @@ class MockAndroidSdk extends Mock implements AndroidSdk {
}
class MockFlutterVersion extends Mock implements FlutterVersion {}
class MockUsage extends Mock implements Usage {
@override
bool enabled = true;
}
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools/runner.dart' as runner;
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
......@@ -9,6 +10,7 @@ import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/upgrade.dart';
import 'package:flutter_tools/src/persistent_tool_state.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart';
......@@ -17,6 +19,7 @@ import 'package:process/process.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
import '../../src/mocks.dart';
void main() {
......@@ -184,6 +187,49 @@ void main() {
ProcessManager: () => processManager,
Platform: () => fakePlatform,
});
group('full command', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>[
'git', 'describe', '--match', 'v*.*.*', '--first-parent', '--long', '--tags',
]),
]);
Directory tempDir;
File flutterToolState;
FlutterVersion mockFlutterVersion;
setUp(() {
Cache.disableLocking();
tempDir = fs.systemTempDirectory.createTempSync('flutter_upgrade_test.');
flutterToolState = tempDir.childFile('.flutter_tool_state');
mockFlutterVersion = MockFlutterVersion(isStable: true);
});
tearDown(() {
Cache.enableLocking();
tryToDelete(tempDir);
});
testUsingContext('upgrade continue prints welcome message', () async {
final UpgradeCommand upgradeCommand = UpgradeCommand(fakeCommandRunner);
await runner.run(
<String>[
'upgrade',
'--continue',
],
<FlutterCommand>[
upgradeCommand,
],
);
expect(testLogger.statusText, contains('Welcome to Flutter!'));
}, overrides: <Type, Generator>{
FlutterVersion: () => mockFlutterVersion,
ProcessManager: () => fakeProcessManager,
PersistentToolState: () => PersistentToolState(flutterToolState),
});
});
});
group('matchesGitLine', () {
......@@ -267,7 +313,6 @@ class FakeUpgradeCommandRunner extends UpgradeCommandRunner {
Future<void> runDoctor() async {}
}
class MockFlutterVersion extends Mock implements FlutterVersion {}
class MockProcess extends Mock implements Process {}
class MockProcessManager extends Mock implements ProcessManager {}
class FakeProcessResult implements ProcessResult {
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/persistent_tool_state.dart';
import '../src/common.dart';
import '../src/testbed.dart';
void main() {
Testbed testbed;
setUp(() {
testbed = Testbed();
});
test('state can be set and persists', () => testbed.run(() {
final File stateFile = fs.file('.flutter_tool_state');
final PersistentToolState state1 = PersistentToolState(stateFile);
expect(state1.redisplayWelcomeMessage, null);
state1.redisplayWelcomeMessage = true;
expect(stateFile.existsSync(), true);
expect(state1.redisplayWelcomeMessage, true);
state1.redisplayWelcomeMessage = false;
expect(state1.redisplayWelcomeMessage, false);
final PersistentToolState state2 = PersistentToolState(stateFile);
expect(state2.redisplayWelcomeMessage, false);
}));
}
......@@ -23,6 +23,7 @@ import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/ios/simulators.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/persistent_tool_state.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/version.dart';
......@@ -71,12 +72,18 @@ void testUsingContext(
}
});
Config buildConfig(FileSystem fs) {
configDir = fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
configDir ??= fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
final File settingsFile = fs.file(
fs.path.join(configDir.path, '.flutter_settings')
);
return Config(settingsFile);
}
PersistentToolState buildPersistentToolState(FileSystem fs) {
configDir ??= fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
final File toolStateFile = fs.file(
fs.path.join(configDir.path, '.flutter_tool_state'));
return PersistentToolState(toolStateFile);
}
test(description, () async {
await runInContext<dynamic>(() {
......@@ -96,6 +103,7 @@ void testUsingContext(
OutputPreferences: () => OutputPreferences.test(),
Logger: () => BufferLogger(),
OperatingSystemUtils: () => FakeOperatingSystemUtils(),
PersistentToolState: () => buildPersistentToolState(fs),
SimControl: () => MockSimControl(),
Usage: () => FakeUsage(),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
......
......@@ -64,7 +64,7 @@ final Map<Type, Generator> _testbedDefaults = <Type, Generator>{
/// });
/// })
///
/// test('Can delete a file', () => testBed.run(() {
/// test('Can delete a file', () => testbed.run(() {
/// expect(fs.file('foo').existsSync(), true);
/// fs.file('foo').deleteSync();
/// expect(fs.file('foo').existsSync(), false);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment