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