feat(traces): surface span links in the trace viewer#2440
Conversation
🦋 Changeset detectedLatest commit: 2ea7017 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.
|
Greptile SummaryThis PR adds OpenTelemetry span link surfacing to the trace viewer, introducing an optional
Confidence Score: 5/5Safe 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
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]
Reviews (3): Last reviewed commit: "feat(traces): surface span links in the ..." | Re-trigger Greptile |
| maxRows: 5, | ||
| }); | ||
|
|
||
| if (!links || links.length === 0) { |
There was a problem hiding this comment.
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.
| 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!
| return spanLinks.filter((link): link is SpanLinkData => { | ||
| return ( | ||
| typeof link.TraceId === 'string' && | ||
| typeof link.SpanId === 'string' && | ||
| link.Attributes !== undefined | ||
| ); | ||
| }); |
There was a problem hiding this comment.
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.
| 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 | |
| ); | |
| }); |
E2E Test Results✅ All tests passed • 200 passed • 3 skipped • 1320s
Tests ran across 4 shards in parallel. |
🔴 Tier 4 — CriticalTouches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD. Why this tier:
Review process: Deep review from a domain expert. Synchronous walkthrough may be required. Stats
|
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>
a6a2984 to
2ea7017
Compare
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
LinksNested column (Links.TraceId,Links.SpanId,Links.TraceState,Links.Attributes) is the structural twin of theEventscolumn we already wire throughspanEventsValueExpression, 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
spanLinksValueExpressionfield 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 theLinks.TraceIdcolumn and defaults toLinks, so existing OTel span sources pick it up with no manual config.What's in this PR
spanLinksValueExpressionon the trace source schema (common-utils), the Mongoose trace model, and the external API source block (openapi.jsonregenerated).source.ts: setsspanLinksValueExpression: 'Links'when aLinks.TraceIdcolumn is present, alongside the existing span-events detection.__hdx_span_linksselect inDBRowDataPaneland a newSpanLinksSubpanelthat renders the links table.DBRowOverviewPanel, shown only when the selected span actually has links.SpanLinksSubpanel(row shaping, columns, empty state, malformed-row filtering) and a Storybook story.One intentional difference from the sibling fields
The new
spanLinksValueExpressionOpenAPI block omitsnullable: true. The field is.optional()(a string or absent, never null), and the read serializer drops undefined optionals throughSourceSchema.safeParse, so the response never carries a null here. The older sibling expression blocks still carrynullable: 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-unitgreen; the newSpanLinksSubpaneltests pass (7 cases).knipclean.ErrorBoundaryerror fallback path. The table sits in the narrow span-detail side panel and reuses the sameTableprimitive 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:DBRowDataPanelselect,SpanLinksSubpanel, the accordion item, tests and the story (the UI).What's NOT in this PR (follow-ups)