Skip to content

feat: Traces push live-refresh + page improvements - #206

Closed
ytallo wants to merge 4 commits into
mainfrom
feat/traces-live-refresh
Closed

feat: Traces push live-refresh + page improvements#206
ytallo wants to merge 4 commits into
mainfrom
feat/traces-live-refresh

Conversation

@ytallo

@ytallo ytallo commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Replace the Traces page's 3-second polling with a push signal driven by harness activity, plus a round of Traces-page improvements. Observability-quality / UX change — no agent behavior change.

Push-based live refresh (replaces polling)

  • A new harness fanout pump (harness/src/harness/fanout/traces-changed.ts) subscribes to the agent::turn_end stream and, coalesced over ~400ms, pushes an empty ui::traces::changed signal to all-sessions subscribers (reusing the existing FanoutState + ui::subscribe, with function_not_found eviction).
  • The console subscribes via a new framework-free core + useTracesLiveRefresh hook (console/web/src/lib/devtools-stream.ts) that invalidates the ['traces'] / ['traceGroups'] query caches on the signal, and re-subscribes + re-syncs on WS reconnect and on tab-visible.
  • useTraceData / useTraceGroups drop their 3s refetchInterval — freshness now comes from the push signal, connect-resync, and the manual Refresh button.

Traces-page improvements

Extracted, unit-tested helpers (spanTree, treeFlatten, minimapMarkers, percent, attributeText, plus traceListItem / traceTransform updates), component refinements (FlameGraph, WaterfallChart, SpanOtelLogsTab, SpanErrorsTab, group/detail panels), and auto-pause of live updates while a detail panel is open (restoring the prior pause intent on close, since polling is no longer the throttle).

Behavior / compatibility

  • The Traces view is now pure-push (no polling interval). A trace produced with no subsequent turn and no reconnect surfaces via the manual Refresh; the WS-connect re-sync covers cold-start/reconnect.
  • The fanout pump requires a harness rebuild + restart to take effect at runtime.

Test plan

  • Console unit suite green (pnpm test, 479 tests) + pnpm typecheck.
  • Harness unit suite green (fanout pump test) + pnpm typecheck.
  • Manual: open Traces, run an agent turn from chat → list updates within ~400ms; DevTools Network shows no engine::traces::list every 3s.
  • Manual: Pause/Resume + auto-pause-on-detail-open behave; manual Refresh works.
  • Manual: restart the harness mid-session with Traces open → live updates resume after the WS reconnects.

Summary by CodeRabbit

Release Notes

  • New Features

    • Live trace data refresh triggered by real-time signals; traces now update automatically when collection service detects changes.
    • Detection and notification when trace collection engine is not properly configured.
  • Improvements

    • Enhanced performance for large and deeply nested trace visualizations.
    • Better error handling and display; query errors now show with a retry option.
    • Improved pause/resume behavior with automatic pausing when selecting traces.
    • More robust span error and attribute display.
  • Bug Fixes

    • Better detection of cycles in span hierarchies.

ytallo added 4 commits June 1, 2026 12:22
- Added multiple test cases to `spanTree.test.ts` to verify the behavior of `flattenTree` when `hideEngineRouting` is enabled.
- Tests include scenarios for keeping worker calls visible while hiding engine dispatch wrappers, nesting worker rows per hop, and ensuring correct visibility when child calls share the same service.
- Updated `spanTree.ts` with a new utility function `isHideableRoutingNode` to determine if a span should be hidden based on its name and parent context, improving the logic for rendering spans in the tree structure.
Add a fanout pump that subscribes to the agent::turn_end stream and, with a
~400ms trailing coalesce, pushes an empty ui::traces::changed signal to every
all-sessions subscriber, reusing the existing FanoutState + ui::subscribe and
the function_not_found eviction path.

Add the console-side subscriber (useTracesLiveRefresh + a framework-free
startTracesSubscription/makeTracesChangedHandler core) that invalidates the
['traces'] and ['traceGroups'] React Query caches on the signal, re-subscribes
and re-syncs on WS reconnect, and re-syncs on tab-visible. This lets the Traces
view refresh on real activity instead of a 3s polling timer.

The Traces page wiring (dropping refetchInterval, calling the hook) lives in
the page files and is tracked with the broader Traces-page changes.
Wire the Traces page to the ui::traces::changed push signal: drop the 3s
refetchInterval from useTraceData/useTraceGroups, call useTracesLiveRefresh,
and auto-pause live updates while a detail panel is open (restoring the prior
pause intent on close, since polling is no longer the throttle).

Also: Traces-view refinements plus extracted, unit-tested helpers
(attributeText, minimapMarkers, percentOfTotal, preorder treeFlatten) with
spanTree/traceListItem/traceTransform updates and component tweaks across
FlameGraph, WaterfallChart, SpanOtelLogsTab, SpanErrorsTab, and the group/
detail panels.
@vercel

vercel Bot commented Jun 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Error Error Jun 1, 2026 3:26pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors the Traces feature to replace polling-based refresh with live signal-driven React Query invalidation, introduces defensive attribute handling, optimizes component rendering, and adds backend fanout infrastructure for broadcasting trace-change events across browsers.

Changes

Traces Live-Refresh Pipeline

Layer / File(s) Summary
Live-refresh subscription mechanism
console/web/src/lib/devtools-stream.ts, console/web/src/lib/devtools-stream.test.ts
New module exports makeTracesChangedHandler (signal→invalidate), startTracesSubscription (register handler, subscribe to all sessions, handle reconnect/visibility), and useTracesLiveRefresh hook for component integration; comprehensive test coverage validates invalidation gating, subscription lifecycle, and visibility-driven resync.
Query hooks refactor for external invalidation
console/web/src/pages/Traces/hooks/useTraceData.ts, console/web/src/pages/Traces/hooks/useTraceGroups.ts
Removed isPaused option and polling (refetchInterval: false); useTraceData now exposes queryError and derives hasOtelConfigured from exporterDisabled (independent of span presence); both hooks rely on external live-refresh invalidation.
Traces page integration of live-refresh and state management
console/web/src/pages/Traces/index.tsx
Integrated useTracesLiveRefresh({ isPaused }); refactored pause state into user pause + auto-pause (on detail open/close); added explicit queryError rendering path with retry button; rewired trace/group selection to update selectedGroup and call selectGroup callback; updated SessionDetailPanel to receive waterfall context in span clicks.
Exporter availability detection in API contract
console/web/src/pages/Traces/api/traces.ts, console/web/src/pages/Traces/api/traces.test.ts
TracesResponse now includes optional exporterDisabled field; fetchTraces returns exporterDisabled: true when memory exporter is not enabled; tests validate error signaling vs. empty-but-legitimate results.
Defensive value coercion & status normalization
console/web/src/pages/Traces/lib/attributeText.ts, console/web/src/pages/Traces/lib/attributeText.test.ts, console/web/src/pages/Traces/lib/traceTransform.ts, console/web/src/pages/Traces/lib/traceTransform.test.ts, console/web/src/pages/Traces/lib/traceListItem.ts, console/web/src/pages/Traces/lib/traceListItem.test.ts, console/web/src/pages/Traces/components/SpanErrorsTab.tsx
New attributeText utility safely coerces unknown span attributes (strings, numbers, objects) to renderable text with JSON serialization fallback. New normalizeSpanStatus helper defensively maps string/numeric status values (including OTel codes) and invalid inputs to canonical 'error' / 'ok' / 'unset'; replaces direct .toLowerCase() calls and used in span list and error tabs.
Component rendering optimizations
console/web/src/pages/Traces/components/FlameGraph.tsx, console/web/src/pages/Traces/components/WaterfallChart.tsx, console/web/src/pages/Traces/components/SpanOtelLogsTab.tsx, console/web/src/pages/Traces/components/TraceFilters.tsx, console/web/src/pages/Traces/components/SessionDetailPanel.tsx, console/web/src/pages/Traces/components/ServiceBreakdown.tsx
FlameGraph replaced recursive tree flattening with iterative flattenPreorder and switched maxDepth to reduce to prevent stack overflow on deep trees; optimized hover handler to skip state updates when hovered span unchanged. WaterfallChart memoizes minimap markers via sampleMinimapMarkers. SpanOtelLogsTab memoizes JSON parsing/pretty-printing and attribute classification. TraceFilters stabilizes empty-array reference. SessionDetailPanel expands onSpanClick callback to include WaterfallData. ServiceBreakdown uses percentOfTotal helper for safe percentage computation.
Math and layout utilities
console/web/src/pages/Traces/lib/percent.ts, console/web/src/pages/Traces/lib/percent.test.ts, console/web/src/pages/Traces/lib/treeFlatten.ts, console/web/src/pages/Traces/lib/treeFlatten.test.ts, console/web/src/pages/Traces/lib/minimapMarkers.ts, console/web/src/pages/Traces/lib/minimapMarkers.test.ts
New percentOfTotal(part, total) guards against divide-by-zero and non-finite inputs, clamps to [0, 100]. New flattenPreorder iteratively flattens trees via explicit stack to avoid recursion depth limits. New sampleMinimapMarkers down-samples spans into bounded marker set (max 200) for minimap rendering, preserving error spans.
Span tree cycle-safety and hideability
console/web/src/pages/Traces/lib/spanTree.ts, console/web/src/pages/Traces/lib/spanTree.test.ts
Enhanced buildSpanTree with cycle detection (self-parent and mutual cycles promoted to roots, preventing node loss). New isHideableRoutingNode predicate refines engine routing span hiding based on parent function-ID relationships and cross-service boundaries. Updated flattenTree to use parent-aware hideability logic.
TraceGroupsView selection refactor
console/web/src/pages/Traces/components/TraceGroupsView.tsx
Updated props to remove isPaused and selectedTraceId, introducing onSelectGroup (preferred, receives full TraceGroup), optional onSelectTrace fallback, and selectedGroupValue for highlighting; click handler prefers group selection when callback available.
Backend fanout pump for trace-change broadcasts
harness/src/harness/fanout/traces-changed.ts, harness/src/harness/fanout/index.ts, harness/tests/harness/fanout/traces-changed.test.ts
New spawnTracesChangedPump subscribes to agent::turn_end, debounces (400ms trailing-edge), and fans out ui::traces::changed::<browser_id> to all-sessions subscribers; evicts browsers on function_not_found errors; includes registration, coalescing, targeting, eviction, and teardown test coverage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • iii-hq/workers#102: Adds the ui::traces::changed::<browser_id> fanout pump that this PR's frontend code subscribes to for live trace updates.

  • iii-hq/workers#157: Likely related groundwork refactoring Traces page architecture that aligns with this PR's hook updates and live-refresh integration.

Suggested reviewers

  • sergiofilhowz
  • andersonleal

Poem

🐰 Live traces hop and skip,
Signals flow without a trip,
Pause and play, the UI's way,
Safer calls with less to say!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the primary changes: introducing push-based live-refresh for Traces (replacing polling) plus related page improvements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/traces-live-refresh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 13 skipped (no docs/).

Layer Result
structure
vale
ai
render

Note

17 stale rendered artifact(s) detected on main, unrelated to this PR. This PR is fine; the drift was already there. A maintainer should open a chore PR to re-render these.

  • shell/README.md
  • shell/skill.md
  • shell/skills/chmod.md
  • shell/skills/exec.md
  • shell/skills/exec_bg.md
  • shell/skills/grep.md
  • shell/skills/kill.md
  • shell/skills/list.md
  • shell/skills/ls.md
  • shell/skills/mkdir.md
  • shell/skills/mv.md
  • shell/skills/read.md
  • …and 5 more (see the workflow logs)

@ytallo

ytallo commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Split out of #205 (originally one combined PR). #205 now carries the harness telemetry/span-volume reductions; this PR carries the Traces push live-refresh + page improvements. Independent — no ordering dependency between the two.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
console/web/src/pages/Traces/api/traces.ts (1)

174-176: ⚖️ Poor tradeoff

Fix: group-by isn’t ambiguous; only trace-tree detail loses the “exporter disabled” signal.

fetchTracesGroupBy returns { groups: [] } on memory exporter … not enabled and TracesGroupByResponse has no exporterDisabled. However, Traces/index.tsx gates all group-by rendering behind hasOtelConfigured derived from the flat fetchTraces response (exporterDisabled → “no observability”), so the groups view won’t fall back to “no traces” when the exporter is disabled.

fetchTraceTree likewise returns { roots: [] } without an exporterDisabled flag; when tree detail is loaded, empty roots are treated as a generic empty/error state (“no span data available for this trace”), so the tree path can still lose the explicit “no observability” messaging. Consider adding an analogous flag to TraceTreeResponse (or wiring tree detail to the existing hasOtelConfigured state) instead of changing TracesGroupByResponse.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/pages/Traces/api/traces.ts` around lines 174 - 176, The
trace-tree path currently swallows the "memory exporter not enabled" signal by
returning only { roots: [] } from fetchTraceTree; update the TraceTreeResponse
type and the fetchTraceTree function to include an exporterDisabled: boolean and
return exporterDisabled: true when isMemoryExporterNotEnabled(err) is true
(i.e., change the error branch that currently returns { roots: [] } to return {
roots: [], exporterDisabled: true }); also update any consumers (e.g.,
Traces/index.tsx or other components that read trace detail) to respect
TraceTreeResponse.exporterDisabled the same way group-by uses exporterDisabled
so the UI can show the explicit "no observability / exporter disabled" state
instead of a generic empty-tree message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@console/web/src/pages/Traces/api/traces.test.ts`:
- Around line 8-11: The test's vi.mock factory is hoisted before `const call =
vi.fn()` so it closes over an uninitialized `call`; fix by declaring `call`
using `vi.hoisted` (e.g. `const call = vi.hoisted(() => vi.fn())`) so the mock
factory in the `getIiiClient` mock can access the initialized `call`; update the
`traces.test.ts` file so `call` is created with `vi.hoisted` and the existing
mock for `getIiiClient` continues to return an object that references `call`.

In `@console/web/src/pages/Traces/hooks/useTraceData.ts`:
- Around line 120-126: When a fetch returns no spans the code clears traceList
items and fingerprintRef but fails to clear pendingTracesRef, allowing stale
pending traces to be flushed later; update the empty-spans branch to also clear
pendingTracesRef.current (e.g., set to new Map() or empty array consistent with
its type) and remove the redundant setHasOtelConfigured(true) call inside
flushPendingTraces since useEffect already sets hasOtelConfigured based on
exporterDisabled; change references to pendingTracesRef and flushPendingTraces
accordingly so the hover/flush path no longer displays outdated traces.

---

Nitpick comments:
In `@console/web/src/pages/Traces/api/traces.ts`:
- Around line 174-176: The trace-tree path currently swallows the "memory
exporter not enabled" signal by returning only { roots: [] } from
fetchTraceTree; update the TraceTreeResponse type and the fetchTraceTree
function to include an exporterDisabled: boolean and return exporterDisabled:
true when isMemoryExporterNotEnabled(err) is true (i.e., change the error branch
that currently returns { roots: [] } to return { roots: [], exporterDisabled:
true }); also update any consumers (e.g., Traces/index.tsx or other components
that read trace detail) to respect TraceTreeResponse.exporterDisabled the same
way group-by uses exporterDisabled so the UI can show the explicit "no
observability / exporter disabled" state instead of a generic empty-tree
message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ed514673-1f64-4c60-8e5b-8b773b7d6bd5

📥 Commits

Reviewing files that changed from the base of the PR and between b9e314c and 3103098.

📒 Files selected for processing (32)
  • console/web/src/lib/devtools-stream.test.ts
  • console/web/src/lib/devtools-stream.ts
  • console/web/src/pages/Traces/api/traces.test.ts
  • console/web/src/pages/Traces/api/traces.ts
  • console/web/src/pages/Traces/components/FlameGraph.tsx
  • console/web/src/pages/Traces/components/ServiceBreakdown.tsx
  • console/web/src/pages/Traces/components/SessionDetailPanel.tsx
  • console/web/src/pages/Traces/components/SpanErrorsTab.tsx
  • console/web/src/pages/Traces/components/SpanOtelLogsTab.tsx
  • console/web/src/pages/Traces/components/TraceFilters.tsx
  • console/web/src/pages/Traces/components/TraceGroupsView.tsx
  • console/web/src/pages/Traces/components/WaterfallChart.tsx
  • console/web/src/pages/Traces/hooks/useTraceData.ts
  • console/web/src/pages/Traces/hooks/useTraceGroups.ts
  • console/web/src/pages/Traces/index.tsx
  • console/web/src/pages/Traces/lib/attributeText.test.ts
  • console/web/src/pages/Traces/lib/attributeText.ts
  • console/web/src/pages/Traces/lib/minimapMarkers.test.ts
  • console/web/src/pages/Traces/lib/minimapMarkers.ts
  • console/web/src/pages/Traces/lib/percent.test.ts
  • console/web/src/pages/Traces/lib/percent.ts
  • console/web/src/pages/Traces/lib/spanTree.test.ts
  • console/web/src/pages/Traces/lib/spanTree.ts
  • console/web/src/pages/Traces/lib/traceListItem.test.ts
  • console/web/src/pages/Traces/lib/traceListItem.ts
  • console/web/src/pages/Traces/lib/traceTransform.test.ts
  • console/web/src/pages/Traces/lib/traceTransform.ts
  • console/web/src/pages/Traces/lib/treeFlatten.test.ts
  • console/web/src/pages/Traces/lib/treeFlatten.ts
  • harness/src/harness/fanout/index.ts
  • harness/src/harness/fanout/traces-changed.ts
  • harness/tests/harness/fanout/traces-changed.test.ts

Comment on lines +8 to +11
const call = vi.fn()
vi.mock('@/lib/iii-client', () => ({
getIiiClient: vi.fn(async () => ({ call })),
}))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Vitest 4 throw an error when a vi.mock factory references a top-level variable not prefixed with "mock"?

💡 Result:

Yes, Vitest throws an error if a vi.mock factory references a variable defined outside of its scope [1][2][3]. This is because vi.mock is hoisted to the top of the file and executed before other module-level code [1][2][4]. The error message specifically states: "There was an error when mocking a module. If you are using 'vi.mock' factory, make sure there are no top level variables inside, since this call is hoisted to top of the file." [5][3]. Note that this requirement is not related to a variable naming convention (e.g., whether it is prefixed with "mock"); it applies to any variable defined outside the factory that is accessed within the factory's closure [2][6]. To resolve this issue, you have two primary options: 1. Use vi.hoisted(): This allows you to define variables that are also hoisted to the top of the file, making them available for use within the vi.mock factory [2][6][7]. 2. Use vi.doMock(): This method is not hoisted, allowing you to reference standard top-level variables. However, because it is not hoisted, it only affects subsequent dynamic imports, meaning you must be careful about the order of your imports [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== File: console/web/src/pages/Traces/api/traces.test.ts (lines 1-60) =="
nl -ba console/web/src/pages/Traces/api/traces.test.ts | sed -n '1,60p'

echo
echo "== Look up Vitest version =="
if [ -f package.json ]; then
  node -p "require('./package.json').devDependencies?.vitest || require('./package.json').dependencies?.vitest || 'no-vitest-in-root-package.json'"
else
  echo "no root package.json"
fi

# Also check for any workspace/package.json that might define vitest
echo
echo "== Any package.json containing vitest in repo =="
fd package.json . -x sh -c 'f="$1"; if rg -n "\"vitest\"" "$f" >/dev/null 2>&1; then echo "$f"; rg -n "\"vitest\"" "$f" | head -n 5; fi' sh

Repository: iii-hq/workers

Length of output: 192


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="console/web/src/pages/Traces/api/traces.test.ts"
echo "== Inspect: $FILE =="
if [ ! -f "$FILE" ]; then
  echo "File not found: $FILE"
  exit 1
fi

# Print with line numbers (no nl available)
awk '{printf "%5d:%s\n", NR, $0}' "$FILE" | sed -n '1,80p'

echo
echo "== Find Vitest version in repo (package.json files) =="
# List packages that mention vitest and print the resolved version field
for p in $(fd package.json .); do
  if rg -n '"vitest"\s*:' "$p" >/dev/null 2>&1; then
    echo "--- $p ---"
    node -e "const p=require('path').resolve(process.argv[1]); const j=require(p); const v=(j.devDependencies&&j.devDependencies.vitest)|| (j.dependencies&&j.dependencies.vitest); console.log('vitest:', v||'(not found)')" "$p" || true
  fi
done

Repository: iii-hq/workers

Length of output: 2487


🌐 Web query:

Vitest 4.1 vi.mock factory top-level variable name prefix "mock" escape hatch call mockCall vi.hoisted

💡 Result:

Vitest 4.1 behavior/answer: when you use vi.mock(path, factory), the vi.mock call is hoisted to the top of the file, so variables from outside the factory that you reference in the factory are not available the way you might expect. To safely create and access values used inside the mock factory, define them with vi.hoisted (and pass them through), or use vi.doMock if you truly need non-hoisted access. What you’re asking about (top-level variable name prefix “mock”, escape hatch “mockCall vi.hoisted”): - The documentation does not define any special variable name prefix (like “mock”) that affects hoisting. Hoisting is about where vi.mock is moved/executed relative to imports and module evaluation, not about naming conventions. Correct pattern (use vi.hoisted for shared values used by the vi.mock factory): - “The call to vi.mock is hoisted … It will always be executed before all imports. If you need to reference some variables outside of its scope, you can define them inside vi.hoisted and reference them inside vi.mock.” [1][2] - “This also means that you cannot use any variables inside the factory that are defined outside the factory.” [1][2] Why (mechanics): - vi.mock is hoisted “to top of the file” so it runs before imports resolve; because of that, closures/outer variables referenced by the factory may be in the temporal dead zone until initialized, or otherwise unavailable. The official guidance is to use vi.hoisted for values you need inside the factory. [1][3] If you need a non-hoisted escape hatch: - Use vi.doMock instead; it “isn’t hoisted … It works the same way but isn’t hoisted.” [1][2]

Citations:


🌐 Web query:

Vitest 4.1 syntax for vi.hoisted: const x = vi.hoisted(() => ...) example

💡 Result:

In Vitest 4.1, the vi.hoisted method is used to execute code before all static import statements and vi.mock calls [1][2]. Because static imports are hoisted to the top of the file in ES modules, standard code defined before them would normally execute after those imports [1][2]. vi.hoisted allows you to bypass this limitation—for example, to set up mocks or system configurations (like system time) that need to be in place before the module is imported [1][2][3]. The method returns the value returned by the factory function, which you can then use in your vi.mock factory to reference local variables [1][2]. ### Example Syntax Here is how to use vi.hoisted to create a mock function and reference it within a vi.mock call [1][2]: ts import { expect, vi } from 'vitest' import { originalMethod } from './path/to/module.js' // Define the mock using vi.hoisted const { mockedMethod } = vi.hoisted(() => { return { mockedMethod: vi.fn() } }) // Reference the hoisted mock inside vi.mock vi.mock('./path/to/module.js', () => { return { originalMethod: mockedMethod } }) // Use the mock in your tests mockedMethod.mockReturnValue(100) expect(originalMethod()).toBe(100) ### Key Considerations - Scope: vi.hoisted must be called at the top level of your module [4]. - Imports: You cannot access standard imported variables inside the vi.hoisted factory because they are not yet defined [1][2]. If you absolutely need to access another module, you must use a dynamic import() inside the factory [1][2]. - Asynchrony: vi.hoisted can be called asynchronously if needed, even if the environment does not support top-level await [1][2]. - Alternatives: It is generally discouraged to import modules inside vi.hoisted; if you need to perform initialization before tests run, consider executing that logic directly within the imported module itself [1][2].

Citations:


Fix Vitest vi.mock hoisting issue in traces.test.ts

vi.mock is hoisted above const call = vi.fn(), so the mock factory closes over call before it’s initialized, which can cause Vitest’s hoisting/TZ errors. Create call with vi.hoisted so it’s available inside the factory.

🐛 Proposed fix
-const call = vi.fn()
+const call = vi.hoisted(() => vi.fn())
 vi.mock('`@/lib/iii-client`', () => ({
   getIiiClient: vi.fn(async () => ({ call })),
 }))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const call = vi.fn()
vi.mock('@/lib/iii-client', () => ({
getIiiClient: vi.fn(async () => ({ call })),
}))
const call = vi.hoisted(() => vi.fn())
vi.mock('`@/lib/iii-client`', () => ({
getIiiClient: vi.fn(async () => ({ call })),
}))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/pages/Traces/api/traces.test.ts` around lines 8 - 11, The
test's vi.mock factory is hoisted before `const call = vi.fn()` so it closes
over an uninitialized `call`; fix by declaring `call` using `vi.hoisted` (e.g.
`const call = vi.hoisted(() => vi.fn())`) so the mock factory in the
`getIiiClient` mock can access the initialized `call`; update the
`traces.test.ts` file so `call` is created with `vi.hoisted` and the existing
mock for `getIiiClient` continues to return an object that references `call`.

Comment on lines 120 to 126
} else {
setTraceListItems([])
setHasOtelConfigured(false)
// Reset the dedup state so a later non-empty fetch is detected as
// fresh (otherwise the fingerprint/new-trace diff would be stale).
fingerprintRef.current = ''
prevTraceIdsRef.current = new Set()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear pending traces when spans become empty.

When the fetch returns no spans, pendingTracesRef is not cleared. If the user was hovering during a previous non-empty fetch, those stale pending traces will be flushed when the user stops hovering, potentially showing outdated data that contradicts the latest empty response.

Additionally, setHasOtelConfigured(true) in flushPendingTraces (line 132) is redundant since the useEffect at line 93 already sets the correct value based on exporterDisabled.

Proposed fix
     } else {
       setTraceListItems([])
       // Reset the dedup state so a later non-empty fetch is detected as
       // fresh (otherwise the fingerprint/new-trace diff would be stale).
       fingerprintRef.current = ''
       prevTraceIdsRef.current = new Set()
+      pendingTracesRef.current = null
     }

And in flushPendingTraces:

   const flushPendingTraces = () => {
     if (pendingTracesRef.current) {
       setTraceListItems(pendingTracesRef.current)
-      setHasOtelConfigured(true)
       pendingTracesRef.current = null
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
setTraceListItems([])
setHasOtelConfigured(false)
// Reset the dedup state so a later non-empty fetch is detected as
// fresh (otherwise the fingerprint/new-trace diff would be stale).
fingerprintRef.current = ''
prevTraceIdsRef.current = new Set()
}
} else {
setTraceListItems([])
// Reset the dedup state so a later non-empty fetch is detected as
// fresh (otherwise the fingerprint/new-trace diff would be stale).
fingerprintRef.current = ''
prevTraceIdsRef.current = new Set()
pendingTracesRef.current = null
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/pages/Traces/hooks/useTraceData.ts` around lines 120 - 126,
When a fetch returns no spans the code clears traceList items and fingerprintRef
but fails to clear pendingTracesRef, allowing stale pending traces to be flushed
later; update the empty-spans branch to also clear pendingTracesRef.current
(e.g., set to new Map() or empty array consistent with its type) and remove the
redundant setHasOtelConfigured(true) call inside flushPendingTraces since
useEffect already sets hasOtelConfigured based on exporterDisabled; change
references to pendingTracesRef and flushPendingTraces accordingly so the
hover/flush path no longer displays outdated traces.

@ytallo ytallo closed this Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant