Visitar URL original
feat(webapp,clickhouse): measure session trace requests and their Cli… · triggerdotdev/trigger.dev@47cf81f · GitHub
Skip to content

Commit 47cf81f

Browse files
MariefayTrigger.dev RepoOps
authored andcommitted
feat(webapp,clickhouse): measure session trace requests and their ClickHouse reads
Mono-RevId: 58344f77f2a95e3c64f14a5fcdec72c14ed2acef
1 parent ee5e5da commit 47cf81f

23 files changed

Lines changed: 858 additions & 55 deletions

‎apps/webapp/app/components/sessions/v1/trace/SessionRunInspector.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "./sessionPanelStore";
1616
import { SESSION_SPAN_TAB_PARAM, type SessionTraceSelection } from "./sessionTraceSelection";
1717
import { fetchRunsPage } from "./useSessionRunPages";
18+
import { useSessionViewAnalytics } from "./useSessionViewAnalytics";
1819

1920
type RunOrSpanSelection = Exclude<SessionTraceSelection, { kind: "session" }>;
2021

@@ -29,6 +30,7 @@ export function SessionRunInspector({
2930
runsPath: string;
3031
onClose: () => void;
3132
}) {
33+
const track = useSessionViewAnalytics();
3234
const organization = useOrganization();
3335
const project = useProject();
3436
const environment = useEnvironment();
@@ -85,7 +87,11 @@ export function SessionRunInspector({
8587
? v3RunSpanPath(organization, project, environment, run, { spanId: selection.spanId })
8688
: v3RunPath(organization, project, environment, run);
8789
const openRunPage = (
88-
<LinkButton variant="secondary/small" to={runPagePath}>
90+
<LinkButton
91+
variant="secondary/small"
92+
to={runPagePath}
93+
onClick={() => track("open_run_page", { from: "panel" })}
94+
>
8995
Open run page
9096
</LinkButton>
9197
);

‎apps/webapp/app/components/sessions/v1/trace/SessionTraceTab.tsx‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ import {
100100
type SearchCoverage,
101101
type SessionTraceFilter,
102102
} from "./sessionTraceSearch";
103+
import { newBudgetOutcomes, searchStarted, type BudgetOutcome } from "./sessionViewAnalytics";
104+
import { useSessionViewAnalytics } from "./useSessionViewAnalytics";
103105
import type { SessionTraceRun } from "./sessionTraceTypes";
104106
import { SESSION_TREE_AUTOSAVE_ID, runReasonLabel } from "./sessionTraceView";
105107
import {
@@ -151,9 +153,18 @@ export function SessionTraceTab({
151153
interval: newRunsPollIntervalMs(pages.runs ?? [], pages.totalCount, checkedAtMs),
152154
disabled: pages.state.status !== "ready",
153155
onLoad: false,
156+
onFocus: false,
154157
callback: () => {
155158
setCheckedAtMs(Date.now());
156-
pages.refreshNewest();
159+
pages.refreshNewest("poll");
160+
},
161+
});
162+
useInterval({
163+
disabled: pages.state.status !== "ready",
164+
onLoad: false,
165+
callback: () => {
166+
setCheckedAtMs(Date.now());
167+
pages.refreshNewest("focus");
157168
},
158169
});
159170

@@ -304,6 +315,22 @@ function SessionTraceTree({
304315
// The page check runs between renders, so it reads a ref that every event updates at once,
305316
// with the same reducer React runs: a count or a release just sent is never missed.
306317
const loadsRef = useRef(loads);
318+
const track = useSessionViewAnalytics(runs.length);
319+
// Budget reached: each outcome once per page view (Expand all near the limit hits it often).
320+
const reportedBudget = useRef(new Set<BudgetOutcome>());
321+
const lastBudgetHits = useRef(loads.budgetHits);
322+
useEffect(() => {
323+
const outcomes = newBudgetOutcomes(
324+
lastBudgetHits.current,
325+
loads.budgetHits,
326+
reportedBudget.current
327+
);
328+
lastBudgetHits.current = loads.budgetHits;
329+
for (const outcome of outcomes) {
330+
reportedBudget.current.add(outcome);
331+
track("budget_reached", { outcome });
332+
}
333+
}, [loads.budgetHits, track]);
307334
const dispatchLoads = useCallback((action: SessionRunLoadsAction) => {
308335
loadsRef.current = sessionRunLoadsReducer(loadsRef.current, action);
309336
reactDispatchLoads(action);
@@ -618,10 +645,12 @@ function SessionTraceTree({
618645
),
619646
};
620647
newestFirst.runFriendlyIds.reverse();
648+
track("expand_all", { runs: newestFirst.runFriendlyIds.length });
621649
dispatchLoads(newestFirst);
622650
expandNodes(collapsed.map((node) => node.id));
623651
};
624652
const retryRun = (runFriendlyId: string) => {
653+
track("retry");
625654
dispatchLoads({ type: "retry", runFriendlyId });
626655
const id = runNodeId(runFriendlyId);
627656
if (!nodes[id]?.expanded) expandNode(id, false);
@@ -677,6 +706,7 @@ function SessionTraceTree({
677706
);
678707
const loadAllForSearch = () => {
679708
if (coverage.toLoad.length === 0) return;
709+
track("load_all", { runs: coverage.toLoad.length });
680710
for (const id of coverage.toLoad) openedForSearch.current.add(id);
681711
dispatchLoads({ type: "expandedMany", runFriendlyIds: coverage.toLoad });
682712
searchMovingRows.current = true;
@@ -697,6 +727,9 @@ function SessionTraceTree({
697727
next.text = next.text.toLowerCase();
698728
if (!isFiltering(next)) next.hiddenRuns = [];
699729
setFilter(next);
730+
if (searchStarted(filter, next)) {
731+
track("search_used", { text: next.text !== "", errors_only: next.errorsOnly });
732+
}
700733
if (!filtering || isFiltering(next)) return;
701734

702735
const keep = new Set(touchedDuringSearch.current);
@@ -1294,6 +1327,7 @@ function RunRowContent({
12941327

12951328
// A run whose trace can't load here links to its run page instead of expanding.
12961329
function RunFallback({ run, block }: { run: SessionTraceRun; block: RunLoadBlock }) {
1330+
const track = useSessionViewAnalytics();
12971331
const organization = useOrganization();
12981332
const project = useProject();
12991333
const environment = useEnvironment();
@@ -1313,7 +1347,10 @@ function RunFallback({ run, block }: { run: SessionTraceRun; block: RunLoadBlock
13131347
<LinkButton
13141348
variant="secondary/small"
13151349
to={v3RunPath(organization, project, environment, run)}
1316-
onClick={(event) => event.stopPropagation()}
1350+
onClick={(event) => {
1351+
event.stopPropagation();
1352+
track("open_run_page", { from: "row", reason: block.kind });
1353+
}}
13171354
>
13181355
Open run page
13191356
</LinkButton>
@@ -1337,6 +1374,7 @@ function RunLoadStatus({
13371374
spanCount: number;
13381375
onRetry: (runFriendlyId: string) => void;
13391376
}) {
1377+
const track = useSessionViewAnalytics();
13401378
const organization = useOrganization();
13411379
const project = useProject();
13421380
const environment = useEnvironment();
@@ -1395,7 +1433,10 @@ function RunLoadStatus({
13951433
<LinkButton
13961434
variant="secondary/small"
13971435
to={v3RunPath(organization, project, environment, run)}
1398-
onClick={(event) => event.stopPropagation()}
1436+
onClick={(event) => {
1437+
event.stopPropagation();
1438+
track("open_run_page", { from: "row", reason: "cutOff" });
1439+
}}
13991440
>
14001441
Open run page
14011442
</LinkButton>

‎apps/webapp/app/components/sessions/v1/trace/sessionRunLoads.ts‎

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export type SessionRunLoads = {
6060
/** The session's newest run, when it's loaded. */
6161
newestRunId: string | undefined;
6262
budget: number;
63+
/** How often the budget freed collapsed runs or cut one off, for analytics. */
64+
budgetHits: { freed: number; cutOff: number };
6365
};
6466

6567
export type SessionRunLoadsAction =
@@ -85,7 +87,7 @@ export type SessionRunLoadsAction =
8587
/** Retry a failed run: in place when some of its spans are on screen, else from the start. */
8688
| { type: "retry"; runFriendlyId: string }
8789
/** Frees runs back to idle, cancelling a fetch in flight (its late settle won't match). */
88-
| { type: "release"; runFriendlyIds: string[] };
90+
| { type: "release"; runFriendlyIds: string[]; forBudget?: boolean };
8991

9092
/** What to do before a run fetches its next page. */
9193
type PageDecision =
@@ -171,6 +173,7 @@ export function createSessionRunLoads(
171173
selectedRunId: undefined,
172174
newestRunId: undefined,
173175
budget,
176+
budgetHits: { freed: 0, cutOff: 0 },
174177
};
175178
}
176179

@@ -300,7 +303,7 @@ export function sessionRunLoadsReducer(
300303
const over = loadedTotal(next) - next.budget;
301304
if (over <= 0) return next;
302305
const freeable = freeableRuns(next);
303-
return withoutLoads(next, takeUntil(next, freeable, over) ?? freeable);
306+
return freeForBudget(next, takeUntil(next, freeable, over) ?? freeable);
304307
}
305308
case "selected": {
306309
if (current.selectedRunId === action.runFriendlyId) return current;
@@ -318,7 +321,9 @@ export function sessionRunLoadsReducer(
318321
attempt: load.state === "paging" ? load.attempt : 0,
319322
}
320323
: { ...action.outcome, loadId: load.loadId };
321-
return startQueued({ ...current, loads: { ...current.loads, [action.runFriendlyId]: next } });
324+
const settled = { ...current, loads: { ...current.loads, [action.runFriendlyId]: next } };
325+
const cutBySession = action.outcome.state === "cutOff" && action.outcome.limit === "session";
326+
return startQueued(cutBySession ? countCutOff(settled) : settled);
322327
}
323328
case "retry": {
324329
const { runFriendlyId: id } = action;
@@ -334,7 +339,9 @@ export function sessionRunLoadsReducer(
334339
case "release": {
335340
const released = action.runFriendlyIds.filter((id) => id in current.loads);
336341
if (released.length === 0) return current;
337-
return loadWanted(withoutLoads(current, released));
342+
return loadWanted(
343+
action.forBudget ? freeForBudget(current, released) : withoutLoads(current, released)
344+
);
338345
}
339346
}
340347
}
@@ -359,7 +366,7 @@ export function pageCheck(
359366
case "free":
360367
return {
361368
allowed: true,
362-
action: { type: "release", runFriendlyIds: decision.runFriendlyIds },
369+
action: { type: "release", runFriendlyIds: decision.runFriendlyIds, forBudget: true },
363370
};
364371
case "cutOff": {
365372
const load = runLoad(current, runFriendlyId);
@@ -424,6 +431,17 @@ function enqueue(current: SessionRunLoads, runFriendlyIds: string[]): SessionRun
424431
return { ...current, loads, queue: [...current.queue, ...runFriendlyIds] };
425432
}
426433

434+
function freeForBudget(current: SessionRunLoads, runFriendlyIds: string[]): SessionRunLoads {
435+
if (runFriendlyIds.length === 0) return current;
436+
const { freed, cutOff } = current.budgetHits;
437+
return { ...withoutLoads(current, runFriendlyIds), budgetHits: { freed: freed + 1, cutOff } };
438+
}
439+
440+
function countCutOff(current: SessionRunLoads): SessionRunLoads {
441+
const { freed, cutOff } = current.budgetHits;
442+
return { ...current, budgetHits: { freed, cutOff: cutOff + 1 } };
443+
}
444+
427445
function withoutLoads(current: SessionRunLoads, runFriendlyIds: string[]): SessionRunLoads {
428446
if (runFriendlyIds.length === 0) return current;
429447
const removed = new Set(runFriendlyIds);
@@ -489,15 +507,17 @@ function startQueued(current: SessionRunLoads): SessionRunLoads {
489507
if (over > 0) {
490508
const toFree = takeUntil(next, freeableRuns(next), over);
491509
if (!toFree) {
492-
next = settleAt(next, id, {
493-
state: "cutOff",
494-
limit: "session",
495-
started: false,
496-
spanLimit: next.budget,
497-
});
510+
next = countCutOff(
511+
settleAt(next, id, {
512+
state: "cutOff",
513+
limit: "session",
514+
started: false,
515+
spanLimit: next.budget,
516+
})
517+
);
498518
continue;
499519
}
500-
next = withoutLoads(next, toFree);
520+
next = freeForBudget(next, toFree);
501521
}
502522
}
503523

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export const SESSION_VIEW_EVENT = "session_view_action";
2+
3+
export type SessionViewAction =
4+
| "expand_all"
5+
| "load_all"
6+
| "search_used"
7+
| "budget_reached"
8+
| "open_run_page"
9+
| "retry"
10+
| "conversation_opened";
11+
12+
export type SessionViewer = "member" | "admin" | "impersonating";
13+
14+
export type SessionViewProperties = Record<string, string | number | boolean | undefined>;
15+
16+
export function sessionViewer(admin: boolean | undefined, impersonating: boolean): SessionViewer {
17+
if (impersonating) return "impersonating";
18+
return admin ? "admin" : "member";
19+
}
20+
21+
export function sessionViewEventProperties(
22+
action: SessionViewAction,
23+
shared: { viewer: SessionViewer; runsShown?: number },
24+
extra: SessionViewProperties = {}
25+
): SessionViewProperties {
26+
return { ...extra, action, viewer: shared.viewer, runs_shown: shared.runsShown };
27+
}
28+
29+
export type BudgetHits = { freed: number; cutOff: number };
30+
export type BudgetOutcome = "freed" | "cut_off";
31+
32+
/** Budget outcomes to report: each one once per page view, the first time it happens. */
33+
export function newBudgetOutcomes(
34+
previous: BudgetHits,
35+
next: BudgetHits,
36+
reported: ReadonlySet<BudgetOutcome>
37+
): BudgetOutcome[] {
38+
const outcomes: BudgetOutcome[] = [];
39+
if (next.freed > previous.freed && !reported.has("freed")) outcomes.push("freed");
40+
if (next.cutOff > previous.cutOff && !reported.has("cut_off")) outcomes.push("cut_off");
41+
return outcomes;
42+
}
43+
44+
/** "Search used" fires when a search starts, not on every keystroke. */
45+
export function searchStarted(
46+
previous: { text: string; errorsOnly: boolean },
47+
next: { text: string; errorsOnly: boolean }
48+
): boolean {
49+
const wasSearching = previous.text !== "" || previous.errorsOnly;
50+
const isSearching = next.text !== "" || next.errorsOnly;
51+
return !wasSearching && isSearching;
52+
}

‎apps/webapp/app/components/sessions/v1/trace/useSessionRunPages.ts‎

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,23 @@ type PagesState =
3333
/** What became of the deep-linked run, if the page opened with one. */
3434
export type DeepLinkResult = "none" | "loaded" | "notFound" | "failed";
3535

36+
/** Why the newest page is read; the route labels its metrics with it. */
37+
export type NewestPageReason = "open" | "focus" | "poll";
38+
3639
export type PageResult =
3740
| { ok: true; page: SessionRunsPage }
3841
| { ok: false; status: number | undefined; message: string };
3942

4043
export async function fetchRunsPage(
4144
runsPath: string,
42-
params: { after?: string; size?: number; run?: string },
45+
params: { after?: string; size?: number; run?: string; reason?: NewestPageReason },
4346
signal: AbortSignal
4447
): Promise<PageResult> {
4548
const url = new URL(runsPath, window.location.origin);
4649
url.searchParams.set("page[size]", String(params.size ?? RUNS_PAGE_SIZE));
4750
if (params.after) url.searchParams.set("page[after]", params.after);
4851
if (params.run) url.searchParams.set("run", params.run);
52+
if (params.reason) url.searchParams.set("reason", params.reason);
4953

5054
const [error, response] = await tryCatch(fetch(url, { signal }));
5155
if (error) return { ok: false, status: undefined, message: "Couldn't load this session's runs." };
@@ -143,7 +147,7 @@ export function useSessionRunPages(runsPath: string, deepLinkRunFriendlyId?: str
143147
useEffect(() => {
144148
const controller = new AbortController();
145149
void Promise.all([
146-
fetchRunsPage(runsPath, {}, controller.signal),
150+
fetchRunsPage(runsPath, { reason: "open" }, controller.signal),
147151
initialDeepLink
148152
? fetchRunsPage(runsPath, { run: initialDeepLink }, controller.signal)
149153
: undefined,
@@ -218,20 +222,23 @@ export function useSessionRunPages(runsPath: string, deepLinkRunFriendlyId?: str
218222
);
219223

220224
// Re-reads the newest page, e.g. when a run starts or finishes. Runs already loaded stay.
221-
const refreshNewest = useCallback(() => {
222-
if (refreshController.current) return;
223-
const controller = new AbortController();
224-
refreshController.current = controller;
225-
void fetchRunsPage(runsPath, {}, controller.signal).then((result) => {
226-
if (refreshController.current === controller) refreshController.current = null;
227-
if (controller.signal.aborted || !result.ok) return;
228-
setState((prev) =>
229-
prev.status === "ready"
230-
? { ...prev, loaded: mergeSessionRunPage(prev.loaded, result.page, "newest") }
231-
: prev
232-
);
233-
});
234-
}, [runsPath]);
225+
const refreshNewest = useCallback(
226+
(reason: NewestPageReason = "poll") => {
227+
if (refreshController.current) return;
228+
const controller = new AbortController();
229+
refreshController.current = controller;
230+
void fetchRunsPage(runsPath, { reason }, controller.signal).then((result) => {
231+
if (refreshController.current === controller) refreshController.current = null;
232+
if (controller.signal.aborted || !result.ok) return;
233+
setState((prev) =>
234+
prev.status === "ready"
235+
? { ...prev, loaded: mergeSessionRunPage(prev.loaded, result.page, "newest") }
236+
: prev
237+
);
238+
});
239+
},
240+
[runsPath]
241+
);
235242

236243
const runs = useMemo(() => (loaded ? [...loaded.runsById.values()] : undefined), [loaded]);
237244

0 commit comments

Comments
 (0)