Skip to content

Commit da8302f

Browse files
committed
add dedicated usage page in sidebar
1 parent d391b25 commit da8302f

19 files changed

Lines changed: 576 additions & 223 deletions

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { describe, expect, it } from "vitest";
2-
import { formatTokens } from "./spendAnalysisFormat";
2+
import {
3+
formatTokens,
4+
type SpendAnalysisWindow,
5+
windowToDateFrom,
6+
windowToDays,
7+
} from "./spendAnalysisFormat";
38

49
describe("formatTokens", () => {
510
it.each([
@@ -15,3 +20,23 @@ describe("formatTokens", () => {
1520
expect(formatTokens(input)).toBe(expected);
1621
});
1722
});
23+
24+
describe("windowToDateFrom", () => {
25+
it.each<[SpendAnalysisWindow, string]>([
26+
["7d", "-7d"],
27+
["30d", "-30d"],
28+
["90d", "-90d"],
29+
])("maps %s to %s", (window, expected) => {
30+
expect(windowToDateFrom(window)).toBe(expected);
31+
});
32+
});
33+
34+
describe("windowToDays", () => {
35+
it.each<[SpendAnalysisWindow, number]>([
36+
["7d", 7],
37+
["30d", 30],
38+
["90d", 90],
39+
])("maps %s to %d", (window, expected) => {
40+
expect(windowToDays(window)).toBe(expected);
41+
});
42+
});

packages/core/src/billing/spendAnalysisFormat.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ export function formatTokens(n: number): string {
1212
return n.toString();
1313
}
1414

15+
export type SpendAnalysisWindow = "7d" | "30d" | "90d";
16+
17+
export function windowToDateFrom(window: SpendAnalysisWindow): string {
18+
return `-${window}`;
19+
}
20+
21+
export function windowToDays(window: SpendAnalysisWindow): number {
22+
return Number.parseInt(window, 10);
23+
}
24+
1525
export function windowDays(fromIso: string, toIso: string): number {
1626
const fromMs = new Date(fromIso).getTime();
1727
const toMs = new Date(toIso).getTime();

packages/shared/src/analytics-events.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,15 @@ export interface InboxReportScrolledProperties {
626626
time_since_open_ms: number;
627627
}
628628

629+
export interface UsageViewedProperties {
630+
is_pro: boolean;
631+
/** Monthly bucket percent (0-100), null when usage is unavailable. */
632+
sustained_used_percent: number | null;
633+
/** Daily bucket percent (0-100), null when usage is unavailable. */
634+
burst_used_percent: number | null;
635+
spend_analysis_window_days: number;
636+
}
637+
629638
export interface SpendAnalysisTaskOpenedProperties {
630639
/** Total LLM spend in USD across all products for the analysed window. */
631640
total_cost_usd: number;
@@ -1129,7 +1138,8 @@ export const ANALYTICS_EVENTS = {
11291138
SCOUT_CHAT_STARTED: "Scout chat started",
11301139
SCOUT_ACTION: "Scout action",
11311140

1132-
// Spend analysis events
1141+
// Usage and spend analysis events
1142+
USAGE_VIEWED: "Usage viewed",
11331143
SPEND_ANALYSIS_TASK_OPENED: "Spend analysis task opened",
11341144

11351145
// Prompt history events
@@ -1281,7 +1291,8 @@ export type EventPropertyMap = {
12811291
[ANALYTICS_EVENTS.SCOUT_CHAT_STARTED]: ScoutChatStartedProperties;
12821292
[ANALYTICS_EVENTS.SCOUT_ACTION]: ScoutActionProperties;
12831293

1284-
// Spend analysis events
1294+
// Usage and spend analysis events
1295+
[ANALYTICS_EVENTS.USAGE_VIEWED]: UsageViewedProperties;
12851296
[ANALYTICS_EVENTS.SPEND_ANALYSIS_TASK_OPENED]: SpendAnalysisTaskOpenedProperties;
12861297

12871298
// Prompt history events

packages/shared/src/flags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const BILLING_FLAG = "posthog-code-billing";
2+
export const SPEND_ANALYSIS_FLAG = "posthog-code-spend-analysis";
23
export const EXPERIMENT_SUGGESTIONS_FLAG =
34
"posthog-code-experiment-suggestions";
45
export const SYNC_CLOUD_TASKS_FLAG = "posthog-code-sync-cloud-tasks";
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { formatResetTime } from "@posthog/core/billing/usageDisplay";
2+
import type { UsageBucket } from "@posthog/core/usage/schemas";
3+
import { Flex, Progress, Text } from "@radix-ui/themes";
4+
5+
interface UsageMeterProps {
6+
label: string;
7+
bucket: UsageBucket;
8+
color?: "red";
9+
}
10+
11+
export function UsageMeter({ label, bucket, color }: UsageMeterProps) {
12+
const percentage = bucket.used_percent;
13+
14+
const borderColor = color === "red" ? "var(--red-7)" : "var(--gray-5)";
15+
16+
return (
17+
<Flex
18+
direction="column"
19+
gap="3"
20+
p="4"
21+
style={{
22+
border: `1px solid ${borderColor}`,
23+
}}
24+
className="rounded-(--radius-3)"
25+
>
26+
<Flex align="center" justify="between">
27+
<Text className="font-medium text-sm">{label}</Text>
28+
<Text className="font-medium text-sm">{percentage.toFixed(2)}%</Text>
29+
</Flex>
30+
<Progress
31+
value={percentage}
32+
size="2"
33+
color={color === "red" ? "red" : undefined}
34+
/>
35+
<Text className="text-(--gray-9) text-[13px]">
36+
{bucket.exceeded ? "Limit exceeded" : formatResetTime(bucket.reset_at)}
37+
</Text>
38+
</Flex>
39+
);
40+
}

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

Lines changed: 0 additions & 50 deletions
This file was deleted.

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

Lines changed: 22 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,18 @@ import {
44
Info,
55
WarningCircle,
66
} from "@phosphor-icons/react";
7-
import {
8-
formatResetTime,
9-
PRO_USAGE_MULTIPLIER,
10-
} from "@posthog/core/billing/usageDisplay";
11-
import type { UsageBucket } from "@posthog/core/usage/schemas";
7+
import { PRO_USAGE_MULTIPLIER } from "@posthog/core/billing/usageDisplay";
128
import { PLAN_PRO_ALPHA } from "@posthog/shared";
139
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
1410
import { useAuthStateValue } from "@posthog/ui/features/auth/store";
1511
import { useSwitchOrgMutation } from "@posthog/ui/features/auth/useAuthMutations";
1612
import { useSeatStore } from "@posthog/ui/features/billing/seatStore";
17-
import { TokenSpendAnalysisBanner } from "@posthog/ui/features/billing/TokenSpendAnalysisBanner";
13+
import { UsageMeter } from "@posthog/ui/features/billing/UsageMeter";
1814
import { useSeat } from "@posthog/ui/features/billing/useSeat";
1915
import { useUsage } from "@posthog/ui/features/billing/useUsage";
20-
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
16+
import { closeSettings } from "@posthog/ui/features/settings/hooks/useOpenSettings";
17+
import { useSpendAnalysisEnabled } from "@posthog/ui/features/usage/useSpendAnalysisEnabled";
18+
import { navigateToUsage } from "@posthog/ui/router/navigationBridge";
2119
import { track } from "@posthog/ui/shell/analytics";
2220
import { logger } from "@posthog/ui/shell/logger";
2321
import { getBillingUrl, getPostHogUrl } from "@posthog/ui/utils/urls";
@@ -27,16 +25,13 @@ import {
2725
Callout,
2826
Dialog,
2927
Flex,
30-
Progress,
3128
Spinner,
3229
Text,
3330
} from "@radix-ui/themes";
3431
import { useEffect, useState } from "react";
3532

3633
const log = logger.scope("plan-usage");
3734

38-
const SPEND_ANALYSIS_FLAG = "posthog-code-spend-analysis";
39-
4035
export function PlanUsageSettings() {
4136
const {
4237
seat,
@@ -85,8 +80,7 @@ export function PlanUsageSettings() {
8580
? (getPostHogUrl(redirectUrl, cloudRegion) ?? billingUrl)
8681
: null;
8782
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
88-
const spendAnalysisEnabled =
89-
useFeatureFlag(SPEND_ANALYSIS_FLAG) || import.meta.env.DEV;
83+
const spendAnalysisEnabled = useSpendAnalysisEnabled();
9084

9185
const isAlpha = orgSeat?.plan_key === PLAN_PRO_ALPHA;
9286
const {
@@ -185,8 +179,6 @@ export function PlanUsageSettings() {
185179
</Callout.Root>
186180
)}
187181

188-
{spendAnalysisEnabled && <TokenSpendAnalysisBanner />}
189-
190182
{hasBetterPlanElsewhere && seat?.organization_name && (
191183
<Callout.Root color="blue" size="1">
192184
<Callout.Icon>
@@ -330,7 +322,22 @@ export function PlanUsageSettings() {
330322
)}
331323

332324
<Flex direction="column" gap="3">
333-
<Text className="font-medium text-(--gray-9) text-sm">Usage</Text>
325+
<Flex align="center" justify="between">
326+
<Text className="font-medium text-(--gray-9) text-sm">Usage</Text>
327+
{spendAnalysisEnabled && (
328+
<Button
329+
size="1"
330+
variant="ghost"
331+
onClick={() => {
332+
// Leave settings first so back from /usage skips the settings route.
333+
closeSettings();
334+
navigateToUsage();
335+
}}
336+
>
337+
View usage & spend analysis
338+
</Button>
339+
)}
340+
</Flex>
334341
{usageLoading ? (
335342
<Flex
336343
align="center"
@@ -448,43 +455,6 @@ export function PlanUsageSettings() {
448455
);
449456
}
450457

451-
interface UsageMeterProps {
452-
label: string;
453-
bucket: UsageBucket;
454-
color?: "red";
455-
}
456-
457-
function UsageMeter({ label, bucket, color }: UsageMeterProps) {
458-
const percentage = bucket.used_percent;
459-
460-
const borderColor = color === "red" ? "var(--red-7)" : "var(--gray-5)";
461-
462-
return (
463-
<Flex
464-
direction="column"
465-
gap="3"
466-
p="4"
467-
style={{
468-
border: `1px solid ${borderColor}`,
469-
}}
470-
className="rounded-(--radius-3)"
471-
>
472-
<Flex align="center" justify="between">
473-
<Text className="font-medium text-sm">{label}</Text>
474-
<Text className="font-medium text-sm">{percentage.toFixed(2)}%</Text>
475-
</Flex>
476-
<Progress
477-
value={percentage}
478-
size="2"
479-
color={color === "red" ? "red" : undefined}
480-
/>
481-
<Text className="text-(--gray-9) text-[13px]">
482-
{bucket.exceeded ? "Limit exceeded" : formatResetTime(bucket.reset_at)}
483-
</Text>
484-
</Flex>
485-
);
486-
}
487-
488458
interface PlanCardProps {
489459
name: string;
490460
price: string;

packages/ui/src/features/sidebar/components/SidebarNavSection.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFla
44
import { useInboxAllReports } from "@posthog/ui/features/inbox/hooks/useInboxAllReports";
55
import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore";
66
import { useTasks } from "@posthog/ui/features/tasks/useTasks";
7+
import { useSpendAnalysisEnabled } from "@posthog/ui/features/usage/useSpendAnalysisEnabled";
78
import {
89
navigateToAgents,
910
navigateToCommandCenter,
1011
navigateToHome,
1112
navigateToInbox,
1213
navigateToMcpServers,
1314
navigateToSkills,
15+
navigateToUsage,
1416
navigateToWebsiteCommandCenter,
1517
navigateToWebsiteHome,
1618
navigateToWebsiteMcpServers,
@@ -29,6 +31,7 @@ import { McpServersItem } from "./items/McpServersItem";
2931
import { NewTaskItem } from "./items/NewTaskItem";
3032
import { SearchItem } from "./items/SearchItem";
3133
import { SkillsItem } from "./items/SkillsItem";
34+
import { UsageItem } from "./items/UsageItem";
3235

3336
const SIDEBAR_INBOX_REFETCH_INTERVAL_MS = 60_000;
3437

@@ -46,18 +49,19 @@ interface SidebarNavSectionProps {
4649
// state, badge count, and click handler is wired here — so it can be dropped
4750
// into either layout. In the Channels space, destinations with a /website
4851
// mirror (Home, Skills, MCP servers, Command Center) stay in that space;
49-
// Inbox, Agents and New task have no mirror yet and jump back to Code. Search
50-
// opens the command menu in place.
52+
// Inbox, Agents, Usage and New task have no mirror yet and jump back to Code.
53+
// Search opens the command menu in place.
5154
export function SidebarNavSection({
5255
commandCenterActiveCount: providedActiveCount,
5356
}: SidebarNavSectionProps = {}) {
5457
const view = useAppView();
5558
const homeTabEnabled = useFeatureFlag(HOME_TAB_FLAG);
59+
const usageEnabled = useSpendAnalysisEnabled();
5660

5761
// When this section renders inside the Channels space, the destinations that
5862
// have a /website mirror stay in that space; everything else (and the whole
59-
// section in the Code space) uses the canonical routes. Inbox, Agents and New
60-
// task have no mirror yet, so they intentionally jump back to Code.
63+
// section in the Code space) uses the canonical routes. Inbox, Agents, Usage
64+
// and New task have no mirror yet, so they intentionally jump back to Code.
6165
const inChannels = useRouterState({
6266
select: (s) => s.location.pathname.startsWith("/website"),
6367
});
@@ -82,6 +86,7 @@ export function SidebarNavSection({
8286
const isCommandCenterActive = view.type === "command-center";
8387
const isSkillsActive = view.type === "skills";
8488
const isMcpServersActive = view.type === "mcp-servers";
89+
const isUsageActive = view.type === "usage";
8590

8691
// Open pull requests in the inbox — the main CTA, and the same count the inbox
8792
// Pull requests tab shows, so the badge and the tab always agree.
@@ -152,6 +157,12 @@ export function SidebarNavSection({
152157
<McpServersItem isActive={isMcpServersActive} onClick={goMcpServers} />
153158
</Box>
154159

160+
{usageEnabled && (
161+
<Box>
162+
<UsageItem isActive={isUsageActive} onClick={navigateToUsage} />
163+
</Box>
164+
)}
165+
155166
<Box mb="2">
156167
<CommandCenterItem
157168
isActive={isCommandCenterActive}

0 commit comments

Comments
 (0)