diff --git a/.changeset/doc-fetch-url-path-telemetry.md b/.changeset/doc-fetch-url-path-telemetry.md new file mode 100644 index 00000000000..374a5495b95 --- /dev/null +++ b/.changeset/doc-fetch-url-path-telemetry.md @@ -0,0 +1,6 @@ +--- +'@shopify/cli': patch +'@shopify/cli-kit': patch +--- + +Record the fetched shopify.dev URL path in `doc fetch` command analytics so document-specific usage (such as the App Store self-review requirements page) can be attributed alongside the app context. diff --git a/packages/cli-kit/src/public/node/metadata.ts b/packages/cli-kit/src/public/node/metadata.ts index 2530ab6d96b..6562dc593ae 100644 --- a/packages/cli-kit/src/public/node/metadata.ts +++ b/packages/cli-kit/src/public/node/metadata.ts @@ -176,11 +176,12 @@ export function createRuntimeMetadataContainer< } // We want to track anything that ends up getting sent to monorail as `cmd_all_*`, -// `cmd_app_*`, `cmd_theme_*`, `store_*`, and `env_auto_upgrade_*` +// `cmd_app_*`, `cmd_theme_*`, `cmd_doc_*`, `store_*`, and `env_auto_upgrade_*` type CmdFieldsFromMonorail = PickByPrefix & PickByPrefix & PickByPrefix & PickByPrefix & + PickByPrefix & PickByPrefix & PickByPrefix diff --git a/packages/cli-kit/src/public/node/monorail.ts b/packages/cli-kit/src/public/node/monorail.ts index c797cfe2e88..1618a09b052 100644 --- a/packages/cli-kit/src/public/node/monorail.ts +++ b/packages/cli-kit/src/public/node/monorail.ts @@ -10,7 +10,7 @@ const url = 'https://monorail-edge.shopifysvc.com/v1/produce' type Optional = 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.28' +export const MONORAIL_COMMAND_TOPIC = 'app_cli3_command/1.29' export interface Schemas { [MONORAIL_COMMAND_TOPIC]: { @@ -66,6 +66,9 @@ export interface Schemas { cmd_all_timing_prompts_ms?: Optional cmd_all_timing_active_ms?: Optional + // Doc related commands + cmd_doc_fetch_url_path?: Optional + // Auto-upgrade env_auto_upgrade_enabled?: Optional env_auto_upgrade_accepted?: Optional diff --git a/packages/cli/src/cli/services/commands/doc/fetch.test.ts b/packages/cli/src/cli/services/commands/doc/fetch.test.ts index 804897640ae..fd3277eb199 100644 --- a/packages/cli/src/cli/services/commands/doc/fetch.test.ts +++ b/packages/cli/src/cli/services/commands/doc/fetch.test.ts @@ -5,9 +5,11 @@ import {outputResult} from '@shopify/cli-kit/node/output' import {AbortError} from '@shopify/cli-kit/node/error' import {inTemporaryDirectory, readFile, fileExistsSync} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' +import {addPublicMetadata} from '@shopify/cli-kit/node/metadata' vi.mock('@shopify/cli-kit/node/http') vi.mock('@shopify/cli-kit/node/output') +vi.mock('@shopify/cli-kit/node/metadata') const okResponse = (body: string) => ({ok: true, status: 200, statusText: 'OK', text: () => Promise.resolve(body)}) as any @@ -26,6 +28,17 @@ describe('docFetchService', () => { expect(outputResult).toHaveBeenCalledWith('# Doc') }) + test('records the fetched URL path, without query or fragment, after a successful response', async () => { + await docFetchService( + 'https://shopify.dev/docs/apps/launch/app-store-review/app-store-ai-self-review-requirements?utm=x#top', + ) + + expect(addPublicMetadata).toHaveBeenCalledTimes(1) + expect(vi.mocked(addPublicMetadata).mock.calls[0]![0]()).toEqual({ + cmd_doc_fetch_url_path: '/docs/apps/launch/app-store-review/app-store-ai-self-review-requirements', + }) + }) + test('accepts shopify.dev subdomains', async () => { await docFetchService('https://www.shopify.dev/docs') @@ -35,6 +48,7 @@ describe('docFetchService', () => { test('rejects URLs from disallowed hosts without fetching', async () => { await expect(docFetchService('https://example.com/docs')).rejects.toThrowError(AbortError) expect(fetch).not.toHaveBeenCalled() + expect(addPublicMetadata).not.toHaveBeenCalled() }) test('rejects malformed URLs without fetching', async () => { @@ -70,5 +84,6 @@ describe('docFetchService', () => { await expect(docFetchService('https://shopify.dev/missing')).rejects.toThrowError(AbortError) expect(outputResult).not.toHaveBeenCalled() + expect(addPublicMetadata).not.toHaveBeenCalled() }) }) diff --git a/packages/cli/src/cli/services/commands/doc/fetch.ts b/packages/cli/src/cli/services/commands/doc/fetch.ts index 34b2aaefb40..4c75cbbe15a 100644 --- a/packages/cli/src/cli/services/commands/doc/fetch.ts +++ b/packages/cli/src/cli/services/commands/doc/fetch.ts @@ -2,6 +2,7 @@ import {fetch} from '@shopify/cli-kit/node/http' import {outputInfo, outputResult} from '@shopify/cli-kit/node/output' import {AbortError} from '@shopify/cli-kit/node/error' import {mkdir, writeFile} from '@shopify/cli-kit/node/fs' +import {addPublicMetadata} from '@shopify/cli-kit/node/metadata' import {dirname, resolvePath} from '@shopify/cli-kit/node/path' // Every page on shopify.dev has a Markdown representation, which is the clean, @@ -46,6 +47,10 @@ export async function docFetchService(url: string, outputPath?: string, language throw new AbortError(`Failed to fetch ${url}: ${response.status} ${response.statusText}`) } + // Recorded only after a successful response, so the field can only hold the + // path of a real shopify.dev document. Path only: no query string or fragment. + await addPublicMetadata(() => ({cmd_doc_fetch_url_path: parsedURL.pathname})) + const body = await response.text() // When an output path is provided, write the document to disk (creating any