Merge pull request #19963 from BerriAI/litellm_ui_spend_logs_em_search

[Feature] UI - Logs: Adding Error message search to ui spend logs
This commit is contained in:
yuneng-jiang 2026-01-28 18:15:35 -08:00 committed by GitHub
commit e796b9eb22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 140 additions and 1 deletions

View file

@ -0,0 +1,127 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../tests/test-utils";
import FilterComponent, { FilterOption } from "./filter";
describe("FilterComponent", () => {
const mockOnApplyFilters = vi.fn();
const mockOnResetFilters = vi.fn();
const defaultOptions: FilterOption[] = [
{
name: "teamId",
label: "Team ID",
options: [
{ label: "Team 1", value: "team1" },
{ label: "Team 2", value: "team2" },
],
},
{
name: "status",
label: "Status",
options: [
{ label: "Active", value: "active" },
{ label: "Inactive", value: "inactive" },
],
},
{
name: "userId",
label: "User ID",
},
];
beforeEach(() => {
vi.clearAllMocks();
});
it("should render", () => {
renderWithProviders(
<FilterComponent
options={defaultOptions}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
expect(screen.getByRole("button", { name: "Filters" })).toBeInTheDocument();
});
it("should display custom button label", () => {
renderWithProviders(
<FilterComponent
options={defaultOptions}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
buttonLabel="Custom Filters"
/>,
);
expect(screen.getByRole("button", { name: "Custom Filters" })).toBeInTheDocument();
});
it("should call onResetFilters when reset button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(
<FilterComponent
options={defaultOptions}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
initialValues={{ teamId: "team1", status: "active" }}
/>,
);
const resetButton = screen.getByRole("button", { name: "Reset Filters" });
await user.click(resetButton);
await waitFor(() => {
expect(mockOnResetFilters).toHaveBeenCalledTimes(1);
});
});
it("should render filters in correct order", async () => {
const user = userEvent.setup();
const options: FilterOption[] = [
{ name: "model", label: "Model" },
{ name: "teamId", label: "Team ID" },
{ name: "status", label: "Status" },
{ name: "userId", label: "User ID" },
];
renderWithProviders(
<FilterComponent
options={options}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
await waitFor(() => {
const labels = screen.getAllByText(/^(Team ID|Status|User ID|Model)$/);
expect(labels[0]).toHaveTextContent("Team ID");
expect(labels[1]).toHaveTextContent("Status");
});
});
it("should handle input filter changes", async () => {
const user = userEvent.setup();
renderWithProviders(
<FilterComponent
options={defaultOptions}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
const userIdInput = screen.getByPlaceholderText("Enter User ID...");
await user.type(userIdInput, "user123");
await waitFor(() => {
expect(mockOnApplyFilters).toHaveBeenCalledWith({ userId: "user123" });
});
});
});

View file

@ -130,6 +130,7 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
"User ID",
"End User",
"Error Code",
"Error Message",
"Key Hash",
"Model",
];

View file

@ -2764,6 +2764,7 @@ export const uiSpendLogsCall = async (
model?: string,
keyAlias?: string,
error_code?: string,
error_message?: string,
) => {
try {
// Construct base URL
@ -2784,6 +2785,7 @@ export const uiSpendLogsCall = async (
if (model) queryParams.append("model", model);
if (keyAlias) queryParams.append("key_alias", keyAlias);
if (error_code) queryParams.append("error_code", error_code);
if (error_message) queryParams.append("error_message", error_message);
// Append query parameters to URL if any exist
const queryString = queryParams.toString();
if (queryString) {

View file

@ -464,6 +464,11 @@ export default function SpendLogsTable({
label: "Key Hash",
isSearchable: false,
},
{
name: "Error Message",
label: "Error Message",
isSearchable: false,
},
];
// When a session is selected, render the SessionView component

View file

@ -18,6 +18,7 @@ export const FILTER_KEYS = {
STATUS: "Status",
KEY_ALIAS: "Key Alias",
ERROR_CODE: "Error Code",
ERROR_MESSAGE: "Error Message",
} as const;
export type FilterKey = keyof typeof FILTER_KEYS;
@ -55,6 +56,7 @@ export function useLogFilterLogic({
[FILTER_KEYS.STATUS]: "",
[FILTER_KEYS.KEY_ALIAS]: "",
[FILTER_KEYS.ERROR_CODE]: "",
[FILTER_KEYS.ERROR_MESSAGE]: "",
}),
[],
);
@ -97,6 +99,7 @@ export function useLogFilterLogic({
filters[FILTER_KEYS.MODEL] || undefined,
filters[FILTER_KEYS.KEY_ALIAS] || undefined,
filters[FILTER_KEYS.ERROR_CODE] || undefined,
filters[FILTER_KEYS.ERROR_MESSAGE] || undefined,
);
if (currentTimestamp === lastSearchTimestamp.current && response.data) {
@ -137,7 +140,8 @@ export function useLogFilterLogic({
filters[FILTER_KEYS.REQUEST_ID] ||
filters[FILTER_KEYS.USER_ID] ||
filters[FILTER_KEYS.END_USER] ||
filters[FILTER_KEYS.ERROR_CODE]
filters[FILTER_KEYS.ERROR_CODE] ||
filters[FILTER_KEYS.ERROR_MESSAGE]
),
[filters],
);