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`
