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.
This commit is contained in:
Elias Yishak 2023-10-26 14:23:24 -04:00 committed by GitHub
parent 6c81009bcb
commit 56ae555992
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 382 additions and 89 deletions

View file

@ -105,6 +105,9 @@ Future<int> run(
globals.flutterUsage.enabled = true;
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);
}

View file

@ -623,6 +623,12 @@ Future<int> exitWithHooks(int code, {required ShutdownHooks shutdownHooks}) asyn
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.
Timer.run(() {
try {

View file

@ -218,7 +218,10 @@ Future<T> runInContext<T>(
logger: globals.logger,
botDetector: globals.botDetector,
),
Doctor: () => Doctor(logger: globals.logger),
Doctor: () => Doctor(
logger: globals.logger,
clock: globals.systemClock,
),
DoctorValidatorsProvider: () => DoctorValidatorsProvider.defaultInstance,
EmulatorManager: () => EmulatorManager(
java: globals.java,

View file

@ -6,6 +6,7 @@ import 'dart:async';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:unified_analytics/unified_analytics.dart';
import 'android/android_studio_validator.dart';
import 'android/android_workflow.dart';
@ -19,6 +20,7 @@ import 'base/net.dart';
import 'base/os.dart';
import 'base/platform.dart';
import 'base/terminal.dart';
import 'base/time.dart';
import 'base/user_messages.dart';
import 'base/utils.dart';
import 'cache.dart';
@ -229,9 +231,15 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
class Doctor {
Doctor({
required Logger logger,
}) : _logger = logger;
required SystemClock clock,
Analytics? analytics,
}) : _logger = logger,
_clock = clock,
_analytics = analytics ?? globals.analytics;
final Logger _logger;
final SystemClock _clock;
final Analytics _analytics;
List<DoctorValidator> get validators {
return DoctorValidatorsProvider._instance.validators;
@ -375,6 +383,10 @@ class Doctor {
bool doctorResult = true;
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()) {
final DoctorValidator validator = validatorTask.validator;
final Status status = _logger.startSpinner(
@ -404,6 +416,37 @@ class Doctor {
break;
}
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();
}
@ -727,8 +770,10 @@ class DeviceValidator extends DoctorValidator {
class DoctorText {
DoctorText(
BufferLogger logger, {
SystemClock? clock,
@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 Doctor _doctor;

View file

@ -304,6 +304,7 @@ class FlutterCommandRunner extends CommandRunner<void> {
if ((topLevelResults[FlutterGlobalOptions.kSuppressAnalyticsFlag] as bool?) ?? false) {
globals.flutterUsage.suppressAnalytics = true;
globals.analytics.suppressTelemetry();
}
globals.flutterVersion.ensureVersionFile();

View file

@ -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/platform.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/build_info.dart';
import 'package:flutter_tools/src/cache.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/web/workflow.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';
import '../../src/common.dart';
import '../../src/context.dart';
@ -166,7 +168,7 @@ void main() {
group('doctor with overridden validators', () {
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(logger.statusText, equals(
'Doctor summary (to see all details, run flutter doctor -v):\n'
@ -190,7 +192,7 @@ void main() {
});
testUsingContext('contains installed', () async {
final Doctor doctor = Doctor(logger: logger);
final Doctor doctor = Doctor(logger: logger, clock: const SystemClock());
await doctor.diagnose(verbose: false);
expect(testUsage.events.length, 3);
@ -508,7 +510,7 @@ void main() {
testUsingContext('PII separated, events only sent once', () async {
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'
' • Contains PII path/to/username\n'
'\n'
@ -816,6 +818,183 @@ void main() {
}, overrides: <Type, Generator>{
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 {
@ -952,7 +1131,8 @@ class AsyncCrashingValidator extends DoctorValidator {
/// A doctor that fails with a missing [ValidationResult].
class FakeDoctor extends Doctor {
FakeDoctor(Logger logger) : super(logger: logger);
FakeDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
@ -966,7 +1146,8 @@ class FakeDoctor extends Doctor {
/// A doctor that should pass, but still has issues in some categories.
class FakePassingDoctor extends Doctor {
FakePassingDoctor(Logger logger) : super(logger: logger);
FakePassingDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
@ -980,7 +1161,8 @@ class FakePassingDoctor extends Doctor {
/// A doctor that should pass, but still has 1 issue to test the singular of
/// categories.
class FakeSinglePassingDoctor extends Doctor {
FakeSinglePassingDoctor(Logger logger) : super(logger: logger);
FakeSinglePassingDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
@ -990,7 +1172,8 @@ class FakeSinglePassingDoctor extends Doctor {
/// A doctor that passes and has no issues anywhere.
class FakeQuietDoctor extends Doctor {
FakeQuietDoctor(Logger logger) : super(logger: logger);
FakeQuietDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
@ -1003,7 +1186,8 @@ class FakeQuietDoctor extends Doctor {
/// A doctor that passes and contains PII that can be hidden.
class FakePiiDoctor extends Doctor {
FakePiiDoctor(Logger logger) : super(logger: logger);
FakePiiDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
@ -1013,7 +1197,8 @@ class FakePiiDoctor extends Doctor {
/// A doctor with a validator that throws an exception.
class FakeCrashingDoctor extends Doctor {
FakeCrashingDoctor(Logger logger) : super(logger: logger);
FakeCrashingDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
@ -1027,7 +1212,8 @@ class FakeCrashingDoctor extends Doctor {
/// A doctor with a validator that will never finish.
class FakeAsyncStuckDoctor extends Doctor {
FakeAsyncStuckDoctor(Logger logger) : super(logger: logger);
FakeAsyncStuckDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
@ -1041,7 +1227,9 @@ class FakeAsyncStuckDoctor extends Doctor {
/// A doctor with a validator that throws an exception.
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;
@ -1121,7 +1309,8 @@ class PassingGroupedValidatorWithStatus extends DoctorValidator {
/// A doctor that has two groups of two validators each.
class FakeGroupedDoctor extends Doctor {
FakeGroupedDoctor(Logger logger) : super(logger: logger);
FakeGroupedDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
@ -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 {
FakeGroupedDoctorWithStatus(Logger logger) : super(logger: logger);
FakeGroupedDoctorWithStatus(Logger logger,
{super.clock = const SystemClock()})
: super(logger: logger);
@override
late final List<DoctorValidator> validators = <DoctorValidator>[
@ -1151,7 +1373,9 @@ class FakeGroupedDoctorWithStatus extends Doctor {
/// A doctor that takes any two validators. Used to check behavior when
/// merging ValidationTypes (installed, missing, partial).
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])],
super(logger: logger);

View file

@ -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/reporting.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_http_client.dart';
import '../../src/fakes.dart';
const String kCustomBugInstructions = 'These are instructions to report with a custom bug tracker.';
@ -317,13 +317,24 @@ void main() {
});
group('unified_analytics', () {
late FakeAnalytics fakeAnalytics;
late MemoryFileSystem fs;
setUp(() {
fs = MemoryFileSystem.test();
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fs: fs,
fakeFlutterVersion: FakeFlutterVersion(),
);
});
testUsingContext(
'runner disable telemetry with flag',
() async {
io.setExitFunctionForTests((int exitCode) {});
expect(globals.analytics.telemetryEnabled, true);
expect(globals.analytics.shouldShowMessage, true);
await runner.run(
<String>['--disable-analytics'],
@ -336,7 +347,7 @@ void main() {
expect(globals.analytics.telemetryEnabled, false);
},
overrides: <Type, Generator>{
Analytics: () => FakeAnalytics(),
Analytics: () => fakeAnalytics,
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
@ -347,8 +358,17 @@ void main() {
() async {
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.shouldShowMessage, false);
await runner.run(
<String>['--enable-analytics'],
@ -361,7 +381,7 @@ void main() {
expect(globals.analytics.telemetryEnabled, true);
},
overrides: <Type, Generator>{
Analytics: () => FakeAnalytics(fakeTelemetryStatusOverride: false),
Analytics: () => fakeAnalytics,
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
@ -373,7 +393,6 @@ void main() {
io.setExitFunctionForTests((int exitCode) {});
expect(globals.analytics.telemetryEnabled, true);
expect(globals.analytics.shouldShowMessage, true);
final int exitCode = await runner.run(
<String>[
@ -392,7 +411,7 @@ void main() {
reason: 'Should not have changed from initialization');
},
overrides: <Type, Generator>{
Analytics: () => FakeAnalytics(),
Analytics: () => fakeAnalytics,
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
@ -536,38 +555,3 @@ class WaitingCrashReporter implements CrashReporter {
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;
}

View file

@ -5,7 +5,6 @@
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.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 '../src/common.dart';
@ -13,42 +12,17 @@ import '../src/fakes.dart';
void main() {
const String userBranch = 'abc123';
const String homeDirectoryName = 'home';
const DashTool tool = DashTool.flutterTool;
late FileSystem fs;
late Directory home;
late FakeAnalytics analyticsOverride;
setUp(() {
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,
analyticsOverride = getInitializedFakeAnalyticsInstance(
fs: fs,
surveyHandler: SurveyHandler(
homeDirectory: home,
fs: fs,
),
);
initialAnalytics.clientShowedMessage();
analyticsOverride = FakeAnalytics(
tool: tool,
homeDirectory: home,
dartVersion: '3.0.0',
platform: DevicePlatform.macos,
fs: fs,
surveyHandler: SurveyHandler(
homeDirectory: home,
fs: fs,
fakeFlutterVersion: FakeFlutterVersion(
branch: userBranch,
),
);
});
@ -135,5 +109,17 @@ void main() {
);
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));
});
});
}

View file

@ -16,6 +16,10 @@ import 'package:meta/meta.dart';
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' 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:test/test.dart' hide isInstanceOf, test;
@ -305,3 +309,38 @@ class FileExceptionHandler {
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(),
);
}

View file

@ -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/template.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/cache.dart';
import 'package:flutter_tools/src/context_runner.dart';
@ -291,7 +292,8 @@ class FakeAndroidLicenseValidator extends Fake implements AndroidLicenseValidato
}
class FakeDoctor extends Doctor {
FakeDoctor(Logger logger) : super(logger: logger);
FakeDoctor(Logger logger, {super.clock = const SystemClock()})
: super(logger: logger);
// True for testing.
@override