From 30a9f99bc80bba85dc8f34a5a0dfb00c9ca654a7 Mon Sep 17 00:00:00 2001 From: Zachary Anderson Date: Thu, 21 Sep 2023 11:49:29 -0700 Subject: [PATCH] Send analytics on 'build ios' and 'build ipa' for plist impeller value (#135193) This analytics event only records the value of the plist entry on `build` commands. This will give an idea of the proportion of users who are disabling Impeller when shipping apps. --- .../lib/src/commands/build_ios.dart | 16 +++ .../lib/src/ios/plist_parser.dart | 1 + .../hermetic/build_ios_test.dart | 129 ++++++++++++++++++ .../hermetic/build_ipa_test.dart | 3 +- 4 files changed, 148 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index 211bf2f16c7c..117414e89f85 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -20,6 +20,7 @@ import '../globals.dart' as globals; import '../ios/application_package.dart'; import '../ios/mac.dart'; import '../ios/plist_parser.dart'; +import '../reporting/reporting.dart'; import '../runner/flutter_command.dart'; import 'build.dart'; @@ -724,6 +725,21 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand { if (result.output != null) { globals.printStatus('Built ${result.output}.'); + // When an app is successfully built, record to analytics whether Impeller + // is enabled or disabled. + final BuildableIOSApp app = await buildableIOSApp; + final String plistPath = app.project.infoPlist.path; + final bool? impellerEnabled = globals.plistParser.getValueFromFile( + plistPath, PlistParser.kFLTEnableImpellerKey, + ); + BuildEvent( + impellerEnabled == false + ? 'plist-impeller-disabled' + : 'plist-impeller-enabled', + type: 'ios', + flutterUsage: globals.flutterUsage, + ).send(); + return FlutterCommandResult.success(); } diff --git a/packages/flutter_tools/lib/src/ios/plist_parser.dart b/packages/flutter_tools/lib/src/ios/plist_parser.dart index b736f7cd6367..a48295745529 100644 --- a/packages/flutter_tools/lib/src/ios/plist_parser.dart +++ b/packages/flutter_tools/lib/src/ios/plist_parser.dart @@ -31,6 +31,7 @@ class PlistParser { static const String kCFBundleVersionKey = 'CFBundleVersion'; static const String kCFBundleDisplayNameKey = 'CFBundleDisplayName'; static const String kCFBundleNameKey = 'CFBundleName'; + static const String kFLTEnableImpellerKey = 'FLTEnableImpeller'; static const String kMinimumOSVersionKey = 'MinimumOSVersion'; static const String kNSPrincipalClassKey = 'NSPrincipalClass'; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart index a36960457498..8f0ae0d5b325 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart @@ -17,7 +17,9 @@ import 'package:flutter_tools/src/commands/build.dart'; import 'package:flutter_tools/src/commands/build_ios.dart'; import 'package:flutter_tools/src/ios/code_signing.dart'; import 'package:flutter_tools/src/ios/mac.dart'; +import 'package:flutter_tools/src/ios/plist_parser.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:test/fake.dart'; @@ -438,6 +440,133 @@ void main() { Usage: () => usage, XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); + + group('Analytics for impeller plist setting', () { + const String plistContents = ''' + + + + + FLTEnableImpeller + + + +'''; + const FakeCommand plutilCommand = FakeCommand( + command: [ + '/usr/bin/plutil', '-convert', 'xml1', '-o', '-', '/ios/Runner/Info.plist', + ], + stdout: plistContents, + ); + + testUsingContext('Sends an analytics event when Impeller is enabled', () async { + final BuildCommand command = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + createMinimalMockProjectFiles(); + + await createTestCommandRunner(command).run( + const ['build', 'ios', '--no-pub'] + ); + + expect(usage.events, contains( + const TestUsageEvent( + 'build', 'ios', + label:'plist-impeller-enabled', + parameters:CustomDimensions(), + ), + )); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.list([ + xattrCommand, + setUpFakeXcodeBuildHandler(onRun: () { + fileSystem.directory('build/ios/Release-iphoneos/Runner.app') + .createSync(recursive: true); + }), + setUpRsyncCommand(onRun: () => + fileSystem.file('build/ios/iphoneos/Runner.app/Frameworks/App.framework/App') + ..createSync(recursive: true) + ..writeAsBytesSync(List.generate(10000, (int index) => 0))), + ]), + Platform: () => macosPlatform, + FileSystemUtils: () => FileSystemUtils( + fileSystem: fileSystem, + platform: macosPlatform, + ), + Usage: () => usage, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), + }); + + testUsingContext('Sends an analytics event when Impeller is disabled', () async { + final BuildCommand command = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: fileSystem, + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + createMinimalMockProjectFiles(); + + fileSystem.file( + fileSystem.path.join('usr', 'bin', 'plutil'), + ).createSync(recursive: true); + + final File infoPlist = fileSystem.file(fileSystem.path.join( + 'ios', 'Runner', 'Info.plist', + ))..createSync(recursive: true); + + infoPlist.writeAsStringSync(plistContents); + + await createTestCommandRunner(command).run( + const ['build', 'ios', '--no-pub'] + ); + + expect(usage.events, contains( + const TestUsageEvent( + 'build', 'ios', + label:'plist-impeller-disabled', + parameters:CustomDimensions(), + ), + )); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.list([ + xattrCommand, + setUpFakeXcodeBuildHandler(onRun: () { + fileSystem.directory('build/ios/Release-iphoneos/Runner.app') + .createSync(recursive: true); + }), + setUpRsyncCommand(onRun: () => + fileSystem.file('build/ios/iphoneos/Runner.app/Frameworks/App.framework/App') + ..createSync(recursive: true) + ..writeAsBytesSync(List.generate(10000, (int index) => 0))), + ]), + Platform: () => macosPlatform, + FileSystemUtils: () => FileSystemUtils( + fileSystem: fileSystem, + platform: macosPlatform, + ), + Usage: () => usage, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), + FlutterProjectFactory: () => FlutterProjectFactory( + fileSystem: fileSystem, + logger: BufferLogger.test(), + ), + PlistParser: () => PlistParser( + fileSystem: fileSystem, + logger: BufferLogger.test(), + processManager: FakeProcessManager.list([ + plutilCommand, plutilCommand, plutilCommand, + ]), + ), + }); + }); + group('xcresults device', () { testUsingContext('Trace error if xcresult is empty.', () async { final BuildCommand command = BuildCommand( diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart index 11f65819e1c0..e92582191784 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart @@ -63,7 +63,8 @@ class FakePlistUtils extends Fake implements PlistParser { @override T? getValueFromFile(String plistFilePath, String key) { - return fileContents[plistFilePath]![key] as T?; + final Map? plistFile = fileContents[plistFilePath]; + return plistFile == null ? null : plistFile[key] as T?; } }