import { StarTraceToggle } from "@/src/components/star-toggle"; import { DataTable } from "@/src/components/table/data-table"; import { DataTableToolbar } from "@/src/components/table/data-table-toolbar"; import { Badge } from "@/src/components/ui/badge"; import { type LangfuseColumnDef } from "@/src/components/table/types"; import { TagTracePopover } from "@/src/features/tag/components/TagTracePopver"; import { TokenUsageBadge } from "@/src/components/token-usage-badge"; import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility"; import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState"; import { api } from "@/src/utils/api"; import { formatIntervalSeconds } from "@/src/utils/dates"; import { type RouterOutput } from "@/src/utils/types"; import { type Row, type RowSelectionState } from "@tanstack/react-table"; import { useEffect, useMemo, useState } from "react"; import { NumberParam, useQueryParams, withDefault } from "use-query-params"; import type Decimal from "decimal.js"; import { compactNumberFormatter, numberFormatter, usdFormatter, } from "@/src/utils/numbers"; import { DeleteTraceButton } from "@/src/components/deleteButton"; import { formatAsLabel, LevelColors, LevelSymbols, } from "@/src/components/level-colors"; import { cn } from "@/src/utils/tailwind"; import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context"; import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState"; import { type FilterState, tracesTableColsWithOptions, type ObservationLevelType, BatchExportTableName, AnnotationQueueObjectType, BatchActionType, TableViewPresetTableName, } from "@langfuse/shared"; import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch"; import { MemoizedIOTableCell } from "../../ui/IOTableCell"; import { useTableDateRange } from "@/src/hooks/useTableDateRange"; import { toAbsoluteTimeRange } from "@/src/utils/date-range-utils"; import { useDebounce } from "@/src/hooks/useDebounce"; import { type ScoreAggregate } from "@langfuse/shared"; import { joinTableCoreAndMetrics } from "@/src/components/table/utils/joinTableCoreAndMetrics"; import { Skeleton } from "@/src/components/ui/skeleton"; import useColumnOrder from "@/src/features/column-visibility/hooks/useColumnOrder"; import { BatchExportTableButton } from "@/src/components/BatchExportTableButton"; import { BreakdownTooltip } from "@/src/components/trace/BreakdownToolTip"; import { InfoIcon, MoreVertical } from "lucide-react"; import { useHasEntitlement } from "@/src/features/entitlements/hooks"; import React from "react"; import { TableActionMenu } from "@/src/features/table/components/TableActionMenu"; import { useSelectAll } from "@/src/features/table/hooks/useSelectAll"; import { LocalIsoDate } from "@/src/components/LocalIsoDate"; import { TableSelectionManager } from "@/src/features/table/components/TableSelectionManager"; import { showSuccessToast } from "@/src/features/notifications/showSuccessToast"; import { type TableAction } from "@/src/features/table/types"; import { LevelCountsDisplay, type LevelCount, } from "@/src/components/level-counts-display"; import { DropdownMenuContent, DropdownMenu, DropdownMenuItem, DropdownMenuTrigger, } from "@/src/components/ui/dropdown-menu"; import { Button } from "@/src/components/ui/button"; import TableIdOrName from "@/src/components/table/table-id"; import { useEnvironmentFilter, convertSelectedEnvironmentsToFilter, } from "@/src/hooks/use-environment-filter"; import { PeekViewTraceDetail } from "@/src/components/table/peek/peek-trace-detail"; import { usePeekNavigation } from "@/src/components/table/peek/hooks/usePeekNavigation"; import { useTableViewManager } from "@/src/components/table/table-view-presets/hooks/useTableViewManager"; import { useFullTextSearch } from "@/src/components/table/use-cases/useFullTextSearch"; import { type TableDateRange } from "@/src/utils/date-range-utils"; import { useScoreColumns } from "@/src/features/scores/hooks/useScoreColumns"; import { scoreFilters } from "@/src/features/scores/lib/scoreColumns"; import { useRouter } from "next/router"; export type TracesTableRow = { // Shown by default bookmarked: boolean; timestamp: Date; name: string; // i/o and metadata not set explicitly, but fetched from the server from the cell input?: unknown; output?: unknown; levelCounts: { errorCount?: bigint; warningCount?: bigint; debugCount?: bigint; defaultCount?: bigint; }; latency?: number; tokenDetails?: Record; totalCost?: Decimal; costDetails?: Record; environment?: string; tags: string[]; metadata?: unknown; // scores holds grouped column with individual scores scores?: ScoreAggregate; // Hidden by default sessionId?: string; userId: string; observationCount?: bigint; level?: ObservationLevelType; version?: string; release?: string; id: string; usage: { inputUsage?: bigint; outputUsage?: bigint; totalUsage?: bigint; }; cost?: { inputCost?: Decimal; outputCost?: Decimal; }; }; export type TracesTableProps = { projectId: string; userId?: string; omittedFilter?: string[]; hideControls?: boolean; externalFilterState?: FilterState; externalDateRange?: TableDateRange; limitRows?: number; }; export default function TracesTable({ projectId, userId, omittedFilter = [], hideControls = false, externalFilterState, externalDateRange, limitRows, }: TracesTableProps) { const router = useRouter(); const utils = api.useUtils(); const [selectedRows, setSelectedRows] = useState({}); const { setDetailPageList } = useDetailPageLists(); const { timeRange, setTimeRange } = useTableDateRange(projectId); // Convert timeRange to absolute date range for compatibility const tableDateRange = useMemo(() => { return toAbsoluteTimeRange(timeRange) ?? undefined; }, [timeRange]); const dateRange = externalDateRange ?? tableDateRange; const [userFilterState, setUserFilterState] = useQueryFilterState( [], "traces", projectId, ); const [orderByState, setOrderByState] = useOrderByState({ column: "timestamp", order: "DESC", }); const dateRangeFilter: FilterState = dateRange ? [ { column: "Timestamp", type: "datetime", operator: ">=", value: dateRange.from, }, ...(dateRange.to ? [ { column: "Timestamp", type: "datetime", operator: "<=", value: dateRange.to, } as const, ] : []), ] : []; const userIdFilter: FilterState = userId ? [ { column: "User ID", type: "string", operator: "=", value: userId, }, ] : []; const environmentFilterOptions = api.projects.environmentFilterOptions.useQuery( { projectId, fromTimestamp: dateRange?.from, }, { trpc: { context: { skipBatch: true } }, refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, staleTime: Infinity, }, ); const environmentOptions = environmentFilterOptions.data?.map((value) => value.environment) || []; const { selectedEnvironments, setSelectedEnvironments } = useEnvironmentFilter(environmentOptions, projectId); const environmentFilter = convertSelectedEnvironmentsToFilter( ["environment"], selectedEnvironments, ); const combinedFilterState = userFilterState.concat( userIdFilter, dateRangeFilter, environmentFilter, ); // Use external filter state if provided, otherwise use combined filter state const filterState = externalFilterState || combinedFilterState; const [paginationState, setPaginationState] = useQueryParams({ pageIndex: withDefault(NumberParam, 0), pageSize: withDefault(NumberParam, 50), }); const { selectAll, setSelectAll } = useSelectAll(projectId, "traces"); const { searchQuery, searchType, setSearchQuery, setSearchType } = useFullTextSearch(); const tracesAllCountFilter = { projectId, filter: filterState, searchQuery: searchQuery, searchType: searchType, page: 0, limit: 0, orderBy: null, }; const totalCountQuery = api.traces.countAll.useQuery(tracesAllCountFilter, { enabled: environmentFilterOptions.data !== undefined, }); const tracesAllQueryFilter = { ...tracesAllCountFilter, searchQuery: searchQuery, searchType: searchType, page: limitRows ? 0 : paginationState.pageIndex, limit: limitRows ?? paginationState.pageSize, orderBy: orderByState, }; const traces = api.traces.all.useQuery(tracesAllQueryFilter, { enabled: environmentFilterOptions.data !== undefined, refetchOnMount: false, refetchOnWindowFocus: true, }); const traceMetrics = api.traces.metrics.useQuery( { projectId, filter: filterState, traceIds: traces.data?.traces.map((t) => t.id) ?? [], }, { enabled: traces.data !== undefined, refetchOnMount: false, refetchOnWindowFocus: true, }, ); type TracesCoreOutput = RouterOutput["traces"]["all"]["traces"][number]; type TraceMetricOutput = RouterOutput["traces"]["metrics"][number]; const traceRowData = joinTableCoreAndMetrics< TracesCoreOutput, TraceMetricOutput >(traces.data?.traces, traceMetrics.data); const totalCount = totalCountQuery.data?.totalCount ?? null; useEffect(() => { if (traces.isSuccess) { setDetailPageList( "traces", traces.data.traces.map((t) => ({ id: t.id, params: { timestamp: t.timestamp.toISOString() }, })), ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [traces.isSuccess, traces.data]); // loading filter options individually from the remaining calls // traces.all should load first together with everything else. // This here happens in the background. const traceFilterOptionsResponse = api.traces.filterOptions.useQuery( { projectId }, { trpc: { context: { skipBatch: true } }, refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, staleTime: Infinity, }, ); const traceFilterOptions = traceFilterOptionsResponse.data; const transformedFilterOptions = useMemo(() => { return tracesTableColsWithOptions(traceFilterOptions).filter( (c) => c.id !== "environment" && c.id !== "timestamp" && !omittedFilter?.includes(c.name) && !omittedFilter?.includes(c.id), ); }, [traceFilterOptions, omittedFilter]); const [storedRowHeight, setRowHeight] = useRowHeightLocalStorage( "traces", "s", ); const rowHeight = hideControls ? "s" : storedRowHeight; const { scoreColumns, isLoading: isColumnLoading } = useScoreColumns({ scoreColumnKey: "scores", projectId, filter: scoreFilters.forTraces(), fromTimestamp: dateRange?.from, }); const hasTraceDeletionEntitlement = useHasEntitlement("trace-deletion"); const { selectActionColumn } = TableSelectionManager({ projectId, tableName: "traces", setSelectedRows, }); const traceDeleteMutation = api.traces.deleteMany.useMutation({ onSuccess: () => { showSuccessToast({ title: "Traces deleted", description: "Selected traces will be deleted. Traces are removed asynchronously and may continue to be visible for up to 15 minutes.", }); }, onSettled: () => { void utils.traces.all.invalidate(); }, }); const addToQueueMutation = api.annotationQueueItems.createMany.useMutation({ onSuccess: (data) => { showSuccessToast({ title: "Traces added to queue", description: `Selected traces will be added to queue "${data.queueName}". This may take a minute.`, link: { href: `/project/${projectId}/annotation-queues/${data.queueId}`, text: `View queue "${data.queueName}"`, }, }); }, }); const handleDeleteTraces = async ({ projectId }: { projectId: string }) => { const selectedTraceIds = Object.keys(selectedRows).filter((traceId) => traces.data?.traces.map((t) => t.id).includes(traceId), ); await traceDeleteMutation.mutateAsync({ projectId, traceIds: selectedTraceIds, query: { filter: filterState, orderBy: orderByState, searchQuery: searchQuery || undefined, searchType, }, isBatchAction: selectAll, }); setSelectedRows({}); }; const handleAddToAnnotationQueue = async ({ projectId, targetId, }: { projectId: string; targetId: string; }) => { const selectedTraceIds = Object.keys(selectedRows).filter((traceId) => traces.data?.traces.map((t) => t.id).includes(traceId), ); await addToQueueMutation.mutateAsync({ projectId, objectIds: selectedTraceIds, objectType: AnnotationQueueObjectType.TRACE, queueId: targetId, isBatchAction: selectAll, query: { filter: filterState, orderBy: orderByState, }, }); setSelectedRows({}); }; const displayCount = totalCountQuery.isPending ? ( ... ) : selectAll ? ( compactNumberFormatter(totalCountQuery.data?.totalCount) ) : ( compactNumberFormatter(Object.keys(selectedRows).length) ); const tableActions: TableAction[] = [ ...(hasTraceDeletionEntitlement ? [ { id: "trace-delete", type: BatchActionType.Delete, label: "Delete Traces", description: `This action permanently deletes ${displayCount} traces and cannot be undone. Trace deletion happens asynchronously and may take up to 15 minutes.`, accessCheck: { scope: "traces:delete", entitlement: "trace-deletion", }, execute: handleDeleteTraces, } as TableAction, ] : []), { id: "trace-add-to-annotation-queue", type: BatchActionType.Create, label: "Add to Annotation Queue", description: "Add selected traces to an annotation queue.", targetLabel: "Annotation Queue", execute: handleAddToAnnotationQueue, accessCheck: { scope: "annotationQueues:CUD", }, }, ]; const enableSorting = !hideControls; const columns: LangfuseColumnDef[] = [ ...(hideControls ? [] : [ selectActionColumn, { accessorKey: "bookmarked", header: undefined, id: "bookmarked", size: 30, isFixedPosition: true, cell: ({ row }: { row: Row }) => { const bookmarked: TracesTableRow["bookmarked"] = row.getValue("bookmarked"); const traceId = row.getValue("id"); return typeof traceId === "string" && typeof bookmarked === "boolean" ? ( ) : undefined; }, enableSorting, }, ]), { accessorKey: "timestamp", header: "Timestamp", id: "timestamp", size: 150, enableHiding: true, enableSorting, cell: ({ row }) => { const value: TracesTableRow["timestamp"] = row.getValue("timestamp"); return value ? : undefined; }, }, { accessorKey: "name", header: "Name", id: "name", size: 150, enableHiding: true, enableSorting, cell: ({ row }) => { const value: TracesTableRow["name"] = row.getValue("name"); return value ? ( {value} ) : undefined; }, }, { accessorKey: "input", header: "Input", id: "input", size: 400, cell: ({ row }) => { const traceId: TracesTableRow["id"] = row.getValue("id"); const traceTimestamp: TracesTableRow["timestamp"] = row.getValue("timestamp"); return ( ); }, enableHiding: true, }, { accessorKey: "output", header: "Output", id: "output", size: 400, cell: ({ row }) => { const traceId: TracesTableRow["id"] = row.getValue("id"); const traceTimestamp: TracesTableRow["timestamp"] = row.getValue("timestamp"); return ( ); }, enableHiding: true, }, { accessorKey: "levelCounts", id: "levelCounts", header: "Observation Levels", size: 150, cell: ({ row }) => { const value: TracesTableRow["levelCounts"] = row.getValue("levelCounts"); if (!traceMetrics.data) return ; const counts: LevelCount[] = Object.entries(value).map( ([level, count]) => ({ level: formatAsLabel(level), count, symbol: LevelSymbols[formatAsLabel(level)], }), ); return ; }, enableHiding: true, }, { accessorKey: "latency", id: "latency", header: "Latency", size: 100, // add seconds to the end of the latency cell: ({ row }) => { const value: TracesTableRow["latency"] = row.getValue("latency"); if (!traceMetrics.data) return ; return value !== undefined ? ( {formatIntervalSeconds(value)} ) : undefined; }, enableHiding: true, enableSorting, }, { accessorKey: "tokens", header: "Tokens", id: "tokens", size: 180, cell: ({ row }) => { const value: TracesTableRow["usage"] = row.getValue("usage"); if (!traceMetrics.data) return ; if (!value.inputUsage && !value.outputUsage && !value.totalUsage) { return null; } return (
); }, enableSorting, enableHiding: true, }, { accessorKey: "totalCost", id: "totalCost", header: "Total Cost", size: 130, cell: ({ row }) => { const cost: TracesTableRow["totalCost"] = row.getValue("totalCost"); if (!traceMetrics.data) return ; return cost != null ? (
{cost ? ( {usdFormatter(cost.toNumber())} ) : ( - )}
) : null; }, enableHiding: true, enableSorting, }, { accessorKey: "environment", header: "Environment", id: "environment", size: 150, enableHiding: true, cell: ({ row }) => { const value: TracesTableRow["environment"] = row.getValue("environment"); return value ? ( {value} ) : null; }, }, { accessorKey: "tags", id: "tags", header: "Tags", size: 150, headerTooltip: { description: "Group traces with tags.", href: "https://langfuse.com/docs/observability/features/tags", }, cell: ({ row }) => { const tags: TracesTableRow["tags"] = row.getValue("tags"); const traceId: TracesTableRow["id"] = row.getValue("id"); const filterOptionTags = traceFilterOptions?.tags ?? []; const allTags = filterOptionTags.map((t) => t.value); return ( ); }, enableHiding: true, }, { accessorKey: "metadata", header: "Metadata", size: 400, headerTooltip: { description: "Add metadata to traces to track additional information.", href: "https://langfuse.com/docs/observability/features/metadata", }, cell: ({ row }) => { const traceId: TracesTableRow["id"] = row.getValue("id"); const traceTimestamp: TracesTableRow["timestamp"] = row.getValue("timestamp"); return ( ); }, enableHiding: true, }, ...(hideControls ? [] : [ { accessorKey: "scores", header: "Scores", id: "scores", enableHiding: true, defaultHidden: true, cell: () => { return isColumnLoading ? ( ) : null; }, columns: scoreColumns, }, ]), { accessorKey: "sessionId", enableColumnFilter: !omittedFilter.find((f) => f === "sessionId"), id: "sessionId", header: "Session", size: 150, headerTooltip: { description: "Add `sessionId` to traces to track sessions.", href: "https://langfuse.com/docs/observability/features/sessions", }, cell: ({ row }) => { const value: TracesTableRow["sessionId"] = row.getValue("sessionId"); return value && typeof value === "string" ? ( ) : undefined; }, defaultHidden: true, enableHiding: true, enableSorting, }, { accessorKey: "userId", header: "User", id: "userId", size: 150, headerTooltip: { description: "Add `userId` to traces to track users.", href: "https://langfuse.com/docs/observability/features/users", }, cell: ({ row }) => { const value: TracesTableRow["userId"] = row.getValue("userId"); return value && typeof value === "string" ? ( ) : undefined; }, defaultHidden: true, enableHiding: true, enableSorting, }, { accessorKey: "observationCount", id: "observationCount", header: "Observations", size: 120, headerTooltip: { description: "The number of observations in the trace.", }, enableHiding: true, defaultHidden: true, cell: ({ row }) => { const value: TracesTableRow["observationCount"] = row.getValue("observationCount"); if (!traceMetrics.data) return ; return {numberFormatter(value, 0)}; }, }, { accessorKey: "level", id: "level", header: "Level", size: 75, cell: ({ row }) => { const value: TracesTableRow["level"] = row.getValue("level"); if (!traceMetrics.data) return ; return value ? ( {value} ) : ( - ); }, defaultHidden: true, enableHiding: true, enableSorting, }, { accessorKey: "version", id: "version", header: "Version", size: 100, headerTooltip: { description: "Track changes via the version tag.", href: "https://langfuse.com/docs/observability/features/releases-and-versioning", }, defaultHidden: true, enableHiding: true, enableSorting, }, { accessorKey: "release", id: "release", header: "Release", size: 100, headerTooltip: { description: "Track changes to your application via the release tag.", href: "https://langfuse.com/docs/observability/features/releases-and-versioning", }, defaultHidden: true, enableHiding: true, enableSorting, }, { accessorKey: "id", header: "Trace ID", id: "id", size: 90, cell: ({ row }) => { const value: TracesTableRow["id"] = row.getValue("id"); return value && typeof value === "string" ? ( ) : undefined; }, defaultHidden: true, enableHiding: true, enableSorting, }, { accessorKey: "cost", header: "Cost", id: "cost", enableHiding: true, defaultHidden: true, cell: () => { return traceMetrics.isPending ? ( ) : null; }, columns: [ { accessorKey: "inputCost", id: "inputCost", header: "Input Cost", size: 100, cell: ({ row }: { row: Row }) => { const cost: TracesTableRow["cost"] = row.getValue("cost"); if (!traceMetrics.data) return ; return (
{cost?.inputCost ? ( {usdFormatter(cost.inputCost.toNumber())} ) : ( - )}
); }, defaultHidden: true, enableHiding: true, enableSorting, }, { accessorKey: "outputCost", id: "outputCost", header: "Output Cost", size: 100, cell: ({ row }: { row: Row }) => { const cost: TracesTableRow["cost"] = row.getValue("cost"); if (!traceMetrics.data) return ; return (
{cost?.outputCost ? ( {usdFormatter(cost.outputCost.toNumber())} ) : ( - )}
); }, enableHiding: true, defaultHidden: true, enableSorting, }, ], }, { accessorKey: "usage", header: "Usage", id: "usage", enableHiding: true, defaultHidden: true, cell: () => { return traceMetrics.isPending ? ( ) : null; }, columns: [ { accessorKey: "inputTokens", id: "inputTokens", header: "Input Tokens", size: 110, cell: ({ row }: { row: Row }) => { const value: TracesTableRow["usage"] = row.getValue("usage"); if (!traceMetrics.data) return ; return {numberFormatter(value.inputUsage, 0)}; }, enableHiding: true, defaultHidden: true, enableSorting, }, { accessorKey: "outputTokens", id: "outputTokens", header: "Output Tokens", size: 110, cell: ({ row }: { row: Row }) => { const value: TracesTableRow["usage"] = row.getValue("usage"); if (!traceMetrics.data) return ; return {numberFormatter(value.outputUsage, 0)}; }, enableHiding: true, defaultHidden: true, enableSorting, }, { accessorKey: "totalTokens", id: "totalTokens", header: "Total Tokens", size: 110, cell: ({ row }: { row: Row }) => { const value: TracesTableRow["usage"] = row.getValue("usage"); if (!traceMetrics.data) return ; return {numberFormatter(value.totalUsage, 0)}; }, enableHiding: true, defaultHidden: true, enableSorting, }, ], }, ...(hideControls ? [] : [ { accessorKey: "action", header: "Action", size: 70, isFixedPosition: true, cell: ({ row }: { row: Row }) => { const traceId: TracesTableRow["id"] = row.getValue("id"); return ( traceId && typeof traceId === "string" && ( ) ); }, }, ]), ]; const [columnVisibility, setColumnVisibility] = useColumnVisibility( `traceColumnVisibility-${projectId}${hideControls ? "-hideControl" : "-showControls"}`, columns, ); const [columnOrder, setColumnOrder] = useColumnOrder( `traceColumnOrder-${projectId}${hideControls ? "-hideControl" : "-showControls"}`, columns, ); const peekNavigationProps = usePeekNavigation({ queryParams: ["observation", "display", "timestamp"], extractParamsValuesFromRow: (row: TracesTableRow) => ({ timestamp: row.timestamp?.toISOString() || "", }), expandConfig: { basePath: `/project/${projectId}/traces`, }, }); const peekConfig = useMemo(() => { if (hideControls) return undefined; return { itemType: "TRACE" as const, detailNavigationKey: "traces", peekEventOptions: { ignoredSelectors: ['[role="checkbox"]', '[aria-label="bookmark"]'], }, children: , tableDataUpdatedAt: Math.max( traces.dataUpdatedAt, traceMetrics.dataUpdatedAt, ), ...peekNavigationProps, }; }, [ projectId, hideControls, peekNavigationProps, traces.dataUpdatedAt, traceMetrics.dataUpdatedAt, ]); const { isLoading: isViewLoading, ...viewControllers } = useTableViewManager({ tableName: TableViewPresetTableName.Traces, projectId, stateUpdaters: { setOrderBy: setOrderByState, setFilters: setUserFilterState, setColumnOrder: setColumnOrder, setColumnVisibility: setColumnVisibility, setSearchQuery: setSearchQuery, }, validationContext: { columns, filterColumnDefinition: transformedFilterOptions, }, }); const rows = useMemo(() => { return traces.isSuccess ? (traceRowData?.rows?.map((trace) => { return { bookmarked: trace.bookmarked, id: trace.id, timestamp: trace.timestamp, name: trace.name ?? "", level: trace.level, observationCount: trace.observationCount, release: trace.release ?? undefined, version: trace.version ?? undefined, userId: trace.userId ?? "", sessionId: trace.sessionId ?? undefined, environment: trace.environment ?? undefined, latency: trace.latency === null ? undefined : trace.latency, tags: trace.tags, usage: { inputUsage: trace.promptTokens, outputUsage: trace.completionTokens, totalUsage: trace.totalTokens, }, tokens: { inputUsage: trace.promptTokens, outputUsage: trace.completionTokens, totalUsage: trace.totalTokens, }, levelCounts: { errorCount: trace.errorCount, warningCount: trace.warningCount, defaultCount: trace.defaultCount, debugCount: trace.debugCount, }, tokenDetails: trace.usageDetails, costDetails: trace.costDetails, scores: trace.scores, cost: { inputCost: trace.calculatedInputCost ?? undefined, outputCost: trace.calculatedOutputCost ?? undefined, }, totalCost: trace.calculatedTotalCost ?? undefined, }; }) ?? []) : []; }, [traces.isSuccess, traceRowData?.rows]); const setFilterState = useDebounce(setUserFilterState); return ( <> {!hideControls && ( traces.data?.traces.map((t) => t.id).includes(traceId), ).length > 0 ? ( ) : null, , ]} orderByState={orderByState} columnVisibility={columnVisibility} setColumnVisibility={setColumnVisibility} columnOrder={columnOrder} setColumnOrder={setColumnOrder} rowHeight={rowHeight} setRowHeight={setRowHeight} timeRange={timeRange} setTimeRange={setTimeRange} multiSelect={{ selectAll, setSelectAll, selectedRowIds: Object.keys(selectedRows).filter((traceId) => traces.data?.traces.map((t) => t.id).includes(traceId), ), setRowSelection: setSelectedRows, totalCount, ...paginationState, }} environmentFilter={{ values: selectedEnvironments, onValueChange: setSelectedEnvironments, options: environmentOptions.map((env) => ({ value: env })), }} /> )} { // Handle Command/Ctrl+click to open trace in new tab if (event && (event.metaKey || event.ctrlKey)) { // Prevent the default peek behavior event.preventDefault(); // Construct the trace URL directly to avoid race conditions const traceId = row.id; const timestamp = row.timestamp; const display = router.query.display ?? "details"; let traceUrl = `/project/${projectId}/traces/${encodeURIComponent(traceId)}`; const params = new URLSearchParams(); if (timestamp) { params.set("timestamp", timestamp.toISOString()); } if (display && typeof display === "string") { params.set("display", display); } if (params.toString()) { traceUrl += `?${params.toString()}`; } const fullUrl = `${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}${traceUrl}`; window.open(fullUrl, "_blank"); } // For normal clicks, let the data-table handle opening the peek view }} tableName={"traces"} /> ); } const TracesDynamicCell = ({ traceId, projectId, timestamp, col, singleLine = false, }: { traceId: string; projectId: string; timestamp: Date; col: "input" | "output" | "metadata"; singleLine?: boolean; }) => { const trace = api.traces.byId.useQuery( { traceId, projectId, timestamp, truncated: true }, { refetchOnMount: false, // prevents refetching loops staleTime: 60 * 1000, // 1 minute }, ); const data = col === "output" ? trace.data?.output : col === "input" ? trace.data?.input : trace.data?.metadata; return ( ); };