fix(logs): address alias pagination review feedback

This commit is contained in:
zhengxinliang 2026-10-05 20:15:38 +08:00
parent a1eb204c7e
commit 7a4848912f
3 changed files with 100 additions and 14 deletions

View file

@ -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]:

View file

@ -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 = {

View file

@ -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>