Skip to content

Commit c87a3c3

Browse files
Prevent theme telemetry failures from replacing command results
1 parent 1eecd6e commit c87a3c3

3 files changed

Lines changed: 78 additions & 19 deletions

File tree

‎packages/theme/src/cli/commands/theme/transfer-environments.test.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Share from './share.js'
44
import {executeThemePush} from '../../services/push.js'
55
import {executeThemePull} from '../../services/pull.js'
66
import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output'
7+
import {addPublicMetadata} from '@shopify/cli-kit/node/metadata'
78
import {loadEnvironment} from '@shopify/cli-kit/node/environments'
89
import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
910
import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs'
@@ -20,17 +21,15 @@ vi.mock('@shopify/cli-kit/node/metadata')
2021
class TestPush extends Push {
2122
public parse = vi.fn()
2223
}
23-
2424
class TestPull extends Pull {
2525
public parse = vi.fn()
2626
}
27-
2827
class TestShare extends Share {
2928
public parse = vi.fn()
3029
}
3130

3231
describe.each([TestPush, TestPull, TestShare])('%s', (Command) => {
33-
test.each(['none', 'partial', 'total', 'cancelled'] as const)(
32+
test.each(['none', 'partial', 'total', 'cancelled', 'analytics'] as const)(
3433
'retains all environments in request order with %s failures',
3534
async (failures) => {
3635
await inTemporaryDirectory(async (path) => {
@@ -45,6 +44,11 @@ describe.each([TestPush, TestPull, TestShare])('%s', (Command) => {
4544
const secondStarted = new Promise<void>((resolve) => {
4645
releaseFirst = resolve
4746
})
47+
vi.mocked(addPublicMetadata).mockImplementation(async (collect) => {
48+
const metadata = await collect()
49+
if (failures === 'analytics' && metadata?.store_domain === 'second.myshopify.com')
50+
throw new Error('analytics failed')
51+
})
4852
const executionOrder: string[] = []
4953
const execute = async (flags: {environment?: string[]}, session?: {storeFqdn: string}) => {
5054
const environment = flags.environment![0]!
@@ -73,7 +77,6 @@ describe.each([TestPush, TestPull, TestShare])('%s', (Command) => {
7377
}
7478
vi.mocked(executeThemePush).mockImplementation(execute)
7579
vi.mocked(executeThemePull).mockImplementation(execute)
76-
7780
const config = new Config({root: __dirname})
7881
await config.load()
7982
const command = new Command(
@@ -114,7 +117,7 @@ describe.each([TestPush, TestPull, TestShare])('%s', (Command) => {
114117
.map((line) => JSON.parse(line))
115118
: []
116119
expect(events.filter((event) => event.level === 'error')).toHaveLength(
117-
{none: 0, partial: 1, total: 3, cancelled: 0}[failures],
120+
{none: 0, partial: 1, total: 3, cancelled: 0, analytics: 0}[failures],
118121
)
119122
})
120123
} finally {
@@ -151,7 +154,6 @@ describe.each([TestPush, TestPull, TestShare])('%s', (Command) => {
151154
])
152155
expect(executeThemePush).not.toHaveBeenCalled()
153156
expect(executeThemePull).not.toHaveBeenCalled()
154-
155157
expect(process.exitCode).toBe(1)
156158
})
157159
} finally {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import ThemeCommand from './theme-command.js'
2+
import {Config, Flags} from '@oclif/core'
3+
import {compileData} from '@shopify/cli-kit/node/analytics'
4+
import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
5+
import {addPublicMetadata, addSensitiveMetadata} from '@shopify/cli-kit/node/metadata'
6+
import {expect, test, vi} from 'vitest'
7+
8+
vi.mock('@shopify/cli-kit/node/analytics')
9+
vi.mock('@shopify/cli-kit/node/session')
10+
vi.mock('@shopify/cli-kit/node/metadata')
11+
12+
test.each(
13+
['compile', 'public metadata', 'sensitive metadata'].flatMap((failure) =>
14+
[false, true].map((commandFails) => ({failure, commandFails})),
15+
),
16+
)('preserves the command outcome when $failure fails, commandFails=$commandFails', async ({failure, commandFails}) => {
17+
const commandError = new Error('Command failed')
18+
const telemetryError = new Error('Telemetry failed')
19+
class TestCommand extends ThemeCommand {
20+
static flags = {
21+
store: Flags.string({env: 'SHOPIFY_FLAG_STORE'}),
22+
password: Flags.string({env: 'SHOPIFY_FLAG_PASSWORD'}),
23+
}
24+
25+
static multiEnvironmentsFlags = ['store']
26+
27+
command = vi.fn(async () => {
28+
if (commandFails) throw commandError
29+
})
30+
}
31+
32+
vi.mocked(ensureAuthenticatedThemes).mockResolvedValue({storeFqdn: 'example.myshopify.com', token: 'token'})
33+
vi.mocked(compileData).mockImplementation(() => {
34+
if (failure === 'compile') throw telemetryError
35+
return {timings: {}, errors: {}, retries: {}, events: {}} as ReturnType<typeof compileData>
36+
})
37+
vi.mocked(addPublicMetadata).mockImplementation(async (metadata) => {
38+
if (failure === 'public metadata' && 'cmd_theme_timings' in (await metadata())) throw telemetryError
39+
})
40+
if (failure === 'sensitive metadata') vi.mocked(addSensitiveMetadata).mockRejectedValue(telemetryError)
41+
const config = new Config({root: __dirname})
42+
await config.load()
43+
const command = new TestCommand(['--store', 'example.myshopify.com', '--password', 'token'], config)
44+
45+
if (commandFails) {
46+
await expect(command.run()).rejects.toBe(commandError)
47+
} else {
48+
await expect(command.run()).resolves.toBeUndefined()
49+
}
50+
expect(command.command).toHaveBeenCalledOnce()
51+
})

‎packages/theme/src/cli/utilities/theme-command.ts‎

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -741,18 +741,24 @@ export default abstract class ThemeCommand extends Command {
741741
private async logAnalyticsData(session?: AdminSession): Promise<void> {
742742
if (!session) return
743743

744-
const data = compileData()
745-
await addPublicMetadata(() => ({
746-
store_fqdn_hash: hashString(session.storeFqdn),
747-
store_domain: session.storeFqdn,
748-
749-
cmd_theme_timings: JSON.stringify(data.timings),
750-
cmd_theme_errors: JSON.stringify(data.errors),
751-
cmd_theme_retries: JSON.stringify(data.retries),
752-
cmd_theme_events: JSON.stringify(data.events),
753-
}))
754-
await addSensitiveMetadata(() => ({
755-
store_fqdn: session.storeFqdn,
756-
}))
744+
try {
745+
const data = compileData()
746+
await addPublicMetadata(() => ({
747+
store_fqdn_hash: hashString(session.storeFqdn),
748+
store_domain: session.storeFqdn,
749+
750+
cmd_theme_timings: JSON.stringify(data.timings),
751+
cmd_theme_errors: JSON.stringify(data.errors),
752+
cmd_theme_retries: JSON.stringify(data.retries),
753+
cmd_theme_events: JSON.stringify(data.events),
754+
}))
755+
await addSensitiveMetadata(() => ({
756+
store_fqdn: session.storeFqdn,
757+
}))
758+
// eslint-disable-next-line no-catch-all/no-catch-all
759+
} catch (error) {
760+
// Telemetry must not discard completed work or replace the command's original error.
761+
outputDebug(`Unable to record theme analytics: ${error instanceof Error ? error.message : String(error)}`)
762+
}
757763
}
758764
}

0 commit comments

Comments
 (0)