Analytics (#2778)

diff --git a/lib/pub.dart b/lib/pub.dart
index b78cbca..841e57d 100644
--- a/lib/pub.dart
+++ b/lib/pub.dart
@@ -9,10 +9,15 @@
 import 'src/pub_embeddable_command.dart';
 export 'src/executable.dart'
     show getExecutableForCommand, CommandResolutionFailedException;
+export 'src/pub_embeddable_command.dart' show PubAnalytics;
 
 /// Returns a [Command] for pub functionality that can be used by an embedding
 /// CommandRunner.
-Command<int> pubCommand() => PubEmbeddableCommand();
+///
+/// If [analytics] is given, pub will use that analytics instance to send
+/// statistics about resolutions.
+Command<int> pubCommand({PubAnalytics analytics}) =>
+    PubEmbeddableCommand(analytics);
 
 /// Support for the `pub` toplevel command.
 @Deprecated('Use [pubCommand] instead.')
diff --git a/lib/src/command.dart b/lib/src/command.dart
index cd4a524..503d174 100644
--- a/lib/src/command.dart
+++ b/lib/src/command.dart
@@ -117,6 +117,8 @@
     return _pubEmbeddableCommand ?? (runner as PubCommandRunner);
   }
 
+  PubAnalytics get analytics => _pubEmbeddableCommand?.analytics;
+
   @override
   String get invocation {
     var command = this;
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index 728f8ea..0775325 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -150,11 +150,10 @@
       // TODO(jonasfj): Stop abusing Entrypoint.global for dry-run output
       await Entrypoint.global(newRoot, entrypoint.lockFile, cache,
               solveResult: solveResult)
-          .acquireDependencies(
-        SolveType.GET,
-        dryRun: true,
-        precompile: argResults['precompile'],
-      );
+          .acquireDependencies(SolveType.GET,
+              dryRun: true,
+              precompile: argResults['precompile'],
+              analytics: analytics);
     } else {
       /// Update the `pubspec.yaml` before calling [acquireDependencies] to
       /// ensure that the modification timestamp on `pubspec.lock` and
@@ -165,14 +164,18 @@
       /// Create a new [Entrypoint] since we have to reprocess the updated
       /// pubspec file.
       final updatedEntrypoint = Entrypoint(directory, cache);
-      await updatedEntrypoint.acquireDependencies(SolveType.GET,
-          precompile: argResults['precompile']);
+      await updatedEntrypoint.acquireDependencies(
+        SolveType.GET,
+        precompile: argResults['precompile'],
+        analytics: analytics,
+      );
 
       if (argResults['example'] && entrypoint.example != null) {
         await entrypoint.example.acquireDependencies(
           SolveType.GET,
           precompile: argResults['precompile'],
           onlyReportSuccessOrFailure: true,
+          analytics: analytics,
         );
       }
     }
diff --git a/lib/src/command/downgrade.dart b/lib/src/command/downgrade.dart
index 2d0840a..2e26bc4 100644
--- a/lib/src/command/downgrade.dart
+++ b/lib/src/command/downgrade.dart
@@ -53,16 +53,21 @@
           'The --packages-dir flag is no longer used and does nothing.'));
     }
     var dryRun = argResults['dry-run'];
+
     await entrypoint.acquireDependencies(
       SolveType.DOWNGRADE,
       unlock: argResults.rest,
       dryRun: dryRun,
+      analytics: analytics,
     );
     if (argResults['example'] && entrypoint.example != null) {
-      await entrypoint.example.acquireDependencies(SolveType.GET,
-          unlock: argResults.rest,
-          dryRun: dryRun,
-          onlyReportSuccessOrFailure: true);
+      await entrypoint.example.acquireDependencies(
+        SolveType.GET,
+        unlock: argResults.rest,
+        dryRun: dryRun,
+        onlyReportSuccessOrFailure: true,
+        analytics: analytics,
+      );
     }
 
     if (isOffline) {
diff --git a/lib/src/command/get.dart b/lib/src/command/get.dart
index 2709238..23efab3 100644
--- a/lib/src/command/get.dart
+++ b/lib/src/command/get.dart
@@ -51,14 +51,21 @@
       log.warning(log.yellow(
           'The --packages-dir flag is no longer used and does nothing.'));
     }
-    await entrypoint.acquireDependencies(SolveType.GET,
-        dryRun: argResults['dry-run'], precompile: argResults['precompile']);
+    await entrypoint.acquireDependencies(
+      SolveType.GET,
+      dryRun: argResults['dry-run'],
+      precompile: argResults['precompile'],
+      analytics: analytics,
+    );
 
     if (argResults['example'] && entrypoint.example != null) {
-      await entrypoint.example.acquireDependencies(SolveType.GET,
-          dryRun: argResults['dry-run'],
-          precompile: argResults['precompile'],
-          onlyReportSuccessOrFailure: true);
+      await entrypoint.example.acquireDependencies(
+        SolveType.GET,
+        dryRun: argResults['dry-run'],
+        precompile: argResults['precompile'],
+        onlyReportSuccessOrFailure: true,
+        analytics: analytics,
+      );
     }
   }
 }
diff --git a/lib/src/command/global_activate.dart b/lib/src/command/global_activate.dart
index 2959e6e..abd0ee3 100644
--- a/lib/src/command/global_activate.dart
+++ b/lib/src/command/global_activate.dart
@@ -139,8 +139,12 @@
 
         var path = readArg('No package to activate given.');
         validateNoExtraArgs();
-        return globals.activatePath(path, executables,
-            overwriteBinStubs: overwrite);
+        return globals.activatePath(
+          path,
+          executables,
+          overwriteBinStubs: overwrite,
+          analytics: analytics,
+        );
     }
 
     throw StateError('unreachable');
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart
index f505785..2072931 100644
--- a/lib/src/command/remove.dart
+++ b/lib/src/command/remove.dart
@@ -68,11 +68,10 @@
       final newRoot = Package.inMemory(newPubspec);
 
       await Entrypoint.global(newRoot, entrypoint.lockFile, cache)
-          .acquireDependencies(
-        SolveType.GET,
-        precompile: argResults['precompile'],
-        dryRun: true,
-      );
+          .acquireDependencies(SolveType.GET,
+              precompile: argResults['precompile'],
+              dryRun: true,
+              analytics: null);
     } else {
       /// Update the pubspec.
       _writeRemovalToPubspec(packages);
@@ -80,13 +79,19 @@
       /// Create a new [Entrypoint] since we have to reprocess the updated
       /// pubspec file.
       final updatedEntrypoint = Entrypoint(directory, cache);
-      await updatedEntrypoint.acquireDependencies(SolveType.GET,
-          precompile: argResults['precompile']);
+      await updatedEntrypoint.acquireDependencies(
+        SolveType.GET,
+        precompile: argResults['precompile'],
+        analytics: analytics,
+      );
 
       if (argResults['example'] && entrypoint.example != null) {
-        await entrypoint.example.acquireDependencies(SolveType.GET,
-            precompile: argResults['precompile'],
-            onlyReportSuccessOrFailure: true);
+        await entrypoint.example.acquireDependencies(
+          SolveType.GET,
+          precompile: argResults['precompile'],
+          onlyReportSuccessOrFailure: true,
+          analytics: analytics,
+        );
       }
     }
   }
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index 39a42c7..b74cbe7 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -122,11 +122,14 @@
   }
 
   Future<void> _runUpgrade(Entrypoint e, {bool onlySummary = false}) async {
-    await e.acquireDependencies(SolveType.UPGRADE,
-        unlock: argResults.rest,
-        dryRun: _dryRun,
-        precompile: _precompile,
-        onlyReportSuccessOrFailure: onlySummary);
+    await e.acquireDependencies(
+      SolveType.UPGRADE,
+      unlock: argResults.rest,
+      dryRun: _dryRun,
+      precompile: _precompile,
+      onlyReportSuccessOrFailure: onlySummary,
+      analytics: analytics,
+    );
 
     _showOfflineWarning();
   }
@@ -233,6 +236,7 @@
         SolveType.UPGRADE,
         dryRun: true,
         precompile: _precompile,
+        analytics: null, // No analytics for dry-run
       );
     } else {
       await _updatePubspec(changes);
@@ -243,6 +247,7 @@
       await Entrypoint(directory, cache).acquireDependencies(
         SolveType.UPGRADE,
         precompile: _precompile,
+        analytics: analytics,
       );
     }
 
@@ -326,6 +331,7 @@
         SolveType.UPGRADE,
         dryRun: true,
         precompile: _precompile,
+        analytics: null,
       );
     } else {
       await _updatePubspec(changes);
@@ -336,6 +342,7 @@
       await Entrypoint(directory, cache).acquireDependencies(
         SolveType.UPGRADE,
         precompile: _precompile,
+        analytics: analytics,
       );
     }
 
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 8820c33..763e707 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -28,6 +28,7 @@
 import 'package_graph.dart';
 import 'package_name.dart';
 import 'packages_file.dart' as packages_file;
+import 'pub_embeddable_command.dart';
 import 'pubspec.dart';
 import 'sdk.dart';
 import 'solver.dart';
@@ -247,6 +248,7 @@
     Iterable<String> unlock,
     bool dryRun = false,
     bool precompile = false,
+    @required PubAnalytics analytics,
     bool onlyReportSuccessOrFailure = false,
   }) async {
     final suffix = root.dir == null || root.dir == '.' ? '' : ' in ${root.dir}';
@@ -307,6 +309,10 @@
     }
 
     if (!dryRun) {
+      if (analytics != null) {
+        result.sendAnalytics(analytics);
+      }
+
       /// Build a package graph from the version solver results so we don't
       /// have to reload and reparse all the pubspecs.
       _packageGraph = PackageGraph.fromSolveResult(this, result);
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index 08722e5..ed2927b 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -19,6 +19,7 @@
 import 'isolate.dart' as isolate;
 import 'log.dart' as log;
 import 'log.dart';
+import 'pub_embeddable_command.dart';
 import 'solver/type.dart';
 import 'system_cache.dart';
 import 'utils.dart';
@@ -236,7 +237,9 @@
 ///  [CommandResolutionFailedException].
 ///
 /// * Otherwise if the current package resolution is outdated do an implicit
-/// `pub get`, if that fails, throw a [CommandResolutionFailedException].
+///   `pub get`, if that fails, throw a [CommandResolutionFailedException].
+///
+///   This pub get will send analytics events to [analytics] if provided.
 ///
 /// * Otherwise let  `<current>` be the name of the package at [root], and
 ///   interpret [descriptor] as `[<package>][:<command>]`.
@@ -269,6 +272,7 @@
   bool allowSnapshot = true,
   String root,
   String pubCacheDir,
+  PubAnalytics analytics,
 }) async {
   root ??= p.current;
   var asPath = descriptor;
@@ -294,7 +298,11 @@
       entrypoint.assertUpToDate();
     } on DataException {
       await warningsOnlyUnlessTerminal(
-          () => entrypoint.acquireDependencies(SolveType.GET));
+        () => entrypoint.acquireDependencies(
+          SolveType.GET,
+          analytics: analytics,
+        ),
+      );
     }
 
     String command;
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index 49d681f..072065e 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -21,6 +21,7 @@
 import 'log.dart' as log;
 import 'package.dart';
 import 'package_name.dart';
+import 'pub_embeddable_command.dart';
 import 'pubspec.dart';
 import 'sdk.dart';
 import 'solver.dart';
@@ -144,11 +145,11 @@
   /// existing binstubs in other packages will be overwritten by this one's.
   /// Otherwise, the previous ones will be preserved.
   Future<void> activatePath(String path, List<String> executables,
-      {bool overwriteBinStubs}) async {
+      {bool overwriteBinStubs, @required PubAnalytics analytics}) async {
     var entrypoint = Entrypoint(path, cache);
 
     // Get the package's dependencies.
-    await entrypoint.acquireDependencies(SolveType.GET);
+    await entrypoint.acquireDependencies(SolveType.GET, analytics: analytics);
     var name = entrypoint.root.name;
 
     try {
diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart
index 4240ad1..aebe06c 100644
--- a/lib/src/pub_embeddable_command.dart
+++ b/lib/src/pub_embeddable_command.dart
@@ -3,7 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 
 // @dart=2.10
+import 'package:meta/meta.dart';
+import 'package:usage/usage.dart';
 
+import 'command.dart' show PubCommand, PubTopLevel;
 import 'command.dart';
 import 'command/add.dart';
 import 'command/build.dart';
@@ -24,6 +27,16 @@
 import 'log.dart' as log;
 import 'log.dart';
 
+/// The information needed for the embedded pub command to send analytics.
+@sealed
+class PubAnalytics {
+  /// Name of the custom dimension of the dependency kind.
+  final String dependencyKindCustomDimensionName;
+  final Analytics analytics;
+  PubAnalytics(this.analytics,
+      {@required this.dependencyKindCustomDimensionName});
+}
+
 /// Exposes the `pub` commands as a command to be embedded in another command
 /// runner such as `dart pub`.
 class PubEmbeddableCommand extends PubCommand implements PubTopLevel {
@@ -35,9 +48,12 @@
   String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-global';
 
   @override
+  final PubAnalytics analytics;
+
+  @override
   String get directory => argResults['directory'];
 
-  PubEmbeddableCommand() : super() {
+  PubEmbeddableCommand(this.analytics) : super() {
     argParser.addFlag('trace',
         help: 'Print debugging information when an error occurs.');
     argParser.addFlag('verbose',
diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart
index ebcdfd7..c3c98d4 100644
--- a/lib/src/solver/result.dart
+++ b/lib/src/solver/result.dart
@@ -7,10 +7,14 @@
 import 'package:collection/collection.dart';
 import 'package:pub_semver/pub_semver.dart';
 
+import '../io.dart';
 import '../lock_file.dart';
+import '../log.dart' as log;
 import '../package.dart';
 import '../package_name.dart';
+import '../pub_embeddable_command.dart';
 import '../pubspec.dart';
+import '../source/hosted.dart';
 import '../source_registry.dart';
 import '../system_cache.dart';
 import 'report.dart';
@@ -44,6 +48,9 @@
   /// because it found an invalid solution.
   final int attemptedSolutions;
 
+  /// The wall clock time the resolution took.
+  final Duration resolutionTime;
+
   /// The [LockFile] representing the packages selected by this version
   /// resolution.
   LockFile get lockFile {
@@ -89,8 +96,15 @@
         .toSet());
   }
 
-  SolveResult(this._sources, this._root, this._previousLockFile, this.packages,
-      this.pubspecs, this.availableVersions, this.attemptedSolutions);
+  SolveResult(
+      this._sources,
+      this._root,
+      this._previousLockFile,
+      this.packages,
+      this.pubspecs,
+      this.availableVersions,
+      this.attemptedSolutions,
+      this.resolutionTime);
 
   /// Displays a report of what changes were made to the lockfile.
   ///
@@ -118,6 +132,44 @@
     }
   }
 
+  /// Send analytics about the package resolution.
+  void sendAnalytics(PubAnalytics analytics) {
+    ArgumentError.checkNotNull(analytics);
+
+    for (final package in packages) {
+      final source = package.source;
+      // Only send analytics for packages from pub.dev.
+      if (source is HostedSource &&
+          (runningFromTest ||
+              package.description['url'] == HostedSource.pubDevUrl)) {
+        final dependencyKind = const {
+          DependencyType.dev: 'dev',
+          DependencyType.direct: 'direct',
+          DependencyType.none: 'transitive'
+        }[_root.dependencyType(package.name)];
+        analytics.analytics.sendEvent(
+          'pub-get',
+          package.name,
+          label: package.version.canonicalizedVersion,
+          value: 1,
+          parameters: {
+            'ni': '1', // We consider a pub-get a non-interactive event.
+            analytics.dependencyKindCustomDimensionName: dependencyKind,
+          },
+        );
+        log.fine(
+            'Sending analytics hit for "pub-get" of ${package.name} version ${package.version} as dependency-kind $dependencyKind');
+      }
+    }
+    analytics.analytics.sendTiming(
+      'resolution',
+      resolutionTime.inMilliseconds,
+      category: 'pub-get',
+    );
+    log.fine(
+        'Sending analytics timing "pub-get" took ${resolutionTime.inMilliseconds} miliseconds');
+  }
+
   @override
   String toString() => 'Took $attemptedSolutions tries to resolve to\n'
       '- ${packages.join("\n- ")}';
diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart
index ee1541d..4e08a6b 100644
--- a/lib/src/solver/version_solver.dart
+++ b/lib/src/solver/version_solver.dart
@@ -78,6 +78,8 @@
   /// The set of packages for which the lockfile should be ignored.
   final Set<String> _unlock;
 
+  final _stopwatch = Stopwatch();
+
   VersionSolver(this._type, this._systemCache, this._root, this._lockFile,
       Iterable<String> unlock)
       : _dependencyOverrides = _root.pubspec.dependencyOverrides,
@@ -86,8 +88,7 @@
   /// Finds a set of dependencies that match the root package's constraints, or
   /// throws an error if no such set is available.
   Future<SolveResult> solve() async {
-    var stopwatch = Stopwatch()..start();
-
+    _stopwatch.start();
     _addIncompatibility(Incompatibility(
         [Term(PackageRange.root(_root), false)], IncompatibilityCause.root));
 
@@ -103,7 +104,7 @@
       });
     } finally {
       // Gather some solving metrics.
-      log.solver('Version solving took ${stopwatch.elapsed} seconds.\n'
+      log.solver('Version solving took ${_stopwatch.elapsed} seconds.\n'
           'Tried ${_solution.attemptedSolutions} solutions.');
     }
   }
@@ -418,13 +419,15 @@
     }
 
     return SolveResult(
-        _systemCache.sources,
-        _root,
-        _lockFile,
-        decisions,
-        pubspecs,
-        await _getAvailableVersions(decisions),
-        _solution.attemptedSolutions);
+      _systemCache.sources,
+      _root,
+      _lockFile,
+      decisions,
+      pubspecs,
+      await _getAvailableVersions(decisions),
+      _solution.attemptedSolutions,
+      _stopwatch.elapsed,
+    );
   }
 
   /// Generates a map containing all of the known available versions for each
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index 9d98397..986dfa6 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -97,6 +97,8 @@
           ? _OfflineHostedSource(this, systemCache)
           : BoundHostedSource(this, systemCache);
 
+  static String pubDevUrl = 'https://pub.dartlang.org';
+
   /// Gets the default URL for the package server for hosted dependencies.
   Uri get defaultUrl {
     // Changing this to pub.dev raises the following concerns:
diff --git a/pubspec.yaml b/pubspec.yaml
index d3e643b..3bfe095 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -20,10 +20,11 @@
   path: ^1.8.0
   pedantic: ^1.11.0
   pool: ^1.5.0
-  pub_semver: ^2.0.0
+  pub_semver: ^2.1.0
   shelf: ^1.1.1
   source_span: ^1.8.1
   stack_trace: ^1.10.0
+  usage: ^4.0.2
   yaml: ^3.1.0
   yaml_edit: ^2.0.0
 
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart
index 257bf4b..dbe6c89 100644
--- a/test/embedding/embedding_test.dart
+++ b/test/embedding/embedding_test.dart
@@ -4,6 +4,7 @@
 
 // @dart=2.10
 
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:path/path.dart' as path;
@@ -24,8 +25,11 @@
     dynamic exitCode = 0}) async {
   final process = await TestProcess.start(
     Platform.resolvedExecutable,
-    [snapshot, ...args],
-    environment: environment,
+    ['--enable-asserts', snapshot, ...args],
+    environment: {
+      ...getPubTestEnvironment(),
+      ...?environment,
+    },
     workingDirectory: workingDirextory,
   );
   await process.shouldExit(exitCode);
@@ -71,14 +75,14 @@
       d.pubspec({
         'name': 'myapp',
         'environment': {
-          'sdk': '>=2.0.0 <3.0.0',
+          'sdk': '0.1.2+3',
         },
       }),
       d.dir('bin', [
         d.file('main.dart', '''
 import 'dart:io';
 main() { 
-  print("Hi");
+  print('Hi');
   exit(123);
 }
 ''')
@@ -100,4 +104,71 @@
       'test/embedding/goldens/run.txt',
     );
   });
+
+  test('analytics', () async {
+    await servePackages((b) => b
+      ..serve('foo', '1.0.0', deps: {'bar': 'any'})
+      ..serve('bar', '1.0.0'));
+    await d.dir('dep', [
+      d.pubspec({
+        'name': 'dep',
+        'environment': {'sdk': '>=0.0.0 <3.0.0'}
+      })
+    ]).create();
+    final app = d.dir(appPath, [
+      d.appPubspec({
+        'foo': '1.0.0',
+        // The path dependency should not go to analytics.
+        'dep': {'path': '../dep'}
+      })
+    ]);
+    await app.create();
+
+    final buffer = StringBuffer();
+
+    await runEmbedding(
+      ['pub', 'get'],
+      buffer,
+      workingDirextory: app.io.path,
+      environment: {...getPubTestEnvironment(), '_PUB_LOG_ANALYTICS': 'true'},
+    );
+    final analytics = buffer
+        .toString()
+        .split('\n')
+        .where((line) => line.startsWith('[E] [analytics]: '))
+        .map((line) => json.decode(line.substring('[E] [analytics]: '.length)));
+    expect(analytics, {
+      {
+        'hitType': 'event',
+        'message': {
+          'category': 'pub-get',
+          'action': 'foo',
+          'label': '1.0.0',
+          'value': 1,
+          'cd1': 'direct',
+          'ni': '1',
+        }
+      },
+      {
+        'hitType': 'event',
+        'message': {
+          'category': 'pub-get',
+          'action': 'bar',
+          'label': '1.0.0',
+          'value': 1,
+          'cd1': 'transitive',
+          'ni': '1',
+        }
+      },
+      {
+        'hitType': 'timing',
+        'message': {
+          'variableName': 'resolution',
+          'time': isA<int>(),
+          'category': 'pub-get',
+          'label': null
+        }
+      },
+    });
+  });
 }
diff --git a/tool/test-bin/pub_command_runner.dart b/tool/test-bin/pub_command_runner.dart
index 2470a91..810c925 100644
--- a/tool/test-bin/pub_command_runner.dart
+++ b/tool/test-bin/pub_command_runner.dart
@@ -1,22 +1,28 @@
-// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-// @dart=2.10
-
+/// A trivial embedding of the pub command. Used from tests.
+// @dart = 2.11
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:args/args.dart';
 import 'package:args/command_runner.dart';
+import 'package:pub/pub.dart';
 import 'package:pub/src/exit_codes.dart' as exit_codes;
 import 'package:pub/src/log.dart' as log;
-import 'package:pub/src/pub_embeddable_command.dart';
+import 'package:usage/usage.dart';
 
 class Runner extends CommandRunner<int> {
   ArgResults _options;
 
   Runner() : super('pub_command_runner', 'Tests the embeddable pub command.') {
-    addCommand(PubEmbeddableCommand());
+    final analytics = Platform.environment['_PUB_LOG_ANALYTICS'] == 'true'
+        ? PubAnalytics(_LoggingAnalytics(),
+            dependencyKindCustomDimensionName: 'cd1')
+        : null;
+    addCommand(pubCommand(analytics: analytics));
   }
 
   @override
@@ -40,3 +46,51 @@
 Future<void> main(List<String> arguments) async {
   exitCode = await Runner().run(arguments);
 }
+
+class _LoggingAnalytics extends AnalyticsMock {
+  _LoggingAnalytics() {
+    onSend.listen((event) {
+      stderr.writeln('[analytics]${json.encode(event)}');
+    });
+  }
+
+  @override
+  bool get firstRun => false;
+
+  @override
+  Future sendScreenView(String viewName, {Map<String, String> parameters}) {
+    parameters ??= <String, String>{};
+    parameters['viewName'] = viewName;
+    return _log('screenView', parameters);
+  }
+
+  @override
+  Future sendEvent(String category, String action,
+      {String label, int value, Map<String, String> parameters}) {
+    parameters ??= <String, String>{};
+    return _log(
+        'event',
+        {'category': category, 'action': action, 'label': label, 'value': value}
+          ..addAll(parameters));
+  }
+
+  @override
+  Future sendSocial(String network, String action, String target) =>
+      _log('social', {'network': network, 'action': action, 'target': target});
+
+  @override
+  Future sendTiming(String variableName, int time,
+      {String category, String label}) {
+    return _log('timing', {
+      'variableName': variableName,
+      'time': time,
+      'category': category,
+      'label': label
+    });
+  }
+
+  Future<void> _log(String hitType, Map message) async {
+    final encoded = json.encode({'hitType': hitType, 'message': message});
+    stderr.writeln('[analytics]: $encoded');
+  }
+}