Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion frontend/components/shared/traces/trace-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ const PureTraceView = ({ trace, spans }: TraceViewProps) => {
spanPath: state.spanPath,
setSpanPath: state.setSpanPath,
}));

const hasLangGraph = useMemo(() => getHasLangGraph(), [getHasLangGraph]);
const llmSpanIds = useMemo(
() =>
Expand Down
16 changes: 14 additions & 2 deletions frontend/components/traces/trace-view/span-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ChevronDown, ChevronRight, X } from "lucide-react";
import React, { useEffect, useMemo, useRef, useState } from "react";

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";

Expand All @@ -24,14 +25,15 @@ interface SpanCardProps {

export function SpanCard({ span, yOffset, parentY, onSpanSelect, containerWidth, depth }: SpanCardProps) {
const [segmentHeight, setSegmentHeight] = useState(0);
const [isHovered, setIsHovered] = useState(false);
const ref = useRef<HTMLDivElement>(null);

const { selectedSpan, spans, toggleCollapse } = useTraceViewStoreContext((state) => ({
selectedSpan: state.selectedSpan,
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]);

Expand Down Expand Up @@ -84,7 +86,7 @@ export function SpanCard({ span, yOffset, parentY, onSpanSelect, containerWidth,
span.pending && "text-muted-foreground"
)}
>
{span.name}
{isHovered && span.spanType === "LLM" ? span.name : getSpanDisplayName(span)}
</div>
{span.pending ? (
isStringDateOld(span.startTime) ? (
Expand Down Expand Up @@ -113,7 +115,17 @@ export function SpanCard({ span, yOffset, parentY, onSpanSelect, containerWidth,
onSpanSelect?.(span);
}
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
/>
{llmMetrics && (
<div className="absolute right-4 flex items-center gap-2 text-xs font-medium text-secondary-foreground z-30">
<span>${llmMetrics.cost}</span>
<span>
{llmMetrics.totalTokens} tokens
</span>
</div>
)}
{isSelected && (
<div
className="absolute top-0 w-full bg-primary/25 border-l-2 border-l-primary"
Expand Down
30 changes: 23 additions & 7 deletions frontend/components/traces/trace-view/timeline-element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { memo, useLayoutEffect, useMemo, useRef, useState } from "react";

import { TraceViewSpan } from "@/components/traces/trace-view/trace-view-store.tsx";
import { TimelineData } from "@/components/traces/trace-view/trace-view-store-utils.ts";
import {getLLMMetrics, getSpanDisplayName} from "@/components/traces/trace-view/utils.ts";
import { SPAN_TYPE_TO_COLOR } from "@/lib/traces/utils";
import { cn, getDurationString } from "@/lib/utils";

Expand All @@ -25,6 +26,7 @@ const TimelineElement = ({
const textRef = useRef<HTMLSpanElement>(null);
const blockRef = useRef<HTMLDivElement>(null);
const [textPosition, setTextPosition] = useState<"inside" | "outside">("inside");
const [isHovered, setIsHovered] = useState(false);

const isSelected = useMemo(() => selectedSpan?.spanId === span.span.spanId, [span.span.spanId, selectedSpan?.spanId]);

Expand All @@ -34,6 +36,8 @@ const TimelineElement = ({
}
};

const llmMetrics = getLLMMetrics(span.span);

useLayoutEffect(() => {
if (!blockRef.current || !textRef.current) return;

Expand All @@ -56,10 +60,12 @@ const TimelineElement = ({
};
}, [span.span.name, span.events.length, span.width]);

const SpanText = useMemo(() => {
const spanTextElement = useMemo(() => {
const displayName = isHovered && span.span.spanType === "LLM" ? span.span.name : getSpanDisplayName(span.span);

const textContent = (
<>
{span.span.name}{" "}
{displayName}{" "}
<span className="text-white/70">{getDurationString(span.span.startTime, span.span.endTime)}</span>
</>
);
Expand Down Expand Up @@ -104,13 +110,15 @@ const TimelineElement = ({
{textContent}
</span>
);
}, [span.span.name, span.span.startTime, span.span.endTime, span.left, span.events.length, textPosition]);
}, [span.span.name, span.span.startTime, span.span.endTime, span.span.spanType, span.left, span.events.length, textPosition, isHovered]);

return (
<div
key={virtualRow.index}
data-index={virtualRow.index}
onClick={handleSpanSelect}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={cn(
"absolute top-0 left-0 w-full h-8 flex items-center px-4 hover:bg-muted cursor-pointer transition duration-200"
)}
Expand All @@ -123,14 +131,14 @@ const TimelineElement = ({
<span
title={span.span.name}
ref={textRef}
className="text-xs font-medium text-black truncate absolute"
className={"text-xs font-medium text-black truncate absolute"}
style={{
right: `calc(100% - ${span.left}% + 16px)`,
textAlign: "right",
maxWidth: "250px",
}}
>
{span.span.name}{" "}
{isHovered && span.span.spanType === "LLM" ? span.span.name : getSpanDisplayName(span.span)}{" "}
<span className="text-secondary-foreground">{getDurationString(span.span.startTime, span.span.endTime)}</span>
</span>
)}
Expand All @@ -156,9 +164,17 @@ const TimelineElement = ({
}}
/>
))}
{textPosition === "inside" && SpanText}
{textPosition === "inside" && spanTextElement}
{llmMetrics && (
<div className="absolute right-4 flex items-center gap-2 text-xs font-medium text-secondary-foreground z-30">
<span>${llmMetrics.cost}</span>
<span>
{llmMetrics.totalTokens} tokens
</span>
</div>
)}
</div>
{textPosition === "outside" && SpanText}
{textPosition === "outside" && spanTextElement}
</div>
);
};
Expand Down
20 changes: 20 additions & 0 deletions frontend/components/traces/trace-view/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,23 @@ export const findSpanToSelect = (
// Priority 3: First span as fallback
return spans?.[0];
};



export const getSpanDisplayName = (span:TraceViewSpan) => {
const modelName = span.model ?? span.attributes["gen_ai.request.model"];
return span.spanType === "LLM" ? modelName : span.name;
};


export const getLLMMetrics = (span:TraceViewSpan)=>{
if (span.spanType !== "LLM") return null;

const cost = span.attributes["gen_ai.usage.cost"].toFixed(3);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure gen_ai.usage.cost exists and is a number before calling toFixed(3) to avoid runtime errors.

const totalTokens =
span.attributes["gen_ai.usage.input_tokens"] + span.attributes["gen_ai.usage.output_tokens"];

if (!cost || !totalTokens) return null;

return { cost, totalTokens };
};