Skip to content

feat(usage): Add dedicated usage page with spend analysis#3197

Closed
charlesvien wants to merge 7 commits into
chore/sync-claude-adapter-v0.54.1from
feat/usage-page
Closed

feat(usage): Add dedicated usage page with spend analysis#3197
charlesvien wants to merge 7 commits into
chore/sync-claude-adapter-v0.54.1from
feat/usage-page

Conversation

@charlesvien

Copy link
Copy Markdown
Member

Stacked on #3076.

Problem

Usage and spend data only exists as a banner buried in plan settings. There is no dedicated place to see spend over time, per-model costs or how close you are to plan limits.

Changes

  • New Usage page at /usage, opened from a sidebar item below the command center
  • Daily spend chart, KPI strip, model breakdown cards, spend tables and insights, with a time window selector
  • Spend analysis moves out of the settings banner (TokenSpendAnalysisBanner) into the new page
  • UsageMeter extracted to its own component and reused by plan settings, keeping the reset time visible when a limit is exceeded (fix(usage): Show reset time when limit is exceeded #3190)
  • Gated by the spend analysis flag, with analytics events for page views

How did you test this?

  • pnpm --filter @posthog/core test: 200 files, 2128 tests pass
  • pnpm typecheck: 22/22 tasks pass
  • pnpm lint: clean, no new warnings in touched packages

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

React Doctor found 1 issue in 1 file · 1 warning.

1 warning

src/features/usage/UsageView.tsx

Reviewed by React Doctor for commit 2a503cb.

@charlesvien charlesvien closed this Jul 6, 2026
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "move usage item below command center" | Re-trigger Greptile

Comment on lines +36 to +45
try {
return await client.getPersonalSpendAnalysis({
dateFrom: windowToDateFrom(window),
product,
});
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
log.warn("Failed to fetch spend analysis", { error: message });
throw err;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 If client.getPersonalSpendAnalysis throws a non-Error value (e.g., a rejected promise with a plain string from a lower-level networking layer), the catch block re-throws the original value unchanged. query.error instanceof Error then evaluates to false, so error in the return maps to null. The result: isLoading is false, data is null, and error is null — the UI silently shows nothing instead of the error callout with the retry button.

Suggested change
try {
return await client.getPersonalSpendAnalysis({
dateFrom: windowToDateFrom(window),
product,
});
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
log.warn("Failed to fetch spend analysis", { error: message });
throw err;
}
try {
return await client.getPersonalSpendAnalysis({
dateFrom: windowToDateFrom(window),
product,
});
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
log.warn("Failed to fetch spend analysis", { error: message });
throw err instanceof Error ? err : new Error(message);
}

Comment on lines +12 to +27
export function useTrackUsageViewed(input: TrackUsageViewedInput): void {
const { isLoading, isPro, sustainedUsedPercent, burstUsedPercent } = input;

const firedRef = useRef(false);
useEffect(() => {
if (firedRef.current) return;
// Wait for data to settle so the once-only event doesn't lock in defaults.
if (isLoading) return;
firedRef.current = true;
track(ANALYTICS_EVENTS.USAGE_VIEWED, {
is_pro: isPro,
sustained_used_percent: sustainedUsedPercent,
burst_used_percent: burstUsedPercent,
});
}, [isLoading, isPro, sustainedUsedPercent, burstUsedPercent]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Analytics event can fire before billing flag resolves

isLoading is computed as billingEnabled && (seatLoading || usageLoading). If the billing feature flag hasn't resolved yet — billingEnabled is still false — the expression short-circuits to false immediately, so useEffect skips the loading guard and fires USAGE_VIEWED on the first render with isPro: false and null percentages. Because firedRef then prevents a second fire, the event records stale defaults even after the flag and seat data resolve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant