"use client"; import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { Search, Filter, Clock, CheckCircle, XCircle, Activity, GitBranch, Layers, ChevronLeft, ChevronRight, ArrowRight, ChevronDown, ChevronUp, RefreshCw, ToggleLeft, ToggleRight, WifiOff, } from "lucide-react"; import { cn, formatDuration, formatRelativeTime } from "@/lib/utils"; type TraceStatus = "RUNNING" | "COMPLETED" | "ERROR"; interface Trace { id: string; name: string; status: TraceStatus; startedAt: string; endedAt: string | null; durationMs: number | null; tags: string[]; metadata: Record; _count: { decisionPoints: number; spans: number; events: number; }; } interface TraceListProps { initialTraces: Trace[]; initialTotal: number; initialTotalPages: number; initialPage: number; } type FilterStatus = "ALL" | TraceStatus; type SortOption = "newest" | "oldest" | "longest" | "shortest" | "costliest"; const statusConfig: Record; color: string; bgColor: string }> = { RUNNING: { label: "Running", icon: Activity, color: "text-amber-400", bgColor: "bg-amber-500/10 border-amber-500/20", }, COMPLETED: { label: "Completed", icon: CheckCircle, color: "text-emerald-400", bgColor: "bg-emerald-500/10 border-emerald-500/20", }, ERROR: { label: "Error", icon: XCircle, color: "text-red-400", bgColor: "bg-red-500/10 border-red-500/20", }, }; const sortOptions: { value: SortOption; label: string }[] = [ { value: "newest", label: "Newest" }, { value: "oldest", label: "Oldest" }, { value: "longest", label: "Longest duration" }, { value: "shortest", label: "Shortest duration" }, { value: "costliest", label: "Highest cost" }, ]; export function TraceList({ initialTraces, initialTotal, initialTotalPages, initialPage, }: TraceListProps) { const searchParams = useSearchParams(); const [traces, setTraces] = useState(initialTraces); const [total, setTotal] = useState(initialTotal); const [totalPages, setTotalPages] = useState(initialTotalPages); const [currentPage, setCurrentPage] = useState(initialPage); const [isLoading, setIsLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("ALL"); const [sortFilter, setSortFilter] = useState("newest"); const [tagsFilter, setTagsFilter] = useState(""); const [dateFrom, setDateFrom] = useState(""); const [dateTo, setDateTo] = useState(""); const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); const [autoRefresh, setAutoRefresh] = useState(false); const [sseConnected, setSseConnected] = useState(false); const [newTracesCount, setNewTracesCount] = useState(0); const updateUrlParams = useCallback(() => { const params = new URLSearchParams(searchParams.toString()); if (statusFilter !== "ALL") { params.set("status", statusFilter); } else { params.delete("status"); } if (searchQuery) { params.set("search", searchQuery); } else { params.delete("search"); } if (sortFilter !== "newest") { params.set("sort", sortFilter); } else { params.delete("sort"); } if (tagsFilter) { params.set("tags", tagsFilter); } else { params.delete("tags"); } if (dateFrom) { params.set("dateFrom", dateFrom); } else { params.delete("dateFrom"); } if (dateTo) { params.set("dateTo", dateTo); } else { params.delete("dateTo"); } params.set("page", currentPage.toString()); const newUrl = `${window.location.pathname}?${params.toString()}`; window.history.replaceState({}, "", newUrl); }, [searchParams, statusFilter, searchQuery, sortFilter, tagsFilter, dateFrom, dateTo, currentPage]); const fetchTraces = useCallback(async () => { setIsLoading(true); try { const params = new URLSearchParams(); params.set("page", currentPage.toString()); params.set("limit", "50"); if (statusFilter !== "ALL") { params.set("status", statusFilter); } if (searchQuery) { params.set("search", searchQuery); } if (sortFilter !== "newest") { params.set("sort", sortFilter); } if (tagsFilter) { params.set("tags", tagsFilter); } if (dateFrom) { params.set("dateFrom", dateFrom); } if (dateTo) { params.set("dateTo", dateTo); } const res = await fetch(`/api/traces?${params.toString()}`, { cache: "no-store" }); if (!res.ok) { throw new Error(`Failed to fetch traces: ${res.status}`); } const data = await res.json(); setTraces(data.traces); setTotal(data.total); setTotalPages(data.totalPages); setNewTracesCount(0); } catch (error) { console.error("Error fetching traces:", error); } finally { setIsLoading(false); } }, [currentPage, statusFilter, searchQuery, sortFilter, tagsFilter, dateFrom, dateTo]); useEffect(() => { const status = searchParams.get("status") as FilterStatus | null; const search = searchParams.get("search") ?? ""; const sort = (searchParams.get("sort") as SortOption | null) ?? "newest"; const tags = searchParams.get("tags") ?? ""; const from = searchParams.get("dateFrom") ?? ""; const to = searchParams.get("dateTo") ?? ""; const page = parseInt(searchParams.get("page") ?? "1", 10); setStatusFilter(status ?? "ALL"); setSearchQuery(search); setSortFilter(sort); setTagsFilter(tags); setDateFrom(from); setDateTo(to); setCurrentPage(page); const savedAutoRefresh = localStorage.getItem("agentlens-auto-refresh"); setAutoRefresh(savedAutoRefresh === "true"); }, [searchParams]); useEffect(() => { updateUrlParams(); }, [updateUrlParams]); useEffect(() => { fetchTraces(); }, [currentPage, fetchTraces]); useEffect(() => { if (!autoRefresh) return; const interval = setInterval(() => { fetchTraces(); }, 5000); return () => clearInterval(interval); }, [autoRefresh, fetchTraces]); useEffect(() => { localStorage.setItem("agentlens-auto-refresh", autoRefresh.toString()); }, [autoRefresh]); useEffect(() => { const eventSource = new EventSource("/api/traces/stream"); eventSource.onopen = () => { setSseConnected(true); }; eventSource.onerror = () => { setSseConnected(false); }; eventSource.addEventListener("trace-update", (event) => { try { const data = JSON.parse(event.data); if (data.type === "new") { setNewTracesCount((prev) => prev + 1); } else if (data.type === "updated") { setNewTracesCount((prev) => prev + 1); } } catch (error) { console.error("Error parsing SSE message:", error); } }); eventSource.addEventListener("heartbeat", () => { setSseConnected(true); }); return () => { eventSource.close(); setSseConnected(false); }; }, []); const filteredTraces = traces.filter((trace) => { const matchesSearch = trace.name.toLowerCase().includes(searchQuery.toLowerCase()) || trace.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()) ); const matchesStatus = statusFilter === "ALL" || trace.status === statusFilter; return matchesSearch && matchesStatus; }); const filterChips: { value: FilterStatus; label: string }[] = [ { value: "ALL", label: "All" }, { value: "RUNNING", label: "Running" }, { value: "COMPLETED", label: "Completed" }, { value: "ERROR", label: "Error" }, ]; const handlePageChange = (newPage: number) => { setCurrentPage(newPage); window.scrollTo({ top: 0, behavior: "smooth" }); }; if (traces.length === 0) { return ; } return (
{/* Header */}

Traces

{total} trace{total !== 1 ? "s" : ""} captured

{sseConnected ? ( <>
Live ) : ( <> Offline )}
{/* New Traces Banner */} {newTracesCount > 0 && (
{newTracesCount} new trace{newTracesCount !== 1 ? "s" : ""} available
)} {/* Search and Filter */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-3 bg-neutral-900 border border-neutral-800 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all" />
{filterChips.map((chip) => ( ))}
{/* Advanced Filters */}
{showAdvancedFilters && (
setDateFrom(e.target.value)} className="w-full bg-neutral-900 border border-neutral-700 rounded-lg px-3 py-2 text-sm text-neutral-100 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500 transition-all" />
setDateTo(e.target.value)} className="w-full bg-neutral-900 border border-neutral-700 rounded-lg px-3 py-2 text-sm text-neutral-100 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500 transition-all" />
setTagsFilter(e.target.value)} className="w-full bg-neutral-900 border border-neutral-700 rounded-lg px-3 py-2 text-sm text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500 transition-all" />
)}
{/* Trace List */}
{filteredTraces.map((trace) => ( ))}
{/* Empty Filtered State */} {filteredTraces.length === 0 && traces.length > 0 && (

No traces match your search criteria

)} {/* Pagination */} {totalPages > 1 && (

Page {currentPage} of {totalPages}

)}
); } function TraceCard({ trace }: { trace: Trace }) { const status = statusConfig[trace.status]; const StatusIcon = status.icon; return (
{/* Left: Name and Status */}

{trace.name}

{status.label}
{formatRelativeTime(trace.startedAt)} {formatDuration(trace.durationMs)}
{/* Middle: Stats */}
Decisions {trace._count.decisionPoints}
Spans {trace._count.spans}
{/* Right: Tags and Arrow */}
{trace.tags.length > 0 && (
{trace.tags.slice(0, 3).map((tag) => ( {tag} ))} {trace.tags.length > 3 && ( +{trace.tags.length - 3} )}
)}
); } function EmptyState() { return (

No traces yet

Install the SDK to start capturing traces from your AI agents

example.py
            
              from{" "}
              agentlens{" "}
              import{" "}
              init
              ,{" "}
              trace
              {"\n"}
              {"\n"}
              init
              (
              {"\n"}
              {"    "}
              api_key
              =
              "your-api-key"
              {"\n"}
              )
              {"\n"}
              {"\n"}
              @trace
              {"\n"}
              def{" "}
              my_agent
              ():
              {"\n"}
              {"    "}
              return{" "}
              
                "Hello, AgentLens!"
              
            
          
View Documentation
); }