import { LogDetailSheet } from "@/app/workspace/logs/sheets/logDetailsSheet"; import { SessionDetailsSheet } from "@/app/workspace/logs/sheets/sessionDetailsSheet"; import { createColumns } from "@/app/workspace/logs/views/columns"; import { EmptyState } from "@/app/workspace/logs/views/emptyState"; import { LogsHeaderView } from "@/app/workspace/logs/views/logsHeaderView"; import { LogsDataTable } from "@/app/workspace/logs/views/logsTable"; import { LogsVolumeChart } from "@/app/workspace/logs/views/logsVolumeChart"; import { LogsFilterSidebar } from "@/components/filters/logsFilterSidebar"; import FullPageLoader from "@/components/fullPageLoader"; import { useColumnConfig } from "@/components/table"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Card, CardContent } from "@/components/ui/card"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { getErrorMessage, useDeleteLogsMutation, useGetAvailableFilterDataQuery, useGetLogsHistogramQuery, useGetLogsQuery, useGetLogsStatsQuery, } from "@/lib/store"; import { useLazyGetLogByIdQuery, useLazyGetLogsQuery } from "@/lib/store/apis/logsApi"; import type { LogEntry, LogFilters, Pagination } from "@/lib/types/logs"; import { dateUtils } from "@/lib/types/logs"; import { COMPACT_NUMBER_FORMAT } from "@/lib/utils/numbers"; import { getRangeForPeriod } from "@/lib/utils/timeRange"; import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; import NumberFlow from "@number-flow/react"; import { useLocation } from "@tanstack/react-router"; import { AlertCircle, BarChart, CheckCircle, Clock, DollarSign, Hash, Info } from "lucide-react"; import { parseAsArrayOf, parseAsBoolean, parseAsInteger, parseAsString, useQueryStates } from "nuqs"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; export default function LogsPage() { const [error, setError] = useState(null); const [showEmptyState, setShowEmptyState] = useState(false); const hasCheckedEmptyState = useRef(false); const hasDeleteAccess = useRbac(RbacResource.Logs, RbacOperation.Delete); const [deleteLogs] = useDeleteLogsMutation(); // Lazy query kept only for handleLogNavigate (fetches adjacent pages on demand) const [triggerGetLogs] = useLazyGetLogsQuery(); const [selectedSessionId, setSelectedSessionId] = useState(null); const [sessionHighlightedLogId, setSessionHighlightedLogId] = useState(null); // Stable handler so SessionDetailsSheet's loadSessionPage useCallback doesn't // recreate on every parent re-render. Without this, every live WebSocket log // tick would re-render LogsPage, hand the sheet a fresh inline arrow, recreate // loadSessionPage, and trip the reset effect — wiping sessionLogs and // refetching from offset 0 while the sheet is open. const handleSessionSheetOpenChange = useCallback((open: boolean) => { if (!open) { setSelectedSessionId(null); setSessionHighlightedLogId(null); } }, []); const [isChartOpen, setIsChartOpen] = useState(true); const [triggerGetLogById] = useLazyGetLogByIdQuery(); const [fetchedLog, setFetchedLog] = useState(null); // Track if user has manually modified the time range const userModifiedTimeRange = useRef(false); // Capture initial defaults on mount to detect shared URLs with custom time ranges const initialDefaults = useRef(dateUtils.getDefaultTimeRange()); // Memoize default time range to prevent recalculation on every render // This is crucial to avoid triggering refetches when the sheet opens/closes const defaultTimeRange = useMemo(() => dateUtils.getDefaultTimeRange(), []); // Get fresh default time range for refresh logic const getDefaultTimeRange = () => dateUtils.getDefaultTimeRange(); const { search } = useLocation(); const hasExplicitTimeRange = (search as Record)?.start_time && (search as Record)?.end_time; // URL state management with nuqs - all filters and pagination in URL const [urlState, setUrlState] = useQueryStates( { parent_request_id: parseAsString.withDefault(""), providers: parseAsArrayOf(parseAsString).withDefault([]), models: parseAsArrayOf(parseAsString).withDefault([]), aliases: parseAsArrayOf(parseAsString).withDefault([]), status: parseAsArrayOf(parseAsString).withDefault([]), objects: parseAsArrayOf(parseAsString).withDefault([]), selected_key_ids: parseAsArrayOf(parseAsString).withDefault([]), virtual_key_ids: parseAsArrayOf(parseAsString).withDefault([]), routing_rule_ids: parseAsArrayOf(parseAsString).withDefault([]), routing_engine_used: parseAsArrayOf(parseAsString).withDefault([]), user_ids: parseAsArrayOf(parseAsString).withDefault([]), team_ids: parseAsArrayOf(parseAsString).withDefault([]), customer_ids: parseAsArrayOf(parseAsString).withDefault([]), business_unit_ids: parseAsArrayOf(parseAsString).withDefault([]), content_search: parseAsString.withDefault(""), start_time: parseAsInteger.withDefault(defaultTimeRange.startTime), end_time: parseAsInteger.withDefault(defaultTimeRange.endTime), limit: parseAsInteger.withDefault(25), // Default fallback, actual value calculated based on table height offset: parseAsInteger.withDefault(0), sort_by: parseAsString.withDefault("timestamp"), order: parseAsString.withDefault("desc"), polling: parseAsBoolean.withDefault(true).withOptions({ clearOnDefault: false }), period: parseAsString.withDefault(hasExplicitTimeRange ? "" : "1h").withOptions({ clearOnDefault: false }), missing_cost_only: parseAsBoolean.withDefault(false), metadata_filters: parseAsString.withDefault(""), selected_log: parseAsString.withDefault(""), }, { history: "push", shallow: false, }, ); // Derive selectedLog: find in current logs array, or fetch by ID from API const selectedLogId = urlState.selected_log || null; const activeLogFetchId = useRef(null); const polling = urlState.polling; // Refresh time range on page focus/visibility useEffect(() => { const refreshDefaultsIfStale = () => { if (!polling) return if (urlState.period) { const { from, to } = getRangeForPeriod(urlState.period); setUrlState( { start_time: Math.floor(from.getTime() / 1000), end_time: Math.floor(to.getTime() / 1000), period: urlState.period ?? "", }, { history: "replace" }, ); return; } // Absolute custom range: skip refresh if user explicitly set it if (userModifiedTimeRange.current) return; // Only slide back to default 1h if the timestamps still match the initial defaults const startTimeDiff = Math.abs(urlState.start_time - initialDefaults.current.startTime); const endTimeDiff = Math.abs(urlState.end_time - initialDefaults.current.endTime); const tolerance = 5; if (startTimeDiff <= tolerance && endTimeDiff <= tolerance) { const defaults = getDefaultTimeRange(); const currentEndDiff = Math.abs(urlState.end_time - defaults.endTime); if (currentEndDiff > 300) { setUrlState( { start_time: defaults.startTime, end_time: defaults.endTime, period: urlState.period ?? "", }, { history: "replace" }, ); initialDefaults.current.startTime = defaults.startTime; initialDefaults.current.endTime = defaults.endTime; } } }; const handleVisibilityChange = () => { if (!document.hidden) refreshDefaultsIfStale(); }; const handleFocus = () => refreshDefaultsIfStale(); document.addEventListener("visibilitychange", handleVisibilityChange); window.addEventListener("focus", handleFocus); return () => { document.removeEventListener("visibilitychange", handleVisibilityChange); window.removeEventListener("focus", handleFocus); }; }, [urlState.period, urlState.start_time, urlState.end_time, setUrlState, polling]); // Refresh the time window every 5s while live polling is on and a relative period is active. // Updating start_time/end_time changes RTK args → triggers a refetch without needing pollingInterval. useEffect(() => { if (!polling || !urlState.period) return; const id = setInterval(() => { if (document.hidden) return; const { from, to } = getRangeForPeriod(urlState.period); setUrlState( { start_time: Math.floor(from.getTime() / 1000), end_time: Math.floor(to.getTime() / 1000), period: urlState.period ?? "", }, { history: "replace" }, ); }, 5000); return () => clearInterval(id); }, [polling, urlState.period, setUrlState]); // Convert URL state to filters and pagination for API calls const filters: LogFilters = useMemo( () => ({ parent_request_id: urlState.parent_request_id, providers: urlState.providers, models: urlState.models, aliases: urlState.aliases, status: urlState.status, objects: urlState.objects, selected_key_ids: urlState.selected_key_ids, virtual_key_ids: urlState.virtual_key_ids, routing_rule_ids: urlState.routing_rule_ids, routing_engine_used: urlState.routing_engine_used, user_ids: urlState.user_ids, team_ids: urlState.team_ids, customer_ids: urlState.customer_ids, business_unit_ids: urlState.business_unit_ids, content_search: urlState.content_search, start_time: dateUtils.toISOString(urlState.start_time), end_time: dateUtils.toISOString(urlState.end_time), missing_cost_only: urlState.missing_cost_only, metadata_filters: urlState.metadata_filters ? (() => { try { return JSON.parse(urlState.metadata_filters); } catch { return undefined; } })() : undefined, }), // Only re-derive filters when filter-related URL params change (not pagination) [ urlState.providers, urlState.models, urlState.aliases, urlState.status, urlState.objects, urlState.selected_key_ids, urlState.virtual_key_ids, urlState.routing_rule_ids, urlState.routing_engine_used, urlState.user_ids, urlState.team_ids, urlState.customer_ids, urlState.business_unit_ids, urlState.content_search, urlState.parent_request_id, urlState.start_time, urlState.end_time, urlState.missing_cost_only, urlState.metadata_filters, ], ); const pagination: Pagination = useMemo( () => ({ limit: urlState.limit, offset: urlState.offset, sort_by: urlState.sort_by as "timestamp" | "latency" | "tokens" | "cost", order: urlState.order as "asc" | "desc", }), [urlState.limit, urlState.offset, urlState.sort_by, urlState.order], ); const period = urlState.period; // Helper to update filters in URL const setFilters = useCallback( (newFilters: LogFilters) => { // Mark time range as user-modified only if start_time or end_time actually changed const timeChanged = newFilters.start_time !== filters.start_time || newFilters.end_time !== filters.end_time; if (timeChanged) { userModifiedTimeRange.current = true; } setUrlState({ // Clear the period whenever an absolute range is applied via setFilters ...(timeChanged && { period: "" }), parent_request_id: newFilters.parent_request_id || "", providers: newFilters.providers || [], models: newFilters.models || [], aliases: newFilters.aliases || [], status: newFilters.status || [], objects: newFilters.objects || [], selected_key_ids: newFilters.selected_key_ids || [], virtual_key_ids: newFilters.virtual_key_ids || [], routing_rule_ids: newFilters.routing_rule_ids || [], routing_engine_used: newFilters.routing_engine_used || [], user_ids: newFilters.user_ids || [], team_ids: newFilters.team_ids || [], customer_ids: newFilters.customer_ids || [], business_unit_ids: newFilters.business_unit_ids || [], content_search: newFilters.content_search || "", start_time: newFilters.start_time ? dateUtils.toUnixTimestamp(new Date(newFilters.start_time)) : undefined, end_time: newFilters.end_time ? dateUtils.toUnixTimestamp(new Date(newFilters.end_time)) : undefined, missing_cost_only: newFilters.missing_cost_only ?? false, metadata_filters: newFilters.metadata_filters ? JSON.stringify(newFilters.metadata_filters) : "", offset: 0, }); }, [setUrlState, filters], ); // Helper to update pagination in URL const setPagination = useCallback( (newPagination: Pagination) => { setUrlState({ limit: newPagination.limit, offset: newPagination.offset, sort_by: newPagination.sort_by, order: newPagination.order, }); }, [setUrlState], ); // Handler for time range changes from the volume chart const handleTimeRangeChange = useCallback( (startTime: number, endTime: number) => { userModifiedTimeRange.current = true; setUrlState({ period: "", start_time: startTime, end_time: endTime, offset: 0, }); }, [setUrlState], ); // Handler for resetting zoom to default 1h view const handleResetZoom = useCallback(() => { const now = Math.floor(Date.now() / 1000); const oneHour = now - 1 * 60 * 60; setUrlState({ start_time: oneHour, end_time: now, offset: 0, }); }, [setUrlState]); // Check if user has zoomed (time range is different from default 1h) const isZoomed = useMemo(() => { const currentRange = urlState.end_time - urlState.start_time; const defaultRange = 24 * 60 * 60; // 24 hours in seconds // Consider zoomed if range is less than 90% of default (to account for minor differences) return currentRange < defaultRange * 0.9; }, [urlState.start_time, urlState.end_time]); // Non-lazy RTK Query hooks — RTK handles caching, deduplication, and loading states. // pollingInterval is only set for the no-period case; period polling is handled by // a setInterval that updates URL timestamps, which changes args and triggers RTK to refetch. const { data: logsData, isLoading: logsIsLoading, isFetching: logsIsFetching, error: logsError, refetch: refetchLogs, } = useGetLogsQuery( { filters, pagination }, { // Poll every 5s on the empty state page so we transition as soon as the first log arrives. // When a relative period is active, the setInterval above updates URL timestamps → RTK // detects arg changes and refetches automatically; no separate pollingInterval needed. pollingInterval: showEmptyState ? 5000 : polling && !period ? 5000 : 0, refetchOnMountOrArgChange: true, skipPollingIfUnfocused: true, }, ); const { data: stats, isFetching: statsIsFetching, refetch: refetchStats, } = useGetLogsStatsQuery( { filters }, { pollingInterval: polling && !period ? 5000 : 0, refetchOnMountOrArgChange: true, skipPollingIfUnfocused: true, }, ); const { data: histogram, isLoading: histogramIsLoading, refetch: refetchHistogram, } = useGetLogsHistogramQuery( { filters }, { pollingInterval: polling && !period ? 5000 : 0, refetchOnMountOrArgChange: true, skipPollingIfUnfocused: true, }, ); // Set showEmptyState on first response; clear it as soon as logs appear. useEffect(() => { if (!logsData) return; if (!hasCheckedEmptyState.current) { setShowEmptyState(!logsData.has_logs); hasCheckedEmptyState.current = true; } else if (showEmptyState && logsData.has_logs) { setShowEmptyState(false); } }, [logsData, showEmptyState]); // On mount: if period is set and stored timestamps are stale, freshen them so the // initial query uses the correct window (RTK will refetch when args change). useEffect(() => { if (urlState.period) { const { from, to } = getRangeForPeriod(urlState.period); const freshEnd = Math.floor(to.getTime() / 1000); if (Math.abs(urlState.end_time - freshEnd) > 60) { setUrlState( { start_time: Math.floor(from.getTime() / 1000), end_time: freshEnd, period: urlState.period ?? "", }, { history: "replace" }, ); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleFilterByParentRequestId = useCallback( (parentRequestId: string) => { setSelectedSessionId(null); setSessionHighlightedLogId(null); setUrlState({ selected_log: "" }, { history: "replace" }); setFilters({ ...filters, parent_request_id: parentRequestId, }); }, [filters, setFilters], ); const handleDelete = useCallback( async (log: LogEntry) => { try { await deleteLogs({ ids: [log.id] }).unwrap(); if (urlState.selected_log === log.id) { setUrlState({ selected_log: "" }); } refetchLogs(); refetchStats(); refetchHistogram(); } catch (err) { setError(getErrorMessage(err)); } }, [deleteLogs, urlState.selected_log, setUrlState, refetchLogs, refetchStats, refetchHistogram], ); const handlePollToggle = useCallback( (enabled: boolean) => { setUrlState({ polling: enabled }); if (enabled) { refetchLogs(); refetchStats(); refetchHistogram(); } }, [setUrlState, refetchLogs, refetchStats, refetchHistogram], ); // Period selection: store relative period + fresh timestamps in URL (bypasses setFilters // so userModifiedTimeRange stays false and tab-focus refresh keeps working) const handlePeriodChange = useCallback( (p: string, from: Date, to: Date) => { setUrlState({ period: p, start_time: Math.floor(from.getTime() / 1000), end_time: Math.floor(to.getTime() / 1000), offset: 0, }); }, [setUrlState], ); const statCards = useMemo( () => [ { title: "Total Requests", value: , icon: , }, { title: "Success Rate", value: , icon: , description: "Success rate as perceived by the system. Each fallback counts as a separate attempt. Retries on the same request are counted as one attempt.", }, { title: "User Success Rate", value: ( ), icon: , description: "Success rate as perceived by the end user. It includes fallback chains as one request.", }, { title: "Avg Latency", value: ( ), icon: , }, { title: "Total Tokens", value: , icon: , }, { title: "Total Cost", value: ( ), icon: , }, ], [stats], ); const { data: filterData } = useGetAvailableFilterDataQuery(); // Get metadata keys from filterdata API so columns always show even with no data on current page const metadataKeys = useMemo(() => { if (!filterData?.metadata_keys) return []; return Object.keys(filterData.metadata_keys).sort(); }, [filterData?.metadata_keys]); const columns = useMemo(() => createColumns(handleDelete, hasDeleteAccess, metadataKeys), [handleDelete, hasDeleteAccess, metadataKeys]); const columnIds = useMemo( () => columns.map((col) => ("id" in col && col.id ? col.id : "accessorKey" in col ? String(col.accessorKey) : "")).filter(Boolean), [columns], ); const COLUMN_LABELS: Record = useMemo( () => ({ timestamp: "Time", request_type: "Type", input: "Message", provider: "Provider", model: "Model", latency: "Latency", tokens: "Tokens", cost: "Cost", }), [], ); const { entries: columnEntries, columnOrder, columnVisibility, columnPinning, toggleVisibility: toggleColumnVisibility, togglePin: toggleColumnPin, reorder: reorderColumns, reset: resetColumns, } = useColumnConfig({ columnIds, paramName: "cols" }); // Navigation for log detail sheet const logs = logsData?.logs ?? []; const totalItems = logsData?.stats?.total_requests ?? 0; const selectedLogFromData = useMemo( () => (selectedLogId ? (logs.find((l) => l.id === selectedLogId) ?? null) : null), [selectedLogId, logs], ); useEffect(() => { if (!selectedLogId || selectedLogFromData) { setFetchedLog(null); activeLogFetchId.current = null; return; } const fetchId = selectedLogId; activeLogFetchId.current = fetchId; triggerGetLogById(selectedLogId).then((result) => { if (activeLogFetchId.current === fetchId) { if (result.data) { setFetchedLog(result.data); } else if (result.error) { setError(getErrorMessage(result.error)); } } }); }, [selectedLogId, selectedLogFromData, triggerGetLogById]); const selectedLog = selectedLogFromData ?? fetchedLog; const selectedLogIndex = useMemo(() => (selectedLogId ? logs.findIndex((l) => l.id === selectedLogId) : -1), [selectedLogId, logs]); const handleLogNavigate = useCallback( (direction: "prev" | "next") => { const currentLogId = selectedLogId || ""; if (direction === "prev") { if (selectedLogIndex > 0) { // Navigate to previous log on current page setUrlState({ selected_log: logs[selectedLogIndex - 1].id }); } else if (pagination.offset > 0) { // Go to previous page and select the last item const newOffset = Math.max(0, pagination.offset - pagination.limit); setUrlState({ offset: newOffset, selected_log: "" }); // Fetch previous page, then select last log triggerGetLogs({ filters, pagination: { ...pagination, offset: newOffset }, }).then((result) => { if (result.data?.logs?.length) { const lastLog = result.data.logs[result.data.logs.length - 1]; setUrlState({ selected_log: lastLog.id }); } else if (result.error) { setUrlState({ offset: pagination.offset, selected_log: currentLogId, }); setError(getErrorMessage(result.error)); } }); } } else { if (selectedLogIndex >= 0 && selectedLogIndex < logs.length - 1) { // Navigate to next log on current page setUrlState({ selected_log: logs[selectedLogIndex + 1].id }); } else if (pagination.offset + pagination.limit < totalItems) { // Go to next page and select the first item const newOffset = pagination.offset + pagination.limit; setUrlState({ offset: newOffset, selected_log: "" }); // Fetch next page, then select first log triggerGetLogs({ filters, pagination: { ...pagination, offset: newOffset }, }).then((result) => { if (result.data?.logs?.length) { const firstLog = result.data.logs[0]; setUrlState({ selected_log: firstLog.id }); } else if (result.error) { setUrlState({ offset: pagination.offset, selected_log: currentLogId, }); setError(getErrorMessage(result.error)); } }); } } }, [selectedLogId, selectedLogIndex, logs, pagination, totalItems, filters, setUrlState, triggerGetLogs], ); return (
{logsIsLoading ? ( ) : showEmptyState ? ( [0]) : null)} /> ) : (
{/* Sidebar Filters */} {/* Main Content */}
{ await refetchLogs(); }} fetchStats={async () => { await refetchStats(); }} fetchHistogram={async () => { await refetchHistogram(); }} loading={logsIsFetching} polling={polling} onPollToggle={handlePollToggle} period={period} onPeriodChange={handlePeriodChange} columnEntries={columnEntries} columnLabels={COLUMN_LABELS} onToggleColumnVisibility={toggleColumnVisibility} onResetColumns={resetColumns} />
{statCards.map((card) => (
{card.title} {"description" in card && card.description && ( {card.description} )}
{card.value}
))}
{(error || !!logsError) && ( {error ?? (logsError ? getErrorMessage(logsError as Parameters[0]) : "")} )}
{ if (columnId === "actions") return; setUrlState({ selected_log: row.id }, { history: "replace" }); setSelectedSessionId(null); setSessionHighlightedLogId(null); }} polling={polling} onRefresh={refetchLogs} columnEntries={columnEntries} columnOrder={columnOrder} columnVisibility={columnVisibility} columnPinning={columnPinning} onToggleColumnVisibility={toggleColumnVisibility} onTogglePin={toggleColumnPin} onReorderColumns={reorderColumns} />
{/* Log Detail Sheet */} !open && setUrlState({ selected_log: "" })} handleDelete={handleDelete} onNavigate={handleLogNavigate} hasPrev={selectedLogIndex > 0 || (selectedLogIndex !== -1 && pagination.offset > 0)} hasNext={selectedLogIndex !== -1 && (selectedLogIndex < logs.length - 1 || pagination.offset + pagination.limit < totalItems)} onFilterByParentRequestId={handleFilterByParentRequestId} onViewSession={(sessionId, logId) => { setUrlState({ selected_log: "" }, { history: "replace" }); setSessionHighlightedLogId(logId); setSelectedSessionId(sessionId); }} /> { setSelectedSessionId(null); setUrlState({ selected_log: log.id }, { history: "replace" }); }} onFilterByParentRequestId={handleFilterByParentRequestId} />
)}
); }