Skip to content

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

Closed
alex-fedotyev wants to merge 1 commit into
mainfrom
alex/hdx-3191-span-links-trace-viewer
Closed

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

Conversation

@alex-fedotyev

Copy link
Copy Markdown
Contributor

This is a draft to gauge how much work it takes to show OpenTelemetry span links in the trace viewer, mirroring the "References N" accordion in the Grafana ClickHouse datasource.

Short answer: not much. Span links ride on the same plumbing that already powers span events. The OTel Links Nested column (Links.TraceId, Links.SpanId, Links.TraceState, Links.Attributes) is the structural twin of the Events column we already wire through spanEventsValueExpression, so the new field flows through schema, model, external API, source form and auto-detection the same way. The only genuinely new code is one render component and one accordion item.

Part of #1593 (HDX-3191).

Summary

Adds an optional spanLinksValueExpression field to trace sources and a new Span Links section in the span detail Overview panel. The section renders a span's outgoing links as a read-only table: linked trace ID, span ID, trace state, and attributes. The field auto-detects from the Links.TraceId column and defaults to Links, so existing OTel span sources pick it up with no manual config.

What's in this PR

  • spanLinksValueExpression on the trace source schema (common-utils), the Mongoose trace model, and the external API source block (openapi.json regenerated).
  • Auto-detection in source.ts: sets spanLinksValueExpression: 'Links' when a Links.TraceId column is present, alongside the existing span-events detection.
  • A "Span Links Expression" row in the source form and the default in the onboarding flow.
  • A __hdx_span_links select in DBRowDataPanel and a new SpanLinksSubpanel that renders the links table.
  • A new Span Links accordion item in DBRowOverviewPanel, shown only when the selected span actually has links.
  • Unit tests for SpanLinksSubpanel (row shaping, columns, empty state, malformed-row filtering) and a Storybook story.

One intentional difference from the sibling fields

The new spanLinksValueExpression OpenAPI block omits nullable: true. The field is .optional() (a string or absent, never null), and the read serializer drops undefined optionals through SourceSchema.safeParse, so the response never carries a null here. The older sibling expression blocks still carry nullable: true; I left those alone to keep this change focused rather than re-touch every block. Happy to make the siblings consistent in a separate change if we want that.

Test plan

  • make ci-lint (lint + tsc + OpenAPI spectral) green.
  • make ci-unit green; the new SpanLinksSubpanel tests pass (7 cases).
  • knip clean.
  • Rendered the new component in Storybook and captured it with Playwright in both light and dark themes, plus the empty state (no links) and exercised the ErrorBoundary error fallback path. The table sits in the narrow span-detail side panel and reuses the same Table primitive as Span Events, which handles horizontal overflow at panel width.

Light and dark screenshots are below.

Tier

This lands as Tier 4 because it touches packages/api/src/routers/external-api/v2/sources.ts (any external-API touch is Tier 4 in the classifier) and spans three layers. For a draft that is fine. For the real merge path I would split it:

  • PR1: schema, model, external API, source form, onboarding, auto-detect (the plumbing).
  • PR2: the DBRowDataPanel select, SpanLinksSubpanel, the accordion item, tests and the story (the UI).

What's NOT in this PR (follow-ups)

  • Click-to-navigate from a link to the linked trace. Validation against the Grafana datasource showed navigation is the fiddly part even where links already render, so it deserves its own change.
  • Waterfall graph topology for linked spans that currently render as orphan roots (the broader framing in Display linked spans in the trace view #1593).
  • Customer docs and e2e coverage.

@changeset-bot

changeset-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2ea7017

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 11, 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 3:45am
hyperdx-storybook Ready Ready Preview, Comment Jun 13, 2026 3:45am

Request Review

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds OpenTelemetry span link surfacing to the trace viewer, introducing an optional spanLinksValueExpression field that flows through schema, model, external API, source form, and auto-detection — and a new SpanLinksSubpanel component that renders the linked trace/span IDs, trace state, and attributes in the span detail panel.

  • Schema/model/API plumbing (common-utils/types.ts, models/source.ts, sources.ts, openapi.json): adds spanLinksValueExpression as an optional string following the exact same pattern as spanEventsValueExpression; the omission of nullable: true in the OpenAPI block is intentional and explained in the PR description.
  • Auto-detection (source.ts): detects Links.TraceId and sets spanLinksValueExpression: 'Links', mirroring the existing Events.Timestamp detection for span events.
  • UI (DBRowDataPanel, DBRowOverviewPanel, SpanLinksSubpanel): adds a __hdx_span_links alias select, a hasSpanLinks memo, a conditionally rendered accordion item, and a new table-based subpanel with a type guard filter and show-more toggle — all closely modeled on the sibling SpanEventsSubpanel.

Confidence Score: 5/5

Safe to merge — the change is purely additive, all new fields are optional, and the UI is guarded behind a non-empty check on the raw data before rendering.

Every new field is optional and backward-compatible; the SpanLinksSubpanel closely mirrors the proven SpanEventsSubpanel pattern; auto-detection is opt-in and column-presence-gated; the external API serializer passes the field through automatically via the existing SourceSchema.safeParse spread. No breaking changes, no data-loss paths, and the seven unit tests cover the key edge cases.

No files require special attention.

Important Files Changed

Filename Overview
packages/app/src/components/SpanLinksSubpanel.tsx New component rendering span links as a table with TraceId, SpanId, TraceState, and Attributes columns; closely follows the SpanEventsSubpanel pattern with a type guard filter and a show-more toggle.
packages/app/src/components/DBRowOverviewPanel.tsx Adds hasSpanLinks memo, imports SpanLinksSubpanel, and conditionally renders a Span Links accordion item; mirrors the existing hasSpanEvents/SpanEventsSubpanel pattern exactly.
packages/app/src/components/DBRowDataPanel.tsx Adds SPAN_LINKS = '__hdx_span_links' alias and conditionally appends a spanLinksValueExpression select for trace sources, consistent with SPAN_EVENTS handling.
packages/app/src/source.ts Auto-detection checks for Links.TraceId column presence and conditionally sets spanLinksValueExpression: 'Links', mirroring the existing hasSpanEvents pattern.
packages/common-utils/src/types.ts Adds optional spanLinksValueExpression: z.string().optional() to TraceSourceSchema, consistent with sibling expression fields.
packages/api/src/routers/external-api/v2/sources.ts Adds spanLinksValueExpression to the OpenAPI JSDoc block; intentionally omits nullable: true per PR description. Field flows through formatExternalSource via spread.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ClickHouse Links Nested column] -->|Links.TraceId detected| B[source.ts auto-detect]
    B --> C[TraceSourceSchema]
    C --> D[Mongoose TraceSource model]
    C --> E[External API]
    C --> F[SourceForm]
    G[useRowData] -->|spanLinksValueExpression present| H[SELECT AS __hdx_span_links]
    H --> I[firstRow.__hdx_span_links]
    I --> J{hasSpanLinks?}
    J -->|yes| K[Accordion.Item: Span Links]
    K --> L[SpanLinksSubpanel]
    L --> M[type guard filter]
    M --> N[Table: TraceId, SpanId, TraceState, Attributes]
    J -->|no| O[section hidden]
Loading

Reviews (3): 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 links is always an array (never null or undefined) because useMemo initializes it to [] in every branch. The !links arm is unreachable dead code — the sibling SpanEventsSubpanel has the same pattern, so this may be copy-paste noise worth cleaning up here.

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

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 +78 to +84
return spanLinks.filter((link): link is SpanLinkData => {
return (
typeof link.TraceId === 'string' &&
typeof link.SpanId === 'string' &&
link.Attributes !== undefined
);
});

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 The type guard validates TraceId and SpanId as strings but doesn't check TraceState. SpanLinkData declares TraceState: string, so a row where TraceState is missing or non-string would pass the cast but violate the interface contract. The cell render handles it gracefully via a truthiness check, but the type safety is incomplete relative to the other two fields.

Suggested change
return spanLinks.filter((link): link is SpanLinkData => {
return (
typeof link.TraceId === 'string' &&
typeof link.SpanId === 'string' &&
link.Attributes !== undefined
);
});
return spanLinks.filter((link): link is SpanLinkData => {
return (
typeof link.TraceId === 'string' &&
typeof link.SpanId === 'string' &&
typeof link.TraceState === 'string' &&
link.Attributes !== undefined
);
});

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

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

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

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

Tests ran across 4 shards in parallel.

View full report →

@alex-fedotyev alex-fedotyev marked this pull request as ready for review June 13, 2026 03:16
@alex-fedotyev alex-fedotyev self-assigned this Jun 13, 2026
@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Jun 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD.

Why this tier:

  • Critical-path files (1):
    • packages/api/src/routers/external-api/v2/sources.ts
  • Cross-layer change: touches frontend (packages/app) + backend (packages/api) + shared utils (packages/common-utils)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

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

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

@alex-fedotyev alex-fedotyev marked this pull request as draft June 13, 2026 03:24
Add an optional `spanLinksValueExpression` field to trace sources,
mirroring `spanEventsValueExpression`, and render a new "Span Links"
section in the span detail Overview panel.

The OTel `Links` Nested column (`Links.TraceId`, `Links.SpanId`,
`Links.TraceState`, `Links.Attributes`) is the structural twin of the
`Events` column already wired through `spanEventsValueExpression`, so
the field flows through schema, model, external API, source form and
auto-detection the same way. `SpanLinksSubpanel` renders the links as a
read-only table (linked trace ID, span ID, trace state, attributes).

The field auto-detects from the `Links.TraceId` column and defaults to
`Links`. The external API GET /sources response gains the optional field
automatically via SourceSchema.safeParse.

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

Copy link
Copy Markdown
Contributor Author

Replaced by #2463, which ships the same feature on a clean branch. This draft's history got tangled during a rebase, and #2463 also trims the scope down to what a display-only field needs (no source-form field, no external API surface). Closing in favor of that one.

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

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant