@salesforce/plugin-telemetry
Version:
Command usage and error telemetry for the Salesforce CLI
147 lines • 6.78 kB
JavaScript
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import enabledCheck from '@salesforce/telemetry/enabledCheck';
import { debug } from '../debugger.js';
import { guessCISystem } from '../guessCI.js';
/**
* A hook that runs before every command that:
* 1. Warns the user about command usage data collection the CLI does unless they have already acknowledged the warning.
* 2. Writes logs to a file, including execution and errors.
* 3. Logs command usage data to the server right after the process ends by spawning a detached process.
*/
const hook = async function (options) {
// Don't even bother logging if telemetry is disabled
if (!(await enabledCheck.isEnabled())) {
debug('Telemetry disabled. Doing nothing.');
return;
}
try {
const [path, Performance, Lifecycle, Telemetry, CommandExecution] = await Promise.all([
await import('node:path'),
(await import('@oclif/core')).Performance,
(await import('@salesforce/core')).Lifecycle,
(await import('../telemetry.js')).default,
(await import('../commandExecution.js')).CommandExecution,
]);
const errors = [];
// Instantiating telemetry shows data collection warning.
// Adding this to the global so that telemetry events are sent even when different
// versions of this plugin are in use by the CLI.
const telemetry = (global.cliTelemetry = await Telemetry.create({
cacheDir: this.config.cacheDir,
executable: this.config.bin,
}));
const commandExecution = await CommandExecution.create({
command: options.Command,
argv: options.argv,
config: this.config,
});
let commonData;
try {
Lifecycle.getInstance().onTelemetry(async (data) => {
await Promise.resolve(telemetry.record({
type: 'EVENT',
...commonDataMemoized(),
...data,
}));
});
}
catch (err) {
// even if this throws, the rest of telemetry is not affected
const error = err;
debug('Error subscribing to telemetry events', error.message);
}
process.on('warning', (warning) => {
telemetry.record({
...commonDataMemoized(),
eventName: 'NODE_WARNING',
warningType: warning.name,
stack: warning.stack,
message: warning.message,
});
});
debug('Setting up process exit handler');
process.once('exit', (status) => {
let oclifPerf;
let nonOclifPerf;
try {
oclifPerf = Performance.oclifPerf;
nonOclifPerf = Object.fromEntries(Array.from(Performance.results.entries()).flatMap(([owner, results]) => results.map((result) => [`${owner}__${result.name}`, result.duration ?? 0])));
}
catch (err) {
debug('Unable to get oclif performance metrics', err);
}
for (const { event, error } of errors) {
telemetry.recordError(error, { ...event, ...oclifPerf, ...nonOclifPerf });
}
commandExecution.status = status;
telemetry.record({ ...commandExecution.toJson(), ...oclifPerf, ...nonOclifPerf });
if (process.listenerCount('exit') >= 20) {
// On exit listeners have been a problem in the past. Make sure we don't accumulate too many...
// This could be from too many plugins. If we start getting this, log number of plugins too.
telemetry.record({
eventName: 'EVENT_EMITTER_WARNING',
count: process.listenerCount('exit'),
});
}
// If it is the first time the telemetry is running, consider it a new "install".
if (telemetry.firstRun) {
telemetry.record({
eventName: 'INSTALL',
ci: guessCISystem(),
installType: this.config.binPath?.includes(path.join('sfdx', 'client')) ??
this.config.binPath?.includes(path.join('sf', 'client'))
? 'installer'
: 'npm',
platform: this.config.platform,
});
}
// Upload to server. Starts another process.
// At this point, any other events during the CLI lifecycle should be logged.
telemetry.upload();
});
// Log command errors to the server.
// Record failed command executions from commands that extend SfdxCommand
process.on('cmdError', (error) => {
errors.push({ error, event: { ...commandExecution.toJson(), eventName: 'COMMAND_ERROR' } });
});
// Record failed command executions from commands that extend SfCommand
process.on('sfCommandError', (error, id) => {
/**
* Only record the error if the id matches the command name or if the id is not provided.
*
* This is to prevent recording duplicate errors when running multiple commands in the same process
* (e.g. a `plugins install` of a JIT plugin before running the provided command).
*
* We still record the error if no id is provided to ensure we capture all errors for plugins that
* have not updated @salesforce/sf-plugins-core to the version that includes the id parameter
*/
if (id === commandExecution.getCommandName() || !id) {
errors.push({ error, event: { ...commandExecution.toJson(), eventName: 'COMMAND_ERROR' } });
}
});
const commonDataMemoized = () => {
if (!commonData) {
const pluginInfo = commandExecution.getPluginInfo();
commonData = {
nodeVersion: process.version,
plugin: pluginInfo.name,
// eslint-disable-next-line camelcase
plugin_version: pluginInfo.version,
command: commandExecution.getCommandName(),
};
}
return commonData;
};
}
catch (err) {
const error = err;
debug('Error with logging or sending telemetry:', error.message);
}
};
export default hook;
//# sourceMappingURL=telemetryPrerun.js.map