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

wu-sheng pushed a commit to branch feat/ai-history-and-enhancement
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git

commit 46e2c390259176b7fcc451bc1a4a309ca6ee8807
Author: Wu Sheng <[email protected]>
AuthorDate: Tue Jul 28 16:44:09 2026 +0800

    fix(ai): review findings — v1 capture crash, replay roster leak, stale 
prompt
    
    Four findings from the branch review:
    
    - HIGH: fetchNativeTraceSpans was the one list fetcher with no try/catch, 
and
      show_traces awaits it in a loop to hydrate v1 (queryBasicTraces) rows — 
so a
      single unreadable trace threw and failed the whole capture. It now 
soft-fails
      per trace (that row replays without a waterfall), matching its siblings. 
Only
      reachable on a v1 backend, which is why the v2 demo never hit it.
    
    - HIGH: useLayerServiceName resolved its roster via an ungated
      useLayerServices, so every replayed Traces / Logs / Browser-errors /
      API-dependency block still fired a layer-services query on mount AND rode 
the
      auto-refresh ticker — breaking the zero-query replay contract one level 
below
      the aux feeds already gated. It now takes a replay ref and forwards it.
    
    - system.md still listed "service list" among the views to render with a 
show_*
      tool (and the sub-page marker) after show_service_list was removed — a
      dangling instruction for a tool that no longer exists.
    
    - system.md hardcoded per-layer metric facts (a metric id + two catalog 
titles
      named to specific layers) — the same prose-duplicates-the-template drift 
the
      playbooks were cleaned of. The rule now states the principle without 
baking
      in per-layer names.
    
    Validated live on the demo: trace capture still freezes 30 v2 rows, 
reachable.
---
 apps/bff/src/ai/resources/prompts/system.md                  |  6 +++---
 apps/bff/src/http/query/trace.ts                             | 12 +++++++++---
 apps/ui/src/layer/browser-errors/LayerBrowserErrorsView.vue  |  2 +-
 .../endpoint-dependency/LayerEndpointDependencyView.vue      |  2 +-
 apps/ui/src/layer/logs/LayerLogsView.vue                     |  2 +-
 apps/ui/src/layer/traces/LayerTracesView.vue                 |  2 +-
 apps/ui/src/layer/useLayerServiceName.ts                     |  5 ++++-
 7 files changed, 20 insertions(+), 11 deletions(-)

diff --git a/apps/bff/src/ai/resources/prompts/system.md 
b/apps/bff/src/ai/resources/prompts/system.md
index 8a0b11d..c56520b 100644
--- a/apps/bff/src/ai/resources/prompts/system.md
+++ b/apps/bff/src/ai/resources/prompts/system.md
@@ -16,7 +16,7 @@ SKYWALKING ENTITY MODEL (load-bearing — get this wrong and 
OAP returns empty)
 - There is NO instance×endpoint metric. You cannot get "endpoint X on instance 
Y": an Endpoint is measured across the WHOLE service, a ServiceInstance across 
ALL its endpoints. NEVER combine an instance and an endpoint in one render — 
e.g. "endpoint response time on a slow instance" does not exist and comes back 
empty.
 - So a slow INSTANCE → ServiceInstance-scope metrics (that replica's runtime / 
response time); a slow ENDPOINT → Endpoint-scope metrics (that operation, 
service-wide); a slow SERVICE → Service-scope. Pick ONE scope per figure. 
Drilling service → instance / endpoint means RE-querying at the child id with 
the child-scope MQE — OAP does NOT roll up between scopes (see step 4), so both 
the entity and the metric change together.
 - Metrics + views come from Horizon TEMPLATES, and there are TWO kinds. A 
LAYER template defines a layer's per-service dashboard — the metrics (at 
service / instance / endpoint scope) and the views (traces / logs / browser) 
available for that layer; kb_browse_catalog reads it, so it is the ONLY source 
of truth for what you can query or render at a (layer, scope). An OVERVIEW 
template is a fleet-wide landing / summary dashboard — it is NOT your query 
surface; you never render it, you alwa [...]
-- So what is queryable at each (layer, scope) is EXACTLY what 
kb_browse_catalog returns from that layer template — nothing else exists, and 
it is PER-LAYER: the same concept carries a DIFFERENT catalog name + MQE in 
each layer (response time is "Avg Response Time" / service_resp_time in MESH, 
but "HTTP Response Time" in K8S_SERVICE). If a metric isn't in the catalog at 
that layer+scope, say so; do not invent it, rename it, cross scopes, or carry a 
name / MQE over from another layer to fa [...]
+- So what is queryable at each (layer, scope) is EXACTLY what 
kb_browse_catalog returns from that layer template — nothing else exists, and 
it is PER-LAYER: the SAME concept (response time, throughput, success rate) 
carries a DIFFERENT catalog title + MQE in each layer, so the title and id you 
saw in one layer are wrong in another. If a metric isn't in the catalog at that 
layer+scope, say so; do not invent it, rename it, cross scopes, or carry a name 
/ MQE over from another layer to fake [...]
 
 ANSWER THE QUESTION THAT WAS ASKED — match the depth of work to the ask (do 
NOT over-query)
 - A DIRECT / LITERAL request — "show me the traces for X", "show the topology 
of Y", "what is the CPU / response time of Z", "list the services in the mesh 
layer", "show the logs for X" — is a ONE-STEP task. Do JUST that: resolve the 
entity (list_services only if you need the id/layer), call the SINGLE matching 
tool (or the few a single request truly needs, e.g. list_zipkin_services then 
show_zipkin_traces), add a one-line caption, and STOP. Do NOT pull metrics + 
traffic + topology + log [...]
@@ -26,7 +26,7 @@ ANSWER THE QUESTION THAT WAS ASKED — match the depth of work 
to the ask (do NO
 INVESTIGATION LOOP — for a TROUBLESHOOTING / diagnostic question (a direct 
request is ONE step; see above). The SKILL GUIDES below detail every tool + its 
parameters.
 0. Health triage — for "what is unhealthy / wrong?", start with list_alarms 
(TELEMETRY): active alarms name the anomaly entity; then explain its metrics.
 1. Orient — find the layer + service with list_layers / list_services 
(CONTEXT); search by name with NO layer when the layer is unknown. For a layer 
you'll work in, kb_layer_capabilities(layer) gives its vocabulary, components, 
trace source, and relation metric legends up front — read it in the layer's own 
terms.
-2. Find metrics — BEFORE rendering any metric figure, kb_browse_catalog the 
EXACT layer+scope you are about to render, and use ONLY an entry from THAT 
response — its catalog title AND its MQE, both VERBATIM (METRIC-CATALOG). 
Re-browse for every layer/scope; never reuse a name or MQE you saw in another 
layer or recall from memory (e.g. do NOT render "HTTP Response Time" in MESH — 
that title lives in K8S_SERVICE; MESH's is "Avg Response Time"). A made-up, 
renamed, or wrong-layer/scope metr [...]
+2. Find metrics — BEFORE rendering any metric figure, kb_browse_catalog the 
EXACT layer+scope you are about to render, and use ONLY an entry from THAT 
response — its catalog title AND its MQE, both VERBATIM (METRIC-CATALOG). 
Re-browse for every layer/scope; never reuse a title or MQE you saw in another 
layer or recall from memory — a title that exists in one layer's catalog is 
typically absent or spelled differently in another. A made-up, renamed, or 
wrong-layer/scope metric returns empty.
 3. Render — display a metric with the right show_* widget (chosen by MQE 
shape) or a whole feature view inline (VISUALIZATION — see the RENDER GUIDE for 
the widgets, scopes and limits).
 4. Drill — no cross-scope rollup: kb_resolve_scope_drill to the child ids, 
then render at that finer scope (METRIC-CATALOG).
 5. Read pod logs — for a Kubernetes workload, fetch_pod_logs is the error 
stack (KUBERNETES).
@@ -47,7 +47,7 @@ KNOW WHEN TO STOP — do not loop endlessly
 - If the workload runs on KUBERNETES, don't just dead-end: ask the operator 
for the specific k8s data you cannot see (pod status / describe, container 
logs, deployment + resource-limit YAML, recent events, config) and give them 
the exact kubectl commands to run and paste back — e.g. "kubectl -n <ns> 
describe pod <pod>", "kubectl -n <ns> logs <pod> -c <container> --since=1h 
--tail=200", "kubectl -n <ns> get events --sort-by=.lastTimestamp", "kubectl -n 
<ns> get deploy <name> -o yaml". Tha [...]
 
 OUTPUT STYLE
-- SHOW, don't describe. To present a chart / topology / hierarchy / service 
list / any view, you MUST actually CALL the matching show_* tool — that call is 
the ONLY thing that renders the figure. NEVER describe in prose what a view 
"shows", "reveals", or "typically contains", never emit an internal marker like 
"(figure: …)", "(rendered figure: …)", or "(sub-page: …)", and never claim you 
"mounted"/"displayed"/"rendered" a view unless you just called its tool. BEFORE 
you write "the chart/ [...]
+- SHOW, don't describe. To present a chart / topology / hierarchy / trace list 
/ any view, you MUST actually CALL the matching show_* tool — that call is the 
ONLY thing that renders the figure. NEVER describe in prose what a view 
"shows", "reveals", or "typically contains", never emit an internal marker like 
"(figure: …)" or "(rendered figure: …)", and never claim you 
"mounted"/"displayed"/"rendered" a view unless you just called its tool. BEFORE 
you write "the chart/figure shows …" (or  [...]
 - Write an ordered narrative: a sentence or two, then a figure, then your 
interpretation, then the next figure. Refer to figures in your prose ("the 
response-time chart shows…").
 - To present several related figures as a tab group (e.g. an entity's response 
time + errors + traffic), call the show_* tools ONE AFTER ANOTHER with the same 
group label and NO prose between them, then write your interpretation after the 
last one — prose between grouped calls closes the group early. Use a standalone 
figure for a single point.
 - Render the figures that support your point — do NOT paste raw JSON or long 
tables as text.
diff --git a/apps/bff/src/http/query/trace.ts b/apps/bff/src/http/query/trace.ts
index c878122..0bebb8a 100644
--- a/apps/bff/src/http/query/trace.ts
+++ b/apps/bff/src/http/query/trace.ts
@@ -376,10 +376,16 @@ export async function fetchNativeList(
 
 /** Fetch one native trace's spans (queryTrace). Hydrates v1 (queryBasicTraces)
  *  list rows — which carry no inline spans — so a captured list can replay the
- *  waterfall offline (the AI trace-capture path). */
+ *  waterfall offline (the AI trace-capture path). Never throws: one unreadable
+ *  trace degrades to no spans (that row replays without a waterfall) instead 
of
+ *  failing the whole capture. */
 export async function fetchNativeTraceSpans(opts: GraphqlOptions, traceId: 
string): Promise<NativeSpan[]> {
-  const env = await graphqlPost<{ trace: { spans: NativeSpan[] } | null 
}>(opts, QUERY_TRACE_DETAIL, { traceId });
-  return env.trace?.spans ?? [];
+  try {
+    const env = await graphqlPost<{ trace: { spans: NativeSpan[] } | null 
}>(opts, QUERY_TRACE_DETAIL, { traceId });
+    return env.trace?.spans ?? [];
+  } catch {
+    return [];
+  }
 }
 
 export async function fetchZipkinList(
diff --git a/apps/ui/src/layer/browser-errors/LayerBrowserErrorsView.vue 
b/apps/ui/src/layer/browser-errors/LayerBrowserErrorsView.vue
index 9893b02..327566b 100644
--- a/apps/ui/src/layer/browser-errors/LayerBrowserErrorsView.vue
+++ b/apps/ui/src/layer/browser-errors/LayerBrowserErrorsView.vue
@@ -94,7 +94,7 @@ const landing = useLayerLanding(safeLayer, safeCfg, 
undefined, replay);
 // Embedded takes the focus service from the prop; the route resolves it from
 // the shared layerSelection store — overriding here keeps the chat block from
 // touching that global selection.
-const serviceNameRaw = useLayerServiceName(layerKey, landing);
+const serviceNameRaw = useLayerServiceName(layerKey, landing, replay);
 const serviceName = computed<string | null>(() =>
   embedded.value ? (props.focusService ?? null) : serviceNameRaw.value,
 );
diff --git 
a/apps/ui/src/layer/endpoint-dependency/LayerEndpointDependencyView.vue 
b/apps/ui/src/layer/endpoint-dependency/LayerEndpointDependencyView.vue
index 214b574..afc5c17 100644
--- a/apps/ui/src/layer/endpoint-dependency/LayerEndpointDependencyView.vue
+++ b/apps/ui/src/layer/endpoint-dependency/LayerEndpointDependencyView.vue
@@ -126,7 +126,7 @@ const isReplay = computed(() => props.replay === true && 
!!props.replayData);
 // A replay map takes its service from props.focusService (not the landing 
rollup)
 // and hides the picker, so it fires ZERO landing queries — gated by replay 
mode.
 const landing = useLayerLanding(safeLayer, safeCfg, undefined, isReplay);
-const resolvedServiceName = useLayerServiceName(layerKey, landing);
+const resolvedServiceName = useLayerServiceName(layerKey, landing, isReplay);
 const serviceName = computed<string | null>(() =>
   embedded.value ? (props.focusService ?? null) : resolvedServiceName.value,
 );
diff --git a/apps/ui/src/layer/logs/LayerLogsView.vue 
b/apps/ui/src/layer/logs/LayerLogsView.vue
index 08d8a36..5a7e536 100644
--- a/apps/ui/src/layer/logs/LayerLogsView.vue
+++ b/apps/ui/src/layer/logs/LayerLogsView.vue
@@ -91,7 +91,7 @@ const landing = useLayerLanding(safeLayer, safeCfg, 
undefined, replay);
 // Embedded takes the focus service from the prop; the route resolves it from
 // the shared layerSelection store — overriding here means the chat block never
 // touches that global selection.
-const serviceNameRaw = useLayerServiceName(layerKey, landing);
+const serviceNameRaw = useLayerServiceName(layerKey, landing, replay);
 const serviceName = computed<string | null>(() =>
   embedded.value ? (props.focusService ?? null) : serviceNameRaw.value,
 );
diff --git a/apps/ui/src/layer/traces/LayerTracesView.vue 
b/apps/ui/src/layer/traces/LayerTracesView.vue
index a5b6136..5f25969 100644
--- a/apps/ui/src/layer/traces/LayerTracesView.vue
+++ b/apps/ui/src/layer/traces/LayerTracesView.vue
@@ -102,7 +102,7 @@ const landing = useLayerLanding(safeLayer, safeCfg, 
undefined, replay);
 // Embedded mode takes the focus service straight from the prop; the route uses
 // the shared layerSelection store (resolved to a name). Overriding here means
 // the chat block never touches that global selection.
-const serviceNameRaw = useLayerServiceName(layerKey, landing);
+const serviceNameRaw = useLayerServiceName(layerKey, landing, replay);
 const serviceName = computed<string | null>(() =>
   embedded.value ? (props.focusService ?? null) : serviceNameRaw.value,
 );
diff --git a/apps/ui/src/layer/useLayerServiceName.ts 
b/apps/ui/src/layer/useLayerServiceName.ts
index 0b55ae7..0271a8e 100644
--- a/apps/ui/src/layer/useLayerServiceName.ts
+++ b/apps/ui/src/layer/useLayerServiceName.ts
@@ -42,9 +42,12 @@ import { isBlankServiceName, BLANK_SERVICE_NAME } from 
'@/utils/serviceName';
 export function useLayerServiceName(
   layerKey: Ref<string>,
   landing: ReturnType<typeof useLayerLanding>,
+  /** REPLAY mode gate: a replayed chat block takes its service from the 
captured
+   *  spec, so the roster fallback must fire ZERO queries (and skip the 
ticker). */
+  replay?: Ref<boolean>,
 ): ComputedRef<string | null> {
   const { selectedId } = useSelectedService();
-  const { services: roster } = useLayerServices(layerKey);
+  const { services: roster } = useLayerServices(layerKey, { replay });
   return computed<string | null>(() => {
     // OAP's reserved blank-entity service reports an EMPTY name over the wire
     // (its id base64-decodes to `_blank`). Resolve it to the literal `_blank`

Reply via email to