Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ import { DEV_HOST_ACTIONS_SERVICE } from "@posthog/platform/dev-host-actions";
import { DIALOG_SERVICE } from "@posthog/platform/dialog";
import { FILE_ICON_SERVICE } from "@posthog/platform/file-icon";
import { IMAGE_PROCESSOR_SERVICE } from "@posthog/platform/image-processor";
import { MAIN_WINDOW_SERVICE } from "@posthog/platform/main-window";
import {
type IMainWindow,
MAIN_WINDOW_SERVICE,
} from "@posthog/platform/main-window";
import { NOTIFIER_SERVICE } from "@posthog/platform/notifier";
import { POWER_MANAGER_SERVICE } from "@posthog/platform/power-manager";
import { SECURE_STORAGE_SERVICE } from "@posthog/platform/secure-storage";
Expand Down Expand Up @@ -680,6 +683,8 @@ container.bind(MAIN_UPDATES_SERVICE).toService(UPDATES_SERVICE);
container.load(usageMonitorModule);
container.bind(USAGE_HOST).toDynamicValue((ctx) => {
const agent = () => ctx.get<AgentService>(AGENT_SERVICE);
const mainWindow = () => ctx.get<IMainWindow>(MAIN_WINDOW_SERVICE);
const windowFocusUnsubscribers = new WeakMap<() => void, () => void>();
return {
fetchUsage: () =>
ctx.get<LlmGatewayService>(MAIN_LLM_GATEWAY_SERVICE).fetchUsage(),
Expand All @@ -688,6 +693,10 @@ container.bind(USAGE_HOST).toDynamicValue((ctx) => {
offLlmActivity: (listener: () => void) =>
agent().off(AgentServiceEvent.LlmActivity, listener),
hasActiveSessions: () => agent().hasActiveSessions(),
onWindowFocus: (listener: () => void) =>
windowFocusUnsubscribers.set(listener, mainWindow().onFocus(listener)),
offWindowFocus: (listener: () => void) =>
windowFocusUnsubscribers.get(listener)?.(),
getThresholdsSeen: () => electronUsageThresholdStore.getThresholdsSeen(),
setThresholdsSeen: (value: Record<string, string>) =>
electronUsageThresholdStore.setThresholdsSeen(value),
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/usage/identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ export interface UsageHost {
offLlmActivity(listener: () => void): void;
hasActiveSessions(): boolean;

// Refreshes usage as soon as the user looks at the window again — the only
// in-app signal is LLM activity, so usage consumed elsewhere (the website,
// another device, a teammate) would otherwise sit stale for up to the
// 30-minute backstop while the window was unfocused.
onWindowFocus(listener: () => void): void;
offWindowFocus(listener: () => void): void;

getThresholdsSeen(): Record<string, string>;
setThresholdsSeen(value: Record<string, string>): void;
}
Expand Down
41 changes: 40 additions & 1 deletion packages/core/src/usage/usage-monitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,35 @@ import { UsageMonitorService } from "./usage-monitor";

type ActivitySlice = Pick<
UsageHost,
"onLlmActivity" | "offLlmActivity" | "hasActiveSessions"
| "onLlmActivity"
| "offLlmActivity"
| "hasActiveSessions"
| "onWindowFocus"
| "offWindowFocus"
>;

interface MockActivityMonitor extends ActivitySlice {
fireLlmActivity(): void;
fireWindowFocus(): void;
}

function makeActivityMonitor(opts?: {
hasActiveSessions?: boolean;
}): MockActivityMonitor {
const listeners = new Set<() => void>();
const focusListeners = new Set<() => void>();
return {
onLlmActivity: (l) => listeners.add(l),
offLlmActivity: (l) => listeners.delete(l),
hasActiveSessions: () => opts?.hasActiveSessions ?? false,
onWindowFocus: (l) => focusListeners.add(l),
offWindowFocus: (l) => focusListeners.delete(l),
fireLlmActivity: () => {
for (const l of [...listeners]) l();
},
fireWindowFocus: () => {
for (const l of [...focusListeners]) l();
},
};
}

Expand Down Expand Up @@ -432,4 +443,32 @@ describe("UsageMonitorService", () => {
await vi.advanceTimersByTimeAsync(10_000);
expect(gateway.fetchUsage).toHaveBeenCalledTimes(baseline);
});

it("refreshes when the window regains focus, not just on LLM activity", async () => {
const gateway = mockGateway(makeUsage({ burstPercent: 10 }));
const agent = makeActivityMonitor();
service = makeService(gateway, agent);
service.init();
await vi.advanceTimersByTimeAsync(0);
expect(gateway.fetchUsage).toHaveBeenCalledTimes(1);

agent.fireWindowFocus();
await vi.advanceTimersByTimeAsync(5_000);
expect(gateway.fetchUsage).toHaveBeenCalledTimes(2);
});

it("unsubscribes from window focus events on stop()", async () => {
const gateway = mockGateway(makeUsage({ burstPercent: 10 }));
const agent = makeActivityMonitor();
service = makeService(gateway, agent);
service.init();
await vi.advanceTimersByTimeAsync(0);
const baseline = (gateway.fetchUsage as ReturnType<typeof vi.fn>).mock.calls
.length;

service.stop();
agent.fireWindowFocus();
await vi.advanceTimersByTimeAsync(10_000);
expect(gateway.fetchUsage).toHaveBeenCalledTimes(baseline);
});
});
3 changes: 3 additions & 0 deletions packages/core/src/usage/usage-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class UsageMonitorService extends TypedEventEmitter<UsageMonitorEvents> {
private latestUsage: UsageOutput | null = null;

private readonly onLlmActivity = (): void => this.requestRefresh();
private readonly onWindowFocus = (): void => this.requestRefresh();

constructor(
@inject(USAGE_HOST)
Expand Down Expand Up @@ -83,13 +84,15 @@ export class UsageMonitorService extends TypedEventEmitter<UsageMonitorEvents> {
init(): void {
this.pruneStaleEntries();
this.host.onLlmActivity(this.onLlmActivity);
this.host.onWindowFocus(this.onWindowFocus);
void this.fetchOnce();
this.scheduleBackstop();
}

@preDestroy()
stop(): void {
this.host.offLlmActivity(this.onLlmActivity);
this.host.offWindowFocus(this.onWindowFocus);
if (this.backstopTimeoutId) {
clearTimeout(this.backstopTimeoutId);
this.backstopTimeoutId = null;
Expand Down
Loading