From 4a3123d731fcc93d061e9de7def4bc4c45d6b8cf Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Mon, 24 Aug 2026 11:21:18 -0400 Subject: [PATCH 1/2] Report store lifecycle command telemetry Assisted-By: devx/9c08eef3-3e18-4b5f-8ed8-bef380040c5d --- .../src/cli/commands/store/create/dev.test.ts | 78 +++++++++++-- .../src/cli/commands/store/create/dev.ts | 14 ++- .../src/cli/commands/store/delete.test.ts | 110 ++++++++++++++---- .../store/src/cli/commands/store/delete.ts | 8 ++ .../src/cli/services/store/create/dev.test.ts | 31 +++++ .../src/cli/services/store/create/dev.ts | 3 + .../src/cli/services/store/delete/dev.test.ts | 52 ++++++++- .../src/cli/services/store/delete/dev.ts | 4 + 8 files changed, 263 insertions(+), 37 deletions(-) diff --git a/packages/store/src/cli/commands/store/create/dev.test.ts b/packages/store/src/cli/commands/store/create/dev.test.ts index 5e77f815bcc..1decbaef7b6 100644 --- a/packages/store/src/cli/commands/store/create/dev.test.ts +++ b/packages/store/src/cli/commands/store/create/dev.test.ts @@ -2,6 +2,7 @@ import StoreCreateDev from './dev.js' import {createDevStore} from '../../../services/store/create/dev.js' import {storeNamePrompt, storePlanPrompt} from '../../../prompts/store.js' import {selectOrg} from '@shopify/organizations' +import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics' import {AbortError} from '@shopify/cli-kit/node/error' import {outputResult} from '@shopify/cli-kit/node/output' import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system' @@ -10,6 +11,9 @@ import {describe, expect, test, vi, beforeEach} from 'vitest' vi.mock('../../../services/store/create/dev.js') vi.mock('../../../prompts/store.js') vi.mock('@shopify/cli-kit/node/system') +vi.mock('@shopify/cli-kit/node/analytics', () => ({ + reportAnalyticsEvent: vi.fn(), +})) vi.mock('@shopify/organizations', () => ({ selectOrg: vi.fn(), @@ -31,6 +35,10 @@ beforeEach(() => { }) describe('store create dev command', () => { + test('requires synchronous analytics', () => { + expect(StoreCreateDev.requiresSyncAnalytics).toBe(true) + }) + test('resolves the organization and passes parsed flags through to the service', async () => { await StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345']) @@ -190,21 +198,77 @@ describe('store create dev command', () => { const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => { throw new Error('process.exit') }) as never) + const mockCommandCatch = vi.spyOn(StoreCreateDev.prototype, 'catch').mockImplementation(async (error) => { + throw error + }) await expect( StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345', '--json']), ).rejects.toThrow('process.exit') - const call = vi.mocked(outputResult).mock.calls[0]![0] as string - const parsed = JSON.parse(call) - expect(parsed).toEqual({ - error: true, - message: 'Something went wrong', - nextSteps: [], - exitCode: 1, + expect(outputResult).toHaveBeenCalledTimes(1) + expect(outputResult).toHaveBeenCalledWith( + JSON.stringify( + { + error: true, + message: 'Something went wrong', + nextSteps: [], + exitCode: 1, + }, + null, + 2, + ), + ) + expect(mockExit).toHaveBeenCalledTimes(1) + expect(mockExit).toHaveBeenCalledWith(1) + expect(reportAnalyticsEvent).toHaveBeenCalledTimes(1) + expect(reportAnalyticsEvent).toHaveBeenCalledWith({ + config: expect.anything(), + errorMessage: 'Something went wrong', + exitMode: 'expected_error', + }) + + mockCommandCatch.mockRestore() + mockExit.mockRestore() + }) + + test('outputs structured JSON error when --json is active and organization selection throws AbortError', async () => { + vi.mocked(selectOrg).mockRejectedValueOnce(new AbortError('Could not select organization')) + const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit') + }) as never) + const mockCommandCatch = vi.spyOn(StoreCreateDev.prototype, 'catch').mockImplementation(async (error) => { + throw error }) + + await expect( + StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345', '--json']), + ).rejects.toThrow('process.exit') + + expect(outputResult).toHaveBeenCalledTimes(1) + expect(outputResult).toHaveBeenCalledWith( + JSON.stringify( + { + error: true, + message: 'Could not select organization', + nextSteps: [], + exitCode: 1, + }, + null, + 2, + ), + ) + expect(createDevStore).not.toHaveBeenCalled() + expect(mockExit).toHaveBeenCalledTimes(1) expect(mockExit).toHaveBeenCalledWith(1) + expect(reportAnalyticsEvent).toHaveBeenCalledTimes(1) + expect(reportAnalyticsEvent).toHaveBeenCalledWith({ + config: expect.anything(), + errorMessage: 'Could not select organization', + exitMode: 'expected_error', + }) + mockCommandCatch.mockRestore() mockExit.mockRestore() }) diff --git a/packages/store/src/cli/commands/store/create/dev.ts b/packages/store/src/cli/commands/store/create/dev.ts index 37a01570c9a..7830e89e39d 100644 --- a/packages/store/src/cli/commands/store/create/dev.ts +++ b/packages/store/src/cli/commands/store/create/dev.ts @@ -5,6 +5,7 @@ import {countryFlag, storeFlags} from '../../../flags.js' import {selectOrg} from '@shopify/organizations' import Command from '@shopify/cli-kit/node/base-command' import {globalFlags, jsonFlag, requiredIfNonInteractive} from '@shopify/cli-kit/node/cli' +import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics' import {AbortError} from '@shopify/cli-kit/node/error' import {outputResult} from '@shopify/cli-kit/node/output' import {Flags} from '@oclif/core' @@ -12,6 +13,10 @@ import {Flags} from '@oclif/core' export default class StoreCreateDev extends Command { static hidden = true + public static get requiresSyncAnalytics(): boolean { + return true + } + static summary = 'Create a new development store.' static descriptionWithMarkdown = 'Creates a new app development store in your organization.' @@ -50,11 +55,11 @@ export default class StoreCreateDev extends Command { async run(): Promise { const {flags} = await this.parse(StoreCreateDev) - const organization = await selectOrg(flags['organization-id']?.toString()) - const name = flags.name ?? (await storeNamePrompt()) - const plan = (flags.plan as DevStorePlan | undefined) ?? (await storePlanPrompt()) - try { + const organization = await selectOrg(flags['organization-id']?.toString()) + const name = flags.name ?? (await storeNamePrompt()) + const plan = (flags.plan as DevStorePlan | undefined) ?? (await storePlanPrompt()) + await createDevStore({ name, organization, @@ -66,6 +71,7 @@ export default class StoreCreateDev extends Command { }) } catch (error) { if (flags.json && error instanceof AbortError) { + await reportAnalyticsEvent({config: this.config, errorMessage: error.message, exitMode: 'expected_error'}) outputResult( JSON.stringify( { diff --git a/packages/store/src/cli/commands/store/delete.test.ts b/packages/store/src/cli/commands/store/delete.test.ts index 4756235b2d6..f6754c0b4b2 100644 --- a/packages/store/src/cli/commands/store/delete.test.ts +++ b/packages/store/src/cli/commands/store/delete.test.ts @@ -1,13 +1,19 @@ import StoreDelete from './delete.js' import {deleteDevStore} from '../../services/store/delete/dev.js' import {resolveOrganizationForStore} from '../../utilities/store-lookup/organization.js' +import {recordStoreFqdnMetadata} from '../../services/store/attribution.js' +import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics' import {AbortError} from '@shopify/cli-kit/node/error' import {outputResult} from '@shopify/cli-kit/node/output' import {isTTY, renderDangerousConfirmationPrompt} from '@shopify/cli-kit/node/ui' import {describe, expect, test, vi, beforeEach} from 'vitest' vi.mock('../../services/store/delete/dev.js') +vi.mock('../../services/store/attribution.js') vi.mock('../../utilities/store-lookup/organization.js') +vi.mock('@shopify/cli-kit/node/analytics', () => ({ + reportAnalyticsEvent: vi.fn(), +})) vi.mock('@shopify/cli-kit/node/output', async (importOriginal) => { const actual: Record = await importOriginal() @@ -35,6 +41,10 @@ beforeEach(() => { }) describe('store delete command', () => { + test('requires synchronous analytics', () => { + expect(StoreDelete.requiresSyncAnalytics).toBe(true) + }) + test('resolves the organization and passes parsed flags through to the service', async () => { await StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345']) @@ -91,6 +101,7 @@ describe('store delete command', () => { await expect(StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345'])).rejects.toThrow() expect(deleteDevStore).not.toHaveBeenCalled() + expect(reportAnalyticsEvent).not.toHaveBeenCalled() }) test('skips the confirmation prompt when --force is passed', async () => { @@ -123,22 +134,39 @@ describe('store delete command', () => { const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => { throw new Error('process.exit') }) as never) + const mockCommandCatch = vi.spyOn(StoreDelete.prototype, 'catch').mockImplementation(async (error) => { + throw error + }) await expect( StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345', '--json']), ).rejects.toThrow('process.exit') - const call = vi.mocked(outputResult).mock.calls[0]![0] as string - const parsed = JSON.parse(call) - expect(parsed).toEqual({ - error: true, - message: 'Deleting the development store my-store.myshopify.com requires confirmation.', - nextSteps: ['Use the `--force` flag to skip confirmation when running non-interactively.'], - exitCode: 1, - }) + expect(outputResult).toHaveBeenCalledTimes(1) + expect(outputResult).toHaveBeenCalledWith( + JSON.stringify( + { + error: true, + message: 'Deleting the development store my-store.myshopify.com requires confirmation.', + nextSteps: ['Use the `--force` flag to skip confirmation when running non-interactively.'], + exitCode: 1, + }, + null, + 2, + ), + ) expect(deleteDevStore).not.toHaveBeenCalled() + expect(mockExit).toHaveBeenCalledTimes(1) expect(mockExit).toHaveBeenCalledWith(1) + expect(recordStoreFqdnMetadata).toHaveBeenCalledWith('my-store.myshopify.com', false) + expect(reportAnalyticsEvent).toHaveBeenCalledTimes(1) + expect(reportAnalyticsEvent).toHaveBeenCalledWith({ + config: expect.anything(), + errorMessage: 'Deleting the development store my-store.myshopify.com requires confirmation.', + exitMode: 'expected_error', + }) + mockCommandCatch.mockRestore() mockExit.mockRestore() }) @@ -147,21 +175,38 @@ describe('store delete command', () => { const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => { throw new Error('process.exit') }) as never) + const mockCommandCatch = vi.spyOn(StoreDelete.prototype, 'catch').mockImplementation(async (error) => { + throw error + }) await expect( StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345', '--json']), ).rejects.toThrow('process.exit') - const call = vi.mocked(outputResult).mock.calls[0]![0] as string - const parsed = JSON.parse(call) - expect(parsed).toEqual({ - error: true, - message: 'Something went wrong', - nextSteps: [], - exitCode: 1, - }) + expect(outputResult).toHaveBeenCalledTimes(1) + expect(outputResult).toHaveBeenCalledWith( + JSON.stringify( + { + error: true, + message: 'Something went wrong', + nextSteps: [], + exitCode: 1, + }, + null, + 2, + ), + ) + expect(mockExit).toHaveBeenCalledTimes(1) expect(mockExit).toHaveBeenCalledWith(1) + expect(recordStoreFqdnMetadata).toHaveBeenCalledWith('my-store.myshopify.com', false) + expect(reportAnalyticsEvent).toHaveBeenCalledTimes(1) + expect(reportAnalyticsEvent).toHaveBeenCalledWith({ + config: expect.anything(), + errorMessage: 'Something went wrong', + exitMode: 'expected_error', + }) + mockCommandCatch.mockRestore() mockExit.mockRestore() }) @@ -170,20 +215,37 @@ describe('store delete command', () => { const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => { throw new Error('process.exit') }) as never) + const mockCommandCatch = vi.spyOn(StoreDelete.prototype, 'catch').mockImplementation(async (error) => { + throw error + }) await expect(StoreDelete.run(['--store', 'my-store.myshopify.com', '--json'])).rejects.toThrow('process.exit') - const call = vi.mocked(outputResult).mock.calls[0]![0] as string - const parsed = JSON.parse(call) - expect(parsed).toEqual({ - error: true, - message: 'Could not resolve organization', - nextSteps: [], - exitCode: 1, - }) + expect(outputResult).toHaveBeenCalledTimes(1) + expect(outputResult).toHaveBeenCalledWith( + JSON.stringify( + { + error: true, + message: 'Could not resolve organization', + nextSteps: [], + exitCode: 1, + }, + null, + 2, + ), + ) expect(deleteDevStore).not.toHaveBeenCalled() + expect(mockExit).toHaveBeenCalledTimes(1) expect(mockExit).toHaveBeenCalledWith(1) + expect(recordStoreFqdnMetadata).toHaveBeenCalledWith('my-store.myshopify.com', false) + expect(reportAnalyticsEvent).toHaveBeenCalledTimes(1) + expect(reportAnalyticsEvent).toHaveBeenCalledWith({ + config: expect.anything(), + errorMessage: 'Could not resolve organization', + exitMode: 'expected_error', + }) + mockCommandCatch.mockRestore() mockExit.mockRestore() }) diff --git a/packages/store/src/cli/commands/store/delete.ts b/packages/store/src/cli/commands/store/delete.ts index a2b8846b28c..7802219b679 100644 --- a/packages/store/src/cli/commands/store/delete.ts +++ b/packages/store/src/cli/commands/store/delete.ts @@ -1,8 +1,10 @@ import {deleteDevStore} from '../../services/store/delete/dev.js' import {storeFlags} from '../../flags.js' import {resolveOrganizationForStore} from '../../utilities/store-lookup/organization.js' +import {recordStoreFqdnMetadata} from '../../services/store/attribution.js' import Command from '@shopify/cli-kit/node/base-command' import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics' import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error' import {outputResult} from '@shopify/cli-kit/node/output' import {isTTY, renderDangerousConfirmationPrompt} from '@shopify/cli-kit/node/ui' @@ -11,6 +13,10 @@ import {Flags} from '@oclif/core' export default class StoreDelete extends Command { static hidden = true + public static get requiresSyncAnalytics(): boolean { + return true + } + static summary = 'Delete a development store.' static descriptionWithMarkdown = 'Deletes a development store from your organization.' @@ -38,6 +44,7 @@ export default class StoreDelete extends Command { async run(): Promise { const {flags} = await this.parse(StoreDelete) + await recordStoreFqdnMetadata(flags.store, false) try { // Deleting a store is irreversible: in non-interactive runs (CI, agents, piped @@ -67,6 +74,7 @@ export default class StoreDelete extends Command { // Only expected failures (AbortError) are rendered as JSON. Unexpected errors rethrow to the // global error handler so they keep their stack traces and get reported as CLI bugs. if (flags.json && error instanceof AbortError) { + await reportAnalyticsEvent({config: this.config, errorMessage: error.message, exitMode: 'expected_error'}) outputResult( JSON.stringify( { diff --git a/packages/store/src/cli/services/store/create/dev.test.ts b/packages/store/src/cli/services/store/create/dev.test.ts index 750c602a0c3..d6d0a75987a 100644 --- a/packages/store/src/cli/services/store/create/dev.test.ts +++ b/packages/store/src/cli/services/store/create/dev.test.ts @@ -1,4 +1,5 @@ import {createDevStore} from './dev.js' +import {recordStoreFqdnMetadata} from '../attribution.js' import {describe, expect, test, vi, beforeEach} from 'vitest' import {businessPlatformOrganizationsRequestDoc} from '@shopify/cli-kit/node/api/business-platform' @@ -32,6 +33,8 @@ vi.mock('@shopify/cli-kit/node/system', () => ({ sleep: vi.fn(), })) +vi.mock('../attribution.js') + const defaultOrg = {id: '123', businessName: 'Test Org'} const defaultMutationResult = { createAppDevelopmentStore: { @@ -253,6 +256,32 @@ describe('createDevStore', () => { expect(renderSuccess).not.toHaveBeenCalled() }) + test('records the created store domain before a later polling failure', async () => { + vi.mocked(businessPlatformOrganizationsRequestDoc) + .mockResolvedValueOnce(defaultMutationResult) + .mockResolvedValueOnce({ + organization: {id: '123', storeCreation: {status: 'FAILED'}}, + }) + + await expect( + createDevStore({name: 'test-store', organization: defaultOrg, plan: 'plus', json: false}), + ).rejects.toThrow('Store creation failed with status: FAILED') + + expect(recordStoreFqdnMetadata).toHaveBeenCalledWith('test-store.myshopify.com', true) + }) + + test('does not record store attribution for mutation failures before a domain is returned', async () => { + vi.mocked(businessPlatformOrganizationsRequestDoc).mockResolvedValueOnce({ + createAppDevelopmentStore: null, + }) + + await expect( + createDevStore({name: 'test-store', organization: defaultOrg, plan: 'plus', json: false}), + ).rejects.toThrow('unexpected empty response') + + expect(recordStoreFqdnMetadata).not.toHaveBeenCalled() + }) + test('throws AbortError when mutation returns null createAppDevelopmentStore', async () => { vi.mocked(businessPlatformOrganizationsRequestDoc).mockResolvedValueOnce({ createAppDevelopmentStore: null, @@ -275,6 +304,7 @@ describe('createDevStore', () => { await expect( createDevStore({name: 'test-store', organization: defaultOrg, plan: 'plus', json: false}), ).rejects.toThrow('Name is taken') + expect(recordStoreFqdnMetadata).not.toHaveBeenCalled() }) test('throws AbortError when mutation returns no shopDomain', async () => { @@ -289,6 +319,7 @@ describe('createDevStore', () => { await expect( createDevStore({name: 'test-store', organization: defaultOrg, plan: 'plus', json: false}), ).rejects.toThrow('no shop domain was returned') + expect(recordStoreFqdnMetadata).not.toHaveBeenCalled() }) test('throws AbortError when polling returns FAILED status', async () => { diff --git a/packages/store/src/cli/services/store/create/dev.ts b/packages/store/src/cli/services/store/create/dev.ts index f6c4bf0ed6e..f398323e0b2 100644 --- a/packages/store/src/cli/services/store/create/dev.ts +++ b/packages/store/src/cli/services/store/create/dev.ts @@ -1,4 +1,5 @@ import {businessPlatformTokenRefreshHandler} from '../business-platform.js' +import {recordStoreFqdnMetadata} from '../attribution.js' import {DEV_STORE_PLANS, DevStorePlan} from '../constants.js' import {CreateAppDevelopmentStore} from '../../../api/graphql/business-platform-organizations/generated/create_app_development_store.js' import { @@ -85,6 +86,8 @@ export async function createDevStore(options: CreateDevStoreOptions): Promise { diff --git a/packages/store/src/cli/services/store/delete/dev.test.ts b/packages/store/src/cli/services/store/delete/dev.test.ts index dd87ac85aa2..26f9c06f334 100644 --- a/packages/store/src/cli/services/store/delete/dev.test.ts +++ b/packages/store/src/cli/services/store/delete/dev.test.ts @@ -1,4 +1,5 @@ import {deleteDevStore, toOrganizationsShopifyShopId} from './dev.js' +import {recordStoreFqdnMetadata} from '../attribution.js' import {businessPlatformOrganizationsRequestDoc} from '@shopify/cli-kit/node/api/business-platform' import {ensureAuthenticatedBusinessPlatform} from '@shopify/cli-kit/node/session' import {renderSingleTask, renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' @@ -32,6 +33,8 @@ vi.mock('@shopify/cli-kit/node/system', () => ({ sleep: vi.fn(), })) +vi.mock('../attribution.js') + const defaultOrg = {id: '123', businessName: 'Test Org'} const defaultOptions = {store: 'test-store.myshopify.com', organization: defaultOrg, json: false} const defaultMutationResult = { @@ -109,6 +112,28 @@ describe('deleteDevStore', () => { }), ) expect(renderWarning).not.toHaveBeenCalled() + expect(recordStoreFqdnMetadata).toHaveBeenCalledWith('test-store.myshopify.com', true, '72193245184') + }) + + test('records matching-store attribution again when a second lookup supplies the ID', async () => { + mockBusinessPlatformRequests({ + lookupResults: [shopLookupResult(), defaultShopLookupResult], + }) + + await deleteDevStore(defaultOptions) + + expect(recordStoreFqdnMetadata).toHaveBeenNthCalledWith(1, 'test-store.myshopify.com', true, undefined) + expect(recordStoreFqdnMetadata).toHaveBeenNthCalledWith(2, 'test-store.myshopify.com', true, '72193245184') + }) + + test('does not record validated attribution when the shop lookup has no match', async () => { + const noShopMatch = {organization: {accessibleShops: {edges: []}}} + mockBusinessPlatformRequests({lookupResults: [noShopMatch, noShopMatch]}) + + await deleteDevStore(defaultOptions) + + expect(recordStoreFqdnMetadata).not.toHaveBeenCalled() + expect(renderWarning).toHaveBeenCalled() }) test('outputs JSON when --json flag is set', async () => { @@ -249,16 +274,22 @@ describe('deleteDevStore', () => { }) }) -function mockBusinessPlatformRequests(options: {mutationResult?: unknown; pollShops?: PollShop[]} = {}) { +function mockBusinessPlatformRequests( + options: {mutationResult?: unknown; pollShops?: PollShop[]; lookupResults?: unknown[]} = {}, +) { const mutationResult = options.mutationResult ?? defaultMutationResult const pollShops = options.pollShops ?? [pollShop({planName: 'cancelled'})] + const lookupResults = options.lookupResults ?? [defaultShopLookupResult] + let lookupIndex = 0 let pollIndex = 0 vi.mocked(businessPlatformOrganizationsRequestDoc).mockImplementation(async (request) => { const variables = request.variables as Record if ('search' in variables) { - return defaultShopLookupResult as never + const lookupResult = lookupResults[Math.min(lookupIndex, lookupResults.length - 1)] + lookupIndex++ + return lookupResult as never } if ('storeFqdn' in variables) { @@ -275,6 +306,23 @@ function mockBusinessPlatformRequests(options: {mutationResult?: unknown; pollSh }) } +function shopLookupResult(shopifyShopId?: string) { + return { + organization: { + accessibleShops: { + edges: [ + { + node: { + ...defaultShopLookupResult.organization.accessibleShops.edges[0]!.node, + shopifyShopId, + }, + }, + ], + }, + }, + } +} + function pollShop(overrides: NonNullable = {}): NonNullable { return { shopifyShopId: '72193245184', diff --git a/packages/store/src/cli/services/store/delete/dev.ts b/packages/store/src/cli/services/store/delete/dev.ts index 85fda9c0bb3..14d317b3461 100644 --- a/packages/store/src/cli/services/store/delete/dev.ts +++ b/packages/store/src/cli/services/store/delete/dev.ts @@ -1,4 +1,5 @@ import {businessPlatformTokenRefreshHandler} from '../business-platform.js' +import {recordStoreFqdnMetadata} from '../attribution.js' import {fetchOptionalOrganizationShop} from '../../../utilities/store-lookup/organization-shop.js' import {DeleteAppDevelopmentStore} from '../../../api/graphql/business-platform-organizations/generated/delete_app_development_store.js' import { @@ -95,6 +96,9 @@ async function fetchShopifyShopId(options: { token: string }): Promise { const shop = await fetchOptionalOrganizationShop(options) + if (shop) { + await recordStoreFqdnMetadata(options.store, true, shop.shopifyShopId) + } return shop?.shopifyShopId } From 2fd53fe1d03eab01ba6cea873a6eba3bd99d1ae4 Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Tue, 25 Aug 2026 09:46:59 -0400 Subject: [PATCH 2/2] Preserve store create error handling Assisted-By: devx/65d0e15e-19bf-4af6-8cb3-53772dd9d107 --- .../src/cli/commands/store/create/dev.test.ts | 38 +++---------------- .../src/cli/commands/store/create/dev.ts | 8 ++-- .../src/cli/commands/store/delete.test.ts | 2 +- 3 files changed, 11 insertions(+), 37 deletions(-) diff --git a/packages/store/src/cli/commands/store/create/dev.test.ts b/packages/store/src/cli/commands/store/create/dev.test.ts index 1decbaef7b6..28bcfd31662 100644 --- a/packages/store/src/cli/commands/store/create/dev.test.ts +++ b/packages/store/src/cli/commands/store/create/dev.test.ts @@ -6,7 +6,7 @@ import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics' import {AbortError} from '@shopify/cli-kit/node/error' import {outputResult} from '@shopify/cli-kit/node/output' import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system' -import {describe, expect, test, vi, beforeEach} from 'vitest' +import {beforeEach, describe, expect, test, vi} from 'vitest' vi.mock('../../../services/store/create/dev.js') vi.mock('../../../prompts/store.js') @@ -232,44 +232,18 @@ describe('store create dev command', () => { mockExit.mockRestore() }) - test('outputs structured JSON error when --json is active and organization selection throws AbortError', async () => { + test('leaves organization selection errors to global handling when --json is active', async () => { vi.mocked(selectOrg).mockRejectedValueOnce(new AbortError('Could not select organization')) - const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => { - throw new Error('process.exit') - }) as never) - const mockCommandCatch = vi.spyOn(StoreCreateDev.prototype, 'catch').mockImplementation(async (error) => { + vi.spyOn(StoreCreateDev.prototype, 'catch').mockImplementation(async (error) => { throw error }) await expect( StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345', '--json']), - ).rejects.toThrow('process.exit') - - expect(outputResult).toHaveBeenCalledTimes(1) - expect(outputResult).toHaveBeenCalledWith( - JSON.stringify( - { - error: true, - message: 'Could not select organization', - nextSteps: [], - exitCode: 1, - }, - null, - 2, - ), - ) - expect(createDevStore).not.toHaveBeenCalled() - expect(mockExit).toHaveBeenCalledTimes(1) - expect(mockExit).toHaveBeenCalledWith(1) - expect(reportAnalyticsEvent).toHaveBeenCalledTimes(1) - expect(reportAnalyticsEvent).toHaveBeenCalledWith({ - config: expect.anything(), - errorMessage: 'Could not select organization', - exitMode: 'expected_error', - }) + ).rejects.toThrow('Could not select organization') - mockCommandCatch.mockRestore() - mockExit.mockRestore() + expect(outputResult).not.toHaveBeenCalled() + expect(reportAnalyticsEvent).not.toHaveBeenCalled() }) test('does not output JSON for non-AbortError even when --json is active', async () => { diff --git a/packages/store/src/cli/commands/store/create/dev.ts b/packages/store/src/cli/commands/store/create/dev.ts index 7830e89e39d..74ba03c7401 100644 --- a/packages/store/src/cli/commands/store/create/dev.ts +++ b/packages/store/src/cli/commands/store/create/dev.ts @@ -55,11 +55,11 @@ export default class StoreCreateDev extends Command { async run(): Promise { const {flags} = await this.parse(StoreCreateDev) - try { - const organization = await selectOrg(flags['organization-id']?.toString()) - const name = flags.name ?? (await storeNamePrompt()) - const plan = (flags.plan as DevStorePlan | undefined) ?? (await storePlanPrompt()) + const organization = await selectOrg(flags['organization-id']?.toString()) + const name = flags.name ?? (await storeNamePrompt()) + const plan = (flags.plan as DevStorePlan | undefined) ?? (await storePlanPrompt()) + try { await createDevStore({ name, organization, diff --git a/packages/store/src/cli/commands/store/delete.test.ts b/packages/store/src/cli/commands/store/delete.test.ts index f6754c0b4b2..2933eb1e7d0 100644 --- a/packages/store/src/cli/commands/store/delete.test.ts +++ b/packages/store/src/cli/commands/store/delete.test.ts @@ -6,7 +6,7 @@ import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics' import {AbortError} from '@shopify/cli-kit/node/error' import {outputResult} from '@shopify/cli-kit/node/output' import {isTTY, renderDangerousConfirmationPrompt} from '@shopify/cli-kit/node/ui' -import {describe, expect, test, vi, beforeEach} from 'vitest' +import {beforeEach, describe, expect, test, vi} from 'vitest' vi.mock('../../services/store/delete/dev.js') vi.mock('../../services/store/attribution.js')