[dartdev] Reimplement the dartdev analytics with events and custom dimensions, matching the flutter cli analytics patterns where possible
Bug: https://github.com/dart-lang/sdk/issues/43198
Change-Id: Iff3ac619965142c10864559ff396b64130f18daf
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/161200
Reviewed-by: Devon Carew <devoncarew@google.com>
Commit-Queue: Jaime Wren <jwren@google.com>
diff --git a/pkg/dartdev/lib/dartdev.dart b/pkg/dartdev/lib/dartdev.dart
index d60260f..41f0901 100644
--- a/pkg/dartdev/lib/dartdev.dart
+++ b/pkg/dartdev/lib/dartdev.dart
@@ -22,6 +22,7 @@
import 'src/commands/run.dart';
import 'src/commands/test.dart';
import 'src/core.dart';
+import 'src/events.dart';
import 'src/experiments.dart';
import 'src/sdk.dart';
import 'src/utils.dart';
@@ -35,10 +36,6 @@
final stopwatch = Stopwatch();
int result;
- // The Analytics instance used to report information back to Google Analytics,
- // see lib/src/analytics.dart.
- Analytics analytics;
-
// The exit code for the dartdev process, null indicates that it has not yet
// been set yet. The value is set in the catch and finally blocks below.
int exitCode;
@@ -47,7 +44,9 @@
Object exception;
StackTrace stackTrace;
- analytics =
+ // The Analytics instance used to report information back to Google Analytics,
+ // see lib/src/analytics.dart.
+ Analytics analytics =
createAnalyticsInstance(args.contains('--disable-dartdev-analytics'));
// If we have not printed the analyticsNoticeOnFirstRunMessage to stdout,
@@ -92,11 +91,13 @@
// RunCommand.ddsHost = ddsUrl[0];
// RunCommand.ddsPort = ddsUrl[1];
}
+
String commandName;
try {
stopwatch.start();
final runner = DartdevRunner(args);
+
// Run can't be called with the '--disable-dartdev-analytics' flag, remove
// it if it is contained in args.
if (args.contains('--disable-dartdev-analytics')) {
@@ -119,14 +120,6 @@
)
.toList();
- // Before calling to run, send the first ping to analytics to have the first
- // ping, as well as the command itself, running in parallel.
- if (analytics.enabled) {
- commandName = getCommandStr(args, runner.commands.keys.toList());
- // ignore: unawaited_futures
- analytics.sendEvent(eventCategory, commandName);
- }
-
// If ... help pub ... is in the args list, remove 'help', and add '--help'
// to the end of the list. This will make it possible to use the help
// command to access subcommands of pub such as `dart help pub publish`, see
@@ -135,6 +128,17 @@
args = PubUtils.modifyArgs(args);
}
+ // For the commands format and migrate, dartdev itself sends the
+ // sendScreenView notification to analytics, for all other
+ // dartdev commands (instances of DartdevCommand) the commands send this
+ // to analytics.
+ commandName = ArgParserUtils.getCommandStr(args);
+ if (analytics.enabled &&
+ (commandName == formatCmdName || commandName == migrateCmdName)) {
+ // ignore: unawaited_futures
+ analytics.sendScreenView(commandName);
+ }
+
// Finally, call the runner to execute the command, see DartdevRunner.
result = await runner.run(args);
} catch (e, st) {
@@ -157,7 +161,22 @@
// Send analytics before exiting
if (analytics.enabled) {
- analytics.setSessionValue(exitCodeParam, exitCode);
+ // For commands that are not DartdevCommand instances, we manually create
+ // and send the UsageEvent from here:
+ if (commandName == formatCmdName) {
+ // ignore: unawaited_futures
+ FormatUsageEvent(
+ exitCode: exitCode,
+ args: args,
+ ).send(analyticsInstance);
+ } else if (commandName == migrateCmdName) {
+ // ignore: unawaited_futures
+ MigrateUsageEvent(
+ exitCode: exitCode,
+ args: args,
+ ).send(analyticsInstance);
+ }
+
// ignore: unawaited_futures
analytics.sendTiming(commandName, stopwatch.elapsedMilliseconds,
category: 'commands');
diff --git a/pkg/dartdev/lib/src/analytics.dart b/pkg/dartdev/lib/src/analytics.dart
index 5a1037a..595fec0 100644
--- a/pkg/dartdev/lib/src/analytics.dart
+++ b/pkg/dartdev/lib/src/analytics.dart
@@ -26,7 +26,6 @@
║ `dart --enable-analytics` ║
╚════════════════════════════════════════════════════════════════════════════╝
''';
-const String _unknownCommand = '<unknown>';
const String _appName = 'dartdev';
const String _dartDirectoryName = '.dart';
const String _settingsFileName = 'dartdev.json';
@@ -40,13 +39,15 @@
const String eventCategory = 'dartdev';
const String exitCodeParam = 'exitCode';
-Analytics instance;
+Analytics _instance;
+
+Analytics get analyticsInstance => _instance;
/// Create and return an [Analytics] instance, this value is cached and returned
/// on subsequent calls.
Analytics createAnalyticsInstance(bool disableAnalytics) {
- if (instance != null) {
- return instance;
+ if (_instance != null) {
+ return _instance;
}
// Dartdev tests pass a hidden 'disable-dartdev-analytics' flag which is
@@ -54,16 +55,16 @@
// Also, stdout.hasTerminal is checked, if there is no terminal we infer that
// a machine is running dartdev so we return analytics shouldn't be set.
if (disableAnalytics) {
- instance = DisabledAnalytics(_trackingId, _appName);
- return instance;
+ _instance = DisabledAnalytics(_trackingId, _appName);
+ return _instance;
}
var settingsDir = getDartStorageDirectory();
if (settingsDir == null) {
// Some systems don't support user home directories; for those, fail
// gracefully by returning a disabled analytics object.
- instance = DisabledAnalytics(_trackingId, _appName);
- return instance;
+ _instance = DisabledAnalytics(_trackingId, _appName);
+ return _instance;
}
if (!settingsDir.existsSync()) {
@@ -72,8 +73,8 @@
} catch (e) {
// If we can't create the directory for the analytics settings, fail
// gracefully by returning a disabled analytics object.
- instance = DisabledAnalytics(_trackingId, _appName);
- return instance;
+ _instance = DisabledAnalytics(_trackingId, _appName);
+ return _instance;
}
}
@@ -85,22 +86,8 @@
}
var settingsFile = File(path.join(settingsDir.path, _settingsFileName));
- instance = DartdevAnalytics(_trackingId, settingsFile, _appName);
- return instance;
-}
-
-/// Return the first member from [args] that occurs in [allCommands], otherwise
-/// '<unknown>' is returned.
-///
-/// 'help' is special cased to have 'dart analyze help', 'dart help analyze',
-/// and 'dart analyze --help' all be recorded as a call to 'help' instead of
-/// 'help' and 'analyze'.
-String getCommandStr(List<String> args, List<String> allCommands) {
- if (args.contains('help') || args.contains('-h') || args.contains('--help')) {
- return 'help';
- }
- return args.firstWhere((arg) => allCommands.contains(arg),
- orElse: () => _unknownCommand);
+ _instance = DartdevAnalytics(_trackingId, settingsFile, _appName);
+ return _instance;
}
/// The directory used to store the analytics settings file.
diff --git a/pkg/dartdev/lib/src/commands/analyze.dart b/pkg/dartdev/lib/src/commands/analyze.dart
index 139d262..08d8590 100644
--- a/pkg/dartdev/lib/src/commands/analyze.dart
+++ b/pkg/dartdev/lib/src/commands/analyze.dart
@@ -5,14 +5,18 @@
import 'dart:async';
import 'dart:io';
+import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import '../core.dart';
+import '../events.dart';
import '../sdk.dart';
import '../utils.dart';
import 'analyze_impl.dart';
class AnalyzeCommand extends DartdevCommand<int> {
+ static const String cmdName = 'analyze';
+
/// The maximum length of any of the existing severity labels.
static const int _severityWidth = 7;
@@ -20,7 +24,7 @@
/// message. The width left for the severity label plus the separator width.
static const int _bodyIndentWidth = _severityWidth + 3;
- AnalyzeCommand() : super('analyze', "Analyze the project's Dart code.") {
+ AnalyzeCommand() : super(cmdName, "Analyze the project's Dart code.") {
argParser
..addFlag('fatal-infos',
help: 'Treat info level issues as fatal.', negatable: false)
@@ -32,7 +36,7 @@
String get invocation => '${super.invocation} [<directory>]';
@override
- FutureOr<int> run() async {
+ FutureOr<int> runImpl() async {
if (argResults.rest.length > 1) {
usageException('Only one directory is expected.');
}
@@ -153,4 +157,19 @@
return 0;
}
}
+
+ @override
+ UsageEvent createUsageEvent(int exitCode) => AnalyzeUsageEvent(
+ usagePath,
+ exitCode: exitCode,
+ args: argResults.arguments,
+ );
+}
+
+/// The [UsageEvent] for the analyze command.
+class AnalyzeUsageEvent extends UsageEvent {
+ AnalyzeUsageEvent(String usagePath,
+ {String label, @required int exitCode, @required List<String> args})
+ : super(AnalyzeCommand.cmdName, usagePath,
+ label: label, args: args, exitCode: exitCode);
}
diff --git a/pkg/dartdev/lib/src/commands/compile.dart b/pkg/dartdev/lib/src/commands/compile.dart
index 5da7168..491d7ae 100644
--- a/pkg/dartdev/lib/src/commands/compile.dart
+++ b/pkg/dartdev/lib/src/commands/compile.dart
@@ -6,9 +6,11 @@
import 'dart:io';
import 'package:dart2native/generate.dart';
+import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import '../core.dart';
+import '../events.dart';
import '../sdk.dart';
import '../vm_interop_handler.dart';
@@ -39,12 +41,13 @@
stderr.flush();
return false;
}
-
return true;
}
-class CompileJSCommand extends DartdevCommand<int> {
- CompileJSCommand() : super('js', 'Compile Dart to JavaScript.') {
+class CompileJSCommand extends CompileSubcommandCommand {
+ static const String cmdName = 'js';
+
+ CompileJSCommand() : super(cmdName, 'Compile Dart to JavaScript.') {
argParser
..addOption(
commonOptions['outputFile'].flag,
@@ -63,7 +66,7 @@
String get invocation => '${super.invocation} <dart entry point>';
@override
- FutureOr<int> run() async {
+ FutureOr<int> runImpl() async {
if (!Sdk.checkArtifactExists(sdk.dart2jsSnapshot)) {
return 255;
}
@@ -97,7 +100,10 @@
}
}
-class CompileSnapshotCommand extends DartdevCommand<int> {
+class CompileSnapshotCommand extends CompileSubcommandCommand {
+ static const String jitSnapshotCmdName = 'jit-snapshot';
+ static const String kernelCmdName = 'kernel';
+
final String commandName;
final String help;
final String fileExt;
@@ -121,7 +127,7 @@
String get invocation => '${super.invocation} <dart entry point>';
@override
- FutureOr<int> run() async {
+ FutureOr<int> runImpl() async {
// We expect a single rest argument; the dart entry point.
if (argResults.rest.length != 1) {
// This throws.
@@ -157,7 +163,10 @@
}
}
-class CompileNativeCommand extends DartdevCommand<int> {
+class CompileNativeCommand extends CompileSubcommandCommand {
+ static const String exeCmdName = 'exe';
+ static const String aotSnapshotCmdName = 'aot-snapshot';
+
final String commandName;
final String format;
final String help;
@@ -194,7 +203,7 @@
String get invocation => '${super.invocation} <dart entry point>';
@override
- FutureOr<int> run() async {
+ FutureOr<int> runImpl() async {
if (!Sdk.checkArtifactExists(genKernel) ||
!Sdk.checkArtifactExists(genSnapshot)) {
return 255;
@@ -230,30 +239,64 @@
}
}
-class CompileCommand extends DartdevCommand {
- CompileCommand() : super('compile', 'Compile Dart to various formats.') {
+abstract class CompileSubcommandCommand extends DartdevCommand<int> {
+ CompileSubcommandCommand(String name, String description,
+ {bool hidden = false})
+ : super(name, description, hidden: hidden);
+
+ @override
+ UsageEvent createUsageEvent(int exitCode) => CompileUsageEvent(
+ usagePath,
+ exitCode: exitCode,
+ args: argResults.arguments,
+ );
+}
+
+class CompileCommand extends DartdevCommand<int> {
+ static const String cmdName = 'compile';
+
+ CompileCommand() : super(cmdName, 'Compile Dart to various formats.') {
addSubcommand(CompileJSCommand());
addSubcommand(CompileSnapshotCommand(
- commandName: 'jit-snapshot',
+ commandName: CompileSnapshotCommand.jitSnapshotCmdName,
help: 'to a JIT snapshot.',
fileExt: 'jit',
formatName: 'app-jit',
));
addSubcommand(CompileSnapshotCommand(
- commandName: 'kernel',
+ commandName: CompileSnapshotCommand.kernelCmdName,
help: 'to a kernel snapshot.',
fileExt: 'dill',
formatName: 'kernel',
));
addSubcommand(CompileNativeCommand(
- commandName: 'exe',
+ commandName: CompileNativeCommand.exeCmdName,
help: 'to a self-contained executable.',
format: 'exe',
));
addSubcommand(CompileNativeCommand(
- commandName: 'aot-snapshot',
+ commandName: CompileNativeCommand.aotSnapshotCmdName,
help: 'to an AOT snapshot.',
format: 'aot',
));
}
+
+ @override
+ UsageEvent createUsageEvent(int exitCode) => null;
+
+ @override
+ FutureOr<int> runImpl() {
+ // do nothing, this command is never run
+ return 0;
+ }
+}
+
+/// The [UsageEvent] for all compile commands, we could have each compile
+/// event be its own class instance, but for the time being [usagePath] takes
+/// care of the only difference.
+class CompileUsageEvent extends UsageEvent {
+ CompileUsageEvent(String usagePath,
+ {String label, @required int exitCode, @required List<String> args})
+ : super(CompileCommand.cmdName, usagePath,
+ label: label, exitCode: exitCode, args: args);
}
diff --git a/pkg/dartdev/lib/src/commands/create.dart b/pkg/dartdev/lib/src/commands/create.dart
index 4eda721..920de13 100644
--- a/pkg/dartdev/lib/src/commands/create.dart
+++ b/pkg/dartdev/lib/src/commands/create.dart
@@ -7,14 +7,18 @@
import 'dart:io' as io;
import 'dart:math' as math;
+import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:stagehand/stagehand.dart' as stagehand;
import '../core.dart';
+import '../events.dart';
import '../sdk.dart';
/// A command to create a new project from a set of templates.
-class CreateCommand extends DartdevCommand {
+class CreateCommand extends DartdevCommand<int> {
+ static const String cmdName = 'create';
+
static String defaultTemplateId = 'console-simple';
static List<String> legalTemplateIds = [
@@ -31,7 +35,7 @@
stagehand.getGenerator(templateId);
CreateCommand({bool verbose = false})
- : super('create', 'Create a new project.') {
+ : super(cmdName, 'Create a new project.') {
argParser.addOption(
'template',
allowed: legalTemplateIds,
@@ -60,7 +64,7 @@
String get invocation => '${super.invocation} <directory>';
@override
- FutureOr<int> run() async {
+ FutureOr<int> runImpl() async {
if (argResults['list-templates']) {
log.stdout(_availableTemplatesJson());
return 0;
@@ -139,6 +143,13 @@
}
@override
+ UsageEvent createUsageEvent(int exitCode) => CreateUsageEvent(
+ usagePath,
+ exitCode: exitCode,
+ args: argResults.arguments,
+ );
+
+ @override
String get usageFooter {
int width = legalTemplateIds.map((s) => s.length).reduce(math.max);
String desc = generators.map((g) {
@@ -169,6 +180,14 @@
}
}
+/// The [UsageEvent] for the create command.
+class CreateUsageEvent extends UsageEvent {
+ CreateUsageEvent(String usagePath,
+ {String label, @required int exitCode, @required List<String> args})
+ : super(CreateCommand.cmdName, usagePath,
+ label: label, exitCode: exitCode, args: args);
+}
+
class DirectoryGeneratorTarget extends stagehand.GeneratorTarget {
final stagehand.Generator generator;
final io.Directory dir;
diff --git a/pkg/dartdev/lib/src/commands/fix.dart b/pkg/dartdev/lib/src/commands/fix.dart
index 36a8579..338158e 100644
--- a/pkg/dartdev/lib/src/commands/fix.dart
+++ b/pkg/dartdev/lib/src/commands/fix.dart
@@ -6,19 +6,23 @@
import 'dart:io';
import 'package:analysis_server_client/protocol.dart' hide AnalysisError;
+import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import '../core.dart';
+import '../events.dart';
import '../sdk.dart';
import '../utils.dart';
import 'analyze_impl.dart';
-class FixCommand extends DartdevCommand {
+class FixCommand extends DartdevCommand<int> {
+ static const String cmdName = 'fix';
+
// This command is hidden as its currently experimental.
- FixCommand() : super('fix', 'Fix Dart source code.', hidden: true);
+ FixCommand() : super(cmdName, 'Fix Dart source code.', hidden: true);
@override
- FutureOr<int> run() async {
+ FutureOr<int> runImpl() async {
log.stdout('\n*** The `fix` command is provisional and subject to change '
'or removal in future releases. ***\n');
@@ -76,7 +80,21 @@
}
log.stdout('Done.');
}
-
return 0;
}
+
+ @override
+ UsageEvent createUsageEvent(int exitCode) => FixUsageEvent(
+ usagePath,
+ exitCode: exitCode,
+ args: argResults.arguments,
+ );
+}
+
+/// The [UsageEvent] for the fix command.
+class FixUsageEvent extends UsageEvent {
+ FixUsageEvent(String usagePath,
+ {String label, @required int exitCode, @required List<String> args})
+ : super(FixCommand.cmdName, usagePath,
+ label: label, exitCode: exitCode, args: args);
}
diff --git a/pkg/dartdev/lib/src/commands/pub.dart b/pkg/dartdev/lib/src/commands/pub.dart
index 6b40ebc..c30bc61 100644
--- a/pkg/dartdev/lib/src/commands/pub.dart
+++ b/pkg/dartdev/lib/src/commands/pub.dart
@@ -5,14 +5,37 @@
import 'dart:async';
import 'package:args/args.dart';
+import 'package:meta/meta.dart';
import '../core.dart';
+import '../events.dart';
import '../experiments.dart';
import '../sdk.dart';
import '../vm_interop_handler.dart';
class PubCommand extends DartdevCommand<int> {
- PubCommand() : super('pub', 'Work with packages.');
+ static const String cmdName = 'pub';
+
+ PubCommand() : super(cmdName, 'Work with packages.');
+
+ // TODO(jwren) as soon as pub commands are are implemented directly in
+ // dartdev, remove this static list.
+ /// A list of all subcommands, used only for the implementation of
+ /// [usagePath], see below.
+ static List<String> pubSubcommands = [
+ 'cache',
+ 'deps',
+ 'downgrade',
+ 'get',
+ 'global',
+ 'logout',
+ 'outdated',
+ 'publish',
+ 'run',
+ 'upgrade',
+ 'uploader',
+ 'version',
+ ];
@override
ArgParser createArgParser() => ArgParser.allowAnything();
@@ -34,8 +57,26 @@
VmInteropHandler.run(command, args);
}
+ /// Since the pub subcommands are not subclasses of DartdevCommand, we
+ /// override [usagePath] here as a special case to cover the first subcommand
+ /// under pub, i.e. we will have the path pub/cache
@override
- FutureOr<int> run() async {
+ String get usagePath {
+ if (argResults == null) {
+ return name;
+ }
+ var args = argResults.arguments;
+ var cmdIndex = args.indexOf(name) ?? 0;
+ for (int i = cmdIndex + 1; i < args.length; i++) {
+ if (pubSubcommands.contains(args[i])) {
+ return '$name/${args[i]}';
+ }
+ }
+ return name;
+ }
+
+ @override
+ FutureOr<int> runImpl() async {
if (!Sdk.checkArtifactExists(sdk.pubSnapshot)) {
return 255;
}
@@ -65,4 +106,26 @@
VmInteropHandler.run(command, args);
return 0;
}
+
+ @override
+ UsageEvent createUsageEvent(int exitCode) => PubUsageEvent(
+ usagePath,
+ exitCode: exitCode,
+ specifiedExperiments: specifiedExperiments,
+ args: argResults.arguments,
+ );
+}
+
+/// The [UsageEvent] for the pub command.
+class PubUsageEvent extends UsageEvent {
+ PubUsageEvent(String usagePath,
+ {String label,
+ @required int exitCode,
+ @required List<String> specifiedExperiments,
+ @required List<String> args})
+ : super(PubCommand.cmdName, usagePath,
+ label: label,
+ exitCode: exitCode,
+ specifiedExperiments: specifiedExperiments,
+ args: args);
}
diff --git a/pkg/dartdev/lib/src/commands/run.dart b/pkg/dartdev/lib/src/commands/run.dart
index feeb99c..9fa39e6 100644
--- a/pkg/dartdev/lib/src/commands/run.dart
+++ b/pkg/dartdev/lib/src/commands/run.dart
@@ -8,15 +8,19 @@
import 'dart:io';
import 'package:args/args.dart';
+import 'package:meta/meta.dart';
import 'package:path/path.dart';
import '../core.dart';
+import '../events.dart';
import '../experiments.dart';
import '../sdk.dart';
import '../utils.dart';
import '../vm_interop_handler.dart';
class RunCommand extends DartdevCommand<int> {
+ static const String cmdName = 'run';
+
static bool launchDds = false;
static String ddsHost;
static String ddsPort;
@@ -41,7 +45,7 @@
RunCommand({this.verbose = false})
: super(
- 'run',
+ cmdName,
'Run a Dart program.',
) {
// NOTE: When updating this list of flags, be sure to add any VM flags to
@@ -153,7 +157,7 @@
String get invocation => '${super.invocation} <dart file | package target>';
@override
- FutureOr<int> run() async {
+ FutureOr<int> runImpl() async {
// The command line arguments after 'run'
var args = argResults.arguments.toList();
@@ -236,6 +240,14 @@
VmInteropHandler.run(path, runArgs);
return 0;
}
+
+ @override
+ UsageEvent createUsageEvent(int exitCode) => RunUsageEvent(
+ usagePath,
+ exitCode: exitCode,
+ specifiedExperiments: specifiedExperiments,
+ args: argResults.arguments,
+ );
}
class _DebuggingSession {
@@ -282,3 +294,17 @@
}
}
}
+
+/// The [UsageEvent] for the run command.
+class RunUsageEvent extends UsageEvent {
+ RunUsageEvent(String usagePath,
+ {String label,
+ @required int exitCode,
+ @required List<String> specifiedExperiments,
+ @required List<String> args})
+ : super(RunCommand.cmdName, usagePath,
+ label: label,
+ exitCode: exitCode,
+ specifiedExperiments: specifiedExperiments,
+ args: args);
+}
diff --git a/pkg/dartdev/lib/src/commands/test.dart b/pkg/dartdev/lib/src/commands/test.dart
index dcb5a7f..beba8c8 100644
--- a/pkg/dartdev/lib/src/commands/test.dart
+++ b/pkg/dartdev/lib/src/commands/test.dart
@@ -5,8 +5,10 @@
import 'dart:async';
import 'package:args/args.dart';
+import 'package:meta/meta.dart';
import '../core.dart';
+import '../events.dart';
import '../experiments.dart';
import '../sdk.dart';
import '../vm_interop_handler.dart';
@@ -15,7 +17,9 @@
///
/// This command largely delegates to `pub run test`.
class TestCommand extends DartdevCommand<int> {
- TestCommand() : super('test', 'Run tests in this package.');
+ static const String cmdName = 'test';
+
+ TestCommand() : super(cmdName, 'Run tests in this package.');
@override
final ArgParser argParser = ArgParser.allowAnything();
@@ -26,7 +30,7 @@
}
@override
- FutureOr<int> run() async {
+ FutureOr<int> runImpl() async {
return _runImpl(argResults.arguments.toList());
}
@@ -70,6 +74,14 @@
return 0;
}
+ @override
+ UsageEvent createUsageEvent(int exitCode) => TestUsageEvent(
+ usagePath,
+ exitCode: exitCode,
+ specifiedExperiments: specifiedExperiments,
+ args: argResults.arguments,
+ );
+
void _printNoPubspecMessage(bool wasHelpCommand) {
log.stdout('''
No pubspec.yaml file found; please run this command from the root of your project.
@@ -118,6 +130,20 @@
}
}
+/// The [UsageEvent] for the test command.
+class TestUsageEvent extends UsageEvent {
+ TestUsageEvent(String usagePath,
+ {String label,
+ @required int exitCode,
+ @required List<String> specifiedExperiments,
+ @required List<String> args})
+ : super(TestCommand.cmdName, usagePath,
+ label: label,
+ exitCode: exitCode,
+ specifiedExperiments: specifiedExperiments,
+ args: args);
+}
+
const String _terseHelp = 'Run tests in this package.';
const String _usageHelp = 'Usage: dart test [files or directories...]';
diff --git a/pkg/dartdev/lib/src/core.dart b/pkg/dartdev/lib/src/core.dart
index cc3a505..5694415 100644
--- a/pkg/dartdev/lib/src/core.dart
+++ b/pkg/dartdev/lib/src/core.dart
@@ -2,6 +2,7 @@
// 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.
+import 'dart:async';
import 'dart:convert';
import 'dart:io';
@@ -10,6 +11,8 @@
import 'package:cli_util/cli_logging.dart';
import 'package:path/path.dart' as path;
+import 'analytics.dart';
+import 'events.dart';
import 'experiments.dart';
import 'sdk.dart';
import 'utils.dart';
@@ -39,6 +42,55 @@
@override
ArgParser get argParser => _argParser ??= createArgParser();
+ /// This method should not be overridden by subclasses, instead classes should
+ /// override [runImpl] and [createUsageEvent]. If analytics is enabled by this
+ /// command and the user, a [sendScreenView] is called to analytics, and then
+ /// after the command is run, an event is sent to analytics.
+ ///
+ /// If analytics is not enabled by this command or the user, then [runImpl] is
+ /// called and the exitCode value is returned.
+ @override
+ FutureOr<int> run() async {
+ var path = usagePath;
+ if (path != null &&
+ analyticsInstance != null &&
+ analyticsInstance.enabled) {
+ // Send the screen view to analytics
+ // ignore: unawaited_futures
+ analyticsInstance.sendScreenView(path);
+
+ // Run this command
+ var exitCode = await runImpl();
+
+ // Send the event to analytics
+ // ignore: unawaited_futures
+ createUsageEvent(exitCode)?.send(analyticsInstance);
+
+ // Finally return the exit code
+ return exitCode;
+ } else {
+ // Analytics is not enabled, run the command and return the exit code
+ return runImpl();
+ }
+ }
+
+ UsageEvent createUsageEvent(int exitCode);
+
+ FutureOr<int> runImpl();
+
+ /// The command name path to send to Google Analytics. Return null to disable
+ /// tracking of the command.
+ String get usagePath {
+ if (parent is DartdevCommand) {
+ final commandParent = parent as DartdevCommand;
+ final parentPath = commandParent.usagePath;
+ // Don't report for parents that return null for usagePath.
+ return parentPath == null ? null : '$parentPath/$name';
+ } else {
+ return name;
+ }
+ }
+
/// Create the ArgParser instance for this command.
///
/// Subclasses can override this in order to create a customized ArgParser.
diff --git a/pkg/dartdev/lib/src/events.dart b/pkg/dartdev/lib/src/events.dart
new file mode 100644
index 0000000..6cc5547
--- /dev/null
+++ b/pkg/dartdev/lib/src/events.dart
@@ -0,0 +1,204 @@
+// 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.
+
+import 'package:meta/meta.dart';
+import 'package:usage/usage.dart';
+
+import 'commands/analyze.dart';
+import 'commands/compile.dart';
+import 'commands/create.dart';
+import 'commands/fix.dart';
+import 'commands/pub.dart';
+import 'commands/run.dart';
+
+/// A list of all commands under dartdev.
+const List<String> allCommands = [
+ 'help',
+ AnalyzeCommand.cmdName,
+ CreateCommand.cmdName,
+ CompileCommand.cmdName,
+ FixCommand.cmdName,
+ formatCmdName,
+ migrateCmdName,
+ PubCommand.cmdName,
+ RunCommand.cmdName,
+ 'test'
+];
+
+/// The [String] identifier `dartdev`, used as the category in the events sent
+/// to analytics.
+const String _dartdev = 'dartdev';
+
+/// The [String] identifier `format`.
+const String formatCmdName = 'format';
+
+/// The [String] identifier `migrate`.
+const String migrateCmdName = 'migrate';
+
+/// The separator used to for joining the flag sets sent to analytics.
+const String _flagSeparator = ',';
+
+/// When some unknown command is used, for instance `dart foo`, the command is
+/// designated with this identifier.
+const String _unknownCommand = '<unknown>';
+
+/// The collection of custom dimensions understood by the analytics backend.
+/// When adding to this list, first ensure that the custom dimension is
+/// defined in the backend, or will be defined shortly after the relevant PR
+/// lands. The pattern here matches the flutter cli.
+enum CustomDimensions {
+ commandExitCode, // cd1
+ enabledExperiments, // cd2
+ commandFlags, // cd3
+}
+
+String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}';
+
+Map<String, String> _useCdKeys(Map<CustomDimensions, String> parameters) {
+ return parameters.map(
+ (CustomDimensions k, String v) => MapEntry<String, String>(cdKey(k), v));
+}
+
+/// Utilities for parsing arguments passed to dartdev. These utilities have all
+/// been marked as static to assist with testing, see events_test.dart.
+class ArgParserUtils {
+ /// Return the first member from [args] that occurs in [allCommands],
+ /// otherwise '<unknown>' is returned.
+ ///
+ /// 'help' is special cased to have 'dart analyze help', 'dart help analyze',
+ /// and 'dart analyze --help' all be recorded as a call to 'help' instead of
+ /// 'help' and 'analyze'.
+ static String getCommandStr(List<String> args) {
+ if (args.contains('help') ||
+ args.contains('-h') ||
+ args.contains('--help')) {
+ return 'help';
+ }
+ return args.firstWhere((arg) => allCommands.contains(arg),
+ orElse: () => _unknownCommand);
+ }
+
+ /// Return true if the first character of the passed [String] is '-'.
+ static bool isFlag(String arg) => arg != null && arg.startsWith('-');
+
+ /// Returns true if and only if the passed argument equals 'help', '--help' or
+ /// '-h'.
+ static bool isHelp(String arg) =>
+ arg == 'help' || arg == '--help' || arg == '-h';
+
+ /// Given some command in args, return the set of flags after the command.
+ static List<String> parseCommandFlags(String command, List<String> args) {
+ var result = <String>[];
+ if (args == null || args.isEmpty) {
+ return result;
+ }
+
+ var indexOfCmd = args.indexOf(command);
+ if (indexOfCmd < 0) {
+ return result;
+ }
+
+ for (var i = indexOfCmd + 1; i < args.length; i++) {
+ if (!isHelp(args[i]) && isFlag(args[i])) {
+ result.add(sanitizeFlag(args[i]));
+ }
+ }
+ return result;
+ }
+
+ /// Return the passed flag, only if it is considered a flag, see [isFlag], and
+ /// if '=' is in the flag, return only the contents of the left hand side of
+ /// the '='.
+ static String sanitizeFlag(String arg) {
+ if (isFlag(arg)) {
+ if (arg.contains('=')) {
+ return arg.substring(0, arg.indexOf('='));
+ } else {
+ return arg;
+ }
+ }
+ return '';
+ }
+}
+
+/// The [UsageEvent] for the format command.
+class FormatUsageEvent extends UsageEvent {
+ FormatUsageEvent(
+ {String label, @required int exitCode, @required List<String> args})
+ : super(formatCmdName, formatCmdName,
+ label: label, exitCode: exitCode, args: args);
+}
+
+/// The [UsageEvent] for the migrate command.
+class MigrateUsageEvent extends UsageEvent {
+ MigrateUsageEvent(
+ {String label, @required int exitCode, @required List<String> args})
+ : super(migrateCmdName, migrateCmdName,
+ label: label, exitCode: exitCode, args: args);
+}
+
+/// The superclass for all dartdev events, see the [send] method to see what is
+/// sent to analytics.
+abstract class UsageEvent {
+ /// The category stores the name of this cli tool, 'dartdev'. This matches the
+ /// pattern from the flutter cli tool which always passes 'flutter' as the
+ /// category.
+ final String category;
+
+ /// The action is the command, and optionally the subcommand, joined with '/',
+ /// an example here is 'pub/get'. The usagePath getter in each of the
+ final String action;
+
+ /// The command name being executed here, 'analyze' and 'pub' are examples.
+ final String command;
+
+ /// Labels are not used yet used when reporting dartdev analytics, but the API
+ /// is included here for possible future use.
+ final String label;
+
+ /// The [String] list of arguments passed to dartdev, the list of args is not
+ /// passed back via analytics itself, but is used to compute other values such
+ /// as the [enabledExperiments] which are passed back as part of analytics.
+ final List<String> args;
+
+ /// The exit code returned from this invocation of dartdev.
+ final int exitCode;
+
+ /// A comma separated list of enabled experiments passed into the dartdev
+ /// command. If the command doesn't use the experiments, they are not reported
+ /// in the [UsageEvent].
+ final String enabledExperiments;
+
+ /// A comma separated list of flags on this commands
+ final String commandFlags;
+
+ UsageEvent(
+ this.command,
+ this.action, {
+ this.label,
+ List<String> specifiedExperiments,
+ @required this.exitCode,
+ @required this.args,
+ }) : category = _dartdev,
+ enabledExperiments = specifiedExperiments?.join(_flagSeparator),
+ commandFlags = ArgParserUtils.parseCommandFlags(command, args)
+ .join(_flagSeparator);
+
+ Future send(Analytics analytics) {
+ final Map<String, String> parameters =
+ _useCdKeys(<CustomDimensions, String>{
+ if (exitCode != null)
+ CustomDimensions.commandExitCode: exitCode.toString(),
+ if (enabledExperiments != null)
+ CustomDimensions.enabledExperiments: enabledExperiments,
+ if (commandFlags != null) CustomDimensions.commandFlags: commandFlags,
+ });
+ return analytics.sendEvent(
+ category,
+ action,
+ label: label,
+ parameters: parameters,
+ );
+ }
+}
diff --git a/pkg/dartdev/pubspec.yaml b/pkg/dartdev/pubspec.yaml
index be43698..ecc93eb 100644
--- a/pkg/dartdev/pubspec.yaml
+++ b/pkg/dartdev/pubspec.yaml
@@ -15,6 +15,8 @@
dart2native:
path: ../dart2native
dart_style: any
+ meta:
+ path: ../meta
nnbd_migration:
path: ../nnbd_migration
path: ^1.0.0
diff --git a/pkg/dartdev/test/analytics_test.dart b/pkg/dartdev/test/analytics_test.dart
index 3ce90de..31355df7 100644
--- a/pkg/dartdev/test/analytics_test.dart
+++ b/pkg/dartdev/test/analytics_test.dart
@@ -7,7 +7,6 @@
void main() {
group('DisabledAnalytics', disabledAnalyticsObject);
- group('utils', utils);
}
void disabledAnalyticsObject() {
@@ -19,21 +18,3 @@
expect(diabledAnalytics.firstRun, isFalse);
});
}
-
-void utils() {
- test('getCommandStr', () {
- var commands = <String>['help', 'foo', 'bar', 'baz'];
-
- // base cases
- expect(getCommandStr(['help'], commands), 'help');
- expect(getCommandStr(['bar', 'help'], commands), 'help');
- expect(getCommandStr(['help', 'bar'], commands), 'help');
- expect(getCommandStr(['bar', '-h'], commands), 'help');
- expect(getCommandStr(['bar', '--help'], commands), 'help');
-
- // non-trivial tests
- expect(getCommandStr(['foo'], commands), 'foo');
- expect(getCommandStr(['bar', 'baz'], commands), 'bar');
- expect(getCommandStr(['bazz'], commands), '<unknown>');
- });
-}
diff --git a/pkg/dartdev/test/core_test.dart b/pkg/dartdev/test/core_test.dart
index bc10cb9..0944f88 100644
--- a/pkg/dartdev/test/core_test.dart
+++ b/pkg/dartdev/test/core_test.dart
@@ -5,6 +5,13 @@
import 'dart:convert';
import 'dart:io';
+import 'package:dartdev/src/commands/analyze.dart';
+import 'package:dartdev/src/commands/compile.dart';
+import 'package:dartdev/src/commands/create.dart';
+import 'package:dartdev/src/commands/fix.dart';
+import 'package:dartdev/src/commands/pub.dart';
+import 'package:dartdev/src/commands/run.dart';
+import 'package:dartdev/src/commands/test.dart';
import 'package:dartdev/src/core.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
@@ -12,10 +19,82 @@
import 'utils.dart';
void main() {
+ group('DartdevCommand', _dartdevCommand);
group('PackageConfig', _packageConfig);
group('Project', _project);
}
+void _dartdevCommand() {
+ void _assertDartdevCommandProperties(
+ DartdevCommand command, String name, String usagePath,
+ [int subcommandCount = 0]) {
+ expect(command, isNotNull);
+ expect(command.name, name);
+ expect(command.description, isNotEmpty);
+ expect(command.project, isNotNull);
+ expect(command.argParser, isNotNull);
+ expect(command.usagePath, usagePath);
+ expect(command.subcommands.length, subcommandCount);
+ }
+
+ test('analyze', () {
+ _assertDartdevCommandProperties(AnalyzeCommand(), 'analyze', 'analyze');
+ });
+
+ test('compile', () {
+ _assertDartdevCommandProperties(CompileCommand(), 'compile', 'compile', 5);
+ });
+
+ test('compile/js', () {
+ _assertDartdevCommandProperties(
+ CompileCommand().subcommands['js'], 'js', 'compile/js');
+ });
+
+ test('compile/jit-snapshot', () {
+ _assertDartdevCommandProperties(
+ CompileCommand().subcommands['jit-snapshot'],
+ 'jit-snapshot',
+ 'compile/jit-snapshot');
+ });
+
+ test('compile/kernel', () {
+ _assertDartdevCommandProperties(
+ CompileCommand().subcommands['kernel'], 'kernel', 'compile/kernel');
+ });
+
+ test('compile/exe', () {
+ _assertDartdevCommandProperties(
+ CompileCommand().subcommands['exe'], 'exe', 'compile/exe');
+ });
+
+ test('compile/aot-snapshot', () {
+ _assertDartdevCommandProperties(
+ CompileCommand().subcommands['aot-snapshot'],
+ 'aot-snapshot',
+ 'compile/aot-snapshot');
+ });
+
+ test('create', () {
+ _assertDartdevCommandProperties(CreateCommand(), 'create', 'create');
+ });
+
+ test('fix', () {
+ _assertDartdevCommandProperties(FixCommand(), 'fix', 'fix');
+ });
+
+ test('pub', () {
+ _assertDartdevCommandProperties(PubCommand(), 'pub', 'pub');
+ });
+
+ test('run', () {
+ _assertDartdevCommandProperties(RunCommand(), 'run', 'run');
+ });
+
+ test('test', () {
+ _assertDartdevCommandProperties(TestCommand(), 'test', 'test');
+ });
+}
+
void _packageConfig() {
test('packages', () {
PackageConfig packageConfig = PackageConfig(jsonDecode(_packageData));
diff --git a/pkg/dartdev/test/events_test.dart b/pkg/dartdev/test/events_test.dart
new file mode 100644
index 0000000..c39e7c4
--- /dev/null
+++ b/pkg/dartdev/test/events_test.dart
@@ -0,0 +1,130 @@
+// 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.
+
+import 'package:dartdev/dartdev.dart';
+import 'package:dartdev/src/events.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('ArgParserUtils', _argParserUtils);
+ group('event constant', _constants);
+}
+
+void _argParserUtils() {
+ test('getCommandStr help', () {
+ expect(ArgParserUtils.getCommandStr(['help']), 'help');
+ expect(ArgParserUtils.getCommandStr(['analyze', 'help']), 'help');
+ expect(ArgParserUtils.getCommandStr(['help', 'analyze']), 'help');
+ expect(ArgParserUtils.getCommandStr(['analyze', '-h']), 'help');
+ expect(ArgParserUtils.getCommandStr(['analyze', '--help']), 'help');
+ });
+
+ test('getCommandStr command', () {
+ expect(ArgParserUtils.getCommandStr(['analyze']), 'analyze');
+ expect(ArgParserUtils.getCommandStr(['analyze', 'foo']), 'analyze');
+ expect(ArgParserUtils.getCommandStr(['foo', 'bar']), '<unknown>');
+ expect(ArgParserUtils.getCommandStr([]), '<unknown>');
+ expect(ArgParserUtils.getCommandStr(['']), '<unknown>');
+ });
+
+ test('isHelp false', () {
+ expect(ArgParserUtils.isHelp(null), isFalse);
+ expect(ArgParserUtils.isHelp(''), isFalse);
+ expect(ArgParserUtils.isHelp(' '), isFalse);
+ expect(ArgParserUtils.isHelp('-help'), isFalse);
+ expect(ArgParserUtils.isHelp('--HELP'), isFalse);
+ expect(ArgParserUtils.isHelp('--Help'), isFalse);
+ expect(ArgParserUtils.isHelp('Help'), isFalse);
+ expect(ArgParserUtils.isHelp('HELP'), isFalse);
+ expect(ArgParserUtils.isHelp('foo'), isFalse);
+ });
+
+ test('isHelp true', () {
+ expect(ArgParserUtils.isHelp('help'), isTrue);
+ expect(ArgParserUtils.isHelp('--help'), isTrue);
+ expect(ArgParserUtils.isHelp('-h'), isTrue);
+ });
+
+ test('isFlag false', () {
+ expect(ArgParserUtils.isFlag(null), isFalse);
+ expect(ArgParserUtils.isFlag(''), isFalse);
+ expect(ArgParserUtils.isFlag(' '), isFalse);
+ expect(ArgParserUtils.isFlag('help'), isFalse);
+ expect(ArgParserUtils.isFlag('_flag'), isFalse);
+ });
+
+ test('isFlag true', () {
+ expect(ArgParserUtils.isFlag('-'), isTrue);
+ expect(ArgParserUtils.isFlag('--'), isTrue);
+ expect(ArgParserUtils.isFlag('--flag'), isTrue);
+ expect(ArgParserUtils.isFlag('--help'), isTrue);
+ expect(ArgParserUtils.isFlag('-h'), isTrue);
+ });
+
+ test('parseCommandFlags analyze', () {
+ expect(
+ ArgParserUtils.parseCommandFlags('analyze', [
+ '-g',
+ 'analyze',
+ ]),
+ <String>[]);
+ expect(
+ ArgParserUtils.parseCommandFlags('analyze', [
+ '-g',
+ 'analyze',
+ '--one',
+ '--two',
+ '--three=bar',
+ '-f',
+ '--fatal-infos',
+ '-h',
+ 'five'
+ ]),
+ <String>['--one', '--two', '--three', '-f', '--fatal-infos']);
+ });
+
+ test('parseCommandFlags trivial', () {
+ expect(ArgParserUtils.parseCommandFlags('foo', []), <String>[]);
+ expect(ArgParserUtils.parseCommandFlags('foo', ['']), <String>[]);
+ expect(
+ ArgParserUtils.parseCommandFlags('foo', ['bar', '-flag']), <String>[]);
+ expect(
+ ArgParserUtils.parseCommandFlags('foo', ['--global', 'bar', '-flag']),
+ <String>[]);
+ expect(ArgParserUtils.parseCommandFlags('foo', ['--global', 'fo', '-flag']),
+ <String>[]);
+ expect(
+ ArgParserUtils.parseCommandFlags('foo', ['--global', 'FOO', '-flag']),
+ <String>[]);
+ });
+
+ test('parseCommandFlags exclude help', () {
+ expect(
+ ArgParserUtils.parseCommandFlags(
+ 'analyze', ['-g', 'analyze', '--flag', '--help']),
+ <String>['--flag']);
+ expect(
+ ArgParserUtils.parseCommandFlags(
+ 'analyze', ['-g', 'analyze', '--flag', '-h']),
+ <String>['--flag']);
+ expect(
+ ArgParserUtils.parseCommandFlags(
+ 'analyze', ['-g', 'analyze', '--flag', 'help']),
+ <String>['--flag']);
+ });
+
+ test('sanitizeFlag', () {
+ expect(ArgParserUtils.sanitizeFlag(null), '');
+ expect(ArgParserUtils.sanitizeFlag(''), '');
+ expect(ArgParserUtils.sanitizeFlag('foo'), '');
+ expect(ArgParserUtils.sanitizeFlag('--foo'), '--foo');
+ expect(ArgParserUtils.sanitizeFlag('--foo=bar'), '--foo');
+ });
+}
+
+void _constants() {
+ test('allCommands', () {
+ expect(allCommands, DartdevRunner([]).commands.keys.toList());
+ });
+}
diff --git a/pkg/dartdev/test/test_all.dart b/pkg/dartdev/test/test_all.dart
index a3b6db4..ca092a4 100644
--- a/pkg/dartdev/test/test_all.dart
+++ b/pkg/dartdev/test/test_all.dart
@@ -17,6 +17,7 @@
import 'commands/run_test.dart' as run;
import 'commands/test_test.dart' as test;
import 'core_test.dart' as core;
+import 'events_test.dart' as events;
import 'experiments_test.dart' as experiments;
import 'sdk_test.dart' as sdk;
import 'utils_test.dart' as utils;
@@ -26,6 +27,7 @@
analytics.main();
analyze.main();
create.main();
+ events.main();
experiments.main();
fix.main();
flag.main();