add google analytics to flutter_tools (#3523)

* add google analytics

* send in the run target type

* track device type targets

* use the real GA code

* review comments

* rev to usage 2.0

* rev to 2.2.0 of usage; add tests

* review comments
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index 1c80023..f51a038 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -15,6 +15,7 @@
 import 'src/base/utils.dart';
 import 'src/commands/analyze.dart';
 import 'src/commands/build.dart';
+import 'src/commands/config.dart';
 import 'src/commands/create.dart';
 import 'src/commands/daemon.dart';
 import 'src/commands/devices.dart';
@@ -56,6 +57,7 @@
   FlutterCommandRunner runner = new FlutterCommandRunner(verboseHelp: verboseHelp)
     ..addCommand(new AnalyzeCommand())
     ..addCommand(new BuildCommand())
+    ..addCommand(new ConfigCommand())
     ..addCommand(new CreateCommand())
     ..addCommand(new DaemonCommand(hidden: !verboseHelp))
     ..addCommand(new DevicesCommand())
@@ -82,10 +84,18 @@
     context[DeviceManager] = new DeviceManager();
     Doctor.initGlobal();
 
+    if (flutterUsage.isFirstRun) {
+      printStatus(
+        'The Flutter tool anonymously reports feature usage statistics and basic crash reports to Google to\n'
+        'help Google contribute improvements to Flutter over time. Use "flutter config" to control this\n'
+        'behavior. See Google\'s privacy policy: https://www.google.com/intl/en/policies/privacy/\n'
+      );
+    }
+
     dynamic result = await runner.run(args);
 
     if (result is int)
-      exit(result);
+      _exit(result);
   }, onError: (dynamic error, Chain chain) {
     if (error is UsageException) {
       stderr.writeln(error.message);
@@ -93,14 +103,16 @@
       stderr.writeln("Run 'flutter -h' (or 'flutter <command> -h') for available "
         "flutter commands and options.");
       // Argument error exit code.
-      exit(64);
+      _exit(64);
     } else if (error is ProcessExit) {
       // We've caught an exit code.
-      exit(error.exitCode);
+      _exit(error.exitCode);
     } else {
       // We've crashed; emit a log report.
       stderr.writeln();
 
+      flutterUsage.sendException(error, chain);
+
       if (Platform.environment.containsKey('FLUTTER_DEV')) {
         // If we're working on the tools themselves, just print the stack trace.
         stderr.writeln('$error');
@@ -118,7 +130,7 @@
           'please let us know at https://github.com/flutter/flutter/issues.');
       }
 
-      exit(1);
+      _exit(1);
     }
   });
 }
@@ -159,3 +171,21 @@
     return 'encountered exception: $error\n\n${trace.toString().trim()}\n';
   }
 }
+
+Future<Null> _exit(int code) async {
+  // Send any last analytics calls that are in progress without overly delaying
+  // the tool's exit (we wait a maximum of 250ms).
+  if (flutterUsage.enabled) {
+    Stopwatch stopwatch = new Stopwatch()..start();
+    await flutterUsage.ensureAnalyticsSent();
+    printTrace('ensureAnalyticsSent: ${stopwatch.elapsedMilliseconds}ms');
+  }
+
+  // Write any buffered output.
+  logger.flush();
+
+  // Give the task / timer queue one cycle through before we hard exit.
+  await Timer.run(() {
+    exit(code);
+  });
+}
diff --git a/packages/flutter_tools/lib/src/commands/config.dart b/packages/flutter_tools/lib/src/commands/config.dart
new file mode 100644
index 0000000..b789955
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/config.dart
@@ -0,0 +1,50 @@
+// Copyright 2016 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.
+
+import 'dart:async';
+
+import '../globals.dart';
+import '../runner/flutter_command.dart';
+
+class ConfigCommand extends FlutterCommand {
+  ConfigCommand() {
+    String usageStatus = flutterUsage.enabled ? 'enabled' : 'disabled';
+
+    argParser.addFlag('analytics',
+      negatable: true,
+      help: 'Enable or disable reporting anonymously tool usage statistics and crash reports.\n(currently $usageStatus)');
+  }
+
+  @override
+  final String name = 'config';
+
+  @override
+  final String description =
+    'Configure Flutter settings.\n\n'
+    'The Flutter tool anonymously reports feature usage statistics and basic crash reports to help improve\n'
+    'Flutter tools over time. See Google\'s privacy policy: www.google.com/intl/en/policies/privacy';
+
+  @override
+  final List<String> aliases = <String>['configure'];
+
+  @override
+  bool get requiresProjectRoot => false;
+
+  /// Return `null` to disable tracking of the `config` command.
+  @override
+  String get usagePath => null;
+
+  @override
+  Future<int> runInProject() async {
+    if (argResults.wasParsed('analytics')) {
+      bool value = argResults['analytics'];
+      flutterUsage.enabled = value;
+      printStatus('Analytics reporting ${value ? 'enabled' : 'disabled'}.');
+    } else {
+      printStatus(usage);
+    }
+
+    return 0;
+  }
+}
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index 6a2269a..122294c 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -5,7 +5,6 @@
 import 'dart:async';
 import 'dart:io';
 
-import 'package:args/command_runner.dart';
 import 'package:path/path.dart' as path;
 
 import '../android/android.dart' as android;
@@ -14,19 +13,10 @@
 import '../cache.dart';
 import '../dart/pub.dart';
 import '../globals.dart';
+import '../runner/flutter_command.dart';
 import '../template.dart';
 
-class CreateCommand extends Command {
-  @override
-  final String name = 'create';
-
-  @override
-  final String description = 'Create a new Flutter project.\n\n'
-    'If run on a project that already exists, this will repair the project, recreating any files that are missing.';
-
-  @override
-  final List<String> aliases = <String>['init'];
-
+class CreateCommand extends FlutterCommand {
   CreateCommand() {
     argParser.addFlag('pub',
       defaultsTo: true,
@@ -46,10 +36,23 @@
   }
 
   @override
+  final String name = 'create';
+
+  @override
+  final String description = 'Create a new Flutter project.\n\n'
+    'If run on a project that already exists, this will repair the project, recreating any files that are missing.';
+
+  @override
+  final List<String> aliases = <String>['init'];
+
+  @override
+  bool get requiresProjectRoot => false;
+
+  @override
   String get invocation => "${runner.executableName} $name <output directory>";
 
   @override
-  Future<int> run() async {
+  Future<int> runInProject() async {
     if (argResults.rest.isEmpty) {
       printStatus('No option specified for the output directory.');
       printStatus(usage);
diff --git a/packages/flutter_tools/lib/src/commands/precache.dart b/packages/flutter_tools/lib/src/commands/precache.dart
index 6b950c2..192b055 100644
--- a/packages/flutter_tools/lib/src/commands/precache.dart
+++ b/packages/flutter_tools/lib/src/commands/precache.dart
@@ -4,11 +4,10 @@
 
 import 'dart:async';
 
-import 'package:args/command_runner.dart';
-
 import '../globals.dart';
+import '../runner/flutter_command.dart';
 
-class PrecacheCommand extends Command {
+class PrecacheCommand extends FlutterCommand {
   @override
   final String name = 'precache';
 
@@ -16,7 +15,10 @@
   final String description = 'Populates the Flutter tool\'s cache of binary artifacts.';
 
   @override
-  Future<int> run() async {
+  bool get requiresProjectRoot => false;
+
+  @override
+  Future<int> runInProject() async {
     if (cache.isUpToDate())
       printStatus('Already up-to-date.');
     else
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index c45110b..7911eb8 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -83,6 +83,17 @@
   bool get requiresDevice => true;
 
   @override
+  String get usagePath {
+    Device device = deviceForCommand;
+
+    if (device == null)
+      return name;
+
+    // Return 'run/ios'.
+    return '$name/${getNameForTargetPlatform(device.platform)}';
+  }
+
+  @override
   Future<int> runInProject() async {
     bool clearLogs = argResults['clear-logs'];
 
diff --git a/packages/flutter_tools/lib/src/commands/skia.dart b/packages/flutter_tools/lib/src/commands/skia.dart
index 3e5f0f4..1f1bb8c 100644
--- a/packages/flutter_tools/lib/src/commands/skia.dart
+++ b/packages/flutter_tools/lib/src/commands/skia.dart
@@ -12,12 +12,6 @@
 import '../runner/flutter_command.dart';
 
 class SkiaCommand extends FlutterCommand {
-  @override
-  final String name = 'skia';
-
-  @override
-  final String description = 'Retrieve the last frame rendered by a Flutter app as a Skia picture.';
-
   SkiaCommand() {
     argParser.addOption('output-file', help: 'Write the Skia picture file to this path.');
     argParser.addOption('skiaserve', help: 'Post the picture to a skiaserve debugger at this URL.');
@@ -27,6 +21,12 @@
   }
 
   @override
+  final String name = 'skia';
+
+  @override
+  final String description = 'Retrieve the last frame rendered by a Flutter app as a Skia picture.';
+
+  @override
   Future<int> runInProject() async {
     File outputFile;
     Uri skiaserveUri;
diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart
index dfe72ed..934d01e 100644
--- a/packages/flutter_tools/lib/src/globals.dart
+++ b/packages/flutter_tools/lib/src/globals.dart
@@ -9,13 +9,15 @@
 import 'device.dart';
 import 'doctor.dart';
 import 'toolchain.dart';
+import 'usage.dart';
 
 DeviceManager get deviceManager => context[DeviceManager];
 Logger get logger => context[Logger];
 AndroidSdk get androidSdk => context[AndroidSdk];
-Doctor get doctor => context[Doctor];
 Cache get cache => Cache.instance;
+Doctor get doctor => context[Doctor];
 ToolConfiguration get tools => ToolConfiguration.instance;
+Usage get flutterUsage => Usage.instance;
 
 /// Display an error level message to the user. Commands should use this if they
 /// fail in some way.
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 432682e6..1ebc038 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -15,6 +15,7 @@
 import '../globals.dart';
 import '../package_map.dart';
 import '../toolchain.dart';
+import '../usage.dart';
 import 'flutter_command_runner.dart';
 
 typedef bool Validator();
@@ -94,13 +95,19 @@
     applicationPackages ??= new ApplicationPackageStore();
   }
 
+  /// The path to send to Google Analytics. Return `null` here to disable
+  /// tracking of the command.
+  String get usagePath => name;
+
   @override
   Future<int> run() {
     Stopwatch stopwatch = new Stopwatch()..start();
+    UsageTimer analyticsTimer = usagePath == null ? null : flutterUsage.startTimer(name);
 
     return _run().then((int exitCode) {
       int ms = stopwatch.elapsedMilliseconds;
       printTrace("'flutter $name' took ${ms}ms; exiting with code $exitCode.");
+      analyticsTimer?.finish();
       return exitCode;
     });
   }
@@ -160,6 +167,10 @@
     _setupToolchain();
     _setupApplicationPackages();
 
+    String commandPath = usagePath;
+    if (commandPath != null)
+      flutterUsage.sendCommand(usagePath);
+
     return await runInProject();
   }
 
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 a050656..955eee1 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
@@ -84,13 +84,15 @@
     argParser.addOption('host-debug-build-path',
         hide: !verboseHelp,
         help:
-            'Path to your host Debug out directory (i.e. the one that runs on your workstation, not a device), if you are building Flutter locally.\n'
+            'Path to your host Debug out directory (i.e. the one that runs on your workstation, not a device),\n'
+            'if you are building Flutter locally.\n'
             'This path is relative to --engine-src-path. Not normally required.',
         defaultsTo: 'out/Debug/');
     argParser.addOption('host-release-build-path',
         hide: !verboseHelp,
         help:
-            'Path to your host Release out directory (i.e. the one that runs on your workstation, not a device), if you are building Flutter locally.\n'
+            'Path to your host Release out directory (i.e. the one that runs on your workstation, not a device),\n'
+            'if you are building Flutter locally.\n'
             'This path is relative to --engine-src-path. Not normally required.',
         defaultsTo: 'out/Release/');
 
@@ -232,6 +234,7 @@
     }
 
     if (globalResults['version']) {
+      flutterUsage.sendCommand('version');
       printStatus(FlutterVersion.getVersion(ArtifactStore.flutterRoot).toString());
       return new Future<int>.value(0);
     }
diff --git a/packages/flutter_tools/lib/src/runner/version.dart b/packages/flutter_tools/lib/src/runner/version.dart
index eb0a53b..dba2f7e 100644
--- a/packages/flutter_tools/lib/src/runner/version.dart
+++ b/packages/flutter_tools/lib/src/runner/version.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:io';
+
 import '../artifacts.dart';
 import '../base/process.dart';
 
@@ -54,4 +56,21 @@
   static FlutterVersion getVersion([String flutterRoot]) {
     return new FlutterVersion(flutterRoot != null ? flutterRoot : ArtifactStore.flutterRoot);
   }
+
+  static String getVersionString() {
+    final String cwd = ArtifactStore.flutterRoot;
+
+    String commit = _runSync('git', <String>['rev-parse', 'HEAD'], cwd);
+    if (commit.length > 8)
+      commit = commit.substring(0, 8);
+
+    String branch = _runSync('git', <String>['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
+
+    return '$commit/$branch';
+  }
+}
+
+String _runSync(String executable, List<String> arguments, String cwd) {
+  ProcessResult results = Process.runSync(executable, arguments, workingDirectory: cwd);
+  return results.exitCode == 0 ? results.stdout.trim() : '';
 }
diff --git a/packages/flutter_tools/lib/src/usage.dart b/packages/flutter_tools/lib/src/usage.dart
new file mode 100644
index 0000000..94a409a
--- /dev/null
+++ b/packages/flutter_tools/lib/src/usage.dart
@@ -0,0 +1,94 @@
+// Copyright 2016 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.
+
+import 'dart:async';
+
+import 'package:usage/src/usage_impl_io.dart';
+import 'package:usage/usage.dart';
+
+import 'base/context.dart';
+import 'runner/version.dart';
+
+// TODO(devoncarew): We'll need to do some work on the user agent in order to
+// correctly track usage by operating system (dart-lang/usage/issues/70).
+
+// TODO(devoncarew): We'll want to find a way to send (sanitized) command parameters.
+
+const String _kFlutterUA = 'UA-67589403-5';
+
+class Usage {
+  Usage() {
+    _analytics = new AnalyticsIO(_kFlutterUA, 'flutter', FlutterVersion.getVersionString());
+    _analytics.analyticsOpt = AnalyticsOpt.optOut;
+  }
+
+  /// Returns [Usage] active in the current app context.
+  static Usage get instance => context[Usage] ?? (context[Usage] = new Usage());
+
+  Analytics _analytics;
+
+  bool get isFirstRun => _analytics.firstRun;
+
+  bool get enabled => _analytics.enabled;
+
+  /// Enable or disable reporting analytics.
+  set enabled(bool value) {
+    _analytics.enabled = value;
+  }
+
+  void sendCommand(String command) {
+    if (!isFirstRun)
+      _analytics.sendScreenView(command);
+  }
+
+  void sendEvent(String category, String parameter) {
+    if (!isFirstRun)
+      _analytics.sendEvent(category, parameter);
+  }
+
+  UsageTimer startTimer(String event) {
+    if (isFirstRun)
+      return new _MockUsageTimer();
+    else
+      return new UsageTimer._(event, _analytics.startTimer(event));
+  }
+
+  void sendException(dynamic exception, StackTrace trace) {
+    if (!isFirstRun)
+      _analytics.sendException('${exception.runtimeType}; ${sanitizeStacktrace(trace)}');
+  }
+
+  /// Fires whenever analytics data is sent over the network; public for testing.
+  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.
+  Future<Null> ensureAnalyticsSent() {
+    // TODO(devoncarew): This may delay tool exit and could cause some analytics
+    // events to not be reported. Perhaps we could send the analytics pings
+    // out-of-process from flutter_tools?
+    return _analytics.waitForLastPing(timeout: new Duration(milliseconds: 250));
+  }
+}
+
+class UsageTimer {
+  UsageTimer._(this.event, this._timer);
+
+  final String event;
+  final AnalyticsTimer _timer;
+
+  void finish() {
+    _timer.finish();
+  }
+}
+
+class _MockUsageTimer implements UsageTimer {
+  @override
+  String event;
+  @override
+  AnalyticsTimer _timer;
+
+  @override
+  void finish() { }
+}
diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml
index ad65998..2b8abef 100644
--- a/packages/flutter_tools/pubspec.yaml
+++ b/packages/flutter_tools/pubspec.yaml
@@ -20,6 +20,7 @@
   path: ^1.3.0
   pub_semver: ^1.0.0
   stack_trace: ^1.4.0
+  usage: ^2.2.0
   web_socket_channel: ^1.0.0
   xml: ^2.4.1
   yaml: ^2.1.3
diff --git a/packages/flutter_tools/test/all.dart b/packages/flutter_tools/test/all.dart
index 76cea4f..1d1afeb 100644
--- a/packages/flutter_tools/test/all.dart
+++ b/packages/flutter_tools/test/all.dart
@@ -8,6 +8,7 @@
 // fix lands.
 
 import 'adb_test.dart' as adb_test;
+import 'analytics_test.dart' as analytics_test;
 import 'analyze_duplicate_names_test.dart' as analyze_duplicate_names_test;
 import 'analyze_test.dart' as analyze_test;
 import 'android_device_test.dart' as android_device_test;
@@ -31,6 +32,7 @@
 
 void main() {
   adb_test.main();
+  analytics_test.main();
   analyze_duplicate_names_test.main();
   analyze_test.main();
   android_device_test.main();
diff --git a/packages/flutter_tools/test/analytics_test.dart b/packages/flutter_tools/test/analytics_test.dart
new file mode 100644
index 0000000..4cd2489
--- /dev/null
+++ b/packages/flutter_tools/test/analytics_test.dart
@@ -0,0 +1,81 @@
+// Copyright 2016 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.
+
+import 'dart:io';
+
+import 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/commands/create.dart';
+import 'package:flutter_tools/src/commands/config.dart';
+import 'package:flutter_tools/src/commands/doctor.dart';
+import 'package:flutter_tools/src/globals.dart';
+import 'package:flutter_tools/src/usage.dart';
+import 'package:test/test.dart';
+
+import 'src/common.dart';
+import 'src/context.dart';
+
+void main() {
+  group('analytics', () {
+    Directory temp;
+    bool wasEnabled;
+
+    setUp(() {
+      ArtifactStore.flutterRoot = '../..';
+      wasEnabled = flutterUsage.enabled;
+      temp = Directory.systemTemp.createTempSync('flutter_tools');
+    });
+
+    tearDown(() {
+      flutterUsage.enabled = wasEnabled;
+      temp.deleteSync(recursive: true);
+    });
+
+    // Ensure we don't send anything when analytics is disabled.
+    testUsingContext('doesn\'t send when disabled', () async {
+      int count = 0;
+      flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
+
+      flutterUsage.enabled = false;
+      CreateCommand command = new CreateCommand();
+      CommandRunner runner = createTestCommandRunner(command);
+      int code = await runner.run(<String>['create', '--no-pub', temp.path]);
+      expect(code, equals(0));
+      expect(count, 0);
+
+      flutterUsage.enabled = true;
+      code = await runner.run(<String>['create', '--no-pub', temp.path]);
+      expect(code, equals(0));
+      expect(count, flutterUsage.isFirstRun ? 0 : 2);
+
+      count = 0;
+      flutterUsage.enabled = false;
+      DoctorCommand doctorCommand = new DoctorCommand();
+      runner = createTestCommandRunner(doctorCommand);
+      code = await runner.run(<String>['doctor']);
+      expect(code, equals(0));
+      expect(count, 0);
+    }, overrides: <Type, dynamic>{
+      Usage: new Usage()
+    });
+
+    // Ensure we con't send for the 'flutter config' command.
+    testUsingContext('config doesn\'t send', () async {
+      int count = 0;
+      flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
+
+      flutterUsage.enabled = false;
+      ConfigCommand command = new ConfigCommand();
+      CommandRunner runner = createTestCommandRunner(command);
+      await runner.run(<String>['config']);
+      expect(count, 0);
+
+      flutterUsage.enabled = true;
+      await runner.run(<String>['config']);
+      expect(count, 0);
+    }, overrides: <Type, dynamic>{
+      Usage: new Usage()
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/create_test.dart b/packages/flutter_tools/test/create_test.dart
index f30f4c3..ec44bed 100644
--- a/packages/flutter_tools/test/create_test.dart
+++ b/packages/flutter_tools/test/create_test.dart
@@ -11,6 +11,7 @@
 import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
 
+import 'src/common.dart';
 import 'src/context.dart';
 
 void main() {
@@ -37,9 +38,9 @@
     // Verify that we can regenerate over an existing project.
     testUsingContext('can re-gen over existing project', () async {
       ArtifactStore.flutterRoot = '../..';
+
       CreateCommand command = new CreateCommand();
-      CommandRunner runner = new CommandRunner('test_flutter', '')
-        ..addCommand(command);
+      CommandRunner runner = createTestCommandRunner(command);
 
       int code = await runner.run(<String>['create', '--no-pub', temp.path]);
       expect(code, equals(0));
@@ -52,8 +53,7 @@
     testUsingContext('fails when file exists', () async {
       ArtifactStore.flutterRoot = '../..';
       CreateCommand command = new CreateCommand();
-      CommandRunner runner = new CommandRunner('test_flutter', '')
-        ..addCommand(command);
+      CommandRunner runner = createTestCommandRunner(command);
       File existingFile = new File("${temp.path.toString()}/bad");
       if (!existingFile.existsSync()) existingFile.createSync();
       int code = await runner.run(<String>['create', existingFile.path]);
@@ -65,8 +65,7 @@
 Future<Null> _createAndAnalyzeProject(Directory dir, List<String> createArgs) async {
   ArtifactStore.flutterRoot = '../..';
   CreateCommand command = new CreateCommand();
-  CommandRunner runner = new CommandRunner('test_flutter', '')
-    ..addCommand(command);
+  CommandRunner runner = createTestCommandRunner(command);
   List<String> args = <String>['create'];
   args.addAll(createArgs);
   args.add(dir.path);
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index c06c0b7..f0ac2f0 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -12,7 +12,7 @@
 import 'package:flutter_tools/src/doctor.dart';
 import 'package:flutter_tools/src/ios/mac.dart';
 import 'package:flutter_tools/src/ios/simulators.dart';
-
+import 'package:flutter_tools/src/usage.dart';
 import 'package:mockito/mockito.dart';
 import 'package:test/test.dart';
 
@@ -45,6 +45,9 @@
     if (!overrides.containsKey(SimControl))
       testContext[SimControl] = new MockSimControl();
 
+    if (!overrides.containsKey(Usage))
+      testContext[Usage] = new MockUsage();
+
     if (!overrides.containsKey(OperatingSystemUtils)) {
       MockOperatingSystemUtils os = new MockOperatingSystemUtils();
       when(os.isWindows).thenReturn(false);
@@ -112,3 +115,42 @@
 class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
 
 class MockIOSSimulatorUtils extends Mock implements IOSSimulatorUtils {}
+
+class MockUsage implements Usage {
+  @override
+  bool get isFirstRun => false;
+
+  @override
+  bool get enabled => true;
+
+  @override
+  set enabled(bool value) { }
+
+  @override
+  void sendCommand(String command) { }
+
+  @override
+  void sendEvent(String category, String parameter) { }
+
+  @override
+  UsageTimer startTimer(String event) => new _MockUsageTimer(event);
+
+  @override
+  void sendException(dynamic exception, StackTrace trace) { }
+
+  @override
+  Stream<Map<String, dynamic>> get onSend => null;
+
+  @override
+  Future<Null> ensureAnalyticsSent() => new Future<Null>.value();
+}
+
+class _MockUsageTimer implements UsageTimer {
+  _MockUsageTimer(this.event);
+
+  @override
+  final String event;
+
+  @override
+  void finish() { }
+}