Skip to content

Commit faec4ca

Browse files
author
Emmanuel Garcia
authored
Instrument add to app flows (#33297)
1 parent 841286d commit faec4ca

9 files changed

Lines changed: 310 additions & 56 deletions

File tree

packages/flutter_tools/lib/src/commands/attach.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import '../resident_runner.dart';
2626
import '../run_cold.dart';
2727
import '../run_hot.dart';
2828
import '../runner/flutter_command.dart';
29+
import '../usage.dart';
2930

3031
/// A Flutter-command that attaches to applications that have been launched
3132
/// without `flutter run`.
@@ -315,8 +316,12 @@ class AttachCommand extends FlutterCommand {
315316
result = await runner.attach();
316317
assert(result != null);
317318
}
318-
if (result != 0)
319+
if (result == 0) {
320+
flutterUsage.sendEvent('attach', 'success');
321+
} else {
322+
flutterUsage.sendEvent('attach', 'failure');
319323
throwToolExit(null, exitCode: result);
324+
}
320325
} finally {
321326
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
322327
for (ForwardedPort port in ports) {

packages/flutter_tools/lib/src/commands/create.dart

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import '../globals.dart';
2424
import '../project.dart';
2525
import '../runner/flutter_command.dart';
2626
import '../template.dart';
27+
import '../usage.dart';
2728
import '../version.dart';
2829

2930
enum _ProjectType {
@@ -148,6 +149,15 @@ class CreateCommand extends FlutterCommand {
148149
@override
149150
String get invocation => '${runner.executableName} $name <output directory>';
150151

152+
@override
153+
Future<Map<String, String>> get usageValues async {
154+
return <String, String>{
155+
kCommandCreateProjectType: argResults['template'],
156+
kCommandCreateAndroidLanguage: argResults['android-language'],
157+
kCommandCreateIosLanguage: argResults['ios-language'],
158+
};
159+
}
160+
151161
// If it has a .metadata file with the project_type in it, use that.
152162
// If it has an android dir and an android/app dir, it's a legacy app
153163
// If it has an ios dir and an ios/Flutter dir, it's a legacy app
@@ -228,6 +238,36 @@ class CreateCommand extends FlutterCommand {
228238
}
229239
}
230240

241+
_ProjectType _getProjectType(Directory projectDir) {
242+
_ProjectType template;
243+
_ProjectType detectedProjectType;
244+
final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
245+
if (argResults['template'] != null) {
246+
template = _stringToProjectType(argResults['template']);
247+
} else {
248+
// If the project directory exists and isn't empty, then try to determine the template
249+
// type from the project directory.
250+
if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) {
251+
detectedProjectType = _determineTemplateType(projectDir);
252+
if (detectedProjectType == null && metadataExists) {
253+
// We can only be definitive that this is the wrong type if the .metadata file
254+
// exists and contains a type that we don't understand, or doesn't contain a type.
255+
throwToolExit('Sorry, unable to detect the type of project to recreate. '
256+
'Try creating a fresh project and migrating your existing code to '
257+
'the new project manually.');
258+
}
259+
}
260+
}
261+
template ??= detectedProjectType ?? _ProjectType.app;
262+
if (detectedProjectType != null && template != detectedProjectType && metadataExists) {
263+
// We can only be definitive that this is the wrong type if the .metadata file
264+
// exists and contains a type that doesn't match.
265+
throwToolExit("The requested template type '${getEnumName(template)}' doesn't match the "
266+
"existing template type of '${getEnumName(detectedProjectType)}'.");
267+
}
268+
return template;
269+
}
270+
231271
@override
232272
Future<FlutterCommandResult> runCommand() async {
233273
if (argResults['list-samples'] != null) {
@@ -283,31 +323,7 @@ class CreateCommand extends FlutterCommand {
283323
sampleCode = await _fetchSampleFromServer(argResults['sample']);
284324
}
285325

286-
_ProjectType template;
287-
_ProjectType detectedProjectType;
288-
final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
289-
if (argResults['template'] != null) {
290-
template = _stringToProjectType(argResults['template']);
291-
} else {
292-
if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) {
293-
detectedProjectType = _determineTemplateType(projectDir);
294-
if (detectedProjectType == null && metadataExists) {
295-
// We can only be definitive that this is the wrong type if the .metadata file
296-
// exists and contains a type that we don't understand, or doesn't contain a type.
297-
throwToolExit('Sorry, unable to detect the type of project to recreate. '
298-
'Try creating a fresh project and migrating your existing code to '
299-
'the new project manually.');
300-
}
301-
}
302-
}
303-
template ??= detectedProjectType ?? _ProjectType.app;
304-
if (detectedProjectType != null && template != detectedProjectType && metadataExists) {
305-
// We can only be definitive that this is the wrong type if the .metadata file
306-
// exists and contains a type that doesn't match.
307-
throwToolExit("The requested template type '${getEnumName(template)}' doesn't match the "
308-
"existing template type of '${getEnumName(detectedProjectType)}'.");
309-
}
310-
326+
final _ProjectType template = _getProjectType(projectDir);
311327
final bool generateModule = template == _ProjectType.module;
312328
final bool generatePlugin = template == _ProjectType.plugin;
313329
final bool generatePackage = template == _ProjectType.package;

packages/flutter_tools/lib/src/commands/packages.dart

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import '../base/os.dart';
99
import '../dart/pub.dart';
1010
import '../project.dart';
1111
import '../runner/flutter_command.dart';
12+
import '../usage.dart';
1213

1314
class PackagesCommand extends FlutterCommand {
1415
PackagesCommand() {
@@ -68,27 +69,57 @@ class PackagesGetCommand extends FlutterCommand {
6869
return '${runner.executableName} pub $name [<target directory>]';
6970
}
7071

71-
Future<void> _runPubGet (String directory) async {
72-
await pubGet(context: PubContext.pubGet,
73-
directory: directory,
74-
upgrade: upgrade ,
75-
offline: argResults['offline'],
76-
checkLastModified: false,
77-
);
72+
@override
73+
Future<Map<String, String>> get usageValues async {
74+
final Map<String, String> usageValues = <String, String>{};
75+
final String workingDirectory = argResults.rest.length == 1 ? argResults.rest[0] : null;
76+
final String target = findProjectRoot(workingDirectory);
77+
if (target == null) {
78+
return usageValues;
79+
}
80+
final FlutterProject rootProject = FlutterProject.fromPath(target);
81+
final bool hasPlugins = await rootProject.flutterPluginsFile.exists();
82+
if (hasPlugins) {
83+
final int numberOfPlugins = (await rootProject.flutterPluginsFile.readAsLines()).length;
84+
usageValues[kCommandPackagesNumberPlugins] = '$numberOfPlugins';
85+
} else {
86+
usageValues[kCommandPackagesNumberPlugins] = '0';
87+
}
88+
usageValues[kCommandPackagesProjectModule] = '${rootProject.isModule}';
89+
return usageValues;
90+
}
91+
92+
Future<void> _runPubGet(String directory) async {
93+
final Stopwatch pubGetTimer = Stopwatch()..start();
94+
try {
95+
await pubGet(context: PubContext.pubGet,
96+
directory: directory,
97+
upgrade: upgrade ,
98+
offline: argResults['offline'],
99+
checkLastModified: false,
100+
);
101+
pubGetTimer.stop();
102+
flutterUsage.sendEvent('packages-pub-get', 'success');
103+
flutterUsage.sendTiming('packages-pub-get', 'success', pubGetTimer.elapsed);
104+
} catch (_) {
105+
pubGetTimer.stop();
106+
flutterUsage.sendEvent('packages-pub-get', 'failure');
107+
flutterUsage.sendTiming('packages-pub-get', 'failure', pubGetTimer.elapsed);
108+
rethrow;
109+
}
78110
}
79111

80112
@override
81113
Future<FlutterCommandResult> runCommand() async {
82114
if (argResults.rest.length > 1)
83115
throwToolExit('Too many arguments.\n$usage');
84116

85-
final String target = findProjectRoot(
86-
argResults.rest.length == 1 ? argResults.rest[0] : null
87-
);
117+
final String workingDirectory = argResults.rest.length == 1 ? argResults.rest[0] : null;
118+
final String target = findProjectRoot(workingDirectory);
88119
if (target == null) {
89120
throwToolExit(
90121
'Expected to find project root in '
91-
'${ argResults.rest.length == 1 ? argResults.rest[0] : "current working directory" }.'
122+
'${ workingDirectory ?? "current working directory" }.'
92123
);
93124
}
94125

packages/flutter_tools/lib/src/commands/run.dart

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import '../run_cold.dart';
1919
import '../run_hot.dart';
2020
import '../runner/flutter_command.dart';
2121
import '../tracing.dart';
22+
import '../usage.dart';
2223
import 'daemon.dart';
2324

2425
abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
@@ -208,8 +209,23 @@ class RunCommand extends RunCommandBase {
208209
final String deviceType = devices.length == 1
209210
? getNameForTargetPlatform(await devices[0].targetPlatform)
210211
: 'multiple';
212+
final AndroidProject androidProject = FlutterProject.current().android;
213+
final IosProject iosProject = FlutterProject.current().ios;
214+
final List<String> hostLanguage = <String>[];
211215

212-
return <String, String>{'cd3': '$isEmulator', 'cd4': deviceType};
216+
if (androidProject != null && androidProject.existsSync()) {
217+
hostLanguage.add(androidProject.isKotlin ? 'kotlin' : 'java');
218+
}
219+
if (iosProject != null && iosProject.exists) {
220+
hostLanguage.add(iosProject.isSwift ? 'swift' : 'objc');
221+
}
222+
223+
return <String, String>{
224+
kCommandRunIsEmulator: '$isEmulator',
225+
kCommandRunTargetName: deviceType,
226+
kCommandRunProjectModule: '${FlutterProject.current().isModule}',
227+
kCommandRunProjectHostLanguage: hostLanguage.join(','),
228+
};
213229
}
214230

215231
@override

packages/flutter_tools/lib/src/project.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ class AndroidProject {
397397
final FlutterProject parent;
398398

399399
static final RegExp _applicationIdPattern = RegExp('^\\s*applicationId\\s+[\'\"](.*)[\'\"]\\s*\$');
400+
static final RegExp _kotlinPluginPattern = RegExp('^\\s*apply plugin\:\\s+[\'\"]kotlin-android[\'\"]\\s*\$');
400401
static final RegExp _groupPattern = RegExp('^\\s*group\\s+[\'\"](.*)[\'\"]\\s*\$');
401402

402403
/// The Gradle root directory of the Android host app. This is the directory
@@ -419,6 +420,12 @@ class AndroidProject {
419420
/// True if the parent Flutter project is a module.
420421
bool get isModule => parent.isModule;
421422

423+
/// True, if the app project is using Kotlin.
424+
bool get isKotlin {
425+
final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
426+
return _firstMatchInFile(gradleFile, _kotlinPluginPattern) != null;
427+
}
428+
422429
File get appManifestFile {
423430
return isUsingGradle
424431
? fs.file(fs.path.join(hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml'))

packages/flutter_tools/lib/src/usage.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ const String kEventReloadInvalidatedSourcesCount = 'cd11';
2626
const String kEventReloadTransferTimeInMs = 'cd12';
2727
const String kEventReloadOverallTimeInMs = 'cd13';
2828

29+
const String kCommandRunIsEmulator = 'cd3';
30+
const String kCommandRunTargetName = 'cd4';
31+
const String kCommandRunProjectType = 'cd14';
32+
const String kCommandRunProjectHostLanguage = 'cd15';
33+
const String kCommandRunProjectModule = 'cd18';
34+
35+
const String kCommandCreateAndroidLanguage = 'cd16';
36+
const String kCommandCreateIosLanguage = 'cd17';
37+
const String kCommandCreateProjectType = 'cd19';
38+
39+
const String kCommandPackagesNumberPlugins = 'cd20';
40+
const String kCommandPackagesProjectModule = 'cd21';
41+
2942
Usage get flutterUsage => Usage.instance;
3043

3144
class Usage {

packages/flutter_tools/test/commands/create_test.dart

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ import 'package:flutter_tools/src/cache.dart';
1414
import 'package:flutter_tools/src/commands/create.dart';
1515
import 'package:flutter_tools/src/dart/sdk.dart';
1616
import 'package:flutter_tools/src/project.dart';
17+
import 'package:flutter_tools/src/usage.dart';
1718
import 'package:flutter_tools/src/version.dart';
19+
1820
import 'package:mockito/mockito.dart';
1921
import 'package:process/process.dart';
2022

2123
import '../src/common.dart';
2224
import '../src/context.dart';
2325

26+
2427
const String frameworkRevision = '12345678';
2528
const String frameworkChannel = 'omega';
2629
final Generator _kNoColorTerminalPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false;
@@ -927,8 +930,71 @@ void main() {
927930
HttpClientFactory: () =>
928931
() => MockHttpClient(404, result: 'not found'),
929932
});
933+
934+
group('usageValues', () {
935+
testUsingContext('set template type as usage value', () async {
936+
Cache.flutterRoot = '../..';
937+
938+
final CreateCommand command = CreateCommand();
939+
final CommandRunner<void> runner = createTestCommandRunner(command);
940+
941+
await runner.run(<String>['create', '--no-pub', '--template=module', projectDir.path]);
942+
expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'module'));
943+
944+
await runner.run(<String>['create', '--no-pub', '--template=app', projectDir.path]);
945+
expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'app'));
946+
947+
await runner.run(<String>['create', '--no-pub', '--template=package', projectDir.path]);
948+
expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'package'));
949+
950+
await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
951+
expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'plugin'));
952+
953+
}, timeout: allowForCreateFlutterProject);
954+
955+
testUsingContext('set iOS host language type as usage value', () async {
956+
Cache.flutterRoot = '../..';
957+
958+
final CreateCommand command = CreateCommand();
959+
final CommandRunner<void> runner = createTestCommandRunner(command);
960+
961+
await runner.run(<String>['create', '--no-pub', '--template=app', projectDir.path]);
962+
expect(await command.usageValues, containsPair(kCommandCreateIosLanguage, 'objc'));
963+
964+
await runner.run(<String>[
965+
'create',
966+
'--no-pub',
967+
'--template=app',
968+
'--ios-language=swift',
969+
projectDir.path,
970+
]);
971+
expect(await command.usageValues, containsPair(kCommandCreateIosLanguage, 'swift'));
972+
973+
}, timeout: allowForCreateFlutterProject);
974+
975+
testUsingContext('set Android host language type as usage value', () async {
976+
Cache.flutterRoot = '../..';
977+
978+
final CreateCommand command = CreateCommand();
979+
final CommandRunner<void> runner = createTestCommandRunner(command);
980+
981+
await runner.run(<String>['create', '--no-pub', '--template=app', projectDir.path]);
982+
expect(await command.usageValues, containsPair(kCommandCreateAndroidLanguage, 'java'));
983+
984+
await runner.run(<String>[
985+
'create',
986+
'--no-pub',
987+
'--template=app',
988+
'--android-language=kotlin',
989+
projectDir.path,
990+
]);
991+
expect(await command.usageValues, containsPair(kCommandCreateAndroidLanguage, 'kotlin'));
992+
993+
}, timeout: allowForCreateFlutterProject);
994+
});
930995
}
931996

997+
932998
Future<void> _createProject(
933999
Directory dir,
9341000
List<String> createArgs,

0 commit comments

Comments
 (0)