feat(traces): surface span links in the trace viewer#2463
feat(traces): surface span links in the trace viewer#2463alex-fedotyev wants to merge 2 commits into
Conversation
Show a "Span Links" section in the span detail when a span carries outgoing OpenTelemetry links. Each link renders as a compact row: an "Open trace" action, the trace state and attributes as chips, and the full Trace and Span IDs on hover. A link with neither state nor attributes collapses to a single line, and links past the first five sit behind a "Show more" toggle. "Open trace" opens the linked trace in the existing nested side panel with breadcrumb back navigation, the same flow the Surrounding Context tab uses, so it stacks above the trace drawer in both the search and direct-trace entry points. The Links column is auto-detected from the standard OTel trace schema and read through a guarded select, so nothing changes for sources that do not have it. This is a display-only field, so no source configuration UI and no external API contract are added. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 4416036 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🟡 Tier 3 — StandardIntroduces new logic, modifies core functionality, or touches areas with non-trivial risk. Why this tier:
Review process: Full human review — logic, architecture, edge cases. Stats
|
Greptile SummarySurfaces OTel span links in the trace viewer by adding a
Confidence Score: 5/5Display-only feature with no new API surface; all changed paths are guarded by the presence of spanLinksValueExpression, leaving sources without it untouched. The span-links section is purely additive and well-isolated. SQL WHERE clauses for linked-span lookups are built with SqlString.format and properly escaped. The type guard in SpanLinksSubpanel filters malformed data before rendering. The two navigation paths both function correctly, and the z-index plumbing in DirectTraceSidePanel ensures nested panels render above the drawer. packages/app/src/components/DBTracePanel.tsx — the in-place navigation prototype carries comments that contradict its live wiring; worth a second read to confirm the intended long-term plan for that code path. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant SpanLinksSubpanel
participant RowOverviewPanel
participant DBRowSidePanel as DBRowSidePanel (nested drawer)
participant DBTracePanel
participant LinkedTraceNavResolver
User->>SpanLinksSubpanel: clicks "Open trace"
SpanLinksSubpanel->>RowOverviewPanel: onOpenTrace(link)
alt From DBRowSidePanel (no onNavigateToLinkedTrace)
RowOverviewPanel->>RowOverviewPanel: setOpenedLink(link) build openedLinkWhere via SqlString
RowOverviewPanel->>DBRowSidePanel: "render nested DBRowSidePanel rowId=openedLinkWhere"
DBRowSidePanel-->>User: stacked drawer with back breadcrumb
else From DBTracePanel (onNavigateToLinkedTrace provided)
RowOverviewPanel->>DBTracePanel: onNavigateToLinkedTrace(link)
DBTracePanel->>DBTracePanel: setPendingLink(link)
DBTracePanel->>LinkedTraceNavResolver: mount resolver fetches linked span row
LinkedTraceNavResolver-->>DBTracePanel: onResolved(TraceNavEntry) timestamp spanId dateRange plus or minus 60 min
DBTracePanel->>DBTracePanel: push hop to navStack update waterfall traceId and dateRange
DBTracePanel-->>User: in-place navigation with breadcrumb bar
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant SpanLinksSubpanel
participant RowOverviewPanel
participant DBRowSidePanel as DBRowSidePanel (nested drawer)
participant DBTracePanel
participant LinkedTraceNavResolver
User->>SpanLinksSubpanel: clicks "Open trace"
SpanLinksSubpanel->>RowOverviewPanel: onOpenTrace(link)
alt From DBRowSidePanel (no onNavigateToLinkedTrace)
RowOverviewPanel->>RowOverviewPanel: setOpenedLink(link) build openedLinkWhere via SqlString
RowOverviewPanel->>DBRowSidePanel: "render nested DBRowSidePanel rowId=openedLinkWhere"
DBRowSidePanel-->>User: stacked drawer with back breadcrumb
else From DBTracePanel (onNavigateToLinkedTrace provided)
RowOverviewPanel->>DBTracePanel: onNavigateToLinkedTrace(link)
DBTracePanel->>DBTracePanel: setPendingLink(link)
DBTracePanel->>LinkedTraceNavResolver: mount resolver fetches linked span row
LinkedTraceNavResolver-->>DBTracePanel: onResolved(TraceNavEntry) timestamp spanId dateRange plus or minus 60 min
DBTracePanel->>DBTracePanel: push hop to navStack update waterfall traceId and dateRange
DBTracePanel-->>User: in-place navigation with breadcrumb bar
end
Reviews (2): Last reviewed commit: "feat(traces): navigate linked traces in ..." | Re-trigger Greptile |
| maxRows: 5, | ||
| }); | ||
|
|
||
| if (!links || links.length === 0) { |
There was a problem hiding this comment.
| const hasSpanLinks = useMemo(() => { | ||
| return ( | ||
| Array.isArray(firstRow?.__hdx_span_links) && | ||
| firstRow?.__hdx_span_links.length > 0 | ||
| ); | ||
| }, [firstRow?.__hdx_span_links]); |
There was a problem hiding this comment.
"Span Links" accordion visible with empty-state message for malformed data
hasSpanLinks is true whenever __hdx_span_links is a non-empty array, but SpanLinksSubpanel applies its own stricter type-filter inside useMemo (requires string TraceId, string SpanId, and a defined Attributes). If every object in the array passes the array check but fails the type-filter, the accordion section renders with a "Span Links" header but shows "No span links available for this trace" inside it — a contradictory UX. In practice real OTel data won't hit this, but the defensive check would be cheap: mirror the same SpanLinkData type guard in hasSpanLinks (or just reuse the filtered links array length).
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
E2E Test Results✅ All tests passed • 201 passed • 3 skipped • 1306s
Tests ran across 4 shards in parallel. |
An alternative to the current nested-drawer "Open trace" on a span link: reuse the inline-split trace panel (#2402) and navigate from one linked trace to the next in a single flyout, with a trace-level back stack and breadcrumb, instead of stacking a nested drawer per hop. Following a chain of links no longer builds a tower of drawers you can get lost in. Adds one optional onNavigateToLinkedTrace callback to RowOverviewPanel. Only the inline-split flyout passes it, so the search-results side panel and the Overview tab keep the existing nested-drawer behavior. The linked span's timestamp is resolved with the same SpanId + TraceId lookup the drawer uses, to set the destination time window and auto-focus the span on arrival. Up for team validation of the navigation approach.
|
Pushed a follow-up commit (4416036) with an alternative to the nested-drawer "Open trace" on a span link, so we can validate the navigation approach together. What it does: instead of opening each linked trace in a new stacked drawer (which builds a tower once you follow a chain of links), it reuses the inline-split trace panel from #2402 and navigates trace to trace in the same flyout, with a back stack and a breadcrumb of the spans you followed. Back pops a level; a breadcrumb crumb jumps straight to any level. The linked span auto-focuses on arrival. Scope: one optional Known gaps if we go this way: the Overview-tab "Open trace" still uses the drawer, there is no unit test yet for the nav stack, and no e2e for the multi-hop flow. I will fill those in once we pick a direction. To try it: open a trace whose span carries outgoing links, click "Open trace", and follow a couple of hops. |
Summary
Span links are the OpenTelemetry way of pointing from one span to a span in a different trace: a producer/consumer hop, the source records behind a batch, a retried request. HyperDX ingests them in the standard
Linkscolumn but never surfaced them, so in fan-out and batch flows the related spans just showed up as orphans.This adds a "Span Links" section to the span detail. It renders only when the span actually has links. Each link is a compact row:
"Open trace" opens the linked trace in the existing nested side panel with breadcrumb back navigation, the same flow the Surrounding Context tab already uses, so it stacks above the trace drawer in both the search-results and the direct-trace entry points.
The
Linkscolumn is auto-detected from the standard OTel trace schema and read through a guarded select, so sources without it are untouched and the rest of the row-detail panel keeps working. This is a display-only field, so it adds no source-configuration UI and no external API surface.Replaces #2440
Fresh branch replacing #2440, which I'm closing. That branch's history got tangled during a rebase. This one ships the same feature with a clean diff and trims the scope to what a display-only field actually needs: it drops the source-form field, the onboarding default, and the external-API and OpenAPI entries that the earlier draft threaded through.
Test plan
make ci-lintandmake ci-unitpass (full app suite plus common-utils). 9 new unit tests cover the compact rows: empty and malformed input, attribute chips, the trace-state chip, single-line collapse, and the "Open trace" callback.Implements #1593