feat(genai): finish the Evaluation records tab, add a related-trace span picker, and an e2e case fed from both trace sources - #158
Merged
Conversation
wu-sheng
force-pushed
the
feat/genai-evaluation-records
branch
from
September 10, 2026 15:20
720885a to
e916937
Compare
wu-sheng
force-pushed
the
feat/genai-evaluation-records
branch
from
September 11, 2026 00:22
e916937 to
a56baac
Compare
Member
Author
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
force-pushed
the
feat/genai-evaluation-records
branch
from
September 11, 2026 00:43
a56baac to
db2026f
Compare
wankai123
approved these changes
Sep 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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 thelogs:readevaluation catalog filtered to the page's layer, so a role withoutmetrics:readcan pick a provider and the auto-pick can no longer land on a service from another layer. Changing the model writesproviderIdandmodelIdto 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 rootCHANGELOG.mdis 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
TypeaheadSelectpickers. 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.tsmirrorsGenAIEvaluationAnalysisListener(layer GenAI andgen_ai.response.model) andAIEvaluationSpanListener(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 needstraces:readas 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 oneuseEmptyTraceRetry— since a record judged seconds ago can name a segment still landing; the Zipkin route now marks OAP's 404 asnotFound(stillreachable: 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 narrowedrelatedTraceand drops the other scheme's field, since OAP appliesspanIndexto 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
genaicase: 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 composetrafficservice 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 bySW_E2E_SERVICE_COMMITlike the provider and consumer:e2e-spring-ai-serviceis joined to the agent image bybuild-images.sh, ande2e-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
genaicase 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 reportslayer: GenAIwith 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. Thecorecase 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.