mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
fix(logs): address alias pagination review feedback
This commit is contained in:
parent
a1eb204c7e
commit
7a4848912f
3 changed files with 100 additions and 14 deletions
|
|
@ -7178,8 +7178,38 @@ async def test_ui_view_spend_logs_grouped_exclusion_reaches_page_count_and_repre
|
|||
page_rows = [
|
||||
_session_page_row("sess-1", "2026-08-29 10:00:00"),
|
||||
_session_page_row("sess-2", "2026-08-29 09:00:00"),
|
||||
_session_page_row("sess-3", "2026-08-29 08:00:00"),
|
||||
]
|
||||
mock_prisma = _session_grouped_mock_prisma(page_rows, 2, [_session_representative_row("req-1", "sess-1")])
|
||||
session_aliases = {"sess-1": "noisy", "sess-2": "quiet", "sess-3": "quiet"}
|
||||
representatives = []
|
||||
for request_id, session_id in (("req-1", "sess-1"), ("req-2", "sess-2"), ("req-3", "sess-3")):
|
||||
representative = _session_representative_row(request_id, session_id)
|
||||
representative["metadata"] = {"user_api_key_alias": session_aliases[session_id]}
|
||||
representatives.append(representative)
|
||||
|
||||
async def mock_query_raw(sql_query, *params):
|
||||
excluded_alias_match = re.search(r"user_api_key_alias' IS DISTINCT FROM \$(\d+)", sql_query)
|
||||
excluded_alias = params[int(excluded_alias_match.group(1)) - 1] if excluded_alias_match else None
|
||||
visible_session_ids = {session_id for session_id, alias in session_aliases.items() if alias != excluded_alias}
|
||||
if "COUNT(*) AS total_count" in sql_query:
|
||||
return [{"total_count": len(visible_session_ids)}]
|
||||
if "DISTINCT ON" in sql_query:
|
||||
requested_sessions = set(params[-2])
|
||||
requested_api_keys = set(params[-1])
|
||||
return [
|
||||
representative
|
||||
for representative in representatives
|
||||
if representative["session_id"] in visible_session_ids
|
||||
and representative["session_id"] in requested_sessions
|
||||
and representative["api_key"] in requested_api_keys
|
||||
]
|
||||
if "COALESCE(SUM(spend)" in sql_query:
|
||||
return []
|
||||
return [row for row in page_rows if row["session_key"] in visible_session_ids]
|
||||
|
||||
mock_prisma = MagicMock()
|
||||
mock_prisma.db = MagicMock()
|
||||
mock_prisma.db.query_raw = AsyncMock(side_effect=mock_query_raw)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.spend_tracking.spend_management_endpoints._is_admin_view_safe",
|
||||
|
|
@ -7203,6 +7233,7 @@ async def test_ui_view_spend_logs_grouped_exclusion_reaches_page_count_and_repre
|
|||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["total"] == 2
|
||||
assert [row["request_id"] for row in response.json()["data"]] == ["req-2"]
|
||||
queries = [call.args for call in mock_prisma.db.query_raw.await_args_list]
|
||||
assert len(queries) >= 3
|
||||
for sql, *params in queries[:3]:
|
||||
|
|
|
|||
|
|
@ -339,6 +339,40 @@ describe("RequestLogsPanel", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("drops the cursor and returns to the first page when the alias changes through URL navigation", async () => {
|
||||
const firstPage = Array.from({ length: 50 }, (_, index) => logEntry({ request_id: `req-${index}` }));
|
||||
vi.mocked(uiSpendLogsCall).mockImplementation(async ({ page }) => ({
|
||||
data: firstPage,
|
||||
total: 80,
|
||||
page: page ?? 1,
|
||||
page_size: 50,
|
||||
total_pages: 2,
|
||||
next_session_cursor: "2026-07-07 09:50:13|key-1|sess-1",
|
||||
has_more: true,
|
||||
}));
|
||||
const tree = (searchParams: string) => (
|
||||
<NuqsTestingAdapter searchParams={searchParams} hasMemory>
|
||||
<QueryClientProvider client={testQueryClient}>
|
||||
<RequestLogsPanel {...defaultProps} />
|
||||
</QueryClientProvider>
|
||||
</NuqsTestingAdapter>
|
||||
);
|
||||
const view = render(tree(""));
|
||||
|
||||
await waitFor(() => expect(row("req-0")).not.toBeNull());
|
||||
fireEvent.click(screen.getByTestId("pagination-next"));
|
||||
await waitFor(() => expect(lastCall()?.page).toBe(2));
|
||||
|
||||
view.rerender(tree("exclude_key_alias=noisy"));
|
||||
|
||||
await waitFor(() => {
|
||||
const call = lastCall();
|
||||
expect(call?.page).toBe(1);
|
||||
expect(call?.params?.exclude_key_alias).toBe("noisy");
|
||||
expect(call?.params?.session_cursor).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores another next click while the next page is still fetching", async () => {
|
||||
const firstPage = Array.from({ length: 50 }, (_, index) => logEntry({ request_id: `req-${index}` }));
|
||||
const firstResponse = {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
{ key_alias: parseAsString, exclude_key_alias: parseAsString },
|
||||
{ history: "push" },
|
||||
);
|
||||
const [observedAliasParams, setObservedAliasParams] = useState({ key_alias, exclude_key_alias });
|
||||
const columnFilters = useMemo<ColumnFiltersState>(() => {
|
||||
const aliasFilters: ColumnFiltersState = [];
|
||||
if (exclude_key_alias) {
|
||||
|
|
@ -70,6 +71,13 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
columnFiltersRef.current = columnFilters;
|
||||
}, [columnFilters]);
|
||||
const [sessionCursors, setSessionCursors] = useState<Record<number, string>>({});
|
||||
const aliasParamsChanged =
|
||||
observedAliasParams.key_alias !== key_alias || observedAliasParams.exclude_key_alias !== exclude_key_alias;
|
||||
const effectivePagination = useMemo(
|
||||
() => (aliasParamsChanged ? { ...pagination, pageIndex: 0 } : pagination),
|
||||
[aliasParamsChanged, pagination],
|
||||
);
|
||||
const effectiveSessionCursors = aliasParamsChanged ? {} : sessionCursors;
|
||||
|
||||
const [timeRange, setTimeRange] = useState<LogsTimeRange>(defaultLogsTimeRange);
|
||||
const { startTime, endTime, isCustomDate } = timeRange;
|
||||
|
|
@ -124,10 +132,10 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
excludeInternalHealthChecks,
|
||||
startTime,
|
||||
endTime,
|
||||
pagination,
|
||||
pagination: effectivePagination,
|
||||
isCustomDate,
|
||||
sorting,
|
||||
sessionCursors,
|
||||
sessionCursors: effectiveSessionCursors,
|
||||
});
|
||||
|
||||
// Follow the table's own last fetch so a live-tail refresh carries the filter
|
||||
|
|
@ -192,7 +200,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
const isDrawerOpen = displayLog !== null || displaySessionId !== null;
|
||||
|
||||
const rows: LogEntry[] = filteredLogs.data;
|
||||
const rowsThroughThisPage = pagination.pageIndex * pagination.pageSize + rows.length;
|
||||
const rowsThroughThisPage = effectivePagination.pageIndex * effectivePagination.pageSize + rows.length;
|
||||
const isLastPage =
|
||||
filteredLogs.has_more === false || (filteredLogs.has_more === undefined && rows.length < pagination.pageSize);
|
||||
const rowCount = isLastPage ? rowsThroughThisPage : Math.max(filteredLogs.total, rowsThroughThisPage);
|
||||
|
|
@ -241,24 +249,37 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
[setAliasParams],
|
||||
);
|
||||
|
||||
const resetToFirstPage = useCallback(() => {
|
||||
setSessionCursors({});
|
||||
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
|
||||
}, []);
|
||||
const resetToFirstPage = useCallback(
|
||||
(nextAliasParams?: { key_alias: string | null; exclude_key_alias: string | null }) => {
|
||||
if (nextAliasParams) {
|
||||
setObservedAliasParams(nextAliasParams);
|
||||
}
|
||||
setSessionCursors({});
|
||||
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!aliasParamsChanged) return;
|
||||
// Browser history changes arrive outside the table event handlers, so local cursor state must follow the URL.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
resetToFirstPage({ key_alias, exclude_key_alias });
|
||||
}, [aliasParamsChanged, key_alias, exclude_key_alias, resetToFirstPage]);
|
||||
|
||||
const handlePaginationChange = useCallback<OnChangeFn<PaginationState>>(
|
||||
(updaterOrValue) => {
|
||||
const requested = typeof updaterOrValue === "function" ? updaterOrValue(pagination) : updaterOrValue;
|
||||
const requested = typeof updaterOrValue === "function" ? updaterOrValue(effectivePagination) : updaterOrValue;
|
||||
if (!usesSessionCursor) {
|
||||
setPagination(requested);
|
||||
return;
|
||||
}
|
||||
if (requested.pageSize !== pagination.pageSize) {
|
||||
if (requested.pageSize !== effectivePagination.pageSize) {
|
||||
setSessionCursors({});
|
||||
setPagination({ ...requested, pageIndex: 0 });
|
||||
return;
|
||||
}
|
||||
if (requested.pageIndex !== pagination.pageIndex + 1) {
|
||||
if (requested.pageIndex !== effectivePagination.pageIndex + 1) {
|
||||
setPagination(requested);
|
||||
return;
|
||||
}
|
||||
|
|
@ -267,7 +288,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
setSessionCursors((previous) => ({ ...previous, [requested.pageIndex]: nextCursor }));
|
||||
setPagination(requested);
|
||||
},
|
||||
[usesSessionCursor, pagination, filteredLogs.next_session_cursor, logsQuery.isPlaceholderData],
|
||||
[usesSessionCursor, effectivePagination, filteredLogs.next_session_cursor, logsQuery.isPlaceholderData],
|
||||
);
|
||||
|
||||
const handleExcludeInternalHealthChecksChange = useCallback(
|
||||
|
|
@ -345,7 +366,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
rowCount={rowCount}
|
||||
isLoading={logsQuery.isLoading}
|
||||
isRefreshing={logsQuery.isFetching}
|
||||
pagination={pagination}
|
||||
pagination={effectivePagination}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
|
|
@ -363,7 +384,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
<LogsToolbar>
|
||||
<LogsTimeRangePicker value={timeRange} onValueChange={handleTimeRangeChange} />
|
||||
<LogsToolbarSwitch label="Live Tail" checked={isLiveTail} onCheckedChange={setIsLiveTail} />
|
||||
{isLiveTail && pagination.pageIndex === 0 && (
|
||||
{isLiveTail && effectivePagination.pageIndex === 0 && (
|
||||
<span role="status" className="whitespace-nowrap text-xs text-muted-foreground">
|
||||
Refreshing every 15s
|
||||
</span>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue