mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
fix(ui): show all session logs in the drawer, not just the first page
The session detail drawer fetched session logs via sessionSpendLogsCall without page/page_size, so it only ever received the backend default of one page (50 rows). Sessions with more than 50 calls had the rest unreachable in the UI (#29153). sessionSpendLogsCall now takes page/page_size, and the drawer fetches the first page, reads total_pages, then fetches the remaining pages and accumulates them before the existing client-side sort. This keeps the single continuous list (and the selected-log lookup and keyboard navigation, which all assume the full session) correct. Fetching is bounded by a page cap, and the sidebar shows a "showing most recent N" note if a session exceeds it. The rows are lightweight metadata (the endpoint excludes messages/response), so the full set is small; request/response bodies are still loaded per log on demand.
This commit is contained in:
parent
0acf4812d9
commit
bc37791e5b
3 changed files with 108 additions and 9 deletions
|
|
@ -453,3 +453,50 @@ describe("teamInfoCall", () => {
|
|||
expect(parsed.searchParams.has("team_id")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessionSpendLogsCall", () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("should request the first page with defaults so the caller can page through the session", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({ data: [], total: 0, page: 1, page_size: 100, total_pages: 1 }),
|
||||
} as any);
|
||||
global.fetch = mockFetch as any;
|
||||
|
||||
await Networking.sessionSpendLogsCall("token", "session-123");
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledOnce();
|
||||
const [url] = mockFetch.mock.calls[0];
|
||||
const urlStr = typeof url === "string" ? url : (url as Request).url;
|
||||
const parsed = typeof url === "string" ? new URL(url, "http://example.com") : new URL((url as Request).url);
|
||||
|
||||
expect(urlStr).toContain("/spend/logs/session/ui");
|
||||
expect(parsed.searchParams.get("session_id")).toBe("session-123");
|
||||
expect(parsed.searchParams.get("page")).toBe("1");
|
||||
expect(parsed.searchParams.get("page_size")).toBe("100");
|
||||
});
|
||||
|
||||
it("should pass explicit page and page_size query params for later pages", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({ data: [], total: 250, page: 3, page_size: 100, total_pages: 3 }),
|
||||
} as any);
|
||||
global.fetch = mockFetch as any;
|
||||
|
||||
await Networking.sessionSpendLogsCall("token", "session-123", 3, 100);
|
||||
|
||||
const [url] = mockFetch.mock.calls[0];
|
||||
const parsed = typeof url === "string" ? new URL(url, "http://example.com") : new URL((url as Request).url);
|
||||
expect(parsed.searchParams.get("page")).toBe("3");
|
||||
expect(parsed.searchParams.get("page_size")).toBe("100");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7143,13 +7143,27 @@ export const teamPermissionsUpdateCall = async (accessToken: string, teamId: str
|
|||
};
|
||||
|
||||
/**
|
||||
* Get all spend logs for a particular session
|
||||
* Get a page of spend logs for a particular session.
|
||||
*
|
||||
* The backend paginates this endpoint (page / page_size, returning
|
||||
* { data, total, page, page_size, total_pages }). Callers that need the whole
|
||||
* session should page through total_pages and accumulate the results.
|
||||
*/
|
||||
export const sessionSpendLogsCall = async (accessToken: string, session_id: string) => {
|
||||
export const sessionSpendLogsCall = async (
|
||||
accessToken: string,
|
||||
session_id: string,
|
||||
page: number = 1,
|
||||
page_size: number = 100,
|
||||
) => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
session_id,
|
||||
page: String(page),
|
||||
page_size: String(page_size),
|
||||
});
|
||||
let url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/spend/logs/session/ui?session_id=${encodeURIComponent(session_id)}`
|
||||
: `/spend/logs/session/ui?session_id=${encodeURIComponent(session_id)}`;
|
||||
? `${proxyBaseUrl}/spend/logs/session/ui?${params.toString()}`
|
||||
: `/spend/logs/session/ui?${params.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,14 @@ export interface LogDetailsDrawerProps {
|
|||
|
||||
const SIDEBAR_WIDTH_PX = 224;
|
||||
|
||||
// Session logs are fetched page-by-page from the paginated backend and
|
||||
// accumulated so the drawer can show the whole session. page_size is the
|
||||
// backend maximum (le=100); the page cap bounds the fetch and the
|
||||
// (un-virtualized) sidebar list for pathological sessions, keeping the most
|
||||
// recent logs since the endpoint returns newest-first.
|
||||
const SESSION_PAGE_SIZE = 100;
|
||||
const MAX_SESSION_PAGES = 50;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* TraceEventRow — compact event row used in both session & non- */
|
||||
/* session sidebar lists. Extracted to avoid JSX duplication. */
|
||||
|
|
@ -112,13 +120,30 @@ export function LogDetailsDrawer({
|
|||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||
const [copiedLeftPanelId, setCopiedLeftPanelId] = useState(false);
|
||||
|
||||
const { data: sessionLogs = [] } = useQuery({
|
||||
const { data: sessionData } = useQuery({
|
||||
queryKey: ["sessionLogs", sessionId],
|
||||
queryFn: async () => {
|
||||
if (!sessionId || !accessToken) return [];
|
||||
const response = await sessionSpendLogsCall(accessToken, sessionId);
|
||||
const allSessionLogs: LogEntry[] = response.data || response || [];
|
||||
return allSessionLogs
|
||||
if (!sessionId || !accessToken) return { logs: [] as LogEntry[], total: 0 };
|
||||
|
||||
// Fetch the first page, then page through the rest so sessions with more
|
||||
// than one page of logs are shown in full (capped for safety).
|
||||
const firstPage = await sessionSpendLogsCall(accessToken, sessionId, 1, SESSION_PAGE_SIZE);
|
||||
let rows: LogEntry[] = firstPage.data || firstPage || [];
|
||||
const total: number = firstPage.total ?? rows.length;
|
||||
const pagesToFetch = Math.min(firstPage.total_pages ?? 1, MAX_SESSION_PAGES);
|
||||
|
||||
if (pagesToFetch > 1) {
|
||||
const remaining = await Promise.all(
|
||||
Array.from({ length: pagesToFetch - 1 }, (_, i) =>
|
||||
sessionSpendLogsCall(accessToken, sessionId, i + 2, SESSION_PAGE_SIZE),
|
||||
),
|
||||
);
|
||||
for (const page of remaining) {
|
||||
rows = rows.concat(page.data || []);
|
||||
}
|
||||
}
|
||||
|
||||
const logs = rows
|
||||
.map((row) => ({
|
||||
...row,
|
||||
request_duration_ms: row.request_duration_ms ?? Date.parse(row.endTime) - Date.parse(row.startTime),
|
||||
|
|
@ -129,10 +154,18 @@ export function LogDetailsDrawer({
|
|||
if (aIsMcp !== bIsMcp) return aIsMcp - bIsMcp;
|
||||
return new Date(a.startTime).getTime() - new Date(b.startTime).getTime();
|
||||
});
|
||||
|
||||
return { logs, total };
|
||||
},
|
||||
enabled: Boolean(open && isSessionMode && sessionId && accessToken),
|
||||
});
|
||||
|
||||
const sessionLogs: LogEntry[] = sessionData?.logs ?? [];
|
||||
// total reported by the backend; when the page cap truncates the fetch this
|
||||
// exceeds sessionLogs.length, which drives the "showing most recent" note.
|
||||
const sessionTotalCount = sessionData?.total ?? sessionLogs.length;
|
||||
const sessionTruncated = sessionTotalCount > sessionLogs.length;
|
||||
|
||||
const currentLog = useMemo(() => {
|
||||
if (!isSessionMode) return logEntry;
|
||||
if (!sessionLogs.length) return null;
|
||||
|
|
@ -327,6 +360,11 @@ export function LogDetailsDrawer({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
{isSessionMode && sessionTruncated && (
|
||||
<div className="mt-1 text-[11px] text-amber-600 font-mono">
|
||||
Showing most recent {logsForList.length} of {sessionTotalCount}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue