This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
The following commit(s) were added to refs/heads/main by this push:
new 8126ac3 fix(traces): Zipkin trace list carries its spans so the
inline waterfall skips a redundant re-query (#97)
8126ac3 is described below
commit 8126ac3de1e4600be50f1b4ee1c9637d2cd461f0
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Tue Jul 7 16:36:02 2026 +0800
fix(traces): Zipkin trace list carries its spans so the inline waterfall
skips a redundant re-query (#97)
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 {