Unverified Commit e8778da1 authored by Zachary Anderson's avatar Zachary Anderson Committed by GitHub

[flutter_tools] Don't crash when analytics fails to initialize (#52775)

parent 1444e772
...@@ -74,11 +74,13 @@ abstract class Usage { ...@@ -74,11 +74,13 @@ abstract class Usage {
String versionOverride, String versionOverride,
String configDirOverride, String configDirOverride,
String logFile, String logFile,
AnalyticsFactory analyticsIOFactory,
@required bool runningOnBot, @required bool runningOnBot,
}) => _DefaultUsage(settingsName: settingsName, }) => _DefaultUsage(settingsName: settingsName,
versionOverride: versionOverride, versionOverride: versionOverride,
configDirOverride: configDirOverride, configDirOverride: configDirOverride,
logFile: logFile, logFile: logFile,
analyticsIOFactory: analyticsIOFactory,
runningOnBot: runningOnBot); runningOnBot: runningOnBot);
/// Uses the global [Usage] instance to send a 'command' to analytics. /// Uses the global [Usage] instance to send a 'command' to analytics.
...@@ -152,12 +154,37 @@ abstract class Usage { ...@@ -152,12 +154,37 @@ abstract class Usage {
void printWelcome(); 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 { class _DefaultUsage implements Usage {
_DefaultUsage({ _DefaultUsage({
String settingsName = 'flutter', String settingsName = 'flutter',
String versionOverride, String versionOverride,
String configDirOverride, String configDirOverride,
String logFile, String logFile,
AnalyticsFactory analyticsIOFactory,
@required bool runningOnBot, @required bool runningOnBot,
}) { }) {
final FlutterVersion flutterVersion = globals.flutterVersion; final FlutterVersion flutterVersion = globals.flutterVersion;
...@@ -166,6 +193,8 @@ class _DefaultUsage implements Usage { ...@@ -166,6 +193,8 @@ class _DefaultUsage implements Usage {
final String logFilePath = logFile ?? globals.platform.environment['FLUTTER_ANALYTICS_LOG_FILE']; final String logFilePath = logFile ?? globals.platform.environment['FLUTTER_ANALYTICS_LOG_FILE'];
final bool usingLogFile = logFilePath != null && logFilePath.isNotEmpty; final bool usingLogFile = logFilePath != null && logFilePath.isNotEmpty;
analyticsIOFactory ??= _defaultAnalyticsIOFactory;
if (// To support testing, only allow other signals to supress analytics if (// To support testing, only allow other signals to supress analytics
// when analytics are not being shunted to a file. // when analytics are not being shunted to a file.
!usingLogFile && ( !usingLogFile && (
...@@ -187,13 +216,21 @@ class _DefaultUsage implements Usage { ...@@ -187,13 +216,21 @@ class _DefaultUsage implements Usage {
if (usingLogFile) { if (usingLogFile) {
_analytics = LogToFileAnalytics(logFilePath); _analytics = LogToFileAnalytics(logFilePath);
} else { } else {
_analytics = AnalyticsIO( try {
_kFlutterUA, _analytics = analyticsIOFactory(
settingsName, _kFlutterUA,
version, settingsName,
documentDirectory: version,
configDirOverride != null ? globals.fs.directory(configDirOverride) : null, 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); assert(_analytics != null);
......
...@@ -7,7 +7,6 @@ import 'package:file/memory.dart'; ...@@ -7,7 +7,6 @@ import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/config.dart'; import 'package:flutter_tools/src/base/config.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/time.dart'; import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build.dart'; import 'package:flutter_tools/src/commands/build.dart';
...@@ -15,12 +14,13 @@ import 'package:flutter_tools/src/commands/config.dart'; ...@@ -15,12 +14,13 @@ import 'package:flutter_tools/src/commands/config.dart';
import 'package:flutter_tools/src/commands/doctor.dart'; import 'package:flutter_tools/src/commands/doctor.dart';
import 'package:flutter_tools/src/doctor.dart'; import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/version.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'package:usage/usage_io.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
...@@ -63,6 +63,7 @@ void main() { ...@@ -63,6 +63,7 @@ void main() {
final DoctorCommand doctorCommand = DoctorCommand(); final DoctorCommand doctorCommand = DoctorCommand();
final CommandRunner<void>runner = createTestCommandRunner(doctorCommand); final CommandRunner<void>runner = createTestCommandRunner(doctorCommand);
await runner.run(<String>['doctor']); await runner.run(<String>['doctor']);
expect(count, 0); expect(count, 0);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(const SystemClock()), FlutterVersion: () => FlutterVersion(const SystemClock()),
...@@ -86,6 +87,7 @@ void main() { ...@@ -86,6 +87,7 @@ void main() {
globals.flutterUsage.enabled = true; globals.flutterUsage.enabled = true;
await runner.run(<String>['config']); await runner.run(<String>['config']);
expect(count, 0); expect(count, 0);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(const SystemClock()), FlutterVersion: () => FlutterVersion(const SystemClock()),
...@@ -103,6 +105,7 @@ void main() { ...@@ -103,6 +105,7 @@ void main() {
usage.sendCommand('test'); usage.sendCommand('test');
final String featuresKey = cdKey(CustomDimensions.enabledFlutterFeatures); final String featuresKey = cdKey(CustomDimensions.enabledFlutterFeatures);
expect(globals.fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web')); expect(globals.fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web'));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(const SystemClock()), FlutterVersion: () => FlutterVersion(const SystemClock()),
...@@ -125,7 +128,11 @@ void main() { ...@@ -125,7 +128,11 @@ void main() {
usage.sendCommand('test'); usage.sendCommand('test');
final String featuresKey = cdKey(CustomDimensions.enabledFlutterFeatures); final String featuresKey = cdKey(CustomDimensions.enabledFlutterFeatures);
expect(globals.fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web,enable-linux-desktop,enable-macos-desktop'));
expect(
globals.fs.file('test').readAsStringSync(),
contains('$featuresKey: enable-web,enable-linux-desktop,enable-macos-desktop'),
);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(const SystemClock()), FlutterVersion: () => FlutterVersion(const SystemClock()),
Config: () => mockFlutterConfig, Config: () => mockFlutterConfig,
...@@ -159,7 +166,10 @@ void main() { ...@@ -159,7 +166,10 @@ void main() {
testUsingContext('flutter commands send timing events', () async { testUsingContext('flutter commands send timing events', () async {
mockTimes = <int>[1000, 2000]; mockTimes = <int>[1000, 2000];
when(mockDoctor.diagnose(androidLicenses: false, verbose: false)).thenAnswer((_) async => true); when(mockDoctor.diagnose(
androidLicenses: false,
verbose: false,
)).thenAnswer((_) async => true);
final DoctorCommand command = DoctorCommand(); final DoctorCommand command = DoctorCommand();
final CommandRunner<void> runner = createTestCommandRunner(command); final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['doctor']); await runner.run(<String>['doctor']);
...@@ -167,7 +177,12 @@ void main() { ...@@ -167,7 +177,12 @@ void main() {
verify(mockClock.now()).called(2); verify(mockClock.now()).called(2);
expect( expect(
verify(mockUsage.sendTiming(captureAny, captureAny, captureAny, label: captureAnyNamed('label'))).captured, verify(mockUsage.sendTiming(
captureAny,
captureAny,
captureAny,
label: captureAnyNamed('label'),
)).captured,
<dynamic>['flutter', 'doctor', const Duration(milliseconds: 1000), 'success'], <dynamic>['flutter', 'doctor', const Duration(milliseconds: 1000), 'success'],
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -186,7 +201,12 @@ void main() { ...@@ -186,7 +201,12 @@ void main() {
verify(mockClock.now()).called(2); verify(mockClock.now()).called(2);
expect( expect(
verify(mockUsage.sendTiming(captureAny, captureAny, captureAny, label: captureAnyNamed('label'))).captured, verify(mockUsage.sendTiming(
captureAny,
captureAny,
captureAny,
label: captureAnyNamed('label'),
)).captured,
<dynamic>['flutter', 'doctor', const Duration(milliseconds: 1000), 'warning'], <dynamic>['flutter', 'doctor', const Duration(milliseconds: 1000), 'warning'],
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -197,6 +217,7 @@ void main() { ...@@ -197,6 +217,7 @@ void main() {
testUsingContext('single command usage path', () async { testUsingContext('single command usage path', () async {
final FlutterCommand doctorCommand = DoctorCommand(); final FlutterCommand doctorCommand = DoctorCommand();
expect(await doctorCommand.usagePath, 'doctor'); expect(await doctorCommand.usagePath, 'doctor');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Usage: () => mockUsage, Usage: () => mockUsage,
...@@ -205,6 +226,7 @@ void main() { ...@@ -205,6 +226,7 @@ void main() {
testUsingContext('compound command usage path', () async { testUsingContext('compound command usage path', () async {
final BuildCommand buildCommand = BuildCommand(); final BuildCommand buildCommand = BuildCommand();
final FlutterCommand buildApkCommand = buildCommand.subcommands['apk'] as FlutterCommand; final FlutterCommand buildApkCommand = buildCommand.subcommands['apk'] as FlutterCommand;
expect(await buildApkCommand.usagePath, 'build/apk'); expect(await buildApkCommand.usagePath, 'build/apk');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Usage: () => mockUsage, Usage: () => mockUsage,
...@@ -226,6 +248,7 @@ void main() { ...@@ -226,6 +248,7 @@ void main() {
final String log = globals.fs.file('analytics.log').readAsStringSync(); final String log = globals.fs.file('analytics.log').readAsStringSync();
final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis); final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
expect(log.contains(formatDateTime(dateTime)), isTrue); expect(log.contains(formatDateTime(dateTime)), isTrue);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem, FileSystem: () => memoryFileSystem,
...@@ -255,6 +278,7 @@ void main() { ...@@ -255,6 +278,7 @@ void main() {
final String log = globals.fs.file('analytics.log').readAsStringSync(); final String log = globals.fs.file('analytics.log').readAsStringSync();
final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis); final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
expect(log.contains(formatDateTime(dateTime)), isTrue); expect(log.contains(formatDateTime(dateTime)), isTrue);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem, FileSystem: () => memoryFileSystem,
...@@ -273,7 +297,9 @@ void main() { ...@@ -273,7 +297,9 @@ void main() {
Directory tempDir; Directory tempDir;
setUp(() { setUp(() {
tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_bots_test.'); tempDir = globals.fs.systemTempDirectory.createTempSync(
'flutter_tools_analytics_bots_test.',
);
}); });
tearDown(() { tearDown(() {
...@@ -283,8 +309,8 @@ void main() { ...@@ -283,8 +309,8 @@ void main() {
testUsingContext("don't send on bots with unknown version", () async { testUsingContext("don't send on bots with unknown version", () async {
int count = 0; int count = 0;
globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++); globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
await createTestCommandRunner().run(<String>['--version']); await createTestCommandRunner().run(<String>['--version']);
expect(count, 0); expect(count, 0);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Usage: () => Usage( Usage: () => Usage(
...@@ -299,8 +325,8 @@ void main() { ...@@ -299,8 +325,8 @@ void main() {
int count = 0; int count = 0;
globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++); globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
globals.flutterUsage.enabled = true; globals.flutterUsage.enabled = true;
await createTestCommandRunner().run(<String>['--version']); await createTestCommandRunner().run(<String>['--version']);
expect(count, 0); expect(count, 0);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Usage: () => Usage( Usage: () => Usage(
...@@ -310,9 +336,33 @@ void main() { ...@@ -310,9 +336,33 @@ void main() {
runningOnBot: false, runningOnBot: false,
), ),
}); });
testUsingContext('Uses AnalyticsMock when .flutter cannot be created', () async {
final Usage usage = Usage(
settingsName: 'flutter_bot_test',
versionOverride: 'dev/known',
configDirOverride: tempDir.path,
analyticsIOFactory: throwingAnalyticsIOFactory,
runningOnBot: false,
);
final AnalyticsMock analyticsMock = AnalyticsMock();
expect(usage.clientId, analyticsMock.clientId);
expect(usage.suppressAnalytics, isTrue);
});
}); });
} }
Analytics throwingAnalyticsIOFactory(
String trackingId,
String applicationName,
String applicationVersion, {
String analyticsUrl,
Directory documentDirectory,
}) {
throw const FileSystemException('Could not create file');
}
class MockUsage extends Mock implements Usage {} class MockUsage extends Mock implements Usage {}
class MockDoctor extends Mock implements Doctor {} class MockDoctor extends Mock implements Doctor {}
......
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