diff --git a/CHANGELOG.md b/CHANGELOG.md index a63bb18a14..fce9a0b093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ This is the log of notable changes to EAS CLI and related packages. - [build-tools] Add composable custom build functions for downloading, installing, and launching application archives in simulator sessions. ([#4222](https://github.com/expo/eas-cli/pull/4222) by [@szdziedzic](https://github.com/szdziedzic)) - [eas-cli] Add `--build-id`, `--application-archive-url`, and `--expo-go` to `eas simulator` to install and launch an application before the session is ready. ([#4223](https://github.com/expo/eas-cli/pull/4223) by [@szdziedzic](https://github.com/szdziedzic)) - [eas-cli] Add `--environment` flag to the `eas observe:*` commands. ([#4275](https://github.com/expo/eas-cli/pull/4275) by [@kadikraman](https://github.com/kadikraman)) +- [eas-cli] Add `apple` value to the `--platform` flag of the `eas observe:*` commands. ([#4276](https://github.com/expo/eas-cli/pull/4276) by [@kadikraman](https://github.com/kadikraman)) ### 🐛 Bug fixes diff --git a/packages/eas-cli/src/commands/observe/__tests__/events.test.ts b/packages/eas-cli/src/commands/observe/__tests__/events.test.ts index 5717f2bdac..6fb8fb04b7 100644 --- a/packages/eas-cli/src/commands/observe/__tests__/events.test.ts +++ b/packages/eas-cli/src/commands/observe/__tests__/events.test.ts @@ -183,7 +183,8 @@ describe(ObserveEvents, () => { appId: projectId, startTime: '2025-06-08T12:00:00.000Z', endTime: '2025-06-15T12:00:00.000Z', - platform: AppObservePlatform.Ios, + platforms: [AppObservePlatform.Ios], + environment: undefined, }); jest.useRealTimers(); @@ -250,7 +251,20 @@ describe(ObserveEvents, () => { await command.runAsync(); const options = mockFetchObserveCustomEventsAsync.mock.calls[0][2]; - expect(options.platform).toBe(AppObservePlatform.Ios); + expect(options.platforms).toEqual([AppObservePlatform.Ios]); + }); + + it('passes --platform apple as every Apple platform', async () => { + const command = createCommand(['my_event', '--platform', 'apple']); + await command.runAsync(); + + const options = mockFetchObserveCustomEventsAsync.mock.calls[0][2]; + expect(options.platforms).toEqual([ + AppObservePlatform.Ios, + AppObservePlatform.Ipados, + AppObservePlatform.Tvos, + AppObservePlatform.Macos, + ]); }); it('passes --app-version', async () => { @@ -277,12 +291,12 @@ describe(ObserveEvents, () => { expect(options.sessionId).toBe('session-xyz'); }); - it('does not pass platform, appVersion, updateId, or sessionId when flags are not provided', async () => { + it('does not pass platforms, appVersion, updateId, or sessionId when flags are not provided', async () => { const command = createCommand(['my_event']); await command.runAsync(); const options = mockFetchObserveCustomEventsAsync.mock.calls[0][2]; - expect(options.platform).toBeUndefined(); + expect(options.platforms).toBeUndefined(); expect(options.appVersion).toBeUndefined(); expect(options.updateId).toBeUndefined(); expect(options.sessionId).toBeUndefined(); diff --git a/packages/eas-cli/src/commands/observe/__tests__/metrics-summary.test.ts b/packages/eas-cli/src/commands/observe/__tests__/metrics-summary.test.ts index 664d1cc469..200a6f87ed 100644 --- a/packages/eas-cli/src/commands/observe/__tests__/metrics-summary.test.ts +++ b/packages/eas-cli/src/commands/observe/__tests__/metrics-summary.test.ts @@ -3,11 +3,12 @@ import { GraphQLError } from 'graphql'; import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient'; import { getMockOclifConfig } from '../../../__tests__/commands/utils'; -import { AppPlatform } from '../../../graphql/generated'; +import { AppObservePlatform } from '../../../graphql/generated'; import { fetchObserveMetricsAsync, validateDateFlag } from '../../../observe/fetchMetrics'; import { EAS_OBSERVE_FEATURE_NOT_AVAILABLE_IN_FREE_TIER_ERROR_CODE } from '../../../observe/planGating'; import { buildObserveMetricsJson, buildObserveMetricsTable } from '../../../observe/formatMetrics'; import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json'; +import { ObservePlatformTarget } from '../../../observe/platforms'; import ObserveMetricsSummary from '../metrics-summary'; jest.mock('../../../observe/fetchMetrics', () => { @@ -31,6 +32,10 @@ const mockBuildObserveMetricsSummaryJson = jest.mocked(buildObserveMetricsJson); const mockEnableJsonOutput = jest.mocked(enableJsonOutput); const mockPrintJsonOnlyOutput = jest.mocked(printJsonOnlyOutput); +function target(platform: AppObservePlatform): ObservePlatformTarget { + return { key: platform, platforms: [platform] }; +} + describe(ObserveMetricsSummary, () => { const graphqlClient = {} as any as ExpoGraphqlClient; const mockConfig = getMockOclifConfig(); @@ -83,7 +88,7 @@ describe(ObserveMetricsSummary, () => { expect(mockFetchObserveMetricsSummaryAsync).toHaveBeenCalledTimes(1); const platforms = mockFetchObserveMetricsSummaryAsync.mock.calls[0][3]; - expect(platforms).toEqual([AppPlatform.Android, AppPlatform.Ios]); + expect(platforms).toEqual([target(AppObservePlatform.Android), target(AppObservePlatform.Ios)]); jest.useRealTimers(); }); @@ -93,7 +98,7 @@ describe(ObserveMetricsSummary, () => { await command.runAsync(); const platforms = mockFetchObserveMetricsSummaryAsync.mock.calls[0][3]; - expect(platforms).toEqual([AppPlatform.Android]); + expect(platforms).toEqual([target(AppObservePlatform.Android)]); }); it('queries only iOS when --platform ios is passed', async () => { @@ -101,7 +106,33 @@ describe(ObserveMetricsSummary, () => { await command.runAsync(); const platforms = mockFetchObserveMetricsSummaryAsync.mock.calls[0][3]; - expect(platforms).toEqual([AppPlatform.Ios]); + expect(platforms).toEqual([target(AppObservePlatform.Ios)]); + }); + + it('queries one combined target covering every Apple platform when --platform apple is passed', async () => { + const command = createCommand(['--platform', 'apple']); + await command.runAsync(); + + const platforms = mockFetchObserveMetricsSummaryAsync.mock.calls[0][3]; + expect(platforms).toEqual([ + { + key: 'APPLE', + platforms: [ + AppObservePlatform.Ios, + AppObservePlatform.Ipados, + AppObservePlatform.Tvos, + AppObservePlatform.Macos, + ], + }, + ]); + }); + + it('queries only macOS when --platform macos is passed', async () => { + const command = createCommand(['--platform', 'macos']); + await command.runAsync(); + + const platforms = mockFetchObserveMetricsSummaryAsync.mock.calls[0][3]; + expect(platforms).toEqual([target(AppObservePlatform.Macos)]); }); it('passes --environment through to fetchObserveMetricsAsync', async () => { diff --git a/packages/eas-cli/src/commands/observe/__tests__/metrics.test.ts b/packages/eas-cli/src/commands/observe/__tests__/metrics.test.ts index c2f051f216..0d40f83021 100644 --- a/packages/eas-cli/src/commands/observe/__tests__/metrics.test.ts +++ b/packages/eas-cli/src/commands/observe/__tests__/metrics.test.ts @@ -193,20 +193,20 @@ describe(ObserveMetrics, () => { await expect(command.runAsync()).rejects.toThrow(); }); - it('passes --platform ios to fetchObserveEventsAsync as AppObservePlatform.Ios', async () => { + it('passes --platform ios to fetchObserveEventsAsync', async () => { const command = createCommand(['tti', '--platform', 'ios']); await command.runAsync(); const options = mockFetchObserveEventsAsync.mock.calls[0][2]; - expect(options.platform).toBe(AppObservePlatform.Ios); + expect(options.platforms).toEqual([AppObservePlatform.Ios]); }); - it('passes --platform android to fetchObserveEventsAsync as AppObservePlatform.Android', async () => { + it('passes --platform android to fetchObserveEventsAsync', async () => { const command = createCommand(['tti', '--platform', 'android']); await command.runAsync(); const options = mockFetchObserveEventsAsync.mock.calls[0][2]; - expect(options.platform).toBe(AppObservePlatform.Android); + expect(options.platforms).toEqual([AppObservePlatform.Android]); }); it('passes --app-version to fetchObserveEventsAsync', async () => { @@ -225,12 +225,12 @@ describe(ObserveMetrics, () => { expect(options.updateId).toBe('update-xyz'); }); - it('does not pass platform, appVersion, or updateId when flags are not provided', async () => { + it('does not pass platforms, appVersion, or updateId when flags are not provided', async () => { const command = createCommand(['tti']); await command.runAsync(); const options = mockFetchObserveEventsAsync.mock.calls[0][2]; - expect(options.platform).toBeUndefined(); + expect(options.platforms).toBeUndefined(); expect(options.appVersion).toBeUndefined(); expect(options.updateId).toBeUndefined(); }); diff --git a/packages/eas-cli/src/commands/observe/__tests__/routes.test.ts b/packages/eas-cli/src/commands/observe/__tests__/routes.test.ts index 9759cd4c69..8265899aae 100644 --- a/packages/eas-cli/src/commands/observe/__tests__/routes.test.ts +++ b/packages/eas-cli/src/commands/observe/__tests__/routes.test.ts @@ -3,7 +3,7 @@ import { GraphQLError } from 'graphql'; import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient'; import { getMockOclifConfig } from '../../../__tests__/commands/utils'; -import { AppPlatform } from '../../../graphql/generated'; +import { AppObservePlatform } from '../../../graphql/generated'; import { fetchObserveNavigationRoutesAsync } from '../../../observe/fetchNavigationRoutes'; import { EAS_OBSERVE_FEATURE_NOT_AVAILABLE_IN_FREE_TIER_ERROR_CODE } from '../../../observe/planGating'; import { @@ -11,6 +11,7 @@ import { buildObserveNavigationRoutesTable, } from '../../../observe/formatNavigationRoutes'; import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json'; +import { ObservePlatformTarget } from '../../../observe/platforms'; import ObserveRoutes from '../routes'; jest.mock('../../../observe/fetchNavigationRoutes'); @@ -31,6 +32,10 @@ const mockBuildObserveNavigationRoutesJson = jest.mocked(buildObserveNavigationR const mockEnableJsonOutput = jest.mocked(enableJsonOutput); const mockPrintJsonOnlyOutput = jest.mocked(printJsonOnlyOutput); +function target(platform: AppObservePlatform): ObservePlatformTarget { + return { key: platform, platforms: [platform] }; +} + describe(ObserveRoutes, () => { const graphqlClient = {} as any as ExpoGraphqlClient; const mockConfig = getMockOclifConfig(); @@ -78,7 +83,10 @@ describe(ObserveRoutes, () => { expect(mockFetchObserveNavigationRoutesAsync).toHaveBeenCalledTimes(1); const options = mockFetchObserveNavigationRoutesAsync.mock.calls[0][2]; - expect(options.platforms).toEqual([AppPlatform.Android, AppPlatform.Ios]); + expect(options.targets).toEqual([ + target(AppObservePlatform.Android), + target(AppObservePlatform.Ios), + ]); expect(options.limit).toBe(50); const tableCall = mockBuildObserveNavigationRoutesTable.mock.calls[0]; @@ -95,7 +103,7 @@ describe(ObserveRoutes, () => { await command.runAsync(); const options = mockFetchObserveNavigationRoutesAsync.mock.calls[0][2]; - expect(options.platforms).toEqual([AppPlatform.Ios]); + expect(options.targets).toEqual([target(AppObservePlatform.Ios)]); }); it('resolves --metric short aliases to navigation metric full names and deduplicates', async () => { diff --git a/packages/eas-cli/src/commands/observe/__tests__/versions.test.ts b/packages/eas-cli/src/commands/observe/__tests__/versions.test.ts index a38aa3fe92..617867df3c 100644 --- a/packages/eas-cli/src/commands/observe/__tests__/versions.test.ts +++ b/packages/eas-cli/src/commands/observe/__tests__/versions.test.ts @@ -1,12 +1,13 @@ import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient'; import { getMockOclifConfig } from '../../../__tests__/commands/utils'; -import { AppPlatform } from '../../../graphql/generated'; +import { AppObservePlatform } from '../../../graphql/generated'; import { fetchObserveVersionsAsync } from '../../../observe/fetchVersions'; import { buildObserveVersionsJson, buildObserveVersionsTable, } from '../../../observe/formatVersions'; import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json'; +import { ObservePlatformTarget } from '../../../observe/platforms'; import ObserveVersions from '../versions'; jest.mock('../../../observe/fetchVersions'); @@ -23,6 +24,10 @@ const mockBuildObserveVersionsJson = jest.mocked(buildObserveVersionsJson); const mockEnableJsonOutput = jest.mocked(enableJsonOutput); const mockPrintJsonOnlyOutput = jest.mocked(printJsonOnlyOutput); +function target(platform: AppObservePlatform): ObservePlatformTarget { + return { key: platform, platforms: [platform] }; +} + describe(ObserveVersions, () => { const graphqlClient = {} as any as ExpoGraphqlClient; const mockConfig = getMockOclifConfig(); @@ -52,7 +57,7 @@ describe(ObserveVersions, () => { expect(mockFetchObserveVersionsAsync).toHaveBeenCalledTimes(1); const platforms = mockFetchObserveVersionsAsync.mock.calls[0][2]; - expect(platforms).toEqual([AppPlatform.Android, AppPlatform.Ios]); + expect(platforms).toEqual([target(AppObservePlatform.Android), target(AppObservePlatform.Ios)]); jest.useRealTimers(); }); @@ -62,7 +67,7 @@ describe(ObserveVersions, () => { await command.runAsync(); const platforms = mockFetchObserveVersionsAsync.mock.calls[0][2]; - expect(platforms).toEqual([AppPlatform.Android]); + expect(platforms).toEqual([target(AppObservePlatform.Android)]); }); it('queries only iOS when --platform ios is passed', async () => { @@ -70,7 +75,7 @@ describe(ObserveVersions, () => { await command.runAsync(); const platforms = mockFetchObserveVersionsAsync.mock.calls[0][2]; - expect(platforms).toEqual([AppPlatform.Ios]); + expect(platforms).toEqual([target(AppObservePlatform.Ios)]); }); it('passes --environment through to fetchObserveVersionsAsync', async () => { @@ -171,7 +176,7 @@ describe(ObserveVersions, () => { it('calls enableJsonOutput and printJsonOnlyOutput when --json is provided', async () => { mockFetchObserveVersionsAsync.mockResolvedValue([ { - platform: AppPlatform.Ios, + platform: AppObservePlatform.Ios, appVersions: [ { appVersion: '1.0.0', diff --git a/packages/eas-cli/src/commands/observe/events.ts b/packages/eas-cli/src/commands/observe/events.ts index 9dd01342ca..92bc050145 100644 --- a/packages/eas-cli/src/commands/observe/events.ts +++ b/packages/eas-cli/src/commands/observe/events.ts @@ -27,7 +27,7 @@ import { buildObserveCustomEventsTable, } from '../../observe/formatCustomEvents'; import { withObservePlanGateHandlingAsync } from '../../observe/planGating'; -import { appObservePlatformFromFlag } from '../../observe/platforms'; +import { observePlatformsFromFlag } from '../../observe/platforms'; import { resolveObserveCommandContextAsync } from '../../observe/resolveProjectContext'; import { resolveTimeRange } from '../../observe/startAndEndTime'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; @@ -102,7 +102,7 @@ export default class ObserveEvents extends EasCommand { const { daysBack, startTime, endTime } = resolveTimeRange(flags); - const platform = appObservePlatformFromFlag(flags.platform); + const platforms = observePlatformsFromFlag(flags.platform); // A session ID narrows to a single session, so show that session's events // (like --all-events) instead of the account-wide name+count summary, which @@ -113,7 +113,7 @@ export default class ObserveEvents extends EasCommand { appId: projectId, startTime, endTime, - platform, + platforms, environment: flags.environment, }) ); @@ -141,7 +141,7 @@ export default class ObserveEvents extends EasCommand { ...(flags.after && { after: flags.after }), startTime, endTime, - platform, + platforms, appVersion: flags['app-version'], updateId: flags['update-id'], sessionId: flags['session-id'], @@ -154,7 +154,7 @@ export default class ObserveEvents extends EasCommand { appId: projectId, startTime, endTime, - platform, + platforms, environment: flags.environment, }); diff --git a/packages/eas-cli/src/commands/observe/metrics-summary.ts b/packages/eas-cli/src/commands/observe/metrics-summary.ts index ec9700c381..c9aee210f0 100644 --- a/packages/eas-cli/src/commands/observe/metrics-summary.ts +++ b/packages/eas-cli/src/commands/observe/metrics-summary.ts @@ -21,7 +21,7 @@ import { } from '../../observe/formatMetrics'; import { METRIC_ALIASES, resolveMetricName } from '../../observe/metricNames'; import { withObservePlanGateHandlingAsync } from '../../observe/planGating'; -import { appPlatformsFromFlag } from '../../observe/platforms'; +import { observePlatformTargetsFromFlag } from '../../observe/platforms'; import { resolveObserveCommandContextAsync } from '../../observe/resolveProjectContext'; import { resolveTimeRange } from '../../observe/startAndEndTime'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; @@ -99,7 +99,7 @@ export default class ObserveMetricsSummary extends EasCommand { const { daysBack, startTime, endTime } = resolveTimeRange(flags); - const platforms = appPlatformsFromFlag(flags.platform); + const targets = observePlatformTargetsFromFlag(flags.platform); const { metricsMap, buildNumbersMap, updateIdsMap, totalEventCounts } = await withObservePlanGateHandlingAsync(() => @@ -107,7 +107,7 @@ export default class ObserveMetricsSummary extends EasCommand { graphqlClient, projectId, metricNames, - platforms, + targets, startTime, endTime, flags.environment diff --git a/packages/eas-cli/src/commands/observe/metrics.ts b/packages/eas-cli/src/commands/observe/metrics.ts index 036cbfb71b..c807f6d7a1 100644 --- a/packages/eas-cli/src/commands/observe/metrics.ts +++ b/packages/eas-cli/src/commands/observe/metrics.ts @@ -26,7 +26,7 @@ import { import { METRIC_ALIASES, METRIC_SHORT_NAMES, resolveMetricName } from '../../observe/metricNames'; import { withObservePlanGateHandlingAsync } from '../../observe/planGating'; import { buildObserveEventsJson, buildObserveEventsTable } from '../../observe/formatEvents'; -import { appObservePlatformFromFlag, appPlatformsFromFlag } from '../../observe/platforms'; +import { observePlatformTargetsFromFlag, observePlatformsFromFlag } from '../../observe/platforms'; import { resolveObserveCommandContextAsync } from '../../observe/resolveProjectContext'; import { resolveTimeRange } from '../../observe/startAndEndTime'; import { selectAsync } from '../../prompts'; @@ -110,8 +110,8 @@ export default class ObserveMetrics extends EasCommand { const { daysBack, startTime, endTime } = resolveTimeRange(flags); - const platform = appObservePlatformFromFlag(flags.platform); - const platforms = appPlatformsFromFlag(flags.platform); + const platforms = observePlatformsFromFlag(flags.platform); + const targets = observePlatformTargetsFromFlag(flags.platform); const [{ events, pageInfo }, totalEventCount] = await withObservePlanGateHandlingAsync(() => Promise.all([ @@ -122,7 +122,7 @@ export default class ObserveMetrics extends EasCommand { ...(flags.after && { after: flags.after }), startTime, endTime, - platform, + platforms, appVersion: flags['app-version'], updateId: flags['update-id'], environment: flags.environment, @@ -131,7 +131,7 @@ export default class ObserveMetrics extends EasCommand { graphqlClient, projectId, metricName, - platforms, + targets, startTime, endTime, flags.environment diff --git a/packages/eas-cli/src/commands/observe/routes.ts b/packages/eas-cli/src/commands/observe/routes.ts index 614616abd1..ee73e5e7e0 100644 --- a/packages/eas-cli/src/commands/observe/routes.ts +++ b/packages/eas-cli/src/commands/observe/routes.ts @@ -26,7 +26,7 @@ import { } from '../../observe/formatNavigationRoutes'; import { NAVIGATION_METRIC_ALIASES, resolveNavigationMetricName } from '../../observe/metricNames'; import { withObservePlanGateHandlingAsync } from '../../observe/planGating'; -import { appPlatformsFromFlag } from '../../observe/platforms'; +import { observePlatformTargetsFromFlag } from '../../observe/platforms'; import { resolveObserveCommandContextAsync } from '../../observe/resolveProjectContext'; import { resolveTimeRange } from '../../observe/startAndEndTime'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; @@ -114,13 +114,13 @@ export default class ObserveRoutes extends EasCommand { : undefined; const { daysBack, startTime, endTime } = resolveTimeRange(flags); - const platforms = appPlatformsFromFlag(flags.platform); + const targets = observePlatformTargetsFromFlag(flags.platform); const { routes, pageInfoByPlatform } = await withObservePlanGateHandlingAsync(() => fetchObserveNavigationRoutesAsync(graphqlClient, projectId, { startTime, endTime, - platforms, + targets, limit: flags.limit ?? DEFAULT_ROUTES_LIMIT, ...(flags.after && { after: flags.after }), appVersion: flags['app-version'], diff --git a/packages/eas-cli/src/commands/observe/versions.ts b/packages/eas-cli/src/commands/observe/versions.ts index e243d889a6..30501141d6 100644 --- a/packages/eas-cli/src/commands/observe/versions.ts +++ b/packages/eas-cli/src/commands/observe/versions.ts @@ -12,7 +12,7 @@ import { ObserveTimeRangeFlags, } from '../../observe/flags'; import { buildObserveVersionsJson, buildObserveVersionsTable } from '../../observe/formatVersions'; -import { appPlatformsFromFlag } from '../../observe/platforms'; +import { observePlatformTargetsFromFlag } from '../../observe/platforms'; import { resolveObserveCommandContextAsync } from '../../observe/resolveProjectContext'; import { resolveTimeRange } from '../../observe/startAndEndTime'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; @@ -55,12 +55,12 @@ export default class ObserveVersions extends EasCommand { const { startTime, endTime } = resolveTimeRange(flags); - const platforms = appPlatformsFromFlag(flags.platform); + const targets = observePlatformTargetsFromFlag(flags.platform); const results = await fetchObserveVersionsAsync( graphqlClient, projectId, - platforms, + targets, startTime, endTime, flags.environment diff --git a/packages/eas-cli/src/graphql/generated.ts b/packages/eas-cli/src/graphql/generated.ts index b25ac1e78e..dd109916ea 100644 --- a/packages/eas-cli/src/graphql/generated.ts +++ b/packages/eas-cli/src/graphql/generated.ts @@ -15467,7 +15467,7 @@ export type AppObserveCustomEventNamesQueryVariables = Exact<{ appId: Scalars['String']['input']; startTime: Scalars['DateTime']['input']; endTime: Scalars['DateTime']['input']; - platform?: InputMaybe; + platforms?: InputMaybe | AppObservePlatform>; environment?: InputMaybe; }>; diff --git a/packages/eas-cli/src/graphql/queries/ObserveQuery.ts b/packages/eas-cli/src/graphql/queries/ObserveQuery.ts index a081b13861..64d1f39c1e 100644 --- a/packages/eas-cli/src/graphql/queries/ObserveQuery.ts +++ b/packages/eas-cli/src/graphql/queries/ObserveQuery.ts @@ -134,7 +134,7 @@ type AppObserveCustomEventNamesQueryVariables = { appId: string; startTime: string; endTime: string; - platform?: AppObservePlatform; + platforms?: AppObservePlatform[]; environment?: string; }; @@ -217,14 +217,14 @@ export const ObserveQuery = { graphqlClient: ExpoGraphqlClient, { appId, - platform, + platforms, startTime, endTime, metricNames, environment, }: { appId: string; - platform: AppObservePlatform; + platforms: AppObservePlatform[]; startTime: string; endTime: string; metricNames?: string[]; @@ -255,7 +255,7 @@ export const ObserveQuery = { { appId, input: { - platform, + platforms, startTime, endTime, ...(metricNames && { metricNames }), @@ -387,13 +387,13 @@ export const ObserveQuery = { appId, startTime, endTime, - platform, + platforms, environment, }: { appId: string; startTime: string; endTime: string; - platform?: AppObservePlatform; + platforms?: AppObservePlatform[]; environment?: string; } ): Promise<{ names: AppObserveCustomEventName[]; isTruncated: boolean }> { @@ -405,7 +405,7 @@ export const ObserveQuery = { $appId: String! $startTime: DateTime! $endTime: DateTime! - $platform: AppObservePlatform + $platforms: [AppObservePlatform!] $environment: String ) { app { @@ -415,7 +415,7 @@ export const ObserveQuery = { customEventNames( startTime: $startTime endTime: $endTime - platform: $platform + platforms: $platforms environment: $environment ) { isTruncated @@ -433,7 +433,7 @@ export const ObserveQuery = { appId, startTime, endTime, - ...(platform && { platform }), + ...(platforms?.length && { platforms }), ...(environment && { environment }), } ) diff --git a/packages/eas-cli/src/observe/__tests__/fetchCustomEvents.test.ts b/packages/eas-cli/src/observe/__tests__/fetchCustomEvents.test.ts index 8f68007ed5..0a40dd4ce1 100644 --- a/packages/eas-cli/src/observe/__tests__/fetchCustomEvents.test.ts +++ b/packages/eas-cli/src/observe/__tests__/fetchCustomEvents.test.ts @@ -63,7 +63,7 @@ describe('fetchObserveCustomEventsAsync', () => { it('forwards platform, appVersion, and sessionId filters when provided', async () => { await fetchObserveCustomEventsAsync(mockGraphqlClient, 'project-123', { limit: 10, - platform: AppObservePlatform.Ios, + platforms: [AppObservePlatform.Ios], appVersion: '2.1.0', sessionId: 'session-xyz', startTime: '2025-01-01T00:00:00.000Z', @@ -71,7 +71,7 @@ describe('fetchObserveCustomEventsAsync', () => { }); const filter = mockCustomEventListAsync.mock.calls[0][1].filter; - expect(filter?.platform).toBe(AppObservePlatform.Ios); + expect(filter?.platforms).toEqual([AppObservePlatform.Ios]); expect(filter?.appVersion).toBe('2.1.0'); expect(filter?.sessionId).toBe('session-xyz'); }); diff --git a/packages/eas-cli/src/observe/__tests__/fetchEvents.test.ts b/packages/eas-cli/src/observe/__tests__/fetchEvents.test.ts index abdad4a7e8..5afb6c3654 100644 --- a/packages/eas-cli/src/observe/__tests__/fetchEvents.test.ts +++ b/packages/eas-cli/src/observe/__tests__/fetchEvents.test.ts @@ -5,7 +5,6 @@ import { AppObserveEventsOrderByDirection, AppObserveEventsOrderByField, AppObservePlatform, - AppPlatform, } from '../../graphql/generated'; import { ObserveQuery } from '../../graphql/queries/ObserveQuery'; import { @@ -15,9 +14,14 @@ import { resolveOrderBy, } from '../fetchEvents'; import { EAS_OBSERVE_FEATURE_NOT_AVAILABLE_IN_FREE_TIER_ERROR_CODE } from '../planGating'; +import { ObservePlatformTarget } from '../platforms'; jest.mock('../../graphql/queries/ObserveQuery'); +function target(platform: AppObservePlatform): ObservePlatformTarget { + return { key: platform, platforms: [platform] }; +} + describe(resolveOrderBy, () => { it('maps "slowest" to METRIC_VALUE DESC', () => { expect(resolveOrderBy(EventsOrderPreset.Slowest)).toEqual({ @@ -104,14 +108,14 @@ describe(fetchObserveEventsAsync, () => { limit: 5, startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', - platform: AppObservePlatform.Ios, + platforms: [AppObservePlatform.Ios], }); expect(mockEventsAsync).toHaveBeenCalledWith( mockGraphqlClient, expect.objectContaining({ filter: expect.objectContaining({ - platform: AppObservePlatform.Ios, + platforms: [AppObservePlatform.Ios], }), }) ); @@ -316,7 +320,7 @@ describe(fetchTotalEventCountAsync, () => { mockGraphqlClient, 'project-123', 'expo.navigation.tti', - [AppPlatform.Ios, AppPlatform.Android], + [target(AppObservePlatform.Ios), target(AppObservePlatform.Android)], '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z' ) @@ -330,7 +334,7 @@ describe(fetchTotalEventCountAsync, () => { mockGraphqlClient, 'project-123', 'expo.app_startup.tti', - [AppPlatform.Ios], + [target(AppObservePlatform.Ios)], '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z' ); diff --git a/packages/eas-cli/src/observe/__tests__/fetchMetrics.test.ts b/packages/eas-cli/src/observe/__tests__/fetchMetrics.test.ts index 848e3f5790..cfab746412 100644 --- a/packages/eas-cli/src/observe/__tests__/fetchMetrics.test.ts +++ b/packages/eas-cli/src/observe/__tests__/fetchMetrics.test.ts @@ -1,15 +1,20 @@ import { CombinedError } from '@urql/core'; import { GraphQLError } from 'graphql'; -import { AppObserveAppVersion, AppObservePlatform, AppPlatform } from '../../graphql/generated'; +import { AppObserveAppVersion, AppObservePlatform } from '../../graphql/generated'; import { ObserveQuery } from '../../graphql/queries/ObserveQuery'; import { makeMetricsKey } from '../formatMetrics'; import { fetchObserveMetricsAsync } from '../fetchMetrics'; import { EAS_OBSERVE_FEATURE_NOT_AVAILABLE_IN_FREE_TIER_ERROR_CODE } from '../planGating'; +import { ObservePlatformTarget } from '../platforms'; jest.mock('../../graphql/queries/ObserveQuery'); jest.mock('../../log'); +function target(platform: AppObservePlatform): ObservePlatformTarget { + return { key: platform, platforms: [platform] }; +} + function makeAppVersion( appVersion: string, metrics: Array<{ @@ -90,7 +95,7 @@ describe('fetchObserveMetricsAsync', () => { mockGraphqlClient, 'project-123', ['expo.app_startup.tti', 'expo.app_startup.cold_launch_time'], - [AppPlatform.Ios], + [target(AppObservePlatform.Ios)], '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z' ); @@ -98,13 +103,13 @@ describe('fetchObserveMetricsAsync', () => { expect(mockAppVersionsAsync).toHaveBeenCalledTimes(1); expect(mockAppVersionsAsync).toHaveBeenCalledWith(mockGraphqlClient, { appId: 'project-123', - platform: AppObservePlatform.Ios, + platforms: [AppObservePlatform.Ios], startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', metricNames: ['expo.app_startup.tti', 'expo.app_startup.cold_launch_time'], }); - const key = makeMetricsKey('1.0.0', AppPlatform.Ios); + const key = makeMetricsKey('1.0.0', AppObservePlatform.Ios); expect(metricsMap.has(key)).toBe(true); const metricsForVersion = metricsMap.get(key)!; @@ -137,21 +142,21 @@ describe('fetchObserveMetricsAsync', () => { mockGraphqlClient, 'project-123', ['expo.app_startup.tti'], - [AppPlatform.Ios, AppPlatform.Android], + [target(AppObservePlatform.Ios), target(AppObservePlatform.Android)], '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z' ); expect(mockAppVersionsAsync).toHaveBeenCalledTimes(2); - const platforms = mockAppVersionsAsync.mock.calls.map(call => call[1].platform); + const platforms = mockAppVersionsAsync.mock.calls.flatMap(call => call[1].platforms); expect(platforms).toContain(AppObservePlatform.Ios); expect(platforms).toContain(AppObservePlatform.Android); }); it('handles partial failures gracefully', async () => { - mockAppVersionsAsync.mockImplementation(async (_client, { platform }) => { - if (platform === AppObservePlatform.Android) { + mockAppVersionsAsync.mockImplementation(async (_client, { platforms }) => { + if (platforms.includes(AppObservePlatform.Android)) { throw new Error('Network error'); } return [ @@ -177,12 +182,12 @@ describe('fetchObserveMetricsAsync', () => { mockGraphqlClient, 'project-123', ['expo.app_startup.tti'], - [AppPlatform.Ios, AppPlatform.Android], + [target(AppObservePlatform.Ios), target(AppObservePlatform.Android)], '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z' ); - const key = makeMetricsKey('2.0.0', AppPlatform.Ios); + const key = makeMetricsKey('2.0.0', AppObservePlatform.Ios); expect(metricsMap.has(key)).toBe(true); expect(metricsMap.get(key)!.get('expo.app_startup.tti')).toEqual({ min: 0.1, @@ -203,7 +208,7 @@ describe('fetchObserveMetricsAsync', () => { mockGraphqlClient, 'project-123', ['expo.app_startup.tti'], - [AppPlatform.Ios], + [target(AppObservePlatform.Ios)], '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z' ); @@ -234,14 +239,14 @@ describe('fetchObserveMetricsAsync', () => { mockGraphqlClient, 'project-123', ['expo.navigation.tti'], - [AppPlatform.Ios, AppPlatform.Android], + [target(AppObservePlatform.Ios), target(AppObservePlatform.Android)], '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z' ) ).rejects.toBe(gateError); }); - it('maps AppObservePlatform back to AppPlatform correctly in metricsMap keys', async () => { + it('uses the target key in metricsMap keys', async () => { mockAppVersionsAsync.mockResolvedValue([ makeAppVersion('3.0.0', [ { @@ -264,7 +269,7 @@ describe('fetchObserveMetricsAsync', () => { mockGraphqlClient, 'project-123', ['expo.app_startup.tti'], - [AppPlatform.Android], + [target(AppObservePlatform.Android)], '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z' ); @@ -295,7 +300,7 @@ describe('fetchObserveMetricsAsync', () => { mockGraphqlClient, 'project-123', ['expo.app_startup.tti'], - [AppPlatform.Ios], + [target(AppObservePlatform.Ios)], '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z' ); diff --git a/packages/eas-cli/src/observe/__tests__/fetchNavigationRoutes.test.ts b/packages/eas-cli/src/observe/__tests__/fetchNavigationRoutes.test.ts index ffba4a7347..0b39f7ce1e 100644 --- a/packages/eas-cli/src/observe/__tests__/fetchNavigationRoutes.test.ts +++ b/packages/eas-cli/src/observe/__tests__/fetchNavigationRoutes.test.ts @@ -6,15 +6,19 @@ import { AppObserveNavigationRoute, AppObserveNavigationRoutesOrderByField, AppObservePlatform, - AppPlatform, } from '../../graphql/generated'; import { ObserveQuery } from '../../graphql/queries/ObserveQuery'; import { fetchObserveNavigationRoutesAsync } from '../fetchNavigationRoutes'; import { EAS_OBSERVE_FEATURE_NOT_AVAILABLE_IN_FREE_TIER_ERROR_CODE } from '../planGating'; +import { ObservePlatformTarget } from '../platforms'; jest.mock('../../graphql/queries/ObserveQuery'); jest.mock('../../log'); +function target(platform: AppObservePlatform): ObservePlatformTarget { + return { key: platform, platforms: [platform] }; +} + function makeRoute(routeName: string): AppObserveNavigationRoute { return { __typename: 'AppObserveNavigationRoute' as const, @@ -41,7 +45,7 @@ describe('fetchObserveNavigationRoutesAsync', () => { await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', { startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', - platforms: [AppPlatform.Ios], + targets: [target(AppObservePlatform.Ios)], limit: 25, appVersion: '2.1.0', updateId: 'update-xyz', @@ -53,7 +57,7 @@ describe('fetchObserveNavigationRoutesAsync', () => { expect(call.appId).toBe('project-123'); expect(call.first).toBe(25); expect(call.filter).toEqual({ - platform: AppObservePlatform.Ios, + platforms: [AppObservePlatform.Ios], startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', appVersion: '2.1.0', @@ -66,7 +70,7 @@ describe('fetchObserveNavigationRoutesAsync', () => { await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', { startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', - platforms: [AppPlatform.Ios], + targets: [target(AppObservePlatform.Ios)], limit: 50, }); @@ -81,7 +85,7 @@ describe('fetchObserveNavigationRoutesAsync', () => { await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', { startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', - platforms: [AppPlatform.Ios], + targets: [target(AppObservePlatform.Ios)], limit: 50, }); @@ -96,7 +100,7 @@ describe('fetchObserveNavigationRoutesAsync', () => { await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', { startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', - platforms: [AppPlatform.Ios], + targets: [target(AppObservePlatform.Ios)], limit: 50, routeNames: ['/home', '/profile'], }); @@ -111,7 +115,7 @@ describe('fetchObserveNavigationRoutesAsync', () => { await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', { startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', - platforms: [AppPlatform.Ios], + targets: [target(AppObservePlatform.Ios)], limit: 50, routeNames: [], }); @@ -123,7 +127,7 @@ describe('fetchObserveNavigationRoutesAsync', () => { await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', { startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', - platforms: [AppPlatform.Ios], + targets: [target(AppObservePlatform.Ios)], limit: 50, after: 'cursor-abc', }); @@ -133,7 +137,7 @@ describe('fetchObserveNavigationRoutesAsync', () => { it('fans out across multiple platforms and tags each route with its platform', async () => { mockNavigationRoutesAsync.mockImplementation(async (_client, vars) => { - if (vars.filter.platform === AppObservePlatform.Android) { + if (vars.filter.platforms?.includes(AppObservePlatform.Android)) { return { routes: [makeRoute('/android-home')], pageInfo: { hasNextPage: false, hasPreviousPage: false }, @@ -151,7 +155,7 @@ describe('fetchObserveNavigationRoutesAsync', () => { { startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', - platforms: [AppPlatform.Ios, AppPlatform.Android], + targets: [target(AppObservePlatform.Ios), target(AppObservePlatform.Android)], limit: 50, } ); @@ -159,15 +163,15 @@ describe('fetchObserveNavigationRoutesAsync', () => { expect(mockNavigationRoutesAsync).toHaveBeenCalledTimes(2); expect(routes).toHaveLength(2); const byName = new Map(routes.map(r => [r.route.routeName, r.platform])); - expect(byName.get('/android-home')).toBe(AppPlatform.Android); - expect(byName.get('/ios-home')).toBe(AppPlatform.Ios); - expect(pageInfoByPlatform.get(AppPlatform.Ios)?.hasNextPage).toBe(true); - expect(pageInfoByPlatform.get(AppPlatform.Android)?.hasNextPage).toBe(false); + expect(byName.get('/android-home')).toBe(AppObservePlatform.Android); + expect(byName.get('/ios-home')).toBe(AppObservePlatform.Ios); + expect(pageInfoByPlatform.get(AppObservePlatform.Ios)?.hasNextPage).toBe(true); + expect(pageInfoByPlatform.get(AppObservePlatform.Android)?.hasNextPage).toBe(false); }); it('handles partial failures gracefully', async () => { mockNavigationRoutesAsync.mockImplementation(async (_client, vars) => { - if (vars.filter.platform === AppObservePlatform.Android) { + if (vars.filter.platforms?.includes(AppObservePlatform.Android)) { throw new Error('Network error'); } return { @@ -182,14 +186,14 @@ describe('fetchObserveNavigationRoutesAsync', () => { { startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', - platforms: [AppPlatform.Ios, AppPlatform.Android], + targets: [target(AppObservePlatform.Ios), target(AppObservePlatform.Android)], limit: 50, } ); expect(routes).toHaveLength(1); - expect(routes[0].platform).toBe(AppPlatform.Ios); - expect(pageInfoByPlatform.has(AppPlatform.Android)).toBe(false); + expect(routes[0].platform).toBe(AppObservePlatform.Ios); + expect(pageInfoByPlatform.has(AppObservePlatform.Android)).toBe(false); }); it('rethrows plan-gate errors instead of swallowing them as a partial failure', async () => { @@ -214,7 +218,7 @@ describe('fetchObserveNavigationRoutesAsync', () => { fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', { startTime: '2025-01-01T00:00:00.000Z', endTime: '2025-03-01T00:00:00.000Z', - platforms: [AppPlatform.Ios, AppPlatform.Android], + targets: [target(AppObservePlatform.Ios), target(AppObservePlatform.Android)], limit: 50, }) ).rejects.toBe(gateError); diff --git a/packages/eas-cli/src/observe/__tests__/formatMetrics.test.ts b/packages/eas-cli/src/observe/__tests__/formatMetrics.test.ts index 7c2ca70332..5d4022c2d6 100644 --- a/packages/eas-cli/src/observe/__tests__/formatMetrics.test.ts +++ b/packages/eas-cli/src/observe/__tests__/formatMetrics.test.ts @@ -1,4 +1,4 @@ -import { AppPlatform } from '../../graphql/generated'; +import { AppObservePlatform } from '../../graphql/generated'; import { type MetricValues, ObserveMetricsMap, @@ -40,7 +40,7 @@ function makeMetricValueWithDefaults(overrides: Partial): MetricVa describe(buildObserveMetricsTable, () => { it('formats metrics grouped by version with metric columns', () => { const metricsMap: ObserveMetricsMap = new Map(); - const iosKey = makeMetricsKey('1.2.0', AppPlatform.Ios); + const iosKey = makeMetricsKey('1.2.0', AppObservePlatform.Ios); metricsMap.set( iosKey, new Map([ @@ -52,7 +52,7 @@ describe(buildObserveMetricsTable, () => { ]) ); - const androidKey = makeMetricsKey('1.1.0', AppPlatform.Android); + const androidKey = makeMetricsKey('1.1.0', AppObservePlatform.Android); metricsMap.set( androidKey, new Map([ @@ -84,7 +84,7 @@ describe(buildObserveMetricsTable, () => { it('shows - for versions with no matching metric data', () => { const metricsMap: ObserveMetricsMap = new Map(); - const key = makeMetricsKey('2.0.0', AppPlatform.Ios); + const key = makeMetricsKey('2.0.0', AppObservePlatform.Ios); metricsMap.set(key, new Map()); const output = buildObserveMetricsTable(metricsMap, DEFAULT_METRICS, DEFAULT_STATS_TABLE); @@ -108,7 +108,7 @@ describe(buildObserveMetricsTable, () => { describe(buildObserveMetricsJson, () => { it('produces JSON with all stats per metric', () => { const metricsMap: ObserveMetricsMap = new Map(); - const key = makeMetricsKey('1.0.0', AppPlatform.Ios); + const key = makeMetricsKey('1.0.0', AppObservePlatform.Ios); metricsMap.set( key, new Map([ @@ -125,7 +125,7 @@ describe(buildObserveMetricsJson, () => { expect(result.versions).toHaveLength(1); expect(result.versions[0]).toEqual({ appVersion: '1.0.0', - platform: AppPlatform.Ios, + platform: AppObservePlatform.Ios, buildNumbers: [], updateIds: [], metrics: { @@ -145,7 +145,7 @@ describe(buildObserveMetricsJson, () => { it('produces null values when no observe data matches for a metric', () => { const metricsMap: ObserveMetricsMap = new Map(); - const key = makeMetricsKey('3.0.0', AppPlatform.Android); + const key = makeMetricsKey('3.0.0', AppObservePlatform.Android); metricsMap.set(key, new Map()); const result = buildObserveMetricsJson( @@ -171,8 +171,8 @@ describe(buildObserveMetricsJson, () => { describe(makeMetricsKey, () => { it('creates a key from version and platform', () => { - expect(makeMetricsKey('1.0.0', AppPlatform.Ios)).toBe('1.0.0:IOS'); - expect(makeMetricsKey('2.0.0', AppPlatform.Android)).toBe('2.0.0:ANDROID'); + expect(makeMetricsKey('1.0.0', AppObservePlatform.Ios)).toBe('1.0.0:IOS'); + expect(makeMetricsKey('2.0.0', AppObservePlatform.Android)).toBe('2.0.0:ANDROID'); }); }); @@ -224,7 +224,7 @@ describe('DEFAULT_STATS_JSON', () => { describe('custom stats parameter', () => { it('table renders only selected stats', () => { const metricsMap: ObserveMetricsMap = new Map(); - const key = makeMetricsKey('1.0.0', AppPlatform.Ios); + const key = makeMetricsKey('1.0.0', AppObservePlatform.Ios); metricsMap.set( key, new Map([ @@ -259,7 +259,7 @@ describe('custom stats parameter', () => { it("table formats eventCount as integer without 's' suffix", () => { const metricsMap: ObserveMetricsMap = new Map(); - const key = makeMetricsKey('1.0.0', AppPlatform.Ios); + const key = makeMetricsKey('1.0.0', AppObservePlatform.Ios); metricsMap.set( key, new Map([ @@ -288,7 +288,7 @@ describe('custom stats parameter', () => { it('JSON includes only selected stats', () => { const metricsMap: ObserveMetricsMap = new Map(); - const key = makeMetricsKey('1.0.0', AppPlatform.Ios); + const key = makeMetricsKey('1.0.0', AppObservePlatform.Ios); metricsMap.set( key, new Map([ @@ -322,7 +322,7 @@ describe('custom stats parameter', () => { it('JSON uses default stats when not specified', () => { const metricsMap: ObserveMetricsMap = new Map(); - const key = makeMetricsKey('1.0.0', AppPlatform.Ios); + const key = makeMetricsKey('1.0.0', AppObservePlatform.Ios); metricsMap.set( key, new Map([ diff --git a/packages/eas-cli/src/observe/__tests__/formatNavigationRoutes.test.ts b/packages/eas-cli/src/observe/__tests__/formatNavigationRoutes.test.ts index fdea0b8c00..ba2db43ba0 100644 --- a/packages/eas-cli/src/observe/__tests__/formatNavigationRoutes.test.ts +++ b/packages/eas-cli/src/observe/__tests__/formatNavigationRoutes.test.ts @@ -1,4 +1,4 @@ -import { AppPlatform } from '../../graphql/generated'; +import { AppObservePlatform } from '../../graphql/generated'; import { NavigationRouteWithPlatform } from '../fetchNavigationRoutes'; import { buildObserveNavigationRoutesJson, @@ -9,7 +9,7 @@ import { function makeRoute( routeName: string, - platform: AppPlatform, + platform: AppObservePlatform, overrides?: Partial<{ coldTtr: { count: number; median: number | null; p90: number | null }; warmTtr: { count: number; median: number | null; p90: number | null }; @@ -87,11 +87,11 @@ describe(buildObserveNavigationRoutesTable, () => { it('formats routes grouped by platform with merged (med + count) cells', () => { const routes = [ - makeRoute('/home', AppPlatform.Ios), - makeRoute('/profile', AppPlatform.Ios, { + makeRoute('/home', AppObservePlatform.Ios), + makeRoute('/profile', AppObservePlatform.Ios, { coldTtr: { count: 5, median: 0.7, p90: 1.0 }, }), - makeRoute('/home', AppPlatform.Android), + makeRoute('/home', AppObservePlatform.Android), ]; const output = buildObserveNavigationRoutesTable( @@ -114,7 +114,7 @@ describe(buildObserveNavigationRoutesTable, () => { }); it('renders separate columns when count is omitted from stats', () => { - const routes = [makeRoute('/home', AppPlatform.Ios)]; + const routes = [makeRoute('/home', AppObservePlatform.Ios)]; const output = buildObserveNavigationRoutesTable( routes, ['expo.navigation.cold_ttr'], @@ -129,7 +129,7 @@ describe(buildObserveNavigationRoutesTable, () => { }); it('renders count-only column when only count stat is requested', () => { - const routes = [makeRoute('/home', AppPlatform.Ios)]; + const routes = [makeRoute('/home', AppObservePlatform.Ios)]; const output = buildObserveNavigationRoutesTable( routes, ['expo.navigation.cold_ttr'], @@ -142,9 +142,9 @@ describe(buildObserveNavigationRoutesTable, () => { }); it('shows a next-page hint per platform when hasNextPage is true', () => { - const routes = [makeRoute('/home', AppPlatform.Ios)]; + const routes = [makeRoute('/home', AppObservePlatform.Ios)]; const pageInfoByPlatform = new Map([ - [AppPlatform.Ios, { hasNextPage: true, endCursor: 'cursor-ios' }], + [AppObservePlatform.Ios, { hasNextPage: true, endCursor: 'cursor-ios' }], ]); const output = buildObserveNavigationRoutesTable( @@ -160,9 +160,9 @@ describe(buildObserveNavigationRoutesTable, () => { describe(buildObserveNavigationRoutesJson, () => { it('maps routes to the requested metrics and stats, including pageInfoByPlatform', () => { - const routes = [makeRoute('/home', AppPlatform.Ios)]; + const routes = [makeRoute('/home', AppObservePlatform.Ios)]; const pageInfoByPlatform = new Map([ - [AppPlatform.Ios, { hasNextPage: true, endCursor: 'cursor-ios' }], + [AppObservePlatform.Ios, { hasNextPage: true, endCursor: 'cursor-ios' }], ]); const result = buildObserveNavigationRoutesJson( @@ -175,7 +175,7 @@ describe(buildObserveNavigationRoutesJson, () => { expect(result.routes).toEqual([ { routeName: '/home', - platform: AppPlatform.Ios, + platform: AppObservePlatform.Ios, metrics: { 'expo.navigation.cold_ttr': { median: 0.5, p90: 0.9, count: 10 }, 'expo.navigation.tti': { median: 0.45, p90: 0.7, count: 12 }, @@ -183,13 +183,13 @@ describe(buildObserveNavigationRoutesJson, () => { }, ]); expect(result.pageInfoByPlatform).toEqual({ - [AppPlatform.Ios]: { hasNextPage: true, endCursor: 'cursor-ios' }, + [AppObservePlatform.Ios]: { hasNextPage: true, endCursor: 'cursor-ios' }, }); }); it('returns null for stats when the underlying value is null', () => { const routes = [ - makeRoute('/home', AppPlatform.Ios, { + makeRoute('/home', AppObservePlatform.Ios, { coldTtr: { count: 0, median: null, p90: null }, }), ]; diff --git a/packages/eas-cli/src/observe/fetchCustomEvents.ts b/packages/eas-cli/src/observe/fetchCustomEvents.ts index b46d6b4ddb..5c8fb9e99d 100644 --- a/packages/eas-cli/src/observe/fetchCustomEvents.ts +++ b/packages/eas-cli/src/observe/fetchCustomEvents.ts @@ -14,7 +14,7 @@ interface FetchCustomEventsOptions { after?: string; startTime?: string; endTime?: string; - platform?: AppObservePlatform; + platforms?: AppObservePlatform[]; appVersion?: string; updateId?: string; sessionId?: string; @@ -36,7 +36,7 @@ export async function fetchObserveCustomEventsAsync( ...(options.startTime && { startTime: options.startTime }), ...(options.endTime && { endTime: options.endTime }), ...(options.eventName && { eventName: options.eventName }), - ...(options.platform && { platform: options.platform }), + ...(options.platforms?.length && { platforms: options.platforms }), ...(options.appVersion && { appVersion: options.appVersion }), ...(options.updateId && { appUpdateId: options.updateId }), ...(options.sessionId && { sessionId: options.sessionId }), diff --git a/packages/eas-cli/src/observe/fetchEvents.ts b/packages/eas-cli/src/observe/fetchEvents.ts index 9da29f9628..ce6bffe38c 100644 --- a/packages/eas-cli/src/observe/fetchEvents.ts +++ b/packages/eas-cli/src/observe/fetchEvents.ts @@ -6,11 +6,11 @@ import { AppObserveEventsOrderByDirection, AppObserveEventsOrderByField, AppObservePlatform, - AppPlatform, PageInfo, } from '../graphql/generated'; import { ObserveQuery } from '../graphql/queries/ObserveQuery'; import { isObservePlanGateError } from './planGating'; +import { ObservePlatformTarget } from './platforms'; export enum EventsOrderPreset { Slowest = 'SLOWEST', @@ -52,7 +52,7 @@ interface FetchObserveEventsOptions { after?: string; startTime?: string; endTime?: string; - platform?: AppObservePlatform; + platforms?: AppObservePlatform[]; appVersion?: string; updateId?: string; sessionId?: string; @@ -73,7 +73,7 @@ export async function fetchObserveEventsAsync( ...(options.startTime && { startTime: options.startTime }), ...(options.endTime && { endTime: options.endTime }), ...(options.metricName && { metricName: options.metricName }), - ...(options.platform && { platform: options.platform }), + ...(options.platforms?.length && { platforms: options.platforms }), ...(options.appVersion && { appVersion: options.appVersion }), ...(options.updateId && { appUpdateId: options.updateId }), ...(options.sessionId && { sessionId: options.sessionId }), @@ -89,25 +89,20 @@ export async function fetchObserveEventsAsync( }); } -const appPlatformToObservePlatform: Record = { - [AppPlatform.Android]: AppObservePlatform.Android, - [AppPlatform.Ios]: AppObservePlatform.Ios, -}; - export async function fetchTotalEventCountAsync( graphqlClient: ExpoGraphqlClient, appId: string, metricName: string, - platforms: AppPlatform[], + targets: ObservePlatformTarget[], startTime: string, endTime: string, environment?: string ): Promise { - const queries = platforms.map(async appPlatform => { + const queries = targets.map(async target => { try { const versions = await ObserveQuery.appVersionsAsync(graphqlClient, { appId, - platform: appPlatformToObservePlatform[appPlatform], + platforms: target.platforms, startTime, endTime, metricNames: [metricName], diff --git a/packages/eas-cli/src/observe/fetchMetrics.ts b/packages/eas-cli/src/observe/fetchMetrics.ts index 8a5870502b..09ad133166 100644 --- a/packages/eas-cli/src/observe/fetchMetrics.ts +++ b/packages/eas-cli/src/observe/fetchMetrics.ts @@ -1,6 +1,5 @@ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient'; import { EasCommandError } from '../commandUtils/errors'; -import { AppPlatform } from '../graphql/generated'; import { ObserveQuery } from '../graphql/queries/ObserveQuery'; import Log from '../log'; import { @@ -11,7 +10,7 @@ import { makeMetricsKey, } from './formatMetrics'; import { isObservePlanGateError } from './planGating'; -import { appPlatformToObservePlatform } from './platforms'; +import { ObservePlatformTarget, observePlatformDisplayNames } from './platforms'; export function validateDateFlag(value: string, flagName: string): void { const parsed = new Date(value); @@ -33,30 +32,33 @@ export async function fetchObserveMetricsAsync( graphqlClient: ExpoGraphqlClient, appId: string, metricNames: string[], - platforms: AppPlatform[], + targets: ObservePlatformTarget[], startTime: string, endTime: string, environment?: string ): Promise { - const queries = platforms.map(async appPlatform => { - const observePlatform = appPlatformToObservePlatform[appPlatform]; + const queries = targets.map(async target => { try { const appVersions = await ObserveQuery.appVersionsAsync(graphqlClient, { appId, - platform: observePlatform, + platforms: target.platforms, startTime, endTime, metricNames, environment, }); - return { appPlatform, appVersions }; + return { target, appVersions }; } catch (error: any) { // A plan gate is an account-wide rejection, not a per-platform failure — // let it propagate so the command surfaces the upgrade prompt. if (isObservePlanGateError(error)) { throw error; } - Log.warn(`Failed to fetch observe data on ${observePlatform}: ${error.message}`); + Log.warn( + `Failed to fetch observe data on ${observePlatformDisplayNames[target.key]}: ${ + error.message + }` + ); return null; } }); @@ -72,10 +74,10 @@ export async function fetchObserveMetricsAsync( if (!result) { continue; } - const { appPlatform, appVersions } = result; + const { target, appVersions } = result; for (const version of appVersions) { - const key = makeMetricsKey(version.appVersion, appPlatform); + const key = makeMetricsKey(version.appVersion, target.key); if (!metricsMap.has(key)) { metricsMap.set(key, new Map()); } @@ -105,7 +107,7 @@ export async function fetchObserveMetricsAsync( }; metricsMap.get(key)!.set(metric.metricName, values); - const eventCountKey = `${metric.metricName}:${appPlatform}`; + const eventCountKey = `${metric.metricName}:${target.key}`; totalEventCounts.set( eventCountKey, (totalEventCounts.get(eventCountKey) ?? 0) + metric.eventCount diff --git a/packages/eas-cli/src/observe/fetchNavigationRoutes.ts b/packages/eas-cli/src/observe/fetchNavigationRoutes.ts index d1e5cfe03d..d36012a184 100644 --- a/packages/eas-cli/src/observe/fetchNavigationRoutes.ts +++ b/packages/eas-cli/src/observe/fetchNavigationRoutes.ts @@ -4,23 +4,26 @@ import { AppObserveNavigationRoute, AppObserveNavigationRoutesOrderBy, AppObserveNavigationRoutesOrderByField, - AppPlatform, PageInfo, } from '../graphql/generated'; import { ObserveQuery } from '../graphql/queries/ObserveQuery'; import Log from '../log'; import { isObservePlanGateError } from './planGating'; -import { appPlatformToObservePlatform } from './platforms'; +import { + ObservePlatformKey, + ObservePlatformTarget, + observePlatformDisplayNames, +} from './platforms'; export interface NavigationRouteWithPlatform { - platform: AppPlatform; + platform: ObservePlatformKey; route: AppObserveNavigationRoute; } export interface FetchNavigationRoutesOptions { startTime: string; endTime: string; - platforms: AppPlatform[]; + targets: ObservePlatformTarget[]; limit: number; after?: string; appVersion?: string; @@ -33,7 +36,7 @@ export interface FetchNavigationRoutesOptions { export interface FetchNavigationRoutesResult { routes: NavigationRouteWithPlatform[]; - pageInfoByPlatform: Map; + pageInfoByPlatform: Map; } export async function fetchObserveNavigationRoutesAsync( @@ -46,13 +49,12 @@ export async function fetchObserveNavigationRoutesAsync( direction: AppObserveEventsOrderByDirection.Desc, }; - const queries = options.platforms.map(async appPlatform => { - const observePlatform = appPlatformToObservePlatform[appPlatform]; + const queries = options.targets.map(async target => { try { const result = await ObserveQuery.navigationRoutesAsync(graphqlClient, { appId, filter: { - platform: observePlatform, + platforms: target.platforms, startTime: options.startTime, endTime: options.endTime, ...(options.appVersion && { appVersion: options.appVersion }), @@ -65,14 +67,18 @@ export async function fetchObserveNavigationRoutesAsync( ...(options.after && { after: options.after }), orderBy, }); - return { appPlatform, ...result }; + return { target, ...result }; } catch (error: any) { // A plan gate is an account-wide rejection, not a per-platform failure — // let it propagate so the command surfaces the upgrade prompt. if (isObservePlanGateError(error)) { throw error; } - Log.warn(`Failed to fetch navigation routes on ${observePlatform}: ${error.message}`); + Log.warn( + `Failed to fetch navigation routes on ${observePlatformDisplayNames[target.key]}: ${ + error.message + }` + ); return null; } }); @@ -80,15 +86,15 @@ export async function fetchObserveNavigationRoutesAsync( const results = await Promise.all(queries); const routes: NavigationRouteWithPlatform[] = []; - const pageInfoByPlatform = new Map(); + const pageInfoByPlatform = new Map(); for (const result of results) { if (!result) { continue; } - pageInfoByPlatform.set(result.appPlatform, result.pageInfo); + pageInfoByPlatform.set(result.target.key, result.pageInfo); for (const route of result.routes) { - routes.push({ platform: result.appPlatform, route }); + routes.push({ platform: result.target.key, route }); } } diff --git a/packages/eas-cli/src/observe/fetchVersions.ts b/packages/eas-cli/src/observe/fetchVersions.ts index b2e41ed5ae..41b9197f79 100644 --- a/packages/eas-cli/src/observe/fetchVersions.ts +++ b/packages/eas-cli/src/observe/fetchVersions.ts @@ -1,39 +1,42 @@ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient'; -import { AppObserveAppVersion, AppObservePlatform, AppPlatform } from '../graphql/generated'; +import { AppObserveAppVersion } from '../graphql/generated'; import { ObserveQuery } from '../graphql/queries/ObserveQuery'; import Log from '../log'; - -const appPlatformToObservePlatform: Record = { - [AppPlatform.Android]: AppObservePlatform.Android, - [AppPlatform.Ios]: AppObservePlatform.Ios, -}; +import { + ObservePlatformKey, + ObservePlatformTarget, + observePlatformDisplayNames, +} from './platforms'; export interface AppVersionsResult { - platform: AppPlatform; + platform: ObservePlatformKey; appVersions: AppObserveAppVersion[]; } export async function fetchObserveVersionsAsync( graphqlClient: ExpoGraphqlClient, appId: string, - platforms: AppPlatform[], + targets: ObservePlatformTarget[], startTime: string, endTime: string, environment?: string ): Promise { - const queries = platforms.map(async (appPlatform): Promise => { - const observePlatform = appPlatformToObservePlatform[appPlatform]; + const queries = targets.map(async (target): Promise => { try { const appVersions = await ObserveQuery.appVersionsAsync(graphqlClient, { appId, - platform: observePlatform, + platforms: target.platforms, startTime, endTime, environment, }); - return { platform: appPlatform, appVersions }; + return { platform: target.key, appVersions }; } catch (error: any) { - Log.warn(`Failed to fetch app versions for ${observePlatform}: ${error.message}`); + Log.warn( + `Failed to fetch app versions for ${observePlatformDisplayNames[target.key]}: ${ + error.message + }` + ); return null; } }); diff --git a/packages/eas-cli/src/observe/flags.ts b/packages/eas-cli/src/observe/flags.ts index 4948280403..9d62d8c729 100644 --- a/packages/eas-cli/src/observe/flags.ts +++ b/packages/eas-cli/src/observe/flags.ts @@ -10,7 +10,7 @@ export const ObserveProjectIdFlag = { export const ObservePlatformFlag = { platform: Flags.option({ - description: 'Filter by platform', + description: 'Filter by platform ("apple" covers iOS, iPadOS, tvOS, and macOS)', options: allowedPlatformFlagValues, })(), }; diff --git a/packages/eas-cli/src/observe/formatMetrics.ts b/packages/eas-cli/src/observe/formatMetrics.ts index 816e8f16ca..7206f6f61f 100644 --- a/packages/eas-cli/src/observe/formatMetrics.ts +++ b/packages/eas-cli/src/observe/formatMetrics.ts @@ -1,11 +1,10 @@ import chalk from 'chalk'; import { EasCommandError } from '../commandUtils/errors'; -import { AppPlatform } from '../graphql/generated'; -import { appPlatformDisplayNames } from '../platform'; import renderTextTable from '../utils/renderTextTable'; import { buildTimeRangeDescription } from './formatUtils'; import { getMetricDisplayName } from './metricNames'; +import { ObservePlatformKey, observePlatformDisplayNames } from './platforms'; export type StatisticKey = | 'min' @@ -84,21 +83,27 @@ export interface MetricValues { eventCount: number | null | undefined; } -type ObserveMetricsKey = `${string}:${AppPlatform}`; +type ObserveMetricsKey = `${string}:${ObservePlatformKey}`; export type ObserveMetricsMap = Map>; export type BuildNumbersMap = Map; export type UpdateIdsMap = Map; -export function makeMetricsKey(appVersion: string, platform: AppPlatform): ObserveMetricsKey { +export function makeMetricsKey( + appVersion: string, + platform: ObservePlatformKey +): ObserveMetricsKey { return `${appVersion}:${platform}`; } -function parseMetricsKey(key: ObserveMetricsKey): { appVersion: string; platform: AppPlatform } { +function parseMetricsKey(key: ObserveMetricsKey): { + appVersion: string; + platform: ObservePlatformKey; +} { const lastColon = key.lastIndexOf(':'); return { appVersion: key.slice(0, lastColon), - platform: key.slice(lastColon + 1) as AppPlatform, + platform: key.slice(lastColon + 1) as ObservePlatformKey, }; } @@ -106,7 +111,7 @@ export type MetricValuesJson = Partial>; export interface ObserveMetricsVersionResult { appVersion: string; - platform: AppPlatform; + platform: ObservePlatformKey; buildNumbers: string[]; updateIds: string[]; metrics: Record; @@ -196,7 +201,7 @@ export function buildObserveMetricsTable( const summaryLine = `${statsDesc} values${countSuffix}${timeDesc ? ` ${timeDesc}` : ''}`; // Group results by platform - const byPlatform = new Map(); + const byPlatform = new Map(); for (const result of results) { if (!byPlatform.has(result.platform)) { byPlatform.set(result.platform, []); @@ -226,7 +231,7 @@ export function buildObserveMetricsTable( for (const [platform, platformResults] of byPlatform) { sections.push(''); - sections.push(chalk.bold(appPlatformDisplayNames[platform])); + sections.push(chalk.bold(observePlatformDisplayNames[platform])); const rows: string[][] = []; for (const result of platformResults) { diff --git a/packages/eas-cli/src/observe/formatNavigationRoutes.ts b/packages/eas-cli/src/observe/formatNavigationRoutes.ts index 512f34748d..efb59961ca 100644 --- a/packages/eas-cli/src/observe/formatNavigationRoutes.ts +++ b/packages/eas-cli/src/observe/formatNavigationRoutes.ts @@ -1,10 +1,9 @@ import chalk from 'chalk'; import { EasCommandError } from '../commandUtils/errors'; -import { AppPlatform } from '../graphql/generated'; -import { appPlatformDisplayNames } from '../platform'; import renderTextTable from '../utils/renderTextTable'; import { NavigationRouteWithPlatform } from './fetchNavigationRoutes'; +import { ObservePlatformKey, observePlatformDisplayNames } from './platforms'; import { buildTimeRangeDescription } from './formatUtils'; import { getMetricDisplayName } from './metricNames'; @@ -73,7 +72,7 @@ function formatMergedCell( export interface NavigationRouteValuesJson { routeName: string; - platform: AppPlatform; + platform: ObservePlatformKey; metrics: Record>>; } @@ -102,7 +101,7 @@ export function buildObserveNavigationRoutesJson( routes: NavigationRouteWithPlatform[], metricNames: string[], stats: NavigationStatKey[], - pageInfoByPlatform: Map + pageInfoByPlatform: Map ): ObserveNavigationRoutesJsonOutput { const jsonRoutes: NavigationRouteValuesJson[] = routes.map(node => { const metrics: Record>> = {}; @@ -139,7 +138,7 @@ export interface BuildNavigationRoutesTableOptions { daysBack?: number; startTime?: string; endTime?: string; - pageInfoByPlatform?: Map; + pageInfoByPlatform?: Map; } export function buildObserveNavigationRoutesTable( @@ -167,7 +166,7 @@ export function buildObserveNavigationRoutesTable( const countSuffix = hasCount && displayStats.length > 0 ? ' (navigation count)' : ''; const summaryLine = `${statsDesc} values${countSuffix}${timeDesc ? ` ${timeDesc}` : ''}`; - const byPlatform = new Map(); + const byPlatform = new Map(); for (const node of routes) { if (!byPlatform.has(node.platform)) { byPlatform.set(node.platform, []); @@ -204,7 +203,7 @@ export function buildObserveNavigationRoutesTable( for (const [platform, platformRoutes] of byPlatform) { sections.push(''); - sections.push(chalk.bold(appPlatformDisplayNames[platform])); + sections.push(chalk.bold(observePlatformDisplayNames[platform])); const rows: string[][] = platformRoutes.map(node => { const cells: string[] = []; @@ -230,7 +229,7 @@ export function buildObserveNavigationRoutesTable( const pageInfo = options?.pageInfoByPlatform?.get(platform); if (pageInfo?.hasNextPage && pageInfo.endCursor) { sections.push( - `Next page (${appPlatformDisplayNames[platform]}): --after ${pageInfo.endCursor}` + `Next page (${observePlatformDisplayNames[platform]}): --after ${pageInfo.endCursor}` ); } } diff --git a/packages/eas-cli/src/observe/formatVersions.ts b/packages/eas-cli/src/observe/formatVersions.ts index 1a61937838..2d6da6cb05 100644 --- a/packages/eas-cli/src/observe/formatVersions.ts +++ b/packages/eas-cli/src/observe/formatVersions.ts @@ -5,10 +5,10 @@ import { AppObserveAppEasBuild, AppObserveAppUpdate, } from '../graphql/generated'; -import { appPlatformDisplayNames } from '../platform'; import renderTextTable from '../utils/renderTextTable'; import { AppVersionsResult } from './fetchVersions'; import { formatDate } from './formatUtils'; +import { observePlatformDisplayNames } from './platforms'; export interface AppVersionJson { platform: string; @@ -109,7 +109,7 @@ export function buildObserveVersionsTable(results: AppVersionsResult[]): string if (sections.length > 0) { sections.push(''); } - sections.push(chalk.bold(appPlatformDisplayNames[platform])); + sections.push(chalk.bold(observePlatformDisplayNames[platform])); const rows: string[][] = appVersions.map(version => [ version.appVersion, diff --git a/packages/eas-cli/src/observe/platforms.ts b/packages/eas-cli/src/observe/platforms.ts index 1c73a0645a..b4491bbe3a 100644 --- a/packages/eas-cli/src/observe/platforms.ts +++ b/packages/eas-cli/src/observe/platforms.ts @@ -1,58 +1,69 @@ -import { AppObservePlatform, AppPlatform } from '../graphql/generated'; +import { AppObservePlatform } from '../graphql/generated'; -/** - * Allowed values for the --platform flag in observe commands. - * Derived from the AppObservePlatform enum so new platforms added on - * the server are automatically picked up. - */ -export const allowedPlatformFlagValues = Object.values(AppObservePlatform).map(s => - s.toLowerCase() -); +export const APPLE_PLATFORM_FLAG_VALUE = 'apple'; -const defaultAppObservePlatform = AppObservePlatform.Ios; -const defaultAppPlatform = AppPlatform.Ios; +const APPLE_OBSERVE_PLATFORMS: AppObservePlatform[] = [ + AppObservePlatform.Ios, + AppObservePlatform.Ipados, + AppObservePlatform.Tvos, + AppObservePlatform.Macos, +]; + +export const allowedPlatformFlagValues = [ + ...Object.values(AppObservePlatform).map(s => s.toLowerCase()), + APPLE_PLATFORM_FLAG_VALUE, +]; type PlatformFlagValue = (typeof allowedPlatformFlagValues)[number]; -/** - * Resolve a single AppObservePlatform from a --platform flag value. - * Returns undefined when no flag was provided. - */ -export function appObservePlatformFromFlag( +export type ObservePlatformKey = AppObservePlatform | 'APPLE'; + +export const observePlatformDisplayNames: Record = { + [AppObservePlatform.Android]: 'Android', + [AppObservePlatform.Ios]: 'iOS', + [AppObservePlatform.Ipados]: 'iPadOS', + [AppObservePlatform.Tvos]: 'tvOS', + [AppObservePlatform.Macos]: 'macOS', + APPLE: 'Apple', +}; + +export interface ObservePlatformTarget { + key: ObservePlatformKey; + platforms: AppObservePlatform[]; +} + +function singlePlatformFromFlag(flag: PlatformFlagValue): AppObservePlatform { + const platform = Object.values(AppObservePlatform).find(p => p.toLowerCase() === flag); + if (!platform) { + throw new Error(`Unknown platform flag value: "${flag}"`); + } + return platform; +} + +export function observePlatformsFromFlag( flag: PlatformFlagValue | undefined -): AppObservePlatform | undefined { +): AppObservePlatform[] | undefined { if (!flag) { return undefined; } - switch (flag) { - case 'android': - return AppObservePlatform.Android; - case 'ios': - return AppObservePlatform.Ios; + if (flag === APPLE_PLATFORM_FLAG_VALUE) { + return APPLE_OBSERVE_PLATFORMS; } - return defaultAppObservePlatform; + return [singlePlatformFromFlag(flag)]; } -/** - * Resolve a list of AppPlatform values from a --platform flag value. - * Returns the single matching platform when a flag is provided, or all - * known platforms when no flag is provided (so the caller queries every - * platform). - */ -export function appPlatformsFromFlag(flag: PlatformFlagValue | undefined): AppPlatform[] { +export function observePlatformTargetsFromFlag( + flag: PlatformFlagValue | undefined +): ObservePlatformTarget[] { if (!flag) { - return [AppPlatform.Android, AppPlatform.Ios]; + return [ + { key: AppObservePlatform.Android, platforms: [AppObservePlatform.Android] }, + { key: AppObservePlatform.Ios, platforms: [AppObservePlatform.Ios] }, + ]; } - switch (flag) { - case 'android': - return [AppPlatform.Android]; - case 'ios': - return [AppPlatform.Ios]; + if (flag === APPLE_PLATFORM_FLAG_VALUE) { + return [{ key: 'APPLE', platforms: APPLE_OBSERVE_PLATFORMS }]; } - return [defaultAppPlatform]; + const platform = singlePlatformFromFlag(flag); + return [{ key: platform, platforms: [platform] }]; } - -export const appPlatformToObservePlatform: Record = { - [AppPlatform.Android]: AppObservePlatform.Android, - [AppPlatform.Ios]: AppObservePlatform.Ios, -};