feat(usage): Add dedicated usage page with spend analysis#3197
feat(usage): Add dedicated usage page with spend analysis#3197charlesvien wants to merge 7 commits into
Conversation
|
React Doctor found 1 issue in 1 file · 1 warning. 1 warning
Reviewed by React Doctor for commit |
|
Reviews (1): Last reviewed commit: "move usage item below command center" | Re-trigger Greptile |
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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); | |
| } |
| 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]); | ||
| } |
There was a problem hiding this comment.
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.
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
/usage, opened from a sidebar item below the command centerTokenSpendAnalysisBanner) into the new pageUsageMeterextracted 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)How did you test this?
pnpm --filter @posthog/core test: 200 files, 2128 tests passpnpm typecheck: 22/22 tasks passpnpm lint: clean, no new warnings in touched packagesAutomatic notifications