Report commands that resulted in success or failure (#34288)

This is added as a dimension cd26
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index fcb6924..e06d1c9 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -26,7 +26,6 @@
 import '../run_cold.dart';
 import '../run_hot.dart';
 import '../runner/flutter_command.dart';
-import '../usage.dart';
 
 /// A Flutter-command that attaches to applications that have been launched
 /// without `flutter run`.
@@ -316,10 +315,7 @@
         result = await runner.attach();
         assert(result != null);
       }
-      if (result == 0) {
-        flutterUsage.sendEvent('attach', 'success');
-      } else {
-        flutterUsage.sendEvent('attach', 'failure');
+      if (result != 0) {
         throwToolExit(null, exitCode: result);
       }
     } finally {
diff --git a/packages/flutter_tools/lib/src/commands/logs.dart b/packages/flutter_tools/lib/src/commands/logs.dart
index c97c62c..e3f8d4b 100644
--- a/packages/flutter_tools/lib/src/commands/logs.dart
+++ b/packages/flutter_tools/lib/src/commands/logs.dart
@@ -32,11 +32,11 @@
   Device device;
 
   @override
-  Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
+  Future<FlutterCommandResult> verifyThenRunCommand() async {
     device = await findTargetDevice();
     if (device == null)
       throwToolExit(null);
-    return super.verifyThenRunCommand(commandPath);
+    return super.verifyThenRunCommand();
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 3fb4bf8..a3603ad 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -219,14 +219,21 @@
 
   @override
   Future<Map<String, String>> get usageValues async {
-    final bool isEmulator = await devices[0].isLocalEmulator;
     String deviceType, deviceOsVersion;
-    if (devices.length == 1) {
+    bool isEmulator;
+
+    if (devices == null || devices.isEmpty) {
+      deviceType = 'none';
+      deviceOsVersion = 'none';
+      isEmulator = false;
+    } else if (devices.length == 1) {
       deviceType = getNameForTargetPlatform(await devices[0].targetPlatform);
       deviceOsVersion = await devices[0].sdkNameAndVersion;
+      isEmulator = await devices[0].isLocalEmulator;
     } else {
       deviceType = 'multiple';
       deviceOsVersion = 'multiple';
+      isEmulator = false;
     }
     final String modeName = getBuildInfo().modeName;
     final AndroidProject androidProject = FlutterProject.current().android;
diff --git a/packages/flutter_tools/lib/src/commands/screenshot.dart b/packages/flutter_tools/lib/src/commands/screenshot.dart
index 38552dd..63e454b 100644
--- a/packages/flutter_tools/lib/src/commands/screenshot.dart
+++ b/packages/flutter_tools/lib/src/commands/screenshot.dart
@@ -64,7 +64,7 @@
   Device device;
 
   @override
-  Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
+  Future<FlutterCommandResult> verifyThenRunCommand() async {
     device = await findTargetDevice();
     if (device == null)
       throwToolExit('Must have a connected device');
@@ -72,7 +72,7 @@
       throwToolExit('Screenshot not supported for ${device.name}.');
     if (argResults[_kType] != _kDeviceType && argResults[_kObservatoryUri] == null)
       throwToolExit('Observatory URI must be specified for screenshot type ${argResults[_kType]}');
-    return super.verifyThenRunCommand(commandPath);
+    return super.verifyThenRunCommand();
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 634d4a8..af65951 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -408,10 +408,9 @@
       body: () async {
         if (flutterUsage.isFirstRun)
           flutterUsage.printWelcome();
-        final String commandPath = await usagePath;
         FlutterCommandResult commandResult;
         try {
-          commandResult = await verifyThenRunCommand(commandPath);
+          commandResult = await verifyThenRunCommand();
         } on ToolExit {
           commandResult = const FlutterCommandResult(ExitStatus.fail);
           rethrow;
@@ -419,32 +418,68 @@
           final DateTime endTime = systemClock.now();
           printTrace(userMessages.flutterElapsedTime(name, getElapsedAsMilliseconds(endTime.difference(startTime))));
           printTrace('"flutter $name" took ${getElapsedAsMilliseconds(endTime.difference(startTime))}.');
-          if (commandPath != null) {
-            final List<String> labels = <String>[];
-            if (commandResult?.exitStatus != null)
-              labels.add(getEnumName(commandResult.exitStatus));
-            if (commandResult?.timingLabelParts?.isNotEmpty ?? false)
-              labels.addAll(commandResult.timingLabelParts);
 
-            final String label = labels
-                .where((String label) => !isBlank(label))
-                .join('-');
-            flutterUsage.sendTiming(
-              'flutter',
-              name,
-              // If the command provides its own end time, use it. Otherwise report
-              // the duration of the entire execution.
-              (commandResult?.endTimeOverride ?? endTime).difference(startTime),
-              // Report in the form of `success-[parameter1-parameter2]`, all of which
-              // can be null if the command doesn't provide a FlutterCommandResult.
-              label: label == '' ? null : label,
-            );
-          }
+          await _sendUsage(commandResult, startTime, endTime);
         }
       },
     );
   }
 
+  /// Logs data about this command.
+  ///
+  /// For example, the command path (e.g. `build/apk`) and the result,
+  /// as well as the time spent running it.
+  Future<void> _sendUsage(FlutterCommandResult commandResult, DateTime startTime, DateTime endTime) async {
+    final String commandPath = await usagePath;
+
+    if (commandPath == null) {
+      return;
+    }
+
+    // Send screen.
+    final Map<String, String> additionalUsageValues = <String, String>{};
+    final Map<String, String> currentUsageValues = await usageValues;
+
+    if (currentUsageValues != null) {
+      additionalUsageValues.addAll(currentUsageValues);
+    }
+    if (commandResult != null) {
+      switch (commandResult.exitStatus) {
+        case ExitStatus.success:
+          additionalUsageValues[kCommandResult] = 'success';
+          break;
+        case ExitStatus.warning:
+          additionalUsageValues[kCommandResult] = 'warning';
+          break;
+        case ExitStatus.fail:
+          additionalUsageValues[kCommandResult] = 'fail';
+          break;
+      }
+    }
+    flutterUsage.sendCommand(commandPath, parameters: additionalUsageValues);
+
+    // Send timing.
+    final List<String> labels = <String>[];
+    if (commandResult?.exitStatus != null)
+      labels.add(getEnumName(commandResult.exitStatus));
+    if (commandResult?.timingLabelParts?.isNotEmpty ?? false)
+      labels.addAll(commandResult.timingLabelParts);
+
+    final String label = labels
+        .where((String label) => !isBlank(label))
+        .join('-');
+    flutterUsage.sendTiming(
+      'flutter',
+      name,
+      // If the command provides its own end time, use it. Otherwise report
+      // the duration of the entire execution.
+      (commandResult?.endTimeOverride ?? endTime).difference(startTime),
+      // Report in the form of `success-[parameter1-parameter2]`, all of which
+      // can be null if the command doesn't provide a FlutterCommandResult.
+      label: label == '' ? null : label,
+    );
+  }
+
   /// Perform validation then call [runCommand] to execute the command.
   /// Return a [Future] that completes with an exit code
   /// indicating whether execution was successful.
@@ -453,7 +488,7 @@
   /// then call this method to execute the command
   /// rather than calling [runCommand] directly.
   @mustCallSuper
-  Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
+  Future<FlutterCommandResult> verifyThenRunCommand() async {
     await validateCommand();
 
     // Populate the cache. We call this before pub get below so that the sky_engine
@@ -470,11 +505,6 @@
 
     setupApplicationPackages();
 
-    if (commandPath != null) {
-      final Map<String, String> additionalUsageValues = await usageValues;
-      flutterUsage.sendCommand(commandPath, parameters: additionalUsageValues);
-    }
-
     return await runCommand();
   }
 
diff --git a/packages/flutter_tools/lib/src/usage.dart b/packages/flutter_tools/lib/src/usage.dart
index d74437b..63d8d7f 100644
--- a/packages/flutter_tools/lib/src/usage.dart
+++ b/packages/flutter_tools/lib/src/usage.dart
@@ -47,7 +47,9 @@
 
 const String kCommandBuildBundleTargetPlatform = 'cd24';
 const String kCommandBuildBundleIsModule = 'cd25';
-// Next ID: cd26
+
+const String kCommandResult = 'cd26';
+// Next ID: cd27
 
 Usage get flutterUsage => Usage.instance;
 
diff --git a/packages/flutter_tools/test/runner/flutter_command_test.dart b/packages/flutter_tools/test/runner/flutter_command_test.dart
index e91b95b..a402b1d 100644
--- a/packages/flutter_tools/test/runner/flutter_command_test.dart
+++ b/packages/flutter_tools/test/runner/flutter_command_test.dart
@@ -49,6 +49,114 @@
       Cache: () => cache,
     });
 
+    testUsingContext('reports command that results in success', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
+        commandFunction: () async {
+          return const FlutterCommandResult(ExitStatus.success);
+        }
+      );
+      await flutterCommand.run();
+
+      expect(
+        verify(usage.sendCommand(captureAny,
+                parameters: captureAnyNamed('parameters'))).captured,
+        <dynamic>[
+          'dummy',
+          const <String, String>{'cd26': 'success'}
+        ],
+      );
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
+    testUsingContext('reports command that results in warning', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
+        commandFunction: () async {
+          return const FlutterCommandResult(ExitStatus.warning);
+        }
+      );
+      await flutterCommand.run();
+
+      expect(
+        verify(usage.sendCommand(captureAny,
+                parameters: captureAnyNamed('parameters'))).captured,
+        <dynamic>[
+          'dummy',
+          const <String, String>{'cd26': 'warning'}
+        ],
+      );
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
+    testUsingContext('reports command that results in failure', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
+        commandFunction: () async {
+          return const FlutterCommandResult(ExitStatus.fail);
+        }
+      );
+
+      try {
+        await flutterCommand.run();
+      } on ToolExit {
+        expect(
+          verify(usage.sendCommand(captureAny,
+                  parameters: captureAnyNamed('parameters'))).captured,
+          <dynamic>[
+            'dummy',
+            const <String, String>{'cd26': 'fail'}
+          ],
+        );
+      }
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
+    testUsingContext('reports command that results in error', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
+        commandFunction: () async {
+          throwToolExit('fail');
+          return null; // unreachable
+        }
+      );
+
+      try {
+        await flutterCommand.run();
+        fail('Mock should make this fail');
+      } on ToolExit {
+        expect(
+          verify(usage.sendCommand(captureAny,
+                  parameters: captureAnyNamed('parameters'))).captured,
+          <dynamic>[
+            'dummy',
+            const <String, String>{'cd26': 'fail'}
+          ],
+        );
+      }
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
     testUsingContext('report execution timing by default', () async {
       // Crash if called a third time which is unexpected.
       mockTimes = <int>[1000, 2000];
@@ -61,7 +169,12 @@
         verify(usage.sendTiming(
                 captureAny, captureAny, captureAny,
                 label: captureAnyNamed('label'))).captured,
-        <dynamic>['flutter', 'dummy', const Duration(milliseconds: 1000), null],
+        <dynamic>[
+          'flutter',
+          'dummy',
+          const Duration(milliseconds: 1000),
+          null
+        ],
       );
     },
     overrides: <Type, Generator>{