Trace Heatmap Visualization#
The heatmap in the trace detail UI color-codes each span's cost and duration metrics relative to the root-level aggregated totals, letting engineers spot expensive or slow spans at a glance. The coloring is opt-in via the colorCodeMetrics view preference.
Color Scale Logic#
heatMapTextColor() in helpers.ts computes a standardized ratio (value − min) / (max − min) and maps it to Tailwind CSS classes:
| Ratio | Class | Color |
|---|---|---|
| ≥ 75% | text-dark-red | Red |
| ≥ 50% | text-dark-yellow | Yellow |
| < 50% | (none) | Default |
Both cost and duration are independently color-coded. The function is called in SpanContent.tsx when colorCodeMetrics is enabled and a parentTotalDuration / parentTotalCost is provided.
Root Aggregation — What "Parent Total" Means#
To keep the color scale consistent across all spans, the "max" value is always the aggregate across all root nodes, not the local parent's value.
Cost#
All root nodes' totalCost values are summed:
TraceTree.tsx— computed directly fromrootson each renderTraceTimeline/index.tsx— computed in auseMemofromrootstree-building.ts→buildTraceUiData()— computed during tree construction and stored in eachTraceSearchListItem.parentTotalCost
⚠️ Known duplication: The aggregation logic appears in three places. All three carry a
// TODO: Extract aggregation logic to shared utilitycomment .
Duration#
Duration uses the maximum latency across all roots (in milliseconds):
rootDuration = max(...roots.map(r => r.latency != null ? r.latency * 1000 : 0))
Multi-Root Scenario Handling#
Traditional traces wrap all observations in a single synthetic TRACE node (id: "trace-{id}"), so there is typically one root . However, events-based traces (identified by trace.rootObservationType) bypass the TRACE wrapper and return multiple root observation nodes directly .
In the multi-root case, buildTraceUiData() reduces over the entire roots array to compute aggregated cost and Math.max(...) for duration before writing them into every TraceSearchListItem . This ensures the heatmap scale is consistent whether there is one root or many.
Missing endTime Fallback#
Trace nodes do not have an explicit endTime (set to null in the tree node) . Duration is therefore derived from latency (a pre-computed server-side field in seconds), not from endTime − startTime. For observation nodes that do have an endTime, the timeline uses endTime.getTime() − startTime.getTime() as a fallback when latency is absent :
r.latency
? r.latency * 1000
: r.endTime
? r.endTime.getTime() - r.startTime.getTime()
: 0
If neither is available, duration defaults to 0, which means the span will never trigger the ≥ 50% yellow threshold and is shown without heatmap color.
Cost Aggregation in the Tree#
Node costs are aggregated bottom-up during tree construction in buildTreeNodesBottomUp(): each TreeNode.totalCost is the node's own cost plus the sum of all descendant costs. When a node lacks totalCost but has inputCost/outputCost, those are summed as a fallback . The root's totalCost therefore represents the full subtree's cost, which is what drives heatmap scaling for all descendants.
Key Files#
| File | Role |
|---|---|
web/src/components/trace2/lib/helpers.ts | heatMapTextColor() implementation |
web/src/components/trace2/lib/tree-building.ts | buildTraceUiData() — tree construction + root aggregation |
web/src/components/trace2/components/TraceTree.tsx | Tree view — computes rootTotalCost / rootTotalDuration |
web/src/components/trace2/components/TraceTimeline/index.tsx | Timeline view — computes parentTotalCost / parentTotalDuration |
web/src/components/trace2/components/SpanContent.tsx | Applies heatMapTextColor() to rendered cost/duration labels |
web/src/components/trace2/lib/types.ts | TreeNode and TraceSearchListItem type definitions |