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 {
fullRestart: true,
reason: reason,
overallTimeInMs: timer.elapsed.inMilliseconds,
nullSafety: usageNullSafety,
).send();
}
return OperationResult.ok;
......
......@@ -39,6 +39,7 @@ class HotEvent extends UsageEvent {
@required this.sdkName,
@required this.emulator,
@required this.fullRestart,
@required this.nullSafety,
this.reason,
this.finalLibraryCount,
this.syncedLibraryCount,
......@@ -55,6 +56,7 @@ class HotEvent extends UsageEvent {
final String sdkName;
final bool emulator;
final bool fullRestart;
final bool nullSafety;
final int finalLibraryCount;
final int syncedLibraryCount;
final int syncedClassesCount;
......@@ -89,6 +91,8 @@ class HotEvent extends UsageEvent {
CustomDimensions.hotEventTransferTimeInMs: transferTimeInMs.toString(),
if (overallTimeInMs != null)
CustomDimensions.hotEventOverallTimeInMs: overallTimeInMs.toString(),
if (nullSafety != null)
CustomDimensions.nullSafety: nullSafety.toString(),
});
flutterUsage.sendEvent(category, parameter, parameters: parameters);
}
......
......@@ -57,13 +57,14 @@ enum CustomDimensions {
commandResultEventMaxRss, // cd44
commandRunAndroidEmbeddingVersion, // cd45
commandPackagesAndroidEmbeddingVersion, // cd46
nullSafety, // cd47
}
String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}';
Map<String, String> _useCdKeys(Map<CustomDimensions, String> parameters) {
return parameters.map((CustomDimensions k, String v) =>
MapEntry<String, String>(cdKey(k), v));
Map<String, String> _useCdKeys(Map<CustomDimensions, Object> parameters) {
return parameters.map((CustomDimensions k, Object v) =>
MapEntry<String, String>(cdKey(k), v.toString()));
}
abstract class Usage {
......@@ -87,7 +88,7 @@ abstract class Usage {
/// Uses the global [Usage] instance to send a 'command' to analytics.
static void command(String command, {
Map<CustomDimensions, String> parameters,
Map<CustomDimensions, Object> parameters,
}) => globals.flutterUsage.sendCommand(command, parameters: _useCdKeys(parameters));
/// Whether this is the first run of the tool.
......
......@@ -733,6 +733,11 @@ abstract class ResidentRunner {
Completer<int> _finished = Completer<int>();
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.
bool get isWaitingForObservatory {
return flutterDevices.every((FlutterDevice device) {
......
......@@ -749,6 +749,7 @@ class HotRunner extends ResidentRunner {
sdkName: sdkName,
emulator: emulator,
fullRestart: true,
nullSafety: usageNullSafety,
reason: reason).send();
status?.cancel();
}
......@@ -790,7 +791,9 @@ class HotRunner extends ResidentRunner {
sdkName: sdkName,
emulator: emulator,
fullRestart: false,
reason: reason).send();
nullSafety: usageNullSafety,
reason: reason,
).send();
return OperationResult(1, 'hot reload failed to complete', fatal: true);
} finally {
status.cancel();
......@@ -868,6 +871,7 @@ class HotRunner extends ResidentRunner {
emulator: emulator,
fullRestart: false,
reason: reason,
nullSafety: usageNullSafety,
).send();
return OperationResult(1, 'Reload rejected');
}
......@@ -895,6 +899,7 @@ class HotRunner extends ResidentRunner {
emulator: emulator,
fullRestart: false,
reason: reason,
nullSafety: usageNullSafety,
).send();
return OperationResult(errorCode, errorMessage);
}
......@@ -1020,6 +1025,7 @@ class HotRunner extends ResidentRunner {
syncedBytes: updatedDevFS.syncedBytes,
invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
nullSafety: usageNullSafety,
).send();
if (shouldReportReloadTime) {
......
......@@ -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.
/// Return a [Future] that completes with an exit code
/// indicating whether execution was successful.
......@@ -836,11 +840,11 @@ abstract class FlutterCommand extends Command<void> {
setupApplicationPackages();
if (commandPath != null) {
final Map<CustomDimensions, String> additionalUsageValues =
<CustomDimensions, String>{
final Map<CustomDimensions, Object> additionalUsageValues =
<CustomDimensions, Object>{
...?await usageValues,
CustomDimensions.commandHasTerminal:
globals.stdio.hasTerminal ? 'true' : 'false',
CustomDimensions.commandHasTerminal: globals.stdio.hasTerminal,
CustomDimensions.nullSafety: _enabledExperiments.contains('non-nullable'),
};
Usage.command(commandPath, parameters: additionalUsageValues);
}
......
......@@ -418,12 +418,78 @@ void main() {
cdKey(CustomDimensions.hotEventSdkName): 'Example',
cdKey(CustomDimensions.hotEventEmulator): 'false',
cdKey(CustomDimensions.hotEventFullRestart): 'false',
cdKey(CustomDimensions.nullSafety): 'false',
})).called(1);
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
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 {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
......@@ -587,6 +653,7 @@ void main() {
cdKey(CustomDimensions.hotEventSdkName): 'Example',
cdKey(CustomDimensions.hotEventEmulator): 'false',
cdKey(CustomDimensions.hotEventFullRestart): 'true',
cdKey(CustomDimensions.nullSafety): 'false',
})).called(1);
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
......
......@@ -91,6 +91,30 @@ void main() {
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 {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async {
......@@ -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 MockProcessInfo extends Mock implements ProcessInfo {}
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