Skip to content

feat(billing): single-source usage via main-process relay#2358

Open
k11kirky wants to merge 1 commit into
posthog-code/context-breakdown-fill-categoriesfrom
posthog-code/usage-monitor-relay
Open

feat(billing): single-source usage via main-process relay#2358
k11kirky wants to merge 1 commit into
posthog-code/context-breakdown-fill-categoriesfrom
posthog-code/usage-monitor-relay

Conversation

@k11kirky
Copy link
Copy Markdown
Contributor

@k11kirky k11kirky commented May 25, 2026

Problem

The renderer was polling the LLM gateway directly every 30 seconds to fetch usage data, coupling the renderer to the gateway and duplicating polling logic that the UsageMonitorService already handles.

Changes

The UsageMonitorService now caches the most recent usage snapshot (latestUsage) and emits a UsageUpdated event after every successful poll. Two new methods are exposed: getLatest() to retrieve the cached snapshot synchronously, and refreshNow() to trigger an immediate poll.

The usageMonitorRouter exposes these via three new tRPC endpoints: onUsageUpdated (a subscription that streams snapshots as they arrive), getLatest (a query to bootstrap the renderer before the first event), and refresh (a mutation to force an immediate poll). The llmGateway.usage query endpoint has been removed since it is no longer needed.

The useUsage hook in the renderer now subscribes to onUsageUpdated instead of polling on an interval. It bootstraps from getLatest on mount and exposes a refetch that calls the refresh mutation. The window-focus-based refetch interval logic has been removed entirely.

How did you test this?

Three new unit tests were added to service.test.ts:

  • Verifies that UsageUpdated is emitted and the snapshot is cached on every successful poll.
  • Verifies that UsageUpdated is not emitted and the snapshot remains null when the gateway throws.
  • Verifies that refreshNow triggers a fresh poll and returns the resulting snapshot.

Publish to changelog?

No

Copy link
Copy Markdown
Contributor Author

k11kirky commented May 25, 2026

@k11kirky k11kirky force-pushed the posthog-code/usage-monitor-relay branch from 3562601 to 4055770 Compare May 25, 2026 16:58
@k11kirky k11kirky force-pushed the posthog-code/context-breakdown-fill-categories branch from 923dacd to 08d0f9b Compare May 25, 2026 16:58
@k11kirky k11kirky force-pushed the posthog-code/context-breakdown-fill-categories branch from 08d0f9b to b7de4cf Compare May 26, 2026 09:11
@k11kirky k11kirky force-pushed the posthog-code/usage-monitor-relay branch from 4055770 to 49e01ad Compare May 26, 2026 09:11
@k11kirky k11kirky marked this pull request as ready for review May 26, 2026 09:19
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 26, 2026

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/code/src/main/services/usage-monitor/schemas.ts:24-25
The `UsageSnapshot` type alias is exported but never consumed — `service.ts` uses `UsageOutput | null` inline everywhere. The Zod schema `usageSnapshotOutput` is legitimately needed for tRPC output validation, but the TypeScript type alongside it is superfluous (simplicity rule #4).

```suggestion
export const usageSnapshotOutput = usageOutput.nullable();
```

### Issue 2 of 2
apps/code/src/renderer/features/billing/hooks/useUsage.ts:37
The entire `refreshMutation` object is listed as a `useCallback` dependency. `useMutation` returns a new object reference on every render (state fields like `isPending` change), so `refetch` is recreated every render. Depending on `refreshMutation.mutateAsync` directly is sufficient — that function reference is stable across renders.

```suggestion
  }, [refreshMutation.mutateAsync, queryClient, trpc.usageMonitor.getLatest]);
```

Reviews (1): Last reviewed commit: "feat(billing): single-source usage via m..." | Re-trigger Greptile

Comment on lines +24 to +25
export const usageSnapshotOutput = usageOutput.nullable();
export type UsageSnapshot = UsageOutput | null;
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 The UsageSnapshot type alias is exported but never consumed — service.ts uses UsageOutput | null inline everywhere. The Zod schema usageSnapshotOutput is legitimately needed for tRPC output validation, but the TypeScript type alongside it is superfluous (simplicity rule #4).

Suggested change
export const usageSnapshotOutput = usageOutput.nullable();
export type UsageSnapshot = UsageOutput | null;
export const usageSnapshotOutput = usageOutput.nullable();
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/services/usage-monitor/schemas.ts
Line: 24-25

Comment:
The `UsageSnapshot` type alias is exported but never consumed — `service.ts` uses `UsageOutput | null` inline everywhere. The Zod schema `usageSnapshotOutput` is legitimately needed for tRPC output validation, but the TypeScript type alongside it is superfluous (simplicity rule #4).

```suggestion
export const usageSnapshotOutput = usageOutput.nullable();
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

queryClient.setQueryData(trpc.usageMonitor.getLatest.queryKey(), fresh);
}
return fresh;
}, [refreshMutation, queryClient, trpc.usageMonitor.getLatest]);
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 The entire refreshMutation object is listed as a useCallback dependency. useMutation returns a new object reference on every render (state fields like isPending change), so refetch is recreated every render. Depending on refreshMutation.mutateAsync directly is sufficient — that function reference is stable across renders.

Suggested change
}, [refreshMutation, queryClient, trpc.usageMonitor.getLatest]);
}, [refreshMutation.mutateAsync, queryClient, trpc.usageMonitor.getLatest]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/billing/hooks/useUsage.ts
Line: 37

Comment:
The entire `refreshMutation` object is listed as a `useCallback` dependency. `useMutation` returns a new object reference on every render (state fields like `isPending` change), so `refetch` is recreated every render. Depending on `refreshMutation.mutateAsync` directly is sufficient — that function reference is stable across renders.

```suggestion
  }, [refreshMutation.mutateAsync, queryClient, trpc.usageMonitor.getLatest]);
```

How can I resolve this? If you propose a fix, please make it concise.

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