Skip to content

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

Open
alex-fedotyev wants to merge 1 commit into
mainfrom
alex/hdx-3191-span-links
Open

feat(traces): surface span links in the trace viewer#2463
alex-fedotyev wants to merge 1 commit 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: 4f38c8a

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 13, 2026 5:24am
hyperdx-storybook Ready Ready Preview, Comment Jun 13, 2026 5:24am

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

This PR surfaces OpenTelemetry span links in the trace viewer by adding a "Span Links" accordion section to the span detail panel. Each link renders as a compact row with an "Open trace" button (tooltip shows full IDs), trace state and attribute chips, and a "show more" toggle past five links; clicking "Open trace" opens the linked span in a stacked nested side panel with breadcrumb back navigation.

  • Data layer: spanLinksValueExpression is auto-detected from Links.TraceId column presence at source inference time and guarded on source.kind === Trace in the select, so sources without the column are unaffected.
  • Navigation: RowOverviewPanel gains optional breadcrumbPath/onBreadcrumbClick props and builds the WHERE clause for the linked span using SqlString.format with SqlString.raw for trusted column expressions and escaped string values for span/trace IDs.
  • Z-index stacking: DirectTraceSidePanel is updated to seed ZIndexContext so panels opened from within a trace drawer stack above it, matching the existing DBRowSidePanel pattern.

Confidence Score: 4/5

Display-only addition with no schema migration and clean isolation via optional props; safe to merge.

The change is well-scoped: auto-detection is guarded, the SQL WHERE clause for linked span lookup uses proper escaping, and z-index stacking follows the established pattern. The two findings are cosmetic — a redundant null-guard and a possible 'Span Links' header with an empty-state body when every link object fails the type filter. Neither affects correctness in any realistic data scenario.

packages/app/src/components/DBRowOverviewPanel.tsx — the hasSpanLinks check and accordion defaultValue wiring are worth a second look.

Important Files Changed

Filename Overview
packages/app/src/components/SpanLinksSubpanel.tsx New component rendering OTel span links as compact rows with 'Open trace' action, attribute chips, and a 'show more' toggle. Has a redundant null-guard (!links) since the memoised value is always an array.
packages/app/src/components/DBRowOverviewPanel.tsx Integrates SpanLinksSubpanel into the span-detail accordion; manages the openedLink state for nested-panel navigation. Minor UX inconsistency when all links fail the type-filter; 'spanLinks' unconditionally present in defaultValue.
packages/app/src/components/Search/DirectTraceSidePanel.tsx Wraps content in ZIndexContext.Provider and seeds the drawer's z-index so panels opened from inside the trace view stack correctly above the drawer. Mirrors the pattern in DBRowSidePanel correctly.
packages/app/src/components/DBRowDataPanel.tsx Guards the new SPAN_LINKS select behind source.kind === Trace && spanLinksValueExpression, consistent with the existing spanEventsValueExpression pattern.
packages/app/src/source.ts Auto-detects the Links.TraceId column and sets spanLinksValueExpression: 'Links', mirroring the spanEventsValueExpression: 'Events' detection. Clean and minimal.
packages/app/src/components/DBRowSidePanel.tsx Passes breadcrumbPath and onBreadcrumbClick down to RowOverviewPanel so span-link navigation can propagate breadcrumb state correctly.
packages/common-utils/src/types.ts Adds optional spanLinksValueExpression to TraceSourceSchema — correctly optional, no breaking changes.
packages/api/src/models/source.ts Adds spanLinksValueExpression String field to the Mongoose TraceSource discriminator, consistent with spanEventsValueExpression.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant ROP as RowOverviewPanel
    participant SLS as SpanLinksSubpanel
    participant DRSP as DBRowSidePanel (nested)
    participant CH as ClickHouse

    U->>ROP: Opens span detail (Overview tab)
    ROP->>CH: SELECT ... Links AS __hdx_span_links
    CH-->>ROP: Row data incl. span links array
    ROP->>SLS: "spanLinks={firstRow.__hdx_span_links}"
    SLS->>SLS: Filter / type-check links
    SLS-->>U: Render link rows (Open trace btn, chips)

    U->>SLS: Click Open trace
    SLS->>ROP: onOpenTrace(link)
    ROP->>ROP: setOpenedLink(link) + compute openedLinkWhere via SqlString
    ROP->>DRSP: "Render nested Drawer (rowId=openedLinkWhere, breadcrumbPath+1)"
    DRSP->>CH: "SELECT ... WHERE SpanId=? AND TraceId=?"
    CH-->>DRSP: Linked span row data
    DRSP-->>U: Render linked span detail

    U->>DRSP: Click breadcrumb / close
    DRSP->>ROP: "onClose => setOpenedLink(null)"
    ROP-->>U: Nested drawer dismissed
Loading

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

Reviews (1): Last reviewed commit: "feat(traces): surface span links in the ..." | 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 on lines 252 to 258
defaultValue={[
'exception',
'spanEvents',
'spanLinks',
'network',
'resourceAttributes',
'eventAttributes',

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 'spanLinks' unconditionally in accordion defaultValue

'spanLinks' is always included in the initial defaultValue array regardless of whether the span has any links. Mantine silently ignores values that don't match a rendered item, so this causes no visual bug, but it is inconsistent with the other entries ('exception', 'spanEvents') which are also always present despite their items being conditionally rendered. No change needed unless the team starts using this array for logic — just flagging for awareness.

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

@github-actions

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 200 passed • 3 skipped • 1336s

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

Tests ran across 4 shards in parallel.

View full report →

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