Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/auto-upgrade-metrics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli-kit': patch
---

Track auto-upgrade events (trigger rate, success/failure, package manager, skip reason) in Monorail.
72 changes: 71 additions & 1 deletion packages/cli-kit/src/public/node/hooks/postrun.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import {autoUpgradeIfNeeded} from './postrun.js'
import {versionToAutoUpgrade, runCLIUpgrade, getOutputUpdateCLIReminder} from '../upgrade.js'
import {isMajorVersionChange} from '../version.js'
import {inferPackageManagerForGlobalCLI} from '../is-global.js'
import {addPublicMetadata} from '../metadata.js'
import {mockAndCaptureOutput} from '../testing/output.js'
import {describe, test, vi, expect, afterEach} from 'vitest'

vi.mock('../upgrade.js')
vi.mock('../version.js')
vi.mock('../is-global.js')
vi.mock('../metadata.js', () => ({
addPublicMetadata: vi.fn().mockResolvedValue(undefined),
}))

afterEach(() => {
mockAndCaptureOutput().clear()
vi.mocked(addPublicMetadata).mockClear()
})

describe('autoUpgradeIfNeeded', () => {
Expand All @@ -35,6 +42,7 @@ describe('autoUpgradeIfNeeded', () => {
test('calls runCLIUpgrade for minor/patch upgrade', async () => {
vi.mocked(versionToAutoUpgrade).mockReturnValue('3.100.0')
vi.mocked(isMajorVersionChange).mockReturnValue(false)
vi.mocked(inferPackageManagerForGlobalCLI).mockReturnValue('npm')
vi.mocked(runCLIUpgrade).mockResolvedValue(undefined)

await autoUpgradeIfNeeded()
Expand All @@ -46,11 +54,73 @@ describe('autoUpgradeIfNeeded', () => {
const outputMock = mockAndCaptureOutput()
vi.mocked(versionToAutoUpgrade).mockReturnValue('3.100.0')
vi.mocked(isMajorVersionChange).mockReturnValue(false)
vi.mocked(inferPackageManagerForGlobalCLI).mockReturnValue('npm')
vi.mocked(runCLIUpgrade).mockRejectedValue(new Error('upgrade failed'))
vi.mocked(getOutputUpdateCLIReminder).mockReturnValue('💡 Version 3.100.0 available! Run `npm install -g @shopify/cli@latest`')
vi.mocked(getOutputUpdateCLIReminder).mockReturnValue(
'💡 Version 3.100.0 available! Run `npm install -g @shopify/cli@latest`',
)

await autoUpgradeIfNeeded()

expect(outputMock.warn()).toContain('3.100.0')
})

test('records triggered metric for minor/patch upgrade', async () => {
vi.mocked(versionToAutoUpgrade).mockReturnValue('3.100.0')
vi.mocked(isMajorVersionChange).mockReturnValue(false)
vi.mocked(inferPackageManagerForGlobalCLI).mockReturnValue('npm')
vi.mocked(runCLIUpgrade).mockResolvedValue(undefined)

await autoUpgradeIfNeeded()

expect(addPublicMetadata).toHaveBeenCalledWith(expect.any(Function))
const calls = vi.mocked(addPublicMetadata).mock.calls.map((call) => call[0]())
expect(calls).toContainEqual(
expect.objectContaining({
cmd_all_auto_upgrade_triggered: true,
cmd_all_auto_upgrade_package_manager: 'npm',
}),
)
})

test('records triggered metric with major_version skipped_reason for major bump', async () => {
vi.mocked(versionToAutoUpgrade).mockReturnValue('4.0.0')
vi.mocked(isMajorVersionChange).mockReturnValue(true)
vi.mocked(getOutputUpdateCLIReminder).mockReturnValue('upgrade reminder')

await autoUpgradeIfNeeded()

const calls = vi.mocked(addPublicMetadata).mock.calls.map((call) => call[0]())
expect(calls).toContainEqual(
expect.objectContaining({
cmd_all_auto_upgrade_triggered: true,
cmd_all_auto_upgrade_skipped_reason: 'major_version',
}),
)
})

test('records success=true on successful upgrade', async () => {
vi.mocked(versionToAutoUpgrade).mockReturnValue('3.100.0')
vi.mocked(isMajorVersionChange).mockReturnValue(false)
vi.mocked(inferPackageManagerForGlobalCLI).mockReturnValue('npm')
vi.mocked(runCLIUpgrade).mockResolvedValue(undefined)

await autoUpgradeIfNeeded()

const calls = vi.mocked(addPublicMetadata).mock.calls.map((call) => call[0]())
expect(calls).toContainEqual(expect.objectContaining({cmd_all_auto_upgrade_success: true}))
})

test('records success=false on failed upgrade', async () => {
vi.mocked(versionToAutoUpgrade).mockReturnValue('3.100.0')
vi.mocked(isMajorVersionChange).mockReturnValue(false)
vi.mocked(inferPackageManagerForGlobalCLI).mockReturnValue('npm')
vi.mocked(runCLIUpgrade).mockRejectedValue(new Error('upgrade failed'))
vi.mocked(getOutputUpdateCLIReminder).mockReturnValue('upgrade reminder')

await autoUpgradeIfNeeded()

const calls = vi.mocked(addPublicMetadata).mock.calls.map((call) => call[0]())
expect(calls).toContainEqual(expect.objectContaining({cmd_all_auto_upgrade_success: false}))
})
})
19 changes: 18 additions & 1 deletion packages/cli-kit/src/public/node/hooks/postrun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {outputDebug, outputWarn} from '../output.js'
import {getOutputUpdateCLIReminder, runCLIUpgrade, versionToAutoUpgrade} from '../upgrade.js'
import BaseCommand from '../base-command.js'
import * as metadata from '../metadata.js'
import {inferPackageManagerForGlobalCLI} from '../is-global.js'
import {CLI_KIT_VERSION} from '../../common/version.js'
import {isMajorVersionChange} from '../version.js'

Expand Down Expand Up @@ -40,19 +41,35 @@ export const hook: Hook.Postrun = async ({config, Command}) => {
*/
export async function autoUpgradeIfNeeded(): Promise<void> {
const newerVersion = versionToAutoUpgrade()
if (!newerVersion) return
if (!newerVersion) {
// versionToAutoUpgrade already logged the reason via outputDebug
return
}

if (isMajorVersionChange(CLI_KIT_VERSION, newerVersion)) {
outputWarn(getOutputUpdateCLIReminder(newerVersion))
await metadata.addPublicMetadata(() => ({
cmd_all_auto_upgrade_triggered: true,
cmd_all_auto_upgrade_skipped_reason: 'major_version',
}))
return
}

const packageManager = inferPackageManagerForGlobalCLI()
await metadata.addPublicMetadata(() => ({
cmd_all_auto_upgrade_triggered: true,
cmd_all_auto_upgrade_package_manager: packageManager,
}))

try {
await runCLIUpgrade()
await metadata.addPublicMetadata(() => ({cmd_all_auto_upgrade_success: true}))
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
const errorMessage = `Auto-upgrade failed: ${error}`
outputDebug(errorMessage)
outputWarn(getOutputUpdateCLIReminder(newerVersion))
await metadata.addPublicMetadata(() => ({cmd_all_auto_upgrade_success: false}))
// Report to Observe as a handled error without showing anything extra to the user
const {sendErrorToBugsnag} = await import('../error-handler.js')
await sendErrorToBugsnag(new Error(errorMessage), 'expected_error')
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-kit/src/public/node/is-global.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {currentProcessIsGlobal, inferPackageManagerForGlobalCLI, installGlobalCL
import {terminalSupportsPrompting} from './system.js'
import {renderSelectPrompt} from './ui.js'
import {globalCLIVersion} from './version.js'
import {beforeEach, describe, expect, test, vi, afterEach} from 'vitest'
import {describe, expect, test, vi, afterEach} from 'vitest'

vi.mock('./system.js')
vi.mock('./ui.js')
Expand Down
6 changes: 6 additions & 0 deletions packages/cli-kit/src/public/node/monorail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ export interface Schemas {
cmd_all_timing_prompts_ms?: Optional<number>
cmd_all_timing_active_ms?: Optional<number>

// Auto-upgrade
cmd_all_auto_upgrade_triggered?: Optional<boolean>
cmd_all_auto_upgrade_skipped_reason?: Optional<string>
cmd_all_auto_upgrade_success?: Optional<boolean>
cmd_all_auto_upgrade_package_manager?: Optional<string>

// Any extension related command
cmd_extensions_binary_from_source?: Optional<boolean>

Expand Down
6 changes: 3 additions & 3 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2765,16 +2765,16 @@ DESCRIPTION

## `shopify upgrade`

Shows details on how to upgrade Shopify CLI.
Upgrades Shopify CLI.

```
USAGE
$ shopify upgrade

DESCRIPTION
Shows details on how to upgrade Shopify CLI.
Upgrades Shopify CLI.

Shows details on how to upgrade Shopify CLI.
Upgrades Shopify CLI using your package manager.
```

## `shopify version`
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7822,8 +7822,8 @@
],
"args": {
},
"description": "Shows details on how to upgrade Shopify CLI.",
"descriptionWithMarkdown": "Shows details on how to upgrade Shopify CLI.",
"description": "Upgrades Shopify CLI using your package manager.",
"descriptionWithMarkdown": "Upgrades Shopify CLI using your package manager.",
"enableJsonFlag": false,
"flags": {
},
Expand All @@ -7835,7 +7835,7 @@
"pluginName": "@shopify/cli",
"pluginType": "core",
"strict": true,
"summary": "Shows details on how to upgrade Shopify CLI."
"summary": "Upgrades Shopify CLI."
},
"version": {
"aliases": [
Expand Down
9 changes: 4 additions & 5 deletions packages/cli/src/cli/commands/upgrade.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import Upgrade from './upgrade.js'
import {promptAutoUpgrade, runCLIUpgrade} from '@shopify/cli-kit/node/upgrade'
import {describe, test, vi, expect} from 'vitest'

vi.mock('@shopify/cli-kit/node/upgrade', () => ({
promptAutoUpgrade: vi.fn().mockResolvedValue(true),
runCLIUpgrade: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('@shopify/cli-kit/node/upgrade')

describe('upgrade command', () => {
test('calls promptAutoUpgrade and runCLIUpgrade', async () => {
const {promptAutoUpgrade, runCLIUpgrade} = await import('@shopify/cli-kit/node/upgrade')
vi.mocked(promptAutoUpgrade).mockResolvedValue(true)
vi.mocked(runCLIUpgrade).mockResolvedValue(undefined)

await Upgrade.run([], import.meta.url)

Expand Down
Loading