[flutter_tool] Clean up usage events and custom dimensions (#36785)

diff --git a/packages/flutter_tools/bin/fuchsia_asset_builder.dart b/packages/flutter_tools/bin/fuchsia_asset_builder.dart
index 063056b..f6974c7 100644
--- a/packages/flutter_tools/bin/fuchsia_asset_builder.dart
+++ b/packages/flutter_tools/bin/fuchsia_asset_builder.dart
@@ -15,8 +15,7 @@
 import 'package:flutter_tools/src/devfs.dart';
 import 'package:flutter_tools/src/bundle.dart';
 import 'package:flutter_tools/src/globals.dart';
-import 'package:flutter_tools/src/reporting/disabled_usage.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 
 const String _kOptionPackages = 'packages';
 const String _kOptionAsset = 'asset-dir';
diff --git a/packages/flutter_tools/bin/fuchsia_tester.dart b/packages/flutter_tools/bin/fuchsia_tester.dart
index b78eea3..7fb9364 100644
--- a/packages/flutter_tools/bin/fuchsia_tester.dart
+++ b/packages/flutter_tools/bin/fuchsia_tester.dart
@@ -18,8 +18,7 @@
 import 'package:flutter_tools/src/artifacts.dart';
 import 'package:flutter_tools/src/globals.dart';
 import 'package:flutter_tools/src/project.dart';
-import 'package:flutter_tools/src/reporting/disabled_usage.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/test/coverage_collector.dart';
 import 'package:flutter_tools/src/test/runner.dart';
 
diff --git a/packages/flutter_tools/lib/runner.dart b/packages/flutter_tools/lib/runner.dart
index 398f5a6..f63553c 100644
--- a/packages/flutter_tools/lib/runner.dart
+++ b/packages/flutter_tools/lib/runner.dart
@@ -19,8 +19,7 @@
 import 'src/context_runner.dart';
 import 'src/doctor.dart';
 import 'src/globals.dart';
-import 'src/reporting/crash_reporting.dart';
-import 'src/reporting/usage.dart';
+import 'src/reporting/reporting.dart';
 import 'src/runner/flutter_command.dart';
 import 'src/runner/flutter_command_runner.dart';
 import 'src/version.dart';
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index edcf064..ce04147 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -24,8 +24,7 @@
 import '../flutter_manifest.dart';
 import '../globals.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
-import '../runner/flutter_command.dart';
+import '../reporting/reporting.dart';
 import 'android_sdk.dart';
 import 'android_studio.dart';
 
@@ -747,11 +746,7 @@
       printError('The Gradle failure may have been because of AndroidX incompatibilities in this Flutter app.');
       printError('See https://goo.gl/CP92wY for more information on the problem and how to fix it.');
       printError('*******************************************************************************************');
-      String commandName = '';
-      if (FlutterCommand.current != null) {
-        commandName = '-${FlutterCommand.current.name}';
-      }
-      flutterUsage.sendEvent('build$commandName', 'android-x-failure');
+      BuildEvent('android-x-failure').send();
     }
     throwToolExit('Gradle task $assembleTask failed with exit code $exitCode', exitCode: exitCode);
   }
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
index ca09c3e..a29762b 100644
--- a/packages/flutter_tools/lib/src/base/build.dart
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -14,7 +14,7 @@
 import '../globals.dart';
 import '../macos/xcode.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 
 import 'context.dart';
 import 'file_system.dart';
diff --git a/packages/flutter_tools/lib/src/commands/build_aar.dart b/packages/flutter_tools/lib/src/commands/build_aar.dart
index e40f522..73c415a 100644
--- a/packages/flutter_tools/lib/src/commands/build_aar.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aar.dart
@@ -9,7 +9,7 @@
 import '../base/os.dart';
 import '../build_info.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
 import 'build.dart';
 
@@ -38,20 +38,20 @@
   final String name = 'aar';
 
   @override
-  Future<Map<String, String>> get usageValues async {
-    final Map<String, String> usage = <String, String>{};
+  Future<Map<CustomDimensions, String>> get usageValues async {
+    final Map<CustomDimensions, String> usage = <CustomDimensions, String>{};
     final FlutterProject futterProject = _getProject();
     if (futterProject == null) {
       return usage;
     }
     if (futterProject.manifest.isModule) {
-      usage[kCommandBuildAarProjectType] = 'module';
+      usage[CustomDimensions.commandBuildAarProjectType] = 'module';
     } else if (futterProject.manifest.isPlugin) {
-      usage[kCommandBuildAarProjectType] = 'plugin';
+      usage[CustomDimensions.commandBuildAarProjectType] = 'plugin';
     } else {
-      usage[kCommandBuildAarProjectType] = 'app';
+      usage[CustomDimensions.commandBuildAarProjectType] = 'app';
     }
-    usage[kCommandBuildAarTargetPlatform] =
+    usage[CustomDimensions.commandBuildAarTargetPlatform] =
         (argResults['target-platform'] as List<String>).join(',');
     return usage;
   }
diff --git a/packages/flutter_tools/lib/src/commands/build_bundle.dart b/packages/flutter_tools/lib/src/commands/build_bundle.dart
index 7e3e21f..92b4474 100644
--- a/packages/flutter_tools/lib/src/commands/build_bundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_bundle.dart
@@ -10,7 +10,7 @@
 import '../bundle.dart';
 import '../features.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 import '../runner/flutter_command.dart' show FlutterOptions, FlutterCommandResult;
 import 'build.dart';
 
@@ -74,17 +74,15 @@
       ' iOS runtimes.';
 
   @override
-  Future<Map<String, String>> get usageValues async {
+  Future<Map<CustomDimensions, String>> get usageValues async {
     final String projectDir = fs.file(targetFile).parent.parent.path;
     final FlutterProject futterProject = FlutterProject.fromPath(projectDir);
-
     if (futterProject == null) {
-      return const <String, String>{};
+      return const <CustomDimensions, String>{};
     }
-
-    return <String, String>{
-      kCommandBuildBundleTargetPlatform: argResults['target-platform'],
-      kCommandBuildBundleIsModule: '${futterProject.isModule}'
+    return <CustomDimensions, String>{
+      CustomDimensions.commandBuildBundleTargetPlatform: argResults['target-platform'],
+      CustomDimensions.commandBuildBundleIsModule: '${futterProject.isModule}'
     };
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/config.dart b/packages/flutter_tools/lib/src/commands/config.dart
index c98a971..9e4c6b7 100644
--- a/packages/flutter_tools/lib/src/commands/config.dart
+++ b/packages/flutter_tools/lib/src/commands/config.dart
@@ -11,7 +11,7 @@
 import '../convert.dart';
 import '../features.dart';
 import '../globals.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 import '../runner/flutter_command.dart';
 import '../version.dart';
 
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index 875beae..98f9227 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -23,7 +23,7 @@
 import '../features.dart';
 import '../globals.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 import '../runner/flutter_command.dart';
 import '../template.dart';
 import '../version.dart';
@@ -165,11 +165,11 @@
   String get invocation => '${runner.executableName} $name <output directory>';
 
   @override
-  Future<Map<String, String>> get usageValues async {
-    return <String, String>{
-      kCommandCreateProjectType: argResults['template'],
-      kCommandCreateAndroidLanguage: argResults['android-language'],
-      kCommandCreateIosLanguage: argResults['ios-language'],
+  Future<Map<CustomDimensions, String>> get usageValues async {
+    return <CustomDimensions, String>{
+      CustomDimensions.commandCreateProjectType: argResults['template'],
+      CustomDimensions.commandCreateAndroidLanguage: argResults['android-language'],
+      CustomDimensions.commandCreateIosLanguage: argResults['ios-language'],
     };
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart
index 767a606..8b60b93 100644
--- a/packages/flutter_tools/lib/src/commands/packages.dart
+++ b/packages/flutter_tools/lib/src/commands/packages.dart
@@ -9,7 +9,7 @@
 import '../cache.dart';
 import '../dart/pub.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 import '../runner/flutter_command.dart';
 
 class PackagesCommand extends FlutterCommand {
@@ -71,8 +71,8 @@
   }
 
   @override
-  Future<Map<String, String>> get usageValues async {
-    final Map<String, String> usageValues = <String, String>{};
+  Future<Map<CustomDimensions, String>> get usageValues async {
+    final Map<CustomDimensions, String> usageValues = <CustomDimensions, String>{};
     final String workingDirectory = argResults.rest.length == 1 ? argResults.rest[0] : null;
     final String target = findProjectRoot(workingDirectory);
     if (target == null) {
@@ -82,11 +82,11 @@
     final bool hasPlugins = await rootProject.flutterPluginsFile.exists();
     if (hasPlugins) {
       final int numberOfPlugins = (await rootProject.flutterPluginsFile.readAsLines()).length;
-      usageValues[kCommandPackagesNumberPlugins] = '$numberOfPlugins';
+      usageValues[CustomDimensions.commandPackagesNumberPlugins] = '$numberOfPlugins';
     } else {
-      usageValues[kCommandPackagesNumberPlugins] = '0';
+      usageValues[CustomDimensions.commandPackagesNumberPlugins] = '0';
     }
-    usageValues[kCommandPackagesProjectModule] = '${rootProject.isModule}';
+    usageValues[CustomDimensions.commandPackagesProjectModule] = '${rootProject.isModule}';
     return usageValues;
   }
 
@@ -100,11 +100,11 @@
         checkLastModified: false,
       );
       pubGetTimer.stop();
-      flutterUsage.sendEvent('packages-pub-get', 'success');
+      PubGetEvent(success: true).send();
       flutterUsage.sendTiming('packages-pub-get', 'success', pubGetTimer.elapsed);
     } catch (_) {
       pubGetTimer.stop();
-      flutterUsage.sendEvent('packages-pub-get', 'failure');
+      PubGetEvent(success: false).send();
       flutterUsage.sendTiming('packages-pub-get', 'failure', pubGetTimer.elapsed);
       rethrow;
     }
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 091b043..298386c 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -17,7 +17,7 @@
 import '../globals.dart';
 import '../macos/xcode.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 import '../resident_runner.dart';
 import '../resident_web_runner.dart';
 import '../run_cold.dart';
@@ -198,7 +198,7 @@
   }
 
   @override
-  Future<Map<String, String>> get usageValues async {
+  Future<Map<CustomDimensions, String>> get usageValues async {
     String deviceType, deviceOsVersion;
     bool isEmulator;
 
@@ -227,13 +227,13 @@
       hostLanguage.add(iosProject.isSwift ? 'swift' : 'objc');
     }
 
-    return <String, String>{
-      kCommandRunIsEmulator: '$isEmulator',
-      kCommandRunTargetName: deviceType,
-      kCommandRunTargetOsVersion: deviceOsVersion,
-      kCommandRunModeName: modeName,
-      kCommandRunProjectModule: '${FlutterProject.current().isModule}',
-      kCommandRunProjectHostLanguage: hostLanguage.join(','),
+    return <CustomDimensions, String>{
+      CustomDimensions.commandRunIsEmulator: '$isEmulator',
+      CustomDimensions.commandRunTargetName: deviceType,
+      CustomDimensions.commandRunTargetOsVersion: deviceOsVersion,
+      CustomDimensions.commandRunModeName: modeName,
+      CustomDimensions.commandRunProjectModule: '${FlutterProject.current().isModule}',
+      CustomDimensions.commandRunProjectHostLanguage: hostLanguage.join(','),
     };
   }
 
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index f086f28..83985e5 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -42,7 +42,7 @@
 import 'macos/macos_workflow.dart';
 import 'macos/xcode.dart';
 import 'macos/xcode_validator.dart';
-import 'reporting/usage.dart';
+import 'reporting/reporting.dart';
 import 'run_hot.dart';
 import 'version.dart';
 import 'web/chrome.dart';
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index 7a28775..f2a5cf2 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+
 import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
 import 'package:meta/meta.dart';
 
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index cf697c7..b2a0311 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -31,7 +31,7 @@
 import 'macos/macos_workflow.dart';
 import 'macos/xcode_validator.dart';
 import 'proxy_validator.dart';
-import 'reporting/usage.dart';
+import 'reporting/reporting.dart';
 import 'tester/flutter_tester.dart';
 import 'version.dart';
 import 'vscode/vscode_validator.dart';
@@ -236,7 +236,7 @@
           break;
       }
 
-      flutterUsage.sendEvent('doctorResult.${validator.runtimeType.toString()}', result.typeStr);
+      DoctorResultEvent(validator: validator, result: result).send();
 
       if (result.statusInfo != null) {
         printStatus('${result.coloredLeadingBox} ${validator.title} (${result.statusInfo})',
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart
index d52b024..cb357ca 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart
@@ -16,7 +16,7 @@
 import '../devfs.dart';
 import '../globals.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 
 import 'fuchsia_pm.dart';
 import 'fuchsia_sdk.dart';
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 5b5ca1f..bbbf868 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -24,7 +24,7 @@
 import '../macos/cocoapod_utils.dart';
 import '../macos/xcode.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 import '../services.dart';
 import 'code_signing.dart';
 import 'xcodeproj.dart';
@@ -472,13 +472,10 @@
   if (result.xcodeBuildExecution != null &&
       result.xcodeBuildExecution.buildForPhysicalDevice &&
       result.stdout?.toUpperCase()?.contains('BITCODE') == true) {
-    flutterUsage.sendEvent(
-      'Xcode',
-      'bitcode-failure',
-      parameters: <String, String>{
-        'build-commands': result.xcodeBuildExecution.buildCommands.toString(),
-        'build-settings': result.xcodeBuildExecution.buildSettings.toString(),
-      });
+    BuildEvent('xcode-bitcode-failure',
+      command: result.xcodeBuildExecution.buildCommands.toString(),
+      settings: result.xcodeBuildExecution.buildSettings.toString(),
+    ).send();
   }
 
   if (result.xcodeBuildExecution != null &&
diff --git a/packages/flutter_tools/lib/src/linux/build_linux.dart b/packages/flutter_tools/lib/src/linux/build_linux.dart
index 1271e38..0e6fd7e 100644
--- a/packages/flutter_tools/lib/src/linux/build_linux.dart
+++ b/packages/flutter_tools/lib/src/linux/build_linux.dart
@@ -13,7 +13,7 @@
 import '../convert.dart';
 import '../globals.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 
 /// Builds the Linux project through the Makefile.
 Future<void> buildLinux(LinuxProject linuxProject, BuildInfo buildInfo, {String target = 'lib/main.dart'}) async {
diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart
index 2ac3b54..2b45525 100644
--- a/packages/flutter_tools/lib/src/macos/build_macos.dart
+++ b/packages/flutter_tools/lib/src/macos/build_macos.dart
@@ -12,7 +12,7 @@
 import '../globals.dart';
 import '../ios/xcodeproj.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 
 import 'cocoapod_utils.dart';
 
diff --git a/packages/flutter_tools/lib/src/reporting/crash_reporting.dart b/packages/flutter_tools/lib/src/reporting/crash_reporting.dart
index 5ab60f6..e7d0042 100644
--- a/packages/flutter_tools/lib/src/reporting/crash_reporting.dart
+++ b/packages/flutter_tools/lib/src/reporting/crash_reporting.dart
@@ -2,17 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
-
-import 'package:http/http.dart' as http;
-import 'package:meta/meta.dart';
-
-import '../base/io.dart';
-import '../base/os.dart';
-import '../base/platform.dart';
-import '../globals.dart';
-
-import 'usage.dart';
+part of reporting;
 
 /// Tells crash backend that the error is from the Flutter CLI.
 const String _kProductId = 'Flutter_Tools';
diff --git a/packages/flutter_tools/lib/src/reporting/disabled_usage.dart b/packages/flutter_tools/lib/src/reporting/disabled_usage.dart
index 68ad76a..2e326da 100644
--- a/packages/flutter_tools/lib/src/reporting/disabled_usage.dart
+++ b/packages/flutter_tools/lib/src/reporting/disabled_usage.dart
@@ -2,9 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
-
-import 'usage.dart';
+part of reporting;
 
 class DisabledUsage implements Usage {
   @override
diff --git a/packages/flutter_tools/lib/src/reporting/events.dart b/packages/flutter_tools/lib/src/reporting/events.dart
new file mode 100644
index 0000000..515314e
--- /dev/null
+++ b/packages/flutter_tools/lib/src/reporting/events.dart
@@ -0,0 +1,137 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+part of reporting;
+
+/// A generic usage even that does not involve custom dimensions.
+///
+/// If sending values for custom dimensions is required, extend this class as
+/// below.
+class UsageEvent {
+  UsageEvent(this.category, this.parameter);
+
+  final String category;
+  final String parameter;
+
+  void send() {
+    flutterUsage.sendEvent(category, parameter);
+  }
+}
+
+/// A usage event related to hot reload/restart.
+///
+/// On a successful hot reload, we collect stats that help understand scale of
+/// the update. For example, [syncedLibraryCount]/[finalLibraryCount] indicates
+/// how many libraries were affected by the hot reload request. Relation of
+/// [invalidatedSourcesCount] to [syncedLibraryCount] should help understand
+/// sync/transfer "overhead" of updating this number of source files.
+class HotEvent extends UsageEvent {
+  HotEvent(String parameter, {
+    @required this.targetPlatform,
+    @required this.sdkName,
+    @required this.emulator,
+    @required this.fullRestart,
+    this.reason,
+    this.finalLibraryCount,
+    this.syncedLibraryCount,
+    this.syncedClassesCount,
+    this.syncedProceduresCount,
+    this.syncedBytes,
+    this.invalidatedSourcesCount,
+    this.transferTimeInMs,
+    this.overallTimeInMs,
+  }) : super('hot', parameter);
+
+  final String reason;
+  final String targetPlatform;
+  final String sdkName;
+  final bool emulator;
+  final bool fullRestart;
+  final int finalLibraryCount;
+  final int syncedLibraryCount;
+  final int syncedClassesCount;
+  final int syncedProceduresCount;
+  final int syncedBytes;
+  final int invalidatedSourcesCount;
+  final int transferTimeInMs;
+  final int overallTimeInMs;
+
+  @override
+  void send() {
+    final Map<String, String> parameters = _useCdKeys(<CustomDimensions, String>{
+      CustomDimensions.hotEventTargetPlatform: targetPlatform,
+      CustomDimensions.hotEventSdkName: sdkName,
+      CustomDimensions.hotEventEmulator: emulator.toString(),
+      CustomDimensions.hotEventFullRestart: fullRestart.toString(),
+      if (reason != null)
+        CustomDimensions.hotEventReason: reason,
+      if (finalLibraryCount != null)
+        CustomDimensions.hotEventFinalLibraryCount: finalLibraryCount.toString(),
+      if (syncedLibraryCount != null)
+        CustomDimensions.hotEventSyncedLibraryCount: syncedLibraryCount.toString(),
+      if (syncedClassesCount != null)
+        CustomDimensions.hotEventSyncedClassesCount: syncedClassesCount.toString(),
+      if (syncedProceduresCount != null)
+        CustomDimensions.hotEventSyncedProceduresCount: syncedProceduresCount.toString(),
+      if (syncedBytes != null)
+        CustomDimensions.hotEventSyncedBytes: syncedBytes.toString(),
+      if (invalidatedSourcesCount != null)
+        CustomDimensions.hotEventInvalidatedSourcesCount: invalidatedSourcesCount.toString(),
+      if (transferTimeInMs != null)
+        CustomDimensions.hotEventTransferTimeInMs: transferTimeInMs.toString(),
+      if (overallTimeInMs != null)
+        CustomDimensions.hotEventOverallTimeInMs: overallTimeInMs.toString(),
+    });
+    flutterUsage.sendEvent(category, parameter, parameters: parameters);
+  }
+}
+
+/// An event that reports the result of a [DoctorValidator]
+class DoctorResultEvent extends UsageEvent {
+  DoctorResultEvent({
+    @required DoctorValidator validator,
+    @required ValidationResult result
+  }) : super('doctorResult.${validator.runtimeType.toString()}',
+             result.typeStr);
+  // TODO(zra): Override send() to detect a GroupedValidator and send separate
+  // events for each sub-validator.
+}
+
+/// An event that reports success or failure of a pub get.
+class PubGetEvent extends UsageEvent {
+  PubGetEvent({
+    @required bool success,
+  }) : super('packages-pub-get', success ? 'success' : 'failure');
+}
+
+/// An event that reports something about a build.
+class BuildEvent extends UsageEvent {
+  BuildEvent(String parameter, {
+    this.command,
+    this.settings,
+  }) : super(
+    'build' +
+      (FlutterCommand.current == null ? '' : '-${FlutterCommand.current.name}'),
+    parameter);
+
+  final String command;
+  final String settings;
+
+  @override
+  void send() {
+    final Map<String, String> parameters = _useCdKeys(<CustomDimensions, String>{
+      if (command != null)
+        CustomDimensions.buildEventCommand: command,
+      if (settings != null)
+        CustomDimensions.buildEventSettings: settings,
+    });
+    flutterUsage.sendEvent(category, parameter, parameters: parameters);
+  }
+}
+
+/// An event that reports the result of a top-level command.
+class CommandResultEvent extends UsageEvent {
+  CommandResultEvent(String commandPath, FlutterCommandResult result)
+      : super(commandPath, result?.toString() ?? 'unspecified');
+}
diff --git a/packages/flutter_tools/lib/src/reporting/reporting.dart b/packages/flutter_tools/lib/src/reporting/reporting.dart
new file mode 100644
index 0000000..759f808
--- /dev/null
+++ b/packages/flutter_tools/lib/src/reporting/reporting.dart
@@ -0,0 +1,30 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library reporting;
+
+import 'dart:async';
+
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+import 'package:usage/usage_io.dart';
+
+import '../base/config.dart';
+import '../base/context.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/os.dart';
+import '../base/platform.dart';
+import '../base/time.dart';
+import '../base/utils.dart';
+import '../doctor.dart';
+import '../features.dart';
+import '../globals.dart';
+import '../runner/flutter_command.dart';
+import '../version.dart';
+
+part 'crash_reporting.dart';
+part 'disabled_usage.dart';
+part 'events.dart';
+part 'usage.dart';
diff --git a/packages/flutter_tools/lib/src/reporting/usage.dart b/packages/flutter_tools/lib/src/reporting/usage.dart
index 57d47a6..5a5eed8 100644
--- a/packages/flutter_tools/lib/src/reporting/usage.dart
+++ b/packages/flutter_tools/lib/src/reporting/usage.dart
@@ -2,78 +2,146 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
-
-import 'package:meta/meta.dart';
-import 'package:usage/usage_io.dart';
-
-import '../base/config.dart';
-import '../base/context.dart';
-import '../base/file_system.dart';
-import '../base/os.dart';
-import '../base/platform.dart';
-import '../base/time.dart';
-import '../base/utils.dart';
-import '../features.dart';
-import '../globals.dart';
-import '../version.dart';
+part of reporting;
 
 const String _kFlutterUA = 'UA-67589403-6';
 
-// Attached to all `Usage.sendCommand` and `Usage.sendEvent`.
-const String _kLocalTimeParameter = 'cd33';
+/// The collection of custom dimensions understood by the analytics backend.
+/// When adding to this list, first ensure that the custom dimension is
+/// defined in the backend, or will be defined shortly after the relevent PR
+/// lands.
+enum CustomDimensions {
+  sessionHostOsDetails,  // cd1
+  sessionChannelName,  // cd2
+  commandRunIsEmulator, // cd3
+  commandRunTargetName, // cd4
+  hotEventReason,  // cd5
+  hotEventFinalLibraryCount,  // cd6
+  hotEventSyncedLibraryCount,  // cd7
+  hotEventSyncedClassesCount,  // cd8
+  hotEventSyncedProceduresCount,  // cd9
+  hotEventSyncedBytes,  // cd10
+  hotEventInvalidatedSourcesCount,  // cd11
+  hotEventTransferTimeInMs,  // cd12
+  hotEventOverallTimeInMs,  // cd13
+  commandRunProjectType,  // cd14
+  commandRunProjectHostLanguage,  // cd15
+  commandCreateAndroidLanguage,  // cd16
+  commandCreateIosLanguage,  // cd17
+  commandRunProjectModule,  // cd18
+  commandCreateProjectType,  // cd19
+  commandPackagesNumberPlugins,  // cd20
+  commandPackagesProjectModule,  // cd21
+  commandRunTargetOsVersion,  // cd22
+  commandRunModeName,  // cd23
+  commandBuildBundleTargetPlatform,  // cd24
+  commandBuildBundleIsModule,  // cd25
+  commandResult,  // cd26
+  hotEventTargetPlatform,  // cd27
+  hotEventSdkName,  // cd28
+  hotEventEmulator,  // cd29
+  hotEventFullRestart,  // cd30
+  commandHasTerminal,  // cd31
+  enabledFlutterFeatures,  // cd32
+  localTime,  // cd33
+  commandBuildAarTargetPlatform,  // cd34
+  commandBuildAarProjectType,  // cd35
+  buildEventCommand,  // cd36
+  buildEventSettings,  // cd37
+}
 
-const String kSessionHostOsDetails = 'cd1';
-const String kSessionChannelName = 'cd2';
+String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}';
 
-const String kEventReloadReasonParameterName = 'cd5';
-const String kEventReloadFinalLibraryCount = 'cd6';
-const String kEventReloadSyncedLibraryCount = 'cd7';
-const String kEventReloadSyncedClassesCount = 'cd8';
-const String kEventReloadSyncedProceduresCount = 'cd9';
-const String kEventReloadSyncedBytes = 'cd10';
-const String kEventReloadInvalidatedSourcesCount = 'cd11';
-const String kEventReloadTransferTimeInMs = 'cd12';
-const String kEventReloadOverallTimeInMs = 'cd13';
-
-const String kCommandRunIsEmulator = 'cd3';
-const String kCommandRunTargetName = 'cd4';
-const String kCommandRunProjectType = 'cd14';
-const String kCommandRunProjectHostLanguage = 'cd15';
-const String kCommandRunProjectModule = 'cd18';
-const String kCommandRunTargetOsVersion = 'cd22';
-const String kCommandRunModeName = 'cd23';
-
-const String kCommandCreateAndroidLanguage = 'cd16';
-const String kCommandCreateIosLanguage = 'cd17';
-const String kCommandCreateProjectType = 'cd19';
-
-const String kCommandPackagesNumberPlugins = 'cd20';
-const String kCommandPackagesProjectModule = 'cd21';
-
-const String kCommandBuildBundleTargetPlatform = 'cd24';
-const String kCommandBuildBundleIsModule = 'cd25';
-
-const String kCommandResult = 'cd26';
-const String kCommandHasTerminal = 'cd31';
-
-const String kCommandBuildAarTargetPlatform = 'cd34';
-const String kCommandBuildAarProjectType = 'cd35';
-
-const String reloadExceptionTargetPlatform = 'cd27';
-const String reloadExceptionSdkName = 'cd28';
-const String reloadExceptionEmulator = 'cd29';
-const String reloadExceptionFullRestart = 'cd30';
-
-const String enabledFlutterFeatures = 'cd32';
-// Next ID: cd36
+Map<String, String> _useCdKeys(Map<CustomDimensions, String> parameters) {
+  return parameters.map((CustomDimensions k, String v) =>
+      MapEntry<String, String>(cdKey(k), v));
+}
 
 Usage get flutterUsage => Usage.instance;
 
-class Usage {
+abstract class Usage {
+  factory Usage({
+    String settingsName = 'flutter',
+    String versionOverride,
+    String configDirOverride
+  }) => _UsageImpl(settingsName: settingsName,
+                   versionOverride: versionOverride,
+                   configDirOverride: configDirOverride);
+
+  /// Returns [Usage] active in the current app context.
+  static Usage get instance => context.get<Usage>();
+
+  /// Uses the global [Usage] instance to send a 'command' to analytics.
+  static void command(String command, {
+    Map<CustomDimensions, String> parameters,
+  }) => flutterUsage.sendCommand(command, parameters: _useCdKeys(parameters));
+
+  /// Whether this is the first run of the tool.
+  bool get isFirstRun;
+
+  /// Whether analytics reporting should be supressed.
+  bool get suppressAnalytics;
+
+  /// Suppress analytics for this session.
+  set suppressAnalytics(bool value);
+
+  /// Whether analytics reporting is enabled.
+  bool get enabled;
+
+  /// Enable or disable reporting analytics.
+  set enabled(bool value);
+
+  /// A stable randomly generated UUID used to deduplicate multiple identical
+  /// reports coming from the same computer.
+  String get clientId;
+
+  /// Sends a 'command' to the underlying analytics implementation.
+  ///
+  /// Note that using [command] above is preferred to ensure that the parameter
+  /// keys are well-defined in [CustomDimensions] above.
+  void sendCommand(String command, {
+    Map<String, String> parameters
+  });
+
+  /// Sends an 'event' to the underlying analytics implementation.
+  ///
+  /// Note that this method should not be used directly, instead see the
+  /// event types defined in this directory in events.dart.
+  @visibleForOverriding
+  @visibleForTesting
+  void sendEvent(String category, String parameter, {
+    Map<String, String> parameters
+  });
+
+  /// Sends timing information to the underlying analytics implementation.
+  void sendTiming(String category, String variableName, Duration duration, {
+    String label
+  });
+
+  /// Sends an exception to the underlying analytics implementation.
+  void sendException(dynamic exception);
+
+  /// Fires whenever analytics data is sent over the network.
+  @visibleForTesting
+  Stream<Map<String, dynamic>> get onSend;
+
+  /// Returns when the last analytics event has been sent, or after a fixed
+  /// (short) delay, whichever is less.
+  Future<void> ensureAnalyticsSent();
+
+  /// Prints a welcome message that informs the tool user about the collection
+  /// of anonymous usage information.
+  void printWelcome();
+}
+
+class _UsageImpl implements Usage {
   /// Create a new Usage instance; [versionOverride] and [configDirOverride] are
   /// used for testing.
-  Usage({ String settingsName = 'flutter', String versionOverride, String configDirOverride}) {
+  _UsageImpl({
+    String settingsName = 'flutter',
+    String versionOverride,
+    String configDirOverride
+  }) {
     final FlutterVersion flutterVersion = FlutterVersion.instance;
     final String version = versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true);
 
@@ -90,9 +158,10 @@
         LogToFileAnalytics(logFilePath);
 
     // Report a more detailed OS version string than package:usage does by default.
-    _analytics.setSessionValue(kSessionHostOsDetails, os.name);
+    _analytics.setSessionValue(cdKey(CustomDimensions.sessionHostOsDetails), os.name);
     // Send the branch name as the "channel".
-    _analytics.setSessionValue(kSessionChannelName, flutterVersion.getBranchName(redactUnknownBranches: true));
+    _analytics.setSessionValue(cdKey(CustomDimensions.sessionChannelName),
+                               flutterVersion.getBranchName(redactUnknownBranches: true));
     // For each flutter experimental feature, record a session value in a comma
     // separated list.
     final String enabledFeatures = allFeatures
@@ -102,7 +171,7 @@
         })
         .map((Feature feature) => feature.configSetting)
         .join(',');
-    _analytics.setSessionValue(enabledFlutterFeatures, enabledFeatures);
+    _analytics.setSessionValue(cdKey(CustomDimensions.enabledFlutterFeatures), enabledFeatures);
 
     // Record the host as the application installer ID - the context that flutter_tools is running in.
     if (platform.environment.containsKey('FLUTTER_HOST')) {
@@ -119,34 +188,34 @@
     }
   }
 
-  /// Returns [Usage] active in the current app context.
-  static Usage get instance => context.get<Usage>();
-
   Analytics _analytics;
 
   bool _printedWelcome = false;
   bool _suppressAnalytics = false;
 
+  @override
   bool get isFirstRun => _analytics.firstRun;
 
-  bool get enabled => _analytics.enabled;
-
+  @override
   bool get suppressAnalytics => _suppressAnalytics || _analytics.firstRun;
 
-  /// Suppress analytics for this session.
+  @override
   set suppressAnalytics(bool value) {
     _suppressAnalytics = value;
   }
 
-  /// Enable or disable reporting analytics.
+  @override
+  bool get enabled => _analytics.enabled;
+
+  @override
   set enabled(bool value) {
     _analytics.enabled = value;
   }
 
-  /// A stable randomly generated UUID used to deduplicate multiple identical
-  /// reports coming from the same computer.
+  @override
   String get clientId => _analytics.clientId;
 
+  @override
   void sendCommand(String command, { Map<String, String> parameters }) {
     if (suppressAnalytics) {
       return;
@@ -154,11 +223,12 @@
 
     final Map<String, String> paramsWithLocalTime = <String, String>{
       ...?parameters,
-      _kLocalTimeParameter: systemClock.now().toString(),
+      cdKey(CustomDimensions.localTime): systemClock.now().toString(),
     };
     _analytics.sendScreenView(command, parameters: paramsWithLocalTime);
   }
 
+  @override
   void sendEvent(
     String category,
     String parameter, {
@@ -170,12 +240,13 @@
 
     final Map<String, String> paramsWithLocalTime = <String, String>{
       ...?parameters,
-      _kLocalTimeParameter: systemClock.now().toString(),
+      cdKey(CustomDimensions.localTime): systemClock.now().toString(),
     };
 
     _analytics.sendEvent(category, parameter, parameters: paramsWithLocalTime);
   }
 
+  @override
   void sendTiming(
     String category,
     String variableName,
@@ -193,6 +264,7 @@
     );
   }
 
+  @override
   void sendException(dynamic exception) {
     if (suppressAnalytics) {
       return;
@@ -200,12 +272,10 @@
     _analytics.sendException(exception.runtimeType.toString());
   }
 
-  /// Fires whenever analytics data is sent over the network.
-  @visibleForTesting
+  @override
   Stream<Map<String, dynamic>> get onSend => _analytics.onSend;
 
-  /// Returns when the last analytics event has been sent, or after a fixed
-  /// (short) delay, whichever is less.
+  @override
   Future<void> ensureAnalyticsSent() async {
     // TODO(devoncarew): This may delay tool exit and could cause some analytics
     // events to not be reported. Perhaps we could send the analytics pings
@@ -213,6 +283,7 @@
     await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
   }
 
+  @override
   void printWelcome() {
     // This gets called if it's the first run by the selected command, if any,
     // and on exit, in case there was no command.
@@ -255,7 +326,9 @@
   final Map<String, String> _sessionValues = <String, String>{};
 
   @override
-  Future<void> sendScreenView(String viewName, {Map<String, String> parameters}) {
+  Future<void> sendScreenView(String viewName, {
+    Map<String, String> parameters,
+  }) {
     parameters ??= <String, String>{};
     parameters['viewName'] = viewName;
     parameters.addAll(_sessionValues);
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 0e6eb3f..08a4956 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -21,7 +21,7 @@
 import 'devfs.dart';
 import 'device.dart';
 import 'globals.dart';
-import 'reporting/usage.dart';
+import 'reporting/reporting.dart';
 import 'resident_runner.dart';
 import 'vmservice.dart';
 
@@ -367,15 +367,12 @@
           futures.add(view.flushUIThreadTasks());
       await Future.wait(futures);
     }
-
   }
 
-  Future<OperationResult> _restartFromSources({ String reason, bool benchmarkMode = false }) async {
-    final Map<String, String> analyticsParameters =
-      reason == null
-        ? null
-        : <String, String>{kEventReloadReasonParameterName: reason};
-
+  Future<OperationResult> _restartFromSources({
+    String reason,
+    bool benchmarkMode = false
+  }) async {
     if (!_isPaused()) {
       printTrace('Refreshing active FlutterViews before restarting.');
       await refreshViews();
@@ -387,8 +384,9 @@
     final UpdateFSReport updatedDevFS = await _updateDevFS(fullRestart: true);
     if (!updatedDevFS.success) {
       for (FlutterDevice device in flutterDevices) {
-        if (device.generator != null)
+        if (device.generator != null) {
           await device.generator.reject();
+        }
       }
       return OperationResult(1, 'DevFS synchronization failed');
     }
@@ -396,30 +394,32 @@
     for (FlutterDevice device in flutterDevices) {
       // VM must have accepted the kernel binary, there will be no reload
       // report, so we let incremental compiler know that source code was accepted.
-      if (device.generator != null)
+      if (device.generator != null) {
         device.generator.accept();
+      }
     }
     // Check if the isolate is paused and resume it.
     final List<Future<void>> futures = <Future<void>>[];
     for (FlutterDevice device in flutterDevices) {
       for (FlutterView view in device.views) {
-        if (view.uiIsolate != null) {
-          // Reload the isolate.
-          final Completer<void> completer = Completer<void>();
-          futures.add(completer.future);
-          unawaited(view.uiIsolate.reload().then(
-            (ServiceObject _) {
-              final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
-              if ((pauseEvent != null) && pauseEvent.isPauseEvent) {
-                // Resume the isolate so that it can be killed by the embedder.
-                return view.uiIsolate.resume();
-              }
-              return null;
-            },
-          ).whenComplete(
-            () { completer.complete(null); },
-          ));
+        if (view.uiIsolate == null) {
+          continue;
         }
+        // Reload the isolate.
+        final Completer<void> completer = Completer<void>();
+        futures.add(completer.future);
+        unawaited(view.uiIsolate.reload().then(
+          (ServiceObject _) {
+            final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
+            if ((pauseEvent != null) && pauseEvent.isPauseEvent) {
+              // Resume the isolate so that it can be killed by the embedder.
+              return view.uiIsolate.resume();
+            }
+            return null;
+          },
+        ).whenComplete(
+          () { completer.complete(null); },
+        ));
       }
     }
     await Future.wait(futures);
@@ -432,7 +432,8 @@
     _runningFromSnapshot = false;
     _addBenchmarkData('hotRestartMillisecondsToFrame',
         restartTimer.elapsed.inMilliseconds);
-    flutterUsage.sendEvent('hot', 'restart', parameters: analyticsParameters);
+
+    // Send timing analytics.
     flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed);
 
     // In benchmark mode, make sure all stream notifications have finished.
@@ -499,81 +500,156 @@
   bool get supportsRestart => true;
 
   @override
-  Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason, bool benchmarkMode = false }) async {
+  Future<OperationResult> restart({
+    bool fullRestart = false,
+    bool pauseAfterRestart = false,
+    String reason,
+    bool benchmarkMode = false
+  }) async {
+    String targetPlatform;
+    String sdkName;
+    bool emulator;
+    if (flutterDevices.length == 1) {
+      final Device device = flutterDevices.first.device;
+      targetPlatform = getNameForTargetPlatform(await device.targetPlatform);
+      sdkName = await device.sdkNameAndVersion;
+      emulator = await device.isLocalEmulator;
+    } else if (flutterDevices.length > 1) {
+      targetPlatform = 'multiple';
+      sdkName = 'multiple';
+      emulator = false;
+    } else {
+      targetPlatform = 'unknown';
+      sdkName = 'unknown';
+      emulator = false;
+    }
     final Stopwatch timer = Stopwatch()..start();
     if (fullRestart) {
-      if (!canHotRestart) {
-        return OperationResult(1, 'hotRestart not supported');
-      }
-      final Status status = logger.startProgress(
-        'Performing hot restart...',
-        timeout: timeoutConfiguration.fastOperation,
-        progressId: 'hot.restart',
+      final OperationResult result = await _fullRestartHelper(
+        targetPlatform: targetPlatform,
+        sdkName: sdkName,
+        emulator: emulator,
+        reason: reason,
+        benchmarkMode: benchmarkMode,
       );
-      try {
-        if (!(await hotRunnerConfig.setupHotRestart()))
-          return OperationResult(1, 'setupHotRestart failed');
-        final OperationResult result = await _restartFromSources(reason: reason, benchmarkMode: benchmarkMode,);
-        if (!result.isOk)
-          return result;
-      } on rpc.RpcException {
-        await _measureJsonRpcException(flutterDevices, fullRestart);
-        return OperationResult(1, 'hot restart failed to complete', fatal: true);
-      } finally {
-        status.cancel();
-      }
       printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
-      return OperationResult.ok;
-    } else {
-      final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
-      final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
-      Status status = logger.startProgress(
-        '$progressPrefix hot reload...',
-        timeout: timeoutConfiguration.fastOperation,
-        progressId: 'hot.reload',
-      );
-      OperationResult result;
-      bool showTime = true;
-      try {
-        result = await _reloadSources(
-          pause: pauseAfterRestart,
-          reason: reason,
-          onSlow: (String message) {
-            status?.cancel();
-            status = logger.startProgress(
-              message,
-              timeout: timeoutConfiguration.slowOperation,
-              progressId: 'hot.reload',
-            );
-            showTime = false;
-          },
-        );
-      } on rpc.RpcException {
-        await _measureJsonRpcException(flutterDevices, fullRestart);
-        return OperationResult(1, 'hot reload failed to complete', fatal: true);
-      } finally {
-        status.cancel();
-      }
-      if (result.isOk) {
-        if (showTime) {
-          printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.');
-        } else {
-          printStatus('${result.message}.');
-        }
-      }
       return result;
     }
+    final OperationResult result = await _hotReloadHelper(
+      targetPlatform: targetPlatform,
+      sdkName: sdkName,
+      emulator: emulator,
+      reason: reason,
+      pauseAfterRestart: pauseAfterRestart,
+    );
+    if (result.isOk) {
+      final String elapsed = getElapsedAsMilliseconds(timer.elapsed);
+      printStatus('${result.message} in $elapsed.');
+    }
+    return result;
   }
 
-  Future<OperationResult> _reloadSources({ bool pause = false, String reason, void Function(String message) onSlow }) async {
-    final Map<String, String> analyticsParameters = <String, String>{};
-    if (reason != null) {
-      analyticsParameters[kEventReloadReasonParameterName] = reason;
+  Future<OperationResult> _fullRestartHelper({
+    String targetPlatform,
+    String sdkName,
+    bool emulator,
+    String reason,
+    bool benchmarkMode,
+  }) async {
+    if (!canHotRestart) {
+      return OperationResult(1, 'hotRestart not supported');
     }
+    final Status status = logger.startProgress(
+      'Performing hot restart...',
+      timeout: timeoutConfiguration.fastOperation,
+      progressId: 'hot.restart',
+    );
+    OperationResult result;
+    String restartEvent = 'restart';
+    try {
+      if (!(await hotRunnerConfig.setupHotRestart())) {
+        return OperationResult(1, 'setupHotRestart failed');
+      }
+      result = await _restartFromSources(
+        reason: reason,
+        benchmarkMode: benchmarkMode,
+      );
+      if (!result.isOk) {
+        restartEvent = 'restart-failed';
+      }
+    } on rpc.RpcException {
+      restartEvent = 'exception';
+      return OperationResult(1, 'hot restart failed to complete', fatal: true);
+    } finally {
+      HotEvent(restartEvent,
+        targetPlatform: targetPlatform,
+        sdkName: sdkName,
+        emulator: emulator,
+        fullRestart: true,
+        reason: reason).send();
+      status.cancel();
+    }
+    return result;
+  }
+
+  Future<OperationResult> _hotReloadHelper({
+    String targetPlatform,
+    String sdkName,
+    bool emulator,
+    String reason,
+    bool pauseAfterRestart = false,
+  }) async {
+    final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
+    final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
+    Status status = logger.startProgress(
+      '$progressPrefix hot reload...',
+      timeout: timeoutConfiguration.fastOperation,
+      progressId: 'hot.reload',
+    );
+    OperationResult result;
+    try {
+      result = await _reloadSources(
+        targetPlatform: targetPlatform,
+        sdkName: sdkName,
+        emulator: emulator,
+        pause: pauseAfterRestart,
+        reason: reason,
+        onSlow: (String message) {
+          status?.cancel();
+          status = logger.startProgress(
+            message,
+            timeout: timeoutConfiguration.slowOperation,
+            progressId: 'hot.reload',
+          );
+        },
+      );
+    } on rpc.RpcException {
+      HotEvent('exception',
+        targetPlatform: targetPlatform,
+        sdkName: sdkName,
+        emulator: emulator,
+        fullRestart: false,
+        reason: reason).send();
+      return OperationResult(1, 'hot reload failed to complete', fatal: true);
+    } finally {
+      status.cancel();
+    }
+    return result;
+  }
+
+  Future<OperationResult> _reloadSources({
+    String targetPlatform,
+    String sdkName,
+    bool emulator,
+    bool pause = false,
+    String reason,
+    void Function(String message) onSlow
+  }) async {
     for (FlutterDevice device in flutterDevices) {
       for (FlutterView view in device.views) {
-        if (view.uiIsolate == null)
+        if (view.uiIsolate == null) {
           throw 'Application isolate not found';
+        }
       }
     }
 
@@ -595,10 +671,12 @@
     final UpdateFSReport updatedDevFS = await _updateDevFS();
     // Record time it took to synchronize to DevFS.
     _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
-    if (!updatedDevFS.success)
+    if (!updatedDevFS.success) {
       return OperationResult(1, 'DevFS synchronization failed');
+    }
     String reloadMessage;
     final Stopwatch vmReloadTimer = Stopwatch()..start();
+    Map<String, dynamic> firstReloadDetails;
     try {
       final String entryPath = fs.path.relative(
         getReloadPath(fullRestart: false),
@@ -635,27 +713,23 @@
         final Map<String, dynamic> reloadReport = report.reports[0];
         if (!validateReloadReport(reloadReport)) {
           // Reload failed.
-          flutterUsage.sendEvent('hot', 'reload-reject');
+          HotEvent('reload-reject',
+            targetPlatform: targetPlatform,
+            sdkName: sdkName,
+            emulator: emulator,
+            fullRestart: false,
+            reason: reason,
+          ).send();
           return OperationResult(1, 'Reload rejected');
-        } else {
-          // Collect stats that help understand scale of update for this hot reload request.
-          // For example, [syncedLibraryCount]/[finalLibraryCount] indicates how
-          // many libraries were affected by the hot reload request.
-          // Relation of [invalidatedSourcesCount] to [syncedLibraryCount] should help
-          // understand sync/transfer "overhead" of updating this number of source files.
-          final Map<String, dynamic> details = reloadReport['details'];
-          analyticsParameters[kEventReloadFinalLibraryCount] = "${details['finalLibraryCount']}";
-          analyticsParameters[kEventReloadSyncedLibraryCount] = "${details['receivedLibraryCount']}";
-          analyticsParameters[kEventReloadSyncedClassesCount] = "${details['receivedClassesCount']}";
-          analyticsParameters[kEventReloadSyncedProceduresCount] = "${details['receivedProceduresCount']}";
-          analyticsParameters[kEventReloadSyncedBytes] = '${updatedDevFS.syncedBytes}';
-          analyticsParameters[kEventReloadInvalidatedSourcesCount] = '${updatedDevFS.invalidatedSourcesCount}';
-          analyticsParameters[kEventReloadTransferTimeInMs] = '${devFSTimer.elapsed.inMilliseconds}';
-          final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
-          final int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
-          printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
-          reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
         }
+        // Collect stats only from the first device. If/when run -d all is
+        // refactored, we'll probably need to send one hot reload/restart event
+        // per device to analytics.
+        firstReloadDetails ??= reloadReport['details'];
+        final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
+        final int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
+        printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
+        reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
       }
     } on Map<String, dynamic> catch (error, stackTrace) {
       printTrace('Hot reload failed: $error\n$stackTrace');
@@ -666,7 +740,13 @@
                        'the source code. Please address the error and then use "R" to '
                        'restart the app.\n'
                        '$errorMessage (error code: $errorCode)';
-        flutterUsage.sendEvent('hot', 'reload-barred');
+        HotEvent('reload-barred',
+          targetPlatform: targetPlatform,
+          sdkName: sdkName,
+          emulator: emulator,
+          fullRestart: false,
+          reason: reason,
+        ).send();
         return OperationResult(errorCode, errorMessage);
       }
       return OperationResult(errorCode, '$errorMessage (error code: $errorCode)');
@@ -783,8 +863,26 @@
     final Duration reloadDuration = reloadTimer.elapsed;
     final int reloadInMs = reloadDuration.inMilliseconds;
 
-    analyticsParameters[kEventReloadOverallTimeInMs] = '$reloadInMs';
-    flutterUsage.sendEvent('hot', 'reload', parameters: analyticsParameters);
+    // Collect stats that help understand scale of update for this hot reload request.
+    // For example, [syncedLibraryCount]/[finalLibraryCount] indicates how
+    // many libraries were affected by the hot reload request.
+    // Relation of [invalidatedSourcesCount] to [syncedLibraryCount] should help
+    // understand sync/transfer "overhead" of updating this number of source files.
+    HotEvent('reload',
+      targetPlatform: targetPlatform,
+      sdkName: sdkName,
+      emulator: emulator,
+      fullRestart: false,
+      reason: reason,
+      overallTimeInMs: reloadInMs,
+      finalLibraryCount: firstReloadDetails['finalLibraryCount'],
+      syncedLibraryCount: firstReloadDetails['receivedLibraryCount'],
+      syncedClassesCount: firstReloadDetails['receivedClassesCount'],
+      syncedProceduresCount: firstReloadDetails['receivedProceduresCount'],
+      syncedBytes: updatedDevFS.syncedBytes,
+      invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
+      transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
+    ).send();
 
     if (shouldReportReloadTime) {
       printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.');
@@ -952,32 +1050,3 @@
     return invalidatedFiles;
   }
 }
-
-// This is an error case we would like to know more about.
-Future<void> _measureJsonRpcException(List<FlutterDevice> flutterDevices, bool fullRestart) async {
-    String targetPlatform;
-    String deviceSdk;
-    bool emulator;
-    if (flutterDevices.length == 1) {
-      final Device device = flutterDevices.first.device;
-      targetPlatform = getNameForTargetPlatform(await device.targetPlatform);
-      deviceSdk = await device.sdkNameAndVersion;
-      emulator = await device.isLocalEmulator;
-    } else if (flutterDevices.length > 1) {
-      targetPlatform = 'multiple';
-      deviceSdk = 'multiple';
-      emulator = false;
-    } else {
-      targetPlatform = 'unknown';
-      deviceSdk = 'unknown';
-      emulator = false;
-    }
-    flutterUsage.sendEvent('hot', 'exception',
-      parameters: <String, String>{
-        reloadExceptionTargetPlatform: targetPlatform,
-        reloadExceptionSdkName: deviceSdk,
-        reloadExceptionEmulator: emulator.toString(),
-        reloadExceptionFullRestart: fullRestart.toString(),
-      },
-    );
-}
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 837e787..dd23247 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -28,7 +28,7 @@
 import '../features.dart';
 import '../globals.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 import 'flutter_command_runner.dart';
 
 export '../cache.dart' show DevelopmentArtifact;
@@ -63,6 +63,21 @@
   /// [FlutterCommand] will automatically measure and report the command's
   /// complete time if not overridden.
   final DateTime endTimeOverride;
+
+  @override
+  String toString() {
+    switch (exitStatus) {
+      case ExitStatus.success:
+        return 'success';
+      case ExitStatus.warning:
+        return 'warning';
+      case ExitStatus.fail:
+        return 'fail';
+      default:
+        assert(false);
+        return null;
+    }
+  }
 }
 
 /// Common flutter command line options.
@@ -366,7 +381,8 @@
   }
 
   /// Additional usage values to be sent with the usage ping.
-  Future<Map<String, String>> get usageValues async => const <String, String>{};
+  Future<Map<CustomDimensions, String>> get usageValues async =>
+      const <CustomDimensions, String>{};
 
   /// Runs this command.
   ///
@@ -382,8 +398,9 @@
       name: 'command',
       overrides: <Type, Generator>{FlutterCommand: () => this},
       body: () async {
-        if (flutterUsage.isFirstRun)
+        if (flutterUsage.isFirstRun) {
           flutterUsage.printWelcome();
+        }
         final String commandPath = await usagePath;
         FlutterCommandResult commandResult;
         try {
@@ -411,21 +428,7 @@
     }
 
     // Send command result.
-    String result = 'unspecified';
-    if (commandResult != null) {
-      switch (commandResult.exitStatus) {
-        case ExitStatus.success:
-          result = 'success';
-          break;
-        case ExitStatus.warning:
-          result = 'warning';
-          break;
-        case ExitStatus.fail:
-          result = 'fail';
-          break;
-      }
-    }
-    flutterUsage.sendEvent(commandPath, result);
+    CommandResultEvent(commandPath, commandResult).send();
 
     // Send timing.
     final List<String> labels = <String>[
@@ -476,12 +479,12 @@
     setupApplicationPackages();
 
     if (commandPath != null) {
-      final Map<String, String> additionalUsageValues = <String,String>{
-        ...?await usageValues,
-      };
-      additionalUsageValues[kCommandHasTerminal] =
-          io.stdout.hasTerminal ? 'true' : 'false';
-      flutterUsage.sendCommand(commandPath, parameters: additionalUsageValues);
+      final Map<CustomDimensions, String> additionalUsageValues =
+        <CustomDimensions, String>{
+          ...?await usageValues,
+          CustomDimensions.commandHasTerminal: io.stdout.hasTerminal ? 'true' : 'false',
+        };
+      Usage.command(commandPath, parameters: additionalUsageValues);
     }
 
     return await runCommand();
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
index babe49c..0e991b3 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
@@ -30,7 +30,7 @@
 import '../dart/package_map.dart';
 import '../device.dart';
 import '../globals.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 import '../tester/flutter_tester.dart';
 import '../version.dart';
 import '../vmservice.dart';
diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart
index 752bae9..d241998 100644
--- a/packages/flutter_tools/lib/src/web/compile.dart
+++ b/packages/flutter_tools/lib/src/web/compile.dart
@@ -13,7 +13,7 @@
 import '../bundle.dart';
 import '../globals.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 
 /// The [WebCompilationProxy] instance.
 WebCompilationProxy get webCompilationProxy => context.get<WebCompilationProxy>();
diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart
index 1d97288..c61cef5 100644
--- a/packages/flutter_tools/lib/src/windows/build_windows.dart
+++ b/packages/flutter_tools/lib/src/windows/build_windows.dart
@@ -13,7 +13,7 @@
 import '../convert.dart';
 import '../globals.dart';
 import '../project.dart';
-import '../reporting/usage.dart';
+import '../reporting/reporting.dart';
 
 import 'msbuild_utils.dart';
 import 'visual_studio.dart';
diff --git a/packages/flutter_tools/test/general.shard/analytics_test.dart b/packages/flutter_tools/test/general.shard/analytics_test.dart
index d738ab2..0cbda9d 100644
--- a/packages/flutter_tools/test/general.shard/analytics_test.dart
+++ b/packages/flutter_tools/test/general.shard/analytics_test.dart
@@ -4,22 +4,21 @@
 
 import 'package:args/command_runner.dart';
 import 'package:file/memory.dart';
-import 'package:mockito/mockito.dart';
-
 import 'package:flutter_tools/src/base/config.dart';
-import 'package:flutter_tools/src/base/platform.dart';
-import 'package:flutter_tools/src/base/time.dart';
-import 'package:flutter_tools/src/features.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/time.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/build.dart';
 import 'package:flutter_tools/src/commands/config.dart';
 import 'package:flutter_tools/src/commands/doctor.dart';
 import 'package:flutter_tools/src/doctor.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/features.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
 import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
 import 'package:platform/platform.dart';
 
 import '../src/common.dart';
@@ -94,7 +93,8 @@
       final Usage usage = Usage();
       usage.sendCommand('test');
 
-      expect(fs.file('test').readAsStringSync(), contains('$enabledFlutterFeatures: enable-web'));
+      final String featuresKey = cdKey(CustomDimensions.enabledFlutterFeatures);
+      expect(fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web'));
     }, overrides: <Type, Generator>{
       FlutterVersion: () => FlutterVersion(const SystemClock()),
       Config: () => mockFlutterConfig,
@@ -114,7 +114,8 @@
       final Usage usage = Usage();
       usage.sendCommand('test');
 
-      expect(fs.file('test').readAsStringSync(), contains('$enabledFlutterFeatures: enable-web,enable-linux-desktop,enable-macos-desktop'));
+      final String featuresKey = cdKey(CustomDimensions.enabledFlutterFeatures);
+      expect(fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web,enable-linux-desktop,enable-macos-desktop'));
     }, overrides: <Type, Generator>{
       FlutterVersion: () => FlutterVersion(const SystemClock()),
       Config: () => mockFlutterConfig,
diff --git a/packages/flutter_tools/test/general.shard/artifacts_test.dart b/packages/flutter_tools/test/general.shard/artifacts_test.dart
index ee78164..eaa7bf7 100644
--- a/packages/flutter_tools/test/general.shard/artifacts_test.dart
+++ b/packages/flutter_tools/test/general.shard/artifacts_test.dart
@@ -3,11 +3,11 @@
 // found in the LICENSE file.
 
 import 'package:file/memory.dart';
+import 'package:flutter_tools/src/artifacts.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
-import 'package:flutter_tools/src/artifacts.dart';
 
 import '../src/common.dart';
 import '../src/context.dart';
diff --git a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
index d02d764..67cfb5b 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
@@ -7,7 +7,7 @@
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/build_aar.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:mockito/mockito.dart';
 
 import '../../src/common.dart';
@@ -51,7 +51,7 @@
 
       final BuildAarCommand command = await runCommandIn(projectPath);
       expect(await command.usageValues,
-          containsPair(kCommandBuildAarProjectType, 'module'));
+          containsPair(CustomDimensions.commandBuildAarProjectType, 'module'));
 
     }, overrides: <Type, Generator>{
       AarBuilder: () => mockAarBuilder,
@@ -63,7 +63,7 @@
 
       final BuildAarCommand command = await runCommandIn(projectPath);
       expect(await command.usageValues,
-          containsPair(kCommandBuildAarProjectType, 'plugin'));
+          containsPair(CustomDimensions.commandBuildAarProjectType, 'plugin'));
 
     }, overrides: <Type, Generator>{
       AarBuilder: () => mockAarBuilder,
@@ -76,7 +76,7 @@
       final BuildAarCommand command = await runCommandIn(projectPath,
           arguments: <String>['--target-platform=android-arm']);
       expect(await command.usageValues,
-          containsPair(kCommandBuildAarTargetPlatform, 'android-arm'));
+          containsPair(CustomDimensions.commandBuildAarTargetPlatform, 'android-arm'));
 
     }, overrides: <Type, Generator>{
       AarBuilder: () => mockAarBuilder,
diff --git a/packages/flutter_tools/test/general.shard/commands/build_bundle_test.dart b/packages/flutter_tools/test/general.shard/commands/build_bundle_test.dart
index 782ac26..f37dba7 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_bundle_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_bundle_test.dart
@@ -6,11 +6,11 @@
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/bundle.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/build_bundle.dart';
-import 'package:flutter_tools/src/bundle.dart';
 import 'package:flutter_tools/src/features.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:mockito/mockito.dart';
 
 import '../../src/common.dart';
@@ -70,7 +70,7 @@
     final BuildBundleCommand command = await runCommandIn(projectPath);
 
     expect(await command.usageValues,
-        containsPair(kCommandBuildBundleIsModule, 'true'));
+        containsPair(CustomDimensions.commandBuildBundleIsModule, 'true'));
   }, timeout: allowForCreateFlutterProject);
 
   testUsingContext('bundle getUsage indicate that project is not a module', () async {
@@ -80,7 +80,7 @@
     final BuildBundleCommand command = await runCommandIn(projectPath);
 
     expect(await command.usageValues,
-        containsPair(kCommandBuildBundleIsModule, 'false'));
+        containsPair(CustomDimensions.commandBuildBundleIsModule, 'false'));
   }, timeout: allowForCreateFlutterProject);
 
   testUsingContext('bundle getUsage indicate the target platform', () async {
@@ -90,7 +90,7 @@
     final BuildBundleCommand command = await runCommandIn(projectPath);
 
     expect(await command.usageValues,
-        containsPair(kCommandBuildBundleTargetPlatform, 'android-arm'));
+        containsPair(CustomDimensions.commandBuildBundleTargetPlatform, 'android-arm'));
   }, timeout: allowForCreateFlutterProject);
 
   testUsingContext('bundle fails to build for Windows if feature is disabled', () async {
diff --git a/packages/flutter_tools/test/general.shard/commands/create_usage_test.dart b/packages/flutter_tools/test/general.shard/commands/create_usage_test.dart
index 6b80557..a8cc1a7 100644
--- a/packages/flutter_tools/test/general.shard/commands/create_usage_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/create_usage_test.dart
@@ -7,7 +7,7 @@
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/create.dart';
 import 'package:flutter_tools/src/doctor.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 
 import '../../src/common.dart';
 import '../../src/context.dart';
@@ -43,25 +43,35 @@
       final CreateCommand command = CreateCommand();
       final CommandRunner<void> runner = createTestCommandRunner(command);
 
-      await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=module', 'testy']);
-      expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'module'));
+      await runner.run(<String>[
+        'create', '--flutter-root=flutter', '--no-pub', '--template=module', 'testy']);
+      expect(await command.usageValues,
+             containsPair(CustomDimensions.commandCreateProjectType, 'module'));
 
-      await runner.run(<String>['create',  '--flutter-root=flutter', '--no-pub', '--template=app', 'testy']);
-      expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'app'));
+      await runner.run(<String>[
+        'create',  '--flutter-root=flutter', '--no-pub', '--template=app', 'testy']);
+      expect(await command.usageValues,
+             containsPair(CustomDimensions.commandCreateProjectType, 'app'));
 
-      await runner.run(<String>['create',  '--flutter-root=flutter', '--no-pub', '--template=package', 'testy']);
-      expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'package'));
+      await runner.run(<String>[
+        'create',  '--flutter-root=flutter', '--no-pub', '--template=package', 'testy']);
+      expect(await command.usageValues,
+             containsPair(CustomDimensions.commandCreateProjectType, 'package'));
 
-      await runner.run(<String>['create',  '--flutter-root=flutter', '--no-pub', '--template=plugin', 'testy']);
-      expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'plugin'));
+      await runner.run(<String>[
+        'create', '--flutter-root=flutter', '--no-pub', '--template=plugin', 'testy']);
+      expect(await command.usageValues,
+             containsPair(CustomDimensions.commandCreateProjectType, 'plugin'));
     }));
 
     test('set iOS host language type as usage value', () => testbed.run(() async {
       final CreateCommand command = CreateCommand();
       final CommandRunner<void> runner = createTestCommandRunner(command);
 
-      await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=app', 'testy']);
-      expect(await command.usageValues, containsPair(kCommandCreateIosLanguage, 'objc'));
+      await runner.run(<String>[
+        'create', '--flutter-root=flutter', '--no-pub', '--template=app', 'testy']);
+      expect(await command.usageValues,
+             containsPair(CustomDimensions.commandCreateIosLanguage, 'objc'));
 
       await runner.run(<String>[
         'create',
@@ -71,7 +81,8 @@
         '--ios-language=swift',
         'testy',
       ]);
-      expect(await command.usageValues, containsPair(kCommandCreateIosLanguage, 'swift'));
+      expect(await command.usageValues,
+             containsPair(CustomDimensions.commandCreateIosLanguage, 'swift'));
 
     }));
 
@@ -80,7 +91,8 @@
       final CommandRunner<void> runner = createTestCommandRunner(command);
 
       await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=app', 'testy']);
-      expect(await command.usageValues, containsPair(kCommandCreateAndroidLanguage, 'java'));
+      expect(await command.usageValues,
+             containsPair(CustomDimensions.commandCreateAndroidLanguage, 'java'));
 
       await runner.run(<String>[
         'create',
@@ -90,7 +102,8 @@
         '--android-language=kotlin',
         'testy',
       ]);
-      expect(await command.usageValues, containsPair(kCommandCreateAndroidLanguage, 'kotlin'));
+      expect(await command.usageValues,
+             containsPair(CustomDimensions.commandCreateAndroidLanguage, 'kotlin'));
     }));
   });
 }
diff --git a/packages/flutter_tools/test/general.shard/commands/doctor_test.dart b/packages/flutter_tools/test/general.shard/commands/doctor_test.dart
index 26dd7c6..555af06 100644
--- a/packages/flutter_tools/test/general.shard/commands/doctor_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/doctor_test.dart
@@ -16,9 +16,9 @@
 import 'package:flutter_tools/src/doctor.dart';
 import 'package:flutter_tools/src/globals.dart';
 import 'package:flutter_tools/src/proxy_validator.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/vscode/vscode.dart';
 import 'package:flutter_tools/src/vscode/vscode_validator.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
 
 import '../../src/common.dart';
 import '../../src/context.dart';
diff --git a/packages/flutter_tools/test/general.shard/commands/packages_test.dart b/packages/flutter_tools/test/general.shard/commands/packages_test.dart
index 5129568..02a20d7 100644
--- a/packages/flutter_tools/test/general.shard/commands/packages_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/packages_test.dart
@@ -10,7 +10,7 @@
 import 'package:flutter_tools/src/base/utils.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/packages.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:process/process.dart';
 
 import '../../src/common.dart';
@@ -226,7 +226,8 @@
       final PackagesCommand command = await runCommandIn(projectPath, 'get');
       final PackagesGetCommand getCommand = command.subcommands['get'] as PackagesGetCommand;
 
-      expect(await getCommand.usageValues, containsPair(kCommandPackagesNumberPlugins, '0'));
+      expect(await getCommand.usageValues,
+             containsPair(CustomDimensions.commandPackagesNumberPlugins, '0'));
     }, timeout: allowForCreateFlutterProject);
 
     testUsingContext('indicate that the project is not a module in usage value', () async {
@@ -237,7 +238,8 @@
       final PackagesCommand command = await runCommandIn(projectPath, 'get');
       final PackagesGetCommand getCommand = command.subcommands['get'] as PackagesGetCommand;
 
-      expect(await getCommand.usageValues, containsPair(kCommandPackagesProjectModule, 'false'));
+      expect(await getCommand.usageValues,
+             containsPair(CustomDimensions.commandPackagesProjectModule, 'false'));
     }, timeout: allowForCreateFlutterProject);
 
     testUsingContext('indicate that the project is a module in usage value', () async {
@@ -248,7 +250,8 @@
       final PackagesCommand command = await runCommandIn(projectPath, 'get');
       final PackagesGetCommand getCommand = command.subcommands['get'] as PackagesGetCommand;
 
-      expect(await getCommand.usageValues, containsPair(kCommandPackagesProjectModule, 'true'));
+      expect(await getCommand.usageValues,
+             containsPair(CustomDimensions.commandPackagesProjectModule, 'true'));
     }, timeout: allowForCreateFlutterProject);
 
     testUsingContext('upgrade fetches packages', () async {
diff --git a/packages/flutter_tools/test/general.shard/compile_test.dart b/packages/flutter_tools/test/general.shard/compile_test.dart
index debfc49..16743a2 100644
--- a/packages/flutter_tools/test/general.shard/compile_test.dart
+++ b/packages/flutter_tools/test/general.shard/compile_test.dart
@@ -6,9 +6,9 @@
 import 'dart:convert';
 
 import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/context.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
-import 'package:flutter_tools/src/base/context.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/base/terminal.dart';
diff --git a/packages/flutter_tools/test/general.shard/crash_reporting_test.dart b/packages/flutter_tools/test/general.shard/crash_reporting_test.dart
index 1624f8a..fec3f0e 100644
--- a/packages/flutter_tools/test/general.shard/crash_reporting_test.dart
+++ b/packages/flutter_tools/test/general.shard/crash_reporting_test.dart
@@ -8,17 +8,16 @@
 import 'package:file/file.dart';
 import 'package:file/local.dart';
 import 'package:file/memory.dart';
-import 'package:flutter_tools/src/base/platform.dart';
-import 'package:http/http.dart';
-import 'package:http/testing.dart';
-
 import 'package:flutter_tools/runner.dart' as tools;
 import 'package:flutter_tools/src/base/context.dart';
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/cache.dart';
-import 'package:flutter_tools/src/reporting/crash_reporting.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:http/http.dart';
+import 'package:http/testing.dart';
 import 'package:pedantic/pedantic.dart';
 
 import '../src/common.dart';
diff --git a/packages/flutter_tools/test/general.shard/hot_test.dart b/packages/flutter_tools/test/general.shard/hot_test.dart
index 2561daf..304fcf7 100644
--- a/packages/flutter_tools/test/general.shard/hot_test.dart
+++ b/packages/flutter_tools/test/general.shard/hot_test.dart
@@ -129,6 +129,7 @@
       final MockDevice mockDevice = MockDevice();
       when(mockDevice.supportsHotReload).thenReturn(true);
       when(mockDevice.supportsHotRestart).thenReturn(false);
+      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
       // Trigger hot restart.
       final List<FlutterDevice> devices = <FlutterDevice>[
         FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
@@ -190,6 +191,7 @@
       final MockDevice mockDevice = MockDevice();
       when(mockDevice.supportsHotReload).thenReturn(true);
       when(mockDevice.supportsHotRestart).thenReturn(true);
+      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
       final List<FlutterDevice> devices = <FlutterDevice>[
         FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug),
       ];
@@ -206,6 +208,7 @@
       final MockDevice mockDevice = MockDevice();
       when(mockDevice.supportsHotReload).thenReturn(true);
       when(mockDevice.supportsHotRestart).thenReturn(true);
+      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
       // Trigger hot restart.
       final List<FlutterDevice> devices = <FlutterDevice>[
         FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart
index 6167b8a..c6c0735 100644
--- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart
@@ -5,14 +5,14 @@
 import 'dart:async';
 
 import 'package:file/file.dart';
+import 'package:flutter_tools/src/artifacts.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
+import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/ios/mac.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
-import 'package:flutter_tools/src/artifacts.dart';
-import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/project.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:mockito/mockito.dart';
 import 'package:platform/platform.dart';
 import 'package:process/process.dart';
@@ -183,9 +183,10 @@
       );
 
       await diagnoseXcodeBuildFailure(buildResult);
-      verify(mockUsage.sendEvent('Xcode', 'bitcode-failure', parameters: <String, String>{
-        'build-commands': buildCommands.toString(),
-        'build-settings': buildSettings.toString(),
+      verify(mockUsage.sendEvent('build', 'xcode-bitcode-failure',
+        parameters: <String, String>{
+          cdKey(CustomDimensions.buildEventCommand): buildCommands.toString(),
+          cdKey(CustomDimensions.buildEventSettings): buildSettings.toString(),
       })).called(1);
     }, overrides: <Type, Generator>{
       Usage: () => mockUsage,
diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart
index fc6697c..dd800b4 100644
--- a/packages/flutter_tools/test/general.shard/project_test.dart
+++ b/packages/flutter_tools/test/general.shard/project_test.dart
@@ -4,17 +4,17 @@
 
 import 'dart:async';
 
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/flutter_manifest.dart';
 import 'package:flutter_tools/src/ios/ios_workflow.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
 import 'package:flutter_tools/src/project.dart';
-import 'package:flutter_tools/src/base/file_system.dart';
-import 'package:file/file.dart';
-import 'package:file/memory.dart';
 import 'package:meta/meta.dart';
 import 'package:mockito/mockito.dart';
 
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
index d1d626c..2b17c50 100644
--- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -11,7 +11,7 @@
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/devfs.dart';
 import 'package:flutter_tools/src/device.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/resident_runner.dart';
 import 'package:flutter_tools/src/run_hot.dart';
 import 'package:flutter_tools/src/vmservice.dart';
@@ -53,6 +53,7 @@
     when(mockDevFS.lastCompiled).thenReturn(DateTime(2000));
     when(mockDevFS.sources).thenReturn(<Uri>[]);
     when(mockDevFS.destroy()).thenAnswer((Invocation invocation) async { });
+    when(mockDevFS.assetPathsToEvict).thenReturn(<String>{});
     // FlutterDevice Mocks.
     when(mockFlutterDevice.updateDevFS(
       // Intentionally provide empty list to match above mock.
@@ -96,6 +97,19 @@
       mockVMService,
     ]);
     when(mockFlutterDevice.refreshViews()).thenAnswer((Invocation invocation) async { });
+    when(mockFlutterDevice.reloadSources(any, pause: anyNamed('pause'))).thenReturn(<Future<Map<String, dynamic>>>[
+      Future<Map<String, dynamic>>.value(<String, dynamic>{
+        'type': 'ReloadReport',
+        'success': true,
+        'details': <String, dynamic>{
+          'loadedLibraryCount': 1,
+          'finalLibraryCount': 1,
+          'receivedLibraryCount': 1,
+          'receivedClassesCount': 1,
+          'receivedProceduresCount': 1,
+        },
+      }),
+    ]);
     // VMService mocks.
     when(mockVMService.wsAddress).thenReturn(testUri);
     when(mockVMService.done).thenAnswer((Invocation invocation) {
@@ -108,6 +122,9 @@
     when(mockIsolate.flutterExit()).thenAnswer((Invocation invocation) {
       return Future<Map<String, Object>>.value(null);
     });
+    when(mockIsolate.reload()).thenAnswer((Invocation invocation) {
+      return Future<ServiceObject>.value(null);
+    });
   });
 
   test('ResidentRunner can attach to device successfully', () => testbed.run(() async {
@@ -162,15 +179,48 @@
     expect(result.fatal, true);
     expect(result.code, 1);
     verify(flutterUsage.sendEvent('hot', 'exception', parameters: <String, String>{
-      reloadExceptionTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
-      reloadExceptionSdkName: 'Example',
-      reloadExceptionEmulator: 'false',
-      reloadExceptionFullRestart: 'false',
+      cdKey(CustomDimensions.hotEventTargetPlatform):
+        getNameForTargetPlatform(TargetPlatform.android_arm),
+      cdKey(CustomDimensions.hotEventSdkName): 'Example',
+      cdKey(CustomDimensions.hotEventEmulator): 'false',
+      cdKey(CustomDimensions.hotEventFullRestart): 'false',
     })).called(1);
   }, overrides: <Type, Generator>{
     Usage: () => MockUsage(),
   }));
 
+
+  // Need one for hot restart as well.
+
+  test('ResidentRunner can send target platform to analytics from hot reload', () => testbed.run(() async {
+    when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async {
+      return 'Example';
+    });
+    when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
+      return TargetPlatform.android_arm;
+    });
+    when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async {
+      return false;
+    });
+    final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
+    final Completer<void> onAppStart = Completer<void>.sync();
+    unawaited(residentRunner.attach(
+      appStartedCompleter: onAppStart,
+      connectionInfoCompleter: onConnectionInfo,
+    ));
+
+    final OperationResult result = await residentRunner.restart(fullRestart: false);
+    expect(result.fatal, false);
+    expect(result.code, 0);
+    expect(verify(flutterUsage.sendEvent('hot', 'reload',
+                  parameters: captureAnyNamed('parameters'))).captured[0],
+      containsPair(cdKey(CustomDimensions.hotEventTargetPlatform),
+                   getNameForTargetPlatform(TargetPlatform.android_arm))
+    );
+  }, overrides: <Type, Generator>{
+    Usage: () => MockUsage(),
+  }));
+
   test('ResidentRunner Can handle an RPC exception from hot restart', () => testbed.run(() async {
     when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async {
       return 'Example';
@@ -206,10 +256,11 @@
     expect(result.fatal, true);
     expect(result.code, 1);
     verify(flutterUsage.sendEvent('hot', 'exception', parameters: <String, String>{
-      reloadExceptionTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
-      reloadExceptionSdkName: 'Example',
-      reloadExceptionEmulator: 'false',
-      reloadExceptionFullRestart: 'true',
+      cdKey(CustomDimensions.hotEventTargetPlatform):
+        getNameForTargetPlatform(TargetPlatform.android_arm),
+      cdKey(CustomDimensions.hotEventSdkName): 'Example',
+      cdKey(CustomDimensions.hotEventEmulator): 'false',
+      cdKey(CustomDimensions.hotEventFullRestart): 'true',
     })).called(1);
   }, overrides: <Type, Generator>{
     Usage: () => MockUsage(),
diff --git a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
index eb06c9d..413e4c6 100644
--- a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
+++ b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
@@ -2,10 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:flutter_tools/src/cache.dart';
-import 'package:flutter_tools/src/base/time.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
 import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/time.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
 import 'package:flutter_tools/src/version.dart';
 import 'package:mockito/mockito.dart';
diff --git a/packages/flutter_tools/test/general.shard/runner/utils.dart b/packages/flutter_tools/test/general.shard/runner/utils.dart
index 57e8af6..f393441 100644
--- a/packages/flutter_tools/test/general.shard/runner/utils.dart
+++ b/packages/flutter_tools/test/general.shard/runner/utils.dart
@@ -5,7 +5,7 @@
 import 'dart:async';
 
 import 'package:flutter_tools/src/cache.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
 import 'package:mockito/mockito.dart';
 
diff --git a/packages/flutter_tools/test/src/common.dart b/packages/flutter_tools/test/src/common.dart
index 3a3a4ea..ff4e549 100644
--- a/packages/flutter_tools/test/src/common.dart
+++ b/packages/flutter_tools/test/src/common.dart
@@ -5,9 +5,6 @@
 import 'dart:async';
 
 import 'package:args/command_runner.dart';
-import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf;
-import 'package:test_api/test_api.dart' as test_package show TypeMatcher;
-
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/platform.dart';
@@ -15,6 +12,8 @@
 import 'package:flutter_tools/src/commands/create.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
 import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
+import 'package:test_api/test_api.dart' as test_package show TypeMatcher;
+import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf;
 
 export 'package:test_core/test_core.dart' hide TypeMatcher, isInstanceOf; // Defines a 'package:test' shim.
 
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index fe82865..2cafbcd 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -13,15 +13,15 @@
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/os.dart';
 import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:flutter_tools/src/base/time.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/context_runner.dart';
 import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/doctor.dart';
 import 'package:flutter_tools/src/ios/simulators.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
-import 'package:flutter_tools/src/base/time.dart';
 import 'package:flutter_tools/src/project.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/version.dart';
 import 'package:meta/meta.dart';
 import 'package:mockito/mockito.dart';
diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart
index a0394e4..446d111 100644
--- a/packages/flutter_tools/test/src/testbed.dart
+++ b/packages/flutter_tools/test/src/testbed.dart
@@ -8,17 +8,17 @@
 import 'dart:typed_data';
 
 import 'package:file/memory.dart';
-import 'package:flutter_tools/src/base/io.dart';
-import 'package:flutter_tools/src/base/os.dart';
-import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/base/context.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/os.dart';
+import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/base/terminal.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/context_runner.dart';
 import 'package:flutter_tools/src/features.dart';
-import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/version.dart';
 
 import 'context.dart';