From 0c3d2a3eb20e8c87fd99e46291c3d8be770efa70 Mon Sep 17 00:00:00 2001 From: Philippe Loulidi <78689302+LipLoody@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:08:09 -0600 Subject: [PATCH 1/3] [eas-cli] Show EAS Simulator minutes in eas account:usage --- .../commandUtils/__tests__/usageUtils.test.ts | 140 +++++++++++++++ .../eas-cli/src/commandUtils/usageUtils.ts | 72 +++++++- .../commands/account/__tests__/usage.test.ts | 159 ++++++++++++++++-- .../eas-cli/src/commands/account/usage.ts | 69 +++++++- packages/eas-cli/src/graphql/generated.ts | 6 +- .../src/graphql/queries/AccountQuery.ts | 22 +++ packages/eas-cli/src/graphql/types/Account.ts | 5 + 7 files changed, 451 insertions(+), 22 deletions(-) diff --git a/packages/eas-cli/src/commandUtils/__tests__/usageUtils.test.ts b/packages/eas-cli/src/commandUtils/__tests__/usageUtils.test.ts index a741054f34..8cc42da939 100644 --- a/packages/eas-cli/src/commandUtils/__tests__/usageUtils.test.ts +++ b/packages/eas-cli/src/commandUtils/__tests__/usageUtils.test.ts @@ -22,6 +22,15 @@ function createMockFullUsageData( bandwidthLimit: number; buildOverageCost: number; updateOverageCost: number; + simulator: { + planValue: number; + limit: number; + iosMinutes: number; + androidMinutes: number; + overageMinutes: number; + overageCost: number; + jobTypeBreakdown?: { workflows: number; simulator: number; other: number }; + }; }> = {} ): AccountFullUsageData { const { @@ -35,6 +44,7 @@ function createMockFullUsageData( bandwidthLimit = 10, buildOverageCost = 0, updateOverageCost = 0, + simulator, } = overrides; const now = new Date(); @@ -167,6 +177,71 @@ function createMockFullUsageData( : [], totalCost: updateOverageCost, }, + EAS_SIMULATOR: { + __typename: 'UsageMetricTotal', + id: 'simulator-metric-id', + billingPeriod: { + __typename: 'BillingPeriod', + id: 'billing-period-id', + start: startOfMonth.toISOString(), + end: endOfMonth.toISOString(), + anchor: startOfMonth.toISOString(), + }, + planMetrics: simulator + ? [ + { + __typename: 'EstimatedUsage', + id: 'simulator-plan-metric-id', + service: EasService.Simulator, + serviceMetric: EasServiceMetric.SimulatorUsage, + metricType: UsageMetricType.Minute, + value: simulator.planValue, + limit: simulator.limit, + jobTypeBreakdown: simulator.jobTypeBreakdown + ? { __typename: 'EstimatedUsageJobTypeBreakdown', ...simulator.jobTypeBreakdown } + : null, + }, + ] + : [], + overageMetrics: + simulator && simulator.overageMinutes > 0 + ? [ + { + __typename: 'EstimatedOverageAndCost', + id: 'simulator-overage-id', + service: EasService.Simulator, + serviceMetric: EasServiceMetric.SimulatorUsage, + metricType: UsageMetricType.Minute, + value: simulator.overageMinutes, + limit: simulator.limit, + totalCost: simulator.overageCost, + }, + ] + : [], + totalCost: simulator?.overageCost ?? 0, + }, + IOS_SIMULATOR_MINUTES: simulator + ? [ + { + __typename: 'AccountUsageMetric', + id: 'ios-simulator-minutes-id', + serviceMetric: EasServiceMetric.SimulatorUsage, + metricType: UsageMetricType.Minute, + value: simulator.iosMinutes, + }, + ] + : [], + ANDROID_SIMULATOR_MINUTES: simulator + ? [ + { + __typename: 'AccountUsageMetric', + id: 'android-simulator-minutes-id', + serviceMetric: EasServiceMetric.SimulatorUsage, + metricType: UsageMetricType.Minute, + value: simulator.androidMinutes, + }, + ] + : [], }, }; } @@ -271,6 +346,71 @@ describe('extractUsageData', () => { expect(result.totalOverageCostCents).toBe(2000); }); + it('omits simulator usage when the account has no simulator metric', () => { + const result = extractUsageData(createMockFullUsageData()); + + expect(result.simulator).toBeUndefined(); + }); + + it('extracts simulator minutes and adds the overage to the total cost', () => { + const mockData = createMockFullUsageData({ + buildValue: 60, + buildLimit: 50, + buildOverageCost: 2000, + simulator: { + planValue: 60, + limit: 60, + iosMinutes: 50.5, + androidMinutes: 14, + overageMinutes: 4.5, + overageCost: 68, + }, + }); + + const result = extractUsageData(mockData); + + expect(result.simulator).toEqual({ + minutes: { + name: 'Simulator minutes', + planValue: 60, + limit: 60, + percentUsed: 100, + overageValue: 4.5, + overageCost: 68, + unit: 'minutes', + maxDecimals: 1, + }, + iosMinutes: 50.5, + androidMinutes: 14, + jobTypeBreakdown: undefined, + overageCostCents: 68, + }); + expect(result.totalOverageCostCents).toBe(2068); + expect(result.estimatedBillCents).toBe(1900 + 2068); + }); + + it('labels the Free plan minute pool and keeps the job type breakdown', () => { + const mockData = createMockFullUsageData({ + subscriptionName: 'Free', + simulator: { + planValue: 45, + limit: 60, + iosMinutes: 5, + androidMinutes: 0, + overageMinutes: 0, + overageCost: 0, + jobTypeBreakdown: { workflows: 40, simulator: 5, other: 0 }, + }, + }); + + const result = extractUsageData(mockData); + + expect(result.simulator?.minutes.name).toBe('CI/CD and Simulator minutes'); + expect(result.simulator?.minutes.percentUsed).toBe(75); + expect(result.simulator?.jobTypeBreakdown).toEqual({ workflows: 40, simulator: 5, other: 0 }); + expect(result.totalOverageCostCents).toBe(0); + }); + it('handles Free plan', () => { const mockData = createMockFullUsageData({ subscriptionName: 'Free', diff --git a/packages/eas-cli/src/commandUtils/usageUtils.ts b/packages/eas-cli/src/commandUtils/usageUtils.ts index 567ee86506..4a31bdbfbd 100644 --- a/packages/eas-cli/src/commandUtils/usageUtils.ts +++ b/packages/eas-cli/src/commandUtils/usageUtils.ts @@ -15,6 +15,21 @@ export interface UsageMetricDisplay { overageValue: number; overageCost: number; unit?: string; + maxDecimals?: number; +} + +export interface SimulatorJobTypeBreakdown { + workflows: number; + simulator: number; + other: number; +} + +export interface SimulatorUsageDisplay { + minutes: UsageMetricDisplay; + iosMinutes: number; + androidMinutes: number; + jobTypeBreakdown?: SimulatorJobTypeBreakdown; + overageCostCents: number; } export interface BuildOverageByWorkerSize { @@ -50,6 +65,7 @@ export interface UsageDisplayData { bandwidth: UsageMetricDisplay; overageCostCents: number; }; + simulator?: SimulatorUsageDisplay; totalOverageCostCents: number; estimatedBillCents: number; recurringCents: number | null; @@ -108,6 +124,9 @@ export function extractUsageData(data: AccountFullUsageData): UsageDisplayData { LARGE_ANDROID_BUILDS, MEDIUM_IOS_BUILDS, LARGE_IOS_BUILDS, + EAS_SIMULATOR, + IOS_SIMULATOR_MINUTES, + ANDROID_SIMULATOR_MINUTES, } = usageMetrics; // Find build metrics @@ -201,9 +220,17 @@ export function extractUsageData(data: AccountFullUsageData): UsageDisplayData { const bandwidthOverageValue = bandwidthOverage?.value ?? 0; const bandwidthLimit = bandwidthMetric?.limit ?? 0; + const simulator = extractSimulatorUsage( + EAS_SIMULATOR, + IOS_SIMULATOR_MINUTES?.[0]?.value ?? 0, + ANDROID_SIMULATOR_MINUTES?.[0]?.value ?? 0 + ); + const buildOverageCostCents = EAS_BUILD.totalCost; const updateOverageCostCents = EAS_UPDATE.totalCost; - const totalOverageCostCents = buildOverageCostCents + updateOverageCostCents; + const simulatorOverageCostCents = simulator?.overageCostCents ?? 0; + const totalOverageCostCents = + buildOverageCostCents + updateOverageCostCents + simulatorOverageCostCents; // Calculate total overage count for display const totalOverageBuilds = overagesByWorkerSize.reduce((sum, o) => sum + o.count, 0); @@ -272,12 +299,55 @@ export function extractUsageData(data: AccountFullUsageData): UsageDisplayData { }, overageCostCents: updateOverageCostCents, }, + simulator, totalOverageCostCents, estimatedBillCents: (subscription?.recurringCents ?? 0) + totalOverageCostCents, recurringCents: subscription?.recurringCents ?? null, }; } +// Accounts without EAS Simulator get no SIMULATOR_USAGE plan metric, so the section is hidden. +// On the Free plan the metric is the minute pool shared with Workflows, split by jobTypeBreakdown. +function extractSimulatorUsage( + simulatorTotal: AccountFullUsageData['usageMetrics']['EAS_SIMULATOR'], + iosMinutes: number, + androidMinutes: number +): SimulatorUsageDisplay | undefined { + const planMetric = simulatorTotal.planMetrics.find( + m => m.serviceMetric === EasServiceMetric.SimulatorUsage + ); + if (!planMetric) { + return undefined; + } + const overage = simulatorTotal.overageMetrics.find( + m => m.serviceMetric === EasServiceMetric.SimulatorUsage + ); + const jobTypeBreakdown = planMetric.jobTypeBreakdown + ? { + workflows: planMetric.jobTypeBreakdown.workflows, + simulator: planMetric.jobTypeBreakdown.simulator, + other: planMetric.jobTypeBreakdown.other, + } + : undefined; + + return { + minutes: { + name: jobTypeBreakdown ? 'CI/CD and Simulator minutes' : 'Simulator minutes', + planValue: planMetric.value, + limit: planMetric.limit, + percentUsed: calculatePercentUsed(planMetric.value, planMetric.limit), + overageValue: overage?.value ?? 0, + overageCost: overage?.totalCost ?? 0, + unit: 'minutes', + maxDecimals: 1, + }, + iosMinutes, + androidMinutes, + jobTypeBreakdown, + overageCostCents: simulatorTotal.totalCost, + }; +} + export function calculateBillingPeriodInfo(data: UsageDisplayData): { daysRemaining: number; daysElapsed: number; diff --git a/packages/eas-cli/src/commands/account/__tests__/usage.test.ts b/packages/eas-cli/src/commands/account/__tests__/usage.test.ts index b0dda600c5..27d811dd70 100644 --- a/packages/eas-cli/src/commands/account/__tests__/usage.test.ts +++ b/packages/eas-cli/src/commands/account/__tests__/usage.test.ts @@ -34,6 +34,15 @@ function createMockFullUsageData( bandwidthLimit: number; buildOverageCost: number; updateOverageCost: number; + simulator: { + planValue: number; + limit: number; + iosMinutes: number; + androidMinutes: number; + overageMinutes?: number; + overageCost?: number; + jobTypeBreakdown?: { workflows: number; simulator: number; other: number }; + }; }> = {} ): AccountFullUsageQueryType['account']['byId'] { const { @@ -47,6 +56,7 @@ function createMockFullUsageData( bandwidthLimit = 10, buildOverageCost = 0, updateOverageCost = 0, + simulator, } = overrides; const now = new Date(); @@ -179,10 +189,87 @@ function createMockFullUsageData( : [], totalCost: updateOverageCost, }, + // Accounts without EAS Simulator get empty metrics from the server + EAS_SIMULATOR: { + __typename: 'UsageMetricTotal', + id: 'simulator-metric-id', + billingPeriod: { + __typename: 'BillingPeriod', + id: 'billing-period-id', + start: startOfMonth.toISOString(), + end: endOfMonth.toISOString(), + anchor: startOfMonth.toISOString(), + }, + planMetrics: simulator + ? [ + { + __typename: 'EstimatedUsage', + id: 'simulator-plan-metric-id', + service: EasService.Simulator, + serviceMetric: EasServiceMetric.SimulatorUsage, + metricType: UsageMetricType.Minute, + value: simulator.planValue, + limit: simulator.limit, + jobTypeBreakdown: simulator.jobTypeBreakdown + ? { __typename: 'EstimatedUsageJobTypeBreakdown', ...simulator.jobTypeBreakdown } + : null, + }, + ] + : [], + overageMetrics: + simulator && (simulator.overageMinutes ?? 0) > 0 + ? [ + { + __typename: 'EstimatedOverageAndCost', + id: 'simulator-overage-id', + service: EasService.Simulator, + serviceMetric: EasServiceMetric.SimulatorUsage, + metricType: UsageMetricType.Minute, + value: simulator.overageMinutes ?? 0, + limit: simulator.limit, + totalCost: simulator.overageCost ?? 0, + }, + ] + : [], + totalCost: simulator?.overageCost ?? 0, + }, + IOS_SIMULATOR_MINUTES: simulator + ? [ + { + __typename: 'AccountUsageMetric', + id: 'ios-simulator-minutes-id', + serviceMetric: EasServiceMetric.SimulatorUsage, + metricType: UsageMetricType.Minute, + value: simulator.iosMinutes, + }, + ] + : [], + ANDROID_SIMULATOR_MINUTES: simulator + ? [ + { + __typename: 'AccountUsageMetric', + id: 'android-simulator-minutes-id', + serviceMetric: EasServiceMetric.SimulatorUsage, + metricType: UsageMetricType.Minute, + value: simulator.androidMinutes, + }, + ] + : [], }, }; } +function renderUsage(usageData: ReturnType): string { + const mockLog = jest.mocked(Log.log); + mockLog.mockClear(); + displayUsage(extractUsageData(usageData), usageData); + // eslint-disable-next-line no-control-regex + return mockLog.mock.calls + .flat() + .join('\n') + .replace(/\u001b\[\d+m/g, ''); +} + describe('AccountQuery', () => { const mockGraphqlClient = {} as ExpoGraphqlClient; const mockGetFullUsageAsync = jest.mocked(AccountQuery.getFullUsageAsync); @@ -219,12 +306,6 @@ describe('AccountQuery', () => { }); describe('displayUsage', () => { - const mockLog = jest.mocked(Log.log); - - beforeEach(() => { - mockLog.mockClear(); - }); - it('labels usage beyond included credits as "additional usage"', () => { const usageData = createMockFullUsageData({ buildValue: 55, @@ -234,21 +315,71 @@ describe('displayUsage', () => { buildOverageCost: 1500, updateOverageCost: 250, }); - const displayData = extractUsageData(usageData); - - displayUsage(displayData, usageData); + const output = renderUsage(usageData); - // eslint-disable-next-line no-control-regex - const output = mockLog.mock.calls - .flat() - .join('\n') - .replace(/\u001b\[\d+m/g, ''); expect(output).toContain('Unique Updaters (additional usage): 500 users ($2.50)'); expect(output).toContain('Additional usage: $17.50'); expect(output).toContain('Builds: $15.00'); expect(output).toContain('Updates: $2.50'); expect(output.toLowerCase()).not.toContain('overage'); }); + + it('hides the EAS Simulator section when the account has no simulator metric', () => { + const usageData = createMockFullUsageData(); + + const output = renderUsage(usageData); + + expect(output).not.toContain('EAS Simulator'); + expect(extractUsageData(usageData).simulator).toBeUndefined(); + }); + + it('shows simulator minutes with the platform breakdown and additional usage', () => { + const usageData = createMockFullUsageData({ + simulator: { + planValue: 60, + limit: 60, + iosMinutes: 50.25, + androidMinutes: 13.5, + overageMinutes: 3.75, + overageCost: 52, + }, + }); + + const output = renderUsage(usageData); + + expect(output).toContain('EAS Simulator'); + expect(output).toContain('Simulator minutes (plan): 60/60 minutes'); + expect(output).toContain('Simulator minutes (additional usage): 3.8 minutes ($0.52)'); + expect(output).toContain('Simulator minutes by platform:'); + expect(output).toContain('iOS: 50.3 minutes'); + expect(output).toContain('Android: 13.5 minutes'); + expect(output).not.toContain('Breakdown by job type'); + expect(output).toContain('Simulator: $0.52'); + expect(output).toContain('Additional usage: $0.52'); + expect(output).toContain('Estimated bill: $19.52'); + }); + + it('shows the shared minute pool by job type on the Free plan', () => { + const usageData = createMockFullUsageData({ + subscriptionName: 'Free', + simulator: { + planValue: 50, + limit: 60, + iosMinutes: 10, + androidMinutes: 0, + jobTypeBreakdown: { workflows: 40, simulator: 10, other: 0 }, + }, + }); + + const output = renderUsage(usageData); + + expect(output).toContain('CI/CD and Simulator minutes (plan): 50/60 minutes'); + expect(output).toContain('Breakdown by job type:'); + expect(output).toContain('Workflows: 40 minutes'); + expect(output).toContain('Simulator: 10 minutes'); + expect(output).not.toContain('Other jobs'); + expect(output).toContain('iOS: 10 minutes'); + }); }); describe('calculatePercentUsed', () => { diff --git a/packages/eas-cli/src/commands/account/usage.ts b/packages/eas-cli/src/commands/account/usage.ts index 4f2a8448e6..f254916020 100644 --- a/packages/eas-cli/src/commands/account/usage.ts +++ b/packages/eas-cli/src/commands/account/usage.ts @@ -4,6 +4,7 @@ import chalk from 'chalk'; import EasCommand from '../../commandUtils/EasCommand'; import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags'; import { + SimulatorUsageDisplay, UsageDisplayData, UsageMetricDisplay, calculateBillingPeriodInfo, @@ -65,12 +66,22 @@ function displayBandwidthMetric(metric: UsageMetricDisplay, indent: string = ' } } +function formatMetricValue(value: number, maxDecimals: number): string { + return value.toLocaleString(undefined, { maximumFractionDigits: maxDecimals }); +} + +function formatMinutes(minutes: number): string { + return `${formatMetricValue(minutes, 1)} minutes`; +} + function displayMetric(metric: UsageMetricDisplay, indent: string = ' '): void { const progressBar = createProgressBar(metric.percentUsed, 20); const percentStr = `${metric.percentUsed}%`; - const planUsageStr = `${formatNumber(metric.planValue)}/${formatNumber(metric.limit)} ${ - metric.unit ?? '' - }`; + const maxDecimals = metric.maxDecimals ?? 0; + const planUsageStr = `${formatMetricValue(metric.planValue, maxDecimals)}/${formatMetricValue( + metric.limit, + maxDecimals + )} ${metric.unit ?? ''}`; let color = chalk.green; if (metric.percentUsed >= 100) { @@ -85,12 +96,34 @@ function displayMetric(metric: UsageMetricDisplay, indent: string = ' '): void if (metric.overageValue > 0) { Log.log( `${indent}${metric.name} (additional usage): ${chalk.red( - `${formatNumber(metric.overageValue)} ${metric.unit ?? ''}` + `${formatMetricValue(metric.overageValue, maxDecimals)} ${metric.unit ?? ''}` )} (${formatCurrency(metric.overageCost)})` ); } } +function displaySimulatorUsage(simulator: SimulatorUsageDisplay): void { + Log.newLine(); + Log.log(chalk.bold.underline('EAS Simulator')); + displayMetric(simulator.minutes); + + if (simulator.jobTypeBreakdown) { + const { workflows, simulator: simulatorMinutes, other } = simulator.jobTypeBreakdown; + Log.log(' Breakdown by job type:'); + Log.log(` Workflows: ${formatMinutes(workflows)}`); + Log.log(` Simulator: ${formatMinutes(simulatorMinutes)}`); + if (other > 0) { + Log.log(` Other jobs: ${formatMinutes(other)}`); + } + } + + if (simulator.iosMinutes > 0 || simulator.androidMinutes > 0) { + Log.log(' Simulator minutes by platform:'); + Log.log(` iOS: ${formatMinutes(simulator.iosMinutes)}`); + Log.log(` Android: ${formatMinutes(simulator.androidMinutes)}`); + } +} + function billingUrl(accountName: string): string { return `https://expo.dev/accounts/${accountName}/settings/billing`; } @@ -141,6 +174,10 @@ export function displayUsage(data: UsageDisplayData, usageData: AccountFullUsage displayMetric(data.updates.mau); displayBandwidthMetric(data.updates.bandwidth); + if (data.simulator) { + displaySimulatorUsage(data.simulator); + } + Log.newLine(); Log.log(chalk.bold.underline('Billing')); @@ -190,6 +227,9 @@ export function displayUsage(data: UsageDisplayData, usageData: AccountFullUsage if (data.updates.overageCostCents > 0) { Log.log(` Updates: ${formatCurrency(data.updates.overageCostCents)}`); } + if (data.simulator && data.simulator.overageCostCents > 0) { + Log.log(` Simulator: ${formatCurrency(data.simulator.overageCostCents)}`); + } } Log.log(` Estimated bill: ${chalk.bold(formatCurrency(data.estimatedBillCents))}`); @@ -387,6 +427,27 @@ export default class AccountUsage extends EasCommand { }, overageCostCents: displayData.updates.overageCostCents, }, + simulator: displayData.simulator + ? { + minutes: { + plan: { + used: displayData.simulator.minutes.planValue, + limit: displayData.simulator.minutes.limit, + percentUsed: displayData.simulator.minutes.percentUsed, + }, + overage: { + count: displayData.simulator.minutes.overageValue, + costCents: displayData.simulator.minutes.overageCost, + }, + }, + byPlatform: { + ios: displayData.simulator.iosMinutes, + android: displayData.simulator.androidMinutes, + }, + byJobType: displayData.simulator.jobTypeBreakdown ?? null, + overageCostCents: displayData.simulator.overageCostCents, + } + : null, billing: { addons: subscription?.addons ?? [], upcomingInvoice: subscription?.upcomingInvoice ?? null, diff --git a/packages/eas-cli/src/graphql/generated.ts b/packages/eas-cli/src/graphql/generated.ts index 7ce0e4e53d..e400c8d924 100644 --- a/packages/eas-cli/src/graphql/generated.ts +++ b/packages/eas-cli/src/graphql/generated.ts @@ -15661,7 +15661,7 @@ export type AccountFullUsageQueryVariables = Exact<{ }>; -export type AccountFullUsageQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byId: { __typename?: 'Account', id: string, name: string, subscription?: { __typename?: 'SubscriptionDetails', id: string, name?: string | null, status?: string | null, nextInvoice?: any | null, nextInvoiceAmountDueCents?: number | null, recurringCents?: number | null, price: number, concurrencies?: { __typename?: 'Concurrencies', total: number, android: number, ios: number } | null, addons: Array<{ __typename?: 'AddonDetails', id: string, name: string, quantity?: number | null }>, upcomingInvoice?: { __typename?: 'Invoice', id: string, total: number, lineItems: Array<{ __typename?: 'InvoiceLineItem', id: string, description: string, amount: number, period: { __typename?: 'InvoicePeriod', start: any, end: any } }> } | null } | null, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, usageMetrics: { __typename?: 'AccountUsageMetrics', MEDIUM_ANDROID_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, LARGE_ANDROID_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, MEDIUM_IOS_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, LARGE_IOS_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, EAS_BUILD: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }, EAS_UPDATE: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> } } } } }; +export type AccountFullUsageQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byId: { __typename?: 'Account', id: string, name: string, subscription?: { __typename?: 'SubscriptionDetails', id: string, name?: string | null, status?: string | null, nextInvoice?: any | null, nextInvoiceAmountDueCents?: number | null, recurringCents?: number | null, price: number, concurrencies?: { __typename?: 'Concurrencies', total: number, android: number, ios: number } | null, addons: Array<{ __typename?: 'AddonDetails', id: string, name: string, quantity?: number | null }>, upcomingInvoice?: { __typename?: 'Invoice', id: string, total: number, lineItems: Array<{ __typename?: 'InvoiceLineItem', id: string, description: string, amount: number, period: { __typename?: 'InvoicePeriod', start: any, end: any } }> } | null } | null, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, usageMetrics: { __typename?: 'AccountUsageMetrics', MEDIUM_ANDROID_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, LARGE_ANDROID_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, MEDIUM_IOS_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, LARGE_IOS_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, EAS_BUILD: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null, jobTypeBreakdown?: { __typename?: 'EstimatedUsageJobTypeBreakdown', workflows: number, simulator: number, other: number } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }, EAS_UPDATE: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null, jobTypeBreakdown?: { __typename?: 'EstimatedUsageJobTypeBreakdown', workflows: number, simulator: number, other: number } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }, EAS_SIMULATOR: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null, jobTypeBreakdown?: { __typename?: 'EstimatedUsageJobTypeBreakdown', workflows: number, simulator: number, other: number } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }, IOS_SIMULATOR_MINUTES: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, ANDROID_SIMULATOR_MINUTES: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }> } } } }; export type AccountUsageForOverageWarningQueryVariables = Exact<{ accountId: Scalars['String']['input']; @@ -16625,11 +16625,11 @@ export type BillingPeriodFragment = { __typename?: 'BillingPeriod', id: string, export type EstimatedUsagePlatformBreakdownFragment = { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } }; -export type EstimatedUsageFragment = { __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null }; +export type EstimatedUsageFragment = { __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null, jobTypeBreakdown?: { __typename?: 'EstimatedUsageJobTypeBreakdown', workflows: number, simulator: number, other: number } | null }; export type EstimatedOverageAndCostFragment = { __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }; -export type UsageMetricTotalFragment = { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }; +export type UsageMetricTotalFragment = { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null, jobTypeBreakdown?: { __typename?: 'EstimatedUsageJobTypeBreakdown', workflows: number, simulator: number, other: number } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }; export type AccountUsageMetricFragment = { __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }; diff --git a/packages/eas-cli/src/graphql/queries/AccountQuery.ts b/packages/eas-cli/src/graphql/queries/AccountQuery.ts index db829a0970..69b99e2d9e 100644 --- a/packages/eas-cli/src/graphql/queries/AccountQuery.ts +++ b/packages/eas-cli/src/graphql/queries/AccountQuery.ts @@ -147,6 +147,28 @@ export const AccountQuery = { id ...UsageMetricTotalFragment } + EAS_SIMULATOR: byBillingPeriod(date: $currentDate, service: SIMULATOR) { + id + ...UsageMetricTotalFragment + } + IOS_SIMULATOR_MINUTES: metricsForServiceMetric( + serviceMetric: SIMULATOR_USAGE + granularity: TOTAL + timespan: { start: $startDate, end: $endDate } + filterParams: { platform: "ios" } + ) { + id + ...AccountUsageMetricFragment + } + ANDROID_SIMULATOR_MINUTES: metricsForServiceMetric( + serviceMetric: SIMULATOR_USAGE + granularity: TOTAL + timespan: { start: $startDate, end: $endDate } + filterParams: { platform: "android" } + ) { + id + ...AccountUsageMetricFragment + } } } } diff --git a/packages/eas-cli/src/graphql/types/Account.ts b/packages/eas-cli/src/graphql/types/Account.ts index cef03f735a..539b478294 100644 --- a/packages/eas-cli/src/graphql/types/Account.ts +++ b/packages/eas-cli/src/graphql/types/Account.ts @@ -117,6 +117,11 @@ export const EstimatedUsageFragmentNode = gql` platformBreakdown { ...EstimatedUsagePlatformBreakdownFragment } + jobTypeBreakdown { + workflows + simulator + other + } } `; From 10c8c677cb2de9ec3fc3490121782837425e5fa7 Mon Sep 17 00:00:00 2001 From: Philippe Loulidi <78689302+LipLoody@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:15:52 -0600 Subject: [PATCH 2/3] [eas-cli] report unavailable Simulator usage estimates --- .../src/commandUtils/__tests__/usageUtils.test.ts | 9 +++++++++ packages/eas-cli/src/commandUtils/usageUtils.ts | 10 ++++++++++ .../src/commands/account/__tests__/usage.test.ts | 1 + packages/eas-cli/src/graphql/generated.ts | 2 +- packages/eas-cli/src/graphql/queries/AccountQuery.ts | 3 +++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/eas-cli/src/commandUtils/__tests__/usageUtils.test.ts b/packages/eas-cli/src/commandUtils/__tests__/usageUtils.test.ts index 8cc42da939..027147aa47 100644 --- a/packages/eas-cli/src/commandUtils/__tests__/usageUtils.test.ts +++ b/packages/eas-cli/src/commandUtils/__tests__/usageUtils.test.ts @@ -178,6 +178,7 @@ function createMockFullUsageData( totalCost: updateOverageCost, }, EAS_SIMULATOR: { + unavailableServiceUsageMetrics: [], __typename: 'UsageMetricTotal', id: 'simulator-metric-id', billingPeriod: { @@ -315,6 +316,14 @@ describe('calculateBillingPeriodDays', () => { }); describe('extractUsageData', () => { + it('does not estimate a bill when Simulator usage is unavailable', () => { + const data = createMockFullUsageData(); + data.usageMetrics.EAS_SIMULATOR.unavailableServiceUsageMetrics = [ + { service: EasService.Simulator }, + ]; + expect(() => extractUsageData(data)).toThrow('Simulator usage is temporarily unavailable'); + }); + it('correctly extracts usage data from API response', () => { const mockData = createMockFullUsageData({ buildValue: 25, diff --git a/packages/eas-cli/src/commandUtils/usageUtils.ts b/packages/eas-cli/src/commandUtils/usageUtils.ts index 4a31bdbfbd..13d07ff465 100644 --- a/packages/eas-cli/src/commandUtils/usageUtils.ts +++ b/packages/eas-cli/src/commandUtils/usageUtils.ts @@ -1,6 +1,7 @@ import { AppPlatform, EasBuildBillingResourceClass, + EasService, EasServiceMetric, UsageMetricType, } from '../graphql/generated'; @@ -313,6 +314,15 @@ function extractSimulatorUsage( iosMinutes: number, androidMinutes: number ): SimulatorUsageDisplay | undefined { + if ( + simulatorTotal.unavailableServiceUsageMetrics.some( + metric => metric.service === EasService.Simulator + ) + ) { + throw new Error( + 'Simulator usage is temporarily unavailable, so the bill cannot be estimated. Try again in a few minutes.' + ); + } const planMetric = simulatorTotal.planMetrics.find( m => m.serviceMetric === EasServiceMetric.SimulatorUsage ); diff --git a/packages/eas-cli/src/commands/account/__tests__/usage.test.ts b/packages/eas-cli/src/commands/account/__tests__/usage.test.ts index 27d811dd70..65eb13f930 100644 --- a/packages/eas-cli/src/commands/account/__tests__/usage.test.ts +++ b/packages/eas-cli/src/commands/account/__tests__/usage.test.ts @@ -191,6 +191,7 @@ function createMockFullUsageData( }, // Accounts without EAS Simulator get empty metrics from the server EAS_SIMULATOR: { + unavailableServiceUsageMetrics: [], __typename: 'UsageMetricTotal', id: 'simulator-metric-id', billingPeriod: { diff --git a/packages/eas-cli/src/graphql/generated.ts b/packages/eas-cli/src/graphql/generated.ts index e400c8d924..5d74aa90fa 100644 --- a/packages/eas-cli/src/graphql/generated.ts +++ b/packages/eas-cli/src/graphql/generated.ts @@ -15661,7 +15661,7 @@ export type AccountFullUsageQueryVariables = Exact<{ }>; -export type AccountFullUsageQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byId: { __typename?: 'Account', id: string, name: string, subscription?: { __typename?: 'SubscriptionDetails', id: string, name?: string | null, status?: string | null, nextInvoice?: any | null, nextInvoiceAmountDueCents?: number | null, recurringCents?: number | null, price: number, concurrencies?: { __typename?: 'Concurrencies', total: number, android: number, ios: number } | null, addons: Array<{ __typename?: 'AddonDetails', id: string, name: string, quantity?: number | null }>, upcomingInvoice?: { __typename?: 'Invoice', id: string, total: number, lineItems: Array<{ __typename?: 'InvoiceLineItem', id: string, description: string, amount: number, period: { __typename?: 'InvoicePeriod', start: any, end: any } }> } | null } | null, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, usageMetrics: { __typename?: 'AccountUsageMetrics', MEDIUM_ANDROID_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, LARGE_ANDROID_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, MEDIUM_IOS_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, LARGE_IOS_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, EAS_BUILD: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null, jobTypeBreakdown?: { __typename?: 'EstimatedUsageJobTypeBreakdown', workflows: number, simulator: number, other: number } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }, EAS_UPDATE: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null, jobTypeBreakdown?: { __typename?: 'EstimatedUsageJobTypeBreakdown', workflows: number, simulator: number, other: number } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }, EAS_SIMULATOR: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null, jobTypeBreakdown?: { __typename?: 'EstimatedUsageJobTypeBreakdown', workflows: number, simulator: number, other: number } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }, IOS_SIMULATOR_MINUTES: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, ANDROID_SIMULATOR_MINUTES: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }> } } } }; +export type AccountFullUsageQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byId: { __typename?: 'Account', id: string, name: string, subscription?: { __typename?: 'SubscriptionDetails', id: string, name?: string | null, status?: string | null, nextInvoice?: any | null, nextInvoiceAmountDueCents?: number | null, recurringCents?: number | null, price: number, concurrencies?: { __typename?: 'Concurrencies', total: number, android: number, ios: number } | null, addons: Array<{ __typename?: 'AddonDetails', id: string, name: string, quantity?: number | null }>, upcomingInvoice?: { __typename?: 'Invoice', id: string, total: number, lineItems: Array<{ __typename?: 'InvoiceLineItem', id: string, description: string, amount: number, period: { __typename?: 'InvoicePeriod', start: any, end: any } }> } | null } | null, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, usageMetrics: { __typename?: 'AccountUsageMetrics', MEDIUM_ANDROID_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, LARGE_ANDROID_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, MEDIUM_IOS_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, LARGE_IOS_BUILDS: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, EAS_BUILD: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null, jobTypeBreakdown?: { __typename?: 'EstimatedUsageJobTypeBreakdown', workflows: number, simulator: number, other: number } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }, EAS_UPDATE: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null, jobTypeBreakdown?: { __typename?: 'EstimatedUsageJobTypeBreakdown', workflows: number, simulator: number, other: number } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }, EAS_SIMULATOR: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, unavailableServiceUsageMetrics: Array<{ __typename?: 'UnavailableServiceUsageMetric', service: EasService }>, billingPeriod: { __typename?: 'BillingPeriod', id: string, start: any, end: any, anchor: any }, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, platformBreakdown?: { __typename?: 'EstimatedUsagePlatformBreakdown', ios: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number }, android: { __typename?: 'EstimatedUsagePlatformDetail', value: number, limit: number } } | null, jobTypeBreakdown?: { __typename?: 'EstimatedUsageJobTypeBreakdown', workflows: number, simulator: number, other: number } | null }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, service: EasService, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number, limit: number, totalCost: number, metadata?: { __typename?: 'AccountUsageEASBuildMetadata', billingResourceClass?: EasBuildBillingResourceClass | null, platform?: AppPlatform | null } | null }> }, IOS_SIMULATOR_MINUTES: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }>, ANDROID_SIMULATOR_MINUTES: Array<{ __typename?: 'AccountUsageMetric', id: string, serviceMetric: EasServiceMetric, metricType: UsageMetricType, value: number }> } } } }; export type AccountUsageForOverageWarningQueryVariables = Exact<{ accountId: Scalars['String']['input']; diff --git a/packages/eas-cli/src/graphql/queries/AccountQuery.ts b/packages/eas-cli/src/graphql/queries/AccountQuery.ts index 69b99e2d9e..e3b2b3b3f2 100644 --- a/packages/eas-cli/src/graphql/queries/AccountQuery.ts +++ b/packages/eas-cli/src/graphql/queries/AccountQuery.ts @@ -148,6 +148,9 @@ export const AccountQuery = { ...UsageMetricTotalFragment } EAS_SIMULATOR: byBillingPeriod(date: $currentDate, service: SIMULATOR) { + unavailableServiceUsageMetrics { + service + } id ...UsageMetricTotalFragment } From 434d35f11ed538777d3cc8dbce192581fbdd4fb6 Mon Sep 17 00:00:00 2001 From: Philippe Loulidi <78689302+LipLoody@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:20:35 -0600 Subject: [PATCH 3/3] [eas-cli] Replace Simulator usage changelog entry with changeset --- .changeset/eas-simulator-usage.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eas-simulator-usage.md diff --git a/.changeset/eas-simulator-usage.md b/.changeset/eas-simulator-usage.md new file mode 100644 index 0000000000..aa8a455101 --- /dev/null +++ b/.changeset/eas-simulator-usage.md @@ -0,0 +1,5 @@ +--- +'eas-cli': minor +--- + +Show EAS Simulator minutes in `eas account:usage`.