Skip to content

feat(genai): hide infrastructure services in logical view - #4312

Open
swetalin-10 wants to merge 6 commits into
jaegertracing:mainfrom
swetalin-10:feat/genai-logical-view
Open

feat(genai): hide infrastructure services in logical view#4312
swetalin-10 wants to merge 6 commits into
jaegertracing:mainfrom
swetalin-10:feat/genai-logical-view

Conversation

@swetalin-10

@swetalin-10 swetalin-10 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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 from utils/genai/detect.ts since 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.
  • This candidate set is sanitized through the same sanitizePrunedServices() root-protection rule the manual service filter already uses (shared via url/svcFilter.ts) - a plain gateway service that owns the trace's root span never gets pruned outright, so the tree never gets orphaned.
  • useTraceTimelineStore gains nonGenAIServicesToHide (computed once per trace load in setTrace, 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), and rootServiceNames (the trace's root-span service names, also cached in setTrace).
  • selectEffectivePrunedServices unions the manual and auto-hidden sets and re-sanitizes the union against rootServiceNames - 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.
  • The service filter panel now shows the effective (manual + auto-hidden) set, not just the manual one, so its checkboxes agree with what the timeline actually shows. Applying the panel is authoritative: it writes the user's full selection into prunedServices and turns hideNonGenAIServicesEnabled off, 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.
  • HideNonGenAIServicesToggle is a small icon-button toggle, styled to match ServiceFilter via a shared HeaderIconButton.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 mirrors useServiceFilter's side-panel-selection cleanup: enabling it while the side panel has a now-hidden span selected deselects it.

How was this change tested?

  • New unit tests: getServicesWithoutGenAIDescendants against the shared genaiTestTrace.json fixture (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.
  • Full affected-file suite: 366/366 passing (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.
  • Manual verification against real traces, via the Upload tab on a local dev server (re-verified after the review-fix push, screenshots below are from this pass):
    • Loaded a real production trace (/api/ai/chat, 13 spans, 2 services - jaeger and jaeger-gemini-sidecar - all GenAI-annotated) and confirmed the toggle is correctly absent: both services already contain GenAI spans, so there's nothing to hide.
    • Loaded the genaiTestTrace.json fixture (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

Real trace: toggle correctly absent

genaiTestTrace.json fixture, toggle off: all services visible

genaiTestTrace.json fixture, toggle on: plain leaf services hidden behind placeholder rows, "Show all services" tooltip visible

Checklist

AI Usage in this PR (choose one)

See AI Usage Policy.

  • None: No AI tools were used in creating this PR
  • Light: AI provided minor assistance (formatting, simple suggestions)
  • Moderate: AI helped with code generation or debugging specific parts
  • Heavy: AI generated most or all of the code changes

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>
@swetalin-10
swetalin-10 requested a review from a team as a code owner August 2, 2026 02:58
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.07%. Comparing base (73af7e8) to head (e7622a8).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jkowall jkowall added the changelog:new-feature Change that should be called out as new feature in CHANGELOG label Aug 4, 2026
@jkowall
jkowall requested a lite review from Copilot August 4, 2026 14:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 LogicalViewToggle UI 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.

swetalin-10 added a commit to swetalin-10/jaeger-ui that referenced this pull request Aug 5, 2026
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>

@yurishkuro yurishkuro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-edgegraphql-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.css duplicates all five .ServiceFilter--button rules — extract a shared class.
  • Three comments cite "yurishkuro's steer on #4272". State the rule, not the attribution.
  • Docstrings on selectEffectivePrunedServices, getServicesWithoutGenAISpans and the component: two sentences each.
  • The two new TimelineHeaderRow tests assert that a node passed as a prop renders. That tests React.
  • LogicalViewToggle/index.test.tsx double-casts a fake trace then mutates span.resource.
  • Drop "generateRowStates.ts has 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>
swetalin-10 added a commit to swetalin-10/jaeger-ui that referenced this pull request Aug 11, 2026
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>
@swetalin-10
swetalin-10 force-pushed the feat/genai-logical-view branch from 0a0def3 to e7622a8 Compare August 11, 2026 10:49
@swetalin-10
swetalin-10 requested a review from yurishkuro August 11, 2026 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog:new-feature Change that should be called out as new feature in CHANGELOG

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants