@shopify/cli-kit
Version:
A set of utilities, interfaces, and models that are common across all the platform features
156 lines • 7.15 kB
JavaScript
import { alwaysLogAnalytics, alwaysLogMetrics, analyticsDisabled, isShopify } from './context/local.js';
import * as metadata from './metadata.js';
import { publishMonorailEvent, MONORAIL_COMMAND_TOPIC } from './monorail.js';
import { fanoutHooks } from './plugins.js';
import { outputContent, outputDebug, outputToken } from '../../public/node/output.js';
import { getEnvironmentData, getSensitiveEnvironmentData } from '../../private/node/analytics.js';
import { CLI_KIT_VERSION } from '../common/version.js';
import { recordMetrics } from '../../private/node/otel-metrics.js';
import { runWithRateLimit } from '../../private/node/conf-store.js';
import { reportingRateLimit } from '../../private/node/constants.js';
import { getLastSeenUserIdAfterAuth } from '../../private/node/session.js';
import { requestIdsCollection } from '../../private/node/request-ids.js';
/**
* Report an analytics event, sending it off to Monorail -- Shopify's internal analytics service.
*
* The payload for an event includes both generic data, and data gathered from installed plug-ins.
*
*/
export async function reportAnalyticsEvent(options) {
try {
const payload = await buildPayload(options);
if (payload === undefined) {
// Nothing to log
return;
}
let withinRateLimit = false;
await runWithRateLimit({
key: 'report-analytics-event',
...reportingRateLimit,
task: async () => {
withinRateLimit = true;
},
});
if (!withinRateLimit) {
outputDebug(outputContent `Skipping command analytics due to rate limiting, payload: ${outputToken.json(payload)}`);
return;
}
const skipMonorailAnalytics = !alwaysLogAnalytics() && analyticsDisabled();
const skipMetricAnalytics = !alwaysLogMetrics() && analyticsDisabled();
if (skipMonorailAnalytics || skipMetricAnalytics) {
outputDebug(outputContent `Skipping command analytics, payload: ${outputToken.json(payload)}`);
}
const doMonorail = async () => {
if (skipMonorailAnalytics) {
return;
}
const response = await publishMonorailEvent(MONORAIL_COMMAND_TOPIC, payload.public, payload.sensitive);
if (response.type === 'error') {
outputDebug(response.message);
}
};
const doOpenTelemetry = async () => {
const active = payload.public.cmd_all_timing_active_ms || 0;
const network = payload.public.cmd_all_timing_network_ms || 0;
const prompt = payload.public.cmd_all_timing_prompts_ms || 0;
return recordMetrics({
skipMetricAnalytics,
cliVersion: payload.public.cli_version,
owningPlugin: payload.public.cmd_all_plugin || '@shopify/cli',
command: payload.public.command,
exitMode: options.exitMode,
}, {
active,
network,
prompt,
});
};
await Promise.all([doMonorail(), doOpenTelemetry()]);
// eslint-disable-next-line no-catch-all/no-catch-all
}
catch (error) {
let message = 'Failed to report usage analytics';
if (error instanceof Error) {
message = message.concat(`: ${error.message}`);
}
outputDebug(message);
}
}
async function buildPayload({ config, errorMessage, exitMode }) {
const { commandStartOptions, environmentFlags, ...sensitiveMetadata } = metadata.getAllSensitiveMetadata();
if (commandStartOptions === undefined) {
outputDebug('Unable to log analytics event - no information on executed command');
return;
}
const { startCommand, startArgs, startTime } = commandStartOptions;
const currentTime = new Date().getTime();
// All bundled plugins appear as `@shopify/cli` in the payload
const { '@shopify/cli': internalPluginsPublic, ...externalPluginsPublic } = await fanoutHooks(config, 'public_command_metadata', {});
const { '@shopify/cli': internalPluginsSensitive, ...externalPluginsSensitive } = await fanoutHooks(config, 'sensitive_command_metadata', {});
const environmentData = await getEnvironmentData(config);
const sensitiveEnvironmentData = await getSensitiveEnvironmentData(config);
const publicMetadata = metadata.getAllPublicMetadata();
// Automatically calculate the total time spent in the command, excluding time spent in subtimers.
const subTimers = ['cmd_all_timing_network_ms', 'cmd_all_timing_prompts_ms'];
const totalTimeFromSubtimers = subTimers.reduce((total, timer) => {
const value = publicMetadata[timer];
if (value !== undefined) {
return total + value;
}
return total;
}, 0);
const wallClockElapsed = currentTime - startTime;
const totalTimeWithoutSubtimers = wallClockElapsed - totalTimeFromSubtimers;
let payload = {
public: {
command: startCommand,
time_start: startTime,
time_end: currentTime,
total_time: wallClockElapsed,
success: exitMode === 'ok' && errorMessage === undefined,
cli_version: CLI_KIT_VERSION,
ruby_version: '',
node_version: process.version.replace('v', ''),
is_employee: await isShopify(),
...environmentData,
...internalPluginsPublic,
...publicMetadata,
cmd_all_timing_active_ms: totalTimeWithoutSubtimers,
cmd_all_exit: exitMode,
user_id: await getLastSeenUserIdAfterAuth(),
request_ids: requestIdsCollection.getRequestIds(),
},
sensitive: {
args: startArgs.join(' '),
cmd_all_environment_flags: environmentFlags,
error_message: errorMessage,
...internalPluginsSensitive,
...sensitiveEnvironmentData,
metadata: JSON.stringify({
...sensitiveMetadata,
extraPublic: {
...externalPluginsPublic,
},
extraSensitive: { ...externalPluginsSensitive },
}),
},
};
// round down timing metrics
const timingMetrics = ['cmd_all_timing_active_ms', 'cmd_all_timing_network_ms', 'cmd_all_timing_prompts_ms'];
timingMetrics.forEach((metric) => {
const current = payload.public[metric];
if (current !== undefined) {
payload.public[metric] = Math.floor(current);
}
});
// strip undefined fields -- they make up the majority of payloads due to wide metadata structure.
payload = JSON.parse(JSON.stringify(payload));
return sanitizePayload(payload);
}
function sanitizePayload(payload) {
const payloadString = JSON.stringify(payload);
// Remove Theme Access passwords from the payload
const sanitizedPayloadString = payloadString.replace(/shptka_\w*/g, '*****');
return JSON.parse(sanitizedPayloadString);
}
//# sourceMappingURL=analytics.js.map