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 pairOfUsageValues = await Future.wait(>[ + usageValues, + unifiedAnalyticsUsageValues(commandPath), + ]); + Usage.command(commandPath, parameters: CustomDimensions( - commandHasTerminal: globals.stdio.hasTerminal, - ).merge(await usageValues)); + commandHasTerminal: hasTerminal, + ).merge(pairOfUsageValues[0] as CustomDimensions)); + analytics.send(pairOfUsageValues[1] as Event); } return runCommand(); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart index 8d4208f85c1..f62d92d9aad 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_tools/src/commands/assemble.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/context.dart'; @@ -24,6 +25,14 @@ void main() { Cache.disableLocking(); Cache.flutterRoot = ''; final StackTrace stackTrace = StackTrace.current; + late FakeAnalytics fakeAnalytics; + + setUp(() { + fakeAnalytics = getInitializedFakeAnalyticsInstance( + fs: MemoryFileSystem.test(), + fakeFlutterVersion: FakeFlutterVersion(), + ); + }); testUsingContext('flutter assemble can run a build', () async { final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand( @@ -85,6 +94,31 @@ void main() { FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), }); + testUsingContext('flutter assemble sends usage values correctly with platform', () async { + final AssembleCommand command = AssembleCommand( + buildSystem: TestBuildSystem.all(BuildResult(success: true))); + final CommandRunner commandRunner = createTestCommandRunner(command); + await commandRunner.run(['assemble', '-o Output', '-dTargetPlatform=darwin', '-dDarwinArchs=x86_64', 'debug_macos_bundle_flutter_assets']); + + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'assemble', + commandHasTerminal: false, + buildBundleTargetPlatform: 'darwin', + buildBundleIsModule: false, + ), + ), + ); + }, overrides: { + Cache: () => Cache.test(processManager: FakeProcessManager.any()), + FileSystem: () => MemoryFileSystem.test(), + ProcessManager: () => FakeProcessManager.any(), + FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), + Analytics: () => fakeAnalytics, + }); + testUsingContext('flutter assemble throws ToolExit if not provided with output', () async { final CommandRunner commandRunner = createTestCommandRunner(AssembleCommand( buildSystem: TestBuildSystem.all(BuildResult(success: true)), diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_aar_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_aar_test.dart index 27a37cc2214..d57f5b30923 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_aar_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_aar_test.dart @@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/build.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/context.dart'; @@ -26,6 +27,7 @@ void main() { late FakeProcessManager processManager; late Platform platform; late Cache cache; + late FakeAnalytics fakeAnalytics; setUpAll(() { Cache.disableLocking(); @@ -44,6 +46,10 @@ void main() { logger: logger, processManager: processManager, ); + fakeAnalytics = getInitializedFakeAnalyticsInstance( + fs: fs, + fakeFlutterVersion: FakeFlutterVersion(), + ); }); testUsingContext('will not build an AAR for a plugin', () async { @@ -126,9 +132,19 @@ flutter: await createTestCommandRunner(command).run(const ['build', 'aar', '--no-pub']); expect(processManager, hasNoRemainingExpectations); + expect( + fakeAnalytics.sentEvents, + contains(Event.commandUsageValues( + workflow: 'build/aar', + commandHasTerminal: false, + buildAarProjectType: 'module', + buildAarTargetPlatform: 'android-arm,android-arm64,android-x64', + )), + ); }, overrides: { FileSystem: () => fs, Platform: () => platform, ProcessManager: () => processManager, + Analytics: () => fakeAnalytics, }); } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index aeb871bd263..3032eafd9a0 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -33,6 +33,7 @@ import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/vmservice.dart'; import 'package:flutter_tools/src/web/compile.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart' as analytics; import 'package:vm_service/vm_service.dart'; import '../../src/common.dart'; @@ -192,6 +193,7 @@ void main() { late Artifacts artifacts; late TestUsage usage; late FakeAnsiTerminal fakeTerminal; + late analytics.FakeAnalytics fakeAnalytics; setUpAll(() { Cache.disableLocking(); @@ -211,6 +213,10 @@ void main() { libDir.createSync(); final File mainFile = libDir.childFile('main.dart'); mainFile.writeAsStringSync('void main() {}'); + fakeAnalytics = getInitializedFakeAnalyticsInstance( + fs: fs, + fakeFlutterVersion: FakeFlutterVersion(), + ); }); testUsingContext('exits with a user message when no supported devices attached', () async { @@ -478,6 +484,23 @@ void main() { 'cd58': 'false', }) ))); + expect( + fakeAnalytics.sentEvents, + contains( + analytics.Event.commandUsageValues( + workflow: 'run', + commandHasTerminal: globals.stdio.hasTerminal, + runIsEmulator: false, + runTargetName: 'ios', + runTargetOsVersion: 'iOS 13', + runModeName: 'debug', + runProjectModule: false, + runProjectHostLanguage: 'swift', + runIOSInterfaceType: 'usb', + runIsTest: false, + ), + ), + ); }, overrides: { AnsiTerminal: () => fakeTerminal, Artifacts: () => artifacts, @@ -487,6 +510,7 @@ void main() { ProcessManager: () => FakeProcessManager.any(), Stdio: () => FakeStdio(), Usage: () => usage, + analytics.Analytics: () => fakeAnalytics, }); testUsingContext('correctly reports tests to usage', () async { @@ -513,6 +537,23 @@ void main() { 'cd58': 'true', })), )); + expect( + fakeAnalytics.sentEvents, + contains( + analytics.Event.commandUsageValues( + workflow: 'run', + commandHasTerminal: globals.stdio.hasTerminal, + runIsEmulator: false, + runTargetName: 'ios', + runTargetOsVersion: 'iOS 13', + runModeName: 'debug', + runProjectModule: false, + runProjectHostLanguage: 'swift', + runIOSInterfaceType: 'usb', + runIsTest: true, + ), + ), + ); }, overrides: { AnsiTerminal: () => fakeTerminal, Artifacts: () => artifacts, @@ -522,6 +563,7 @@ void main() { ProcessManager: () => FakeProcessManager.any(), Stdio: () => FakeStdio(), Usage: () => usage, + analytics.Analytics: () => fakeAnalytics, }); group('--machine', () { diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart index 7e41fcfd7b5..6335e80e222 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:args/command_runner.dart'; +import 'package:file/memory.dart'; import 'package:flutter_tools/src/android/android_builder.dart'; import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_studio.dart'; @@ -16,11 +17,13 @@ import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/android_common.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fake_process_manager.dart'; +import '../../src/fakes.dart' show FakeFlutterVersion; import '../../src/test_flutter_command_runner.dart'; void main() { @@ -29,10 +32,15 @@ void main() { group('Usage', () { late Directory tempDir; late TestUsage testUsage; + late FakeAnalytics fakeAnalytics; setUp(() { testUsage = TestUsage(); tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.'); + fakeAnalytics = getInitializedFakeAnalyticsInstance( + fs: MemoryFileSystem.test(), + fakeFlutterVersion: FakeFlutterVersion(), + ); }); tearDown(() { @@ -46,8 +54,21 @@ void main() { expect((await command.usageValues).commandBuildApkTargetPlatform, 'android-arm,android-arm64,android-x64'); + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'apk', + commandHasTerminal: false, + buildApkTargetPlatform: 'android-arm,android-arm64,android-x64', + buildApkBuildMode: 'release', + buildApkSplitPerAbi: false, + ), + ), + ); }, overrides: { AndroidBuilder: () => FakeAndroidBuilder(), + Analytics: () => fakeAnalytics, }); testUsingContext('split per abi', () async { diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart index 2b6bceafc45..18de5ccc3cd 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:args/command_runner.dart'; +import 'package:file/memory.dart'; import 'package:flutter_tools/src/android/android_builder.dart'; import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/base/file_system.dart'; @@ -13,10 +14,12 @@ import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/android_common.dart'; import '../../src/common.dart'; import '../../src/context.dart'; +import '../../src/fakes.dart' show FakeFlutterVersion; import '../../src/test_flutter_command_runner.dart'; void main() { @@ -25,10 +28,15 @@ void main() { group('Usage', () { late Directory tempDir; late TestUsage testUsage; + late FakeAnalytics fakeAnalytics; setUp(() { tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.'); testUsage = TestUsage(); + fakeAnalytics = getInitializedFakeAnalyticsInstance( + fs: MemoryFileSystem.test(), + fakeFlutterVersion: FakeFlutterVersion(), + ); }); tearDown(() { @@ -42,8 +50,18 @@ void main() { expect((await command.usageValues).commandBuildAppBundleTargetPlatform, 'android-arm,android-arm64,android-x64'); + expect( + fakeAnalytics.sentEvents, + contains(Event.commandUsageValues( + workflow: 'appbundle', + commandHasTerminal: false, + buildAppBundleTargetPlatform: 'android-arm,android-arm64,android-x64', + buildAppBundleBuildMode: 'release', + )), + ); }, overrides: { AndroidBuilder: () => FakeAndroidBuilder(), + Analytics: () => fakeAnalytics, }); testUsingContext('build type', () async { diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart index 7ae9b6cfa99..9669f85f18d 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart @@ -17,6 +17,7 @@ import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:meta/meta.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/context.dart'; @@ -30,21 +31,26 @@ void main() { late FakeBundleBuilder fakeBundleBuilder; final FileSystemStyle fileSystemStyle = globals.fs.path.separator == '/' ? FileSystemStyle.posix : FileSystemStyle.windows; + late FakeAnalytics fakeAnalytics; + + MemoryFileSystem fsFactory() { + return MemoryFileSystem.test(style: fileSystemStyle); + } setUp(() { tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.'); fakeBundleBuilder = FakeBundleBuilder(); + fakeAnalytics = getInitializedFakeAnalyticsInstance( + fs: fsFactory(), + fakeFlutterVersion: FakeFlutterVersion(), + ); }); tearDown(() { tryToDelete(tempDir); }); - MemoryFileSystem fsFactory() { - return MemoryFileSystem.test(style: fileSystemStyle); - } - Future runCommandIn(String projectPath, { List? arguments }) async { final BuildBundleCommand command = BuildBundleCommand( logger: BufferLogger.test(), @@ -67,6 +73,19 @@ void main() { final BuildBundleCommand command = await runCommandIn(projectPath); expect((await command.usageValues).commandBuildBundleIsModule, true); + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'bundle', + commandHasTerminal: false, + buildBundleTargetPlatform: 'android-arm', + buildBundleIsModule: true, + ), + ), + ); + }, overrides: { + Analytics: () => fakeAnalytics, }); testUsingContext('bundle getUsage indicate that project is not a module', () async { @@ -76,6 +95,19 @@ void main() { final BuildBundleCommand command = await runCommandIn(projectPath); expect((await command.usageValues).commandBuildBundleIsModule, false); + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'bundle', + commandHasTerminal: false, + buildBundleTargetPlatform: 'android-arm', + buildBundleIsModule: false, + ), + ), + ); + }, overrides: { + Analytics: () => fakeAnalytics, }); testUsingContext('bundle getUsage indicate the target platform', () async { diff --git a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart index b52344542ad..985457083d1 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'dart:io' as io; import 'package:args/command_runner.dart'; +import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/android/gradle_utils.dart' show templateAndroidGradlePluginVersion, templateAndroidGradlePluginVersionForModule, templateDefaultGradleVersion; import 'package:flutter_tools/src/android/java.dart'; @@ -30,6 +31,7 @@ import 'package:flutter_tools/src/version.dart'; import 'package:process/process.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import 'package:uuid/uuid.dart'; import 'package:yaml/yaml.dart'; @@ -71,6 +73,7 @@ void main() { late FakeProcessManager fakeProcessManager; late BufferLogger logger; late FakeStdio mockStdio; + late FakeAnalytics fakeAnalytics; setUpAll(() async { Cache.disableLocking(); @@ -88,6 +91,10 @@ void main() { ); fakeProcessManager = FakeProcessManager.empty(); mockStdio = FakeStdio(); + fakeAnalytics = getInitializedFakeAnalyticsInstance( + fs: MemoryFileSystem.test(), + fakeFlutterVersion: fakeFlutterVersion, + ); }); tearDown(() { @@ -171,10 +178,24 @@ void main() { ], ); expect(logger.statusText, contains('In order to run your application, type:')); - // check that we're telling them about documentation + // Check that we're telling them about documentation expect(logger.statusText, contains('https://docs.flutter.dev/')); expect(logger.statusText, contains('https://api.flutter.dev/')); - // check that the tests run clean + + // Check for usage values sent in analytics + expect( + fakeAnalytics.sentEvents, + contains( + Event.commandUsageValues( + workflow: 'create', + commandHasTerminal: false, + createAndroidLanguage: 'java', + createIosLanguage: 'objc', + ), + ), + ); + + // Check that the tests run clean return _runFlutterTest(projectDir); }, overrides: { Pub: () => Pub.test( @@ -187,6 +208,7 @@ void main() { stdio: mockStdio, ), Logger: () => logger, + Analytics: () => fakeAnalytics, }); testUsingContext('can create a skeleton (list/detail) app', () async { diff --git a/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart b/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart index 8b197e6a03c..de1505d0917 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart @@ -355,6 +355,11 @@ flutter: final PackagesGetCommand getCommand = command.subcommands['get']! as PackagesGetCommand; expect((await getCommand.usageValues).commandPackagesNumberPlugins, 0); + expect( + (await getCommand.unifiedAnalyticsUsageValues('pub/get')) + .eventData['packagesNumberPlugins'], + 0, + ); }, overrides: { Stdio: () => mockStdio, Pub: () => Pub.test( @@ -380,6 +385,11 @@ flutter: // A plugin example depends on the plugin itself, and integration_test. expect((await getCommand.usageValues).commandPackagesNumberPlugins, 2); + expect( + (await getCommand.unifiedAnalyticsUsageValues('pub/get')) + .eventData['packagesNumberPlugins'], + 2, + ); }, overrides: { Stdio: () => mockStdio, Pub: () => Pub.test( @@ -402,6 +412,11 @@ flutter: final PackagesGetCommand getCommand = command.subcommands['get']! as PackagesGetCommand; expect((await getCommand.usageValues).commandPackagesProjectModule, false); + expect( + (await getCommand.unifiedAnalyticsUsageValues('pub/get')) + .eventData['packagesProjectModule'], + false, + ); }, overrides: { Stdio: () => mockStdio, Pub: () => Pub.test( @@ -424,6 +439,11 @@ flutter: final PackagesGetCommand getCommand = command.subcommands['get']! as PackagesGetCommand; expect((await getCommand.usageValues).commandPackagesProjectModule, true); + expect( + (await getCommand.unifiedAnalyticsUsageValues('pub/get')) + .eventData['packagesProjectModule'], + true, + ); }, overrides: { Stdio: () => mockStdio, Pub: () => Pub.test( @@ -455,6 +475,11 @@ flutter: final PackagesGetCommand getCommand = command.subcommands['get']! as PackagesGetCommand; expect((await getCommand.usageValues).commandPackagesAndroidEmbeddingVersion, 'v1'); + expect( + (await getCommand.unifiedAnalyticsUsageValues('pub/get')) + .eventData['packagesAndroidEmbeddingVersion'], + 'v1', + ); }, overrides: { Stdio: () => mockStdio, Pub: () => Pub.test( @@ -477,6 +502,11 @@ flutter: final PackagesGetCommand getCommand = command.subcommands['get']! as PackagesGetCommand; expect((await getCommand.usageValues).commandPackagesAndroidEmbeddingVersion, 'v2'); + expect( + (await getCommand.unifiedAnalyticsUsageValues('pub/get')) + .eventData['packagesAndroidEmbeddingVersion'], + 'v2', + ); }, overrides: { Stdio: () => mockStdio, Pub: () => Pub.test( diff --git a/packages/flutter_tools/test/src/common.dart b/packages/flutter_tools/test/src/common.dart index b6143a9be94..63b40e6db5e 100644 --- a/packages/flutter_tools/test/src/common.dart +++ b/packages/flutter_tools/test/src/common.dart @@ -373,7 +373,7 @@ bool analyticsTimingEventExists({ }; for (final Event e in sentEvents) { - final Map eventData = e.eventData; + final Map eventData = {...e.eventData}; eventData.remove('elapsedMilliseconds'); if (const DeepCollectionEquality().equals(lookup, eventData)) {