From 60dc52973833417b672159770c56c20d5ba77be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Tue, 2 Jun 2026 07:49:57 +0000 Subject: [PATCH 1/2] fix(actors info): handle tiered pay-per-event pricing The pricing renderer assumed eventPriceUsd was always a number, but tiered pay-per-event Actors (e.g. lukaskrivka/google-maps-with-contact-details) ship eventTieredPricingUsd instead, so calling .toFixed(2) on undefined crashed `actors info` with exit 1 for these Store Actors. --input / --readme / --json were unaffected because they bypass the pricing renderer. Replace the local PricingInfo interface with apify-client's ActorRunPricingInfo discriminated union, augment ActorChargeEvent locally for the tiered field the SDK doesn't expose yet, and render `from \$X (tiered)` for tiered events. Add an e2e regression test against the actor from the bug report. Fixes #1171 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/commands/actors/info.ts | 44 +++++++++++++++------------ test/e2e/commands/actors/info.test.ts | 15 +++++++++ 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/commands/actors/info.ts b/src/commands/actors/info.ts index 8f4e36701..819e56f67 100644 --- a/src/commands/actors/info.ts +++ b/src/commands/actors/info.ts @@ -1,4 +1,4 @@ -import type { Actor, ActorTaggedBuild, Build, User } from 'apify-client'; +import type { Actor, ActorChargeEvent, ActorTaggedBuild, Build, User } from 'apify-client'; import chalk from 'chalk'; import { ApifyCommand } from '../../lib/command-framework/apify-command.js'; @@ -14,20 +14,13 @@ interface HydratedActorInfo extends Omit { actorMaker?: User; } -interface PricingInfo { - pricingModel: 'PRICE_PER_DATASET_ITEM' | 'FLAT_PRICE_PER_MONTH' | 'PAY_PER_EVENT' | 'FREE'; - pricePerUnitUsd: number; - unitName: string; - startedAt: string; - createdAt: string; - apifyMarginPercentage: number; - notifiedAboutFutureChangeAt: string; - notifiedAboutChangeAt: string; - trialMinutes?: number; - pricingPerEvent?: { - actorChargeEvents: Record; - }; -} +// apify-client's ActorChargeEvent declares eventPriceUsd as required and is missing +// eventTieredPricingUsd. Tiered pay-per-event pricing shipped on the platform after the SDK type +// was last updated; remodel locally until upstream catches up: https://github.com/apify/apify-client-js +type ChargeEventShape = Omit & { + eventPriceUsd?: number; + eventTieredPricingUsd?: Record; +}; const eventTitleColumn = '\u200b'; const eventPriceUsdColumn = '\u200b\u200b'; @@ -210,7 +203,7 @@ export class ActorsInfoCommand extends ApifyCommand { } // Pricing info - const pricingInfo = Reflect.get(actorInfo, 'pricingInfos') as PricingInfo[] | undefined; + const pricingInfo = actorInfo.pricingInfos; if (pricingInfo?.length) { // We only print the latest pricing info @@ -245,10 +238,21 @@ export class ActorsInfoCommand extends ApifyCommand { const events = Object.values(latestPricingInfo.pricingPerEvent?.actorChargeEvents ?? {}); - for (const eventInfo of events) { + for (const eventInfo of events as ChargeEventShape[]) { + const flat = eventInfo.eventPriceUsd; + const tiered = eventInfo.eventTieredPricingUsd; + let priceLabel: string; + if (typeof flat === 'number') { + priceLabel = `$${flat.toFixed(2)}`; + } else if (tiered && Object.keys(tiered).length > 0) { + const minPrice = Math.min(...Object.values(tiered).map((t) => t.tieredEventPriceUsd)); + priceLabel = `from $${minPrice.toFixed(2)} (tiered)`; + } else { + priceLabel = 'N/A'; + } payPerEventTable.pushRow({ [eventTitleColumn]: eventInfo.eventTitle, - [eventPriceUsdColumn]: chalk.bold(`$${eventInfo.eventPriceUsd.toFixed(2)}`), + [eventPriceUsdColumn]: chalk.bold(priceLabel), }); } @@ -269,8 +273,10 @@ export class ActorsInfoCommand extends ApifyCommand { } default: { + // Runtime fallback for pricing models the SDK union doesn't know about yet. + const unknownModel = (latestPricingInfo as { pricingModel: string }).pricingModel; message.push( - `${chalk.yellow('Pricing information:')} ${chalk.bgGray(`Unknown pricing model (${chalk.yellow(latestPricingInfo.pricingModel)})`)}`, + `${chalk.yellow('Pricing information:')} ${chalk.bgGray(`Unknown pricing model (${chalk.yellow(unknownModel)})`)}`, ); } } diff --git a/test/e2e/commands/actors/info.test.ts b/test/e2e/commands/actors/info.test.ts index 5bc688ee5..30a42d5d5 100644 --- a/test/e2e/commands/actors/info.test.ts +++ b/test/e2e/commands/actors/info.test.ts @@ -45,4 +45,19 @@ describe('[e2e][api] actors info', () => { expect(result.stdout).toContain('was not found'); }); + + // Regression test for https://github.com/apify/apify-cli/issues/1171 + // The human-readable renderer crashed with "Cannot read properties of undefined (reading 'toFixed')" + // on PAY_PER_EVENT actors that use tiered pricing (eventTieredPricingUsd) instead of a flat + // eventPriceUsd. --json bypasses the renderer, so the bug only surfaces on the default output. + it('renders pricing for a tiered PAY_PER_EVENT actor without crashing', async () => { + const result = await runCli('apify', ['actors', 'info', 'lukaskrivka/google-maps-with-contact-details'], { + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}\nstdout: ${result.stdout}`).toBe(0); + expect(result.stderr).not.toContain('toFixed'); + expect(result.stdout).toContain('Pricing information'); + expect(result.stdout).toContain('Pay per event'); + }); }); From 6432a2e7b04eab63dd0ed075784d0205a300347d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Tue, 2 Jun 2026 10:35:10 +0000 Subject: [PATCH 2/2] fix(actors info): show sub-cent event prices instead of rounding to $0.00 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most tiered pay-per-event Actors charge sub-cent prices (e.g. $0.005, $0.00079). toFixed(2) rounded these to "$0.00", so the renderer printed "from $0.00 (tiered)" — making real costs look free. Same problem for flat sub-cent eventPriceUsd, not just tiered. Use 2 decimals when the price is >= $0.01, otherwise 2 significant figures: $0.005 -> "$0.005", $0.00079 -> "$0.00079", $0.25 -> "$0.25". Refs #1171 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/commands/actors/info.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/commands/actors/info.ts b/src/commands/actors/info.ts index 819e56f67..51c845436 100644 --- a/src/commands/actors/info.ts +++ b/src/commands/actors/info.ts @@ -22,6 +22,14 @@ type ChargeEventShape = Omit & { eventTieredPricingUsd?: Record; }; +// Event prices are routinely sub-cent (e.g. $0.005, $0.00079) so plain toFixed(2) rounds them to +// "$0.00" and hides the real cost. Use 2 decimals at >= $0.01, otherwise 2 significant figures. +function formatEventPrice(price: number): string { + if (price === 0) return '$0.00'; + if (price >= 0.01) return `$${price.toFixed(2)}`; + return `$${Number(price.toPrecision(2))}`; +} + const eventTitleColumn = '\u200b'; const eventPriceUsdColumn = '\u200b\u200b'; @@ -243,10 +251,10 @@ export class ActorsInfoCommand extends ApifyCommand { const tiered = eventInfo.eventTieredPricingUsd; let priceLabel: string; if (typeof flat === 'number') { - priceLabel = `$${flat.toFixed(2)}`; + priceLabel = formatEventPrice(flat); } else if (tiered && Object.keys(tiered).length > 0) { const minPrice = Math.min(...Object.values(tiered).map((t) => t.tieredEventPriceUsd)); - priceLabel = `from $${minPrice.toFixed(2)} (tiered)`; + priceLabel = `from ${formatEventPrice(minPrice)} (tiered)`; } else { priceLabel = 'N/A'; }