Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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<TraceTreeSettingsState>(
"traceTreeSettings",
{
latency: true,
cost: true,
tokens: true,
},
)
const {sessionTraces, aggregatedStats} = useSessionDrawer()

const turnsTree: TraceSpanNode[] = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const TraceContent = ({
traces={traces}
/>

<Splitter className="h-[87vh] flex">
<Splitter className="flex-1 min-h-0">
<Splitter.Panel min={400} className="w-full flex-1">
<div className="flex-1">
<Tabs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,14 @@ const TraceDrawerContent = ({onClose, onToggleWidth, isExpanded}: TraceDrawerCon
/>
</div>
</div>
<Spin spinning={Boolean(isLoading)} tip="Loading trace…" size="large">
<Spin
spinning={Boolean(isLoading)}
tip="Loading trace…"
size="large"
wrapperClassName="flex-1 min-h-0 [&_.ant-spin-container]:h-full"
>
<div className="h-full">
<Splitter className="h-[calc(100%-48px)]">
<Splitter className="h-full">
<Splitter.Panel defaultSize={320} collapsible>
<TraceTree
activeTraceId={activeId}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import {SpanCategory, StatusCode, TraceSpanNode} from "@/oss/services/tracing/types"

/**
* Span tree visibility modes.
*
* - `all`: show every span (default, original behaviour).
* - `key`: collapse the tree down to the spans that carry real signal and hide
* structural/utility wrappers (chains, parsers, lambdas, ...).
*/
export type SpanVisibilityMode = "all" | "key"

export interface SpanVisibilityOption {
value: SpanVisibilityMode
label: string
hint: string
}

export const SPAN_VISIBILITY_OPTIONS: SpanVisibilityOption[] = [
{value: "all", label: "All spans", hint: "Show every span in the trace"},
{value: "key", label: "Key spans", hint: "LLM, tool, and agent spans, plus errors"},
]

/**
* Span categories that represent actual model/tool/retrieval work rather than
* structural plumbing. Spans of these types are always kept by the "Key spans"
* filter.
*
* This set is intentionally the main knob for tuning the filter: add or remove a
* category here to change what counts as a key span across the whole app.
*/
export const KEY_SPAN_TYPES: ReadonlySet<SpanCategory> = 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}
}
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"

Expand Down Expand Up @@ -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<TraceTreeSettingsState>(
"traceTreeSettings",
{
latency: true,
cost: true,
tokens: true,
visibility: "key",
},
)

const {getTraceById, traces: allTraces} = useTraceDrawer()
const activeTrace = active || getTraceById(activeTraceId)
Expand Down Expand Up @@ -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) => <TreeContent value={node} settings={traceTreeSettings} />,
[traceTreeSettings],
Expand Down Expand Up @@ -171,26 +194,49 @@ const TraceTree = ({activeTrace: active, activeTraceId, selected, setSelected}:
<TraceTreeSettings
settings={traceTreeSettings}
setSettings={setTraceTreeSettings}
showVisibility
/>
}
placement="bottomRight"
classNames={{body: "!p-0 w-[200px]"}}
classNames={{body: "!p-0 w-[240px]"}}
arrow={false}
>
<Button icon={<SlidersHorizontal size={14} />} type="text" size="small" />
</Popover>
</div>
<Divider orientation="horizontal" className="m-0" />

<CustomTreeComponent
data={filteredTree}
getKey={(node) => node.span_id}
getChildren={(node) => node.children as TraceSpanNode[] | undefined}
renderLabel={renderTraceLabel}
selectedKey={selected}
onSelect={(key) => setSelected(key)}
defaultExpanded
/>
<div className="flex-1 min-h-0 overflow-y-auto">
<CustomTreeComponent
data={displayTree}
getKey={(node) => node.span_id}
getChildren={(node) => node.children as TraceSpanNode[] | undefined}
renderLabel={renderTraceLabel}
selectedKey={selected}
onSelect={(key) => setSelected(key)}
defaultExpanded
/>

{hiddenCount > 0 && (
<div className="flex items-center gap-2 mx-2 mb-2 px-3 py-2 rounded-md bg-colorFillTertiary border border-solid border-colorBorderSecondary">
<Info size={14} className="shrink-0 text-colorTextTertiary" />
<Typography.Text className="text-[12px] text-colorTextSecondary">
<span className="font-medium text-colorText">{hiddenCount}</span>{" "}
{hiddenCount === 1 ? "span" : "spans"} hidden by key spans
</Typography.Text>
<Button
type="link"
size="small"
className="ml-auto !px-0 !h-auto text-[12px]"
onClick={() =>
setTraceTreeSettings((prev) => ({...prev, visibility: "all"}))
}
>
Show all
</Button>
</div>
)}
</div>
</div>
)
}
Expand Down
Loading
Loading