fix(ui): open guardrail monitor log details regardless of browser timezone

The details lookup window was built by converting local midnight to UTC before taking end-of-day, so browsers ahead of UTC queried the previous UTC day and the drawer silently rendered nothing.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
milan 2026-07-24 22:14:49 +00:00
parent 8177230a29
commit df013f6b77
4 changed files with 126 additions and 6 deletions

View file

@ -0,0 +1,64 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import moment from "moment";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { LogViewer } from "./LogViewer";
import { uiSpendLogsCall } from "@/components/networking";
import type { LogEntry } from "./mockData";
vi.mock("@/components/networking", () => ({
uiSpendLogsCall: vi.fn(),
}));
vi.mock("@/components/view_logs/LogDetailsDrawer", () => ({
LogDetailsDrawer: () => null,
}));
const originalTimezone = process.env.TZ;
const logs: LogEntry[] = [
{
id: "chatcmpl-1",
timestamp: "2026-07-22T09:38:46.397000+00:00",
action: "passed",
model: "claude-haiku-4-5",
input_snippet: "hello",
},
];
const renderViewer = (startDate: string, endDate: string) => {
vi.mocked(uiSpendLogsCall).mockResolvedValue({ data: [], total: 0 });
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(
<QueryClientProvider client={queryClient}>
<LogViewer logs={logs} accessToken="token" startDate={startDate} endDate={endDate} />
</QueryClientProvider>,
);
};
describe("LogViewer log lookup window", () => {
beforeAll(() => {
// Reproduces a UTC+2 browser: converting local midnight to UTC before
// taking end-of-day used to move the window onto the previous UTC day.
process.env.TZ = "Europe/Berlin";
});
afterAll(() => {
process.env.TZ = originalTimezone;
});
it("covers the whole selected local range when looking up the clicked request", async () => {
renderViewer("2026-07-22", "2026-07-22");
fireEvent.click(screen.getByText("hello"));
await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalled());
const { start_date, end_date, params } = vi.mocked(uiSpendLogsCall).mock.calls[0][0];
expect(params?.request_id).toBe("chatcmpl-1");
const logTimestamp = moment.utc("2026-07-22T09:38:46.397Z");
expect(moment.utc(start_date, "YYYY-MM-DD HH:mm:ss").isSameOrBefore(logTimestamp)).toBe(true);
expect(moment.utc(end_date, "YYYY-MM-DD HH:mm:ss").isSameOrAfter(logTimestamp)).toBe(true);
});
});

View file

@ -68,13 +68,13 @@ export function LogViewer({
const filters: Array<"all" | "blocked" | "flagged" | "passed"> = ["all", "blocked", "flagged", "passed"];
const startTime = startDate
? moment(startDate).utc().format("YYYY-MM-DD HH:mm:ss")
? moment(startDate).startOf("day").utc().format("YYYY-MM-DD HH:mm:ss")
: moment().subtract(24, "hours").utc().format("YYYY-MM-DD HH:mm:ss");
const endTime = endDate
? moment(endDate).utc().endOf("day").format("YYYY-MM-DD HH:mm:ss")
? moment(endDate).endOf("day").utc().format("YYYY-MM-DD HH:mm:ss")
: moment().utc().format("YYYY-MM-DD HH:mm:ss");
const { data: fullLogResponse } = useQuery({
const { data: fullLogResponse, isFetching: isFetchingFullLog } = useQuery({
queryKey: ["spend-log-by-request", selectedRequestId, startTime, endTime],
queryFn: async () => {
if (!accessToken || !selectedRequestId) return null;
@ -197,6 +197,7 @@ export function LogViewer({
open={drawerOpen}
onClose={handleCloseDrawer}
logEntry={selectedLog}
logEntryLoading={isFetchingFullLog}
accessToken={accessToken}
allLogs={selectedLog ? [selectedLog] : []}
startTime={startTime}

View file

@ -124,6 +124,30 @@ describe("LogDetailsDrawer session sidebar sorting", () => {
});
});
describe("LogDetailsDrawer without a resolved log", () => {
const renderDrawer = (props: { logEntry: LogEntry | null; logEntryLoading?: boolean }) => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(
<QueryClientProvider client={queryClient}>
<LogDetailsDrawer open onClose={() => {}} accessToken="token" {...props} />
</QueryClientProvider>,
);
};
it("explains why the details are missing instead of rendering nothing", () => {
renderDrawer({ logEntry: null });
expect(screen.getByText(/Log details are unavailable for this request/i)).toBeDefined();
});
it("shows a spinner while the log is still being fetched", () => {
renderDrawer({ logEntry: null, logEntryLoading: true });
expect(document.body.querySelector(".ant-spin")).not.toBeNull();
expect(screen.queryByText(/Log details are unavailable for this request/i)).toBeNull();
});
});
describe("LogDetailsDrawer session sidebar auto-router icon", () => {
const routedSessionLogs = [
makeLog({ request_id: "routed", model: "claude-opus-4-8", model_group: "smart-router" }),

View file

@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { Button, Drawer, Segmented } from "antd";
import { Button, Drawer, Segmented, Spin } from "antd";
import { CheckOutlined, CopyOutlined, LeftOutlined, RightOutlined } from "@ant-design/icons";
import { Bot, Sparkles, Wrench } from "lucide-react";
import { LogEntry } from "../columns";
@ -20,6 +20,7 @@ export interface LogDetailsDrawerProps {
open: boolean;
onClose: () => void;
logEntry: LogEntry | null;
logEntryLoading?: boolean;
sessionId?: string | null;
accessToken?: string | null;
allLogs?: LogEntry[];
@ -112,6 +113,7 @@ export function LogDetailsDrawer({
open,
onClose,
logEntry,
logEntryLoading = false,
sessionId,
accessToken,
allLogs = [],
@ -124,7 +126,7 @@ export function LogDetailsDrawer({
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [copiedLeftPanelId, setCopiedLeftPanelId] = useState(false);
const { data: sessionData } = useQuery({
const { data: sessionData, isFetching: isFetchingSession } = useQuery({
queryKey: ["sessionLogs", sessionId],
queryFn: async () => {
if (!sessionId || !accessToken) return { logs: [] as LogEntry[], total: 0 };
@ -292,7 +294,36 @@ export function LogDetailsDrawer({
}
};
if (!currentLog || !enrichedLog) return null;
if (!currentLog || !enrichedLog) {
if (!open) return null;
const isResolvingLog = isSessionMode ? isFetchingSession : logEntryLoading;
return (
<Drawer
title={null}
placement="right"
onClose={onClose}
open={open}
width={DRAWER_WIDTH}
closable={false}
mask={true}
maskClosable={true}
styles={{ header: { display: "none" } }}
>
<div className="h-full flex items-center justify-center">
{isResolvingLog ? (
<Spin />
) : (
<p className="text-sm text-slate-500 text-center px-6">
Log details are unavailable for this request. It may have been purged from the spend logs, or it falls
outside the selected date range.
</p>
)}
</div>
</Drawer>
);
}
return (
<Drawer