Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
2 changes: 1 addition & 1 deletion frontend/components/shared/traces/span-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function SpanView({ spanId, traceId }: SpanViewProps) {
<MonoWithCopy className="text-muted-foreground">{span.spanId}</MonoWithCopy>
</div>
<div className="flex flex-wrap py-1 gap-2">
<SpanStatsShields startTime={span.startTime} endTime={span.endTime} attributes={span.attributes}>
<SpanStatsShields span={span}>
<div className="flex flex-row text-xs font-mono space-x-2 rounded-md p-0.5 px-2 border items-center">
{new Date(span.startTime).toLocaleString()}
</div>
Expand Down
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 @@ -86,7 +86,6 @@ const PureTraceView = ({ trace, spans }: TraceViewProps) => {
spanPath: state.spanPath,
setSpanPath: state.setSpanPath,
}));

const hasLangGraph = useMemo(() => getHasLangGraph(), [getHasLangGraph]);
const llmSpanIds = useMemo(
() =>
Expand Down
22 changes: 12 additions & 10 deletions frontend/components/traces/span-controls.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand All @@ -37,7 +42,10 @@ export function SpanControls({ children, span, events }: PropsWithChildren<SpanC
);

const { toast } = useToast();
const { openInSql, isLoading } = useOpenInSql({ projectId: projectId as string, params: { type: 'span', spanId: span.spanId } });
const { openInSql, isLoading } = useOpenInSql({
projectId: projectId as string,
params: { type: "span", spanId: span.spanId },
});

const handleCopySpanId = useCallback(async () => {
if (span?.spanId) {
Expand Down Expand Up @@ -82,12 +90,7 @@ export function SpanControls({ children, span, events }: PropsWithChildren<SpanC
)}
</div>
<div className="flex flex-col flex-wrap gap-1.5">
<SpanStatsShields
className="flex-wrap"
startTime={span.startTime}
endTime={span.endTime}
attributes={span.attributes}
>
<SpanStatsShields className="flex-wrap" span={span}>
<div className="text-xs font-mono space-x-2 rounded-md p-0.5 truncate px-2 border items-center">
{new Date(span.startTime).toLocaleString()}
</div>
Expand All @@ -101,7 +104,6 @@ export function SpanControls({ children, span, events }: PropsWithChildren<SpanC
</div>
<TagsList />
<EvaluatorScoresList spanId={span.spanId} />

</TagsContextProvider>
</div>

Expand Down
108 changes: 43 additions & 65 deletions frontend/components/traces/stats-shields.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -18,9 +19,7 @@ interface TraceStatsShieldsProps {
}

interface SpanStatsShieldsProps {
startTime: string;
endTime: string;
attributes: Record<string, any>;
span: Span;
className?: string;
}

Expand Down Expand Up @@ -119,61 +118,48 @@ const extractToolsFromAttributes = (attributes: Record<string, any>): 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;
})
);
};

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 (
<div className={cn("flex items-center gap-2 font-mono min-w-0", className)}>
<div className="flex space-x-1 items-center p-0.5 min-w-8 px-2 border rounded-md">
<Clock3 size={12} className="min-w-3 min-h-3" />
<Label className="text-xs truncate text-foreground" title={getDurationString(startTime, endTime)}>
{getDurationString(startTime, endTime)}
<Label className="text-xs truncate text-foreground" title={getDurationString(stats.startTime, stats.endTime)}>
{getDurationString(stats.startTime, stats.endTime)}
</Label>
</div>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger className="min-w-8">
<div className="flex space-x-1 items-center p-0.5 min-w-8 px-2 border rounded-md">
<Coins className="min-w-3" size={12} />
<Label className="text-xs truncate text-foreground">{totalTokens}</Label>
<Label className="text-xs truncate text-foreground">{stats.totalTokens}</Label>
</div>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="bottom" className="p-2 border">
<div className="flex-col space-y-1">
<Label className="flex text-xs gap-1">
<span className="text-secondary-foreground">Input tokens</span> {inputTokens}
<span className="text-secondary-foreground">Input tokens</span> {stats.inputTokens}
</Label>
<Label className="flex text-xs gap-1">
<span className="text-secondary-foreground">Output tokens</span> {outputTokens}
<span className="text-secondary-foreground">Output tokens</span> {stats.outputTokens}
</Label>
</div>
</TooltipContent>
Expand All @@ -185,20 +171,20 @@ function StatsShieldsContent({
<TooltipTrigger className="min-w-8">
<div className="flex space-x-1 items-center p-0.5 px-2 min-w-8 border rounded-md">
<CircleDollarSign className="min-w-3" size={12} />
<Label className="text-xs truncate text-foreground">{totalCost?.toFixed(3)}</Label>
<Label className="text-xs truncate text-foreground">{stats.totalCost?.toFixed(3)}</Label>
</div>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="bottom" className="p-2 border">
<div className="flex-col space-y-1">
<Label className="flex text-xs gap-1">
<span className="text-secondary-foreground">Total cost</span> {"$" + totalCost?.toFixed(5)}
<span className="text-secondary-foreground">Total cost</span> {"$" + stats.totalCost?.toFixed(5)}
</Label>
<Label className="flex text-xs gap-1">
<span className="text-secondary-foreground">Input cost</span> {"$" + inputCost?.toFixed(5)}
<span className="text-secondary-foreground">Input cost</span> {"$" + stats.inputCost?.toFixed(5)}
</Label>
<Label className="flex text-xs gap-1">
<span className="text-secondary-foreground">Output cost</span> {"$" + outputCost?.toFixed(5)}
<span className="text-secondary-foreground">Output cost</span> {"$" + stats.outputCost?.toFixed(5)}
</Label>
</div>
</TooltipContent>
Expand All @@ -212,49 +198,41 @@ function StatsShieldsContent({

const PureTraceStatsShields = ({ trace, className, children }: PropsWithChildren<TraceStatsShieldsProps>) => (
<StatsShieldsContent
startTime={trace.startTime}
endTime={trace.endTime}
totalTokens={trace.totalTokens}
inputTokens={trace.inputTokens}
outputTokens={trace.outputTokens}
inputCost={trace.inputCost}
outputCost={trace.outputCost}
totalCost={trace.totalCost}
stats={pick(trace, [
"startTime",
"endTime",
"inputTokens",
"outputTokens",
"totalTokens",
"inputCost",
"outputCost",
"totalCost",
])}
className={className}
>
{children}
</StatsShieldsContent>
);

const SpanStatsShields = ({
startTime,
endTime,
attributes,
className,
children,
}: PropsWithChildren<SpanStatsShieldsProps>) => {
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<SpanStatsShieldsProps>) => {
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 (
<div className="flex flex-wrap flex-col gap-1.5">
<StatsShieldsContent
startTime={startTime}
endTime={endTime}
totalTokens={totalTokenCount}
inputTokens={inputTokenCount}
outputTokens={outputTokenCount}
inputCost={inputCost}
outputCost={outputCost}
totalCost={cost}
stats={pick(span, [
"startTime",
"endTime",
"inputTokens",
"outputTokens",
"totalTokens",
"inputCost",
"outputCost",
"totalCost",
])}
className={className}
>
{children}
Expand Down
51 changes: 38 additions & 13 deletions frontend/components/traces/trace-view/span-card.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -30,6 +32,10 @@ interface SpanCardProps {
onSpanSelect?: (span?: TraceViewSpan) => void;
}

const numberFormatter = new Intl.NumberFormat("en-US", {
notation: "standard",
});

export function SpanCard({ span, yOffset, parentY, onSpanSelect, depth }: SpanCardProps) {
const [segmentHeight, setSegmentHeight] = useState(0);
const ref = useRef<HTMLDivElement>(null);
Expand All @@ -39,7 +45,7 @@ export function SpanCard({ span, yOffset, parentY, onSpanSelect, depth }: SpanCa
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 @@ -95,14 +101,11 @@ export function SpanCard({ span, yOffset, parentY, onSpanSelect, depth }: SpanCa
status={span.status}
className={cn("min-w-[22px]", { "text-muted-foreground bg-muted ": span.pending })}
/>
<div
className={cn(
"whitespace-nowrap text-base",
span.pending && "text-muted-foreground"
)}
>
{span.name}
</div>
<SpanDisplayTooltip isLLM={span.spanType === "LLM"} name={span.name}>
<div className={cn("whitespace-nowrap text-base", span.pending && "text-muted-foreground")}>
{getSpanDisplayName(span)}
</div>
</SpanDisplayTooltip>
{span.pending ? (
isStringDateOld(span.startTime) ? (
<NoSpanTooltip>
Expand All @@ -114,9 +117,31 @@ export function SpanCard({ span, yOffset, parentY, onSpanSelect, depth }: SpanCa
<Skeleton className="w-10 h-4 text-secondary-foreground px-2 py-0.5 bg-secondary rounded-full text-xs" />
)
) : (
<div className="text-secondary-foreground px-2 py-0.5 bg-muted rounded-full text-xs">
{getDurationString(span.startTime, span.endTime)}
</div>
<>
<div className="text-secondary-foreground px-2 py-0.5 bg-muted rounded-full text-xs">
{getDurationString(span.startTime, span.endTime)}
</div>
{llmMetrics && (
<>
<div
className={
"text-secondary-foreground px-2 py-0.5 bg-muted rounded-full text-xs inline-flex items-center gap-1"
}
>
<Coins className="min-w-3" size={12} />
{numberFormatter.format(llmMetrics.tokens)}
</div>
<div
className={
"text-secondary-foreground px-2 py-0.5 bg-muted rounded-full text-xs inline-flex items-center gap-1"
}
>
<CircleDollarSign className="min-w-3" size={12} />
{llmMetrics.cost.toFixed(3)}
</div>
</>
)}
</>
)}
{hasChildren && (
<button
Expand Down
33 changes: 33 additions & 0 deletions frontend/components/traces/trace-view/span-display-tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { TooltipPortal } from "@radix-ui/react-tooltip";
import React, { PropsWithChildren } from "react";

import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip.tsx";
import { cn } from "@/lib/utils.ts";

interface SpanDisplayTooltipProps {
isLLM: boolean;
name: string;
}

export const SpanDisplayTooltip = ({ name, isLLM, children }: PropsWithChildren<SpanDisplayTooltipProps>) => {
if (isLLM) {
return (
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger>{children}</TooltipTrigger>
<TooltipPortal>
<TooltipContent
side="top"
align="start"
className={cn("text-sm p-2 text-center border whitespace-pre-wrap text-secondary-foreground")}
>
{name}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</TooltipProvider>
);
}

return children;
};
Loading