Skip to content

Commit 55abbb6

Browse files
[flutter_tools] track null safety usage (flutter#59822)
* [flutter_tools] track null safety usage * Update flutter_command_test.dart * cleanups
1 parent 09f1764 commit 55abbb6

File tree

8 files changed

+138
-9
lines changed

8 files changed

+138
-9
lines changed

packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ class _ResidentWebRunner extends ResidentWebRunner {
569569
fullRestart: true,
570570
reason: reason,
571571
overallTimeInMs: timer.elapsed.inMilliseconds,
572+
nullSafety: usageNullSafety,
572573
).send();
573574
}
574575
return OperationResult.ok;

packages/flutter_tools/lib/src/reporting/events.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class HotEvent extends UsageEvent {
3939
@required this.sdkName,
4040
@required this.emulator,
4141
@required this.fullRestart,
42+
@required this.nullSafety,
4243
this.reason,
4344
this.finalLibraryCount,
4445
this.syncedLibraryCount,
@@ -55,6 +56,7 @@ class HotEvent extends UsageEvent {
5556
final String sdkName;
5657
final bool emulator;
5758
final bool fullRestart;
59+
final bool nullSafety;
5860
final int finalLibraryCount;
5961
final int syncedLibraryCount;
6062
final int syncedClassesCount;
@@ -89,6 +91,8 @@ class HotEvent extends UsageEvent {
8991
CustomDimensions.hotEventTransferTimeInMs: transferTimeInMs.toString(),
9092
if (overallTimeInMs != null)
9193
CustomDimensions.hotEventOverallTimeInMs: overallTimeInMs.toString(),
94+
if (nullSafety != null)
95+
CustomDimensions.nullSafety: nullSafety.toString(),
9296
});
9397
flutterUsage.sendEvent(category, parameter, parameters: parameters);
9498
}

packages/flutter_tools/lib/src/reporting/usage.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,14 @@ enum CustomDimensions {
5757
commandResultEventMaxRss, // cd44
5858
commandRunAndroidEmbeddingVersion, // cd45
5959
commandPackagesAndroidEmbeddingVersion, // cd46
60+
nullSafety, // cd47
6061
}
6162

6263
String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}';
6364

64-
Map<String, String> _useCdKeys(Map<CustomDimensions, String> parameters) {
65-
return parameters.map((CustomDimensions k, String v) =>
66-
MapEntry<String, String>(cdKey(k), v));
65+
Map<String, String> _useCdKeys(Map<CustomDimensions, Object> parameters) {
66+
return parameters.map((CustomDimensions k, Object v) =>
67+
MapEntry<String, String>(cdKey(k), v.toString()));
6768
}
6869

6970
abstract class Usage {
@@ -87,7 +88,7 @@ abstract class Usage {
8788

8889
/// Uses the global [Usage] instance to send a 'command' to analytics.
8990
static void command(String command, {
90-
Map<CustomDimensions, String> parameters,
91+
Map<CustomDimensions, Object> parameters,
9192
}) => globals.flutterUsage.sendCommand(command, parameters: _useCdKeys(parameters));
9293

9394
/// Whether this is the first run of the tool.

packages/flutter_tools/lib/src/resident_runner.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,11 @@ abstract class ResidentRunner {
733733
Completer<int> _finished = Completer<int>();
734734
bool hotMode;
735735

736+
/// Whether the compiler was instructed to run with null-safety enabled.
737+
@protected
738+
bool get usageNullSafety => debuggingOptions?.buildInfo
739+
?.extraFrontEndOptions?.any((String option) => option.contains('non-nullable')) ?? false;
740+
736741
/// Returns true if every device is streaming observatory URIs.
737742
bool get isWaitingForObservatory {
738743
return flutterDevices.every((FlutterDevice device) {

packages/flutter_tools/lib/src/run_hot.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,7 @@ class HotRunner extends ResidentRunner {
749749
sdkName: sdkName,
750750
emulator: emulator,
751751
fullRestart: true,
752+
nullSafety: usageNullSafety,
752753
reason: reason).send();
753754
status?.cancel();
754755
}
@@ -790,7 +791,9 @@ class HotRunner extends ResidentRunner {
790791
sdkName: sdkName,
791792
emulator: emulator,
792793
fullRestart: false,
793-
reason: reason).send();
794+
nullSafety: usageNullSafety,
795+
reason: reason,
796+
).send();
794797
return OperationResult(1, 'hot reload failed to complete', fatal: true);
795798
} finally {
796799
status.cancel();
@@ -868,6 +871,7 @@ class HotRunner extends ResidentRunner {
868871
emulator: emulator,
869872
fullRestart: false,
870873
reason: reason,
874+
nullSafety: usageNullSafety,
871875
).send();
872876
return OperationResult(1, 'Reload rejected');
873877
}
@@ -895,6 +899,7 @@ class HotRunner extends ResidentRunner {
895899
emulator: emulator,
896900
fullRestart: false,
897901
reason: reason,
902+
nullSafety: usageNullSafety,
898903
).send();
899904
return OperationResult(errorCode, errorMessage);
900905
}
@@ -1020,6 +1025,7 @@ class HotRunner extends ResidentRunner {
10201025
syncedBytes: updatedDevFS.syncedBytes,
10211026
invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
10221027
transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
1028+
nullSafety: usageNullSafety,
10231029
).send();
10241030

10251031
if (shouldReportReloadTime) {

packages/flutter_tools/lib/src/runner/flutter_command.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,10 @@ abstract class FlutterCommand extends Command<void> {
802802
);
803803
}
804804

805+
List<String> get _enabledExperiments => argParser.options.containsKey(FlutterOptions.kEnableExperiment)
806+
? stringsArg(FlutterOptions.kEnableExperiment)
807+
: <String>[];
808+
805809
/// Perform validation then call [runCommand] to execute the command.
806810
/// Return a [Future] that completes with an exit code
807811
/// indicating whether execution was successful.
@@ -836,11 +840,11 @@ abstract class FlutterCommand extends Command<void> {
836840
setupApplicationPackages();
837841

838842
if (commandPath != null) {
839-
final Map<CustomDimensions, String> additionalUsageValues =
840-
<CustomDimensions, String>{
843+
final Map<CustomDimensions, Object> additionalUsageValues =
844+
<CustomDimensions, Object>{
841845
...?await usageValues,
842-
CustomDimensions.commandHasTerminal:
843-
globals.stdio.hasTerminal ? 'true' : 'false',
846+
CustomDimensions.commandHasTerminal: globals.stdio.hasTerminal,
847+
CustomDimensions.nullSafety: _enabledExperiments.contains('non-nullable'),
844848
};
845849
Usage.command(commandPath, parameters: additionalUsageValues);
846850
}

packages/flutter_tools/test/general.shard/resident_runner_test.dart

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,12 +418,78 @@ void main() {
418418
cdKey(CustomDimensions.hotEventSdkName): 'Example',
419419
cdKey(CustomDimensions.hotEventEmulator): 'false',
420420
cdKey(CustomDimensions.hotEventFullRestart): 'false',
421+
cdKey(CustomDimensions.nullSafety): 'false',
421422
})).called(1);
422423
expect(fakeVmServiceHost.hasRemainingExpectations, false);
423424
}, overrides: <Type, Generator>{
424425
Usage: () => MockUsage(),
425426
}));
426427

428+
testUsingContext('ResidentRunner reports hot reload event with null safety analytics', () => testbed.run(() async {
429+
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
430+
listViews,
431+
listViews,
432+
listViews,
433+
]);
434+
residentRunner = HotRunner(
435+
<FlutterDevice>[
436+
mockFlutterDevice,
437+
],
438+
stayResident: false,
439+
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
440+
BuildMode.debug, '', treeShakeIcons: false, extraFrontEndOptions: <String>[
441+
'--enable-experiment=non-nullable',
442+
],
443+
)),
444+
);
445+
when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async {
446+
return 'Example';
447+
});
448+
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
449+
return TargetPlatform.android_arm;
450+
});
451+
when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async {
452+
return false;
453+
});
454+
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
455+
final Completer<void> onAppStart = Completer<void>.sync();
456+
unawaited(residentRunner.attach(
457+
appStartedCompleter: onAppStart,
458+
connectionInfoCompleter: onConnectionInfo,
459+
));
460+
await onAppStart.future;
461+
when(mockFlutterDevice.updateDevFS(
462+
mainUri: anyNamed('mainUri'),
463+
target: anyNamed('target'),
464+
bundle: anyNamed('bundle'),
465+
firstBuildTime: anyNamed('firstBuildTime'),
466+
bundleFirstUpload: anyNamed('bundleFirstUpload'),
467+
bundleDirty: anyNamed('bundleDirty'),
468+
fullRestart: anyNamed('fullRestart'),
469+
projectRootPath: anyNamed('projectRootPath'),
470+
pathToReload: anyNamed('pathToReload'),
471+
invalidatedFiles: anyNamed('invalidatedFiles'),
472+
dillOutputPath: anyNamed('dillOutputPath'),
473+
packageConfig: anyNamed('packageConfig'),
474+
)).thenThrow(vm_service.RPCError('something bad happened', 666, ''));
475+
476+
final OperationResult result = await residentRunner.restart(fullRestart: false);
477+
expect(result.fatal, true);
478+
expect(result.code, 1);
479+
verify(globals.flutterUsage.sendEvent('hot', 'exception', parameters: <String, String>{
480+
cdKey(CustomDimensions.hotEventTargetPlatform):
481+
getNameForTargetPlatform(TargetPlatform.android_arm),
482+
cdKey(CustomDimensions.hotEventSdkName): 'Example',
483+
cdKey(CustomDimensions.hotEventEmulator): 'false',
484+
cdKey(CustomDimensions.hotEventFullRestart): 'false',
485+
cdKey(CustomDimensions.nullSafety): 'true',
486+
})).called(1);
487+
expect(fakeVmServiceHost.hasRemainingExpectations, false);
488+
}, overrides: <Type, Generator>{
489+
Usage: () => MockUsage(),
490+
}));
491+
492+
427493
testUsingContext('ResidentRunner can send target platform to analytics from hot reload', () => testbed.run(() async {
428494
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
429495
listViews,
@@ -587,6 +653,7 @@ void main() {
587653
cdKey(CustomDimensions.hotEventSdkName): 'Example',
588654
cdKey(CustomDimensions.hotEventEmulator): 'false',
589655
cdKey(CustomDimensions.hotEventFullRestart): 'true',
656+
cdKey(CustomDimensions.nullSafety): 'false',
590657
})).called(1);
591658
expect(fakeVmServiceHost.hasRemainingExpectations, false);
592659
}, overrides: <Type, Generator>{

packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,30 @@ void main() {
9191
expect(flutterCommand.hidden, isTrue);
9292
});
9393

94+
testUsingContext('null-safety is surfaced in command usage analytics', () async {
95+
final FakeNullSafeCommand fake = FakeNullSafeCommand();
96+
final CommandRunner<void> commandRunner = createTestCommandRunner(fake);
97+
98+
await commandRunner.run(<String>['safety', '--enable-experiment=non-nullable']);
99+
100+
final VerificationResult resultA = verify(usage.sendCommand(
101+
'safety',
102+
parameters: captureAnyNamed('parameters'),
103+
));
104+
expect(resultA.captured.first, containsPair('cd47', 'true'));
105+
reset(usage);
106+
107+
await commandRunner.run(<String>['safety', '--enable-experiment=foo']);
108+
109+
final VerificationResult resultB = verify(usage.sendCommand(
110+
'safety',
111+
parameters: captureAnyNamed('parameters'),
112+
));
113+
expect(resultB.captured.first, containsPair('cd47', 'false'));
114+
}, overrides: <Type, Generator>{
115+
Usage: () => usage,
116+
});
117+
94118
testUsingContext('uses the error handling file system', () async {
95119
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
96120
commandFunction: () async {
@@ -463,6 +487,23 @@ class FakeDeprecatedCommand extends FlutterCommand {
463487
}
464488
}
465489

490+
class FakeNullSafeCommand extends FlutterCommand {
491+
FakeNullSafeCommand() {
492+
addEnableExperimentation(hide: false);
493+
}
494+
495+
@override
496+
String get description => 'test null safety';
497+
498+
@override
499+
String get name => 'safety';
500+
501+
@override
502+
Future<FlutterCommandResult> runCommand() async {
503+
return FlutterCommandResult.success();
504+
}
505+
}
506+
466507
class MockVersion extends Mock implements FlutterVersion {}
467508
class MockProcessInfo extends Mock implements ProcessInfo {}
468509
class MockIoProcessSignal extends Mock implements io.ProcessSignal {}

0 commit comments

Comments
 (0)