You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/07/07 08:19:25 UTC

(skywalking-horizon-ui) branch fix/zipkin-inline-detail-embedded-spans created (now 7668873)

This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a change to branch fix/zipkin-inline-detail-embedded-spans
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git


      at 7668873  fix(traces): Zipkin trace list carries its spans so the inline waterfall skips a redundant re-query

This branch includes the following new commits:

     new 7668873  fix(traces): Zipkin trace list carries its spans so the inline waterfall skips a redundant re-query

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(skywalking-horizon-ui) 01/01: fix(traces): Zipkin trace list carries its spans so the inline waterfall skips a redundant re-query

Posted by wu...@apache.org.
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 <wu...@foxmail.com>
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 {