blob: 6cc55475758b36271e5cc62650ff91e0a911fb75 [file] [log] [blame]
// 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,
);
}
}