Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions apps/api/autumn.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,16 @@ export const startup = plan({
},
},
{
// BETA (2026-08-17): free and unlimited while product events are in
// beta. Usage is still metered by the gateway and tracked in Autumn so
// we know real volumes before pricing it. To start charging, replace
// `unlimited` with e.g. `included: 1_000_000` and a
// `price: { amount: 0.05, billingUnits: 1000, billingMethod:
// "usage_based", interval: "month" }` ($0.05 per 1,000 events).
// $5 per 1M events past 1M. Billed per 1,000 because Autumn rounds
// overage up to whole blocks; a 1M block would charge $5 for one event.
featureId: "product_events",
unlimited: true,
included: 1_000_000,
price: {
amount: 0.005,
billingUnits: 1000,
billingMethod: "usage_based",
interval: "month",
},
},
{
// One credit is one dollar of model spend after markup.
Expand Down
10 changes: 6 additions & 4 deletions apps/landing/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,14 @@
"pe_point_1_body": "Set maple.product_event.name on a span and it becomes an event, in any language. The event keeps a link to the trace that ran it.",
"pe_point_2_title": "One person, start to finish",
"pe_point_2_body": "An anonymous visit to your site joins the user it signs up as. Funnels count people, not cookies.",
"pe_point_3_title": "Free while in beta",
"pe_point_3_body": "Product events are unlimited during the beta and kept for 365 days. Automatic page views are not counted.",
"pe_point_3_title": "1M events free every month",
"pe_point_3_body": "Then $5 per million. Automatic page views are never counted, and every event is kept for 365 days.",
"pe_link_feature": "Product events →",
"pe_link_docs": "Product events docs",
"nav_product_events": "Product Events",
"nav_desc_product_events": "Funnels, drop-off and paths on your events",
"feat_pe_seo_title": "Product Analytics and Funnels | Maple",
"feat_pe_seo_desc": "Track product events from the browser, your backend or any OpenTelemetry span. Build funnels, drop-off and paths next to your traces and metrics. Unlimited while in beta.",
"feat_pe_seo_desc": "Track product events from the browser, your backend or any OpenTelemetry span. Build funnels, drop-off and paths next to your traces and metrics. 1M events a month included, then $5 per million.",
"feat_pe_hero_title": "Product analytics, next to the trace",
"feat_pe_hero_lede": "Page views arrive on their own. Track the rest from the browser, your backend or a span you already emit, and see where people stop on the same dashboards as your latency.",
"feat_pe_artifact_title": "Find the step where people stop",
Expand Down Expand Up @@ -802,7 +802,9 @@
"faq_metrics_q": "Do metrics cost extra?",
"faq_metrics_a": "No. Metrics are metered by volume like logs and traces: {included} GB included, then {rate} per GB. There is no per-series, per-sample or custom-metric surcharge.",
"faq_sessions_q": "How are browser sessions and product events billed?",
"faq_sessions_a": "A browser session is one visitor session recorded by the browser SDK, replay included. {included} are included each month, then {rate} per session. Product events are unlimited and free while they are in beta.",
"faq_sessions_a": "A browser session is one visitor session recorded by the browser SDK, replay included. {included} are included each month, then {rate} per session.",
"faq_events_priced": "Product events are your track() calls, /v1/events posts and annotated spans: {included} are included each month, then {rate} per million. Automatic page views are not counted.",
"faq_events_beta": "Product events are unlimited and free while they are in beta.",
"faq_spend_q": "Can I cap what I spend?",
"faq_spend_a": "Yes. Set an overage cap per signal in the billing settings, and usage alerts at the thresholds you choose. When a signal reaches its cap, ingest for that signal is rejected until the billing cycle resets. Caps and alerts run on the same usage your invoice is built from.",
"stack_eyebrow": "What it works with",
Expand Down
6 changes: 3 additions & 3 deletions apps/landing/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
"pe_point_1_body": "\u30b9\u30d1\u30f3\u306b maple.product_event.name \u3092\u8a2d\u5b9a\u3059\u308b\u3060\u3051\u3067\u30a4\u30d9\u30f3\u30c8\u306b\u306a\u308a\u307e\u3059\u3002\u8a00\u8a9e\u306f\u554f\u3044\u307e\u305b\u3093\u3002\u30a4\u30d9\u30f3\u30c8\u306f\u5b9f\u884c\u3057\u305f\u30c8\u30ec\u30fc\u30b9\u3078\u306e\u30ea\u30f3\u30af\u3092\u4fdd\u6301\u3057\u307e\u3059\u3002",
"pe_point_2_title": "\u6700\u521d\u304b\u3089\u6700\u5f8c\u307e\u3067\u540c\u3058\u4eba",
"pe_point_2_body": "\u30b5\u30a4\u30c8\u3078\u306e\u533f\u540d\u306e\u8a2a\u554f\u306f\u3001\u30b5\u30a4\u30f3\u30a2\u30c3\u30d7\u5f8c\u306e\u30e6\u30fc\u30b6\u30fc\u306b\u7d10\u3065\u304d\u307e\u3059\u3002\u30d5\u30a1\u30cd\u30eb\u306f Cookie \u3067\u306f\u306a\u304f\u4eba\u3092\u6570\u3048\u307e\u3059\u3002",
"pe_point_3_title": "\u30d9\u30fc\u30bf\u671f\u9593\u4e2d\u306f\u7121\u6599",
"pe_point_3_body": "\u30d7\u30ed\u30c0\u30af\u30c8\u30a4\u30d9\u30f3\u30c8\u306f\u30d9\u30fc\u30bf\u671f\u9593\u4e2d\u306f\u7121\u5236\u9650\u3067\u3001365 \u65e5\u9593\u4fdd\u5b58\u3055\u308c\u307e\u3059\u3002\u81ea\u52d5\u306e\u30da\u30fc\u30b8\u30d3\u30e5\u30fc\u306f\u30ab\u30a6\u30f3\u30c8\u3055\u308c\u307e\u305b\u3093\u3002",
"pe_point_3_title": "\u6bce\u6708 100 \u4e07\u30a4\u30d9\u30f3\u30c8\u307e\u3067\u7121\u6599",
"pe_point_3_body": "\u8d85\u904e\u5206\u306f 100 \u4e07\u30a4\u30d9\u30f3\u30c8\u3042\u305f\u308a $5\u3002\u81ea\u52d5\u306e\u30da\u30fc\u30b8\u30d3\u30e5\u30fc\u306f\u30ab\u30a6\u30f3\u30c8\u3055\u308c\u305a\u3001\u3059\u3079\u3066\u306e\u30a4\u30d9\u30f3\u30c8\u306f 365 \u65e5\u9593\u4fdd\u5b58\u3055\u308c\u307e\u3059\u3002",
"pe_link_feature": "\u30d7\u30ed\u30c0\u30af\u30c8\u30a4\u30d9\u30f3\u30c8 \u2192",
"pe_link_docs": "\u30d7\u30ed\u30c0\u30af\u30c8\u30a4\u30d9\u30f3\u30c8\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8",
"nav_product_events": "\u30d7\u30ed\u30c0\u30af\u30c8\u30a4\u30d9\u30f3\u30c8",
"nav_desc_product_events": "\u30a4\u30d9\u30f3\u30c8\u306e\u30d5\u30a1\u30cd\u30eb\u3001\u96e2\u8131\u3001\u30d1\u30b9",
"feat_pe_seo_title": "\u30d7\u30ed\u30c0\u30af\u30c8\u5206\u6790\u3068\u30d5\u30a1\u30cd\u30eb | Maple",
"feat_pe_seo_desc": "\u30d6\u30e9\u30a6\u30b6\u3001\u30d0\u30c3\u30af\u30a8\u30f3\u30c9\u3001\u307e\u305f\u306f\u4efb\u610f\u306e OpenTelemetry \u30b9\u30d1\u30f3\u304b\u3089\u30d7\u30ed\u30c0\u30af\u30c8\u30a4\u30d9\u30f3\u30c8\u3092\u8a08\u6e2c\u3002\u30c8\u30ec\u30fc\u30b9\u3084\u30e1\u30c8\u30ea\u30af\u30b9\u306e\u96a3\u3067\u30d5\u30a1\u30cd\u30eb\u3001\u96e2\u8131\u3001\u30d1\u30b9\u3092\u69cb\u7bc9\u3067\u304d\u307e\u3059\u3002\u30d9\u30fc\u30bf\u671f\u9593\u4e2d\u306f\u7121\u5236\u9650\u3067\u3059\u3002",
"feat_pe_seo_desc": "\u30d6\u30e9\u30a6\u30b6\u3001\u30d0\u30c3\u30af\u30a8\u30f3\u30c9\u3001\u307e\u305f\u306f\u4efb\u610f\u306e OpenTelemetry \u30b9\u30d1\u30f3\u304b\u3089\u30d7\u30ed\u30c0\u30af\u30c8\u30a4\u30d9\u30f3\u30c8\u3092\u8a08\u6e2c\u3002\u30c8\u30ec\u30fc\u30b9\u3084\u30e1\u30c8\u30ea\u30af\u30b9\u306e\u96a3\u3067\u30d5\u30a1\u30cd\u30eb\u3001\u96e2\u8131\u3001\u30d1\u30b9\u3092\u69cb\u7bc9\u3067\u304d\u307e\u3059\u3002\u6bce\u6708 100 \u4e07\u30a4\u30d9\u30f3\u30c8\u307e\u3067\u542b\u307e\u308c\u3001\u8d85\u904e\u5206\u306f 100 \u4e07\u30a4\u30d9\u30f3\u30c8\u3042\u305f\u308a $5 \u3067\u3059\u3002",
"feat_pe_hero_title": "\u30c8\u30ec\u30fc\u30b9\u306e\u96a3\u306b\u30d7\u30ed\u30c0\u30af\u30c8\u5206\u6790\u3092",
"feat_pe_hero_lede": "\u30da\u30fc\u30b8\u30d3\u30e5\u30fc\u306f\u81ea\u52d5\u3067\u8a18\u9332\u3055\u308c\u307e\u3059\u3002\u305d\u308c\u4ee5\u5916\u306f\u30d6\u30e9\u30a6\u30b6\u3001\u30d0\u30c3\u30af\u30a8\u30f3\u30c9\u3001\u307e\u305f\u306f\u65e2\u5b58\u306e\u30b9\u30d1\u30f3\u304b\u3089\u8a08\u6e2c\u3057\u3001\u30e6\u30fc\u30b6\u30fc\u304c\u96e2\u8131\u3059\u308b\u5834\u6240\u3092\u30ec\u30a4\u30c6\u30f3\u30b7\u3068\u540c\u3058\u30c0\u30c3\u30b7\u30e5\u30dc\u30fc\u30c9\u3067\u78ba\u8a8d\u3067\u304d\u307e\u3059\u3002",
"feat_pe_artifact_title": "\u30e6\u30fc\u30b6\u30fc\u304c\u96e2\u8131\u3059\u308b\u30b9\u30c6\u30c3\u30d7\u3092\u898b\u3064\u3051\u308b",
Expand Down
6 changes: 3 additions & 3 deletions apps/landing/messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
"pe_point_1_body": "\uc2a4\ud32c\uc5d0 maple.product_event.name\uc744 \uc124\uc815\ud558\uba74 \uc774\ubca4\ud2b8\uac00 \ub429\ub2c8\ub2e4. \uc5b8\uc5b4\ub294 \uc0c1\uad00\uc5c6\uc2b5\ub2c8\ub2e4. \uc774\ubca4\ud2b8\ub294 \uc2e4\ud589\ub41c \ud2b8\ub808\uc774\uc2a4\ub85c\uc758 \ub9c1\ud06c\ub97c \uc720\uc9c0\ud569\ub2c8\ub2e4.",
"pe_point_2_title": "\ucc98\uc74c\ubd80\ud130 \ub05d\uae4c\uc9c0 \ud55c \uc0ac\ub78c",
"pe_point_2_body": "\uc0ac\uc774\ud2b8\uc758 \uc775\uba85 \ubc29\ubb38\uc740 \uac00\uc785\ud55c \uc0ac\uc6a9\uc790\uc640 \uc5f0\uacb0\ub429\ub2c8\ub2e4. \ud37c\ub110\uc740 \ucfe0\ud0a4\uac00 \uc544\ub2c8\ub77c \uc0ac\ub78c\uc744 \uc149\ub2c8\ub2e4.",
"pe_point_3_title": "\ubca0\ud0c0 \uae30\uac04 \ubb34\ub8cc",
"pe_point_3_body": "\ud504\ub85c\ub355\ud2b8 \uc774\ubca4\ud2b8\ub294 \ubca0\ud0c0 \uae30\uac04 \ub3d9\uc548 \ubb34\uc81c\ud55c\uc774\uba70 365\uc77c\uac04 \ubcf4\uad00\ub429\ub2c8\ub2e4. \uc790\ub3d9 \ud398\uc774\uc9c0\ubdf0\ub294 \uc9d1\uacc4\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
"pe_point_3_title": "\ub9e4\uc6d4 100\ub9cc \uc774\ubca4\ud2b8 \ubb34\ub8cc",
"pe_point_3_body": "\ucd08\uacfc\ubd84\uc740 100\ub9cc \uc774\ubca4\ud2b8\ub2f9 $5\uc785\ub2c8\ub2e4. \uc790\ub3d9 \ud398\uc774\uc9c0\ubdf0\ub294 \uc9d1\uacc4\ub418\uc9c0 \uc54a\uc73c\uba70 \ubaa8\ub4e0 \uc774\ubca4\ud2b8\ub294 365\uc77c\uac04 \ubcf4\uad00\ub429\ub2c8\ub2e4.",
"pe_link_feature": "\ud504\ub85c\ub355\ud2b8 \uc774\ubca4\ud2b8 \u2192",
"pe_link_docs": "\ud504\ub85c\ub355\ud2b8 \uc774\ubca4\ud2b8 \ubb38\uc11c",
"nav_product_events": "\ud504\ub85c\ub355\ud2b8 \uc774\ubca4\ud2b8",
"nav_desc_product_events": "\uc774\ubca4\ud2b8 \uae30\ubc18 \ud37c\ub110, \uc774\ud0c8, \uacbd\ub85c",
"feat_pe_seo_title": "\ud504\ub85c\ub355\ud2b8 \ubd84\uc11d\uacfc \ud37c\ub110 | Maple",
"feat_pe_seo_desc": "\ube0c\ub77c\uc6b0\uc800, \ubc31\uc5d4\ub4dc \ub610\ub294 \ubaa8\ub4e0 OpenTelemetry \uc2a4\ud32c\uc5d0\uc11c \ud504\ub85c\ub355\ud2b8 \uc774\ubca4\ud2b8\ub97c \uc218\uc9d1\ud558\uc138\uc694. \ud2b8\ub808\uc774\uc2a4\uc640 \uba54\ud2b8\ub9ad \uc606\uc5d0\uc11c \ud37c\ub110, \uc774\ud0c8, \uacbd\ub85c\ub97c \ub9cc\ub4e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubca0\ud0c0 \uae30\uac04 \ubb34\uc81c\ud55c.",
"feat_pe_seo_desc": "\ube0c\ub77c\uc6b0\uc800, \ubc31\uc5d4\ub4dc \ub610\ub294 \ubaa8\ub4e0 OpenTelemetry \uc2a4\ud32c\uc5d0\uc11c \ud504\ub85c\ub355\ud2b8 \uc774\ubca4\ud2b8\ub97c \uc218\uc9d1\ud558\uc138\uc694. \ud2b8\ub808\uc774\uc2a4\uc640 \uba54\ud2b8\ub9ad \uc606\uc5d0\uc11c \ud37c\ub110, \uc774\ud0c8, \uacbd\ub85c\ub97c \ub9cc\ub4e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub9e4\uc6d4 100\ub9cc \uc774\ubca4\ud2b8 \ud3ec\ud568, \ucd08\uacfc\ubd84\uc740 100\ub9cc \uc774\ubca4\ud2b8\ub2f9 $5.",
"feat_pe_hero_title": "\ud2b8\ub808\uc774\uc2a4 \uc606\uc758 \ud504\ub85c\ub355\ud2b8 \ubd84\uc11d",
"feat_pe_hero_lede": "\ud398\uc774\uc9c0\ubdf0\ub294 \uc790\ub3d9\uc73c\ub85c \uae30\ub85d\ub429\ub2c8\ub2e4. \ub098\uba38\uc9c0\ub294 \ube0c\ub77c\uc6b0\uc800, \ubc31\uc5d4\ub4dc \ub610\ub294 \uc774\ubbf8 \ub0b4\ubcf4\ub0b4\ub294 \uc2a4\ud32c\uc5d0\uc11c \uc218\uc9d1\ud558\uace0, \uc0ac\uc6a9\uc790\uac00 \uba48\ucd94\ub294 \uacf3\uc744 \ub808\uc774\ud134\uc2dc\uc640 \uac19\uc740 \ub300\uc2dc\ubcf4\ub4dc\uc5d0\uc11c \ud655\uc778\ud558\uc138\uc694.",
"feat_pe_artifact_title": "\uc0ac\uc6a9\uc790\uac00 \uba48\ucd94\ub294 \ub2e8\uacc4 \ucc3e\uae30",
Expand Down
10 changes: 8 additions & 2 deletions apps/landing/src/lib/pricing-faq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* resolves the locale per render (see `pricing-offer.ts`).
*/
import * as m from "../paraglide/messages.js"
import { getOffer, rateLabel } from "./pricing-offer"
import { getOffer, money, rateLabel, volume } from "./pricing-offer"

export interface FaqItem {
question: string
Expand All @@ -27,6 +27,12 @@ export async function pricingFaq(): Promise<FaqItem[]> {
const gbRate = rateLabel(gb?.rate ?? 0)
const includedSessions = (sessions?.included ?? 0).toLocaleString("en-US")
const sessionRate = rateLabel(sessions?.rate ?? 0)
// Quote an events rate only when the catalog prices them; an unlimited item has none.
const events = offer.allotments.find((a) => a.unit === "events")
Comment thread
maple-review-bot[bot] marked this conversation as resolved.
const eventsAnswer =
events === undefined || events.unlimited || events.rate === undefined
? m.faq_events_beta()
: m.faq_events_priced({ included: volume(events, events.included), rate: money(events.rate) })

return [
{ question: m.faq_measure_q(), answer: m.faq_measure_a({ included: includedGB }) },
Expand All @@ -35,7 +41,7 @@ export async function pricingFaq(): Promise<FaqItem[]> {
{ question: m.faq_metrics_q(), answer: m.faq_metrics_a({ included: includedGB, rate: gbRate }) },
{
question: m.faq_sessions_q(),
answer: m.faq_sessions_a({ included: includedSessions, rate: sessionRate }),
answer: `${m.faq_sessions_a({ included: includedSessions, rate: sessionRate })} ${eventsAnswer}`,
},
{ question: m.faq_retention_q(), answer: m.faq_retention_a() },
{ question: m.faq_trial_q(), answer: m.faq_trial_a() },
Expand Down
42 changes: 24 additions & 18 deletions apps/landing/src/lib/pricing-offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ export interface Allotment {
/** Price per `rateUnits` units once `included` is used up. */
rate?: number
/**
* Units the rate is quoted per: 1 for GB and sessions, 1,000 for events.
* Autumn's `billingUnits`; the rate is the price of one such block.
* Units the rate is quoted per: 1 for GB and sessions, 1M for events. Autumn's
* `billingUnits` except for events, which bill per 1,000 but are quoted per
* million. The rate is the price of one such block.
*/
rateUnits: number
}
Expand Down Expand Up @@ -73,6 +74,9 @@ const hasOwnUnit = (featureId: string): featureId is keyof typeof FEATURE_UNITS

const unitFor = (featureId: string): Unit => (hasOwnUnit(featureId) ? FEATURE_UNITS[featureId] : "gb")

/** "$5 / 1M events" reads better than the billed "$0.005 / 1,000 events". */
const EVENTS_QUOTE_UNITS = 1_000_000

/**
* Capitalized, localized labels for the metered rows, keyed by Autumn featureId
* (Autumn returns lowercase names like "logs").
Expand Down Expand Up @@ -141,14 +145,18 @@ export async function getOffer(): Promise<Offer> {
.filter((item) => item.featureId && !HIDDEN_FEATURE_IDS.has(item.featureId))
.map((item) => {
const featureId = item.featureId
const unit = unitFor(featureId)
const billingUnits = Math.max(1, item.price?.billingUnits ?? 1)
const rateUnits = unit === "events" ? EVENTS_QUOTE_UNITS : billingUnits
const amount = item.price?.amount
return {
featureId,
label: labels[featureId] ?? item.feature?.name ?? featureId,
unit: unitFor(featureId),
unit,
included: Number(item.included ?? 0),
unlimited: item.unlimited === true,
rate: item.price?.amount ?? undefined,
rateUnits: Math.max(1, item.price?.billingUnits ?? 1),
rate: amount == null ? undefined : (amount / billingUnits) * rateUnits,
rateUnits,
}
})
.sort((a, b) => dataFeatureRank(a.featureId) - dataFeatureRank(b.featureId)),
Expand Down Expand Up @@ -197,10 +205,10 @@ export async function getOffer(): Promise<Offer> {
featureId: "product_events",
label: labels.product_events!,
unit: "events",
// Free and unlimited during beta; mirrors `apps/api/autumn.config.ts`.
included: 0,
unlimited: true,
rateUnits: 1,
// Mirrors `apps/api/autumn.config.ts`: $0.005 per 1,000, quoted per 1M.
included: 1_000_000,
rate: 5,
rateUnits: EVENTS_QUOTE_UNITS,
},
{
featureId: "ai_credits",
Expand Down Expand Up @@ -229,21 +237,19 @@ export const money = (n: number) => {
/** Sub-cent rates (per session) need three places; per-GB rates need two. */
export const rateLabel = (n: number) => `$${n < 0.01 ? n.toFixed(3) : n.toFixed(2)}`

/** Whole millions as "1M"; anything else exact. */
const count = (n: number) =>
n >= 1_000_000 && n % 1_000_000 === 0 ? `${n / 1_000_000}M` : n.toLocaleString("en-US")

/**
* The block a rate is quoted per, for the "then {rate} / …" sentence: 1 for
* GB and sessions, "1,000" for events. Localized unit words come from the
* GB and sessions, "1M" for events. Localized unit words come from the
* message catalog; this only supplies the number.
*/
export const rateBlock = (a: Allotment) => a.rateUnits.toLocaleString("en-US")
export const rateBlock = (a: Allotment) => count(a.rateUnits)

export const volume = (a: Allotment, n: number) =>
a.unlimited
? m.pricing_unlimited()
: a.unit === "gb"
? `${n} GB`
: a.unit === "usd"
? money(n)
: n.toLocaleString("en-US")
a.unlimited ? m.pricing_unlimited() : a.unit === "gb" ? `${n} GB` : a.unit === "usd" ? money(n) : count(n)

/**
* AI usage has no flat rate: a credit is a dollar of model spend, priced per
Expand Down
2 changes: 1 addition & 1 deletion apps/landing/src/pages/llms.txt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const GET: APIRoute = ({ site }) => {

"## Pricing",
"",
"One plan: $39 a month with 100 GB each of logs, traces and metrics included, then $0.30 per GB per signal; 5,000 browser sessions included, then $0.002 per session; product events free during beta.",
"One plan: $39 a month with 100 GB each of logs, traces and metrics included, then $0.30 per GB per signal; 5,000 browser sessions included, then $0.002 per session; 1M product events included, then $5 per million (page views not counted).",
"",
...both("Pricing", "/pricing"),
"",
Expand Down
17 changes: 15 additions & 2 deletions apps/web/src/components/settings/pricing-cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Result, useAtomRefresh, useAtomValue } from "@/lib/effect-atom"
import { billingCustomerAtom, billingPlansAtom } from "@/lib/services/atoms/billing-atoms"
import { useBillingActions } from "@/hooks/use-billing-actions"
import { showErrorToast } from "@/lib/error-toast"
import { formatCount } from "@/lib/billing/usage"
import { formatCount, formatQuoteUnits, quoteUnits } from "@/lib/billing/usage"
import { useTimezonePreference } from "@/hooks/use-timezone-preference"
import { formatDateInTimezone } from "@/lib/timezone-format"
import { getTrialStatus } from "@/lib/billing/plan-gating"
Expand Down Expand Up @@ -131,6 +131,19 @@ function normalizeDetailText(text: string): string {
return text.replace(/\bper\s+(?:[\d,]+\s+)?(?:logs?|traces?|metrics?)\b/i, "per GB")
}

// Autumn words the rate per billing block ("$0.005 per 1,000"); events are quoted per million, in its style.
function formatDetail(item: PlanItem): string | undefined {
const amount = item.price?.amount
const billingUnits = item.price?.billingUnits ?? 1
const unit = item.featureId ? COUNT_UNITS[item.featureId] : undefined
const quote = item.featureId ? quoteUnits(item.featureId, billingUnits) : billingUnits
if (amount != null && unit !== undefined && quote !== billingUnits && billingUnits > 0) {
const perQuote = (amount / billingUnits) * quote
return `then $${Number(perQuote.toPrecision(6))} per ${formatQuoteUnits(quote)} ${unit}`
}
return item.display?.secondaryText ? normalizeDetailText(item.display.secondaryText) : undefined
}

function getFeatureRows(plan: Plan) {
return plan.items
.filter((item) => item.featureId && !HIDDEN_FEATURE_IDS.has(item.featureId))
Expand All @@ -141,7 +154,7 @@ function getFeatureRows(plan: Plan) {
item.feature?.name ??
item.featureId,
value: formatIncludedUsage(item),
detail: item.display?.secondaryText ? normalizeDetailText(item.display.secondaryText) : undefined,
detail: formatDetail(item),
}))
}

Expand Down
Loading
Loading