Skip to content

feat(traces): surface span links in the trace viewer#2463

Draft
alex-fedotyev wants to merge 2 commits into
mainfrom
alex/hdx-3191-span-links
Draft

feat(traces): surface span links in the trace viewer#2463
alex-fedotyev wants to merge 2 commits into
mainfrom
alex/hdx-3191-span-links

Conversation

@alex-fedotyev

Copy link
Copy Markdown
Contributor

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 Links column 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:

  • an "Open trace" action, with the full Trace and Span IDs on hover;
  • the trace state and any link attributes as chips;
  • a link with neither 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 already uses, so it stacks above the trace drawer in both the search-results and the direct-trace entry points.

The Links column 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-lint and make ci-unit pass (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.
  • Storybook story added (Default / SingleLink / Empty).
  • Drove the live trace viewer in both light and dark themes: the section renders on a span with three links and is absent (empty case) on spans without links; an error in rendering is contained by an error boundary; "Open trace" opens the linked trace in the nested panel with a working back breadcrumb, stacked above the trace drawer in both the search and direct-trace paths.
  • Viewport: the section sits inside the existing span-detail side panel and inherits its responsive width.

Implements #1593

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-bot

changeset-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4416036

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Patch
@hyperdx/app Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

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

@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jun 17, 2026 1:47am
hyperdx-storybook Ready Ready Preview, Comment Jun 17, 2026 1:47am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 432 production lines changed (Tier 2 max: < 250)
  • Cross-layer change: touches frontend (packages/app) + backend (packages/api) + shared utils (packages/common-utils)
  • Touches API routes or data models — hidden complexity risk

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 9
  • Production lines changed: 432 (+ 114 in test files, excluded from tier calculation)
  • Branch: alex/hdx-3191-span-links
  • Author: alex-fedotyev

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown

Greptile Summary

Surfaces OTel span links in the trace viewer by adding a spanLinksValueExpression field to the TraceSource model and rendering a collapsible "Span Links" accordion section in the span detail panel. The feature is display-only: it auto-detects the standard OTel Links column during source inference and reads it through a guarded select, so sources without it are untouched.

  • SpanLinksSubpanel: new component that renders each link as a compact row with a Tooltip-wrapped "Open trace" action, trace-state and attribute chips, and a "Show more" toggle capped at 5 rows; type-guards malformed data before rendering.
  • Navigation plumbing: two distinct paths are wired — a nested-drawer flow (from DBRowSidePanel) and an in-place trace-nav back-stack (from DBTracePanel) — each reusing existing side-panel and breadcrumb infrastructure.
  • DirectTraceSidePanel: wraps its content in ZIndexContext.Provider so panels opened inside the trace drawer stack correctly above it.

Confidence Score: 5/5

Display-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

Filename Overview
packages/app/src/components/SpanLinksSubpanel.tsx New component rendering OTel span links as compact rows; type-guards malformed data, uses useShowMoreRows for pagination, wraps in ErrorBoundary. Consistent with SpanEventsSubpanel patterns throughout.
packages/app/src/components/DBTracePanel.tsx Adds ~130 lines of in-place trace navigation (back-stack + breadcrumb) labelled 'Prototype (HDX-3191 demo): nothing here ships as-is' — but the code IS wired to the live UI. The feature works correctly; the misleading comments are the main concern.
packages/app/src/components/DBRowOverviewPanel.tsx Adds span-links accordion section and the nested-drawer Open trace path. SQL WHERE clause built via SqlString.format with proper escaping. State/memo wiring is correct.
packages/app/src/components/Search/DirectTraceSidePanel.tsx Wraps content in ZIndexContext.Provider seeded from the drawer z-index, ensuring nested span-link panels stack above the trace drawer. Clean refactor.
packages/app/src/source.ts Auto-detects the OTel Links column (Links.TraceId) during source inference and sets spanLinksValueExpression. Mirrors the hasSpanEvents pattern exactly.
packages/common-utils/src/types.ts Adds optional spanLinksValueExpression zod field to TraceSourceSchema. Minimal, correct schema extension.
packages/api/src/models/source.ts Adds spanLinksValueExpression: String to the Mongoose TraceSource discriminator. Straightforward schema addition.
packages/app/src/components/tests/SpanLinksSubpanel.test.tsx 9 unit tests covering empty/malformed input, attribute chips, trace-state chip, callback invocation, and multi-link rendering. Good coverage of the type-guard edge cases.

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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "feat(traces): navigate linked traces in ..." | Re-trigger Greptile

maxRows: 5,
});

if (!links || links.length === 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant null-guard on links

links is always an array — useMemo initialises it to [] and the filter always returns an array — so !links is permanently false and the branch can never be reached. The dead-code check could mislead future readers into thinking useMemo can return null.

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Comment on lines +199 to +204
const hasSpanLinks = useMemo(() => {
return (
Array.isArray(firstRow?.__hdx_span_links) &&
firstRow?.__hdx_span_links.length > 0
);
}, [firstRow?.__hdx_span_links]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 "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!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Comment thread packages/app/src/components/DBRowOverviewPanel.tsx
@github-actions

github-actions Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 201 passed • 3 skipped • 1306s

Status Count
✅ Passed 201
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@alex-fedotyev alex-fedotyev marked this pull request as draft June 15, 2026 22:24
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.
@alex-fedotyev

Copy link
Copy Markdown
Contributor Author

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 onNavigateToLinkedTrace callback on RowOverviewPanel. Only the inline-split flyout passes it, so the search-results side panel and the Overview tab still open the nested drawer. That two-behavior split is the main thing I want a read on: do we take the whole feature to in-place navigation, keep the drawer everywhere, or land some mix.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant