diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart index 1f9a212c076..a8728c20e4d 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(), @@ -265,7 +267,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)) { @@ -296,7 +298,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 445620c7d1e..1096423bd47 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'; @@ -75,14 +77,15 @@ class BuildAarCommand extends BuildSubCommand { DevelopmentArtifact.androidGenSnapshot, }; + late final FlutterProject project = _getProject(); + @override Future get usageValues async { - final FlutterProject flutterProject = _getProject(); String projectType; - if (flutterProject.manifest.isModule) { + if (project.manifest.isModule) { projectType = 'module'; - } else if (flutterProject.manifest.isPlugin) { + } else if (project.manifest.isPlugin) { projectType = 'plugin'; } else { projectType = 'app'; @@ -94,6 +97,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 434ab774323..534498d8c7b 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 97d70b06728..da9017a353b 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 824077cd8c9..7f57d8858e9 100644 --- a/packages/flutter_tools/lib/src/commands/packages.dart +++ b/packages/flutter_tools/lib/src/commands/packages.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:args/args.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../base/common.dart'; import '../base/os.dart'; @@ -372,6 +373,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 @@ -388,7 +407,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; @@ -397,7 +416,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 bb6538fcf37..d8d184b85ca 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -7,6 +7,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'; @@ -431,6 +432,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; @@ -496,19 +534,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 { @@ -783,3 +821,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/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index a5a4f92c250..1c412832016 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -61,6 +61,7 @@ import 'persistent_tool_state.dart'; import 'reporting/crash_reporting.dart'; import 'reporting/first_run.dart'; import 'reporting/reporting.dart'; +import 'reporting/unified_analytics.dart'; import 'resident_runner.dart'; import 'run_hot.dart'; import 'runner/local_engine.dart'; @@ -88,11 +89,10 @@ Future runInContext( body: runnerWrapper, overrides: overrides, fallbacks: { - Analytics: () => Analytics( - tool: DashTool.flutterTool, - flutterChannel: globals.flutterVersion.channel, - flutterVersion: globals.flutterVersion.frameworkVersion, - dartVersion: globals.flutterVersion.dartSdkVersion, + Analytics: () => getAnalytics( + runningOnBot: runningOnBot, + flutterVersion: globals.flutterVersion, + environment: globals.platform.environment, ), AndroidBuilder: () => AndroidGradleBuilder( java: globals.java, diff --git a/packages/flutter_tools/lib/src/reporting/unified_analytics.dart b/packages/flutter_tools/lib/src/reporting/unified_analytics.dart new file mode 100644 index 00000000000..767397dbb7a --- /dev/null +++ b/packages/flutter_tools/lib/src/reporting/unified_analytics.dart @@ -0,0 +1,52 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// 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 '../version.dart'; + +/// This function is called from within the context runner to perform +/// checks that are necessary for determining if a no-op version of +/// [Analytics] gets returned. +/// +/// When [enableAsserts] is set to `true`, various assert statements +/// will be enabled to ensure usage of this class is within GA4 limitations. +/// +/// For testing purposes, pass in a [FakeAnalytics] instance initialized with +/// an in-memory [FileSystem] to prevent writing to disk. +Analytics getAnalytics({ + required bool runningOnBot, + required FlutterVersion flutterVersion, + required Map environment, + bool enableAsserts = false, + FakeAnalytics? analyticsOverride, +}) { + final String version = flutterVersion.getVersionString(redactUnknownBranches: true); + final bool suppressEnvFlag = environment['FLUTTER_SUPPRESS_ANALYTICS']?.toLowerCase() == 'true'; + + if (// Ignore local user branches. + version.startsWith('[user-branch]') || + // Many CI systems don't do a full git checkout. + version.endsWith('/unknown') || + // Ignore bots. + runningOnBot || + // Ignore when suppressed by FLUTTER_SUPPRESS_ANALYTICS. + suppressEnvFlag) { + return NoOpAnalytics(); + } + + // Providing an override of the [Analytics] instance is preferred when + // running tests for this function to prevent writing to the filesystem + if (analyticsOverride != null) { + return analyticsOverride; + } + + return Analytics( + tool: DashTool.flutterTool, + flutterChannel: flutterVersion.channel, + flutterVersion: flutterVersion.frameworkVersion, + dartVersion: flutterVersion.dartSdkVersion, + enableAsserts: enableAsserts, + ); +} diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 1d99c9a29e4..19a1c66aef8 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -7,6 +7,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:meta/meta.dart'; import 'package:package_config/package_config_types.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../application_package.dart'; import '../base/common.dart'; @@ -213,6 +214,11 @@ abstract class FlutterCommand extends Command { bool _excludeDebug = false; bool _excludeRelease = false; + /// Grabs the [Analytics] instance from the global context. It is defined + /// at the [FlutterCommand] level to enable any classes that extend it to + /// easily reference it or overwrite as necessary. + Analytics get analytics => globals.analytics; + void requiresPubspecYaml() { _requiresPubspecYaml = true; } @@ -327,6 +333,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. @@ -1321,6 +1330,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 @@ -1654,9 +1671,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/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 027601888c8..fa69407acee 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -48,7 +48,7 @@ dependencies: http_multi_server: 3.2.1 convert: 3.1.1 async: 2.11.0 - unified_analytics: 4.0.0 + unified_analytics: 5.8.0 cli_config: 0.1.1 graphs: 2.3.1 @@ -112,4 +112,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: 05d8 +# PUBSPEC CHECKSUM: e7e1 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 4e302df9b80..e6ac57507d3 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/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index 2a5a644a85c..94c5be88b71 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_aar_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart index 8fdbac1efc4..34bd5039bd1 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart @@ -69,17 +69,6 @@ void main() { AndroidBuilder: () => FakeAndroidBuilder(), }); - testUsingContext('indicate that project is a plugin', () async { - final String projectPath = await createProject(tempDir, - arguments: ['--no-pub', '--template=plugin', '--project-name=aar_test']); - - final BuildAarCommand command = await runCommandIn(projectPath); - expect((await command.usageValues).commandBuildAarProjectType, 'plugin'); - - }, overrides: { - AndroidBuilder: () => FakeAndroidBuilder(), - }); - testUsingContext('indicate the target platform', () async { final String projectPath = await createProject(tempDir, arguments: ['--no-pub', '--template=module']); @@ -128,7 +117,7 @@ void main() { testUsingContext('defaults', () async { final String projectPath = await createProject(tempDir, - arguments: ['--no-pub']); + arguments: ['--no-pub', '--template=module']); await runCommandIn(projectPath); expect(fakeAndroidBuilder.buildNumber, '1.0'); @@ -158,7 +147,7 @@ void main() { testUsingContext('parses flags', () async { final String projectPath = await createProject(tempDir, - arguments: ['--no-pub']); + arguments: ['--no-pub', '--template=module']); await runCommandIn( projectPath, arguments: [ 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 8e82ff48702..521374c7a94 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 0bd3eaa08ad..04910b88d91 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 f20ec4999dd..9df5ec7ebf8 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 @@ -20,6 +20,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'; @@ -33,21 +34,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(), @@ -70,6 +76,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 { @@ -79,6 +98,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 0dee6582241..bf6739b1998 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 df145350049..ffe3462fd83 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart @@ -339,6 +339,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( @@ -364,6 +369,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( @@ -386,6 +396,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( @@ -408,6 +423,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( @@ -439,6 +459,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( @@ -461,6 +486,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/general.shard/unified_analytics_test.dart b/packages/flutter_tools/test/general.shard/unified_analytics_test.dart new file mode 100644 index 00000000000..f52a977622a --- /dev/null +++ b/packages/flutter_tools/test/general.shard/unified_analytics_test.dart @@ -0,0 +1,139 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/reporting/unified_analytics.dart'; +import 'package:unified_analytics/src/enums.dart'; +import 'package:unified_analytics/unified_analytics.dart'; + +import '../src/common.dart'; +import '../src/fakes.dart'; + +void main() { + const String userBranch = 'abc123'; + const String homeDirectoryName = 'home'; + const DashTool tool = DashTool.flutterTool; + + late FileSystem fs; + late Directory home; + late FakeAnalytics analyticsOverride; + + setUp(() { + fs = MemoryFileSystem.test(); + home = fs.directory(homeDirectoryName); + + // Prepare the tests by "onboarding" the tool into the package + // by invoking the [clientShowedMessage] method for the provided + // [tool] + final FakeAnalytics initialAnalytics = FakeAnalytics( + tool: tool, + homeDirectory: home, + dartVersion: '3.0.0', + platform: DevicePlatform.macos, + fs: fs, + surveyHandler: SurveyHandler( + homeDirectory: home, + fs: fs, + ), + ); + initialAnalytics.clientShowedMessage(); + + analyticsOverride = FakeAnalytics( + tool: tool, + homeDirectory: home, + dartVersion: '3.0.0', + platform: DevicePlatform.macos, + fs: fs, + surveyHandler: SurveyHandler( + homeDirectory: home, + fs: fs, + ), + ); + }); + + group('Unit testing getAnalytics', () { + testWithoutContext('Successfully creates the instance for standard branch', () { + final Analytics analytics = getAnalytics( + runningOnBot: false, + flutterVersion: FakeFlutterVersion(), + environment: const {}, + analyticsOverride: analyticsOverride, + ); + + expect(analytics.clientId, isNot(NoOpAnalytics.staticClientId), + reason: 'The CLIENT ID should be a randomly generated id'); + expect(analytics, isNot(isA())); + }); + + testWithoutContext('NoOp instance for user branch', () { + final Analytics analytics = getAnalytics( + runningOnBot: false, + flutterVersion: FakeFlutterVersion( + branch: userBranch, + frameworkRevision: '3.14.0-14.0.pre.370', + ), + environment: const {}, + analyticsOverride: analyticsOverride, + ); + + expect( + analytics.clientId, + NoOpAnalytics.staticClientId, + reason: 'The client ID should match the NoOp client id', + ); + expect(analytics, isA()); + }); + + testWithoutContext('NoOp instance for unknown branch', () { + final Analytics analytics = getAnalytics( + runningOnBot: false, + flutterVersion: FakeFlutterVersion( + frameworkRevision: 'unknown', + ), + environment: const {}, + analyticsOverride: analyticsOverride, + ); + + expect( + analytics.clientId, + NoOpAnalytics.staticClientId, + reason: 'The client ID should match the NoOp client id', + ); + expect(analytics, isA()); + }); + + testWithoutContext('NoOp instance when running on bots', () { + final Analytics analytics = getAnalytics( + runningOnBot: true, + flutterVersion: FakeFlutterVersion(), + environment: const {}, + analyticsOverride: analyticsOverride, + ); + + expect( + analytics.clientId, + NoOpAnalytics.staticClientId, + reason: 'The client ID should match the NoOp client id', + ); + expect(analytics, isA()); + }); + + testWithoutContext('NoOp instance when suppressing via env variable', () { + final Analytics analytics = getAnalytics( + runningOnBot: true, + flutterVersion: FakeFlutterVersion(), + environment: const {'FLUTTER_SUPPRESS_ANALYTICS': 'true'}, + analyticsOverride: analyticsOverride, + ); + + expect( + analytics.clientId, + NoOpAnalytics.staticClientId, + reason: 'The client ID should match the NoOp client id', + ); + expect(analytics, isA()); + }); + }); +} diff --git a/packages/flutter_tools/test/src/common.dart b/packages/flutter_tools/test/src/common.dart index ca005f21e52..fca13fa2e5c 100644 --- a/packages/flutter_tools/test/src/common.dart +++ b/packages/flutter_tools/test/src/common.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:args/command_runner.dart'; +import 'package:collection/collection.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/context.dart'; @@ -16,6 +17,10 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; // flutter_ignore: package_path_import import 'package:test/test.dart' as test_package show test; import 'package:test/test.dart' hide test; +import 'package:unified_analytics/src/enums.dart'; +import 'package:unified_analytics/unified_analytics.dart'; + +import 'fakes.dart'; export 'package:path/path.dart' show Context; // flutter_ignore: package_path_import export 'package:test/test.dart' hide isInstanceOf, test; @@ -305,3 +310,75 @@ class FileExceptionHandler { throw exception; } } + +/// This method is required to fetch an instance of [FakeAnalytics] +/// because there is initialization logic that is required. An initial +/// instance will first be created and will let package:unified_analytics +/// know that the consent message has been shown. After confirming on the first +/// instance, then a second instance will be generated and returned. This second +/// instance will be cleared to send events. +FakeAnalytics getInitializedFakeAnalyticsInstance({ + required FileSystem fs, + required FakeFlutterVersion fakeFlutterVersion, + String? clientIde, +}) { + final Directory homeDirectory = fs.directory('/'); + final FakeAnalytics initialAnalytics = FakeAnalytics( + tool: DashTool.flutterTool, + homeDirectory: homeDirectory, + dartVersion: fakeFlutterVersion.dartSdkVersion, + platform: DevicePlatform.linux, + fs: fs, + surveyHandler: SurveyHandler(homeDirectory: homeDirectory, fs: fs), + flutterChannel: fakeFlutterVersion.channel, + flutterVersion: fakeFlutterVersion.getVersionString(), + ); + initialAnalytics.clientShowedMessage(); + + return FakeAnalytics( + tool: DashTool.flutterTool, + homeDirectory: homeDirectory, + dartVersion: fakeFlutterVersion.dartSdkVersion, + platform: DevicePlatform.linux, + fs: fs, + surveyHandler: SurveyHandler(homeDirectory: homeDirectory, fs: fs), + flutterChannel: fakeFlutterVersion.channel, + flutterVersion: fakeFlutterVersion.getVersionString(), + clientIde: clientIde, + ); +} + +/// Returns "true" if the timing event searched for exists in [sentEvents]. +/// +/// This utility function allows us to check for an instance of +/// [Event.timing] within a [FakeAnalytics] instance. Normally, we can +/// use the equality operator for [Event] to check if the event exists, but +/// we are unable to do so for the timing event because the elapsed time +/// is variable so we cannot predict what that value will be in tests. +/// +/// This function allows us to check for the other keys that have +/// string values by removing the `elapsedMilliseconds` from the +/// [Event.eventData] map and checking for a match. +bool analyticsTimingEventExists({ + required List sentEvents, + required String workflow, + required String variableName, + String? label, +}) { + final Map lookup = { + 'workflow': workflow, + 'variableName': variableName, + if (label != null) 'label': label, + }; + + for (final Event e in sentEvents) { + final Map eventData = {...e.eventData}; + eventData.remove('elapsedMilliseconds'); + + if (const DeepCollectionEquality().equals(lookup, eventData)) { + return true; + } + } + + return false; +} diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index 7bea7532c3f..f0f4fd0f7d1 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -35,6 +35,7 @@ import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/version.dart'; import 'package:meta/meta.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import 'common.dart'; import 'fake_http_client.dart'; @@ -120,6 +121,7 @@ void testUsingContext( Pub: () => ThrowingPub(), // prevent accidentally using pub. CrashReporter: () => const NoopCrashReporter(), TemplateRenderer: () => const MustacheTemplateRenderer(), + Analytics: () => NoOpAnalytics(), }, body: () { return runZonedGuarded>(() { diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index 9f3bb754271..bca86131dc4 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -438,7 +438,7 @@ class FakeFlutterVersion implements FlutterVersion { @override String getVersionString({bool redactUnknownBranches = false}) { - return 'v0.0.0'; + return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkRevision'; } @override