diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.integration.test.tsx
index 179de1f6643..0d9344fdd14 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.integration.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.integration.test.tsx
@@ -1,8 +1,10 @@
-import { screen, within } from "@testing-library/react";
+import { act, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders, testQueryClient } from "@/../tests/test-utils";
+import { ApiError } from "@/lib/http/client";
import { apiClient } from "@/components/networking";
+import { LIVE_TAIL_INTERVAL_MS } from "@/components/view_logs/log_filter_logic";
import { LensView } from "./LensView";
import { nextCheckStatus, type Lens, type Finding } from "./lensData";
@@ -217,12 +219,16 @@ it("runs saved settings immediately without opening setup", async () => {
it("guides a first-time administrator into worker connection and lens setup", async () => {
testQueryClient.clear();
vi.mocked(apiClient.get).mockImplementation(async (path) =>
- path === "/lens" ? { lenses: [], workers: [], tracing_enabled: true } : { data: [] },
+ path === "/lens" ? { lenses: [], workers: [], tracing_enabled: true } : { data: [{ trace_id: "first-trace" }] },
);
const user = userEvent.setup();
renderWithProviders( );
const guide = within(await screen.findByRole("region", { name: "Understand what your agents are doing" }));
- expect(guide.getByRole("link", { name: "View logs" })).toHaveAttribute("href", "/ui/logs/");
+ expect(apiClient.get).toHaveBeenCalledWith("/v1/traces", { accessToken: "test", query: { start_ms: 0 } });
+ expect(guide.getByRole("link", { name: "View traces" })).toHaveAttribute(
+ "href",
+ expect.stringMatching(/^\/ui\/lens\/?\?tab=traces$/),
+ );
await user.click(guide.getByRole("button", { name: "Connect analyzer" }));
const connection = within(await screen.findByRole("dialog", { name: "Set up Lens analysis" }));
expect(connection.getByRole("button", { name: "Generate setup command" })).toBeVisible();
@@ -298,3 +304,78 @@ it("reads request content from the beginning after its abbreviated preview", asy
await user.click(screen.getByRole("button", { name: "Previous section" }));
expect(await screen.findByText("Abbreviated preview")).toBeVisible();
});
+
+it.each([false, true])(
+ "directs a new user to traces when tracing_enabled=%s and there are no traces",
+ async (enabled) => {
+ testQueryClient.clear();
+ vi.mocked(apiClient.get).mockImplementation(async (path) =>
+ path === "/lens" ? { lenses: [], workers: [], tracing_enabled: enabled } : { data: [] },
+ );
+ renderWithProviders( );
+ expect(await screen.findByRole("heading", { name: "Set up traces to start running investigations" })).toBeVisible();
+ expect(screen.getByRole("link", { name: "Set up traces" })).toHaveAttribute(
+ "href",
+ expect.stringMatching(/^\/ui\/lens\/?\?tab=traces$/),
+ );
+ expect(screen.queryByRole("button", { name: "Set up your first lens" })).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: "Set up analysis" })).not.toBeInTheDocument();
+ },
+);
+
+it("enables first-lens setup when a trace arrives without leaving Investigations", async () => {
+ testQueryClient.clear();
+ const traceCheck = vi.fn().mockResolvedValue({ data: [] });
+ vi.mocked(apiClient.get).mockImplementation(async (path) =>
+ path === "/lens" ? { lenses: [], workers: [], tracing_enabled: true } : traceCheck(),
+ );
+ vi.useFakeTimers();
+ try {
+ const view = renderWithProviders( );
+ await act(async () => vi.advanceTimersByTimeAsync(50));
+ expect(screen.getByRole("link", { name: "Set up traces" })).toBeVisible();
+
+ traceCheck.mockResolvedValue({ data: [{ trace_id: "first-trace" }] });
+ await act(async () => vi.advanceTimersByTimeAsync(LIVE_TAIL_INTERVAL_MS));
+ expect(screen.getByRole("button", { name: "Set up your first lens" })).toBeVisible();
+ expect(screen.queryByRole("link", { name: "Set up traces" })).not.toBeInTheDocument();
+
+ const completedChecks = traceCheck.mock.calls.length;
+ await act(async () => vi.advanceTimersByTimeAsync(LIVE_TAIL_INTERVAL_MS * 2));
+ expect(traceCheck).toHaveBeenCalledTimes(completedChecks);
+ view.unmount();
+ } finally {
+ vi.useRealTimers();
+ }
+});
+
+it("allows retrying a failed trace readiness check without treating it as an empty account", async () => {
+ testQueryClient.clear();
+ const traceCheck = vi
+ .fn()
+ .mockRejectedValueOnce(new ApiError("Trace storage unavailable", 503, {}))
+ .mockResolvedValue({ data: [] });
+ vi.mocked(apiClient.get).mockImplementation(async (path) => {
+ if (path === "/lens") return { lenses: [], workers: [], tracing_enabled: true };
+ if (path === "/v1/traces") return traceCheck();
+ return { data: [] };
+ });
+ const user = userEvent.setup();
+ renderWithProviders( );
+ expect(await screen.findByRole("alert")).toHaveTextContent("Could not check traces. Trace storage unavailable");
+ expect(screen.queryByRole("link", { name: "Set up traces" })).not.toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "Retry" }));
+ expect(await screen.findByRole("link", { name: "Set up traces" })).toBeVisible();
+});
+
+it("keeps saved investigations accessible when tracing is disabled", async () => {
+ testQueryClient.clear();
+ vi.mocked(apiClient.get).mockImplementation(async (path) => {
+ if (path === "/lens") return { lenses: [lens], workers: [], tracing_enabled: false };
+ if (path === "/lens/lens/runs") return lens.jobs;
+ return { data: [] };
+ });
+ renderWithProviders( );
+ expect(await screen.findByText(issue.title)).toBeVisible();
+ expect(screen.queryByRole("link", { name: "Set up traces" })).not.toBeInTheDocument();
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.tsx
index dacd93310fd..7a31e0773d6 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.tsx
@@ -3,18 +3,7 @@
import type { components } from "@/lib/http/schema";
import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
-import {
- Aperture,
- ArrowUpRight,
- CheckCircle2,
- Circle,
- Info,
- Layers3,
- Pause,
- Play,
- Plus,
- Settings2,
-} from "lucide-react";
+import { ArrowUpRight, CheckCircle2, Circle, Info, Layers3, Pause, Play, Plus, Settings2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "@/components/ui/sheet";
@@ -186,18 +175,9 @@ export function LensView({ accessToken, readOnly = false }: { accessToken: strin
};
return (
-
-
+
);
}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensWelcome.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensWelcome.tsx
index 0b92aa9018e..317e9c92747 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensWelcome.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensWelcome.tsx
@@ -1,18 +1,58 @@
+import Link from "next/link";
+import { isTracingNotEnabled, useTraceAvailability } from "@/components/view_logs/TraceView/useAgentTraces";
import { Aperture, ArrowUpRight, CheckCircle2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { uiHref } from "@/utils/uiHref";
export function LensWelcome({
+ accessToken,
+ tracingEnabled,
connected,
readOnly,
onConnect,
onCreate,
}: {
+ accessToken: string;
+ tracingEnabled: boolean;
connected: boolean;
readOnly: boolean;
onConnect: () => void;
onCreate: () => void;
}) {
+ const traces = useTraceAvailability(accessToken, tracingEnabled);
+ if (tracingEnabled && traces.isPending) {
+ return (
+
+ Checking for traces…
+
+ );
+ }
+ if (traces.error && !isTracingNotEnabled(traces.error)) {
+ return (
+
+
Could not check traces. {traces.error.message}
+
void traces.refetch()}>
+ Retry
+
+
+ );
+ }
+ if (!tracingEnabled || !traces.data || isTracingNotEnabled(traces.error)) {
+ return (
+
+ Set up traces to start running investigations
+ {tracingEnabled && !traces.error && (
+ No agent traces received yet.
+ )}
+
+ Set up traces
+
+
+ );
+ }
return (
@@ -34,12 +74,12 @@ export function LensWelcome({
Use the agent traces or LLM requests already in LiteLLM. Lens needs their inputs and outputs to understand
what happened.
-
- View logs
-
+ View traces
+
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/page.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.test.tsx
new file mode 100644
index 00000000000..d57ed0dc427
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.test.tsx
@@ -0,0 +1,63 @@
+import { screen } 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 LensPage from "./page";
+
+const { auth } = vi.hoisted(() => ({ auth: vi.fn() }));
+vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: auth }));
+vi.mock("@/components/view_logs/TraceView/AgentTracesPage", () => ({
+ default: ({ isActive }: { isActive: boolean }) => Trace polling {isActive ? "active" : "paused"}
,
+}));
+vi.mock("./_components/LensView", () => ({
+ LensView: ({ readOnly }: { readOnly: boolean }) => (
+ {readOnly ? "Read-only investigations" : "Manage investigations"}
+ ),
+}));
+
+describe("Lens navigation", () => {
+ beforeEach(() => auth.mockReturnValue({ accessToken: "test-token", userRole: "Admin", isViewOnly: false }));
+
+ it("opens traces and pauses trace polling when navigating to investigations", async () => {
+ const user = userEvent.setup();
+ const onUrlUpdate = vi.fn();
+ renderWithProviders( , { onUrlUpdate });
+ expect(screen.getByRole("tab", { name: "Traces" })).toHaveAttribute("aria-selected", "true");
+ expect(screen.getByText("Trace polling active")).toBeVisible();
+ await user.click(screen.getByRole("tab", { name: "Investigations" }));
+ expect(screen.getByText("Manage investigations")).toBeVisible();
+ expect(screen.getByText("Trace polling paused")).not.toBeVisible();
+ expect(onUrlUpdate.mock.lastCall?.[0].searchParams.get("tab")).toBe("investigations");
+ await user.click(screen.getByRole("tab", { name: "Traces" }));
+ expect(screen.getByText("Trace polling active")).toBeVisible();
+ });
+
+ it("opens existing lens links directly in investigations", () => {
+ renderWithProviders( , { searchParams: "?lens=saved-lens" });
+ expect(screen.getByRole("tab", { name: "Investigations" })).toHaveAttribute("aria-selected", "true");
+ expect(screen.getByText("Manage investigations")).toBeVisible();
+ });
+
+ it.each(["Internal User", "Internal Viewer", "Org Admin"])(
+ "preserves trace access without granting investigations to %s",
+ async (userRole) => {
+ auth.mockReturnValue({ accessToken: "test-token", userRole, isViewOnly: false });
+ const user = userEvent.setup();
+ renderWithProviders( );
+ expect(screen.getByText("Trace polling active")).toBeVisible();
+ await user.click(screen.getByRole("tab", { name: "Investigations" }));
+ expect(screen.getByText(/Investigations require proxy administrator access/)).toBeVisible();
+ expect(screen.queryByText("Manage investigations")).not.toBeInTheDocument();
+ },
+ );
+
+ it.each([
+ { userRole: "Admin Viewer", isViewOnly: false },
+ { userRole: "Admin", isViewOnly: true },
+ ])("preserves read-only investigation access for $userRole with isViewOnly=$isViewOnly", (session) => {
+ auth.mockReturnValue({ accessToken: "test-token", ...session });
+ renderWithProviders( , { searchParams: "?tab=investigations" });
+ expect(screen.getByText("Read-only investigations")).toBeVisible();
+ expect(screen.queryByText("Manage investigations")).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx
index dd0dc1518a7..282262b6322 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx
@@ -1,14 +1,54 @@
"use client";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
+import { Aperture } from "lucide-react";
+import { parseAsString, parseAsStringLiteral, useQueryState } from "nuqs";
+import AgentTracesPage from "@/components/view_logs/TraceView/AgentTracesPage";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { isProxyAdminRole, isProxyAdminTierRole } from "@/utils/roles";
import { LensView } from "./_components/LensView";
export default function LensPage() {
- const { accessToken, userRole } = useAuthorized();
+ const { accessToken, userRole, isViewOnly } = useAuthorized();
+ const [tab, setTab] = useQueryState(
+ "tab",
+ parseAsStringLiteral(["traces", "investigations"]).withOptions({ history: "push" }),
+ );
+ const [lensId] = useQueryState("lens", parseAsString);
+ const activeTab = tab ?? (lensId ? "investigations" : "traces");
if (!accessToken) return null;
- if (!isProxyAdminTierRole(userRole ?? "")) {
- return Lens requires proxy administrator access.
;
- }
- return ;
+ return (
+
+
+
+ Lens
+
+ void setTab(value as "traces" | "investigations")}
+ className="min-h-0 flex-1 gap-4"
+ >
+
+
+ Traces
+
+
+ Investigations
+
+
+
+
+
+
+ {isProxyAdminTierRole(userRole ?? "") ? (
+
+ ) : (
+
+ Investigations require proxy administrator access. You can still view your traces.
+
+ )}
+
+
+
+ );
}
diff --git a/ui/litellm-dashboard/src/components/leftnav.test.tsx b/ui/litellm-dashboard/src/components/leftnav.test.tsx
index 10c5a2fc7a7..2c4a129e4cf 100644
--- a/ui/litellm-dashboard/src/components/leftnav.test.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.test.tsx
@@ -496,6 +496,7 @@ describe("Sidebar (leftnav)", () => {
expect(screen.queryByText("Guardrails Monitor")).not.toBeInTheDocument();
expect(screen.getByText("Usage")).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: "Lens Beta" })).toHaveAttribute("href", "/ui/lens");
expect(screen.getByText("Cost Optimization")).toBeInTheDocument();
});
diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx
index 2679478a414..5ac365fecfb 100644
--- a/ui/litellm-dashboard/src/components/leftnav.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.tsx
@@ -71,7 +71,6 @@ import { cn } from "@/lib/cva.config";
import { rolesWithCapability } from "../utils/capabilities";
import {
all_admin_roles,
- proxyAdminTierRoles,
internalUserRoles,
isAdminRole,
isUserTeamAdminForAnyTeam,
@@ -252,7 +251,6 @@ const menuGroups: MenuGroup[] = [
),
icon: ,
- roles: proxyAdminTierRoles,
},
{
key: "guardrails-monitor",
diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesPage.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesPage.tsx
index 5b506cadbb0..3a009c9f9fe 100644
--- a/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesPage.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesPage.tsx
@@ -8,8 +8,7 @@ import { AgentTracesSection } from "./AgentTracesSection";
const DEFAULT_RANGE_HOURS = 24;
const TIME_FORMAT = "YYYY-MM-DDTHH:mm";
-/** Agent Traces: agent runs (developer view). Lives as the "Agent Traces" tab of the Logs page. */
-export default function AgentTracesPage({ accessToken }: { accessToken: string }) {
+export default function AgentTracesPage({ accessToken, isActive = true }: { accessToken: string; isActive?: boolean }) {
const [rangeHours, setRangeHours] = useState(DEFAULT_RANGE_HOURS);
const [live, setLive] = useState(true);
const [anchor, setAnchor] = useState(() => moment());
@@ -30,7 +29,7 @@ export default function AgentTracesPage({ accessToken }: { accessToken: string }
({
+ apiClient: { get: vi.fn() },
agentTraceListCall: vi.fn(),
agentTraceCall: vi.fn(),
agentTraceSpanCall: vi.fn(),
@@ -25,7 +26,7 @@ vi.mock("./TraceDrawer", () => ({
),
}));
-import { agentTraceListCall } from "../../networking";
+import { agentTraceListCall, apiClient } from "../../networking";
const runs = (traceList as TracePage).data as TraceSummary[];
@@ -76,6 +77,7 @@ describe("AgentTracesSection", () => {
} as DOMRect);
testQueryClient.clear();
vi.mocked(agentTraceListCall).mockReset();
+ vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
});
it("renders the setup snippet when the proxy answers 501", async () => {
@@ -87,10 +89,10 @@ describe("AgentTracesSection", () => {
const card = await screen.findByTestId("tracing-setup-card");
expect(card).toHaveTextContent("Tracing is not enabled");
expect(card).toHaveTextContent("store: clickhouse");
- expect(card).toHaveTextContent("OTEL_EXPORTER_OTLP_ENDPOINT=");
+ expect(screen.getByRole("button", { name: "Check setup" })).toBeEnabled();
expect(card).not.toHaveTextContent(/langsmith/i);
- expect(card).toHaveTextContent('OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $LITELLM_API_KEY"');
- expect(card).toHaveTextContent("Let Claude Code or Codex set it up");
+ expect(card).toHaveTextContent("ClickHouse and proxy setup");
+ expect(card).toHaveTextContent("Ask your proxy administrator");
});
it("shows the waiting guide when tracing is on but no runs have arrived", async () => {
@@ -98,11 +100,97 @@ describe("AgentTracesSection", () => {
renderSection();
const card = await screen.findByTestId("tracing-setup-card");
- expect(card).toHaveTextContent("Waiting for traces");
- expect(card).toHaveTextContent("No traces detected yet");
+ expect(card).toHaveTextContent("Connect your agent");
+ expect(card).toHaveTextContent("Waiting for your first trace");
expect(card).not.toHaveTextContent("store: clickhouse");
});
+ it("keeps the trace list available when traces exist outside the current time window", async () => {
+ vi.mocked(agentTraceListCall).mockResolvedValue({ ...(traceList as TracePage), data: [] });
+ vi.mocked(apiClient.get).mockResolvedValue(traceList);
+ renderSection();
+ expect(await screen.findByText("No runs match these filters.")).toBeVisible();
+ expect(screen.queryByTestId("tracing-setup-card")).not.toBeInTheDocument();
+ expect(apiClient.get).toHaveBeenCalledWith("/v1/traces", { accessToken: "sk-test", query: { start_ms: 0 } });
+ });
+
+ it("checks proxy readiness, waits for an agent, and confirms receipt using actual query results", async () => {
+ vi.mocked(agentTraceListCall).mockRejectedValue(new ApiError("Tracing is not enabled", 501, {}));
+ renderSection();
+ const checkSetup = await screen.findByRole("button", { name: "Check setup" });
+ vi.mocked(agentTraceListCall).mockResolvedValue({ ...(traceList as TracePage), data: [] });
+ fireEvent.click(checkSetup);
+ expect(await screen.findByRole("heading", { name: "Connect your agent" })).toBeVisible();
+ expect(screen.getByText("Waiting for your first trace")).toBeVisible();
+ vi.mocked(agentTraceListCall).mockResolvedValue(traceList as TracePage);
+ fireEvent.click(screen.getByRole("button", { name: "Check for traces" }));
+ expect(await screen.findAllByTestId("agent-trace-row")).toHaveLength(runs.length);
+ expect(screen.getByText("Traces received. Select a run to inspect it.")).toBeVisible();
+ expect(screen.queryByTestId("tracing-setup-card")).not.toBeInTheDocument();
+ });
+
+ it("keeps setup visible while checking and explains when tracing is still disabled", async () => {
+ const failure = new ApiError("Tracing is not enabled", 501, {});
+ vi.mocked(agentTraceListCall).mockRejectedValue(failure);
+ renderSection();
+ const checkSetup = await screen.findByRole("button", { name: "Check setup" });
+ let rejectCheck: (error: Error) => void = () => {};
+ vi.mocked(agentTraceListCall).mockImplementationOnce(
+ () =>
+ new Promise((_, reject) => {
+ rejectCheck = reject;
+ }),
+ );
+ fireEvent.click(checkSetup);
+ expect(await screen.findByRole("button", { name: "Checking…" })).toBeDisabled();
+ expect(screen.getByRole("heading", { name: "Enable tracing" })).toBeVisible();
+ await act(async () => rejectCheck(failure));
+ expect(await screen.findByText(/Tracing is still unavailable/)).toBeVisible();
+ expect(screen.getByRole("button", { name: "Check setup" })).toBeEnabled();
+ });
+
+ it("keeps received traces and the open drawer visible during subsequent fetches", async () => {
+ vi.mocked(agentTraceListCall).mockRejectedValue(new ApiError("Tracing is not enabled", 501, {}));
+ renderSection();
+ const checkSetup = await screen.findByRole("button", { name: "Check setup" });
+ vi.mocked(agentTraceListCall).mockResolvedValue(traceList as TracePage);
+ fireEvent.click(checkSetup);
+ const rows = await screen.findAllByTestId("agent-trace-row");
+ fireEvent.click(rows[0]);
+ const drawer = screen.getByRole("complementary", { name: "Trace details" });
+
+ let finishRefresh: (page: TracePage) => void = () => {};
+ vi.mocked(agentTraceListCall).mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ finishRefresh = resolve;
+ }),
+ );
+ await act(async () => {
+ void testQueryClient.invalidateQueries({ queryKey: ["agentTraces"] });
+ });
+ expect(await screen.findByText("Updating…")).toBeVisible();
+ expect(screen.getAllByTestId("agent-trace-row")).toHaveLength(runs.length);
+ expect(screen.getByRole("complementary", { name: "Trace details" })).toBe(drawer);
+ expect(screen.queryByTestId("tracing-setup-card")).not.toBeInTheDocument();
+ await act(async () => finishRefresh(traceList as TracePage));
+ });
+
+ it("separates a failed history check from the empty list and retries that check", async () => {
+ vi.mocked(agentTraceListCall).mockResolvedValue({ ...(traceList as TracePage), data: [] });
+ vi.mocked(apiClient.get).mockRejectedValue(new ApiError("History unavailable", 503, {}));
+ renderSection();
+ expect(await screen.findByRole("alert")).toHaveTextContent("Could not check earlier traces. History unavailable");
+ expect(screen.getByText("No runs match these filters.")).toBeVisible();
+ expect(screen.queryByText(/Could not load runs/)).not.toBeInTheDocument();
+
+ vi.mocked(apiClient.get).mockResolvedValue(traceList);
+ fireEvent.click(screen.getByRole("button", { name: "Retry trace check" }));
+ await waitFor(() => expect(screen.queryByRole("alert")).not.toBeInTheDocument());
+ expect(screen.getByText("No runs match these filters.")).toBeVisible();
+ expect(agentTraceListCall).toHaveBeenCalledTimes(1);
+ });
+
it("treats a proxy without the trace routes (404) like tracing being off", async () => {
vi.mocked(agentTraceListCall).mockRejectedValue(new ApiError("Not Found", 404, { detail: "Not Found" }));
renderSection();
@@ -261,6 +349,7 @@ describe("AgentTracesPage", () => {
beforeEach(() => {
testQueryClient.clear();
vi.mocked(agentTraceListCall).mockReset();
+ vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
});
it("shows the actual range, switches presets from the popover, and toggles Live", async () => {
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 8f95573d0dd..1c07b270a2b 100644
--- a/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesSection.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesSection.tsx
@@ -14,7 +14,7 @@ import { previewText } from "./traceUtils";
import { TimeRangeControls } from "./TimeRangeControls";
import { TracesTimeline, type TimeWindow } from "./TracesTimeline";
import { TracingSetupCard } from "./TracingSetupCard";
-import { traceWindowStartMs, useAgentTraces } from "./useAgentTraces";
+import { type AgentTracesResult, traceWindowStartMs, useAgentTraces, useTraceAvailability } from "./useAgentTraces";
/** Client-side search (input text or trace id) plus service / status filters over the loaded runs. */
export function filterRuns(
@@ -61,6 +61,33 @@ interface AgentTracesSectionProps {
onRunOpenChange?: (open: boolean) => void;
}
+function useTracingSetup(traces: AgentTracesResult, isActive: boolean, rangeChanged: boolean) {
+ const [setupResult, setSetupResult] = useState<{ detail: string | null } | null>(null);
+ const waitingForFirstTrace = traces.traces.length === 0 && !rangeChanged;
+ const settledResponse = isActive && !traces.isFetching && !traces.error;
+ const rememberSetup = waitingForFirstTrace || setupResult !== null;
+ if (settledResponse && rememberSetup && setupResult?.detail !== traces.notEnabledDetail) {
+ setSetupResult({ detail: traces.notEnabledDetail });
+ }
+
+ const disabledDetail = traces.notEnabledDetail ?? (traces.isFetching ? setupResult?.detail : null);
+ const loadingFirstPage = traces.isLoading && setupResult === null;
+ const isEmpty = !loadingFirstPage && !traces.error && traces.traces.length === 0;
+ return { disabledDetail, isEmpty, received: setupResult !== null && traces.traces.length > 0 };
+}
+
+function TraceHistoryError({ history }: { history: ReturnType }) {
+ if (!history.error) return null;
+ return (
+
+
Could not check earlier traces. {history.error.message}
+
void history.refetch()}>
+ Retry trace check
+
+
+ );
+}
+
/** The Runs view: filters, the runs table and footer — or one run, in place, once a row is clicked. */
export function AgentTracesSection({
accessToken,
@@ -81,6 +108,14 @@ export function AgentTracesSection({
const [rangeChanged, setRangeChanged] = useState(false);
const traceQuery = { accessToken, startTime, endTime, isCustomDate, isLiveTail, enabled: isActive };
const traces = useAgentTraces(traceQuery);
+ const setup = useTracingSetup(traces, isActive, rangeChanged);
+ const checkHistory = setup.isEmpty && !rangeChanged;
+ const history = useTraceAvailability(accessToken, isActive && checkHistory && setup.disabledDetail == null);
+
+ const checkTraces = () => {
+ traces.refetch();
+ if (setup.disabledDetail == null) void history.refetch();
+ };
const services = 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.
@@ -106,10 +141,11 @@ export function AgentTracesSection({
onRunOpenChange?.(trace !== null);
};
- if (traces.notEnabledDetail !== null) return ;
+ if (setup.disabledDetail != null)
+ return ;
// Onboarding only on the first, default view; an empty range the user picked keeps its controls.
- const isEmpty = !traces.isLoading && !traces.error && traces.traces.length === 0;
- if (isEmpty && !rangeChanged) return ;
+ if (checkHistory && !history.error && history.data === false)
+ return ;
if (showSetup) {
return (
@@ -120,7 +156,7 @@ export function AgentTracesSection({
>
← Back to traces
-
+
);
}
@@ -130,6 +166,12 @@ export function AgentTracesSection({
return (
+ {checkHistory &&
}
+ {setup.received && (
+
+ Traces received. Select a run to inspect it.
+
+ )}
({ getProxyBaseUrl: () => "http://proxy.test/" }));
vi.mock("@/utils/dataUtils", () => ({ copyToClipboard: vi.fn().mockResolvedValue(true) }));
describe("TracingSetupCard", () => {
- it("shows the waiting state and OTEL-only setup when tracing is on", () => {
- render( );
-
- const card = screen.getByTestId("tracing-setup-card");
- expect(card).toHaveTextContent("Waiting for traces");
- expect(card).toHaveTextContent("No traces detected yet. Follow our guide to start tracing your application.");
- expect(card).toHaveTextContent("OTEL_EXPORTER_OTLP_ENDPOINT=http://proxy.test");
- expect(card).not.toHaveTextContent(/langsmith/i);
- expect(card).not.toHaveTextContent("store: clickhouse");
- });
-
- it("swaps the install command and prompt when another framework is picked", async () => {
+ it("shows agent connection guidance once tracing is enabled without presenting example data as live", async () => {
const user = userEvent.setup();
render( );
-
- expect(screen.getByTestId("tracing-setup-card")).toHaveTextContent("openinference-instrumentation-langchain");
- await user.click(screen.getByRole("radio", { name: "CrewAI" }));
-
- const card = screen.getByTestId("tracing-setup-card");
- expect(screen.getByRole("radio", { name: "CrewAI" })).toBeChecked();
- expect(card).toHaveTextContent("pip install -U opentelemetry-distro");
- expect(card).toHaveTextContent("crewai openinference-instrumentation-crewai");
- expect(card).toHaveTextContent("Send this CrewAI project's OpenTelemetry traces to LiteLLM.");
- expect(card).not.toHaveTextContent("openinference-instrumentation-langchain");
+ expect(screen.getByRole("heading", { name: "Connect your agent" })).toBeVisible();
+ expect(screen.getByText("Tracing enabled")).toBeVisible();
+ expect(screen.getByText("Waiting for your first trace")).toBeVisible();
+ expect(screen.getByRole("img", { hidden: true, name: /Example agent trace/ })).not.toBeVisible();
+ expect(screen.getByTestId("tracing-setup-card")).not.toHaveTextContent("store: clickhouse");
+ await user.click(screen.getByText("Set up manually"));
+ expect(screen.getByText(/OTEL_EXPORTER_OTLP_ENDPOINT=http:\/\/proxy.test/)).toBeVisible();
+ expect(screen.getByTestId("tracing-setup-card")).not.toHaveTextContent(/langsmith/i);
});
- it("shows the proxy config step only when tracing is not enabled", () => {
- render( );
- const card = screen.getByTestId("tracing-setup-card");
- expect(card).toHaveTextContent("Tracing is not enabled");
- expect(card).toHaveTextContent("store: clickhouse");
+ it("copies instructions for the selected framework and preserves both manual installers", async () => {
+ const user = userEvent.setup();
+ render( );
+ await chooseSelectOption(user, screen.getByRole("combobox", { name: "Your agent framework" }), "CrewAI");
+ await user.click(screen.getByRole("button", { name: "Copy setup prompt" }));
+ expect(copyToClipboard).toHaveBeenLastCalledWith(
+ codingAgentPrompt("http://proxy.test", {
+ label: "CrewAI",
+ packages: "crewai openinference-instrumentation-crewai",
+ }),
+ );
+ expect(screen.getByRole("button", { name: "Prompt copied" })).toBeVisible();
+ await user.click(screen.getByText("Set up manually"));
+ expect(screen.getByText(/pip install -U opentelemetry-distro/)).toHaveTextContent(
+ "crewai openinference-instrumentation-crewai",
+ );
+ await user.click(screen.getByRole("tab", { name: "uv" }));
+ expect(screen.getByText(/uv add opentelemetry-distro/)).toHaveTextContent(
+ "crewai openinference-instrumentation-crewai",
+ );
+ });
+
+ it("guides proxy setup before agent setup and allows checking readiness", async () => {
+ const user = userEvent.setup();
+ const onCheck = vi.fn();
+ render( );
+ expect(screen.getByRole("heading", { name: "Enable tracing" })).toBeVisible();
+ expect(screen.getByTestId("tracing-setup-card")).toHaveTextContent("store: clickhouse");
+ expect(screen.queryByRole("combobox", { name: "Your agent framework" })).not.toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "Check setup" }));
+ expect(onCheck).toHaveBeenCalledOnce();
+ expect(screen.getByText(/Tracing is still unavailable/)).toBeVisible();
});
});
describe("setup snippets", () => {
- it("points OTLP at the proxy base URL and reads the key from the environment", () => {
+ it("uses OTLP HTTP with the proxy base URL and reads the key from the environment", () => {
const env = tracingEnvSnippet("http://proxy.test");
expect(env).toContain("OTEL_EXPORTER_OTLP_ENDPOINT=http://proxy.test\n");
+ expect(env).toContain("OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf");
expect(env).not.toContain("/v1/traces");
expect(env).toContain("Bearer $LITELLM_API_KEY");
-
const prompt = codingAgentPrompt("http://proxy.test", { label: "LangChain", packages: "langchain" });
expect(prompt).toContain("base_url=http://proxy.test/v1");
expect(prompt).toContain("opentelemetry-distro opentelemetry-exporter-otlp-proto-http langchain");
+ expect(prompt).toContain("OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf");
+ expect(prompt).toContain("Lens > Traces");
expect(prompt).not.toMatch(/langsmith/i);
});
});
diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx
index 35bfb8c57d8..183dd24cb5e 100644
--- a/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx
@@ -4,6 +4,8 @@ import { ArrowUpRight, Check, Copy, Loader2 } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/cva.config";
+import { Button } from "@/components/ui/button";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { copyToClipboard } from "@/utils/dataUtils";
import previewImg from "../../../../public/assets/agent-traces-preview.png";
@@ -17,7 +19,7 @@ import pydanticAiLogo from "../../../../public/assets/logos/pydantic-ai-color.sv
import { getProxyBaseUrl } from "../../networking";
const COPIED_RESET_MS = 1500;
-const DOCS_URL = "https://docs.litellm.ai";
+const DOCS_URL = "https://docs.litellm.ai/docs/proxy/lens";
const OTEL_BASE_PACKAGES = "opentelemetry-distro opentelemetry-exporter-otlp-proto-http";
const RUN_SNIPPET = "opentelemetry-instrument python my_agent.py";
@@ -117,13 +119,6 @@ with tracer.start_as_current_span("my_agent", attributes=attrs):
},
];
-const HIGHLIGHTS = [
- ["Input and output", "What the agent was asked and what it answered, at the top of every run."],
- ["Every step, nested", "LLM calls, tool calls and subagents in one tree, with timing."],
- ["Failures pinpointed", "See whether the tool, the model or LiteLLM broke."],
- ["Hand off to Claude / Codex", "Copy one command and your coding agent debugs the run."],
-] as const;
-
const installPackages = (guide: Pick): string =>
[OTEL_BASE_PACKAGES, guide.packages].filter(Boolean).join(" ");
@@ -131,6 +126,7 @@ const installPackages = (guide: Pick): string =>
export const tracingEnvSnippet = (proxyUrl: string): string =>
[
`export OTEL_EXPORTER_OTLP_ENDPOINT=${proxyUrl}`,
+ "export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf",
'export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $LITELLM_API_KEY"',
"export OTEL_SERVICE_NAME=my-agent",
].join("\n");
@@ -142,12 +138,13 @@ export const codingAgentPrompt = (proxyUrl: string, guide: Pick",
"3. Start the app through OTEL auto-instrumentation: opentelemetry-instrument .",
`4. Point every LLM client at LiteLLM: base_url=${proxyUrl}/v1, api key from LITELLM_API_KEY.`,
"5. Give each agent and subagent a name so runs are easy to read.",
- "6. Run the agent once and confirm the run shows up in the LiteLLM UI under Logs > Agent Traces.",
+ "6. Run the agent once and confirm the run shows up in the LiteLLM UI under Lens > Traces.",
"",
"Never hardcode the key. Read it from LITELLM_API_KEY.",
].join("\n");
@@ -163,9 +160,10 @@ export const PROXY_CONFIG_SNIPPET = [
function CodeBlock({ code, tabs, wrap = false }: { code: string; tabs?: React.ReactNode; wrap?: boolean }) {
const [copied, setCopied] = useState(false);
const copy = async () => {
- await copyToClipboard(code);
- setCopied(true);
- window.setTimeout(() => setCopied(false), COPIED_RESET_MS);
+ if (await copyToClipboard(code)) {
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), COPIED_RESET_MS);
+ }
};
return (
@@ -237,154 +235,198 @@ function Step({ title, children }: { title: string; children: React.ReactNode })
);
}
-function SetupStatus({ detail, connected }: { detail: string | null; connected: boolean }) {
- const pill = "inline-flex items-center gap-1.5 rounded-full bg-muted px-2.5 py-1 text-[12px] text-foreground";
- const hint = "text-[13px] text-muted-foreground";
- if (connected) {
- return (
- <>
-
- Receiving traces
-
-
Add another agent: point its OpenTelemetry exporter at LiteLLM.
- >
- );
- }
- if (detail === null) {
- return (
- <>
-
- Waiting for traces…
-
-
No traces detected yet. Follow our guide to start tracing your application.
- >
- );
- }
+function TraceReceipt({
+ connected,
+ checked,
+ checking,
+ onCheck,
+}: {
+ connected: boolean;
+ checked: boolean;
+ checking: boolean;
+ onCheck: () => void;
+}) {
+ const missingAfterCheck = checked && !checking && !connected;
return (
- <>
-
Tracing is not enabled
-
Turn on tracing in the proxy config, then point your agent at LiteLLM.
- >
- );
-}
-
-function WhatYoullSee() {
- return (
-
- What you'll see
-
-
+
+
+ Keep this page open. Your runs will appear here when LiteLLM receives them.
+
+
+
+ {checking && }
+ {checking ? "Checking…" : "Check for traces"}
+
+ {!connected && (
+
+ Waiting for your first trace
+
+ )}
-
- {HIGHLIGHTS.map(([title, body]) => (
-
- ))}
-
-
+ {missingAfterCheck && (
+
+ No traces received yet. Check the exporter URL and LiteLLM key in your agent’s environment, then check its
+ logs for export errors.
+
+ )}
+
);
}
-function FrameworkPicker({ value, onChange }: { value: string; onChange: (id: string) => void }) {
- return (
-
- {FRAMEWORKS.map((f) => (
-
onChange(f.id)}
- className={cn(
- "inline-flex items-center gap-2 rounded-md border px-3 py-2 text-[13px]",
- value === f.id
- ? "border-foreground bg-accent text-foreground"
- : "border-border text-muted-foreground hover:text-foreground",
- )}
- >
-
- {f.label}
-
- ))}
-
- );
+function setupTitle(enabled: boolean, connected: boolean) {
+ if (!enabled) return "Enable tracing";
+ return connected ? "Connect another agent" : "Connect your agent";
}
-/**
- * Agent Traces onboarding: shown until the first trace arrives (and when tracing isn't enabled on the proxy).
- * `detail` is the proxy's 501 message when tracing is off; null means tracing is on and we're waiting.
- */
-export function TracingSetupCard({ detail, connected = false }: { detail: string | null; connected?: boolean }) {
+export function TracingSetupCard({
+ detail,
+ connected = false,
+ onCheck,
+ checking = false,
+}: {
+ detail: string | null;
+ connected?: boolean;
+ onCheck?: () => void;
+ checking?: boolean;
+}) {
const proxyUrl = getProxyBaseUrl().replace(/\/$/, "");
const [framework, setFramework] = useState(FRAMEWORKS[0].id);
const [installer, setInstaller] = useState
("pip");
+ const [copied, setCopied] = useState(false);
+ const [checked, setChecked] = useState(false);
const guide = FRAMEWORKS.find((f) => f.id === framework) ?? FRAMEWORKS[0];
const packages = installPackages(guide);
const install = installer === "pip" ? `pip install -U ${packages}` : `uv add ${packages}`;
+ const enabled = detail === null;
+ const copyPrompt = async () => {
+ if (await copyToClipboard(codingAgentPrompt(proxyUrl, guide))) {
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), COPIED_RESET_MS);
+ }
+ };
+ const check = () => {
+ setChecked(true);
+ onCheck?.();
+ };
return (
-
-
-
+
+
{setupTitle(enabled, connected)}
+
+ {enabled
+ ? "Send your agent’s runs to LiteLLM to see its inputs, outputs, and tool calls."
+ : "Tracing needs ClickHouse and a small update to your LiteLLM proxy configuration."}
+
+
+ {enabled && }
+ {enabled ? "Tracing enabled" : "Tracing is not enabled"}
+
-
-
-
-
- Send your agent's OpenTelemetry traces to LiteLLM
-
-
- Standard OTLP. Pick your framework, set three env vars, and run your agent as usual.
-
-
-
- {detail !== null && (
-
- config.yaml} />
-
- )}
-
-
-
- Paste this into your coding agent from the project root, or follow the steps below by hand.
+ {!enabled ? (
+ <>
+
+
+ Set your ClickHouse writer and read-only reader URLs, add this to config.yaml, then restart the proxy. Ask
+ your proxy administrator if you don’t manage this deployment.
- Prompt} />
+ config.yaml} />
+
+ ClickHouse and proxy setup
+
-
-
- }
+ {checked && !checking && (
+
+ Tracing is still unavailable. Check that the configuration was applied to this proxy and it has restarted.
+
+ )}
+
+ {checking && }
+ {checking ? "Checking…" : "Check setup"}
+
+ >
+ ) : (
+ <>
+
+
+ Your agent framework
+
+
{
+ if (value) {
+ setFramework(value);
+ setCopied(false);
+ }
+ }}
+ >
+
+ {guide.label}
+
+
+ {FRAMEWORKS.map((f) => (
+
+
+ {f.label}
+
+ ))}
+
+
+
+
+
+ Paste the setup prompt into Claude Code or Codex in your agent’s project. It uses your existing LiteLLM
+ key from the environment.
+
+ void copyPrompt()}>
+ {copied ? (
+
+ ) : (
+
+ )}
+ {copied ? "Prompt copied" : "Copy setup prompt"}
+
+
+
+ Set up manually
+
+ }
+ />
+
+
+ Shell} />
+
+
+
+ Replace the example model with a model configured on your proxy.
+
+ my_agent.py}
+ />
+
+ Shell} />
+
+
+
+
+
+ See an example trace
+ Example only. These are not your agent’s runs.
+
-
-
-
- Shell} />
-
-
-
- my_agent.py}
- />
-
- Shell} />
-
-
-
-
+
+ >
+ )}
);
}
diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/useAgentTraces.ts b/ui/litellm-dashboard/src/components/view_logs/TraceView/useAgentTraces.ts
index 4e17331b001..b01cfebaaa1 100644
--- a/ui/litellm-dashboard/src/components/view_logs/TraceView/useAgentTraces.ts
+++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/useAgentTraces.ts
@@ -1,10 +1,10 @@
-import { useInfiniteQuery } from "@tanstack/react-query";
+import { useInfiniteQuery, useQuery, type UseQueryOptions } from "@tanstack/react-query";
import moment from "moment";
import { useMemo } from "react";
import { ApiError } from "@/lib/http/client";
-import { agentTraceListCall } from "../../networking";
+import { agentTraceListCall, apiClient } from "../../networking";
import { LIVE_TAIL_INTERVAL_MS } from "../log_filter_logic";
import type { TracePage, TraceSummary } from "./traceTypes";
@@ -89,3 +89,16 @@ export function useAgentTraces({
refetch: () => void query.refetch(),
};
}
+
+export function useTraceAvailability(accessToken: string, enabled: boolean) {
+ const options: UseQueryOptions
= {
+ queryKey: ["trace-availability", accessToken],
+ queryFn: () => apiClient.get("/v1/traces", { accessToken, query: { start_ms: 0 } }),
+ select: (page: TracePage) => page.data.length > 0,
+ enabled,
+ retry: false,
+ refetchInterval: (query) => (query.state.data?.data.length ? false : LIVE_TAIL_INTERVAL_MS),
+ refetchIntervalInBackground: false,
+ };
+ return useQuery(options);
+}
diff --git a/ui/litellm-dashboard/src/components/view_logs/index.test.tsx b/ui/litellm-dashboard/src/components/view_logs/index.test.tsx
index e904080ede5..7c8c14771bd 100644
--- a/ui/litellm-dashboard/src/components/view_logs/index.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/index.test.tsx
@@ -65,13 +65,13 @@ describe("SpendLogsTable", () => {
useOrganizationsMock.mockReturnValue({ data: [] });
});
- it("renders the log tabs, with Agent Traces marked new and Request Logs selected", () => {
+ it("keeps request and audit logs here while traces live in Lens", () => {
renderAs("Admin");
for (const label of ["Request Logs", "Audit Logs", "Deleted Keys", "Deleted Teams"]) {
expect(screen.getByRole("tab", { name: label })).toBeInTheDocument();
}
- expect(screen.getByRole("tab", { name: /Agent Traces/ })).toHaveTextContent("New");
+ expect(screen.queryByRole("tab", { name: /Agent Traces/ })).not.toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Request Logs" })).toHaveAttribute("aria-selected", "true");
});
@@ -117,7 +117,7 @@ describe("SpendLogsTable", () => {
it("does not hand an org admin the Audit Logs tab, which the backend still refuses them", () => {
renderAs("Internal User", ORG_ADMIN_MEMBERSHIPS);
- expect(tabNames()).toEqual(["Request Logs", "Agent TracesNew", "Deleted Keys", "Deleted Teams"]);
+ expect(tabNames()).toEqual(["Request Logs", "Deleted Keys", "Deleted Teams"]);
expect(screen.queryByTestId("audit-logs-panel")).not.toBeInTheDocument();
});
@@ -126,7 +126,7 @@ describe("SpendLogsTable", () => {
{ organization_id: "org-1", members: [{ user_id: "user-1", user_role: "internal_user" }] },
]);
- expect(tabNames()).toEqual(["Request Logs", "Agent TracesNew", "Deleted Keys"]);
+ expect(tabNames()).toEqual(["Request Logs", "Deleted Keys"]);
});
it("activates the org admin's selected tab rather than the one at the four-tab index", async () => {
diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx
index c30a960a500..fe3b99a1882 100644
--- a/ui/litellm-dashboard/src/components/view_logs/index.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx
@@ -4,7 +4,6 @@ import DeletedKeysPage from "../DeletedKeysPage/DeletedKeysPage";
import DeletedTeamsPage from "../DeletedTeamsPage/DeletedTeamsPage";
import AuditLogsPanel from "./AuditLogsPanel";
import RequestLogsPanel from "./RequestLogsPanel";
-import AgentTracesPage from "./TraceView/AgentTracesPage";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
@@ -16,24 +15,20 @@ interface SpendLogsTableProps {
premiumUser: boolean;
}
-type LogsTabId = "request logs" | "agent traces" | "audit logs" | "deleted keys" | "deleted teams";
+type LogsTabId = "request logs" | "audit logs" | "deleted keys" | "deleted teams";
interface LogsTab {
id: LogsTabId;
label: string;
- isNew?: boolean;
}
const REQUEST_LOGS_TAB: LogsTab = { id: "request logs", label: "Request Logs" };
-const AGENT_TRACES_TAB: LogsTab = { id: "agent traces", label: "Agent Traces", isNew: true };
const AUDIT_LOGS_TAB: LogsTab = { id: "audit logs", label: "Audit Logs" };
const DELETED_KEYS_TAB: LogsTab = { id: "deleted keys", label: "Deleted Keys" };
const DELETED_TEAMS_TAB: LogsTab = { id: "deleted teams", label: "Deleted Teams" };
const tabContentClassName = (tabId: LogsTabId): string =>
- tabId === REQUEST_LOGS_TAB.id || tabId === AGENT_TRACES_TAB.id
- ? "flex min-h-0 flex-1 flex-col"
- : "min-h-0 flex-1 overflow-y-auto";
+ tabId === REQUEST_LOGS_TAB.id ? "flex min-h-0 flex-1 flex-col" : "min-h-0 flex-1 overflow-y-auto";
export default function SpendLogsTable({ accessToken, token, userRole, userID, premiumUser }: SpendLogsTableProps) {
const [activeTab, setActiveTab] = useState(REQUEST_LOGS_TAB.id);
@@ -50,7 +45,6 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p
const tabs: LogsTab[] = [
REQUEST_LOGS_TAB,
- AGENT_TRACES_TAB,
...(canViewAuditLogs ? [AUDIT_LOGS_TAB] : []),
DELETED_KEYS_TAB,
...(canViewDeletedTeams ? [DELETED_TEAMS_TAB] : []),
@@ -68,8 +62,6 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p
isActive={activeTab === "request logs"}
/>
);
- case "agent traces":
- return activeTab === "agent traces" ? : null;
case "audit logs":
return (
(
{tab.label}
- {tab.isNew && (
-
- New
-
- )}
))}