feat(genai): hide infrastructure services in logical view - #4312
feat(genai): hide infrastructure services in logical view#4312swetalin-10 wants to merge 6 commits into
Conversation
Resolves jaegertracing#4272 Adds a toggle to the GenAI timeline that hides services with zero GenAI spans, approximating the agent's logical flow by folding away infrastructure noise (HTTP/DB/RPC-only services). Per yurishkuro's steer on jaegertracing#4272, this reuses the service filter's own mechanism rather than a new span-level predicate: GenAI applications concentrate GenAI work in specific services, so pruning whole services with no GenAI spans is a reasonable approximation, and it lets the feature ride entirely on the existing prunedServices/generateRowStates/placeholder-row machinery instead of new filtering logic. Signed-off-by: Swetalin Rout <swetalinrout2006@gmail.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4312 +/- ##
==========================================
+ Coverage 92.02% 92.07% +0.04%
==========================================
Files 373 374 +1
Lines 12056 12129 +73
Branches 3258 3279 +21
==========================================
+ Hits 11095 11168 +73
Misses 839 839
Partials 122 122 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds an optional “logical view” to the GenAI trace timeline by reusing the existing service-pruning mechanism: services that contain zero GenAI spans can be hidden behind the existing pruned-service placeholder rows, helping emphasize the higher-level agent flow.
Changes:
- Added
getServicesWithoutGenAISpans()to detect which services have no GenAI spans and use that to compute a logical-view prune set per trace load. - Introduced a
LogicalViewToggleUI control and new store fields/selector (logicalViewEnabled,logicalViewPrunedServices,selectEffectivePrunedServices) to apply logical pruning consistently across row rendering and uiFind match filtering. - Moved and unit-tested
sanitizePrunedServices()in the shared URL svc filter module so both manual and logical pruning share the same root-service protection.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/jaeger-ui/src/utils/genai/detect.ts | Adds getServicesWithoutGenAISpans() to compute candidate services for logical-view pruning. |
| packages/jaeger-ui/src/utils/genai/detect.test.ts | Adds unit tests for getServicesWithoutGenAISpans(). |
| packages/jaeger-ui/src/components/TracePage/url/svcFilter.ts | Exposes sanitizePrunedServices() for shared root-service protection behavior. |
| packages/jaeger-ui/src/components/TracePage/url/svcFilter.test.ts | Adds direct unit tests for sanitizePrunedServices(). |
| packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.tsx | Switches to selectEffectivePrunedServices so row generation uses the combined prune set. |
| packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/useServiceFilter.tsx | Removes local sanitizePrunedServices() in favor of shared export. |
| packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx | Adds a render slot for the logical-view toggle next to the service filter. |
| packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.test.jsx | Tests rendering behavior for the new header slot. |
| packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/store.ts | Re-exports selectEffectivePrunedServices. |
| packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/store.timeline.ts | Computes/stores logical pruning per trace load and adds selectEffectivePrunedServices. |
| packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/store.test.ts | Adds store/selector tests for logical view behavior and root-service protection. |
| packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/LogicalViewToggle/LogicalViewToggle.css | Styles the logical-view toggle to match the header controls. |
| packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/LogicalViewToggle/index.tsx | Implements the logical-view toggle, including deselecting a now-hidden side-panel selection. |
| packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/LogicalViewToggle/index.test.tsx | Adds component tests for toggle presence, accessibility behavior, and side-panel deselection. |
| packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/index.tsx | Wires the logical-view toggle into the timeline header. |
| packages/jaeger-ui/src/components/TracePage/index.tsx | Switches uiFind match counting/filtering to use the combined prune set selector. |
| packages/jaeger-ui/src/components/TracePage/index.test.jsx | Updates store mocking to include selectEffectivePrunedServices. |
Address Copilot review comment on jaegertracing#4312: selectEffectivePrunedServices allocated a new Set on every call once both the manual and logical-view prune sets were non-empty, defeating Zustand's Object.is equality check and re-rendering subscribers on unrelated store updates. Memoize the union with memoize-one, matching the pattern already used elsewhere in this package (e.g. VirtualizedTraceView.tsx).
Address Copilot review comment on jaegertracing#4312: selectEffectivePrunedServices allocated a new Set on every call once both the manual and logical-view prune sets were non-empty, defeating Zustand's Object.is equality check and re-rendering subscribers on unrelated store updates. Memoize the union with memoize-one, matching the pattern already used elsewhere in this package (e.g. VirtualizedTraceView.tsx). Signed-off-by: Swetalin Rout <swetalinrout2006@gmail.com>
f092d39 to
bac2a9d
Compare
yurishkuro
left a comment
There was a problem hiding this comment.
Reusing the service filter's pruning is the right mechanism, and the placeholder rows keep prunedErrorCount. Two bugs block; one design question needs your call.
Test case: #4372 adds TraceTimelineViewer/genaiTestTrace.json — a sample trace with exactly the topology at issue here: a plain api-edge → graphql-gateway above the agent, and a nested agent below a plain mcp-gateway. Please rebase on it and use it for the fixes below; it also replaces the hand-built spans in your new tests.
Blocking — a plain service above or between GenAI spans hides them. buildVisibleRows prunes a service and its whole subtree (generateRowStates.ts:65-89), but getServicesWithoutGenAISpans only asks whether a service owns a GenAI span. Two common shapes break: a gateway above the agent (a GenAI SDK usually starts its own trace part-way down a request, so the agent is rarely the root), and an MCP server that itself calls an LLM. In the fixture, pruning graphql-gateway leaves 1 of 57 rows and zero GenAI spans — generateRowStates.test.ts asserts it.
The rule must be "no GenAI span at or below". Spans are pre-order DFS with depth (generateRowStates.ts:33-34), so the depth stack in insertPrunedPlaceholders computes it in one pass: push once, pop once, propagate the flag to the parent on close. Don't walk ancestors per GenAI span — that's O(n·d) and goes quadratic on deep traces. Then rename to getServicesWithoutGenAIDescendants, and move it out of utils/genai/detect.ts, which knows nothing about tree order. detect.test.ts's makeServiceSpan has no depth and no tree, so it can't cover the new rule.
Blocking — the union isn't sanitized, so every root can be pruned. Both inputs sanitize separately and selectEffectivePrunedServices unions without re-sanitizing. Two roots, root-a (plain) and root-b (GenAI): the logical view prunes root-a, the manual filter prunes root-b, each legal on its own. The union gives 0 visible rows and no placeholder, since stats are dropped at depth === 0. Store rootServiceNames in setTrace and sanitize inside the memoized union.
The service filter panel contradicts the timeline. You raised this on #4272 and asked for a call: I don't think the trade works as it stands.
useServiceFilter passes the manual prunedServices to ServiceFilter, not the effective set. So with the logical view on, the timeline shows auth-service and friends collapsed behind a "N spans pruned" row, while the funnel panel still lists them as checked and isFilterActive reports no filter. The two views of "what is hidden" disagree.
The checkboxes for those services are then inert in both directions. Unchecking one and applying changes nothing, because it was already hidden. Checking one to bring it back also changes nothing, because handleServiceFilterApply writes only prunedServices and the logical set is still unioned on top. A user who wants one infrastructure service back has no way to get it. Your own docstring on selectEffectivePrunedServices says every read site that needs "what is hidden right now" must use the selector — ServiceFilter is such a site and was left out.
My suggestion: pass the effective set to ServiceFilter for display, and treat an apply as authoritative — write the user's set into prunedServices and set logicalViewEnabled to false. The toggle then reads as a one-shot preset that selects the GenAI services for you, and manual edits always win afterwards. One source of truth in the UI, unhiding works, and it costs a little bookkeeping in one callback. If you'd rather keep the two sets independent, the panel at least has to show logically-hidden rows as hidden and not user-controllable, but I prefer the preset.
Naming. "show only the GenAI/agent flow" overpromises: the filter keeps every span of a GenAI-owning service, including its HTTP and DB spans, because they share a service.name. Use "Hide services with no GenAI spans", and the same for the field and component names.
Scope. Please make it Part of #4272. This handles noise that crosses service boundaries; plumbing inside the agent's own service needs span-level filtering and deserves its own issue rather than being auto-closed.
Cleanups
LogicalViewToggle.cssduplicates all five.ServiceFilter--buttonrules — extract a shared class.- Three comments cite "yurishkuro's steer on #4272". State the rule, not the attribution.
- Docstrings on
selectEffectivePrunedServices,getServicesWithoutGenAISpansand the component: two sentences each. - The two new
TimelineHeaderRowtests assert that a node passed as a prop renders. That tests React. LogicalViewToggle/index.test.tsxdouble-casts a fake trace then mutatesspan.resource.- Drop "
generateRowStates.tshas zero lines changed" from the description — that untouched skip logic is the first bug.
🤖 Drafted with Claude Code, reviewed by @yurishkuro
Fixes two bugs from review: pruning only checked whether a service
directly owned a GenAI span, so a plain gateway above an agent (or an
MCP server above a nested agent) got hidden along with everything
below it; and the union of the manual and auto-hidden pruned sets was
never re-sanitized, so two individually-legal prunes could together
orphan every root.
Also makes the service filter panel authoritative over the auto-hidden
set per the reviewer's design call, renames LogicalViewToggle and all
related store fields/props/tests to state what the toggle does ("hide
services with no GenAI spans") instead of an overpromising "logical
view" framing, trims docstrings to two sentences, replaces attribution
comments with the rule itself, and removes dead prop-rendering tests.
Addresses: jaegertracing#4312 (review)
Signed-off-by: Swetalin Rout <swetalinrout2006@gmail.com>
Fixes two bugs from review: pruning only checked whether a service
directly owned a GenAI span, so a plain gateway above an agent (or an
MCP server above a nested agent) got hidden along with everything
below it; and the union of the manual and auto-hidden pruned sets was
never re-sanitized, so two individually-legal prunes could together
orphan every root.
Also makes the service filter panel authoritative over the auto-hidden
set per the reviewer's design call, renames LogicalViewToggle and all
related store fields/props/tests to state what the toggle does ("hide
services with no GenAI spans") instead of an overpromising "logical
view" framing, trims docstrings to two sentences, replaces attribution
comments with the rule itself, and removes dead prop-rendering tests.
Addresses: jaegertracing#4312 (review)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NvdxzxkjsKcTpN4tGngUgE
Signed-off-by: Swetalin Rout <swetalinrout2006@gmail.com>
Yuri's review said the genaiTestTrace.json fixture (jaegertracing#4372) "also replaces the hand-built spans in your new tests." Only generateRowStates.test.ts picked that up; store.test.ts and the toggle's own test still built synthetic spans. store.test.ts's synthetic spans also had a real bug: two of the "setTrace computing nonGenAIServicesToHide" cases never overrode `depth`, so every span defaulted to depth 0 and was treated as a sibling rather than a child - the tests passed, but only because root-service protection independently discarded the result, not because the parent/child nesting they were named for was exercised. Swapped the two cases that test real GenAI-topology detection for the fixture trace, fixed the remaining two synthetic cases (a non-GenAI trace and a childless-root case, neither representable by the fixture) to pass explicit depths, and swapped the toggle's synthetic spanID/serviceName pairs for real fixture span IDs. Signed-off-by: Swetalin Rout <swetalinrout2006@gmail.com>
0a0def3 to
e7622a8
Compare
Which problem is this PR solving?
Part of #4272
In a trace from an agent framework, the spans that carry meaning - LLM calls, tool invocations, retrievals - are a minority. Most rows are the HTTP/RPC/DB spans the framework generates underneath them, so reading the actual agent flow means scrolling past the plumbing to find it. The GenAI view currently gives these traces icons and a detail tab, but still shows every span - nothing narrows the tree to the agent's logical steps (deliverable 4 in jaegertracing/jaeger#8401). This handles noise that crosses service boundaries; plumbing inside the agent's own service needs span-level filtering and deserves its own issue rather than being auto-closed by this one.
Description of the changes
This reuses the existing service filter's mechanism rather than a new span-level predicate: real GenAI applications concentrate GenAI work in specific services, calling out to plain infrastructure services underneath, so pruning whole services with no GenAI span anywhere in their subtree is a reasonable approximation of "show only the agent's flow" - it costs nothing beyond the existing
prunedServices/ placeholder-row machinery, only changing what populates that set.getServicesWithoutGenAIDescendants()(generateRowStates.ts, moved here fromutils/genai/detect.tssince it needs tree order) does a single O(n) pass over the pre-order DFS span list with a depth stack, propagating a "has GenAI descendant" flag to each parent frame on close. The rule is "no GenAI span at or below", not "owns no GenAI span directly" - a plain gateway sitting above an agent, or a plain MCP server sitting above a nested agent, must stay visible even though it owns no GenAI span itself, since pruning removes a service's whole subtree.sanitizePrunedServices()root-protection rule the manual service filter already uses (shared viaurl/svcFilter.ts) - a plain gateway service that owns the trace's root span never gets pruned outright, so the tree never gets orphaned.useTraceTimelineStoregainsnonGenAIServicesToHide(computed once per trace load insetTrace, not recomputed per render or per toggle),hideNonGenAIServicesEnabled(a simple boolean, off by default, reset on every new trace - not persisted to the URL or localStorage, unlike the manual filter), androotServiceNames(the trace's root-span service names, also cached insetTrace).selectEffectivePrunedServicesunions the manual and auto-hidden sets and re-sanitizes the union againstrootServiceNames- two prunes that are each individually legal (their own root stays visible) can still together orphan every root once combined, which sanitizing each input separately can't catch. The union is memoized so repeated calls between actual state changes return the same Set reference.prunedServicesand turnshideNonGenAIServicesEnabledoff, so the toggle reads as a one-shot preset that seeds the manual filter - manual edits always win afterwards, and unhiding an auto-hidden service by unchecking it in the panel now actually works.HideNonGenAIServicesToggleis a small icon-button toggle, styled to matchServiceFiltervia a sharedHeaderIconButton.css, rendered in the timeline header next to it. It's hidden when there's nothing to hide (no GenAI spans, or every service already has some), and mirrorsuseServiceFilter's side-panel-selection cleanup: enabling it while the side panel has a now-hidden span selected deselects it.How was this change tested?
getServicesWithoutGenAIDescendantsagainst the sharedgenaiTestTrace.jsonfixture (test(genai): Add sample GenAI trace fixture and generator #4372), covering a plain gateway directly above an agent, a plain tier two levels above an agent, and a plain tier above a nested agent under another plain service;sanitizePrunedServices(svcFilter.test.ts); the store's union re-sanitization (store.test.ts, including the two-roots-pruned-independently collision); the effective-set-to-panel and authoritative-apply behavior (useServiceFilter.test.ts); and the toggle component itself (HideNonGenAIServicesToggle/index.test.tsx), including the root-service-protection case and the side-panel-deselection case.generateRowStates,store,useServiceFilter,TimelineHeaderRow,TracePage,svcFilter,VirtualizedTraceView,ServiceFilter,detect, and the toggle component).tsc(jaeger-ui),oxlint, and formatting all clean on the changed files./api/ai/chat, 13 spans, 2 services -jaegerandjaeger-gemini-sidecar- all GenAI-annotated) and confirmed the toggle is correctly absent: both services already contain GenAI spans, so there's nothing to hide.genaiTestTrace.jsonfixture (test(genai): Add sample GenAI trace fixture and generator #4372, 57 spans, 10 services, the exact plain-gateway-above-agent / nested-agent-below-plain-MCP-server topology the review's Bug 1 was about) and exercised the toggle live: off, it shows every service; clicking it hides the 5 plain leaf services (auth-service,metrics-backend,trace-store,k8s-api,vector-store) behind "N spans pruned" / "N spans pruned, 1 error" placeholder rows while every GenAI span and the plain tiers above the agents stay visible, and the tooltip correctly reads "Show all services" while active.Screenshots
Checklist
AI Usage in this PR (choose one)
See AI Usage Policy.