mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
[flutter_tools] report basic analytics for null-safety (#71487)
This commit is contained in:
parent
b358854172
commit
c92cc25830
|
@ -49,4 +49,7 @@ abstract class BuildSubCommand extends FlutterCommand {
|
|||
BuildSubCommand() {
|
||||
requiresPubspecYaml();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get reportNullSafety => true;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -108,6 +108,9 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
|
|||
|
||||
FlutterVersion _flutterVersion;
|
||||
|
||||
@override
|
||||
bool get reportNullSafety => false;
|
||||
|
||||
@override
|
||||
final String name = 'ios-framework';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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('');
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 { }
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue