This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch fix/zipkin-inline-detail-embedded-spans in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 766887330b348044e6cff106dbe6fe1912178b4a Author: Wu Sheng <[email protected]> AuthorDate: Tue Jul 7 16:19:18 2026 +0800 fix(traces): Zipkin trace list carries its spans so the inline waterfall skips a redundant re-query Zipkin's `/api/v2/traces` already returns every span per trace, but the BFF summarised the list rows and discarded the spans, so clicking a trace re-fetched `/api/v2/trace/{id}` on the client to draw the waterfall — a redundant second round-trip to OAP for data it already had (native BanyanDB traces ship spans inline on the row and render with no re-fetch; this brings Zipkin in line). - ZipkinTraceListRow gains an optional `spans?: ZipkinSpan[]` (mirrors NativeTraceListRow.spans?). - The /api/zipkin/traces route keeps the spans it already fetched instead of dropping them. - LayerZipkinTracesView renders the inline waterfall from the row's spans, falling back to the by-id fetch only when a selected id isn't in the current list. - The /api/zipkin/trace/:id route + popout stay for opening a trace by pasted id / a shared deep link / a log->trace jump (no list row → the by-id query is right there). - The AI-assistant Zipkin list path is unaffected (spans optional; it omits them). --- CHANGELOG.md | 1 + apps/bff/src/http/query/zipkin.ts | 21 ++++++++++----------- apps/ui/src/layer/traces/LayerZipkinTracesView.vue | 20 ++++++++++++++++---- packages/api-client/src/trace.ts | 5 +++++ 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a60862..e9695de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The version line is shared by every package in the monorepo (apps + shared packa ### Traces & logs - **Custom time ranges on the Traces and Logs tabs now return results when your browser and the OAP server sit in different timezones.** A custom range (and the metric→trace drill's centered window) was sent as a browser-local wall-clock string that the server re-read in its own timezone — so on a UTC-container deployment the window shifted by your UTC offset and came back empty, while the rolling presets kept working. Both paths now send absolute timestamps and the server applies the OA [...] +- **Opening a Zipkin trace's spans is now instant — no second query to OAP.** On a Zipkin-tracing layer (mesh / Kubernetes), clicking a trace in the results list rendered its span waterfall by re-fetching the trace by id — even though Zipkin's list response already contained every span. The list now carries those spans through, so the waterfall opens immediately from what's already on screen, one round-trip lighter. Opening a trace by a pasted id or a shared deep link still fetches it directly. ### Deployment & configuration diff --git a/apps/bff/src/http/query/zipkin.ts b/apps/bff/src/http/query/zipkin.ts index edee784..2e6a8a3 100644 --- a/apps/bff/src/http/query/zipkin.ts +++ b/apps/bff/src/http/query/zipkin.ts @@ -58,10 +58,9 @@ export interface ZipkinRouteDeps { fetch?: FetchLike; } -/** Derive a one-row summary (`ZipkinTraceListRow`) from a trace's - * full span array. Zipkin's REST list endpoint returns the full span - * set per trace — the SPA's list view doesn't need every span, just - * the root + counts. */ +/** Derive the summary fields (`ZipkinTraceListRow` minus `spans`) from a + * trace's full span array — the root entry + counts. The caller attaches + * the spans themselves so the inline detail can render from the list. */ function summariseTrace(spans: ZipkinSpan[]): ZipkinTraceListRow { // Root = span with no parent. Falls back to the earliest span when // every span has a parentId (broken trace). @@ -187,16 +186,16 @@ export function registerZipkinRoutes(app: FastifyInstance, deps: ZipkinRouteDeps limit, }, ); - // Zipkin's `/traces` returns `Array<Array<Span>>` — one inner - // array per trace. Compress each into a `ZipkinTraceListRow` - // (root summary + counts) so the SPA's list view doesn't have - // to ship the full span tree just to render rows. The detail - // route serves the full spans for the popout. + // Zipkin's `/traces` returns `Array<Array<Span>>` — one inner array + // per trace, spans included. Summarise the root for the list row AND + // carry the spans through, so the inline detail renders without a + // redundant `/trace/{id}` re-query. (The `/trace/{id}` route stays for + // the paste-an-id / deep-link popout, which has no list row.) const raw = Array.isArray(body) ? (body as ZipkinSpan[][]) : []; - const summaries: ZipkinTraceListRow[] = raw.map(summariseTrace); + const rows: ZipkinTraceListRow[] = raw.map((spans) => ({ ...summariseTrace(spans), spans })); const response: ZipkinTraceListResponse = { source: 'zipkin', - traces: summaries, + traces: rows, reachable: true, }; return reply.code(status).send(response); diff --git a/apps/ui/src/layer/traces/LayerZipkinTracesView.vue b/apps/ui/src/layer/traces/LayerZipkinTracesView.vue index da11d53..fa1157e 100644 --- a/apps/ui/src/layer/traces/LayerZipkinTracesView.vue +++ b/apps/ui/src/layer/traces/LayerZipkinTracesView.vue @@ -25,7 +25,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import type { NativeTraceListRow, ZipkinTraceListRow } from '@skywalking-horizon-ui/api-client'; +import type { NativeTraceListRow, ZipkinSpan, ZipkinTraceListRow } from '@skywalking-horizon-ui/api-client'; const { t } = useI18n({ useScope: 'global' }); import { useLayerZipkinTraces, useZipkinTrace } from '@/layer/traces/useZipkinTraces'; @@ -235,9 +235,21 @@ watch([cService, cLookback, cLimit, cSpan, cRemote, cAnno], () => { selectedTraceId.value = null; }); -// Full Zipkin span set for the selected trace — rendered by the shared -// ZipkinTraceDetailCard (waterfall + KPIs + span modal). -const { spans: selectedSpans, isLoading: selectedLoading } = useZipkinTrace(selectedTraceId); +// The selected trace's spans already ride on the list row (Zipkin's /traces +// ships the full span tree), so the inline detail renders from it with no +// extra query. Fall back to the by-id fetch only when a selected id isn't in +// the current list — defensive; the row-click path always has it. +const selectedRowSpans = computed<ZipkinSpan[] | null>(() => { + const id = selectedTraceId.value; + if (!id) return null; + return traces.value.find((r) => r.traceId === id)?.spans ?? null; +}); +const fallbackTraceId = computed<string | null>(() => + selectedTraceId.value && !selectedRowSpans.value ? selectedTraceId.value : null, +); +const { spans: fetchedSpans, isLoading: fetchLoading } = useZipkinTrace(fallbackTraceId); +const selectedSpans = computed<ZipkinSpan[]>(() => selectedRowSpans.value ?? fetchedSpans.value); +const selectedLoading = computed<boolean>(() => Boolean(fallbackTraceId.value) && fetchLoading.value); // Esc cascade: the detail card owns the span modal — close it first // (via the card's `closeSpanModal`), then the inline detail. diff --git a/packages/api-client/src/trace.ts b/packages/api-client/src/trace.ts index 4bd8947..e4ffb3c 100644 --- a/packages/api-client/src/trace.ts +++ b/packages/api-client/src/trace.ts @@ -184,6 +184,11 @@ export interface ZipkinTraceListRow { duration: number | null; spanCount: number; errorCount: number; + /** The trace's full span tree. Zipkin's `/traces` already ships every + * span per trace, so the list route carries them through and the inline + * detail renders without a second `/trace/{id}` round-trip (mirrors + * native `queryTraces`). Optional — the AI-assistant list path omits it. */ + spans?: ZipkinSpan[]; } export interface ZipkinTraceListResponse {
