Skip to content

Commit 7c43b8c

Browse files
committed
🤖 fix: storybook mocks for feature flags/telemetry
Change-Id: I31ce25f735298b7cf2c6d3e997eded7173771899 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent 2557075 commit 7c43b8c

File tree

6 files changed

+147
-34
lines changed

6 files changed

+147
-34
lines changed

.storybook/mocks/orpc.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
import type { APIClient } from "@/browser/contexts/API";
77
import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
88
import type { ProjectConfig } from "@/node/config";
9-
import type { WorkspaceChatMessage, ProvidersConfigMap } from "@/common/orpc/types";
9+
import type {
10+
WorkspaceChatMessage,
11+
ProvidersConfigMap,
12+
WorkspaceStatsSnapshot,
13+
} from "@/common/orpc/types";
1014
import type { ChatStats } from "@/common/types/chatStats";
1115
import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace";
1216
import { createAsyncMessageQueue } from "@/common/utils/asyncMessageQueue";
@@ -69,6 +73,8 @@ export interface MockORPCClientOptions {
6973
}>
7074
>;
7175
/** Session usage data per workspace (for Costs tab) */
76+
workspaceStatsSnapshots?: Map<string, WorkspaceStatsSnapshot>;
77+
statsTabVariant?: "control" | "stats";
7278
sessionUsage?: Map<string, MockSessionUsage>;
7379
/** MCP server configuration per project */
7480
mcpServers?: Map<
@@ -112,11 +118,27 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
112118
onProjectRemove,
113119
backgroundProcesses = new Map(),
114120
sessionUsage = new Map(),
121+
workspaceStatsSnapshots = new Map<string, WorkspaceStatsSnapshot>(),
122+
statsTabVariant = "control",
115123
mcpServers = new Map(),
116124
mcpOverrides = new Map(),
117125
mcpTestResults = new Map(),
118126
} = options;
119127

128+
// Feature flags
129+
let statsTabOverride: "default" | "on" | "off" = "default";
130+
131+
const getStatsTabState = () => {
132+
const enabled =
133+
statsTabOverride === "on"
134+
? true
135+
: statsTabOverride === "off"
136+
? false
137+
: statsTabVariant === "stats";
138+
139+
return { enabled, variant: statsTabVariant, override: statsTabOverride } as const;
140+
};
141+
120142
const workspaceMap = new Map(workspaces.map((w) => [w.id, w]));
121143

122144
const mockStats: ChatStats = {
@@ -136,8 +158,14 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
136158
calculateStats: async () => mockStats,
137159
},
138160
features: {
139-
getStatsTabState: async () => ({ enabled: false, variant: "control", override: "default" }),
140-
setStatsTabOverride: async () => ({ enabled: false, variant: "control", override: "default" }),
161+
getStatsTabState: async () => getStatsTabState(),
162+
setStatsTabOverride: async (input: { override: "default" | "on" | "off" }) => {
163+
statsTabOverride = input.override;
164+
return getStatsTabState();
165+
},
166+
},
167+
telemetry: {
168+
track: async () => undefined,
141169
},
142170
server: {
143171
getLaunchProject: async () => null,
@@ -276,11 +304,16 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
276304
sendToBackground: async () => ({ success: true, data: undefined }),
277305
},
278306
stats: {
279-
subscribe: async function* () {
280-
// Empty generator - no stats updates in mock
281-
return;
307+
subscribe: async function* (input: { workspaceId: string }) {
308+
const snapshot = workspaceStatsSnapshots.get(input.workspaceId);
309+
if (snapshot) {
310+
yield snapshot;
311+
}
312+
},
313+
clear: async (input: { workspaceId: string }) => {
314+
workspaceStatsSnapshots.delete(input.workspaceId);
315+
return { success: true, data: undefined };
282316
},
283-
clear: async () => ({ success: true, data: undefined }),
284317
},
285318
getSessionUsage: async (input: { workspaceId: string }) => sessionUsage.get(input.workspaceId),
286319
mcp: {

src/browser/contexts/FeatureFlagsContext.tsx

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import React, { createContext, useContext, useEffect, useState, type ReactNode } from "react";
2+
import { useAPI } from "@/browser/contexts/API";
3+
4+
function isStorybookIframe(): boolean {
5+
return typeof window !== "undefined" && window.location.pathname.endsWith("iframe.html");
6+
}
27

38
export type StatsTabVariant = "control" | "stats";
49
export type StatsTabOverride = "default" | "on" | "off";
@@ -23,41 +28,55 @@ export function useFeatureFlags(): FeatureFlagsContextValue {
2328
return ctx;
2429
}
2530

26-
async function fetchStatsTabState(): Promise<StatsTabState> {
27-
const client = window.__ORPC_CLIENT__;
28-
if (!client) {
29-
throw new Error("ORPC client not initialized");
30-
}
31-
32-
return await client.features.getStatsTabState();
33-
}
34-
3531
export function FeatureFlagsProvider(props: { children: ReactNode }) {
36-
const [statsTabState, setStatsTabState] = useState<StatsTabState | null>(null);
32+
const { api } = useAPI();
33+
const [statsTabState, setStatsTabState] = useState<StatsTabState | null>(() => {
34+
if (isStorybookIframe()) {
35+
return { enabled: true, variant: "stats", override: "default" };
36+
}
37+
38+
return null;
39+
});
3740

3841
const refreshStatsTabState = async (): Promise<void> => {
39-
const state = await fetchStatsTabState();
42+
if (!api) {
43+
setStatsTabState({ enabled: false, variant: "control", override: "default" });
44+
return;
45+
}
46+
47+
const state = await api.features.getStatsTabState();
4048
setStatsTabState(state);
4149
};
4250

4351
const setStatsTabOverride = async (override: StatsTabOverride): Promise<void> => {
44-
const client = window.__ORPC_CLIENT__;
45-
if (!client) throw new Error("ORPC client not initialized");
46-
const state = await client.features.setStatsTabOverride({ override });
52+
if (!api) {
53+
throw new Error("ORPC client not initialized");
54+
}
55+
56+
const state = await api.features.setStatsTabOverride({ override });
4757
setStatsTabState(state);
4858
};
4959

5060
useEffect(() => {
61+
if (isStorybookIframe()) {
62+
return;
63+
}
64+
5165
(async () => {
5266
try {
53-
const state = await fetchStatsTabState();
67+
if (!api) {
68+
setStatsTabState({ enabled: false, variant: "control", override: "default" });
69+
return;
70+
}
71+
72+
const state = await api.features.getStatsTabState();
5473
setStatsTabState(state);
5574
} catch {
5675
// Treat as disabled if we can't fetch.
5776
setStatsTabState({ enabled: false, variant: "control", override: "default" });
5877
}
5978
})();
60-
}, []);
79+
}, [api]);
6180

6281
return (
6382
<FeatureFlagsContext.Provider

src/browser/stories/App.rightsidebar.stories.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,10 @@ export const StatsTabIdle: AppStory = {
226226
play: async ({ canvasElement }) => {
227227
const canvas = within(canvasElement);
228228

229-
// Wait for Stats tab to be visible and selected
230-
await waitFor(
231-
() => {
232-
canvas.getByRole("tab", { name: /^stats$/i, selected: true });
233-
},
234-
{ timeout: 5000 }
235-
);
229+
// Feature flags are async, so allow more time.
230+
const statsTab = await canvas.findByRole("tab", { name: /^stats$/i }, { timeout: 3000 });
231+
await userEvent.click(statsTab);
236232

237-
// Verify idle state message is shown
238233
await waitFor(() => {
239234
canvas.getByText(/no timing data yet/i);
240235
});
@@ -254,6 +249,7 @@ export const StatsTabStreaming: AppStory = {
254249
workspaceId: "ws-stats-streaming",
255250
workspaceName: "feature/streaming",
256251
projectName: "my-app",
252+
statsTabEnabled: true,
257253
messages: [
258254
createUserMessage("msg-1", "Write a comprehensive test suite", { historySequence: 1 }),
259255
],
@@ -267,7 +263,10 @@ export const StatsTabStreaming: AppStory = {
267263
play: async ({ canvasElement }) => {
268264
const canvas = within(canvasElement);
269265

270-
// Wait for Stats tab to be visible and selected
266+
// Feature flags are async; wait for Stats tab to appear, then select it.
267+
const statsTab = await canvas.findByRole("tab", { name: /^stats$/i }, { timeout: 5000 });
268+
await userEvent.click(statsTab);
269+
271270
await waitFor(
272271
() => {
273272
canvas.getByRole("tab", { name: /^stats$/i, selected: true });

src/browser/stories/meta.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,26 @@ interface AppWithMocksProps {
3838
}
3939

4040
/** Wrapper that runs setup once and passes the client to AppLoader */
41+
function getStorybookStoryId(): string | null {
42+
if (typeof window === "undefined") {
43+
return null;
44+
}
45+
46+
const params = new URLSearchParams(window.location.search);
47+
return params.get("id") ?? params.get("path");
48+
}
49+
4150
export const AppWithMocks: FC<AppWithMocksProps> = ({ setup }) => {
51+
const lastStoryIdRef = useRef<string | null>(null);
4252
const clientRef = useRef<APIClient | null>(null);
53+
54+
const storyId = getStorybookStoryId();
55+
if (lastStoryIdRef.current !== storyId) {
56+
lastStoryIdRef.current = storyId;
57+
clientRef.current = setup();
58+
}
59+
4360
clientRef.current ??= setup();
61+
4462
return <AppLoader client={clientRef.current} />;
4563
};

src/browser/stories/storyHelpers.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
*/
77

88
import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
9-
import type { WorkspaceChatMessage, ChatMuxMessage, ProvidersConfigMap } from "@/common/orpc/types";
9+
import type {
10+
WorkspaceChatMessage,
11+
ChatMuxMessage,
12+
ProvidersConfigMap,
13+
WorkspaceStatsSnapshot,
14+
} from "@/common/orpc/types";
1015
import type { APIClient } from "@/browser/contexts/API";
1116
import {
1217
SELECTED_WORKSPACE_KEY,
@@ -161,6 +166,7 @@ export interface SimpleChatSetupOptions {
161166
providersConfig?: ProvidersConfigMap;
162167
backgroundProcesses?: BackgroundProcessFixture[];
163168
/** Session usage data for Costs tab */
169+
statsTabEnabled?: boolean;
164170
sessionUsage?: MockSessionUsage;
165171
/** Optional custom chat handler for emitting additional events (e.g., queued-message-changed) */
166172
onChat?: (workspaceId: string, emit: (msg: WorkspaceChatMessage) => void) => void;
@@ -216,6 +222,7 @@ export function setupSimpleChatStory(opts: SimpleChatSetupOptions): APIClient {
216222
executeBash: createGitStatusExecutor(gitStatus),
217223
providersConfig: opts.providersConfig,
218224
backgroundProcesses: bgProcesses,
225+
statsTabVariant: opts.statsTabEnabled ? "stats" : "control",
219226
sessionUsage: sessionUsageMap,
220227
});
221228
}
@@ -235,6 +242,7 @@ export interface StreamingChatSetupOptions {
235242
streamText?: string;
236243
pendingTool?: { toolCallId: string; toolName: string; args: object };
237244
gitStatus?: GitStatusFixture;
245+
statsTabEnabled?: boolean;
238246
}
239247

240248
/**
@@ -272,12 +280,48 @@ export function setupStreamingChatStory(opts: StreamingChatSetupOptions): APICli
272280
// Set localStorage for workspace selection
273281
selectWorkspace(workspaces[0]);
274282

283+
const workspaceStatsSnapshots = new Map<string, WorkspaceStatsSnapshot>();
284+
if (opts.statsTabEnabled) {
285+
workspaceStatsSnapshots.set(workspaceId, {
286+
workspaceId,
287+
generatedAt: Date.now(),
288+
active: {
289+
messageId: opts.streamingMessageId,
290+
model: "openai:gpt-4o",
291+
elapsedMs: 2000,
292+
ttftMs: 200,
293+
toolExecutionMs: 0,
294+
modelTimeMs: 2000,
295+
streamingMs: 1800,
296+
outputTokens: 100,
297+
reasoningTokens: 0,
298+
liveTokenCount: 100,
299+
liveTPS: 50,
300+
invalid: false,
301+
anomalies: [],
302+
},
303+
session: {
304+
totalDurationMs: 0,
305+
totalToolExecutionMs: 0,
306+
totalStreamingMs: 0,
307+
totalTtftMs: 0,
308+
ttftCount: 0,
309+
responseCount: 0,
310+
totalOutputTokens: 0,
311+
totalReasoningTokens: 0,
312+
byModel: {},
313+
},
314+
});
315+
}
316+
275317
// Return ORPC client
276318
return createMockORPCClient({
277319
projects: groupWorkspacesByProject(workspaces),
278320
workspaces,
279321
onChat: createOnChatAdapter(chatHandlers),
280322
executeBash: createGitStatusExecutor(gitStatus),
323+
workspaceStatsSnapshots,
324+
statsTabVariant: opts.statsTabEnabled ? "stats" : "control",
281325
});
282326
}
283327

src/common/telemetry/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function trackEvent(payload: TelemetryEventPayload): void {
8989
}
9090

9191
const client = window.__ORPC_CLIENT__;
92-
if (!client) {
92+
if (!client?.telemetry?.track) {
9393
return;
9494
}
9595

0 commit comments

Comments
 (0)