Skip to content

Commit a24f438

Browse files
committed
fix(billing): give the threshold toasts the cutover treatment
The threshold-crossing toasts were the one seat-era billing surface the stack never touched: they fired on raw valve percentages regardless of subscription state and spoke in "your monthly limit" language. The monitor now emits thresholds only for confirmed free-tier orgs (a subscribed org's valves are internal rails; unknown never reads as free), the copy says "monthly/daily free usage", and the 100% path opens the modal with the org_limit cause so it renders "Free usage used up". Generated-By: PostHog Code Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
1 parent dc90acd commit a24f438

3 files changed

Lines changed: 39 additions & 5 deletions

File tree

packages/core/src/usage/usage-monitor.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ function makeUsage(overrides?: {
7575
user_id: 42,
7676
is_rate_limited: false,
7777
is_pro: overrides?.isPro ?? false,
78+
code_usage_subscribed: false,
7879
billing_period_end:
7980
overrides?.billingPeriodEnd === undefined
8081
? null
@@ -246,6 +247,32 @@ describe("UsageMonitorService", () => {
246247
expect(updates[1].burst.used_percent).toBe(35);
247248
});
248249

250+
it.each([
251+
["subscribed", true],
252+
["unknown", undefined],
253+
] as const)(
254+
"does not emit thresholds for a %s org's internal valves",
255+
async (_name, subscribed) => {
256+
const usage = {
257+
...makeUsage({ sustainedPercent: 90 }),
258+
code_usage_subscribed: subscribed,
259+
};
260+
service = makeService(mockGateway(usage), makeActivityMonitor());
261+
const thresholds: unknown[] = [];
262+
const updates: unknown[] = [];
263+
service.on(UsageMonitorEvent.ThresholdCrossed, (e) =>
264+
thresholds.push(e),
265+
);
266+
service.on(UsageMonitorEvent.UsageUpdated, (u) => updates.push(u));
267+
268+
await service.fetchOnce();
269+
270+
expect(thresholds).toHaveLength(0);
271+
// The snapshot still flows to the meters.
272+
expect(updates).toHaveLength(1);
273+
},
274+
);
275+
249276
// The titlebar meter keys off this bit; a subscribe flip must not wait for
250277
// some other field to change.
251278
it("emits UsageUpdated when only the subscription bit flips", async () => {

packages/core/src/usage/usage-monitor.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ROOT_LOGGER, type RootLogger } from "@posthog/di/logger";
22
import { TypedEventEmitter } from "@posthog/shared";
33
import { inject, injectable, postConstruct, preDestroy } from "inversify";
4+
import { isCodeUsageFreeTier } from "../billing/usageDisplay";
45
import { USAGE_HOST, type UsageHost, type UsageLogger } from "./identifiers";
56
import {
67
USAGE_THRESHOLDS,
@@ -125,6 +126,9 @@ export class UsageMonitorService extends TypedEventEmitter<UsageMonitorEvents> {
125126
}
126127

127128
private processUsage(usage: UsageOutput): void {
129+
// Valve thresholds are a free-tier concept — a subscribed org's valves
130+
// are internal rails, and unknown must never read as free.
131+
if (!isCodeUsageFreeTier(usage)) return;
128132
const userId = usage.user_id.toString();
129133
const product = usage.product;
130134
this.maybeEmit(usage, "burst", usage.burst, userId, product, usage.is_pro);

packages/ui/src/features/billing/billing.contribution.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,25 @@ export class BillingContribution implements Contribution {
2828
onData: (event) => {
2929
const resetLabel = formatResetTime(event.resetAt);
3030

31+
// The monitor only emits thresholds for confirmed free-tier orgs, so
32+
// the free-usage framing (and the org_limit cause) is always right.
3133
if (event.threshold === 100) {
3234
if (event.userIsActive) {
33-
useUsageLimitStore.getState().show({ resetAt: event.resetAt });
35+
useUsageLimitStore
36+
.getState()
37+
.show({ resetAt: event.resetAt, cause: "org_limit" });
3438
return;
3539
}
36-
toast.error("Usage limit reached", {
40+
toast.error("Free usage used up", {
3741
id: `usage-threshold-${event.bucket}-100`,
3842
description: resetLabel,
3943
});
4044
return;
4145
}
4246

43-
const limitName =
44-
event.bucket === "burst" ? "daily limit" : "monthly limit";
47+
const period = event.bucket === "burst" ? "daily" : "monthly";
4548
toast.warning(
46-
`You've used ${Math.round(event.usedPercent)}% of your ${limitName}`,
49+
`You've used ${Math.round(event.usedPercent)}% of your ${period} free usage`,
4750
{
4851
id: `usage-threshold-${event.bucket}-${event.threshold}`,
4952
description: resetLabel,

0 commit comments

Comments
 (0)