[flutter_tools] report basic analytics for null-safety (#71487)

This commit is contained in:
Jonah Williams 2020-12-03 15:14:20 -08:00 committed by GitHub
parent b358854172
commit c92cc25830
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 223 additions and 0 deletions

View file

@ -49,4 +49,7 @@ abstract class BuildSubCommand extends FlutterCommand {
BuildSubCommand() {
requiresPubspecYaml();
}
@override
bool get reportNullSafety => true;
}

View file

@ -62,6 +62,9 @@ class BuildAarCommand extends BuildSubCommand {
@override
final String name = 'aar';
@override
bool get reportNullSafety => false;
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => <DevelopmentArtifact>{
DevelopmentArtifact.androidGenSnapshot,

View file

@ -108,6 +108,9 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
FlutterVersion _flutterVersion;
@override
bool get reportNullSafety => false;
@override
final String name = 'ios-framework';

View file

@ -145,6 +145,9 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
bool get runningWithPrebuiltApplication => argResults['use-application-binary'] != null;
bool get trackWidgetCreation => boolArg('track-widget-creation');
@override
bool get reportNullSafety => true;
/// Whether to start the application paused by default.
bool get startPausedDefault;

View file

@ -249,3 +249,65 @@ class CodeSizeEvent extends UsageEvent {
class ErrorHandlingEvent extends UsageEvent {
ErrorHandlingEvent(String parameter) : super('error-handling', parameter, flutterUsage: globals.flutterUsage);
}
/// Emit various null safety analytic events.
///
/// 1. The current null safety runtime mode.
/// 2. The number of packages that are migrated, along with the total number of packages
/// 3. The main packages language version.
class NullSafetyAnalysisEvent implements UsageEvent {
NullSafetyAnalysisEvent(
this.packageConfig,
this.nullSafetyMode,
this.currentPackage,
this.flutterUsage,
);
/// The category for analytics events related to null safety.
static const String kNullSafetyCategory = 'null-safety';
final PackageConfig packageConfig;
final NullSafetyMode nullSafetyMode;
final String currentPackage;
@override
final Usage flutterUsage;
@override
void send() {
if (packageConfig.packages.isEmpty) {
return;
}
int migrated = 0;
LanguageVersion languageVersion;
for (final Package package in packageConfig.packages) {
if (package.name == currentPackage) {
languageVersion = package.languageVersion;
}
if (package.languageVersion.major >= nullSafeVersion.major &&
package.languageVersion.minor >= nullSafeVersion.minor) {
migrated += 1;
}
}
flutterUsage.sendEvent(kNullSafetyCategory, 'runtime-mode', label: nullSafetyMode.toString());
flutterUsage.sendEvent(kNullSafetyCategory, 'stats', parameters: <String, String>{
cdKey(CustomDimensions.nullSafeMigratedLibraries): migrated.toString(),
cdKey(CustomDimensions.nullSafeTotalLibraries): packageConfig.packages.length.toString(),
});
if (languageVersion != null) {
final String formattedVersion = '${languageVersion.major}.${languageVersion.minor}';
flutterUsage.sendEvent(kNullSafetyCategory, 'language-version', label: formattedVersion);
}
}
@override
String get category => kNullSafetyCategory;
@override
String get label => throw UnsupportedError('');
@override
String get parameter => throw UnsupportedError('');
@override
int get value => throw UnsupportedError('');
}

View file

@ -10,6 +10,7 @@ import 'package:file/file.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:usage/usage_io.dart';
import '../base/async_guard.dart';
@ -21,8 +22,10 @@ import '../base/os.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/time.dart';
import '../build_info.dart';
import '../build_system/exceptions.dart';
import '../convert.dart';
import '../dart/language_version.dart';
import '../devfs.dart';
import '../doctor.dart';
import '../features.dart';

View file

@ -59,6 +59,8 @@ enum CustomDimensions {
commandPackagesAndroidEmbeddingVersion, // cd46
nullSafety, // cd47
fastReassemble, // cd48
nullSafeMigratedLibraries, // cd49
nullSafeTotalLibraries, // cd 50
}
String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}';

View file

@ -487,6 +487,9 @@ abstract class FlutterCommand extends Command<void> {
/// Whether it is safe for this command to use a cached pub invocation.
bool get cachePubGet => true;
/// Whether this command should report null safety analytics.
bool get reportNullSafety => false;
Duration get deviceDiscoveryTimeout {
if (_deviceDiscoveryTimeout == null
&& argResults.options.contains(FlutterOptions.kDeviceTimeout)
@ -1112,6 +1115,9 @@ abstract class FlutterCommand extends Command<void> {
checkUpToDate: cachePubGet,
);
await project.regeneratePlatformSpecificTooling();
if (reportNullSafety) {
await _sendNullSafetyAnalyticsEvents(project);
}
}
setupApplicationPackages();
@ -1128,6 +1134,16 @@ abstract class FlutterCommand extends Command<void> {
return await runCommand();
}
Future<void> _sendNullSafetyAnalyticsEvents(FlutterProject project) async {
final BuildInfo buildInfo = await getBuildInfo();
NullSafetyAnalysisEvent(
buildInfo.packageConfig,
buildInfo.nullSafetyMode,
project.manifest.appName,
globals.flutterUsage,
).send();
}
/// The set of development artifacts required for this command.
///
/// Defaults to an empty set. Including [DevelopmentArtifact.universal] is

View file

@ -2,9 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:mockito/mockito.dart';
import 'package:package_config/package_config.dart';
import '../../src/common.dart';
@ -50,6 +52,50 @@ void main() {
verify(usage.sendEvent('doctor-result', any, label: anyNamed('label'))).called(1);
});
testWithoutContext('Reports null safe analytics events', () {
final Usage usage = MockUsage();
final PackageConfig packageConfig = PackageConfig(<Package>[
Package('foo', Uri.parse('file:///foo/'), languageVersion: LanguageVersion(2, 12)),
Package('bar', Uri.parse('file:///fizz/'), languageVersion: LanguageVersion(2, 1)),
Package('baz', Uri.parse('file:///bar/'), languageVersion: LanguageVersion(2, 2)),
]);
NullSafetyAnalysisEvent(
packageConfig,
NullSafetyMode.sound,
'foo',
usage,
).send();
verify(usage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'runtime-mode', label: 'NullSafetyMode.sound')).called(1);
verify(usage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'stats', parameters: <String, String>{
'cd49': '1', 'cd50': '3',
})).called(1);
verify(usage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'language-version', label: '2.12')).called(1);
});
testWithoutContext('Does not crash if main package is missing', () {
final Usage usage = MockUsage();
final PackageConfig packageConfig = PackageConfig(<Package>[
Package('foo', Uri.parse('file:///foo/lib/'), languageVersion: LanguageVersion(2, 12)),
Package('bar', Uri.parse('file:///fizz/lib/'), languageVersion: LanguageVersion(2, 1)),
Package('baz', Uri.parse('file:///bar/lib/'), languageVersion: LanguageVersion(2, 2)),
]);
NullSafetyAnalysisEvent(
packageConfig,
NullSafetyMode.sound,
'something-unrelated',
usage,
).send();
verify(usage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'runtime-mode', label: 'NullSafetyMode.sound')).called(1);
verify(usage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'stats', parameters: <String, String>{
'cd49': '1', 'cd50': '3',
})).called(1);
verifyNever(usage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'language-version', label: anyNamed('label')));
});
}
class FakeGroupedValidator extends GroupedValidator {

View file

@ -14,6 +14,7 @@ import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/signals.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/version.dart';
@ -485,6 +486,47 @@ void main() {
);
}
});
testUsingContext('reports null safety analytics when reportNullSafety is true', () async {
globals.fs.file('lib/main.dart')
..createSync(recursive: true)
..writeAsStringSync('// @dart=2.12');
globals.fs.file('pubspec.yaml')
.writeAsStringSync('name: example\n');
globals.fs.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync(r'''
{
"configVersion": 2,
"packages": [
{
"name": "example",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "2.12"
}
],
"generated": "2020-12-02T19:30:53.862346Z",
"generator": "pub",
"generatorVersion": "2.12.0-76.0.dev"
}
''');
final FakeReportingNullSafetyCommand command = FakeReportingNullSafetyCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['test']);
verify(globals.flutterUsage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'runtime-mode', label: 'NullSafetyMode.sound')).called(1);
verify(globals.flutterUsage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'stats', parameters: <String, String>{
'cd49': '1', 'cd50': '1',
})).called(1);
verify(globals.flutterUsage.sendEvent(NullSafetyAnalysisEvent.kNullSafetyCategory, 'language-version', label: '2.12')).called(1);
}, overrides: <Type, Generator>{
Pub: () => FakePub(),
Usage: () => MockitoUsage(),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
});
}
@ -541,6 +583,32 @@ class FakeTargetCommand extends FlutterCommand {
String get name => 'test';
}
class FakeReportingNullSafetyCommand extends FlutterCommand {
FakeReportingNullSafetyCommand() {
argParser.addFlag('debug');
argParser.addFlag('release');
argParser.addFlag('jit-release');
argParser.addFlag('profile');
}
@override
String get description => 'test';
@override
String get name => 'test';
@override
bool get shouldRunPub => true;
@override
bool get reportNullSafety => true;
@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 {}
@ -569,3 +637,17 @@ class FakeSignals implements Signals {
@override
Stream<Object> get errors => delegate.errors;
}
class FakePub extends Fake implements Pub {
@override
Future<void> get({
PubContext context,
String directory,
bool skipIfAbsent = false,
bool upgrade = false,
bool offline = false,
bool generateSyntheticPackage = false,
String flutterRootOverride,
bool checkUpToDate = false,
}) async { }
}