Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/quiet-app-events-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/app': minor
---

Show a status message when an Analytics App Events extension loads during `shopify app dev`.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -82,6 +83,7 @@ function loadSpecifications() {
channelSpecificationSpec,
orderAttributionConfigSpec,
adminLinkSpec,
analyticsAppEventsSpec,
]

return [...configModuleSpecs, ...moduleSpecs] as ExtensionSpecification[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})

Expand Down Expand Up @@ -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', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/cli/models/extensions/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ export function createContractBasedModuleSpecification<TConfiguration extends Ba
| 'experience'
| 'transformRemoteToLocal'
| 'devSessionWatchConfig'
| 'getDevSessionUpdateMessages'
>,
) {
return createExtensionSpecification({
Expand All @@ -312,6 +313,7 @@ export function createContractBasedModuleSpecification<TConfiguration extends Ba
uidStrategy: spec.uidStrategy,
transformRemoteToLocal: spec.transformRemoteToLocal,
devSessionWatchConfig: spec.devSessionWatchConfig,
getDevSessionUpdateMessages: spec.getDevSessionUpdateMessages,
deployConfig: async (config, directory) => {
let parsedConfig = configWithoutFirstClassFields(config)
if (spec.appModuleFeatures().includes('localization')) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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'])
})
})
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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')
})
})
Loading