Skip to content
Merged
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
77 changes: 76 additions & 1 deletion packages/app/src/cli/services/dev.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,92 @@
import {warnIfScopesDifferBeforeDev, blockIfMigrationIncomplete} from './dev.js'
import {dev, warnIfScopesDifferBeforeDev, blockIfMigrationIncomplete} from './dev.js'
import {setupDevProcesses} from './dev/processes/setup-dev-processes.js'
import {renderDev} from './dev/ui.js'
import {fetchAppRemoteConfiguration} from './app/select-app.js'
import {
testAppLinked,
testDeveloperPlatformClient,
testOrganization,
testOrganizationApp,
testOrganizationStore,
testProject,
} from '../models/app/app.test-data.js'
import metadata from '../metadata.js'
import {describe, expect, test, vi} from 'vitest'
import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output'
import {hashString} from '@shopify/cli-kit/node/crypto'
import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics'
import {checkPortAvailability, getAvailableTCPPort} from '@shopify/cli-kit/node/tcp'

vi.mock('./dev/fetch.js')
vi.mock('./dev/processes/setup-dev-processes.js')
vi.mock('./dev/ui.js')
vi.mock('./app/select-app.js')
vi.mock('@shopify/cli-kit/node/analytics')
vi.mock('@shopify/cli-kit/node/tcp')
vi.mock('../utilities/mkcert.js')

describe('dev', () => {
test('logs store domain metadata when launching dev', async () => {
const store = testOrganizationStore({shopDomain: 'dev-store.myshopify.com'})
const app = testAppLinked()
let publicMetadata: Record<string, unknown> | undefined
let sensitiveMetadata: Record<string, unknown> | undefined

vi.mocked(fetchAppRemoteConfiguration).mockResolvedValue({name: 'Remote app', application_url: '', embedded: true})
vi.mocked(getAvailableTCPPort).mockResolvedValue(3456)
vi.mocked(checkPortAvailability).mockResolvedValue(true)
vi.mocked(setupDevProcesses).mockResolvedValue({
processes: [],
previewUrl: 'https://dev-store.myshopify.com/admin/apps/api-key',
graphiqlUrl: undefined,
devSessionStatusManager: {} as any,
})
vi.mocked(renderDev).mockResolvedValue(undefined)

const addPublicMetadata = vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async (getMetadata) => {
publicMetadata = getMetadata() as Record<string, unknown>
})
const addSensitiveMetadata = vi.spyOn(metadata, 'addSensitiveMetadata').mockImplementation(async (getMetadata) => {
sensitiveMetadata = getMetadata() as Record<string, unknown>
})

await dev({
app,
project: testProject({usesWorkspaces: true}),
remoteApp: testOrganizationApp({apiKey: 'api-key'}),
organization: testOrganization(),
specifications: [],
developerPlatformClient: testDeveloperPlatformClient({supportsDevSessions: false}),
store,
directory: app.directory,
update: false,
commandConfig: {} as any,
skipDependenciesInstallation: true,
tunnel: {mode: 'custom', url: 'https://localhost:3456'},
})

expect(publicMetadata).toEqual(
expect.objectContaining({
cmd_dev_tunnel_type: 'localhost',
cmd_dev_urls_updated: false,
store_fqdn_hash: hashString(store.shopDomain),
store_domain: store.shopDomain,
cmd_app_dependency_installation_skipped: true,
}),
)
expect(sensitiveMetadata).toEqual(
expect.objectContaining({
store_fqdn: store.shopDomain,
cmd_dev_tunnel_custom: undefined,
}),
)
expect(reportAnalyticsEvent).toHaveBeenCalledWith({config: {}, exitMode: 'ok'})

addPublicMetadata.mockRestore()
addSensitiveMetadata.mockRestore()
})
})

describe('warnIfScopesDifferBeforeDev', () => {
const appsWithScopes = (local: string, remote: string) => {
const localApp = testAppLinked({})
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/services/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ async function logMetadataForDev(options: {
cmd_dev_tunnel_custom_hash: tunnelType === 'custom' ? hashString(options.tunnelUrl) : undefined,
cmd_dev_urls_updated: options.shouldUpdateURLs,
store_fqdn_hash: hashString(options.storeFqdn),
store_domain: options.storeFqdn,
cmd_app_dependency_installation_skipped: options.devOptions.skipDependenciesInstallation,
}))

Expand Down
26 changes: 26 additions & 0 deletions packages/app/src/cli/services/store-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ describe('storeContext', () => {
expect(meta).toEqual(
expect.objectContaining({
store_fqdn_hash: hashString(mockStore.shopDomain),
store_domain: mockStore.shopDomain,
}),
)

Expand All @@ -186,6 +187,31 @@ describe('storeContext', () => {
})
})

test('normalizes the selected store before logging metadata', async () => {
await inTemporaryDirectory(async (dir) => {
const unnormalizedStore = testOrganizationStore({shopId: 'store1', shopDomain: 'test-store'})
vi.mocked(fetchStore).mockResolvedValue(unnormalizedStore)
await prepareAppFolder(mockApp, dir)

await storeContext({appContextResult, forceReselectStore: false})

const meta = metadata.getAllPublicMetadata()
expect(meta).toEqual(
expect.objectContaining({
store_fqdn_hash: hashString('test-store.myshopify.com'),
store_domain: 'test-store.myshopify.com',
}),
)

const sensitiveMeta = metadata.getAllSensitiveMetadata()
expect(sensitiveMeta).toEqual(
expect.objectContaining({
store_fqdn: 'test-store.myshopify.com',
}),
)
})
})

test('adds hidden config to gitignore if needed', async () => {
await inTemporaryDirectory(async (dir) => {
await prepareAppFolder(mockApp, dir)
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/cli/services/store-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export async function storeContext({
selectedStore = await selectStore(allStores, organization, developerPlatformClient)
}

await logMetadata(selectedStore, forceReselectStore)
selectedStore.shopDomain = normalizeStoreFqdn(selectedStore.shopDomain)
await logMetadata(selectedStore, forceReselectStore)

// Save the selected store in the hidden config file
if (selectedStore.shopDomain !== cachedStoreURL || !devStoreUrlFromHiddenConfig) {
Expand All @@ -82,6 +82,7 @@ async function logMetadata(selectedStore: OrganizationStore, resetUsed: boolean)
await metadata.addPublicMetadata(() => ({
cmd_app_reset_used: resetUsed,
store_fqdn_hash: hashString(selectedStore.shopDomain),
store_domain: selectedStore.shopDomain,
}))

await metadata.addSensitiveMetadata(() => ({
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-kit/src/public/node/monorail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ describe('monorail', () => {
user_id: '42',
store_fqdn_hash: 'hashed-store',
store_fqdn_validated: true,
store_domain: 'shop.myshopify.com',
},
{
args: '--store shop.myshopify.com',
Expand Down Expand Up @@ -98,6 +99,7 @@ describe('monorail', () => {
user_id: '42',
store_fqdn_hash: 'hashed-store',
store_fqdn_validated: true,
store_domain: 'shop.myshopify.com',
args: '--store shop.myshopify.com',
store_fqdn: 'shop.myshopify.com',
},
Expand Down
3 changes: 2 additions & 1 deletion packages/cli-kit/src/public/node/monorail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const url = 'https://monorail-edge.shopifysvc.com/v1/produce'
type Optional<T> = T | null

// This is the topic name of the main event we log to Monorail, the command tracker
export const MONORAIL_COMMAND_TOPIC = 'app_cli3_command/1.24'
export const MONORAIL_COMMAND_TOPIC = 'app_cli3_command/1.26'

export interface Schemas {
[MONORAIL_COMMAND_TOPIC]: {
Expand Down Expand Up @@ -46,6 +46,7 @@ export interface Schemas {
is_employee: boolean
store_fqdn_hash?: Optional<string>
store_fqdn_validated?: Optional<boolean>
store_domain?: Optional<string>
user_id: string

// Any and all commands
Expand Down
4 changes: 3 additions & 1 deletion packages/store/src/cli/services/store/attribution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('store command attribution', () => {
vi.mocked(hashString).mockReturnValue('hashed-store')
})

test('records the sensitive, hashed, and validation state for a store fqdn', async () => {
test('records the sensitive, hashed, validation, and public store domain for a store fqdn', async () => {
await recordStoreFqdnMetadata('shop.myshopify.com', true)

expect(addSensitiveMetadata).toHaveBeenCalledWith(expect.any(Function))
Expand All @@ -21,7 +21,9 @@ describe('store command attribution', () => {
expect(vi.mocked(addPublicMetadata).mock.calls[0]![0]()).toEqual({
store_fqdn_hash: 'hashed-store',
store_fqdn_validated: true,
store_domain: 'shop.myshopify.com',
})
expect(hashString).toHaveBeenCalledWith('shop.myshopify.com')
})

})
6 changes: 5 additions & 1 deletion packages/store/src/cli/services/store/attribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ import {addPublicMetadata, addSensitiveMetadata} from '@shopify/cli-kit/node/met

export async function recordStoreFqdnMetadata(storeFqdn: string, validated: boolean): Promise<void> {
await addSensitiveMetadata(() => ({store_fqdn: storeFqdn}))
await addPublicMetadata(() => ({store_fqdn_hash: hashString(storeFqdn), store_fqdn_validated: validated}))
await addPublicMetadata(() => ({
store_fqdn_hash: hashString(storeFqdn),
store_fqdn_validated: validated,
store_domain: storeFqdn,
}))
}
60 changes: 60 additions & 0 deletions packages/theme/src/cli/utilities/theme-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ import {fileExistsSync} from '@shopify/cli-kit/node/fs'
import {AbortError} from '@shopify/cli-kit/node/error'
import {resolvePath} from '@shopify/cli-kit/node/path'
import {renderConcurrent, renderConfirmationPrompt, renderError, renderWarning} from '@shopify/cli-kit/node/ui'
import {addPublicMetadata, addSensitiveMetadata} from '@shopify/cli-kit/node/metadata'
import {hashString} from '@shopify/cli-kit/node/crypto'

import type {Writable} from 'stream'

vi.mock('@shopify/cli-kit/node/session')
vi.mock('@shopify/cli-kit/node/environments')
vi.mock('@shopify/cli-kit/node/ui')
vi.mock('@shopify/cli-kit/node/metadata', () => ({
addPublicMetadata: vi.fn(),
addSensitiveMetadata: vi.fn(),
}))
vi.mock('./theme-store.js')
vi.mock('@shopify/cli-kit/node/fs')

Expand Down Expand Up @@ -168,6 +174,8 @@ describe('ThemeCommand', () => {
let mockSession: AdminSession

beforeEach(() => {
vi.clearAllMocks()

mockSession = {
token: 'test-token',
storeFqdn: 'test-store.myshopify.com',
Expand Down Expand Up @@ -227,6 +235,15 @@ describe('ThemeCommand', () => {
args: {},
context: undefined,
})
const publicMetadata = vi.mocked(addPublicMetadata).mock.calls.map(([getMetadata]) => getMetadata())
expect(publicMetadata).toContainEqual(
expect.objectContaining({
store_fqdn_hash: hashString(mockSession.storeFqdn),
store_domain: mockSession.storeFqdn,
}),
)
const sensitiveMetadata = vi.mocked(addSensitiveMetadata).mock.calls.map(([getMetadata]) => getMetadata())
expect(sensitiveMetadata).toContainEqual({store_fqdn: mockSession.storeFqdn})
})

test('single environment provided but not found in TOML - throws AbortError', async () => {
Expand Down Expand Up @@ -319,6 +336,49 @@ describe('ThemeCommand', () => {
)
})

test('multiple environments provided - logs metadata for each authenticated session', async () => {
// Given
vi.mocked(loadEnvironment)
.mockResolvedValueOnce({store: 'store1.myshopify.com', development: true})
.mockResolvedValueOnce({store: 'store2.myshopify.com', theme: 'staging'})
vi.mocked(ensureThemeStore).mockImplementation((options: any) => options.store)
vi.mocked(ensureAuthenticatedThemes).mockImplementation(async (store) => ({
token: 'test-token',
storeFqdn: store as string,
}))
vi.mocked(renderConcurrent).mockImplementation(async ({processes}) => {
for (const process of processes) {
// eslint-disable-next-line no-await-in-loop
await process.action({} as Writable, {} as Writable, {} as any)
}
})

await CommandConfig.load()
const command = new TestThemeCommand(['--environment', 'development', '--environment', 'staging'], CommandConfig)

// When
await command.run()

// Then
const publicMetadata = vi.mocked(addPublicMetadata).mock.calls.map(([getMetadata]) => getMetadata())
expect(publicMetadata).toEqual(
expect.arrayContaining([
expect.objectContaining({
store_fqdn_hash: hashString('store1.myshopify.com'),
store_domain: 'store1.myshopify.com',
}),
expect.objectContaining({
store_fqdn_hash: hashString('store2.myshopify.com'),
store_domain: 'store2.myshopify.com',
}),
]),
)
const sensitiveMetadata = vi.mocked(addSensitiveMetadata).mock.calls.map(([getMetadata]) => getMetadata())
expect(sensitiveMetadata).toEqual(
expect.arrayContaining([{store_fqdn: 'store1.myshopify.com'}, {store_fqdn: 'store2.myshopify.com'}]),
)
})

test("throws an AbortError if the path doesn't exist", async () => {
await CommandConfig.load()
const command = new TestThemeCommand([], CommandConfig)
Expand Down
1 change: 1 addition & 0 deletions packages/theme/src/cli/utilities/theme-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ export default abstract class ThemeCommand extends Command {
const data = compileData()
await addPublicMetadata(() => ({
store_fqdn_hash: hashString(session.storeFqdn),
store_domain: session.storeFqdn,

cmd_theme_timings: JSON.stringify(data.timings),
cmd_theme_errors: JSON.stringify(data.errors),
Expand Down
Loading