Unverified Commit 55abbb6b authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] track null safety usage (#59822)

* [flutter_tools] track null safety usage

* Update flutter_command_test.dart

* cleanups
parent 09f1764d
...@@ -569,6 +569,7 @@ class _ResidentWebRunner extends ResidentWebRunner { ...@@ -569,6 +569,7 @@ class _ResidentWebRunner extends ResidentWebRunner {
fullRestart: true, fullRestart: true,
reason: reason, reason: reason,
overallTimeInMs: timer.elapsed.inMilliseconds, overallTimeInMs: timer.elapsed.inMilliseconds,
nullSafety: usageNullSafety,
).send(); ).send();
} }
return OperationResult.ok; return OperationResult.ok;
......
...@@ -39,6 +39,7 @@ class HotEvent extends UsageEvent { ...@@ -39,6 +39,7 @@ class HotEvent extends UsageEvent {
@required this.sdkName, @required this.sdkName,
@required this.emulator, @required this.emulator,
@required this.fullRestart, @required this.fullRestart,
@required this.nullSafety,
this.reason, this.reason,
this.finalLibraryCount, this.finalLibraryCount,
this.syncedLibraryCount, this.syncedLibraryCount,
...@@ -55,6 +56,7 @@ class HotEvent extends UsageEvent { ...@@ -55,6 +56,7 @@ class HotEvent extends UsageEvent {
final String sdkName; final String sdkName;
final bool emulator; final bool emulator;
final bool fullRestart; final bool fullRestart;
final bool nullSafety;
final int finalLibraryCount; final int finalLibraryCount;
final int syncedLibraryCount; final int syncedLibraryCount;
final int syncedClassesCount; final int syncedClassesCount;
...@@ -89,6 +91,8 @@ class HotEvent extends UsageEvent { ...@@ -89,6 +91,8 @@ class HotEvent extends UsageEvent {
CustomDimensions.hotEventTransferTimeInMs: transferTimeInMs.toString(), CustomDimensions.hotEventTransferTimeInMs: transferTimeInMs.toString(),
if (overallTimeInMs != null) if (overallTimeInMs != null)
CustomDimensions.hotEventOverallTimeInMs: overallTimeInMs.toString(), CustomDimensions.hotEventOverallTimeInMs: overallTimeInMs.toString(),
if (nullSafety != null)
CustomDimensions.nullSafety: nullSafety.toString(),
}); });
flutterUsage.sendEvent(category, parameter, parameters: parameters); flutterUsage.sendEvent(category, parameter, parameters: parameters);
} }
......
...@@ -57,13 +57,14 @@ enum CustomDimensions { ...@@ -57,13 +57,14 @@ enum CustomDimensions {
commandResultEventMaxRss, // cd44 commandResultEventMaxRss, // cd44
commandRunAndroidEmbeddingVersion, // cd45 commandRunAndroidEmbeddingVersion, // cd45
commandPackagesAndroidEmbeddingVersion, // cd46 commandPackagesAndroidEmbeddingVersion, // cd46
nullSafety, // cd47
} }
String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}'; String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}';
Map<String, String> _useCdKeys(Map<CustomDimensions, String> parameters) { Map<String, String> _useCdKeys(Map<CustomDimensions, Object> parameters) {
return parameters.map((CustomDimensions k, String v) => return parameters.map((CustomDimensions k, Object v) =>
MapEntry<String, String>(cdKey(k), v)); MapEntry<String, String>(cdKey(k), v.toString()));
} }
abstract class Usage { abstract class Usage {
...@@ -87,7 +88,7 @@ abstract class Usage { ...@@ -87,7 +88,7 @@ abstract class Usage {
/// Uses the global [Usage] instance to send a 'command' to analytics. /// Uses the global [Usage] instance to send a 'command' to analytics.
static void command(String command, { static void command(String command, {
Map<CustomDimensions, String> parameters, Map<CustomDimensions, Object> parameters,
}) => globals.flutterUsage.sendCommand(command, parameters: _useCdKeys(parameters)); }) => globals.flutterUsage.sendCommand(command, parameters: _useCdKeys(parameters));
/// Whether this is the first run of the tool. /// Whether this is the first run of the tool.
......
...@@ -733,6 +733,11 @@ abstract class ResidentRunner { ...@@ -733,6 +733,11 @@ abstract class ResidentRunner {
Completer<int> _finished = Completer<int>(); Completer<int> _finished = Completer<int>();
bool hotMode; bool hotMode;
/// Whether the compiler was instructed to run with null-safety enabled.
@protected
bool get usageNullSafety => debuggingOptions?.buildInfo
?.extraFrontEndOptions?.any((String option) => option.contains('non-nullable')) ?? false;
/// Returns true if every device is streaming observatory URIs. /// Returns true if every device is streaming observatory URIs.
bool get isWaitingForObservatory { bool get isWaitingForObservatory {
return flutterDevices.every((FlutterDevice device) { return flutterDevices.every((FlutterDevice device) {
......
...@@ -749,6 +749,7 @@ class HotRunner extends ResidentRunner { ...@@ -749,6 +749,7 @@ class HotRunner extends ResidentRunner {
sdkName: sdkName, sdkName: sdkName,
emulator: emulator, emulator: emulator,
fullRestart: true, fullRestart: true,
nullSafety: usageNullSafety,
reason: reason).send(); reason: reason).send();
status?.cancel(); status?.cancel();
} }
...@@ -790,7 +791,9 @@ class HotRunner extends ResidentRunner { ...@@ -790,7 +791,9 @@ class HotRunner extends ResidentRunner {
sdkName: sdkName, sdkName: sdkName,
emulator: emulator, emulator: emulator,
fullRestart: false, fullRestart: false,
reason: reason).send(); nullSafety: usageNullSafety,
reason: reason,
).send();
return OperationResult(1, 'hot reload failed to complete', fatal: true); return OperationResult(1, 'hot reload failed to complete', fatal: true);
} finally { } finally {
status.cancel(); status.cancel();
...@@ -868,6 +871,7 @@ class HotRunner extends ResidentRunner { ...@@ -868,6 +871,7 @@ class HotRunner extends ResidentRunner {
emulator: emulator, emulator: emulator,
fullRestart: false, fullRestart: false,
reason: reason, reason: reason,
nullSafety: usageNullSafety,
).send(); ).send();
return OperationResult(1, 'Reload rejected'); return OperationResult(1, 'Reload rejected');
} }
...@@ -895,6 +899,7 @@ class HotRunner extends ResidentRunner { ...@@ -895,6 +899,7 @@ class HotRunner extends ResidentRunner {
emulator: emulator, emulator: emulator,
fullRestart: false, fullRestart: false,
reason: reason, reason: reason,
nullSafety: usageNullSafety,
).send(); ).send();
return OperationResult(errorCode, errorMessage); return OperationResult(errorCode, errorMessage);
} }
...@@ -1020,6 +1025,7 @@ class HotRunner extends ResidentRunner { ...@@ -1020,6 +1025,7 @@ class HotRunner extends ResidentRunner {
syncedBytes: updatedDevFS.syncedBytes, syncedBytes: updatedDevFS.syncedBytes,
invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount, invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
transferTimeInMs: devFSTimer.elapsed.inMilliseconds, transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
nullSafety: usageNullSafety,
).send(); ).send();
if (shouldReportReloadTime) { if (shouldReportReloadTime) {
......
...@@ -802,6 +802,10 @@ abstract class FlutterCommand extends Command<void> { ...@@ -802,6 +802,10 @@ abstract class FlutterCommand extends Command<void> {
); );
} }
List<String> get _enabledExperiments => argParser.options.containsKey(FlutterOptions.kEnableExperiment)
? stringsArg(FlutterOptions.kEnableExperiment)
: <String>[];
/// Perform validation then call [runCommand] to execute the command. /// Perform validation then call [runCommand] to execute the command.
/// Return a [Future] that completes with an exit code /// Return a [Future] that completes with an exit code
/// indicating whether execution was successful. /// indicating whether execution was successful.
...@@ -836,11 +840,11 @@ abstract class FlutterCommand extends Command<void> { ...@@ -836,11 +840,11 @@ abstract class FlutterCommand extends Command<void> {
setupApplicationPackages(); setupApplicationPackages();
if (commandPath != null) { if (commandPath != null) {
final Map<CustomDimensions, String> additionalUsageValues = final Map<CustomDimensions, Object> additionalUsageValues =
<CustomDimensions, String>{ <CustomDimensions, Object>{
...?await usageValues, ...?await usageValues,
CustomDimensions.commandHasTerminal: CustomDimensions.commandHasTerminal: globals.stdio.hasTerminal,
globals.stdio.hasTerminal ? 'true' : 'false', CustomDimensions.nullSafety: _enabledExperiments.contains('non-nullable'),
}; };
Usage.command(commandPath, parameters: additionalUsageValues); Usage.command(commandPath, parameters: additionalUsageValues);
} }
......
...@@ -418,12 +418,78 @@ void main() { ...@@ -418,12 +418,78 @@ void main() {
cdKey(CustomDimensions.hotEventSdkName): 'Example', cdKey(CustomDimensions.hotEventSdkName): 'Example',
cdKey(CustomDimensions.hotEventEmulator): 'false', cdKey(CustomDimensions.hotEventEmulator): 'false',
cdKey(CustomDimensions.hotEventFullRestart): 'false', cdKey(CustomDimensions.hotEventFullRestart): 'false',
cdKey(CustomDimensions.nullSafety): 'false',
})).called(1); })).called(1);
expect(fakeVmServiceHost.hasRemainingExpectations, false); expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Usage: () => MockUsage(), Usage: () => MockUsage(),
})); }));
testUsingContext('ResidentRunner reports hot reload event with null safety analytics', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
listViews,
listViews,
]);
residentRunner = HotRunner(
<FlutterDevice>[
mockFlutterDevice,
],
stayResident: false,
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
BuildMode.debug, '', treeShakeIcons: false, extraFrontEndOptions: <String>[
'--enable-experiment=non-nullable',
],
)),
);
when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async {
return 'Example';
});
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
return TargetPlatform.android_arm;
});
when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async {
return false;
});
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
unawaited(residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
));
await onAppStart.future;
when(mockFlutterDevice.updateDevFS(
mainUri: anyNamed('mainUri'),
target: anyNamed('target'),
bundle: anyNamed('bundle'),
firstBuildTime: anyNamed('firstBuildTime'),
bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleDirty: anyNamed('bundleDirty'),
fullRestart: anyNamed('fullRestart'),
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'),
dillOutputPath: anyNamed('dillOutputPath'),
packageConfig: anyNamed('packageConfig'),
)).thenThrow(vm_service.RPCError('something bad happened', 666, ''));
final OperationResult result = await residentRunner.restart(fullRestart: false);
expect(result.fatal, true);
expect(result.code, 1);
verify(globals.flutterUsage.sendEvent('hot', 'exception', parameters: <String, String>{
cdKey(CustomDimensions.hotEventTargetPlatform):
getNameForTargetPlatform(TargetPlatform.android_arm),
cdKey(CustomDimensions.hotEventSdkName): 'Example',
cdKey(CustomDimensions.hotEventEmulator): 'false',
cdKey(CustomDimensions.hotEventFullRestart): 'false',
cdKey(CustomDimensions.nullSafety): 'true',
})).called(1);
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
Usage: () => MockUsage(),
}));
testUsingContext('ResidentRunner can send target platform to analytics from hot reload', () => testbed.run(() async { testUsingContext('ResidentRunner can send target platform to analytics from hot reload', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews, listViews,
...@@ -587,6 +653,7 @@ void main() { ...@@ -587,6 +653,7 @@ void main() {
cdKey(CustomDimensions.hotEventSdkName): 'Example', cdKey(CustomDimensions.hotEventSdkName): 'Example',
cdKey(CustomDimensions.hotEventEmulator): 'false', cdKey(CustomDimensions.hotEventEmulator): 'false',
cdKey(CustomDimensions.hotEventFullRestart): 'true', cdKey(CustomDimensions.hotEventFullRestart): 'true',
cdKey(CustomDimensions.nullSafety): 'false',
})).called(1); })).called(1);
expect(fakeVmServiceHost.hasRemainingExpectations, false); expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
......
...@@ -91,6 +91,30 @@ void main() { ...@@ -91,6 +91,30 @@ void main() {
expect(flutterCommand.hidden, isTrue); expect(flutterCommand.hidden, isTrue);
}); });
testUsingContext('null-safety is surfaced in command usage analytics', () async {
final FakeNullSafeCommand fake = FakeNullSafeCommand();
final CommandRunner<void> commandRunner = createTestCommandRunner(fake);
await commandRunner.run(<String>['safety', '--enable-experiment=non-nullable']);
final VerificationResult resultA = verify(usage.sendCommand(
'safety',
parameters: captureAnyNamed('parameters'),
));
expect(resultA.captured.first, containsPair('cd47', 'true'));
reset(usage);
await commandRunner.run(<String>['safety', '--enable-experiment=foo']);
final VerificationResult resultB = verify(usage.sendCommand(
'safety',
parameters: captureAnyNamed('parameters'),
));
expect(resultB.captured.first, containsPair('cd47', 'false'));
}, overrides: <Type, Generator>{
Usage: () => usage,
});
testUsingContext('uses the error handling file system', () async { testUsingContext('uses the error handling file system', () async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand( final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async { commandFunction: () async {
...@@ -463,6 +487,23 @@ class FakeDeprecatedCommand extends FlutterCommand { ...@@ -463,6 +487,23 @@ class FakeDeprecatedCommand extends FlutterCommand {
} }
} }
class FakeNullSafeCommand extends FlutterCommand {
FakeNullSafeCommand() {
addEnableExperimentation(hide: false);
}
@override
String get description => 'test null safety';
@override
String get name => 'safety';
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
}
class MockVersion extends Mock implements FlutterVersion {} class MockVersion extends Mock implements FlutterVersion {}
class MockProcessInfo extends Mock implements ProcessInfo {} class MockProcessInfo extends Mock implements ProcessInfo {}
class MockIoProcessSignal extends Mock implements io.ProcessSignal {} class MockIoProcessSignal extends Mock implements io.ProcessSignal {}
......
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