Unverified Commit 56ae5559 authored by Elias Yishak's avatar Elias Yishak Committed by GitHub

Unified analytics events for doctor validators (#136647)

Related to tracking issue:
- https://github.com/flutter/flutter/issues/128251

This PR sends analytic events for each of the doctor validators.

This PR below will need to land first in `dart-lang/tools` before this merges.
parent 6c81009b
...@@ -105,6 +105,9 @@ Future<int> run( ...@@ -105,6 +105,9 @@ Future<int> run(
globals.flutterUsage.enabled = true; globals.flutterUsage.enabled = true;
globals.printStatus('Analytics reporting enabled.'); globals.printStatus('Analytics reporting enabled.');
// TODO(eliasyishak): Set the telemetry for the unified_analytics
// package as well, the above will be removed once we have
// fully transitioned to using the new package
await globals.analytics.setTelemetry(true); await globals.analytics.setTelemetry(true);
} }
......
...@@ -623,6 +623,12 @@ Future<int> exitWithHooks(int code, {required ShutdownHooks shutdownHooks}) asyn ...@@ -623,6 +623,12 @@ Future<int> exitWithHooks(int code, {required ShutdownHooks shutdownHooks}) asyn
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
// Allow any pending analytics events to send and close the http connection
//
// By default, we will wait 250 ms before canceling any pending events, we
// can change the [delayDuration] in the close method if it needs to be changed
await globals.analytics.close();
// Give the task / timer queue one cycle through before we hard exit. // Give the task / timer queue one cycle through before we hard exit.
Timer.run(() { Timer.run(() {
try { try {
......
...@@ -218,7 +218,10 @@ Future<T> runInContext<T>( ...@@ -218,7 +218,10 @@ Future<T> runInContext<T>(
logger: globals.logger, logger: globals.logger,
botDetector: globals.botDetector, botDetector: globals.botDetector,
), ),
Doctor: () => Doctor(logger: globals.logger), Doctor: () => Doctor(
logger: globals.logger,
clock: globals.systemClock,
),
DoctorValidatorsProvider: () => DoctorValidatorsProvider.defaultInstance, DoctorValidatorsProvider: () => DoctorValidatorsProvider.defaultInstance,
EmulatorManager: () => EmulatorManager( EmulatorManager: () => EmulatorManager(
java: globals.java, java: globals.java,
......
...@@ -6,6 +6,7 @@ import 'dart:async'; ...@@ -6,6 +6,7 @@ import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import 'package:unified_analytics/unified_analytics.dart';
import 'android/android_studio_validator.dart'; import 'android/android_studio_validator.dart';
import 'android/android_workflow.dart'; import 'android/android_workflow.dart';
...@@ -19,6 +20,7 @@ import 'base/net.dart'; ...@@ -19,6 +20,7 @@ import 'base/net.dart';
import 'base/os.dart'; import 'base/os.dart';
import 'base/platform.dart'; import 'base/platform.dart';
import 'base/terminal.dart'; import 'base/terminal.dart';
import 'base/time.dart';
import 'base/user_messages.dart'; import 'base/user_messages.dart';
import 'base/utils.dart'; import 'base/utils.dart';
import 'cache.dart'; import 'cache.dart';
...@@ -229,9 +231,15 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider { ...@@ -229,9 +231,15 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
class Doctor { class Doctor {
Doctor({ Doctor({
required Logger logger, required Logger logger,
}) : _logger = logger; required SystemClock clock,
Analytics? analytics,
}) : _logger = logger,
_clock = clock,
_analytics = analytics ?? globals.analytics;
final Logger _logger; final Logger _logger;
final SystemClock _clock;
final Analytics _analytics;
List<DoctorValidator> get validators { List<DoctorValidator> get validators {
return DoctorValidatorsProvider._instance.validators; return DoctorValidatorsProvider._instance.validators;
...@@ -375,6 +383,10 @@ class Doctor { ...@@ -375,6 +383,10 @@ class Doctor {
bool doctorResult = true; bool doctorResult = true;
int issues = 0; int issues = 0;
// This timestamp will be used on the backend of GA4 to group each of the events that
// were sent for each doctor validator and its result
final int analyticsTimestamp = _clock.now().millisecondsSinceEpoch;
for (final ValidatorTask validatorTask in startedValidatorTasks ?? startValidatorTasks()) { for (final ValidatorTask validatorTask in startedValidatorTasks ?? startValidatorTasks()) {
final DoctorValidator validator = validatorTask.validator; final DoctorValidator validator = validatorTask.validator;
final Status status = _logger.startSpinner( final Status status = _logger.startSpinner(
...@@ -404,6 +416,37 @@ class Doctor { ...@@ -404,6 +416,37 @@ class Doctor {
break; break;
} }
if (sendEvent) { if (sendEvent) {
if (validator is GroupedValidator) {
for (int i = 0; i < validator.subValidators.length; i++) {
final DoctorValidator subValidator = validator.subValidators[i];
// Ensure that all of the subvalidators in the group have
// a corresponding subresult incase a validator crashed
final ValidationResult subResult;
try {
subResult = validator.subResults[i];
} on RangeError {
continue;
}
_analytics.send(Event.doctorValidatorResult(
validatorName: subValidator.title,
result: subResult.typeStr,
statusInfo: subResult.statusInfo,
partOfGroupedValidator: true,
doctorInvocationId: analyticsTimestamp,
));
}
} else {
_analytics.send(Event.doctorValidatorResult(
validatorName: validator.title,
result: result.typeStr,
statusInfo: result.statusInfo,
partOfGroupedValidator: false,
doctorInvocationId: analyticsTimestamp,
));
}
// TODO(eliasyishak): remove this after migrating from package:usage
DoctorResultEvent(validator: validator, result: result).send(); DoctorResultEvent(validator: validator, result: result).send();
} }
...@@ -727,8 +770,10 @@ class DeviceValidator extends DoctorValidator { ...@@ -727,8 +770,10 @@ class DeviceValidator extends DoctorValidator {
class DoctorText { class DoctorText {
DoctorText( DoctorText(
BufferLogger logger, { BufferLogger logger, {
SystemClock? clock,
@visibleForTesting Doctor? doctor, @visibleForTesting Doctor? doctor,
}) : _doctor = doctor ?? Doctor(logger: logger), _logger = logger; }) : _doctor = doctor ?? Doctor(logger: logger, clock: clock ?? globals.systemClock),
_logger = logger;
final BufferLogger _logger; final BufferLogger _logger;
final Doctor _doctor; final Doctor _doctor;
......
...@@ -304,6 +304,7 @@ class FlutterCommandRunner extends CommandRunner<void> { ...@@ -304,6 +304,7 @@ class FlutterCommandRunner extends CommandRunner<void> {
if ((topLevelResults[FlutterGlobalOptions.kSuppressAnalyticsFlag] as bool?) ?? false) { if ((topLevelResults[FlutterGlobalOptions.kSuppressAnalyticsFlag] as bool?) ?? false) {
globals.flutterUsage.suppressAnalytics = true; globals.flutterUsage.suppressAnalytics = true;
globals.analytics.suppressTelemetry();
} }
globals.flutterVersion.ensureVersionFile(); globals.flutterVersion.ensureVersionFile();
......
...@@ -12,6 +12,7 @@ import 'package:flutter_tools/src/base/file_system.dart'; ...@@ -12,6 +12,7 @@ import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
...@@ -26,6 +27,7 @@ import 'package:flutter_tools/src/vscode/vscode.dart'; ...@@ -26,6 +27,7 @@ import 'package:flutter_tools/src/vscode/vscode.dart';
import 'package:flutter_tools/src/vscode/vscode_validator.dart'; import 'package:flutter_tools/src/vscode/vscode_validator.dart';
import 'package:flutter_tools/src/web/workflow.dart'; import 'package:flutter_tools/src/web/workflow.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
...@@ -166,7 +168,7 @@ void main() { ...@@ -166,7 +168,7 @@ void main() {
group('doctor with overridden validators', () { group('doctor with overridden validators', () {
testUsingContext('validate non-verbose output format for run without issues', () async { testUsingContext('validate non-verbose output format for run without issues', () async {
final Doctor doctor = Doctor(logger: logger); final Doctor doctor = Doctor(logger: logger, clock: const SystemClock());
expect(await doctor.diagnose(verbose: false), isTrue); expect(await doctor.diagnose(verbose: false), isTrue);
expect(logger.statusText, equals( expect(logger.statusText, equals(
'Doctor summary (to see all details, run flutter doctor -v):\n' 'Doctor summary (to see all details, run flutter doctor -v):\n'
...@@ -190,7 +192,7 @@ void main() { ...@@ -190,7 +192,7 @@ void main() {
}); });
testUsingContext('contains installed', () async { testUsingContext('contains installed', () async {
final Doctor doctor = Doctor(logger: logger); final Doctor doctor = Doctor(logger: logger, clock: const SystemClock());
await doctor.diagnose(verbose: false); await doctor.diagnose(verbose: false);
expect(testUsage.events.length, 3); expect(testUsage.events.length, 3);
...@@ -508,7 +510,7 @@ void main() { ...@@ -508,7 +510,7 @@ void main() {
testUsingContext('PII separated, events only sent once', () async { testUsingContext('PII separated, events only sent once', () async {
final Doctor fakeDoctor = FakePiiDoctor(logger); final Doctor fakeDoctor = FakePiiDoctor(logger);
final DoctorText doctorText = DoctorText(logger, doctor: fakeDoctor); final DoctorText doctorText = DoctorText(logger,doctor: fakeDoctor);
const String expectedPiiText = '[✓] PII Validator\n' const String expectedPiiText = '[✓] PII Validator\n'
' • Contains PII path/to/username\n' ' • Contains PII path/to/username\n'
'\n' '\n'
...@@ -816,6 +818,183 @@ void main() { ...@@ -816,6 +818,183 @@ void main() {
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
AndroidWorkflow: () => FakeAndroidWorkflow(appliesToHostPlatform: false), AndroidWorkflow: () => FakeAndroidWorkflow(appliesToHostPlatform: false),
}); });
group('Doctor events with unified_analytics', () {
late FakeAnalytics fakeAnalytics;
final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion();
final DateTime fakeDate = DateTime(1995, 3, 3);
final SystemClock fakeSystemClock = SystemClock.fixed(fakeDate);
setUp(() {
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fakeFlutterVersion: fakeFlutterVersion,
fs: fs,
);
});
testUsingContext('ensure fake is being used and initialized', () {
expect(fakeAnalytics.sentEvents.length, 0);
expect(fakeAnalytics.okToSend, true);
}, overrides: <Type, Generator>{
Analytics: () => fakeAnalytics,
});
testUsingContext('contains installed', () async {
final Doctor doctor = Doctor(logger: logger, clock: fakeSystemClock, analytics: fakeAnalytics);
await doctor.diagnose(verbose: false);
expect(fakeAnalytics.sentEvents.length, 3);
// The event that should have been fired off during the doctor invocation
final Event eventToFind = Event.doctorValidatorResult(
validatorName: 'Passing Validator',
result: 'installed',
partOfGroupedValidator: false,
doctorInvocationId: DateTime(1995, 3, 3).millisecondsSinceEpoch,
statusInfo: 'with statusInfo',
);
expect(fakeAnalytics.sentEvents, contains(eventToFind));
}, overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
});
testUsingContext('contains installed and partial', () async {
await FakePassingDoctor(logger, clock: fakeSystemClock).diagnose(verbose: false);
expect(fakeAnalytics.sentEvents, hasLength(4));
expect(fakeAnalytics.sentEvents, unorderedEquals(<Event>[
Event.doctorValidatorResult(
validatorName: 'Passing Validator',
result: 'installed',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
statusInfo: 'with statusInfo',
),
Event.doctorValidatorResult(
validatorName: 'Partial Validator with only a Hint',
result: 'partial',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Partial Validator with Errors',
result: 'partial',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Another Passing Validator',
result: 'installed',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
statusInfo: 'with statusInfo',
),
]));
}, overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
Analytics: () => fakeAnalytics,
});
testUsingContext('contains installed, missing and partial', () async {
await FakeDoctor(logger, clock: fakeSystemClock).diagnose(verbose: false);
expect(fakeAnalytics.sentEvents, hasLength(5));
expect(fakeAnalytics.sentEvents, unorderedEquals(<Event>[
Event.doctorValidatorResult(
validatorName: 'Passing Validator',
result: 'installed',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
statusInfo: 'with statusInfo',
),
Event.doctorValidatorResult(
validatorName: 'Missing Validator',
result: 'missing',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Not Available Validator',
result: 'notAvailable',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Partial Validator with only a Hint',
result: 'partial',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Partial Validator with Errors',
result: 'partial',
partOfGroupedValidator: false,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
]));
}, overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
Analytics: () => fakeAnalytics,
});
testUsingContext('events for grouped validators are properly decomposed', () async {
await FakeGroupedDoctor(logger, clock: fakeSystemClock).diagnose(verbose: false);
expect(fakeAnalytics.sentEvents, hasLength(4));
expect(fakeAnalytics.sentEvents, unorderedEquals(<Event>[
Event.doctorValidatorResult(
validatorName: 'Category 1',
result: 'installed',
partOfGroupedValidator: true,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Category 1',
result: 'installed',
partOfGroupedValidator: true,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Category 2',
result: 'installed',
partOfGroupedValidator: true,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
Event.doctorValidatorResult(
validatorName: 'Category 2',
result: 'missing',
partOfGroupedValidator: true,
doctorInvocationId: fakeDate.millisecondsSinceEpoch,
),
]));
}, overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
Analytics: () => fakeAnalytics,
});
testUsingContext('grouped validator subresult and subvalidators different lengths', () async {
final FakeGroupedDoctorWithCrash fakeDoctor = FakeGroupedDoctorWithCrash(logger, clock: fakeSystemClock);
await fakeDoctor.diagnose(verbose: false);
expect(fakeDoctor.validators, hasLength(1));
expect(fakeDoctor.validators.first.runtimeType == FakeGroupedValidatorWithCrash, true);
expect(fakeAnalytics.sentEvents, hasLength(0));
// Attempt to send a random event to ensure that the
// analytics package is still working, despite not sending
// above (as expected)
final Event testEvent = Event.analyticsCollectionEnabled(status: true);
fakeAnalytics.send(testEvent);
expect(fakeAnalytics.sentEvents, hasLength(1));
expect(fakeAnalytics.sentEvents, contains(testEvent));
}, overrides: <Type, Generator>{Analytics: () => fakeAnalytics});
testUsingContext('sending events can be skipped', () async {
await FakePassingDoctor(logger).diagnose(verbose: false, sendEvent: false);
expect(fakeAnalytics.sentEvents, isEmpty);
}
,overrides: <Type, Generator>{Analytics: () => fakeAnalytics});
});
} }
class FakeAndroidWorkflow extends Fake implements AndroidWorkflow { class FakeAndroidWorkflow extends Fake implements AndroidWorkflow {
...@@ -952,7 +1131,8 @@ class AsyncCrashingValidator extends DoctorValidator { ...@@ -952,7 +1131,8 @@ class AsyncCrashingValidator extends DoctorValidator {
/// A doctor that fails with a missing [ValidationResult]. /// A doctor that fails with a missing [ValidationResult].
class FakeDoctor extends Doctor { class FakeDoctor extends Doctor {
FakeDoctor(Logger logger) : super(logger: logger); FakeDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
...@@ -966,7 +1146,8 @@ class FakeDoctor extends Doctor { ...@@ -966,7 +1146,8 @@ class FakeDoctor extends Doctor {
/// A doctor that should pass, but still has issues in some categories. /// A doctor that should pass, but still has issues in some categories.
class FakePassingDoctor extends Doctor { class FakePassingDoctor extends Doctor {
FakePassingDoctor(Logger logger) : super(logger: logger); FakePassingDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
...@@ -980,7 +1161,8 @@ class FakePassingDoctor extends Doctor { ...@@ -980,7 +1161,8 @@ class FakePassingDoctor extends Doctor {
/// A doctor that should pass, but still has 1 issue to test the singular of /// A doctor that should pass, but still has 1 issue to test the singular of
/// categories. /// categories.
class FakeSinglePassingDoctor extends Doctor { class FakeSinglePassingDoctor extends Doctor {
FakeSinglePassingDoctor(Logger logger) : super(logger: logger); FakeSinglePassingDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
...@@ -990,7 +1172,8 @@ class FakeSinglePassingDoctor extends Doctor { ...@@ -990,7 +1172,8 @@ class FakeSinglePassingDoctor extends Doctor {
/// A doctor that passes and has no issues anywhere. /// A doctor that passes and has no issues anywhere.
class FakeQuietDoctor extends Doctor { class FakeQuietDoctor extends Doctor {
FakeQuietDoctor(Logger logger) : super(logger: logger); FakeQuietDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
...@@ -1003,7 +1186,8 @@ class FakeQuietDoctor extends Doctor { ...@@ -1003,7 +1186,8 @@ class FakeQuietDoctor extends Doctor {
/// A doctor that passes and contains PII that can be hidden. /// A doctor that passes and contains PII that can be hidden.
class FakePiiDoctor extends Doctor { class FakePiiDoctor extends Doctor {
FakePiiDoctor(Logger logger) : super(logger: logger); FakePiiDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
...@@ -1013,7 +1197,8 @@ class FakePiiDoctor extends Doctor { ...@@ -1013,7 +1197,8 @@ class FakePiiDoctor extends Doctor {
/// A doctor with a validator that throws an exception. /// A doctor with a validator that throws an exception.
class FakeCrashingDoctor extends Doctor { class FakeCrashingDoctor extends Doctor {
FakeCrashingDoctor(Logger logger) : super(logger: logger); FakeCrashingDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
...@@ -1027,7 +1212,8 @@ class FakeCrashingDoctor extends Doctor { ...@@ -1027,7 +1212,8 @@ class FakeCrashingDoctor extends Doctor {
/// A doctor with a validator that will never finish. /// A doctor with a validator that will never finish.
class FakeAsyncStuckDoctor extends Doctor { class FakeAsyncStuckDoctor extends Doctor {
FakeAsyncStuckDoctor(Logger logger) : super(logger: logger); FakeAsyncStuckDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
...@@ -1041,7 +1227,9 @@ class FakeAsyncStuckDoctor extends Doctor { ...@@ -1041,7 +1227,9 @@ class FakeAsyncStuckDoctor extends Doctor {
/// A doctor with a validator that throws an exception. /// A doctor with a validator that throws an exception.
class FakeAsyncCrashingDoctor extends Doctor { class FakeAsyncCrashingDoctor extends Doctor {
FakeAsyncCrashingDoctor(this._time, Logger logger) : super(logger: logger); FakeAsyncCrashingDoctor(this._time, Logger logger,
{super.clock = const SystemClock()})
: super(logger: logger);
final FakeAsync _time; final FakeAsync _time;
...@@ -1121,7 +1309,8 @@ class PassingGroupedValidatorWithStatus extends DoctorValidator { ...@@ -1121,7 +1309,8 @@ class PassingGroupedValidatorWithStatus extends DoctorValidator {
/// A doctor that has two groups of two validators each. /// A doctor that has two groups of two validators each.
class FakeGroupedDoctor extends Doctor { class FakeGroupedDoctor extends Doctor {
FakeGroupedDoctor(Logger logger) : super(logger: logger); FakeGroupedDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
...@@ -1136,8 +1325,41 @@ class FakeGroupedDoctor extends Doctor { ...@@ -1136,8 +1325,41 @@ class FakeGroupedDoctor extends Doctor {
]; ];
} }
/// Fake grouped doctor that is intended to be used with [FakeGroupedValidatorWithCrash].
class FakeGroupedDoctorWithCrash extends Doctor {
FakeGroupedDoctorWithCrash(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
FakeGroupedValidatorWithCrash(<DoctorValidator>[
PassingGroupedValidator('Category 1'),
PassingGroupedValidator('Category 1'),
]),
];
}
/// This extended grouped validator will have a list of sub validators
/// provided in the constructor, but it will have no [subResults] in the
/// list which simulates what happens if a validator crashes.
///
/// Usually, the grouped validators have 2 lists, a [subValidators] and
/// a [subResults] list, and if nothing crashes, those 2 lists will have the
/// same length. This fake is simulating what happens when the validators
/// crash and results in no results getting returned.
class FakeGroupedValidatorWithCrash extends GroupedValidator {
FakeGroupedValidatorWithCrash(super.subValidators);
@override
List<ValidationResult> get subResults => <ValidationResult>[];
}
class FakeGroupedDoctorWithStatus extends Doctor { class FakeGroupedDoctorWithStatus extends Doctor {
FakeGroupedDoctorWithStatus(Logger logger) : super(logger: logger); FakeGroupedDoctorWithStatus(Logger logger,
{super.clock = const SystemClock()})
: super(logger: logger);
@override @override
late final List<DoctorValidator> validators = <DoctorValidator>[ late final List<DoctorValidator> validators = <DoctorValidator>[
...@@ -1151,7 +1373,9 @@ class FakeGroupedDoctorWithStatus extends Doctor { ...@@ -1151,7 +1373,9 @@ class FakeGroupedDoctorWithStatus extends Doctor {
/// A doctor that takes any two validators. Used to check behavior when /// A doctor that takes any two validators. Used to check behavior when
/// merging ValidationTypes (installed, missing, partial). /// merging ValidationTypes (installed, missing, partial).
class FakeSmallGroupDoctor extends Doctor { class FakeSmallGroupDoctor extends Doctor {
FakeSmallGroupDoctor(Logger logger, DoctorValidator val1, DoctorValidator val2) FakeSmallGroupDoctor(
Logger logger, DoctorValidator val1, DoctorValidator val2,
{super.clock = const SystemClock()})
: validators = <DoctorValidator>[GroupedValidator(<DoctorValidator>[val1, val2])], : validators = <DoctorValidator>[GroupedValidator(<DoctorValidator>[val1, val2])],
super(logger: logger); super(logger: logger);
......
...@@ -18,12 +18,12 @@ import 'package:flutter_tools/src/globals.dart' as globals; ...@@ -18,12 +18,12 @@ import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/reporting/crash_reporting.dart'; import 'package:flutter_tools/src/reporting/crash_reporting.dart';
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:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart'; import 'package:unified_analytics/unified_analytics.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
import '../../src/fake_http_client.dart'; import '../../src/fake_http_client.dart';
import '../../src/fakes.dart';
const String kCustomBugInstructions = 'These are instructions to report with a custom bug tracker.'; const String kCustomBugInstructions = 'These are instructions to report with a custom bug tracker.';
...@@ -317,13 +317,24 @@ void main() { ...@@ -317,13 +317,24 @@ void main() {
}); });
group('unified_analytics', () { group('unified_analytics', () {
late FakeAnalytics fakeAnalytics;
late MemoryFileSystem fs;
setUp(() {
fs = MemoryFileSystem.test();
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fs: fs,
fakeFlutterVersion: FakeFlutterVersion(),
);
});
testUsingContext( testUsingContext(
'runner disable telemetry with flag', 'runner disable telemetry with flag',
() async { () async {
io.setExitFunctionForTests((int exitCode) {}); io.setExitFunctionForTests((int exitCode) {});
expect(globals.analytics.telemetryEnabled, true); expect(globals.analytics.telemetryEnabled, true);
expect(globals.analytics.shouldShowMessage, true);
await runner.run( await runner.run(
<String>['--disable-analytics'], <String>['--disable-analytics'],
...@@ -336,7 +347,7 @@ void main() { ...@@ -336,7 +347,7 @@ void main() {
expect(globals.analytics.telemetryEnabled, false); expect(globals.analytics.telemetryEnabled, false);
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
Analytics: () => FakeAnalytics(), Analytics: () => fakeAnalytics,
FileSystem: () => MemoryFileSystem.test(), FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}, },
...@@ -347,8 +358,17 @@ void main() { ...@@ -347,8 +358,17 @@ void main() {
() async { () async {
io.setExitFunctionForTests((int exitCode) {}); io.setExitFunctionForTests((int exitCode) {});
expect(globals.analytics.telemetryEnabled, true);
await runner.run(
<String>['--disable-analytics'],
() => <FlutterCommand>[],
// This flutterVersion disables crash reporting.
flutterVersion: '[user-branch]/',
shutdownHooks: ShutdownHooks(),
);
expect(globals.analytics.telemetryEnabled, false); expect(globals.analytics.telemetryEnabled, false);
expect(globals.analytics.shouldShowMessage, false);
await runner.run( await runner.run(
<String>['--enable-analytics'], <String>['--enable-analytics'],
...@@ -361,7 +381,7 @@ void main() { ...@@ -361,7 +381,7 @@ void main() {
expect(globals.analytics.telemetryEnabled, true); expect(globals.analytics.telemetryEnabled, true);
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
Analytics: () => FakeAnalytics(fakeTelemetryStatusOverride: false), Analytics: () => fakeAnalytics,
FileSystem: () => MemoryFileSystem.test(), FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}, },
...@@ -373,7 +393,6 @@ void main() { ...@@ -373,7 +393,6 @@ void main() {
io.setExitFunctionForTests((int exitCode) {}); io.setExitFunctionForTests((int exitCode) {});
expect(globals.analytics.telemetryEnabled, true); expect(globals.analytics.telemetryEnabled, true);
expect(globals.analytics.shouldShowMessage, true);
final int exitCode = await runner.run( final int exitCode = await runner.run(
<String>[ <String>[
...@@ -392,7 +411,7 @@ void main() { ...@@ -392,7 +411,7 @@ void main() {
reason: 'Should not have changed from initialization'); reason: 'Should not have changed from initialization');
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
Analytics: () => FakeAnalytics(), Analytics: () => fakeAnalytics,
FileSystem: () => MemoryFileSystem.test(), FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}, },
...@@ -536,38 +555,3 @@ class WaitingCrashReporter implements CrashReporter { ...@@ -536,38 +555,3 @@ class WaitingCrashReporter implements CrashReporter {
return _future; return _future;
} }
} }
/// A fake [Analytics] that will be used to test
/// the --disable-analytics flag
class FakeAnalytics extends Fake implements Analytics {
FakeAnalytics({bool fakeTelemetryStatusOverride = true})
: _fakeTelemetryStatus = fakeTelemetryStatusOverride,
_fakeShowMessage = fakeTelemetryStatusOverride;
// Both of the members below can be initialized with [fakeTelemetryStatusOverride]
// because if we pass in false for the status, that means we can also
// assume the message has been shown before
bool _fakeTelemetryStatus;
bool _fakeShowMessage;
@override
String get getConsentMessage => 'message';
@override
bool get shouldShowMessage => _fakeShowMessage;
@override
void clientShowedMessage() {
_fakeShowMessage = false;
}
@override
Future<void> setTelemetry(bool reportingBool) {
_fakeTelemetryStatus = reportingBool;
return Future<void>.value();
}
@override
bool get telemetryEnabled => _fakeTelemetryStatus;
}
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/reporting/unified_analytics.dart'; import 'package:flutter_tools/src/reporting/unified_analytics.dart';
import 'package:unified_analytics/src/enums.dart';
import 'package:unified_analytics/unified_analytics.dart'; import 'package:unified_analytics/unified_analytics.dart';
import '../src/common.dart'; import '../src/common.dart';
...@@ -13,42 +12,17 @@ import '../src/fakes.dart'; ...@@ -13,42 +12,17 @@ import '../src/fakes.dart';
void main() { void main() {
const String userBranch = 'abc123'; const String userBranch = 'abc123';
const String homeDirectoryName = 'home';
const DashTool tool = DashTool.flutterTool;
late FileSystem fs; late FileSystem fs;
late Directory home;
late FakeAnalytics analyticsOverride; late FakeAnalytics analyticsOverride;
setUp(() { setUp(() {
fs = MemoryFileSystem.test(); fs = MemoryFileSystem.test();
home = fs.directory(homeDirectoryName);
// Prepare the tests by "onboarding" the tool into the package
// by invoking the [clientShowedMessage] method for the provided
// [tool]
final FakeAnalytics initialAnalytics = FakeAnalytics(
tool: tool,
homeDirectory: home,
dartVersion: '3.0.0',
platform: DevicePlatform.macos,
fs: fs,
surveyHandler: SurveyHandler(
homeDirectory: home,
fs: fs,
),
);
initialAnalytics.clientShowedMessage();
analyticsOverride = FakeAnalytics( analyticsOverride = getInitializedFakeAnalyticsInstance(
tool: tool,
homeDirectory: home,
dartVersion: '3.0.0',
platform: DevicePlatform.macos,
fs: fs, fs: fs,
surveyHandler: SurveyHandler( fakeFlutterVersion: FakeFlutterVersion(
homeDirectory: home, branch: userBranch,
fs: fs,
), ),
); );
}); });
...@@ -135,5 +109,17 @@ void main() { ...@@ -135,5 +109,17 @@ void main() {
); );
expect(analytics, isA<NoOpAnalytics>()); expect(analytics, isA<NoOpAnalytics>());
}); });
testWithoutContext('Suppression prevents events from being sent', () {
expect(analyticsOverride.okToSend, true);
analyticsOverride.send(Event.surveyShown(surveyId: 'surveyId'));
expect(analyticsOverride.sentEvents, hasLength(1));
analyticsOverride.suppressTelemetry();
expect(analyticsOverride.okToSend, false);
analyticsOverride.send(Event.surveyShown(surveyId: 'surveyId'));
expect(analyticsOverride.sentEvents, hasLength(1));
});
}); });
} }
...@@ -16,6 +16,10 @@ import 'package:meta/meta.dart'; ...@@ -16,6 +16,10 @@ import 'package:meta/meta.dart';
import 'package:path/path.dart' as path; // flutter_ignore: package_path_import import 'package:path/path.dart' as path; // flutter_ignore: package_path_import
import 'package:test/test.dart' as test_package show test; import 'package:test/test.dart' as test_package show test;
import 'package:test/test.dart' hide test; import 'package:test/test.dart' hide test;
import 'package:unified_analytics/src/enums.dart';
import 'package:unified_analytics/unified_analytics.dart';
import 'fakes.dart';
export 'package:path/path.dart' show Context; // flutter_ignore: package_path_import export 'package:path/path.dart' show Context; // flutter_ignore: package_path_import
export 'package:test/test.dart' hide isInstanceOf, test; export 'package:test/test.dart' hide isInstanceOf, test;
...@@ -305,3 +309,38 @@ class FileExceptionHandler { ...@@ -305,3 +309,38 @@ class FileExceptionHandler {
throw exception; throw exception;
} }
} }
/// This method is required to fetch an instance of [FakeAnalytics]
/// because there is initialization logic that is required. An initial
/// instance will first be created and will let package:unified_analytics
/// know that the consent message has been shown. After confirming on the first
/// instance, then a second instance will be generated and returned. This second
/// instance will be cleared to send events.
FakeAnalytics getInitializedFakeAnalyticsInstance({
required FileSystem fs,
required FakeFlutterVersion fakeFlutterVersion,
}) {
final Directory homeDirectory = fs.directory('/');
final FakeAnalytics initialAnalytics = FakeAnalytics(
tool: DashTool.flutterTool,
homeDirectory: homeDirectory,
dartVersion: fakeFlutterVersion.dartSdkVersion,
platform: DevicePlatform.linux,
fs: fs,
surveyHandler: SurveyHandler(homeDirectory: homeDirectory, fs: fs),
flutterChannel: fakeFlutterVersion.channel,
flutterVersion: fakeFlutterVersion.getVersionString(),
);
initialAnalytics.clientShowedMessage();
return FakeAnalytics(
tool: DashTool.flutterTool,
homeDirectory: homeDirectory,
dartVersion: fakeFlutterVersion.dartSdkVersion,
platform: DevicePlatform.linux,
fs: fs,
surveyHandler: SurveyHandler(homeDirectory: homeDirectory, fs: fs),
flutterChannel: fakeFlutterVersion.channel,
flutterVersion: fakeFlutterVersion.getVersionString(),
);
}
...@@ -16,6 +16,7 @@ import 'package:flutter_tools/src/base/process.dart'; ...@@ -16,6 +16,7 @@ import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/signals.dart'; import 'package:flutter_tools/src/base/signals.dart';
import 'package:flutter_tools/src/base/template.dart'; import 'package:flutter_tools/src/base/template.dart';
import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/context_runner.dart'; import 'package:flutter_tools/src/context_runner.dart';
...@@ -291,7 +292,8 @@ class FakeAndroidLicenseValidator extends Fake implements AndroidLicenseValidato ...@@ -291,7 +292,8 @@ class FakeAndroidLicenseValidator extends Fake implements AndroidLicenseValidato
} }
class FakeDoctor extends Doctor { class FakeDoctor extends Doctor {
FakeDoctor(Logger logger) : super(logger: logger); FakeDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
// True for testing. // True for testing.
@override @override
......
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