[CP] Migrate command usage values (#139383) (#141019)

Fixes:
- https://github.com/flutter/flutter/issues/141017
This commit is contained in:
Elias Yishak 2024-01-09 15:21:10 -05:00 committed by GitHub
parent 4ec0689fa1
commit 019fc5d65f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 748 additions and 64 deletions

View File

@ -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<CustomDimensions> get usageValues async {
final FlutterProject flutterProject = FlutterProject.current();
try {
return CustomDimensions(
Future<CustomDimensions> get usageValues async => CustomDimensions(
commandBuildBundleTargetPlatform: _environment.defines[kTargetPlatform],
commandBuildBundleIsModule: flutterProject.isModule,
commandBuildBundleIsModule: _flutterProject.isModule,
);
@override
Future<Event> unifiedAnalyticsUsageValues(String commandPath) async => Event.commandUsageValues(
workflow: commandPath,
commandHasTerminal: hasTerminal,
buildBundleTargetPlatform: _environment.defines[kTargetPlatform],
buildBundleIsModule: _flutterProject.isModule,
);
} on Exception {
// We've failed to send usage.
}
return const CustomDimensions();
}
@override
Future<Set<DevelopmentArtifact>> 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()) {

View File

@ -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<CustomDimensions> 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<Event> 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'

View File

@ -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<Event> 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<FlutterCommandResult> runCommand() async {
if (globals.androidSdk == null) {

View File

@ -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<Event> 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<FlutterCommandResult> runCommand() async {
if (globals.androidSdk == null) {

View File

@ -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<Event> 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<void> validateCommand() async {
if (boolArg('tree-shake-icons')) {

View File

@ -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<Event> 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<HttpClientFactory>(),

View File

@ -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<List<Plugin>> _pluginsFound = (() async {
final FlutterProject? rootProject = _rootProject;
if (rootProject == null) {
return <Plugin>[];
}
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<Plugin> plugins = await findPlugins(rootProject, throwOnError: false);
final List<Plugin> 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<Event> 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<Plugin> plugins = await _pluginsFound;
numberPlugins = plugins.length;
} else {
numberPlugins = 0;
}
return Event.commandUsageValues(
workflow: commandPath,
commandHasTerminal: hasTerminal,
packagesNumberPlugins: numberPlugins,
packagesProjectModule: rootProject.isModule,
packagesAndroidEmbeddingVersion: _androidEmbeddingVersion,
);
}
}

View File

@ -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<CustomDimensions> 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<analytics.Event> 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<AnalyticsUsageValuesRecord> _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,
});

View File

@ -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<T> runInContext<T>(
body: runnerWrapper,
overrides: overrides,
fallbacks: <Type, Generator>{
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,

View File

@ -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<String, String> 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,
);
}

View File

@ -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<void> {
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<void> {
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<void> {
/// Additional usage values to be sent with the usage ping.
Future<CustomDimensions> 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<Event> 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 <command> -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<Object> pairOfUsageValues = await Future.wait<Object>(<Future<Object>>[
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();

View File

@ -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

View File

@ -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<void> 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<void> commandRunner = createTestCommandRunner(command);
await commandRunner.run(<String>['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: <Type, Generator>{
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<void> commandRunner = createTestCommandRunner(AssembleCommand(
buildSystem: TestBuildSystem.all(BuildResult(success: true)),

View File

@ -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: <Type, Generator>{
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: <Type, Generator>{
AnsiTerminal: () => fakeTerminal,
Artifacts: () => artifacts,
@ -522,6 +563,7 @@ void main() {
ProcessManager: () => FakeProcessManager.any(),
Stdio: () => FakeStdio(),
Usage: () => usage,
analytics.Analytics: () => fakeAnalytics,
});
group('--machine', () {

View File

@ -69,17 +69,6 @@ void main() {
AndroidBuilder: () => FakeAndroidBuilder(),
});
testUsingContext('indicate that project is a plugin', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=plugin', '--project-name=aar_test']);
final BuildAarCommand command = await runCommandIn(projectPath);
expect((await command.usageValues).commandBuildAarProjectType, 'plugin');
}, overrides: <Type, Generator>{
AndroidBuilder: () => FakeAndroidBuilder(),
});
testUsingContext('indicate the target platform', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=module']);
@ -128,7 +117,7 @@ void main() {
testUsingContext('defaults', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub']);
arguments: <String>['--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: <String>['--no-pub']);
arguments: <String>['--no-pub', '--template=module']);
await runCommandIn(
projectPath,
arguments: <String>[

View File

@ -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: <Type, Generator>{
AndroidBuilder: () => FakeAndroidBuilder(),
Analytics: () => fakeAnalytics,
});
testUsingContext('split per abi', () async {

View File

@ -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: <Type, Generator>{
AndroidBuilder: () => FakeAndroidBuilder(),
Analytics: () => fakeAnalytics,
});
testUsingContext('build type', () async {

View File

@ -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<BuildBundleCommand> runCommandIn(String projectPath, { List<String>? 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: <Type, Generator>{
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: <Type, Generator>{
Analytics: () => fakeAnalytics,
});
testUsingContext('bundle getUsage indicate the target platform', () async {

View File

@ -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: <Type, Generator>{
Pub: () => Pub.test(
@ -187,6 +208,7 @@ void main() {
stdio: mockStdio,
),
Logger: () => logger,
Analytics: () => fakeAnalytics,
});
testUsingContext('can create a skeleton (list/detail) app', () async {

View File

@ -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: <Type, Generator>{
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: <Type, Generator>{
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: <Type, Generator>{
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: <Type, Generator>{
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: <Type, Generator>{
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: <Type, Generator>{
Stdio: () => mockStdio,
Pub: () => Pub.test(

View File

@ -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 <String, String>{},
analyticsOverride: analyticsOverride,
);
expect(analytics.clientId, isNot(NoOpAnalytics.staticClientId),
reason: 'The CLIENT ID should be a randomly generated id');
expect(analytics, isNot(isA<NoOpAnalytics>()));
});
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 <String, String>{},
analyticsOverride: analyticsOverride,
);
expect(
analytics.clientId,
NoOpAnalytics.staticClientId,
reason: 'The client ID should match the NoOp client id',
);
expect(analytics, isA<NoOpAnalytics>());
});
testWithoutContext('NoOp instance for unknown branch', () {
final Analytics analytics = getAnalytics(
runningOnBot: false,
flutterVersion: FakeFlutterVersion(
frameworkRevision: 'unknown',
),
environment: const <String, String>{},
analyticsOverride: analyticsOverride,
);
expect(
analytics.clientId,
NoOpAnalytics.staticClientId,
reason: 'The client ID should match the NoOp client id',
);
expect(analytics, isA<NoOpAnalytics>());
});
testWithoutContext('NoOp instance when running on bots', () {
final Analytics analytics = getAnalytics(
runningOnBot: true,
flutterVersion: FakeFlutterVersion(),
environment: const <String, String>{},
analyticsOverride: analyticsOverride,
);
expect(
analytics.clientId,
NoOpAnalytics.staticClientId,
reason: 'The client ID should match the NoOp client id',
);
expect(analytics, isA<NoOpAnalytics>());
});
testWithoutContext('NoOp instance when suppressing via env variable', () {
final Analytics analytics = getAnalytics(
runningOnBot: true,
flutterVersion: FakeFlutterVersion(),
environment: const <String, String>{'FLUTTER_SUPPRESS_ANALYTICS': 'true'},
analyticsOverride: analyticsOverride,
);
expect(
analytics.clientId,
NoOpAnalytics.staticClientId,
reason: 'The client ID should match the NoOp client id',
);
expect(analytics, isA<NoOpAnalytics>());
});
});
}

View File

@ -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<Event> sentEvents,
required String workflow,
required String variableName,
String? label,
}) {
final Map<String, String> lookup = <String, String>{
'workflow': workflow,
'variableName': variableName,
if (label != null) 'label': label,
};
for (final Event e in sentEvents) {
final Map<String, Object?> eventData = <String, Object?>{...e.eventData};
eventData.remove('elapsedMilliseconds');
if (const DeepCollectionEquality().equals(lookup, eventData)) {
return true;
}
}
return false;
}

View File

@ -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<Future<dynamic>>(() {

View File

@ -438,7 +438,7 @@ class FakeFlutterVersion implements FlutterVersion {
@override
String getVersionString({bool redactUnknownBranches = false}) {
return 'v0.0.0';
return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkRevision';
}
@override