From 2b218fd1fc6470e4722aa6614c82c31cd7889601 Mon Sep 17 00:00:00 2001
From: Elias Yishak <42216813+eliasyishak@users.noreply.github.com>
Date: Thu, 7 Dec 2023 09:12:03 -0700
Subject: [PATCH] Migrate command usage values (#139383)
Related to the tracker issue:
- https://github.com/flutter/flutter/issues/128251
This PR migrates the `Usage.command` static method that sent custom dimensions for each command (if applicable). The screenshot below shows the different places where the `usageValues` getter is overwritten to return the necessary custom dimensions for that command.
---
.../lib/src/commands/assemble.dart | 38 +++++-----
.../lib/src/commands/build_aar.dart | 21 +++++
.../lib/src/commands/build_apk.dart | 26 +++++++
.../lib/src/commands/build_appbundle.dart | 25 ++++++
.../lib/src/commands/build_bundle.dart | 14 ++++
.../lib/src/commands/create.dart | 10 +++
.../lib/src/commands/packages.dart | 53 ++++++++++++-
.../flutter_tools/lib/src/commands/run.dart | 76 ++++++++++++++++---
.../lib/src/runner/flutter_command.dart | 25 +++++-
.../hermetic/assemble_test.dart | 34 +++++++++
.../hermetic/build_aar_test.dart | 16 ++++
.../commands.shard/hermetic/run_test.dart | 42 ++++++++++
.../permeable/build_apk_test.dart | 21 +++++
.../permeable/build_appbundle_test.dart | 18 +++++
.../permeable/build_bundle_test.dart | 40 +++++++++-
.../commands.shard/permeable/create_test.dart | 26 ++++++-
.../permeable/packages_test.dart | 30 ++++++++
packages/flutter_tools/test/src/common.dart | 2 +-
18 files changed, 475 insertions(+), 42 deletions(-)
diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart
index 21093f6879f..974a768cb43 100644
--- a/packages/flutter_tools/lib/src/commands/assemble.dart
+++ b/packages/flutter_tools/lib/src/commands/assemble.dart
@@ -4,6 +4,7 @@
import 'package:args/args.dart';
import 'package:meta/meta.dart';
+import 'package:unified_analytics/unified_analytics.dart';
import '../artifacts.dart';
import '../base/common.dart';
@@ -126,6 +127,8 @@ class AssembleCommand extends FlutterCommand {
final BuildSystem _buildSystem;
+ late final FlutterProject _flutterProject = FlutterProject.current();
+
@override
String get description => 'Assemble and build Flutter resources.';
@@ -136,18 +139,18 @@ class AssembleCommand extends FlutterCommand {
String get category => FlutterCommandCategory.project;
@override
- Future get usageValues async {
- final FlutterProject flutterProject = FlutterProject.current();
- try {
- return CustomDimensions(
- commandBuildBundleTargetPlatform: _environment.defines[kTargetPlatform],
- commandBuildBundleIsModule: flutterProject.isModule,
- );
- } on Exception {
- // We've failed to send usage.
- }
- return const CustomDimensions();
- }
+ Future get usageValues async => CustomDimensions(
+ commandBuildBundleTargetPlatform: _environment.defines[kTargetPlatform],
+ commandBuildBundleIsModule: _flutterProject.isModule,
+ );
+
+ @override
+ Future unifiedAnalyticsUsageValues(String commandPath) async => Event.commandUsageValues(
+ workflow: commandPath,
+ commandHasTerminal: hasTerminal,
+ buildBundleTargetPlatform: _environment.defines[kTargetPlatform],
+ buildBundleIsModule: _flutterProject.isModule,
+ );
@override
Future> get requiredArtifacts async {
@@ -208,22 +211,21 @@ class AssembleCommand extends FlutterCommand {
/// The environmental configuration for a build invocation.
Environment _createEnvironment() {
- final FlutterProject flutterProject = FlutterProject.current();
String? output = stringArg('output');
if (output == null) {
throwToolExit('--output directory is required for assemble.');
}
// If path is relative, make it absolute from flutter project.
if (globals.fs.path.isRelative(output)) {
- output = globals.fs.path.join(flutterProject.directory.path, output);
+ output = globals.fs.path.join(_flutterProject.directory.path, output);
}
final Artifacts artifacts = globals.artifacts!;
final Environment result = Environment(
outputDir: globals.fs.directory(output),
- buildDir: flutterProject.directory
+ buildDir: _flutterProject.directory
.childDirectory('.dart_tool')
.childDirectory('flutter_build'),
- projectDir: flutterProject.directory,
+ projectDir: _flutterProject.directory,
defines: _parseDefines(stringsArg('define')),
inputs: _parseDefines(stringsArg('input')),
cacheDir: globals.cache.getRoot(),
@@ -266,7 +268,7 @@ class AssembleCommand extends FlutterCommand {
}
results[kDeferredComponents] = 'false';
- if (FlutterProject.current().manifest.deferredComponents != null && isDeferredComponentsTargets() && !isDebug()) {
+ if (_flutterProject.manifest.deferredComponents != null && isDeferredComponentsTargets() && !isDebug()) {
results[kDeferredComponents] = 'true';
}
if (argumentResults.wasParsed(FlutterOptions.kExtraFrontEndOptions)) {
@@ -297,7 +299,7 @@ class AssembleCommand extends FlutterCommand {
"Try re-running 'flutter build ios' or the appropriate build command."
);
}
- if (FlutterProject.current().manifest.deferredComponents != null
+ if (_flutterProject.manifest.deferredComponents != null
&& decodedDefines.contains('validate-deferred-components=true')
&& deferredTargets.isNotEmpty
&& !isDebug()) {
diff --git a/packages/flutter_tools/lib/src/commands/build_aar.dart b/packages/flutter_tools/lib/src/commands/build_aar.dart
index 042ad664a40..b14fa775738 100644
--- a/packages/flutter_tools/lib/src/commands/build_aar.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aar.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:unified_analytics/unified_analytics.dart';
+
import '../android/android_builder.dart';
import '../android/android_sdk.dart';
import '../android/gradle_utils.dart';
@@ -94,6 +96,25 @@ class BuildAarCommand extends BuildSubCommand {
);
}
+ @override
+ Future unifiedAnalyticsUsageValues(String commandPath) async {
+ final String projectType;
+ if (project.manifest.isModule) {
+ projectType = 'module';
+ } else if (project.manifest.isPlugin) {
+ projectType = 'plugin';
+ } else {
+ projectType = 'app';
+ }
+
+ return Event.commandUsageValues(
+ workflow: commandPath,
+ commandHasTerminal: hasTerminal,
+ buildAarProjectType: projectType,
+ buildAarTargetPlatform: stringsArg('target-platform').join(','),
+ );
+ }
+
@override
final String description = 'Build a repository containing an AAR and a POM file.\n\n'
'By default, AARs are built for `release`, `debug` and `profile`.\n'
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index d255d4dd68c..b9fb98c84a2 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:unified_analytics/unified_analytics.dart';
+
import '../android/android_builder.dart';
import '../android/build_validation.dart';
import '../android/gradle_utils.dart';
@@ -98,6 +100,30 @@ class BuildApkCommand extends BuildSubCommand {
);
}
+ @override
+ Future unifiedAnalyticsUsageValues(String commandPath) async {
+ final String buildMode;
+
+ if (boolArg('release')) {
+ buildMode = 'release';
+ } else if (boolArg('debug')) {
+ buildMode = 'debug';
+ } else if (boolArg('profile')) {
+ buildMode = 'profile';
+ } else {
+ // The build defaults to release.
+ buildMode = 'release';
+ }
+
+ return Event.commandUsageValues(
+ workflow: commandPath,
+ commandHasTerminal: hasTerminal,
+ buildApkTargetPlatform: stringsArg('target-platform').join(','),
+ buildApkBuildMode: buildMode,
+ buildApkSplitPerAbi: boolArg('split-per-abi'),
+ );
+ }
+
@override
Future runCommand() async {
if (globals.androidSdk == null) {
diff --git a/packages/flutter_tools/lib/src/commands/build_appbundle.dart b/packages/flutter_tools/lib/src/commands/build_appbundle.dart
index 000d2cc2f3c..5ad613b2ddf 100644
--- a/packages/flutter_tools/lib/src/commands/build_appbundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_appbundle.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:unified_analytics/unified_analytics.dart';
+
import '../android/android_builder.dart';
import '../android/build_validation.dart';
import '../android/deferred_components_prebuild_validator.dart';
@@ -105,6 +107,29 @@ class BuildAppBundleCommand extends BuildSubCommand {
);
}
+ @override
+ Future unifiedAnalyticsUsageValues(String commandPath) async {
+ final String buildMode;
+
+ if (boolArg('release')) {
+ buildMode = 'release';
+ } else if (boolArg('debug')) {
+ buildMode = 'debug';
+ } else if (boolArg('profile')) {
+ buildMode = 'profile';
+ } else {
+ // The build defaults to release.
+ buildMode = 'release';
+ }
+
+ return Event.commandUsageValues(
+ workflow: commandPath,
+ commandHasTerminal: hasTerminal,
+ buildAppBundleTargetPlatform: stringsArg('target-platform').join(','),
+ buildAppBundleBuildMode: buildMode,
+ );
+ }
+
@override
Future runCommand() async {
if (globals.androidSdk == null) {
diff --git a/packages/flutter_tools/lib/src/commands/build_bundle.dart b/packages/flutter_tools/lib/src/commands/build_bundle.dart
index 8bf6e1c10d8..21291093d69 100644
--- a/packages/flutter_tools/lib/src/commands/build_bundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_bundle.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:unified_analytics/unified_analytics.dart';
+
import '../base/common.dart';
import '../build_info.dart';
import '../bundle.dart';
@@ -83,6 +85,18 @@ class BuildBundleCommand extends BuildSubCommand {
);
}
+ @override
+ Future unifiedAnalyticsUsageValues(String commandPath) async {
+ final String projectDir = globals.fs.file(targetFile).parent.parent.path;
+ final FlutterProject flutterProject = FlutterProject.fromDirectory(globals.fs.directory(projectDir));
+ return Event.commandUsageValues(
+ workflow: commandPath,
+ commandHasTerminal: hasTerminal,
+ buildBundleTargetPlatform: stringArg('target-platform'),
+ buildBundleIsModule: flutterProject.isModule,
+ );
+ }
+
@override
Future validateCommand() async {
if (boolArg('tree-shake-icons')) {
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index 1fb2d4a6cc3..2e146fd6eea 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:meta/meta.dart';
+import 'package:unified_analytics/unified_analytics.dart';
import '../android/gradle_utils.dart' as gradle;
import '../base/common.dart';
@@ -91,6 +92,15 @@ class CreateCommand extends CreateBase {
);
}
+ @override
+ Future unifiedAnalyticsUsageValues(String commandPath) async => Event.commandUsageValues(
+ workflow: commandPath,
+ commandHasTerminal: hasTerminal,
+ createProjectType: stringArg('template'),
+ createAndroidLanguage: stringArg('android-language'),
+ createIosLanguage: stringArg('ios-language'),
+ );
+
// Lazy-initialize the net utilities with values from the context.
late final Net _net = Net(
httpClientFactory: context.get(),
diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart
index bae2f4fd7e9..e443ec994af 100644
--- a/packages/flutter_tools/lib/src/commands/packages.dart
+++ b/packages/flutter_tools/lib/src/commands/packages.dart
@@ -389,6 +389,24 @@ class PackagesGetCommand extends FlutterCommand {
return FlutterCommandResult.success();
}
+ late final Future> _pluginsFound = (() async {
+ final FlutterProject? rootProject = _rootProject;
+ if (rootProject == null) {
+ return [];
+ }
+
+ return findPlugins(rootProject, throwOnError: false);
+ })();
+
+ late final String? _androidEmbeddingVersion = (() {
+ final FlutterProject? rootProject = _rootProject;
+ if (rootProject == null) {
+ return null;
+ }
+
+ return rootProject.android.getEmbeddingVersion().toString().split('.').last;
+ })();
+
/// The pub packages usage values are incorrect since these are calculated/sent
/// before pub get completes. This needs to be performed after dependency resolution.
@override
@@ -405,7 +423,7 @@ class PackagesGetCommand extends FlutterCommand {
if (hasPlugins) {
// Do not fail pub get if package config files are invalid before pub has
// had a chance to run.
- final List plugins = await findPlugins(rootProject, throwOnError: false);
+ final List plugins = await _pluginsFound;
numberPlugins = plugins.length;
} else {
numberPlugins = 0;
@@ -414,7 +432,38 @@ class PackagesGetCommand extends FlutterCommand {
return CustomDimensions(
commandPackagesNumberPlugins: numberPlugins,
commandPackagesProjectModule: rootProject.isModule,
- commandPackagesAndroidEmbeddingVersion: rootProject.android.getEmbeddingVersion().toString().split('.').last,
+ commandPackagesAndroidEmbeddingVersion: _androidEmbeddingVersion,
+ );
+ }
+
+ /// The pub packages usage values are incorrect since these are calculated/sent
+ /// before pub get completes. This needs to be performed after dependency resolution.
+ @override
+ Future unifiedAnalyticsUsageValues(String commandPath) async {
+ final FlutterProject? rootProject = _rootProject;
+ if (rootProject == null) {
+ return Event.commandUsageValues(workflow: commandPath, commandHasTerminal: hasTerminal);
+ }
+
+ final int numberPlugins;
+ // Do not send plugin analytics if pub has not run before.
+ final bool hasPlugins = rootProject.flutterPluginsDependenciesFile.existsSync()
+ && rootProject.packageConfigFile.existsSync();
+ if (hasPlugins) {
+ // Do not fail pub get if package config files are invalid before pub has
+ // had a chance to run.
+ final List plugins = await _pluginsFound;
+ numberPlugins = plugins.length;
+ } else {
+ numberPlugins = 0;
+ }
+
+ return Event.commandUsageValues(
+ workflow: commandPath,
+ commandHasTerminal: hasTerminal,
+ packagesNumberPlugins: numberPlugins,
+ packagesProjectModule: rootProject.isModule,
+ packagesAndroidEmbeddingVersion: _androidEmbeddingVersion,
);
}
}
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index b5c73e5c8d3..f63e0429d87 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -5,6 +5,7 @@
import 'dart:async';
import 'package:meta/meta.dart';
+import 'package:unified_analytics/unified_analytics.dart' as analytics;
import 'package:vm_service/vm_service.dart';
import '../android/android_device.dart';
@@ -447,6 +448,43 @@ class RunCommand extends RunCommandBase {
@override
Future get usageValues async {
+ final AnalyticsUsageValuesRecord record = await _sharedAnalyticsUsageValues;
+
+ return CustomDimensions(
+ commandRunIsEmulator: record.runIsEmulator,
+ commandRunTargetName: record.runTargetName,
+ commandRunTargetOsVersion: record.runTargetOsVersion,
+ commandRunModeName: record.runModeName,
+ commandRunProjectModule: record.runProjectModule,
+ commandRunProjectHostLanguage: record.runProjectHostLanguage,
+ commandRunAndroidEmbeddingVersion: record.runAndroidEmbeddingVersion,
+ commandRunEnableImpeller: record.runEnableImpeller,
+ commandRunIOSInterfaceType: record.runIOSInterfaceType,
+ commandRunIsTest: record.runIsTest,
+ );
+ }
+
+ @override
+ Future unifiedAnalyticsUsageValues(String commandPath) async {
+ final AnalyticsUsageValuesRecord record = await _sharedAnalyticsUsageValues;
+
+ return analytics.Event.commandUsageValues(
+ workflow: commandPath,
+ commandHasTerminal: hasTerminal,
+ runIsEmulator: record.runIsEmulator,
+ runTargetName: record.runTargetName,
+ runTargetOsVersion: record.runTargetOsVersion,
+ runModeName: record.runModeName,
+ runProjectModule: record.runProjectModule,
+ runProjectHostLanguage: record.runProjectHostLanguage,
+ runAndroidEmbeddingVersion: record.runAndroidEmbeddingVersion,
+ runEnableImpeller: record.runEnableImpeller,
+ runIOSInterfaceType: record.runIOSInterfaceType,
+ runIsTest: record.runIsTest,
+ );
+ }
+
+ late final Future _sharedAnalyticsUsageValues = (() async {
String deviceType, deviceOsVersion;
bool isEmulator;
bool anyAndroidDevices = false;
@@ -512,19 +550,19 @@ class RunCommand extends RunCommandBase {
final BuildInfo buildInfo = await getBuildInfo();
final String modeName = buildInfo.modeName;
- return CustomDimensions(
- commandRunIsEmulator: isEmulator,
- commandRunTargetName: deviceType,
- commandRunTargetOsVersion: deviceOsVersion,
- commandRunModeName: modeName,
- commandRunProjectModule: FlutterProject.current().isModule,
- commandRunProjectHostLanguage: hostLanguage.join(','),
- commandRunAndroidEmbeddingVersion: androidEmbeddingVersion,
- commandRunEnableImpeller: enableImpeller.asBool,
- commandRunIOSInterfaceType: iOSInterfaceType,
- commandRunIsTest: targetFile.endsWith('_test.dart'),
+ return (
+ runIsEmulator: isEmulator,
+ runTargetName: deviceType,
+ runTargetOsVersion: deviceOsVersion,
+ runModeName: modeName,
+ runProjectModule: FlutterProject.current().isModule,
+ runProjectHostLanguage: hostLanguage.join(','),
+ runAndroidEmbeddingVersion: androidEmbeddingVersion,
+ runEnableImpeller: enableImpeller.asBool,
+ runIOSInterfaceType: iOSInterfaceType,
+ runIsTest: targetFile.endsWith('_test.dart'),
);
- }
+ })();
@override
bool get shouldRunPub {
@@ -801,3 +839,17 @@ class RunCommand extends RunCommandBase {
);
}
}
+
+/// Schema for the usage values to send for analytics reporting.
+typedef AnalyticsUsageValuesRecord = ({
+ String? runAndroidEmbeddingVersion,
+ bool? runEnableImpeller,
+ String? runIOSInterfaceType,
+ bool runIsEmulator,
+ bool runIsTest,
+ String runModeName,
+ String runProjectHostLanguage,
+ bool runProjectModule,
+ String runTargetName,
+ String runTargetOsVersion,
+});
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index e1509a3ddb3..1e47503ec8c 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -366,6 +366,9 @@ abstract class FlutterCommand extends Command {
return bundle.defaultMainPath;
}
+ /// Indicates if the currenet command running has a terminal attached.
+ bool get hasTerminal => globals.stdio.hasTerminal;
+
/// Path to the Dart's package config file.
///
/// This can be overridden by some of its subclasses.
@@ -1359,6 +1362,14 @@ abstract class FlutterCommand extends Command {
/// Additional usage values to be sent with the usage ping.
Future get usageValues async => const CustomDimensions();
+ /// Additional usage values to be sent with the usage ping for
+ /// package:unified_analytics.
+ ///
+ /// Implementations of [FlutterCommand] can override this getter in order
+ /// to add additional parameters in the [Event.commandUsageValues] constructor.
+ Future unifiedAnalyticsUsageValues(String commandPath) async =>
+ Event.commandUsageValues(workflow: commandPath, commandHasTerminal: hasTerminal);
+
/// Runs this command.
///
/// Rather than overriding this method, subclasses should override
@@ -1621,7 +1632,7 @@ abstract class FlutterCommand extends Command {
commandPath: commandPath,
result: commandResult.toString(),
maxRss: maxRss,
- commandHasTerminal: globals.stdio.hasTerminal,
+ commandHasTerminal: hasTerminal,
));
// Send timing.
@@ -1748,9 +1759,17 @@ Run 'flutter -h' (or 'flutter -h') for available flutter commands and
setupApplicationPackages();
if (commandPath != null) {
+ // Until the GA4 migration is complete, we will continue to send to the GA3 instance
+ // as well as GA4. Once migration is complete, we will only make a call for GA4 values
+ final List