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/authentication/client.dart b/lib/src/authentication/client.dart
index 3de5b2e..2ede545 100644
--- a/lib/src/authentication/client.dart
+++ b/lib/src/authentication/client.dart
@@ -77,7 +77,9 @@
                 challenge.scheme == 'bearer' &&
                 challenge.parameters['realm'] == 'pub' &&
                 challenge.parameters['message'] != null);
-        serverMessage = challenge?.parameters['message'];
+        if (challenge?.parameters != null) {
+          serverMessage = challenge.parameters['message'];
+        }
       } on FormatException {
         // Ignore errors might be caused when parsing invalid header values
       }
diff --git a/lib/src/authentication/token_store.dart b/lib/src/authentication/token_store.dart
index 24f70fc..8431088 100644
--- a/lib/src/authentication/token_store.dart
+++ b/lib/src/authentication/token_store.dart
@@ -5,9 +5,12 @@
 // @dart=2.11
 
 import 'dart:convert';
+import 'dart:io';
 
+import 'package:meta/meta.dart';
 import 'package:path/path.dart' as path;
 
+import '../exceptions.dart';
 import '../io.dart';
 import '../log.dart' as log;
 import 'credential.dart';
@@ -28,9 +31,9 @@
   /// Reads "pub-tokens.json" and parses / deserializes it into list of
   /// [Credential].
   List<Credential> _loadCredentials() {
-    final result = List<Credential>.empty(growable: true);
+    final result = <Credential>[];
     final path = _tokensFile;
-    if (!fileExists(path)) {
+    if (path == null || !fileExists(path)) {
       return result;
     }
 
@@ -90,11 +93,21 @@
     return result;
   }
 
+  @alwaysThrows
+  void missingConfigDir() {
+    final variable = Platform.isWindows ? '%APPDATA%' : r'$HOME';
+    throw DataException('No config dir found. Check that $variable is set');
+  }
+
   /// Writes [credentials] into "pub-tokens.json".
   void _saveCredentials(List<Credential> credentials) {
-    ensureDir(path.dirname(_tokensFile));
+    final tokensFile = _tokensFile;
+    if (tokensFile == null) {
+      missingConfigDir();
+    }
+    ensureDir(path.dirname(tokensFile));
     writeTextFile(
-        _tokensFile,
+        tokensFile,
         jsonEncode(<String, dynamic>{
           'version': 1,
           'hosted': credentials.map((it) => it.toJson()).toList(),
@@ -161,10 +174,18 @@
 
   /// Deletes pub-tokens.json file from the disk.
   void deleteTokensFile() {
-    deleteEntry(_tokensFile);
-    log.message('pub-tokens.json is deleted.');
+    final tokensFile = _tokensFile;
+    if (tokensFile == null) {
+      missingConfigDir();
+    } else if (!fileExists(tokensFile)) {
+      log.message('No credentials file found at "$tokensFile"');
+    } else {
+      deleteEntry(_tokensFile);
+      log.message('pub-tokens.json is deleted.');
+    }
   }
 
   /// Full path to the "pub-tokens.json" file.
-  String get _tokensFile => path.join(configDir, 'pub-tokens.json');
+  String get _tokensFile =>
+      configDir == null ? null : path.join(configDir, 'pub-tokens.json');
 }
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/lish.dart b/lib/src/command/lish.dart
index 151ee79..0e7a725 100644
--- a/lib/src/command/lish.dart
+++ b/lib/src/command/lish.dart
@@ -156,7 +156,8 @@
         // explicitly have to define mock servers as official server to test
         // publish command with oauth2 credentials.
         if (runningFromTest &&
-            Platform.environment.containsKey('PUB_HOSTED_URL'))
+            Platform.environment.containsKey('PUB_HOSTED_URL') &&
+            Platform.environment['_PUB_TEST_AUTH_METHOD'] == 'oauth2')
           Platform.environment['PUB_HOSTED_URL'],
       };
 
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/git.dart b/lib/src/git.dart
index 0633169..5fa7536 100644
--- a/lib/src/git.dart
+++ b/lib/src/git.dart
@@ -5,6 +5,8 @@
 /// Helper functionality for invoking Git.
 import 'dart:async';
 
+import 'package:path/path.dart' as p;
+
 import 'exceptions.dart';
 import 'io.dart';
 import 'log.dart' as log;
@@ -103,6 +105,22 @@
 
 String? _commandCache;
 
+/// Returns the root of the git repo [dir] belongs to. Returns `null` if not
+/// in a git repo or git is not installed.
+String? repoRoot(String dir) {
+  if (isInstalled) {
+    try {
+      return p.normalize(
+        runSync(['rev-parse', '--show-toplevel'], workingDir: dir).first,
+      );
+    } on GitException {
+      // Not in a git folder.
+      return null;
+    }
+  }
+  return null;
+}
+
 /// Checks whether [command] is the Git command for this computer.
 bool _tryGitCommand(String command) {
   // If "git --version" prints something familiar, git is working.
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/io.dart b/lib/src/io.dart
index f95408d..b4898ff 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -8,6 +8,8 @@
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:cli_util/cli_util.dart'
+    show EnvironmentNotFoundException, applicationConfigHome;
 import 'package:http/http.dart' show ByteStream;
 import 'package:http_multi_server/http_multi_server.dart';
 import 'package:meta/meta.dart';
@@ -1025,22 +1027,16 @@
 }
 
 /// The location for dart-specific configuration.
-final String dartConfigDir = () {
-  // TODO: Migrate to new value from cli_util
-  if (runningFromTest) {
+///
+/// `null` if no config dir could be found.
+final String? dartConfigDir = () {
+  if (runningFromTest &&
+      Platform.environment.containsKey('_PUB_TEST_CONFIG_DIR')) {
     return Platform.environment['_PUB_TEST_CONFIG_DIR'];
   }
-  String configDir;
-  if (Platform.isLinux) {
-    configDir = Platform.environment['XDG_CONFIG_HOME'] ??
-        path.join(Platform.environment['HOME']!, '.config');
-  } else if (Platform.isWindows) {
-    configDir = Platform.environment['APPDATA']!;
-  } else if (Platform.isMacOS) {
-    configDir = path.join(
-        Platform.environment['HOME']!, 'Library', 'Application Support');
-  } else {
-    configDir = path.join(Platform.environment['HOME']!, '.config');
+  try {
+    return applicationConfigHome('dart');
+  } on EnvironmentNotFoundException {
+    return null;
   }
-  return path.join(configDir, 'dart');
-}()!;
+}();
diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart
index c1284e9..20d32c1 100644
--- a/lib/src/oauth2.dart
+++ b/lib/src/oauth2.dart
@@ -191,10 +191,17 @@
 /// best place for storing secrets, as it might be shared.
 ///
 /// To provide backwards compatibility we use the legacy file if only it exists.
+///
+/// Returns `null` if there is no good place for the file.
 String _credentialsFile(SystemCache cache) {
-  final newCredentialsFile = path.join(dartConfigDir, 'pub-credentials.json');
-  return [newCredentialsFile, _legacyCredentialsFile(cache)]
-      .firstWhere(fileExists, orElse: () => newCredentialsFile);
+  final configDir = dartConfigDir;
+
+  final newCredentialsFile =
+      configDir == null ? null : path.join(configDir, 'pub-credentials.json');
+  return [
+    if (newCredentialsFile != null) newCredentialsFile,
+    _legacyCredentialsFile(cache)
+  ].firstWhere(fileExists, orElse: () => newCredentialsFile);
 }
 
 String _legacyCredentialsFile(SystemCache cache) {
diff --git a/lib/src/package.dart b/lib/src/package.dart
index 5c46709..1aeb9dc 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -224,16 +224,7 @@
     // An in-memory package has no files.
     if (dir == null) return [];
 
-    var root = dir;
-    if (git.isInstalled) {
-      try {
-        root = p.normalize(
-          git.runSync(['rev-parse', '--show-toplevel'], workingDir: dir).first,
-        );
-      } on git.GitException {
-        // Not in a git folder.
-      }
-    }
+    var root = git.repoRoot(dir) ?? dir;
     beneath = p
         .toUri(p.normalize(p.relative(p.join(dir, beneath ?? '.'), from: root)))
         .path;
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/lib/src/validator/gitignore.dart b/lib/src/validator/gitignore.dart
index df01214..0986fed 100644
--- a/lib/src/validator/gitignore.dart
+++ b/lib/src/validator/gitignore.dart
@@ -31,18 +31,25 @@
         '--exclude-standard',
         '--recurse-submodules'
       ], workingDir: entrypoint.root.dir);
+      final root = git.repoRoot(entrypoint.root.dir) ?? entrypoint.root.dir;
+      var beneath = p.posix.joinAll(
+          p.split(p.normalize(p.relative(entrypoint.root.dir, from: root))));
+      if (beneath == './') {
+        beneath = '';
+      }
       String resolve(String path) {
         if (Platform.isWindows) {
-          return p.joinAll([entrypoint.root.dir, ...p.posix.split(path)]);
+          return p.joinAll([root, ...p.posix.split(path)]);
         }
-        return p.join(entrypoint.root.dir, path);
+        return p.join(root, path);
       }
 
       final unignoredByGitignore = Ignore.listFiles(
+        beneath: beneath,
         listDir: (dir) {
           var contents = Directory(resolve(dir)).listSync();
-          return contents.map((entity) => p.posix.joinAll(
-              p.split(p.relative(entity.path, from: entrypoint.root.dir))));
+          return contents.map((entity) =>
+              p.posix.joinAll(p.split(p.relative(entity.path, from: root))));
         },
         ignoreForDir: (dir) {
           final gitIgnore = resolve('$dir/.gitignore');
@@ -52,8 +59,12 @@
           return rules.isEmpty ? null : Ignore(rules);
         },
         isDir: (dir) => dirExists(resolve(dir)),
-      ).toSet();
-
+      ).map((file) {
+        final relative = p.relative(resolve(file), from: entrypoint.root.dir);
+        return Platform.isWindows
+            ? p.posix.joinAll(p.split(relative))
+            : relative;
+      }).toSet();
       final ignoredFilesCheckedIn = checkedIntoGit
           .where((file) => !unignoredByGitignore.contains(file))
           .toList();
diff --git a/lib/src/validator/pubspec_typo.dart b/lib/src/validator/pubspec_typo.dart
index 2e10ed6..a770b76 100644
--- a/lib/src/validator/pubspec_typo.dart
+++ b/lib/src/validator/pubspec_typo.dart
@@ -73,4 +73,6 @@
   'publish_to',
   'false_secrets',
   'flutter',
+  'screenshots',
+  'platforms',
 ];
diff --git a/pubspec.yaml b/pubspec.yaml
index 23fc0c8..3bfe095 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -9,7 +9,7 @@
   analyzer: ^1.5.0
   args: ^2.1.0
   async: ^2.6.1
-  cli_util: ^0.3.0
+  cli_util: ^0.3.5
   collection: ^1.15.0
   crypto: ^3.0.1
   frontend_server_client: ^2.0.0
@@ -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/test/test_pub.dart b/test/test_pub.dart
index 356b959..3c6b94b 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -138,6 +138,7 @@
   int exitCode,
   Map<String, String> environment,
   String workingDirectory,
+  includeParentEnvironment = true,
 }) async {
   if (error != null && warning != null) {
     throw ArgumentError("Cannot pass both 'error' and 'warning'.");
@@ -161,7 +162,8 @@
       silent: silent,
       exitCode: exitCode,
       environment: environment,
-      workingDirectory: workingDirectory);
+      workingDirectory: workingDirectory,
+      includeParentEnvironment: includeParentEnvironment);
 }
 
 Future<void> pubAdd({
@@ -192,6 +194,7 @@
   int exitCode,
   Map<String, String> environment,
   String workingDirectory,
+  bool includeParentEnvironment = true,
 }) async =>
     await pubCommand(
       RunCommand.get,
@@ -202,6 +205,7 @@
       exitCode: exitCode,
       environment: environment,
       workingDirectory: workingDirectory,
+      includeParentEnvironment: includeParentEnvironment,
     );
 
 Future<void> pubUpgrade(
@@ -330,23 +334,27 @@
 ///
 /// If [environment] is given, any keys in it will override the environment
 /// variables passed to the spawned process.
-Future<void> runPub({
-  List<String> args,
-  output,
-  error,
-  outputJson,
-  silent,
-  int exitCode,
-  String workingDirectory,
-  Map<String, String> environment,
-  List<String> input,
-}) async {
+Future<void> runPub(
+    {List<String> args,
+    output,
+    error,
+    outputJson,
+    silent,
+    int exitCode,
+    String workingDirectory,
+    Map<String, String> environment,
+    List<String> input,
+    includeParentEnvironment = true}) async {
   exitCode ??= exit_codes.SUCCESS;
   // Cannot pass both output and outputJson.
   assert(output == null || outputJson == null);
 
   var pub = await startPub(
-      args: args, workingDirectory: workingDirectory, environment: environment);
+    args: args,
+    workingDirectory: workingDirectory,
+    environment: environment,
+    includeParentEnvironment: includeParentEnvironment,
+  );
 
   if (input != null) {
     input.forEach(pub.stdin.writeln);
@@ -379,14 +387,19 @@
 /// package server.
 ///
 /// Any futures in [args] will be resolved before the process is started.
-Future<PubProcess> startPublish(PackageServer server,
-    {List<String> args}) async {
+Future<PubProcess> startPublish(
+  PackageServer server, {
+  List<String> args,
+  String authMethod = 'oauth2',
+  Map<String, String> environment,
+}) async {
   var tokenEndpoint = Uri.parse(server.url).resolve('/token').toString();
   args = ['lish', ...?args];
-  return await startPub(
-      args: args,
-      tokenEndpoint: tokenEndpoint,
-      environment: {'PUB_HOSTED_URL': server.url});
+  return await startPub(args: args, tokenEndpoint: tokenEndpoint, environment: {
+    'PUB_HOSTED_URL': server.url,
+    '_PUB_TEST_AUTH_METHOD': authMethod,
+    if (environment != null) ...environment,
+  });
 }
 
 /// Handles the beginning confirmation process for uploading a packages.
@@ -460,21 +473,12 @@
     String tokenEndpoint,
     String workingDirectory,
     Map<String, String> environment,
-    bool verbose = true}) async {
+    bool verbose = true,
+    includeParentEnvironment = true}) async {
   args ??= [];
 
   ensureDir(_pathInSandbox(appPath));
 
-  // Find a Dart executable we can use to spawn. Use the same one that was
-  // used to run this script itself.
-  var dartBin = Platform.executable;
-
-  // If the executable looks like a path, get its full path. That way we
-  // can still find it when we spawn it with a different working directory.
-  if (dartBin.contains(Platform.pathSeparator)) {
-    dartBin = p.absolute(dartBin);
-  }
-
   // If there's a snapshot for "pub" available we use it. If the snapshot is
   // out-of-date local source the tests will be useless, therefore it is
   // recommended to use a temporary file with a unique name for each test run.
@@ -492,11 +496,20 @@
     ..addAll([pubPath, if (verbose) '--verbose'])
     ..addAll(args);
 
-  return await PubProcess.start(dartBin, dartArgs,
-      environment: getPubTestEnvironment(tokenEndpoint)
-        ..addAll(environment ?? {}),
+  final mergedEnvironment = getPubTestEnvironment(tokenEndpoint);
+  for (final e in (environment ?? {}).entries) {
+    if (e.value == null) {
+      mergedEnvironment.remove(e.key);
+    } else {
+      mergedEnvironment[e.key] = e.value;
+    }
+  }
+
+  return await PubProcess.start(Platform.resolvedExecutable, dartArgs,
+      environment: mergedEnvironment,
       workingDirectory: workingDirectory ?? _pathInSandbox(appPath),
-      description: args.isEmpty ? 'pub' : 'pub ${args.first}');
+      description: args.isEmpty ? 'pub' : 'pub ${args.first}',
+      includeParentEnvironment: includeParentEnvironment);
 }
 
 /// A subclass of [TestProcess] that parses pub's verbose logging output and
diff --git a/test/token/add_token_test.dart b/test/token/add_token_test.dart
index edce8fd..5e6b1f7 100644
--- a/test/token/add_token_test.dart
+++ b/test/token/add_token_test.dart
@@ -141,4 +141,15 @@
 
     await d.dir(configPath, [d.nothing('pub-tokens.json')]).validate();
   });
+
+  test('with empty environment gives error message', () async {
+    await runPub(
+      args: ['token', 'add', 'https://mypub.com'],
+      input: ['auth-token'],
+      error: contains('No config dir found.'),
+      exitCode: exit_codes.DATA,
+      environment: {'_PUB_TEST_CONFIG_DIR': null},
+      includeParentEnvironment: false,
+    );
+  });
 }
diff --git a/test/token/error_message_test.dart b/test/token/error_message_test.dart
new file mode 100644
index 0000000..c5ebc80
--- /dev/null
+++ b/test/token/error_message_test.dart
@@ -0,0 +1,87 @@
+// Copyright (c) 2021, 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
+
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:test/test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+void respondWithWwwAuthenticate(String headerValue) {
+  globalPackageServer.expect('GET', '/api/packages/versions/new', (request) {
+    return shelf.Response(403, headers: {'www-authenticate': headerValue});
+  });
+}
+
+Future<void> expectPubErrorMessage(dynamic matcher) {
+  return runPub(
+    args: ['lish'],
+    environment: {
+      'PUB_HOSTED_URL': globalPackageServer.url,
+      '_PUB_TEST_AUTH_METHOD': 'token',
+    },
+    exitCode: 65,
+    input: ['y'],
+    error: matcher,
+  );
+}
+
+void main() {
+  setUp(() async {
+    await d.validPackage.create();
+    await servePackages();
+    await d.tokensFile({
+      'version': 1,
+      'hosted': [
+        {'url': globalPackageServer.url, 'token': 'access token'},
+      ]
+    }).create();
+  });
+
+  test('prints www-authenticate message', () async {
+    respondWithWwwAuthenticate('bearer realm="pub", message="custom message"');
+    await expectPubErrorMessage(contains('custom message'));
+  });
+
+  test('sanitizes and prints dirty www-authenticate message', () {
+    // Unable to test this case because shelf does not allow characters [1]
+    // that pub cli supposed to sanitize.
+    //
+    // [1] https://github.com/dart-lang/sdk/blob/main/sdk/lib/_http/http_headers.dart#L653-L662
+  });
+
+  test('trims and prints long www-authenticate message', () async {
+    var message = List.generate(2048, (_) => 'a').join();
+
+    respondWithWwwAuthenticate('bearer realm="pub", message="$message"');
+    await expectPubErrorMessage(allOf(
+      isNot(contains(message)),
+      contains(message.substring(0, 1024)),
+    ));
+  });
+
+  test('does not prints message if realm is not equals to pub', () async {
+    respondWithWwwAuthenticate('bearer realm="web", message="custom message"');
+    await expectPubErrorMessage(isNot(contains('custom message')));
+  });
+
+  test('does not prints message if challenge is not equals to bearer',
+      () async {
+    respondWithWwwAuthenticate('basic realm="pub", message="custom message"');
+    await expectPubErrorMessage(isNot(contains('custom message')));
+  });
+
+  test('prints message for bearer challenge for pub realm only', () async {
+    respondWithWwwAuthenticate(
+      'basic realm="pub", message="enter username and password", '
+      'newAuth message="use web portal to login", '
+      'bearer realm="api", message="contact IT dept to enroll", '
+      'bearer realm="pub", '
+      'bearer realm="pub", message="pub realm message"',
+    );
+    await expectPubErrorMessage(contains('pub realm message'));
+  });
+}
diff --git a/test/token/remove_token_test.dart b/test/token/remove_token_test.dart
index f9ce67d..24fefa2 100644
--- a/test/token/remove_token_test.dart
+++ b/test/token/remove_token_test.dart
@@ -61,4 +61,14 @@
 
     await d.dir(configPath, [d.nothing('pub-tokens.json')]).validate();
   });
+
+  test('with empty environment gives error message', () async {
+    await runPub(
+      args: ['token', 'remove', 'http://mypub.com'],
+      error: contains('No config dir found.'),
+      exitCode: exit_codes.DATA,
+      environment: {'_PUB_TEST_CONFIG_DIR': null},
+      includeParentEnvironment: false,
+    );
+  });
 }
diff --git a/test/token/token_authentication_test.dart b/test/token/token_authentication_test.dart
new file mode 100644
index 0000000..94574c7
--- /dev/null
+++ b/test/token/token_authentication_test.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2021, 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
+
+import 'package:test/test.dart';
+
+import '../descriptor.dart' as d;
+import '../lish/utils.dart';
+import '../test_pub.dart';
+
+void main() {
+  setUp(d.validPackage.create);
+
+  test('with a pre existing environment token authenticates', () async {
+    await servePackages();
+    await d.tokensFile({
+      'version': 1,
+      'hosted': [
+        {'url': globalPackageServer.url, 'env': 'TOKEN'},
+      ]
+    }).create();
+    var pub = await startPublish(globalPackageServer,
+        authMethod: 'token', environment: {'TOKEN': 'access token'});
+    await confirmPublish(pub);
+
+    handleUploadForm(globalPackageServer);
+
+    await pub.shouldExit(1);
+  });
+
+  test('with a pre existing opaque token authenticates', () async {
+    await servePackages();
+    await d.tokensFile({
+      'version': 1,
+      'hosted': [
+        {'url': globalPackageServer.url, 'token': 'access token'},
+      ]
+    }).create();
+    var pub = await startPublish(globalPackageServer, authMethod: 'token');
+    await confirmPublish(pub);
+
+    handleUploadForm(globalPackageServer);
+
+    await pub.shouldExit(1);
+  });
+}
diff --git a/test/token/when_receives_401_removes_token_test.dart b/test/token/when_receives_401_removes_token_test.dart
new file mode 100644
index 0000000..cb1f3e4
--- /dev/null
+++ b/test/token/when_receives_401_removes_token_test.dart
@@ -0,0 +1,35 @@
+// Copyright (c) 2021, 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
+
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:test/test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+void main() {
+  setUp(d.validPackage.create);
+
+  test('when receives 401 response removes saved token', () async {
+    await servePackages();
+    await d.tokensFile({
+      'version': 1,
+      'hosted': [
+        {'url': globalPackageServer.url, 'token': 'access token'},
+      ]
+    }).create();
+    var pub = await startPublish(globalPackageServer, authMethod: 'token');
+    await confirmPublish(pub);
+
+    globalPackageServer.expect('GET', '/api/packages/versions/new', (request) {
+      return shelf.Response(401);
+    });
+
+    await pub.shouldExit(65);
+
+    await d.tokensFile({'version': 1, 'hosted': []}).validate();
+  });
+}
diff --git a/test/token/when_receives_403_persists_saved_token_test.dart b/test/token/when_receives_403_persists_saved_token_test.dart
new file mode 100644
index 0000000..4035e5f
--- /dev/null
+++ b/test/token/when_receives_403_persists_saved_token_test.dart
@@ -0,0 +1,40 @@
+// Copyright (c) 2021, 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
+
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:test/test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+void main() {
+  setUp(d.validPackage.create);
+
+  test('when receives 403 response persists saved token', () async {
+    await servePackages();
+    await d.tokensFile({
+      'version': 1,
+      'hosted': [
+        {'url': globalPackageServer.url, 'token': 'access token'},
+      ]
+    }).create();
+    var pub = await startPublish(globalPackageServer, authMethod: 'token');
+    await confirmPublish(pub);
+
+    globalPackageServer.expect('GET', '/api/packages/versions/new', (request) {
+      return shelf.Response(403);
+    });
+
+    await pub.shouldExit(65);
+
+    await d.tokensFile({
+      'version': 1,
+      'hosted': [
+        {'url': globalPackageServer.url, 'token': 'access token'},
+      ]
+    }).validate();
+  });
+}
diff --git a/test/validator/gitignore_test.dart b/test/validator/gitignore_test.dart
index d6140f5..06d3be4 100644
--- a/test/validator/gitignore_test.dart
+++ b/test/validator/gitignore_test.dart
@@ -4,18 +4,23 @@
 
 // @dart=2.10
 
+import 'package:path/path.dart' as p;
 import 'package:pub/src/exit_codes.dart' as exit_codes;
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 
-Future<void> expectValidation(error, int exitCode) async {
+Future<void> expectValidation(
+  error,
+  int exitCode, {
+  String workingDirectory,
+}) async {
   await runPub(
     error: error,
     args: ['publish', '--dry-run'],
     environment: {'_PUB_TEST_SDK_VERSION': '2.12.0'},
-    workingDirectory: d.path(appPath),
+    workingDirectory: workingDirectory ?? d.path(appPath),
     exitCode: exitCode,
   );
 }
@@ -46,4 +51,37 @@
         ]),
         exit_codes.DATA);
   });
+
+  test('Should also consider gitignores from above the package root', () async {
+    await d.git('reporoot', [
+      d.dir(
+        'myapp',
+        [
+          d.file('foo.txt'),
+          ...d.validPackage.contents,
+        ],
+      ),
+    ]).create();
+    final packageRoot = p.join(d.sandbox, 'reporoot', 'myapp');
+    await pubGet(
+        environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'},
+        workingDirectory: packageRoot);
+
+    await expectValidation(contains('Package has 0 warnings.'), 0,
+        workingDirectory: packageRoot);
+
+    await d.dir('reporoot', [
+      d.file('.gitignore', '*.txt'),
+    ]).create();
+
+    await expectValidation(
+        allOf([
+          contains('Package has 1 warning.'),
+          contains('foo.txt'),
+          contains(
+              'Consider adjusting your `.gitignore` files to not ignore those files'),
+        ]),
+        exit_codes.DATA,
+        workingDirectory: packageRoot);
+  });
 }
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');
+  }
+}