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');
+ }
+}