From 5eb5adb6ea0d9694addff44385a7b54617d53d61 Mon Sep 17 00:00:00 2001 From: Adam Bowker Date: Thu, 16 Jul 2026 10:01:01 -0400 Subject: [PATCH 1/4] fix(billing): clarify org usage vs personal spend on Plan & usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Plan & usage page showed an org-level usage meter directly above a user-level spend analysis with no cue that the two have different scopes, so the spend numbers could read as the whole organization's. - Label the usage meter "Organization usage" with a caption noting it's shared across the whole organization (and "Your usage" for the per-user free-tier valve fallback). - Rename "Spend analysis" to "Your spend analysis" with a caption noting it covers only the signed-in user's usage. - Reword the org billing-period countdown from "Resets …" to "Billing period ends …" via an optional label on formatResetTime; kept "Resets" for the per-user free-tier valve windows, which really do reset. Generated-By: PostHog Code Task-Id: d69678da-415c-450b-aafe-3d834d0bc557 --- packages/core/src/billing/usageDisplay.test.ts | 18 +++++++++++++++++- packages/core/src/billing/usageDisplay.ts | 12 ++++++------ .../ui/src/features/billing/UsageButton.tsx | 7 ++++--- .../settings/sections/PlanUsageSettings.tsx | 14 ++++++++++++-- .../usage/components/SpendAnalysisSection.tsx | 13 +++++++++---- 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/core/src/billing/usageDisplay.test.ts b/packages/core/src/billing/usageDisplay.test.ts index 7ae0c2c050..5509f845e8 100644 --- a/packages/core/src/billing/usageDisplay.test.ts +++ b/packages/core/src/billing/usageDisplay.test.ts @@ -264,11 +264,27 @@ describe("formatResetTime", () => { expected: "Resets shortly", }, ])("$name", ({ resetAt, expected }) => { - const result = formatResetTime(resetAt, NOW); + const result = formatResetTime(resetAt, { now: NOW }); if (expected instanceof RegExp) { expect(result).toMatch(expected); } else { expect(result).toBe(expected); } }); + + it("swaps the leading phrase when a custom label is given", () => { + const opts = { now: NOW, label: "Billing period ends" }; + expect(formatResetTime(isoAt(30 * 60 * 1000), opts)).toBe( + "Billing period ends in 30m", + ); + expect(formatResetTime(isoAt(4 * 3600 * 1000), opts)).toBe( + "Billing period ends in 4h", + ); + expect(formatResetTime(isoAt(-60_000), opts)).toBe( + "Billing period ends shortly", + ); + expect(formatResetTime(isoAt(30 * 86400 * 1000), opts)).toMatch( + /^Billing period ends [A-Za-z]+ \d+ at /, + ); + }); }); diff --git a/packages/core/src/billing/usageDisplay.ts b/packages/core/src/billing/usageDisplay.ts index 1207cf9d91..2dd67e05ce 100644 --- a/packages/core/src/billing/usageDisplay.ts +++ b/packages/core/src/billing/usageDisplay.ts @@ -88,14 +88,14 @@ export function isUsageExceeded(usage: UsageOutput): boolean { export function formatResetTime( resetAtIso: string, - now: number = Date.now(), + { now = Date.now(), label = "Resets" }: { now?: number; label?: string } = {}, ): string { const parsed = Date.parse(resetAtIso); const ms = Number.isNaN(parsed) ? 0 : Math.max(0, parsed - now); const totalMinutes = Math.ceil(ms / 60_000); - if (totalMinutes <= 0) return "Resets shortly"; - if (totalMinutes < 60) return `Resets in ${totalMinutes}m`; + if (totalMinutes <= 0) return `${label} shortly`; + if (totalMinutes < 60) return `${label} in ${totalMinutes}m`; const totalHours = ms / 3_600_000; if (totalHours < 24) { @@ -106,8 +106,8 @@ export function formatResetTime( minutes = 0; } return minutes === 0 - ? `Resets in ${hours}h` - : `Resets in ${hours}h ${minutes}m`; + ? `${label} in ${hours}h` + : `${label} in ${hours}h ${minutes}m`; } const target = new Date(now + ms); @@ -120,5 +120,5 @@ export function formatResetTime( minute: "2-digit", timeZoneName: "short", }); - return `Resets ${date} at ${time}`; + return `${label} ${date} at ${time}`; } diff --git a/packages/ui/src/features/billing/UsageButton.tsx b/packages/ui/src/features/billing/UsageButton.tsx index f6fce7457e..1a4432b4a1 100644 --- a/packages/ui/src/features/billing/UsageButton.tsx +++ b/packages/ui/src/features/billing/UsageButton.tsx @@ -68,9 +68,10 @@ export function UsageButton() { meter.kind === "dollars" ? `${formatUsdAmount(meter.usedUsd)} of ${formatUsdAmount(meter.limitUsd)} used` : `${percent}% used`; - const resetLabel = formatResetTime( - meter.kind === "dollars" ? meter.resetAt : meter.bucket.reset_at, - ); + const resetLabel = + meter.kind === "dollars" + ? formatResetTime(meter.resetAt, { label: "Billing period ends" }) + : formatResetTime(meter.bucket.reset_at); const breakdownLabel = meter.kind === "dollars" && meter.breakdown ? formatUsageBreakdown(meter.breakdown) diff --git a/packages/ui/src/features/settings/sections/PlanUsageSettings.tsx b/packages/ui/src/features/settings/sections/PlanUsageSettings.tsx index e794e5afd1..e3f1374e5b 100644 --- a/packages/ui/src/features/settings/sections/PlanUsageSettings.tsx +++ b/packages/ui/src/features/settings/sections/PlanUsageSettings.tsx @@ -66,6 +66,7 @@ export function PlanUsageSettings() { const subscribed = usage?.code_usage_subscribed === true; const orgLimitReached = usage?.ai_credits?.exhausted === true; const meter = codeUsageMeter(usage); + const usageIsPerUser = meter.kind === "bucket"; const openBilling = () => { if (billingUrl) window.open(billingUrl, "_blank"); @@ -163,7 +164,16 @@ export function PlanUsageSettings() { - Usage + + + {usageIsPerUser ? "Your usage" : "Organization usage"} + + + {usageIsPerUser + ? "Your personal free-tier allowance for this period." + : "Usage-based billing shared across your whole organization."} + + {usageLoading ? ( - - - Spend analysis - + + + + Your spend analysis + + + Just your own usage, not your whole organization's. + +