From df013f6b774906acfb06e362e6f07dda9d30c9e4 Mon Sep 17 00:00:00 2001 From: milan Date: Fri, 24 Jul 2026 22:14:49 +0000 Subject: [PATCH] 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> --- .../GuardrailsMonitor/LogViewer.test.tsx | 64 +++++++++++++++++++ .../GuardrailsMonitor/LogViewer.tsx | 7 +- .../LogDetailsDrawer.test.tsx | 24 +++++++ .../LogDetailsDrawer/LogDetailsDrawer.tsx | 37 ++++++++++- 4 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.test.tsx diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.test.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.test.tsx new file mode 100644 index 00000000000..73f43528840 --- /dev/null +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.test.tsx @@ -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( + + + , + ); +}; + +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); + }); +}); diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx index c3671fc9e2c..8befdbf4d09 100644 --- a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx @@ -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} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.test.tsx index b913f7c8d21..16d4a77dd33 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.test.tsx @@ -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( + + {}} accessToken="token" {...props} /> + , + ); + }; + + 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" }), diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx index cdf8e0b1c60..23530479d4b 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx @@ -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 ( + +
+ {isResolvingLog ? ( + + ) : ( +

+ Log details are unavailable for this request. It may have been purged from the spend logs, or it falls + outside the selected date range. +

+ )} +
+
+ ); + } return (