Skip to content

Commit 81052a7

Browse files
authored
Add usage event to track when a iOS network device is used (#118915)
* Add usage event to track when a iOS network device is used * update usage event to track percentage of iOS network vs usb devices, update and fix tests * refactor tracking to happen in usageValues with a custom dimension
1 parent e85547b commit 81052a7

File tree

3 files changed

+169
-4
lines changed

3 files changed

+169
-4
lines changed

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import '../daemon.dart';
1919
import '../device.dart';
2020
import '../features.dart';
2121
import '../globals.dart' as globals;
22+
import '../ios/devices.dart';
23+
import '../ios/iproxy.dart';
2224
import '../project.dart';
2325
import '../reporting/reporting.dart';
2426
import '../resident_runner.dart';
@@ -407,18 +409,23 @@ class RunCommand extends RunCommandBase {
407409
bool isEmulator;
408410
bool anyAndroidDevices = false;
409411
bool anyIOSDevices = false;
412+
bool anyIOSNetworkDevices = false;
410413

411414
if (devices == null || devices!.isEmpty) {
412415
deviceType = 'none';
413416
deviceOsVersion = 'none';
414417
isEmulator = false;
415418
} else if (devices!.length == 1) {
416-
final TargetPlatform platform = await devices![0].targetPlatform;
419+
final Device device = devices![0];
420+
final TargetPlatform platform = await device.targetPlatform;
417421
anyAndroidDevices = platform == TargetPlatform.android;
418422
anyIOSDevices = platform == TargetPlatform.ios;
423+
if (device is IOSDevice && device.interfaceType == IOSDeviceConnectionInterface.network) {
424+
anyIOSNetworkDevices = true;
425+
}
419426
deviceType = getNameForTargetPlatform(platform);
420-
deviceOsVersion = await devices![0].sdkNameAndVersion;
421-
isEmulator = await devices![0].isLocalEmulator;
427+
deviceOsVersion = await device.sdkNameAndVersion;
428+
isEmulator = await device.isLocalEmulator;
422429
} else {
423430
deviceType = 'multiple';
424431
deviceOsVersion = 'multiple';
@@ -427,12 +434,20 @@ class RunCommand extends RunCommandBase {
427434
final TargetPlatform platform = await device.targetPlatform;
428435
anyAndroidDevices = anyAndroidDevices || (platform == TargetPlatform.android);
429436
anyIOSDevices = anyIOSDevices || (platform == TargetPlatform.ios);
437+
if (device is IOSDevice && device.interfaceType == IOSDeviceConnectionInterface.network) {
438+
anyIOSNetworkDevices = true;
439+
}
430440
if (anyAndroidDevices && anyIOSDevices) {
431441
break;
432442
}
433443
}
434444
}
435445

446+
String? iOSInterfaceType;
447+
if (anyIOSDevices) {
448+
iOSInterfaceType = anyIOSNetworkDevices ? 'wireless' : 'usb';
449+
}
450+
436451
String? androidEmbeddingVersion;
437452
final List<String> hostLanguage = <String>[];
438453
if (anyAndroidDevices) {
@@ -464,6 +479,7 @@ class RunCommand extends RunCommandBase {
464479
commandRunProjectHostLanguage: hostLanguage.join(','),
465480
commandRunAndroidEmbeddingVersion: androidEmbeddingVersion,
466481
commandRunEnableImpeller: enableImpeller,
482+
commandRunIOSInterfaceType: iOSInterfaceType,
467483
);
468484
}
469485

packages/flutter_tools/lib/src/reporting/custom_dimensions.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class CustomDimensions {
6767
this.hotEventReassembleTimeInMs,
6868
this.hotEventReloadVMTimeInMs,
6969
this.commandRunEnableImpeller,
70+
this.commandRunIOSInterfaceType,
7071
});
7172

7273
final String? sessionHostOsDetails; // cd1
@@ -125,6 +126,7 @@ class CustomDimensions {
125126
final int? hotEventReassembleTimeInMs; // cd 54
126127
final int? hotEventReloadVMTimeInMs; // cd 55
127128
final bool? commandRunEnableImpeller; // cd 56
129+
final String? commandRunIOSInterfaceType; // cd 57
128130

129131
/// Convert to a map that will be used to upload to the analytics backend.
130132
Map<String, String> toMap() => <String, String>{
@@ -184,6 +186,7 @@ class CustomDimensions {
184186
if (hotEventReassembleTimeInMs != null) cdKey(CustomDimensionsEnum.hotEventReassembleTimeInMs): hotEventReassembleTimeInMs.toString(),
185187
if (hotEventReloadVMTimeInMs != null) cdKey(CustomDimensionsEnum.hotEventReloadVMTimeInMs): hotEventReloadVMTimeInMs.toString(),
186188
if (commandRunEnableImpeller != null) cdKey(CustomDimensionsEnum.commandRunEnableImpeller): commandRunEnableImpeller.toString(),
189+
if (commandRunIOSInterfaceType != null) cdKey(CustomDimensionsEnum.commandRunIOSInterfaceType): commandRunIOSInterfaceType.toString(),
187190
};
188191

189192
/// Merge the values of two [CustomDimensions] into one. If a value is defined
@@ -250,6 +253,7 @@ class CustomDimensions {
250253
hotEventReassembleTimeInMs: other.hotEventReassembleTimeInMs ?? hotEventReassembleTimeInMs,
251254
hotEventReloadVMTimeInMs: other.hotEventReloadVMTimeInMs ?? hotEventReloadVMTimeInMs,
252255
commandRunEnableImpeller: other.commandRunEnableImpeller ?? commandRunEnableImpeller,
256+
commandRunIOSInterfaceType: other.commandRunIOSInterfaceType ?? commandRunIOSInterfaceType,
253257
);
254258
}
255259

@@ -310,6 +314,7 @@ class CustomDimensions {
310314
hotEventReassembleTimeInMs: _extractInt(map, CustomDimensionsEnum.hotEventReassembleTimeInMs),
311315
hotEventReloadVMTimeInMs: _extractInt(map, CustomDimensionsEnum.hotEventReloadVMTimeInMs),
312316
commandRunEnableImpeller: _extractBool(map, CustomDimensionsEnum.commandRunEnableImpeller),
317+
commandRunIOSInterfaceType: _extractString(map, CustomDimensionsEnum.commandRunIOSInterfaceType),
313318
);
314319

315320
static bool? _extractBool(Map<String, String> map, CustomDimensionsEnum field) =>
@@ -396,6 +401,7 @@ enum CustomDimensionsEnum {
396401
hotEventReassembleTimeInMs, // cd54
397402
hotEventReloadVMTimeInMs, // cd55
398403
commandRunEnableImpeller, // cd56
404+
commandRunIOSInterfaceType, // cd57
399405
}
400406

401407
String cdKey(CustomDimensionsEnum cd) => 'cd${cd.index + 1}';

packages/flutter_tools/test/commands.shard/hermetic/run_test.dart

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import 'package:flutter_tools/src/commands/run.dart';
2424
import 'package:flutter_tools/src/devfs.dart';
2525
import 'package:flutter_tools/src/device.dart';
2626
import 'package:flutter_tools/src/globals.dart' as globals;
27+
import 'package:flutter_tools/src/ios/devices.dart';
28+
import 'package:flutter_tools/src/ios/iproxy.dart';
2729
import 'package:flutter_tools/src/project.dart';
2830
import 'package:flutter_tools/src/reporting/reporting.dart';
2931
import 'package:flutter_tools/src/resident_runner.dart';
@@ -426,7 +428,7 @@ void main() {
426428
TestUsageCommand('run', parameters: CustomDimensions.fromMap(<String, String>{
427429
'cd3': 'false', 'cd4': 'ios', 'cd22': 'iOS 13',
428430
'cd23': 'debug', 'cd18': 'false', 'cd15': 'swift', 'cd31': 'true',
429-
'cd56': 'false',
431+
'cd56': 'false', 'cd57': 'usb',
430432
})
431433
)));
432434
}, overrides: <Type, Generator>{
@@ -664,6 +666,104 @@ void main() {
664666
FileSystem: () => MemoryFileSystem.test(),
665667
ProcessManager: () => FakeProcessManager.any(),
666668
});
669+
670+
group('usageValues', () {
671+
testUsingContext('with only non-iOS usb device', () async {
672+
final List<Device> devices = <Device>[
673+
FakeDevice(targetPlatform: TargetPlatform.android_arm, platformType: PlatformType.android),
674+
];
675+
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices);
676+
final CustomDimensions dimensions = await command.usageValues;
677+
678+
expect(dimensions, const CustomDimensions(
679+
commandRunIsEmulator: false,
680+
commandRunTargetName: 'android-arm',
681+
commandRunTargetOsVersion: '',
682+
commandRunModeName: 'debug',
683+
commandRunProjectModule: false,
684+
commandRunProjectHostLanguage: '',
685+
commandRunEnableImpeller: false,
686+
));
687+
}, overrides: <Type, Generator>{
688+
DeviceManager: () => testDeviceManager,
689+
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
690+
FileSystem: () => MemoryFileSystem.test(),
691+
ProcessManager: () => FakeProcessManager.any(),
692+
});
693+
694+
testUsingContext('with only iOS usb device', () async {
695+
final List<Device> devices = <Device>[
696+
FakeIOSDevice(interfaceType: IOSDeviceConnectionInterface.usb, sdkNameAndVersion: 'iOS 16.2'),
697+
];
698+
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices);
699+
final CustomDimensions dimensions = await command.usageValues;
700+
701+
expect(dimensions, const CustomDimensions(
702+
commandRunIsEmulator: false,
703+
commandRunTargetName: 'ios',
704+
commandRunTargetOsVersion: 'iOS 16.2',
705+
commandRunModeName: 'debug',
706+
commandRunProjectModule: false,
707+
commandRunProjectHostLanguage: '',
708+
commandRunEnableImpeller: false,
709+
commandRunIOSInterfaceType: 'usb',
710+
));
711+
}, overrides: <Type, Generator>{
712+
DeviceManager: () => testDeviceManager,
713+
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
714+
FileSystem: () => MemoryFileSystem.test(),
715+
ProcessManager: () => FakeProcessManager.any(),
716+
});
717+
718+
testUsingContext('with only iOS network device', () async {
719+
final List<Device> devices = <Device>[
720+
FakeIOSDevice(interfaceType: IOSDeviceConnectionInterface.network, sdkNameAndVersion: 'iOS 16.2'),
721+
];
722+
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices);
723+
final CustomDimensions dimensions = await command.usageValues;
724+
725+
expect(dimensions, const CustomDimensions(
726+
commandRunIsEmulator: false,
727+
commandRunTargetName: 'ios',
728+
commandRunTargetOsVersion: 'iOS 16.2',
729+
commandRunModeName: 'debug',
730+
commandRunProjectModule: false,
731+
commandRunProjectHostLanguage: '',
732+
commandRunEnableImpeller: false,
733+
commandRunIOSInterfaceType: 'wireless',
734+
));
735+
}, overrides: <Type, Generator>{
736+
DeviceManager: () => testDeviceManager,
737+
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
738+
FileSystem: () => MemoryFileSystem.test(),
739+
ProcessManager: () => FakeProcessManager.any(),
740+
});
741+
742+
testUsingContext('with both iOS usb and network devices', () async {
743+
final List<Device> devices = <Device>[
744+
FakeIOSDevice(interfaceType: IOSDeviceConnectionInterface.network, sdkNameAndVersion: 'iOS 16.2'),
745+
FakeIOSDevice(interfaceType: IOSDeviceConnectionInterface.usb, sdkNameAndVersion: 'iOS 16.2'),
746+
];
747+
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices);
748+
final CustomDimensions dimensions = await command.usageValues;
749+
750+
expect(dimensions, const CustomDimensions(
751+
commandRunIsEmulator: false,
752+
commandRunTargetName: 'multiple',
753+
commandRunTargetOsVersion: 'multiple',
754+
commandRunModeName: 'debug',
755+
commandRunProjectModule: false,
756+
commandRunProjectHostLanguage: '',
757+
commandRunEnableImpeller: false,
758+
commandRunIOSInterfaceType: 'wireless',
759+
));
760+
}, overrides: <Type, Generator>{
761+
DeviceManager: () => testDeviceManager,
762+
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
763+
FileSystem: () => MemoryFileSystem.test(),
764+
ProcessManager: () => FakeProcessManager.any(),
765+
});
766+
});
667767
});
668768

669769
group('dart-defines and web-renderer options', () {
@@ -1032,6 +1132,49 @@ class FakeDevice extends Fake implements Device {
10321132
}
10331133
}
10341134

1135+
// Unfortunately Device, despite not being immutable, has an `operator ==`.
1136+
// Until we fix that, we have to also ignore related lints here.
1137+
// ignore: avoid_implementing_value_types
1138+
class FakeIOSDevice extends Fake implements IOSDevice {
1139+
FakeIOSDevice({
1140+
this.interfaceType = IOSDeviceConnectionInterface.none,
1141+
bool isLocalEmulator = false,
1142+
String sdkNameAndVersion = '',
1143+
}): _isLocalEmulator = isLocalEmulator,
1144+
_sdkNameAndVersion = sdkNameAndVersion;
1145+
1146+
final bool _isLocalEmulator;
1147+
final String _sdkNameAndVersion;
1148+
1149+
@override
1150+
Future<bool> get isLocalEmulator => Future<bool>.value(_isLocalEmulator);
1151+
1152+
@override
1153+
Future<String> get sdkNameAndVersion => Future<String>.value(_sdkNameAndVersion);
1154+
1155+
@override
1156+
final IOSDeviceConnectionInterface interfaceType;
1157+
1158+
@override
1159+
Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
1160+
}
1161+
1162+
class TestRunCommandForUsageValues extends RunCommand {
1163+
TestRunCommandForUsageValues({
1164+
this.devices,
1165+
});
1166+
1167+
@override
1168+
// devices is not set within usageValues, so we override the field
1169+
// ignore: overridden_fields
1170+
List<Device>? devices;
1171+
1172+
@override
1173+
Future<BuildInfo> getBuildInfo({ BuildMode? forcedBuildMode, File? forcedTargetFile }) async {
1174+
return const BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
1175+
}
1176+
}
1177+
10351178
class TestRunCommandWithFakeResidentRunner extends RunCommand {
10361179
late FakeResidentRunner fakeResidentRunner;
10371180

0 commit comments

Comments
 (0)