Skip to content

feat(genai): finish the Evaluation records tab, add a related-trace span picker, and an e2e case fed from both trace sources - #158

Merged
wu-sheng merged 1 commit into
mainfrom
feat/genai-evaluation-records
Sep 11, 2026
Merged

feat(genai): finish the Evaluation records tab, add a related-trace span picker, and an e2e case fed from both trace sources#158
wu-sheng merged 1 commit into
mainfrom
feat/genai-evaluation-records

Conversation

@wu-sheng

@wu-sheng wu-sheng commented Sep 10, 2026

Copy link
Copy Markdown
Member

Why

The Evaluation records tab shipped in #99 had three review findings — a logs-only role had no working provider selector, a model change left the previous provider in the address, and an OTLP trace link dropped the record's timestamp — and, once driven against a live judge, a set of gaps an operator hits at once: it queried on load and on every keystroke where every sibling tab waits for Run query; the provider, model and service pickers could not be filtered; the value type and its operand were unrelated fields; the stream had no column titles; and there was no way to narrow a trace-id condition to the one LLM span the judge scored. There was also no e2e environment producing evaluation records, from either trace source.

What

Fixes from review. The tab owns its provider picker (ownsServiceSelector), drawn from the logs:read evaluation catalog filtered to the page's layer, so a role without metrics:read can pick a provider and the auto-pick can no longer land on a service from another layer. Changing the model writes providerId and modelId to the address together. The OTLP branch of the result-trace popout passes the record time as the lookup moment, the same as the native branch. Also from #99: the empty root CHANGELOG.md is removed, the encoding damage in the pager, the histogram tooltip and every comment is repaired, and the feature gets its changelog entry and an operator docs page (docs/operate/genai-evaluation.md).

The tab's contract. Conditions are staged until Run query; a provider switch returns to the prompt; a dashboard drill or a link carrying a trace id runs on arrival; the level chips under the results commit and re-read at once. Provider, Model and Service are TypeaheadSelect pickers. A caller that reports only through the Zipkin receiver registers no layer service, so the facet sample now carries the callers' ids and the picker lists the callers seen in the records beside the catalog; a caller once seen stays pickable and the picked one stays visible after a query whose sample has no row of it. A catalog that cannot be read is reported with a retry where the provider prompt would otherwise stand. Value is one condition — the type and, beside it, the operand that type takes — and sort is one ordering. The conditions sit in fixed rows so a control that appears (the value's operand, the trace address) never reflows its neighbours, and the pickers take the native inputs' metrics. The stream has a header row. The level strip shows each level's share of the newest sampled records with the count in the tooltip, since a count in a fixed-size sample saturates as soon as the window holds more records than the sample. In the record detail, a score or a true/false verdict leads the table, which then spans the popout; a string or JSON verdict keeps its content pane beside the table.

Span picker. The trace-id condition narrows to one span. Pick span… opens the trace, native or OTLP, and lists its spans with the ones OAP's judge samples marked LLM and shown first; the rule in genaiSpan.ts mirrors GenAIEvaluationAnalysisListener (layer GenAI and gen_ai.response.model) and AIEvaluationSpanListener (the tag alone). A pick fills the segment and span index (native) or the span id (OTLP); the fields can also be typed from ids found in logs. The picker looks the trace up over the page's whole selected window, taken when it opens, and needs traces:read as the row links do. A trace that reads as empty is re-read for a while — in the picker and in both trace popouts, through one useEmptyTraceRetry — since a record judged seconds ago can name a segment still landing; the Zipkin route now marks OAP's 404 as notFound (still reachable: false, since a misconfigured Zipkin URL answers 404 too), so that case is re-read while a failure the route reports inside a 200 is shown as a failure. The BFF forwards the narrowed relatedTrace and drops the other scheme's field, since OAP applies spanIndex to any non-OTLP query.

Zipkin popout. The selected span shows in a side panel beside the waterfall, as the native popout does, instead of a dialog over it.

e2e. A new genai case: OAP with its LLM judge on, upstream's mock provider, spring-ai on the Java agent for native traces and a Python OpenTelemetry caller for OTLP. A compose traffic service drives both callers in turn for the whole run, so records of both sources stay in every page the specs read; the case declares no trigger. Readiness checks cover records and traces of both types, and a Playwright project covers the tab, both popouts, the span picker and the layer's card widgets. The demo-service pin moves to the upstream commit that added the mock LLM. Both callers' Java halves come from apache/skywalking's published e2e images on ghcr, pinned by SW_E2E_SERVICE_COMMIT like the provider and consumer: e2e-spring-ai-service is joined to the agent image by build-images.sh, and e2e-mock-llm-server — the mock that answers both callers and OAP's judge — runs as its own service. The pin moves to the upstream commit that vendored the app and split the mock out (#14071). Nothing is built from a checkout; the only image built at all is the Python caller's pip install.

Validation

Type-check, both app builds, lint, license headers, and the unit suites (BFF 1741, UI 960) are green; the new UI strings are in all eight catalogs. The genai case passed end to end through infra-e2e (all eight verify cases, six browser specs) on an earlier build of this branch; the later UI work was validated on a local dev build against the same compose stack with the same Playwright project, and by driving the page by hand. The first CI run of the case failed one spec: the picker opened on the newest record read a trace whose segment had not landed yet and never looked again — the picker now re-reads an empty answer for a while, and the spec picks from a trace the popout has drawn. Every screen was checked against a live OAP running the judge: the judged native span reports layer: GenAI with the model tag, and its segment id and span id equal the record's address, which is what the LLM marking and the pick rely on. The core case was not re-run locally after the demo-service pin bump, and the popouts' re-read was validated by unit tests only, the local stack having been shut down by then; CI runs every case.

@wu-sheng wu-sheng added this to the 1.1.0 milestone Sep 10, 2026
@wu-sheng wu-sheng added bug Something isn't working enhancement New feature or request labels Sep 10, 2026
@wu-sheng
wu-sheng force-pushed the feat/genai-evaluation-records branch from 720885a to e916937 Compare September 10, 2026 15:20
@wu-sheng wu-sheng changed the title feat(genai): finish the Evaluation records tab, add a span picker for the related trace, and an e2e case fed from both trace sources feat(genai): finish the Evaluation records tab, add a related-trace span picker, and an e2e case fed from both trace sources Sep 10, 2026
@wu-sheng
wu-sheng force-pushed the feat/genai-evaluation-records branch from e916937 to a56baac Compare September 11, 2026 00:22
@wu-sheng

Copy link
Copy Markdown
Member Author

I polished the layout in a better way.
image

wankai123
wankai123 previously approved these changes Sep 11, 2026
…pan picker, and an e2e case fed from both trace sources

The Evaluation records tab from #99 reaches operators as designed, and gains what reading a judged call needs.

Fixes from review: a role with logs:read alone gets a working provider picker, drawn from the evaluation catalog on the page's layer rather than the metrics roster (the tab owns its picker, so the auto-pick can no longer land on a service from another layer); changing the model writes provider and model to the address together, so a reload or a shared link applies the pair; an OTLP trace link keeps the record's time as its lookup moment, so an older trace still opens with the cold stage on. Also from #99: the empty root CHANGELOG.md the repo forbids is removed, the encoding damage in the pager, the histogram tooltip and every comment is repaired, and the feature gets its changelog entry and an operator docs page.

The tab follows the Logs tab's contract: conditions stage until Run query, a provider switch returns to the prompt, a dashboard drill or a link with a trace id runs on arrival, and the level chips under the results act at once. Provider, model and the calling service are type-to-filter pickers; a caller that only reports through the Zipkin receiver has no catalog entry, so the facet sample carries the callers seen in the records with their ids and the picker lists them. The value is one condition with the operand its type takes; sort is one ordering; the conditions sit in fixed rows so a control that appears never reflows its neighbours; the stream has a header row. The level strip shows each level's share of the newest sampled records, with the count in the tooltip, since a count in a fixed-size sample saturates and says nothing. In the record detail a score or a true/false verdict leads the table, which then spans the popout; a string or JSON verdict keeps its content pane.

The trace-id condition narrows to one span: Pick span… opens the trace (native or OTLP) and lists its spans with the ones OAP's judge samples marked LLM and shown first — the rule mirrors GenAIEvaluationAnalysisListener and AIEvaluationSpanListener — and a pick fills segment + span index or span id; the same fields can be typed from ids in logs. The picker looks the trace up over the page's whole selected window, taken when it opens, and needs traces:read as the row links do; a failure the trace route reports inside a 200 is shown as a failure. A trace that reads as empty is re-read for a while — in the picker and in both trace popouts — since a record judged seconds ago can name a segment still landing; the Zipkin route marks OAP's 404 as notFound so that case is told from a misconfigured Zipkin URL, which it otherwise looks like. The BFF forwards the narrowed relatedTrace, dropping the other scheme's field since OAP applies spanIndex to any non-OTLP query. A caller seen in any facet sample stays pickable, and the picked one stays visible after a query whose sample has no row of it; a catalog that cannot be read is reported with a retry where the provider prompt would otherwise stand.

The Zipkin trace popout shows the selected span in a side panel beside the waterfall, as the native popout does.

e2e: a new `genai` case runs OAP's LLM judge over a mock provider called from spring-ai on the Java agent (native traces) and from a Python OpenTelemetry caller (OTLP traces). A compose `traffic` service drives both callers in turn for the whole run, so records of both sources stay in every page the specs read; readiness checks cover records and traces of both types, and a browser project covers the tab, both popouts, the span picker and the layer's card widgets. The callers come from apache/skywalking's published e2e images: the Spring AI app (e2e-spring-ai-service) is joined to the agent image by build-images.sh the way the provider and consumer are, and the mock LLM that answers both callers and OAP's judge runs as its own service (e2e-mock-llm-server). The demo-service pin moves to the upstream commit that vendored the app and split the mock out; nothing is built from a checkout.
@wu-sheng
wu-sheng merged commit b779c07 into main Sep 11, 2026
21 of 23 checks passed
@wu-sheng
wu-sheng deleted the feat/genai-evaluation-records branch September 11, 2026 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants