@salesforce/plugin-telemetry
Version:
Command usage and error telemetry for the Salesforce CLI
206 lines • 9.38 kB
JavaScript
/*
* Copyright 2025, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Flags, Parser } from '@oclif/core';
import { AsyncCreatable } from '@salesforce/kit';
import { isNumber } from '@salesforce/ts-types';
import { parseVarArgs } from '@salesforce/sf-plugins-core';
import { debug } from './debugger.js';
import { getRelevantEnvs } from './gatherEnvs.js';
export class CommandExecution extends AsyncCreatable {
status;
specifiedFlags = [];
specifiedFlagFullNames = [];
deprecatedFlagsUsed = [];
deprecatedCommandUsed;
command;
argv;
config;
orgId;
devhubId;
agentPseudoTypeUsed;
orgApiVersion;
devhubApiVersion;
argKeys = [];
constructor(options) {
super(options);
this.command = options.command;
this.argv = options.argv;
this.config = options.config;
}
/**
* @deprecated. Will always return en empty string.
*/
static async resolveVCSInfo() {
return Promise.resolve('');
}
toJson() {
const pluginInfo = this.getPluginInfo();
const envs = getRelevantEnvs();
return {
eventName: 'COMMAND_EXECUTION',
// System information
platform: this.config.platform,
shell: this.config.shell,
arch: this.config.arch,
nodeEnv: process.env.NODE_ENV,
nodeVersion: process.version,
processUptime: process.uptime() * 1000,
// CLI information
version: this.config.version,
channel: this.config.channel,
executable: this.config.bin,
origin: this.config.userAgent,
plugin: pluginInfo.name,
// eslint-disable-next-line camelcase
plugin_version: pluginInfo.version,
command: this.command.id,
// As the user specified, including short names
specifiedFlags: this.specifiedFlags.sort().join(' '),
// Flags the user specified, only the full names
specifiedFlagFullNames: this.specifiedFlagFullNames.sort().join(' '),
agentPseudoTypeUsed: this.agentPseudoTypeUsed ?? false,
deprecatedFlagsUsed: this.deprecatedFlagsUsed.sort().join(' '),
deprecatedCommandUsed: this.deprecatedCommandUsed,
sfdxEnv: process.env.SFDX_ENV,
s3HostOverride: process.env.SF_S3_HOST ?? process.env.SFDX_S3_HOST,
npmRegistryOverride: process.env.SF_NPM_REGISTRY ?? process.env.SFDX_NPM_REGISTRY,
tool: process.env.SFDX_TOOL,
// Execution information
date: new Date().toUTCString(),
// Don't log status or timestamp as a number, otherwise vscode will think it is a metric
status: isNumber(this.status) ? this.status.toString() : undefined,
timestamp: String(Date.now()),
// Salesforce Information
orgId: this.orgId,
devhubId: this.devhubId,
orgApiVersion: this.orgApiVersion,
devhubApiVersion: this.devhubApiVersion,
specifiedEnvs: envs.specifiedEnvs.join(' '),
uniqueEnvs: envs.uniqueEnvs.join(' '),
argKeys: this.argKeys.sort().join(' '),
};
}
getPluginInfo() {
return {
name: this.command.plugin?.name,
version: this.command.plugin?.version,
};
}
getCommandName() {
return this.command.id;
}
async init() {
const argv = this.argv;
const flagDefinitions = {
...this.command.flags,
...this.command.baseFlags,
...(this.command.enableJsonFlag ? { json: Flags.boolean() } : {}),
};
// slice off node or bin path, and the executable path, and then remove anything that's been processed as a flag
const typedCommand = process.argv
.splice(2)
.filter((arg) => !argv.includes(arg))
.join(':');
if (Boolean(this.command.deprecationOptions) ||
(typedCommand !== this.command.id &&
this.command.aliases?.includes(typedCommand) &&
this.command.deprecateAliases)) {
// check the deprecationOptions for cases where we've used OCLIF to point to a replacement
// check the aliases and deprecated aliases for where we're deprecating in place
this.deprecatedCommandUsed = typedCommand;
}
let flags = {};
try {
const parseResult = await Parser.parse(argv, {
flags: flagDefinitions,
args: this.command.args,
// @ts-expect-error because varargs is not on SfCommand but is on SfdxCommand
strict: this.command.strict ?? !this.command.varargs,
});
flags = parseResult.flags;
this.agentPseudoTypeUsed =
flags['metadata']?.some((metadata) => metadata.toLowerCase().startsWith('agent')) ??
false;
this.argKeys = [...new Set(Object.keys(parseVarArgs(parseResult.args, parseResult.argv)))];
}
catch (error) {
debug('Error parsing flags');
}
this.orgId = flags['target-org'] ? flags['target-org'].getOrgId() : null;
this.devhubId = flags['target-dev-hub'] ? flags['target-dev-hub'].getOrgId() : null;
this.orgApiVersion = flags['target-org']
? flags['target-org'].getConnection().getApiVersion()
: null;
this.devhubApiVersion = flags['target-dev-hub']
? flags['target-dev-hub'].getConnection().getApiVersion()
: null;
this.determineSpecifiedFlags(argv, flags, flagDefinitions);
}
determineSpecifiedFlags(argv, flags, flagDefinitions) {
// Help won't be in the parsed flags
const shortHelp = argv.find((arg) => /^-h$/.test(arg));
const fullHelp = argv.find((arg) => /^--help$/.test(arg));
if (Boolean(shortHelp) || fullHelp) {
if (shortHelp) {
this.specifiedFlags.push('h');
}
else {
this.specifiedFlags.push('help');
}
this.specifiedFlagFullNames.push('help');
// All other flags don't matter if help is specified, so end here.
}
else {
Object.keys(flags).forEach((flagName) => {
const shortCode = flagDefinitions[flagName] && flagDefinitions[flagName].char;
// Oclif will include the flag if there is a default, but we only want to add it if the
// user specified it, so confirm in the argv list.
if (shortCode && argv.find((arg) => new RegExp(`^-${shortCode}(=.*)?$`).test(arg))) {
this.specifiedFlags.push(shortCode);
this.specifiedFlagFullNames.push(flagName);
}
else if (argv.find((arg) => new RegExp(`^--${flagName}(=.*)?$`).test(arg))) {
this.specifiedFlags.push(flagName);
this.specifiedFlagFullNames.push(flagName);
}
else {
// we can't find the flag as the key (long name) or short char, so it must be an alias
// get present flags in argv, that is everything starting with `-`, then strip dashes from it.
// e.g. ['-u', 'test', '--json'] -> [ 'u', undefined, 'json' ]
const argvFlags = this.argv.map((a) => a.match(/-([a-zA-Z]+)/g)).map((a) => a?.[0].replace('-', ''));
const possibleAliases = [
...(flagDefinitions[flagName].aliases ?? []),
// charAliases is optional. Ensure compatibility with commands using oclif/core < 3 where `charAliases` isn't supported.
...(flagDefinitions[flagName].charAliases ?? []),
];
// if you have a flag that sets a default value and has aliases
// this check will ensure it only gets captured if the user specified it using aliases
const specifiedFlag = possibleAliases.find((a) => argvFlags.includes(a));
if (specifiedFlag) {
// save flag with their original name instead of the typed alias
this.specifiedFlagFullNames.push(flagName);
this.specifiedFlags.push(flagName);
// user typed a deprecated alias
if (flagDefinitions[flagName].deprecateAliases) {
this.deprecatedFlagsUsed.push(specifiedFlag);
}
}
}
});
}
}
}
//# sourceMappingURL=commandExecution.js.map