diff --git a/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionTree/index.tsx b/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionTree/index.tsx index eb9b11ca86..d5ce96d82d 100644 --- a/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionTree/index.tsx +++ b/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionTree/index.tsx @@ -12,6 +12,7 @@ import {TraceSpanNode} from "@/oss/services/tracing/types" import {TreeContent} from "../../../TraceDrawer/components/TraceTree" import TraceTreeSettings from "../../../TraceDrawer/components/TraceTreeSettings" +import {TraceTreeSettingsState} from "../../../TraceDrawer/components/TraceTreeSettings/types" import {openTraceDrawerAtom} from "../../../TraceDrawer/store/traceDrawerStore" import {useSessionDrawer} from "../../hooks/useSessionDrawer" @@ -21,11 +22,17 @@ const SessionTree = ({selected, setSelected}: SessionTreeProps) => { const [searchValue, setSearchValue] = useState("") const openTraceDrawer = useSetAtom(openTraceDrawerAtom) - const [traceTreeSettings, setTraceTreeSettings] = useLocalStorage("traceTreeSettings", { - latency: true, - cost: true, - tokens: true, - }) + // Shares the "traceTreeSettings" key with the trace drawer. SessionTree does + // not filter spans, so it renders the settings popover without the visibility + // section and never reads `visibility`. + const [traceTreeSettings, setTraceTreeSettings] = useLocalStorage( + "traceTreeSettings", + { + latency: true, + cost: true, + tokens: true, + }, + ) const {sessionTraces, aggregatedStats} = useSessionDrawer() const turnsTree: TraceSpanNode[] = useMemo(() => { diff --git a/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceContent/index.tsx b/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceContent/index.tsx index 093948c731..7493e43344 100644 --- a/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceContent/index.tsx +++ b/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceContent/index.tsx @@ -136,7 +136,7 @@ const TraceContent = ({ traces={traces} /> - +
- +
- + = new Set([ + SpanCategory.AGENT, + SpanCategory.LLM, + SpanCategory.CHAT, + SpanCategory.COMPLETION, + SpanCategory.TOOL, + SpanCategory.EMBEDDING, + SpanCategory.QUERY, + SpanCategory.RERANK, +]) + +/** + * Span name patterns that mark a span as meaningful even when its type is a + * generic wrapper (e.g. LangChain reports output parsers as `chain` spans, but + * a tool/agent output parser carries the model's tool-call decision). + * + * This is the second main knob: add a pattern to surface a span the type-based + * rule would otherwise hide. + */ +export const KEY_SPAN_NAME_PATTERNS: readonly RegExp[] = [/OutputParser/i] + +/** + * A rule that decides whether a single span is relevant on its own merits. + * + * A span survives the "Key spans" filter when ANY rule matches it, when it is the + * trace root, or when one of its descendants survives (so the path to a key span + * stays intact). Add, remove, or reorder rules here to evolve the definition of a + * key span over time — the filter logic below does not need to change. + */ +export interface KeySpanRule { + id: string + description: string + test: (node: TraceSpanNode) => boolean +} + +export const keySpanRules: KeySpanRule[] = [ + { + id: "work-span-type", + description: "Model, tool, agent, and retrieval spans", + test: (node) => !!node.span_type && KEY_SPAN_TYPES.has(node.span_type), + }, + { + id: "key-name", + description: "Spans whose name marks them as meaningful (e.g. output parsers)", + test: (node) => + !!node.span_name && KEY_SPAN_NAME_PATTERNS.some((re) => re.test(node.span_name!)), + }, + { + id: "errored-span", + description: "Spans that ended in an error", + test: (node) => node.status_code === StatusCode.STATUS_CODE_ERROR, + }, +] + +export const isKeySpan = (node: TraceSpanNode): boolean => + keySpanRules.some((rule) => rule.test(node)) + +export interface SpanFilterResult { + /** Pruned tree, or null when there is no input tree. */ + tree: TraceSpanNode | null + /** Number of spans removed from view by the filter. */ + hiddenCount: number +} + +/** + * Prune a span tree down to its key spans. + * + * The root is always kept so the trace stays anchored. Non-key spans are removed + * and their surviving descendants are promoted to the nearest kept ancestor — so + * wrapper chains (RunnableSequence, RunnableMap, ...) disappear and the key spans + * they contain attach directly to the root, rather than being kept as scaffolding. + */ +export const filterKeySpans = (root?: TraceSpanNode): SpanFilterResult => { + if (!root) return {tree: null, hiddenCount: 0} + + let hiddenCount = 0 + + const collect = (node: TraceSpanNode, isRoot: boolean): TraceSpanNode[] => { + const keptChildren = ((node.children as TraceSpanNode[] | undefined) || []).flatMap( + (child) => collect(child, false), + ) + + if (isRoot || isKeySpan(node)) { + return [{...node, children: keptChildren}] + } + + hiddenCount += 1 + return keptChildren + } + + const [tree] = collect(root, true) + return {tree: tree ?? null, hiddenCount} +} diff --git a/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTree/index.tsx b/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTree/index.tsx index 8009787052..27197b9265 100644 --- a/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTree/index.tsx +++ b/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTree/index.tsx @@ -1,6 +1,13 @@ import {useCallback, useMemo, useState} from "react" -import {Coins, MagnifyingGlass, PlusCircle, SlidersHorizontal, Timer} from "@phosphor-icons/react" +import { + Coins, + Info, + MagnifyingGlass, + PlusCircle, + SlidersHorizontal, + Timer, +} from "@phosphor-icons/react" import {Button, Divider, Input, Popover, Space, Tooltip, Typography} from "antd" import clsx from "clsx" import {useAtomValue} from "jotai" @@ -18,7 +25,9 @@ import { import useTraceDrawer from "../../hooks/useTraceDrawer" import TraceTreeSettings from "../TraceTreeSettings" +import {TraceTreeSettingsState} from "../TraceTreeSettings/types" +import {filterKeySpans} from "./assets/spanVisibility" import {useStyles} from "./assets/styles" import {TraceTreeProps} from "./assets/types" @@ -96,11 +105,15 @@ const TraceTree = ({activeTrace: active, activeTraceId, selected, setSelected}: const classes = useStyles() const [searchValue, setSearchValue] = useState("") - const [traceTreeSettings, setTraceTreeSettings] = useLocalStorage("traceTreeSettings", { - latency: true, - cost: true, - tokens: true, - }) + const [traceTreeSettings, setTraceTreeSettings] = useLocalStorage( + "traceTreeSettings", + { + latency: true, + cost: true, + tokens: true, + visibility: "key", + }, + ) const {getTraceById, traces: allTraces} = useTraceDrawer() const activeTrace = active || getTraceById(activeTraceId) @@ -132,12 +145,22 @@ const TraceTree = ({activeTrace: active, activeTraceId, selected, setSelected}: return nodes[0] || activeTrace }, [activeTrace, allTraces]) - const filteredTree = useMemo(() => { + // Tree after the text search filter (the original behaviour). + const searchedTree = useMemo(() => { if (!searchValue.trim()) return treeRoot as any const result = filterTree(treeRoot as any, searchValue) return result || {...treeRoot, children: []} }, [searchValue, treeRoot]) + // Apply the span visibility filter on top of the searched tree. + const {displayTree, hiddenCount} = useMemo(() => { + if ((traceTreeSettings.visibility ?? "key") !== "key" || !searchedTree) { + return {displayTree: searchedTree, hiddenCount: 0} + } + const {tree, hiddenCount: count} = filterKeySpans(searchedTree as TraceSpanNode) + return {displayTree: (tree as any) || {...searchedTree, children: []}, hiddenCount: count} + }, [searchedTree, traceTreeSettings.visibility]) + const renderTraceLabel = useCallback( (node: TraceSpanNode) => , [traceTreeSettings], @@ -171,10 +194,11 @@ const TraceTree = ({activeTrace: active, activeTraceId, selected, setSelected}: } placement="bottomRight" - classNames={{body: "!p-0 w-[200px]"}} + classNames={{body: "!p-0 w-[240px]"}} arrow={false} >
- node.span_id} - getChildren={(node) => node.children as TraceSpanNode[] | undefined} - renderLabel={renderTraceLabel} - selectedKey={selected} - onSelect={(key) => setSelected(key)} - defaultExpanded - /> +
+ node.span_id} + getChildren={(node) => node.children as TraceSpanNode[] | undefined} + renderLabel={renderTraceLabel} + selectedKey={selected} + onSelect={(key) => setSelected(key)} + defaultExpanded + /> + + {hiddenCount > 0 && ( +
+ + + {hiddenCount}{" "} + {hiddenCount === 1 ? "span" : "spans"} hidden by key spans + + +
+ )} +
) } diff --git a/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTreeSettings/index.tsx b/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTreeSettings/index.tsx index b78cf68654..8dd952b999 100644 --- a/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTreeSettings/index.tsx +++ b/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTreeSettings/index.tsx @@ -1,51 +1,89 @@ -import React from "react" +import {ReactNode} from "react" -import {Switch, Typography} from "antd" +import {Check} from "@phosphor-icons/react" +import {Divider, Switch, Typography} from "antd" import clsx from "clsx" +import {SPAN_VISIBILITY_OPTIONS, SpanVisibilityMode} from "../TraceTree/assets/spanVisibility" + import {TraceTreeSettingsProps} from "./types" -const TraceTreeSettings = ({settings, setSettings}: TraceTreeSettingsProps) => { - const handleSwitchChange = (key: keyof typeof settings, checked: boolean) => { - setSettings((prev) => ({ - ...prev, - [key]: checked, - })) +const DISPLAY_TOGGLES = [ + {key: "latency", label: "Show latency"}, + {key: "cost", label: "Show cost"}, + {key: "tokens", label: "Show tokens"}, +] as const + +const SectionLabel = ({children}: {children: ReactNode}) => ( + + {children} + +) + +const TraceTreeSettings = ({ + settings, + setSettings, + showVisibility = false, +}: TraceTreeSettingsProps) => { + const handleSwitchChange = (key: (typeof DISPLAY_TOGGLES)[number]["key"], checked: boolean) => { + setSettings((prev) => ({...prev, [key]: checked})) + } + + const visibility = settings.visibility ?? "key" + const setVisibility = (mode: SpanVisibilityMode) => { + setSettings((prev) => ({...prev, visibility: mode})) } return ( -
-
- Settings -
- -
-
- Show Latency - handleSwitchChange("latency", checked)} - /> -
-
- Show Cost - handleSwitchChange("cost", checked)} - /> -
-
- Show Tokens +
+ Display + {DISPLAY_TOGGLES.map(({key, label}) => ( +
+ {label} handleSwitchChange("tokens", checked)} + size="small" + checked={settings[key]} + onChange={(checked) => handleSwitchChange(key, checked)} />
-
+ ))} + + {showVisibility && ( + <> + + Visibility + {SPAN_VISIBILITY_OPTIONS.map((option) => ( +
setVisibility(option.value)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + setVisibility(option.value) + } + }} + > +
+ {option.label} + + {option.hint} + +
+ +
+ ))} + + )}
) } diff --git a/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTreeSettings/types.ts b/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTreeSettings/types.ts index af9303bd9f..6d3884cb15 100644 --- a/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTreeSettings/types.ts +++ b/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTreeSettings/types.ts @@ -1,8 +1,17 @@ import React from "react" +import {SpanVisibilityMode} from "../TraceTree/assets/spanVisibility" + +export interface TraceTreeSettingsState { + latency: boolean + cost: boolean + tokens: boolean + visibility?: SpanVisibilityMode +} + export interface TraceTreeSettingsProps { - settings: {latency: boolean; cost: boolean; tokens: boolean} - setSettings: React.Dispatch< - React.SetStateAction<{latency: boolean; cost: boolean; tokens: boolean}> - > + settings: TraceTreeSettingsState + setSettings: React.Dispatch> + /** Render the span visibility section (key spans vs all spans). */ + showVisibility?: boolean }