Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 9755063

Browse files
authored
fix(billing): clarify org usage vs personal spend on Plan & usage
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
1 parent 1082a7c commit 9755063

5 files changed

Lines changed: 58 additions & 16 deletions

File tree

packages/core/src/billing/usageDisplay.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,27 @@ describe("formatResetTime", () => {
186186
expected: "Resets shortly",
187187
},
188188
])("$name", ({ resetAt, expected }) => {
189-
const result = formatResetTime(resetAt, NOW);
189+
const result = formatResetTime(resetAt, { now: NOW });
190190
if (expected instanceof RegExp) {
191191
expect(result).toMatch(expected);
192192
} else {
193193
expect(result).toBe(expected);
194194
}
195195
});
196+
197+
it("swaps the leading phrase when a custom label is given", () => {
198+
const opts = { now: NOW, label: "Billing period ends" };
199+
expect(formatResetTime(isoAt(30 * 60 * 1000), opts)).toBe(
200+
"Billing period ends in 30m",
201+
);
202+
expect(formatResetTime(isoAt(4 * 3600 * 1000), opts)).toBe(
203+
"Billing period ends in 4h",
204+
);
205+
expect(formatResetTime(isoAt(-60_000), opts)).toBe(
206+
"Billing period ends shortly",
207+
);
208+
expect(formatResetTime(isoAt(30 * 86400 * 1000), opts)).toMatch(
209+
/^Billing period ends [A-Za-z]+ \d+ at /,
210+
);
211+
});
196212
});

packages/core/src/billing/usageDisplay.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,22 @@ export function isUsageExceeded(usage: UsageOutput): boolean {
5757
);
5858
}
5959

60+
/**
61+
* Renders a countdown to `resetAtIso` as a short phrase. `label` is the leading
62+
* verb phrase (default "Resets") so callers pick wording that matches what the
63+
* timestamp actually is — e.g. "Billing period ends" for an org's
64+
* `billing_period_end`, "Resets" for a per-user free-tier valve window.
65+
*/
6066
export function formatResetTime(
6167
resetAtIso: string,
62-
now: number = Date.now(),
68+
{ now = Date.now(), label = "Resets" }: { now?: number; label?: string } = {},
6369
): string {
6470
const parsed = Date.parse(resetAtIso);
6571
const ms = Number.isNaN(parsed) ? 0 : Math.max(0, parsed - now);
6672

6773
const totalMinutes = Math.ceil(ms / 60_000);
68-
if (totalMinutes <= 0) return "Resets shortly";
69-
if (totalMinutes < 60) return `Resets in ${totalMinutes}m`;
74+
if (totalMinutes <= 0) return `${label} shortly`;
75+
if (totalMinutes < 60) return `${label} in ${totalMinutes}m`;
7076

7177
const totalHours = ms / 3_600_000;
7278
if (totalHours < 24) {
@@ -77,8 +83,8 @@ export function formatResetTime(
7783
minutes = 0;
7884
}
7985
return minutes === 0
80-
? `Resets in ${hours}h`
81-
: `Resets in ${hours}h ${minutes}m`;
86+
? `${label} in ${hours}h`
87+
: `${label} in ${hours}h ${minutes}m`;
8288
}
8389

8490
const target = new Date(now + ms);
@@ -91,5 +97,5 @@ export function formatResetTime(
9197
minute: "2-digit",
9298
timeZoneName: "short",
9399
});
94-
return `Resets ${date} at ${time}`;
100+
return `${label} ${date} at ${time}`;
95101
}

packages/ui/src/features/billing/UsageButton.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,10 @@ export function UsageButton() {
6767
meter.kind === "dollars"
6868
? `${formatUsdAmount(meter.usedUsd)} of ${formatUsdAmount(meter.limitUsd)} used`
6969
: `${percent}% used`;
70-
const resetLabel = formatResetTime(
71-
meter.kind === "dollars" ? meter.resetAt : meter.bucket.reset_at,
72-
);
70+
const resetLabel =
71+
meter.kind === "dollars"
72+
? formatResetTime(meter.resetAt, { label: "Billing period ends" })
73+
: formatResetTime(meter.bucket.reset_at);
7374

7475
// Upgrade-prompt analytics only apply to free-tier orgs — a subscribed
7576
// org's meter is not an upgrade prompt.

packages/ui/src/features/settings/sections/PlanUsageSettings.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export function PlanUsageSettings() {
6666
const subscribed = usage?.code_usage_subscribed === true;
6767
const orgLimitReached = usage?.ai_credits?.exhausted === true;
6868
const meter = codeUsageMeter(usage);
69+
// The dollars meter tracks the whole organization's usage-based spend; only
70+
// the free-tier valve bucket is a per-user allowance. Spell out which scope
71+
// is on screen so this bar isn't misread as personal usage like the spend
72+
// analysis (which really is just the signed-in user) further down the page.
73+
const usageIsPerUser = meter.kind === "bucket";
6974

7075
const openBilling = () => {
7176
if (billingUrl) window.open(billingUrl, "_blank");
@@ -163,7 +168,16 @@ export function PlanUsageSettings() {
163168
</Flex>
164169

165170
<Flex direction="column" gap="3">
166-
<Text className="font-medium text-(--gray-9) text-sm">Usage</Text>
171+
<Flex direction="column" gap="1">
172+
<Text className="font-medium text-(--gray-9) text-sm">
173+
{usageIsPerUser ? "Your usage" : "Organization usage"}
174+
</Text>
175+
<Text className="text-(--gray-11) text-[13px]">
176+
{usageIsPerUser
177+
? "Your personal free-tier allowance for this period."
178+
: "Usage-based billing shared across your whole organization."}
179+
</Text>
180+
</Flex>
167181
{usageLoading ? (
168182
<Flex
169183
align="center"
@@ -178,7 +192,7 @@ export function PlanUsageSettings() {
178192
label={freeTier ? "Monthly free usage" : "Usage this period"}
179193
percent={meter.percent}
180194
valueLabel={`${formatUsdAmount(meter.usedUsd)} of ${formatUsdAmount(meter.limitUsd)}${freeTier ? " included" : ""}`}
181-
detail={`${meter.exceeded ? "Limit exceeded. " : ""}${formatResetTime(meter.resetAt)}`}
195+
detail={`${meter.exceeded ? "Limit exceeded. " : ""}${formatResetTime(meter.resetAt, { label: "Billing period ends" })}`}
182196
color={meter.exceeded ? "red" : undefined}
183197
/>
184198
) : meter.kind === "bucket" ? (

packages/ui/src/features/usage/components/SpendAnalysisSection.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,15 @@ export function SpendAnalysisSection() {
3636

3737
return (
3838
<Flex direction="column" gap="3">
39-
<Flex align="center" justify="between">
40-
<Text className="font-medium text-(--gray-9) text-sm">
41-
Spend analysis
42-
</Text>
39+
<Flex align="start" justify="between" gap="4">
40+
<Flex direction="column" gap="1">
41+
<Text className="font-medium text-(--gray-9) text-sm">
42+
Your spend analysis
43+
</Text>
44+
<Text className="text-(--gray-11) text-[13px]">
45+
Just your own usage, not your whole organization's.
46+
</Text>
47+
</Flex>
4348
<Flex align="center" gap="4">
4449
<WindowSelector value={spendWindow} onChange={setSpendWindow} />
4550
<Button

0 commit comments

Comments
 (0)