From 32a22f287bddc15ec28585c009c6b54cfad46192 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Fri, 22 May 2026 13:04:29 +0200 Subject: [PATCH] Send analytics in background so the main command can finish early --- packages/cli-kit/src/public/node/analytics.ts | 109 +++++++++++++----- .../cli-kit/src/public/node/hooks/postrun.ts | 3 +- .../src/public/node/notifications-system.ts | 1 + packages/cli/oclif.manifest.json | 26 +++++ .../cli/src/cli/commands/send-analytics.ts | 19 +++ packages/cli/src/index.ts | 2 + 6 files changed, 128 insertions(+), 32 deletions(-) create mode 100644 packages/cli/src/cli/commands/send-analytics.ts diff --git a/packages/cli-kit/src/public/node/analytics.ts b/packages/cli-kit/src/public/node/analytics.ts index 5578e3af328..13ccc5bd320 100644 --- a/packages/cli-kit/src/public/node/analytics.ts +++ b/packages/cli-kit/src/public/node/analytics.ts @@ -36,6 +36,52 @@ interface ReportAnalyticsEventOptions { exitMode: CommandExitMode } +export async function sendAnalyticsEventFromFile(payloadFile: string): Promise { + const {readFile, removeFile} = await import('./fs.js') + try { + const payloadStr = await readFile(payloadFile) + const payload = JSON.parse(payloadStr) + + // remove file + await removeFile(payloadFile) + + const doMonorail = async () => { + if (payload.skipMonorailAnalytics) return + const response = await publishMonorailEvent(MONORAIL_COMMAND_TOPIC, payload.public, payload.sensitive) + if (response.type === 'error') { + outputDebug(response.message) + } + } + + const doOpenTelemetry = async () => { + if (payload.skipMetricAnalytics) return + + 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: payload.skipMetricAnalytics, + cliVersion: payload.public.cli_version, + owningPlugin: payload.public.cmd_all_plugin ?? '@shopify/cli', + command: payload.public.command, + exitMode: payload.public.cmd_all_exit, + }, + { + active, + network, + prompt, + }, + ) + } + + await Promise.all([doMonorail(), doOpenTelemetry()]) + } catch (error) { + outputDebug(`Failed to send analytics in background: ${error}`) + } +} + /** * Report an analytics event, sending it off to Monorail -- Shopify's internal analytics service. * @@ -45,8 +91,7 @@ interface ReportAnalyticsEventOptions { export async function reportAnalyticsEvent(options: ReportAnalyticsEventOptions): Promise { try { const payload = await buildPayload(options) - if (payload === undefined) { - // Nothing to log + if (payload === undefined || payload.public.command === 'send-analytics') { return } @@ -65,40 +110,42 @@ export async function reportAnalyticsEvent(options: ReportAnalyticsEventOptions) const skipMonorailAnalytics = !alwaysLogAnalytics() && analyticsDisabled() const skipMetricAnalytics = !alwaysLogMetrics() && analyticsDisabled() - if (skipMonorailAnalytics || skipMetricAnalytics) { + if (skipMonorailAnalytics && skipMetricAnalytics) { outputDebug(outputContent`Skipping command analytics, payload: ${outputToken.json(payload)}`) + return } - 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 + outputDebug(outputContent`Sending command analytics in background, payload: ${outputToken.json(payload)}`) - 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, - }, - ) + const {joinPath} = await import('./path.js') + const {tmpdir} = await import('node:os') + const {writeFile} = await import('./fs.js') + + const payloadPath = joinPath(tmpdir(), `shopify-cli-analytics-${Date.now()}.json`) + + const fullPayload = { + ...payload, + skipMonorailAnalytics, + skipMetricAnalytics, } - await Promise.all([doMonorail(), doOpenTelemetry()]) + + await writeFile(payloadPath, JSON.stringify(fullPayload)) + + const {exec} = await import('./system.js') + const argv = process.argv + if (!argv[0] || !argv[1]) return + const nodeBinary = argv[0] + const shopifyBinary = argv[1] + const args = [shopifyBinary, 'send-analytics', '--payload-file', payloadPath] + + // eslint-disable-next-line no-void + void exec(nodeBinary, args, { + background: true, + env: {...process.env, SHOPIFY_CLI_NO_ANALYTICS: '1'}, + externalErrorHandler: async (error: unknown) => { + outputDebug(`Failed to send analytics in background: ${(error as Error).message}`) + }, + }) // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { diff --git a/packages/cli-kit/src/public/node/hooks/postrun.ts b/packages/cli-kit/src/public/node/hooks/postrun.ts index 09146d0cd8c..675b283ee1d 100644 --- a/packages/cli-kit/src/public/node/hooks/postrun.ts +++ b/packages/cli-kit/src/public/node/hooks/postrun.ts @@ -66,7 +66,8 @@ export const hook: Hook.Postrun = async ({config, Command}) => { const command = Command.id.replace(/:/g, ' ') outputDebug(`Completed command ${command}`) - if (!command.includes('notifications') && !command.includes('upgrade')) await autoUpgradeIfNeeded() + if (!command.includes('notifications') && !command.includes('upgrade') && !command.includes('send-analytics')) + await autoUpgradeIfNeeded() postRunHookCompleted = true } diff --git a/packages/cli-kit/src/public/node/notifications-system.ts b/packages/cli-kit/src/public/node/notifications-system.ts index d9eade1f1cb..35730a7f709 100644 --- a/packages/cli-kit/src/public/node/notifications-system.ts +++ b/packages/cli-kit/src/public/node/notifications-system.ts @@ -21,6 +21,7 @@ const COMMANDS_TO_SKIP = [ 'theme:init', 'hydrogen:init', 'cache:clear', + 'send-analytics', ] function url(): string { diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 046ac204728..fa28010f4c3 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -5669,6 +5669,32 @@ "strict": true, "usage": "search [query]" }, + "send-analytics": { + "aliases": [ + ], + "args": { + }, + "enableJsonFlag": false, + "flags": { + "payload-file": { + "hasDynamicHelp": false, + "hidden": true, + "multiple": false, + "name": "payload-file", + "required": true, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [ + ], + "id": "send-analytics", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true + }, "store:auth": { "aliases": [ ], diff --git a/packages/cli/src/cli/commands/send-analytics.ts b/packages/cli/src/cli/commands/send-analytics.ts new file mode 100644 index 00000000000..5c0780d6273 --- /dev/null +++ b/packages/cli/src/cli/commands/send-analytics.ts @@ -0,0 +1,19 @@ +import Command from '@shopify/cli-kit/node/base-command' +import {Flags} from '@oclif/core' +import {sendAnalyticsEventFromFile} from '@shopify/cli-kit/node/analytics' + +export default class SendAnalytics extends Command { + static hidden = true + + static flags = { + 'payload-file': Flags.string({ + hidden: true, + required: true, + }), + } + + async run(): Promise { + const {flags} = await this.parse(SendAnalytics) + await sendAnalyticsEventFromFile(flags['payload-file']) + } +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ff506419c63..29e8cba5112 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,6 +1,7 @@ import VersionCommand from './cli/commands/version.js' import Search from './cli/commands/search.js' import Upgrade from './cli/commands/upgrade.js' +import SendAnalytics from './cli/commands/send-analytics.js' import Logout from './cli/commands/auth/logout.js' import Login from './cli/commands/auth/login.js' import CommandFlags from './cli/commands/debug/command-flags.js' @@ -147,6 +148,7 @@ export const COMMANDS: any = { search: Search, upgrade: Upgrade, version: VersionCommand, + 'send-analytics': SendAnalytics, help: HelpCommand, 'auth:logout': Logout, 'auth:login': Login,