diff --git a/frontend/components/shared/traces/span-view.tsx b/frontend/components/shared/traces/span-view.tsx index 6dd86c263..4fb6d62cb 100644 --- a/frontend/components/shared/traces/span-view.tsx +++ b/frontend/components/shared/traces/span-view.tsx @@ -62,7 +62,7 @@ export function SpanView({ spanId, traceId }: SpanViewProps) { {span.spanId}
- +
{new Date(span.startTime).toLocaleString()}
diff --git a/frontend/components/shared/traces/trace-view.tsx b/frontend/components/shared/traces/trace-view.tsx index b028a28d9..06044ef9f 100644 --- a/frontend/components/shared/traces/trace-view.tsx +++ b/frontend/components/shared/traces/trace-view.tsx @@ -86,7 +86,6 @@ const PureTraceView = ({ trace, spans }: TraceViewProps) => { spanPath: state.spanPath, setSpanPath: state.setSpanPath, })); - const hasLangGraph = useMemo(() => getHasLangGraph(), [getHasLangGraph]); const llmSpanIds = useMemo( () => diff --git a/frontend/components/traces/span-controls.tsx b/frontend/components/traces/span-controls.tsx index 0586f6fe0..fd6221ae3 100644 --- a/frontend/components/traces/span-controls.tsx +++ b/frontend/components/traces/span-controls.tsx @@ -1,5 +1,5 @@ import { get } from "lodash"; -import {ChevronDown, Copy, Database, Loader, PlayCircle} from "lucide-react"; +import { ChevronDown, Copy, Database, Loader, PlayCircle } from "lucide-react"; import Link from "next/link"; import { useParams } from "next/navigation"; import React, { PropsWithChildren, useCallback, useMemo } from "react"; @@ -14,7 +14,12 @@ import ErrorCard from "@/components/traces/error-card"; import ExportSpansPopover from "@/components/traces/export-spans-popover"; import { useOpenInSql } from "@/components/traces/trace-view/use-open-in-sql.tsx"; import { Button } from "@/components/ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Event } from "@/lib/events/types"; import { useToast } from "@/lib/hooks/use-toast"; import { Span, SpanType } from "@/lib/traces/types"; @@ -37,7 +42,10 @@ export function SpanControls({ children, span, events }: PropsWithChildren { if (span?.spanId) { @@ -82,12 +90,7 @@ export function SpanControls({ children, span, events }: PropsWithChildren
- +
{new Date(span.startTime).toLocaleString()}
@@ -101,7 +104,6 @@ export function SpanControls({ children, span, events }: PropsWithChildren -
diff --git a/frontend/components/traces/stats-shields.tsx b/frontend/components/traces/stats-shields.tsx index 74416fa38..bb1e42d77 100644 --- a/frontend/components/traces/stats-shields.tsx +++ b/frontend/components/traces/stats-shields.tsx @@ -1,12 +1,13 @@ import { TooltipPortal } from "@radix-ui/react-tooltip"; -import { compact, get, isNil, sortBy, uniq } from "lodash"; +import { compact, get, isNil, pick, sortBy, uniq } from "lodash"; import { Bolt, Braces, ChevronDown, CircleDollarSign, Clock3, Coins } from "lucide-react"; import { memo, PropsWithChildren } from "react"; -import { TraceViewTrace } from "@/components/traces/trace-view/trace-view-store.tsx"; +import { TraceViewSpan, TraceViewTrace } from "@/components/traces/trace-view/trace-view-store.tsx"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { Span } from "@/lib/traces/types.ts"; import { cn, getDurationString, pluralize } from "@/lib/utils"; import ContentRenderer from "../ui/content-renderer/index"; @@ -18,9 +19,7 @@ interface TraceStatsShieldsProps { } interface SpanStatsShieldsProps { - startTime: string; - endTime: string; - attributes: Record; + span: Span; className?: string; } @@ -119,9 +118,7 @@ const extractToolsFromAttributes = (attributes: Record): Tool[] => const name = attributes[`llm.request.functions.${index}.name`]; const description = attributes[`llm.request.functions.${index}.description`]; const rawParameters = attributes[`llm.request.functions.${index}.parameters`]; - const parameters = typeof rawParameters === "string" - ? rawParameters - : JSON.stringify(rawParameters || {}); + const parameters = typeof rawParameters === "string" ? rawParameters : JSON.stringify(rawParameters || {}); return name ? { name, description, parameters } : null; }) @@ -129,33 +126,22 @@ const extractToolsFromAttributes = (attributes: Record): Tool[] => }; function StatsShieldsContent({ - startTime, - endTime, - totalTokens, - inputTokens, - outputTokens, - inputCost, - outputCost, - totalCost, + stats, className, children, }: PropsWithChildren<{ - startTime: string; - endTime: string; - totalTokens: number; - inputTokens: number; - outputTokens: number; - inputCost: number; - outputCost: number; - totalCost: number; + stats: Pick< + TraceViewSpan, + "startTime" | "endTime" | "inputTokens" | "outputTokens" | "totalTokens" | "inputCost" | "outputCost" | "totalCost" + >; className?: string; }>) { return (
-
@@ -163,17 +149,17 @@ function StatsShieldsContent({
- +
@@ -185,20 +171,20 @@ function StatsShieldsContent({
- +
@@ -212,49 +198,41 @@ function StatsShieldsContent({ const PureTraceStatsShields = ({ trace, className, children }: PropsWithChildren) => ( {children} ); -const SpanStatsShields = ({ - startTime, - endTime, - attributes, - className, - children, -}: PropsWithChildren) => { - const inputTokenCount = get(attributes, "gen_ai.usage.input_tokens", 0); - const outputTokenCount = get(attributes, "gen_ai.usage.output_tokens", 0); - const totalTokenCount = inputTokenCount + outputTokenCount; - const inputCost = get(attributes, "gen_ai.usage.input_cost", 0); - const outputCost = get(attributes, "gen_ai.usage.output_cost", 0); - const cost = get(attributes, "gen_ai.usage.cost", 0); - const model = get(attributes, "gen_ai.response.model") || get(attributes, "gen_ai.request.model") || ""; - const tools = extractToolsFromAttributes(attributes); +const SpanStatsShields = ({ span, className, children }: PropsWithChildren) => { + const model = get(span.attributes, "gen_ai.response.model") || get(span.attributes, "gen_ai.request.model") || ""; + const tools = extractToolsFromAttributes(span.attributes); const structuredOutputSchema = - get(attributes, "gen_ai.request.structured_output_schema") || get(attributes, "ai.schema"); + get(span.attributes, "gen_ai.request.structured_output_schema") || get(span.attributes, "ai.schema"); return (
{children} diff --git a/frontend/components/traces/trace-view/span-card.tsx b/frontend/components/traces/trace-view/span-card.tsx index 6e502f709..064d3f90e 100644 --- a/frontend/components/traces/trace-view/span-card.tsx +++ b/frontend/components/traces/trace-view/span-card.tsx @@ -1,7 +1,9 @@ -import { ChevronDown, ChevronRight, X } from "lucide-react"; +import { ChevronDown, ChevronRight, CircleDollarSign, Coins, X } from "lucide-react"; import React, { useEffect, useMemo, useRef, useState } from "react"; +import { SpanDisplayTooltip } from "@/components/traces/trace-view/span-display-tooltip.tsx"; import { TraceViewSpan, useTraceViewStoreContext } from "@/components/traces/trace-view/trace-view-store.tsx"; +import { getLLMMetrics, getSpanDisplayName } from "@/components/traces/trace-view/utils.ts"; import { isStringDateOld } from "@/lib/traces/utils"; import { cn, getDurationString } from "@/lib/utils"; @@ -13,16 +15,28 @@ const ROW_HEIGHT = 36; const SQUARE_SIZE = 22; const SQUARE_ICON_SIZE = 16; +const DEPTH_INDENT = 24; +const TREE_CONTAINER_PADDING_LEFT = 16; +const BASE_PADDING_LEFT = 8; + +const TREE_LINE_WIDTH = 12; +const TREE_LINE_HEIGHT_ADJUSTMENT = 12; +const TREE_LINE_TOP_ANCHOR = 31; +const TREE_LINE_LEFT_BASE = 10; + interface SpanCardProps { span: TraceViewSpan; parentY: number; - containerWidth: number; depth: number; yOffset: number; onSpanSelect?: (span?: TraceViewSpan) => void; } -export function SpanCard({ span, yOffset, parentY, onSpanSelect, containerWidth, depth }: SpanCardProps) { +const numberFormatter = new Intl.NumberFormat("en-US", { + notation: "compact", +}); + +export function SpanCard({ span, yOffset, parentY, onSpanSelect, depth }: SpanCardProps) { const [segmentHeight, setSegmentHeight] = useState(0); const ref = useRef(null); @@ -31,7 +45,7 @@ export function SpanCard({ span, yOffset, parentY, onSpanSelect, containerWidth, spans: state.spans, toggleCollapse: state.toggleCollapse, })); - + const llmMetrics = getLLMMetrics(span); // Get child spans from the store const childSpans = useMemo(() => spans.filter((s) => s.parentSpanId === span.spanId), [spans, span.spanId]); @@ -46,27 +60,36 @@ export function SpanCard({ span, yOffset, parentY, onSpanSelect, containerWidth, const isSelected = useMemo(() => selectedSpan?.spanId === span.spanId, [selectedSpan?.spanId, span.spanId]); return ( -
-
+
+
{ + if (!span.pending) { + onSpanSelect?.(span); + } + }} + >
-
- {span.name} -
+ +
+ {getSpanDisplayName(span)} +
+
{span.pending ? ( isStringDateOld(span.startTime) ? ( @@ -97,32 +117,31 @@ export function SpanCard({ span, yOffset, parentY, onSpanSelect, containerWidth, ) ) : ( -
- {getDurationString(span.startTime, span.endTime)} -
- )} -
{ - if (!span.pending) { - onSpanSelect?.(span); - } - }} - /> - {isSelected && ( -
+ <> +
+ {getDurationString(span.startTime, span.endTime)} +
+ {llmMetrics && ( + <> +
+ + {numberFormatter.format(llmMetrics.tokens)} +
+
+ + {llmMetrics.cost.toFixed(3)} +
+ + )} + )} {hasChildren && (