fix(ui): order each lifecycle phase on its own clock

This commit is contained in:
Yuneng Jiang 2026-09-17 13:29:45 -07:00
parent d67c7894dd
commit 6544671a31
No known key found for this signature in database
2 changed files with 48 additions and 12 deletions

View file

@ -42,6 +42,24 @@ const timedPreCall: Partial<GuardrailInformation> = {
duration: 0.1,
};
const latePreCall: Partial<GuardrailInformation> = {
guardrail_name: "late-pre-rail",
guardrail_status: "success",
guardrail_mode: "pre_call",
start_time: 1_700_000_500,
end_time: 1_700_000_500.1,
duration: 0.1,
};
const untimedPostCall: Partial<GuardrailInformation> = {
guardrail_name: "untimed-post-rail",
guardrail_status: "success",
guardrail_mode: "post_call",
start_time: null,
end_time: null,
duration: null,
};
const ranPostCall: Partial<GuardrailInformation> = {
guardrail_name: "ran-rail",
guardrail_status: "success",
@ -141,6 +159,22 @@ describe("GuardrailViewer", () => {
expect(untimedIndex).toBeLessThan(timedIndex);
});
it("orders each phase on its own clock when a later pre-call outlives an earlier post-call", () => {
const latePre = makeGuardrailInformation(latePreCall);
const untimedPost = makeGuardrailInformation(untimedPostCall);
const earlyPost = makeGuardrailInformation(ranPostCall);
renderWithProviders(<GuardrailViewer data={[latePre, untimedPost, earlyPost]} />);
const rows = screen.getAllByTestId("lifecycle-row");
const rowIndex = (label: RegExp): number => rows.findIndex((r) => within(r).queryByText(label) !== null);
const untimedIndex = rowIndex(/Post-call guardrail: untimed-post-rail/);
const earlyIndex = rowIndex(/Post-call guardrail: ran-rail/);
expect(untimedIndex).toBeGreaterThanOrEqual(0);
expect(earlyIndex).toBeGreaterThanOrEqual(0);
expect(untimedIndex).toBeLessThan(earlyIndex);
});
it("anchors offsets on the timed entries and gives the untimed one no fabricated offset", () => {
const untimed = makeGuardrailInformation(untimedPreCall);
const ran = makeGuardrailInformation(ranPostCall);

View file

@ -372,15 +372,17 @@ const isTimed = (e: GuardrailInformation): e is TimedGuardrailInformation =>
const belongsOnLifecycle = (e: GuardrailInformation): boolean => isTimed(e) || getEntryOutcome(e) !== "not_run";
// Sorts a phase's timed entries by start time while leaving its untimed entries in the
// slots they were recorded in. Applied per phase, never globally: an entry can land in
// more than one phase bucket, so a global pass can reorder one phase by another's clock.
const orderWithinPhase = (group: GuardrailInformation[]): GuardrailInformation[] => {
const byStart = group.filter(isTimed).sort((a, b) => a.start_time - b.start_time);
const timedSlots = new Map(group.flatMap((e, i) => (isTimed(e) ? [i] : [])).map((slot, k) => [slot, byStart[k]]));
return group.map((e, i) => timedSlots.get(i) ?? e);
};
const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
const sorted = useMemo(() => {
const onLifecycle = entries.filter(belongsOnLifecycle);
const byStart = onLifecycle.filter(isTimed).sort((a, b) => a.start_time - b.start_time);
const timedSlots = new Map(
onLifecycle.flatMap((e, i) => (isTimed(e) ? [i] : [])).map((slot, k) => [slot, byStart[k]]),
);
return onLifecycle.map((e, i) => timedSlots.get(i) ?? e);
}, [entries]);
const sorted = useMemo(() => entries.filter(belongsOnLifecycle), [entries]);
const timeline = useMemo(() => {
if (sorted.length === 0) return [];
@ -396,11 +398,11 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
// Pre-call guardrails — use modeMatches so array modes (e.g. ["pre_call", "post_call"])
// place the entry in every matching bucket.
const preCalls = sorted.filter((e) => modeMatches(e.guardrail_mode, "pre_call"));
const postCalls = sorted.filter(
(e) => modeMatches(e.guardrail_mode, "post_call") || modeMatches(e.guardrail_mode, "logging_only"),
const preCalls = orderWithinPhase(sorted.filter((e) => modeMatches(e.guardrail_mode, "pre_call")));
const postCalls = orderWithinPhase(
sorted.filter((e) => modeMatches(e.guardrail_mode, "post_call") || modeMatches(e.guardrail_mode, "logging_only")),
);
const duringCalls = sorted.filter((e) => modeMatches(e.guardrail_mode, "during_call"));
const duringCalls = orderWithinPhase(sorted.filter((e) => modeMatches(e.guardrail_mode, "during_call")));
for (const e of preCalls) {
items.push({