Skip to content

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

Open
alex-fedotyev wants to merge 5 commits into
mainfrom
alex/hdx-3191-span-links
Open

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

Conversation

@alex-fedotyev

@alex-fedotyev alex-fedotyev commented Jun 13, 2026

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" jumps to the linked trace in place, through the same cross-source navigation #2541 introduced: the trace opens in the same panel, the breadcrumb trail picks up the hop, and back and the URL both reflect it. A span link is really the same kind of jump as the existing "View Trace" action (log to trace), just keyed off the link's trace and span ids instead of the current row's, so it reuses that mechanism directly instead of adding a second one.

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.

Rebased onto #2541

This branch tried two navigation approaches while #2541 (single-drawer navigation) was still in review: a nested drawer, then a separate in-place flyout prototype for feedback. Now that #2541 shipped a shared mechanism for exactly this, jumping across sources in place with one breadcrumb trail, I rebased onto it and dropped both. "Open trace" is just another caller of that mechanism now, the same as "View Trace": it pushes the linked trace onto the shared stack instead of opening a second drawer.

Test plan

  • make ci-lint and make ci-unit pass: full app suite, common-utils.
  • 9 unit tests on the compact rows themselves: empty and malformed input, attribute chips, the trace-state chip, single-line collapse, and the "Open trace" callback.
  • 2 new unit tests on the navigation wiring after the rebase: clicking "Open trace" pushes the right trace source, row, and label onto the shared stack and switches to the Trace tab; it's a no-op when the current row has no resolvable trace source.
  • Storybook story (Default / SingleLink / Empty).
  • Section render-gating (present on a span with links, absent without) and light/dark theming were verified live before this rebase and are unchanged by it.
  • Live click-through of the rewired "Open trace": walked the multi-hop chain (trace to trace to trace) on a local stack; each hop re-roots in place and the breadcrumb now names the landed span. Covered by a new breadcrumb-label test that drives the real panel tree through a hop.

Implements #1593

@changeset-bot

changeset-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fb97673

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 Jul 20, 2026 3:47pm
hyperdx-storybook Ready Ready Preview, Comment Jul 20, 2026 3:47pm

Request Review

@github-actions

github-actions Bot commented Jun 13, 2026

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: 391 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: 8
  • Production lines changed: 391 (+ 621 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
Contributor

Greptile Summary

This PR surfaces OpenTelemetry span links in the trace viewer. The main changes are:

  • Adds optional span-link source configuration and row selection.
  • Renders a Span Links section with compact rows and link attributes.
  • Opens linked traces through the existing side-panel navigation stack.
  • Adds tests and stories for span-link rendering and navigation.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
packages/app/src/components/SpanLinksSubpanel.tsx Adds span-link validation, compact row rendering, show-more behavior, and the Open trace action.
packages/app/src/components/DBRowOverviewPanel.tsx Adds the Span Links accordion and gates it on validated span-link rows.
packages/app/src/components/DBRowSidePanel.tsx Wires span-link navigation into the existing source stack and updates breadcrumb labels after the linked row loads.
packages/app/src/components/DBRowDataPanel.tsx Selects the span-link alias only for trace sources with a configured span-link expression.
packages/app/src/source.ts Infers the span-link expression from the standard OTel Links column.
packages/api/src/models/source.ts Persists the optional span-link expression on trace sources.
packages/common-utils/src/types.ts Adds the optional span-link expression to the shared trace source schema.

Reviews (5): Last reviewed commit: "fix(traces): keep span-link row keys uni..." | Re-trigger Greptile

Comment thread packages/app/src/components/SpanLinksSubpanel.tsx Outdated
Comment thread packages/app/src/components/DBRowOverviewPanel.tsx
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 • 240 passed • 1 skipped • 1060s

Status Count
✅ Passed 240
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 1

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
@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.

@alex-fedotyev

Copy link
Copy Markdown
Contributor Author

Heads up on direction. #2541 reworks the side panel into a single drawer with a shared cross-source navigation stack (its new "View Trace" action pushes onto it) plus a shared breadcrumb component. That replaces the stacked/nested drawer pattern this PR's "Open trace" currently relies on.

Rather than ship navigation that #2541 then has to unwind, I'm going to rebuild "Open trace" on that shared stack: following a span link becomes a push of the linked span's trace frame, the same way "View Trace" works for logs. That reuses the breadcrumb trail, URL persistence, and back/jump behavior for free, and lets me drop the in-place trace-nav code in DBTracePanel and the nested-drawer path entirely. The Span Links display section and the schema auto-detection stay as they are.

Sequencing: let #2541 land first, then rebase this onto it and swap in the push call. I'll hold the navigation rewire until then. The display and schema parts are reviewable on their own in the meantime.

alex-fedotyev and others added 2 commits July 8, 2026 12:30
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>
Rebase onto #2541 (single-drawer navigation) and replace the nested-drawer
"Open trace" path with a push onto the same cross-source stack "View Trace"
uses. Deletes the DBTracePanel in-place-nav prototype, the setOpenedLink
nested panel, the breadcrumbPath/onBreadcrumbClick threading, and the
DirectTraceSidePanel z-index seed that only existed to support the nested
drawer.

Adds onOpenLinkedTrace to RowSidePanelContext, computed in
DBRowSidePanelInner from the same traceIdExpression/spanIdExpression used
for "View Trace", and consumed by RowOverviewPanel -> SpanLinksSubpanel.
Also fixes a restricted-import lint violation in SpanLinksSubpanel.test.tsx
(parent-relative import instead of the @/ alias) hit while touching the
file.

Co-Authored-By: Claude <noreply@anthropic.com>
alex-fedotyev and others added 2 commits July 19, 2026 17:10
Span-link "Open trace" hops now label their breadcrumb with the landed
span name once the destination row loads, instead of a "Trace <id>"
fallback. The name is resolved into a small in-memory store keyed by the
hop's row id; the shared side-panel navigation hook is untouched and the
"View Trace" hop keeps its existing label.

Address review feedback on the span-links section:
- Gate the "Span Links" section on at least one valid link, so a
  non-empty but all-malformed Links array no longer opens an empty
  section. The validity check is now one exported getValidSpanLinks
  helper reused by both the subpanel and the overview gate.
- Remove a dead null-guard in SpanLinksSubpanel (the memoized list is
  always an array).

Tests: getValidSpanLinks unit cases, a breadcrumb-label integration
test that drives the real panel tree through an Open-trace hop, and the
existing span-link suites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alex-fedotyev

Copy link
Copy Markdown
Contributor Author

Pushed 086ead9 on top of the rebase onto main.

Breadcrumb labels: a span-link "Open trace" hop now shows the landed span name (for example consume order.created) once the destination row loads, instead of the Trace <id> placeholder. The name resolves into a small in-memory map keyed by the hop's row id, so the shared single-drawer navigation and the existing "View Trace" hop are left as they were.

Both review threads are addressed in the same commit: the dead null-guard is gone, and the "Span Links" section now gates on at least one valid link through a shared getValidSpanLinks helper, so a non-empty but all-malformed Links array no longer opens an empty section.

Validation: the eslint warning gate, tsc, and the app unit suite (2387 tests) pass locally; added a getValidSpanLinks unit test and a breadcrumb-label test that drives the real panel tree through a hop. Out of draft now.

@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

No P0/P1 issues. The core navigation and SQL wiring are sound: handleOpenLinkedTrace passes the untrusted link.TraceId/link.SpanId through SqlString.format ? placeholders (escaped), with only the trusted source-config expressions as SqlString.raw, and all span-link fields render through React's auto-escaping. The two P2 items below are a robustness gap and a public-API documentation drift.

🟡 P2 -- recommended

  • packages/app/src/components/SpanLinksSubpanel.tsx:34 -- getValidSpanLinks calls typeof link.TraceId without first confirming each array element is a non-null object, so a null/undefined element throws a TypeError inside the render-time useMemo; because hasSpanLinks in DBRowOverviewPanel.tsx calls the same function, the throw escapes the subpanel's own ErrorBoundary and blanks the entire Overview tab, contradicting the intended graceful degradation for malformed input.
    • Fix: Add link != null && typeof link === 'object' to the filter predicate before the property accesses, and cover it with a test passing [null, LINK].
    • correctness, testing
  • packages/api/src/routers/external-api/v2/sources.ts:669 -- the new spanLinksValueExpression is added to the shared TraceSourceSchema, which the public v2 Sources API reuses via SourceSchema, so the field is accepted and returned by the external API yet has no entry in openapi.json (unlike the sibling spanEventsValueExpression), leaving the documented contract out of sync with the served one.
    • Fix: Add a spanLinksValueExpression JSDoc property mirroring spanEventsValueExpression and regenerate openapi.json.
    • api-contract
🔵 P3 nitpicks (3)
  • packages/app/src/components/SpanLinksSubpanel.tsx:34 -- the type guard narrows to SpanLinkData (which declares TraceState: string) without validating TraceState, so the produced type overstates what was checked; render code compensates with its own typeof check.
    • Fix: Either check typeof link.TraceState === 'string' in the guard or loosen SpanLinkData.TraceState to string | undefined.
  • packages/app/src/components/DBRowSidePanel.tsx:412 -- the span-link breadcrumb-label machinery (spanLinkFrameRowIdsRef, resolvedFrameLabels, the mark-on-push, the resolve effect, and the breadcrumb lookup) is threaded across four spots inside an already large component.
    • Fix: Extract it into a dedicated useSpanLinkBreadcrumbLabels(activeRowId, mainContent) hook.
  • packages/api/src/mcp/tools/trace/waterfall.ts:1 -- the trace-viewer gains a user-facing "Open trace" span-link capability, but the MCP clickstack_trace_waterfall tool omits span links, so an agent inspecting a trace cannot see or follow links a user now can (out of scope for this display-only PR; noted as a fast-follow).
    • Fix: Include linked trace/span ids in the waterfall tool's span rows in a follow-up.

Reviewers (7): correctness, testing, security, maintainability, api-contract, agent-native, learnings-researcher.

Testing gaps:

  • No test for a null/non-object element inside the Links array (only malformed-object entries are covered).
  • Show-more/hide toggle (maxRows=5, >5 links) in SpanLinksSubpanel is never exercised.
  • Non-string attribute value coercion via String(value) is untested (e.g. number/object rendering to [object Object]).
  • handleOpenLinkedTrace guard covers only the missing-traceSourceData branch, not the missing traceIdExpression/spanIdExpression disjuncts.
  • No test asserts SQL-injection payloads in TraceId/SpanId are escaped in the built WHERE clause, nor that XSS payloads in attributes/TraceState render as inert text.
  • inferTableSourceConfig span-links auto-detection is untested (shared pre-existing gap with spanEvents).

Coverage note: 4 selected reviewers (adversarial, kieran-typescript, julik-frontend-races, project-standards) did not return before synthesis; findings above reflect the 7 completed reviewers.

Address Greptile review on #2463.

A span can carry two links to the same destination span (same TraceId
and SpanId, different attributes). The row key was TraceId-SpanId, which
collides in that case, so React warns and can drop a row. Add the list
index to the key; the span-link list is append-only and never reordered,
so the index stays stable across renders.

Adds a regression test that renders two links to the same destination
span and asserts both rows render with no duplicate-key warning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alex-fedotyev

Copy link
Copy Markdown
Contributor Author

Pushed fb97673 for the remaining review note.

Span-link rows now key on TraceId-SpanId-index instead of TraceId-SpanId. A span can carry two links to the same destination span (same trace and span id, different attributes), and the old key collided in that case, so React warned and could drop a row. The list is append-only and never reordered, so the index is a stable disambiguator. Added a regression test that renders two links to the same destination and asserts both rows render with no duplicate-key warning.

The defaultValue note is answered inline: 'spanLinks' stays in the array to match the sibling 'exception' / 'spanEvents' entries.

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