mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-17 23:51:30 +00:00
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:
parent
8177230a29
commit
df013f6b77
4 changed files with 126 additions and 6 deletions
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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" }),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue