From 1eadd7aba0bdb2cf7ce038f10b44291ad67e0aff Mon Sep 17 00:00:00 2001 From: ishaan-berri <155045088+ishaan-berri@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:41:55 -0700 Subject: [PATCH] fix(ui): label lens trace services as agents (#44116) * fix(ui): label the lens trace service filter as agent * fix(ui): rename the lens traces service column to agent * refactor(ui): name the lens trace filter state after agents * test(ui): cover the agent column and filter on lens traces * style(ui): format lens runs toolbar with prettier Co-Authored-By: Claude Opus 5.5 * refactor(ui): name the lens selection footer condition Keeps local/no-long-condition-chain within its eslint budget on main Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .../lens/_components/ActivityScope.tsx | 11 ++++--- .../TraceView/AgentTracesSection.test.tsx | 25 ++++++++++++++- .../TraceView/AgentTracesSection.tsx | 24 +++++++------- .../view_logs/TraceView/AgentTracesTable.tsx | 2 +- .../view_logs/TraceView/RunsToolbar.tsx | 31 +++++++++---------- 5 files changed, 57 insertions(+), 36 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/ActivityScope.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/ActivityScope.tsx index a8f845b02fd..91a74abe8e1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/ActivityScope.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/ActivityScope.tsx @@ -417,6 +417,7 @@ function MatchingActivity({ }) { const paginated = data?.next_offset != null || offset > 0; const showSelection = selectedCount !== data?.eligible || paginated; + const selectionData = ready && showSelection ? data : undefined; return (
@@ -473,15 +474,15 @@ function MatchingActivity({
))} - {ready && data && showSelection && ( + {selectionData && (

{selectedCount} selected for analysis {paginated && ( <> {" "} - · Showing {offset + (data.executions.length ? 1 : 0)}–{offset + data.executions.length} of{" "} - {data.eligible} + · Showing {offset + (selectionData.executions.length ? 1 : 0)}– + {offset + selectionData.executions.length} of {selectionData.eligible} )}

@@ -498,8 +499,8 @@ function MatchingActivity({ diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesSection.test.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesSection.test.tsx index 6bbd54f50ad..595bd7684d5 100644 --- a/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesSection.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesSection.test.tsx @@ -1,9 +1,10 @@ import { act, fireEvent, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ApiError } from "@/lib/http/client"; -import { renderWithProviders, testQueryClient } from "../../../../tests/test-utils"; +import { chooseSelectOption, renderWithProviders, testQueryClient } from "../../../../tests/test-utils"; import traceList from "./__fixtures__/trace_list.json"; import AgentTracesPage from "./AgentTracesPage"; import { AgentTracesSection, filterRuns } from "./AgentTracesSection"; @@ -269,6 +270,28 @@ describe("AgentTracesSection", () => { expect(rows[0]).toHaveTextContent("Should we store OTEL agent spans"); }); + it("labels the OTEL service as the agent and filters runs by it", async () => { + vi.mocked(agentTraceListCall).mockResolvedValue({ + ...(traceList as TracePage), + data: [...runs.slice(1), { ...runs[0], service: "billing-agent" }], + }); + const user = userEvent.setup(); + renderSection(); + await screen.findAllByTestId("agent-trace-row"); + + expect(screen.getByRole("columnheader", { name: "Agent" })).toBeInTheDocument(); + const agentFilter = screen.getByRole("combobox", { name: "Filter by agent" }); + expect(agentFilter).toHaveTextContent("All agents"); + + await chooseSelectOption(user, agentFilter, "billing-agent"); + const rows = screen.getAllByTestId("agent-trace-row"); + expect(rows).toHaveLength(1); + expect(rows[0]).toHaveTextContent("billing-agent"); + + await chooseSelectOption(user, agentFilter, "All agents"); + expect(screen.getAllByTestId("agent-trace-row")).toHaveLength(runs.length); + }); + it("status filter 'Failed' keeps only runs with errors", () => { const failed = filterRuns(runs, "", "all", "error"); expect(failed.length).toBeGreaterThan(0); diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesSection.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesSection.tsx index 851f2b50a6c..06094864230 100644 --- a/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesSection.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesSection.tsx @@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button"; import { AgentTracesTable } from "./AgentTracesTable"; import { RunDrawer } from "./RunDrawer"; -import { ALL_SERVICES, RunsToolbar, type RunStatusFilter } from "./RunsToolbar"; +import { ALL_AGENTS, RunsToolbar, type RunStatusFilter } from "./RunsToolbar"; import type { TraceSummary } from "./traceTypes"; import { previewText } from "./traceUtils"; import { TimeRangeControls } from "./TimeRangeControls"; @@ -16,21 +16,21 @@ import { ActiveDot } from "./ActiveDot"; import { TracingSetupCard } from "./TracingSetupCard"; import { type AgentTracesResult, traceWindowStartMs, useAgentTraces, useTraceAvailability } from "./useAgentTraces"; -/** Client-side search (input text or trace id) plus service / status filters over the loaded runs. */ +/** Client-side search (input text or trace id) plus agent / status filters over the loaded runs. */ export function filterRuns( runs: TraceSummary[], query: string, - service: string, + agent: string, status: RunStatusFilter, ): TraceSummary[] { const q = query.trim().toLowerCase(); return runs.filter((run) => { const haystack = [run.trace_id, previewText(run.input_preview), run.name].map((s) => s.toLowerCase()); const matchesQuery = !q || haystack.some((text) => text.includes(q)); - const matchesService = service === ALL_SERVICES || run.service === service; + const matchesAgent = agent === ALL_AGENTS || run.service === agent; const failed = run.error_count > 0; const matchesStatus = status === "all" || (status === "error" ? failed : !failed); - return matchesQuery && matchesService && matchesStatus; + return matchesQuery && matchesAgent && matchesStatus; }); } @@ -105,7 +105,7 @@ export function AgentTracesSection({ }: AgentTracesSectionProps) { const [openTrace, setOpenTrace] = useState(null); const [query, setQuery] = useState(""); - const [service, setService] = useState(ALL_SERVICES); + const [agent, setAgent] = useState(ALL_AGENTS); const [status, setStatus] = useState("all"); const [showSetup, setShowSetup] = useState(false); const [zoom, setZoom] = useState(null); @@ -121,7 +121,7 @@ export function AgentTracesSection({ if (setup.disabledDetail == null) void history.refetch(); }; - const services = useMemo(() => Array.from(new Set(traces.traces.map((t) => t.service))).sort(), [traces.traces]); + const agents = useMemo(() => Array.from(new Set(traces.traces.map((t) => t.service))).sort(), [traces.traces]); // Relative ranges end "now" (the list query uses Date.now() too); round to the minute so the histogram is stable. const endMs = isCustomDate ? moment(endTime).valueOf() : moment().endOf("minute").valueOf(); const range = useMemo( @@ -129,8 +129,8 @@ export function AgentTracesSection({ [startTime, endTime, isCustomDate, endMs], ); const filtered = useMemo( - () => filterRuns(traces.traces, query, service, status), - [traces.traces, query, service, status], + () => filterRuns(traces.traces, query, agent, status), + [traces.traces, query, agent, status], ); const runs = useMemo(() => (zoom ? filterByWindow(filtered, zoom) : filtered), [filtered, zoom]); @@ -193,11 +193,11 @@ export function AgentTracesSection({