From 07a4568af333ed6201bb628442968d58c0b12d47 Mon Sep 17 00:00:00 2001 From: Adam Barefoot Date: Wed, 12 Aug 2026 16:30:56 -0400 Subject: [PATCH] Show App Events extension status in dev sessions Assisted-By: devx/b10cb676-29cc-4a4f-8b91-853b8aa9b5a7 --- .changeset/quiet-app-events-status.md | 5 +++ .../models/extensions/load-specifications.ts | 2 + .../specification.integration.test.ts | 22 ++++++++++ .../cli/models/extensions/specification.ts | 2 + .../analytics_app_events.test.ts | 12 ++++++ .../specifications/analytics_app_events.ts | 12 ++++++ .../dev-session/dev-session-logger.test.ts | 41 +++++++++++++++++++ .../fetch-extension-specifications.test.ts | 38 +++++++++++++++++ 8 files changed, 134 insertions(+) create mode 100644 .changeset/quiet-app-events-status.md create mode 100644 packages/app/src/cli/models/extensions/specifications/analytics_app_events.test.ts create mode 100644 packages/app/src/cli/models/extensions/specifications/analytics_app_events.ts diff --git a/.changeset/quiet-app-events-status.md b/.changeset/quiet-app-events-status.md new file mode 100644 index 00000000000..14cf7984c70 --- /dev/null +++ b/.changeset/quiet-app-events-status.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': minor +--- + +Show a status message when an Analytics App Events extension loads during `shopify app dev`. diff --git a/packages/app/src/cli/models/extensions/load-specifications.ts b/packages/app/src/cli/models/extensions/load-specifications.ts index 375be10885d..c4aa731ab30 100644 --- a/packages/app/src/cli/models/extensions/load-specifications.ts +++ b/packages/app/src/cli/models/extensions/load-specifications.ts @@ -29,6 +29,7 @@ import editorExtensionCollectionSpecification from './specifications/editor_exte import channelSpecificationSpec from './specifications/channel.js' import orderAttributionConfigSpec from './specifications/order_attribution_config.js' import adminLinkSpec from './specifications/admin_link.js' +import analyticsAppEventsSpec from './specifications/analytics_app_events.js' const SORTED_CONFIGURATION_SPEC_IDENTIFIERS = [ BrandingSpecIdentifier, @@ -82,6 +83,7 @@ function loadSpecifications() { channelSpecificationSpec, orderAttributionConfigSpec, adminLinkSpec, + analyticsAppEventsSpec, ] return [...configModuleSpecs, ...moduleSpecs] as ExtensionSpecification[] diff --git a/packages/app/src/cli/models/extensions/specification.integration.test.ts b/packages/app/src/cli/models/extensions/specification.integration.test.ts index f49d0712f3a..7153bb049f3 100644 --- a/packages/app/src/cli/models/extensions/specification.integration.test.ts +++ b/packages/app/src/cli/models/extensions/specification.integration.test.ts @@ -29,9 +29,13 @@ describe('allLocalSpecs', () => { test('loads the specifications successfully', async () => { // When const got = await loadLocalExtensionsSpecifications() + const analyticsAppEventsSpec = got.find((specification) => specification.identifier === 'analytics_app_events') + const adminLinkSpec = got.find((specification) => specification.identifier === 'admin_link') // Then expect(got.length).not.toEqual(0) + expect(analyticsAppEventsSpec).toBeDefined() + expect(adminLinkSpec?.getDevSessionUpdateMessages).toBeUndefined() }) }) @@ -95,6 +99,24 @@ describe('createContractBasedModuleSpecification', () => { // Then expect(got.clientSteps).toBeUndefined() }) + + test('passes dev session update messages through to the created specification', async () => { + // Given + const getDevSessionUpdateMessages = async () => ['Extension loaded'] + const specification = createContractBasedModuleSpecification({ + identifier: 'test', + uidStrategy: 'uuid', + experience: 'extension', + appModuleFeatures: () => [], + getDevSessionUpdateMessages, + }) + + // When + const messages = await specification.getDevSessionUpdateMessages!({}) + + // Then + expect(messages).toEqual(['Extension loaded']) + }) }) describe('createExtensionSpecification', () => { diff --git a/packages/app/src/cli/models/extensions/specification.ts b/packages/app/src/cli/models/extensions/specification.ts index 0dc1825a0a1..b7f7f49bd38 100644 --- a/packages/app/src/cli/models/extensions/specification.ts +++ b/packages/app/src/cli/models/extensions/specification.ts @@ -301,6 +301,7 @@ export function createContractBasedModuleSpecification, ) { return createExtensionSpecification({ @@ -312,6 +313,7 @@ export function createContractBasedModuleSpecification { let parsedConfig = configWithoutFirstClassFields(config) if (spec.appModuleFeatures().includes('localization')) { diff --git a/packages/app/src/cli/models/extensions/specifications/analytics_app_events.test.ts b/packages/app/src/cli/models/extensions/specifications/analytics_app_events.test.ts new file mode 100644 index 00000000000..8ae3dc44d4c --- /dev/null +++ b/packages/app/src/cli/models/extensions/specifications/analytics_app_events.test.ts @@ -0,0 +1,12 @@ +import analyticsAppEventsSpec from './analytics_app_events.js' +import {describe, expect, test} from 'vitest' + +describe('analytics_app_events', () => { + test('reports when the extension has loaded', async () => { + // When + const messages = await analyticsAppEventsSpec.getDevSessionUpdateMessages!({}) + + // Then + expect(messages).toEqual(['Extension loaded']) + }) +}) diff --git a/packages/app/src/cli/models/extensions/specifications/analytics_app_events.ts b/packages/app/src/cli/models/extensions/specifications/analytics_app_events.ts new file mode 100644 index 00000000000..8ab29f08d00 --- /dev/null +++ b/packages/app/src/cli/models/extensions/specifications/analytics_app_events.ts @@ -0,0 +1,12 @@ +import {createContractBasedModuleSpecification} from '../specification.js' + +// The platform owns the App Events contract; CLI contributes only this dev-session status message. +const analyticsAppEventsSpec = createContractBasedModuleSpecification({ + identifier: 'analytics_app_events', + uidStrategy: 'single', + experience: 'extension', + appModuleFeatures: () => [], + getDevSessionUpdateMessages: async () => ['Extension loaded'], +}) + +export default analyticsAppEventsSpec diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.test.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.test.ts index bbe871c54ca..020b7a8b725 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.test.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.test.ts @@ -2,10 +2,16 @@ import {DevSessionLogger} from './dev-session-logger.js' import {UserError} from './dev-session.js' import {AppEvent, EventType} from '../../app-events/app-event-watcher.js' import {ExtensionInstance} from '../../../../models/extensions/extension-instance.js' +import analyticsAppEventsSpec from '../../../../models/extensions/specifications/analytics_app_events.js' import {describe, expect, test, vi, beforeEach} from 'vitest' import {JsonMapType} from '@shopify/cli-kit/node/toml' +import {useConcurrentOutputContext} from '@shopify/cli-kit/node/ui/components' import {Writable} from 'stream' +vi.mock('@shopify/cli-kit/node/ui/components', () => ({ + useConcurrentOutputContext: vi.fn((_, callback: () => void) => callback()), +})) + describe('DevSessionLogger', () => { let output: string[] let stdout: Writable @@ -226,6 +232,41 @@ describe('DevSessionLogger', () => { expect(output).toMatchInlineSnapshot(`[]`) expect(mockExtension.getDevSessionUpdateMessages).not.toHaveBeenCalled() }) + + test('prefixes Analytics App Events messages with the extension handle', async () => { + // Given + const analyticsAppEventsExtension = new ExtensionInstance({ + configuration: {}, + configurationPath: '', + directory: '', + specification: analyticsAppEventsSpec, + }) + const event: AppEvent = { + app: {configuration: {}} as any, + extensionEvents: [ + { + type: EventType.Created, + extension: analyticsAppEventsExtension, + }, + ], + path: '', + startTime: [0, 0], + } + + // When + await logger.logExtensionUpdateMessages(event) + + // Then + expect(output).toMatchInlineSnapshot(` + [ + "\u001b[90m└ \u001b[39mExtension loaded", + ] + `) + expect(vi.mocked(useConcurrentOutputContext)).toHaveBeenCalledWith( + {outputPrefix: 'analytics_app_events', stripAnsi: false}, + expect.any(Function), + ) + }) }) describe('logMultipleErrors', () => { diff --git a/packages/app/src/cli/services/generate/fetch-extension-specifications.test.ts b/packages/app/src/cli/services/generate/fetch-extension-specifications.test.ts index 189242534d7..0a239339040 100644 --- a/packages/app/src/cli/services/generate/fetch-extension-specifications.test.ts +++ b/packages/app/src/cli/services/generate/fetch-extension-specifications.test.ts @@ -1,4 +1,5 @@ import {fetchSpecifications} from './fetch-extension-specifications.js' +import {RemoteSpecification} from '../../api/graphql/extension_specifications.js' import {testDeveloperPlatformClient, testOrganizationApp} from '../../models/app/app.test-data.js' import {describe, expect, test} from 'vitest' @@ -106,4 +107,41 @@ describe('fetchExtensionSpecifications', () => { expect(withoutLocalization?.appModuleFeatures()).toEqual([]) expect(withLocalization?.appModuleFeatures()).toEqual(['localization']) }) + + test('uses the remote App Events contract with the local dev session message', async () => { + // Given + const analyticsAppEventsRemoteSpec: RemoteSpecification = { + name: 'App Events', + externalName: 'App Events', + identifier: 'analytics_app_events', + externalIdentifier: 'analytics_app_events', + gated: false, + experience: 'extension', + managementExperience: 'cli', + registrationLimit: 1, + uidStrategy: 'single', + validationSchema: { + jsonSchema: + '{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","additionalProperties":false,"properties":{"namespace":{"type":"string"},"events":{"type":"array"}},"required":["namespace","events"]}', + }, + } + const developerPlatformClient = testDeveloperPlatformClient({ + specifications: () => Promise.resolve([analyticsAppEventsRemoteSpec]), + }) + + // When + const specifications = await fetchSpecifications({ + developerPlatformClient, + app: testOrganizationApp(), + }) + const analyticsAppEventsSpec = specifications.find( + (specification) => specification.identifier === 'analytics_app_events', + )! + + // Then + expect(analyticsAppEventsSpec.uidStrategy).toBe('single') + await expect(analyticsAppEventsSpec.getDevSessionUpdateMessages!({})).resolves.toEqual(['Extension loaded']) + expect(analyticsAppEventsSpec.parseConfigurationObject({namespace: 'example-app', events: []}).state).toBe('ok') + expect(analyticsAppEventsSpec.parseConfigurationObject({namespace: 'example-app'}).state).toBe('error') + }) })