Skip to content
This repository was archived by the owner on Mar 5, 2026. It is now read-only.

feat: add runtime configuration system and enhance trace filtering#2

Merged
andersonleal merged 2 commits intomainfrom
feat/improvements
Feb 9, 2026
Merged

feat: add runtime configuration system and enhance trace filtering#2
andersonleal merged 2 commits intomainfrom
feat/improvements

Conversation

@andersonleal
Copy link
Copy Markdown
Contributor

@andersonleal andersonleal commented Feb 6, 2026

feat: add runtime configuration system and enhance trace filtering

Summary

  • Runtime configuration: Config is now loaded at app startup via a new ConfigProvider that fetches /api/config from the Rust server. The frontend no longer relies on window.__CONSOLE_CONFIG__ or build-time env vars for API base URLs. All API modules use getters (getDevtoolsApi(), getManagementApi(), getStreamsWs()) so URLs are correct in both dev and binary mode.
  • Trace filtering and tree API: Traces list supports richer filtering and sorting; new trace-tree endpoint and types (SpanTreeNode, TraceTreeResponse, fetchTraceTree, otelTraceTreeQuery) for hierarchical trace data. TraceFilters.tsx and related components (e.g. AttributesFilter, useTraceFilters, traceTransform) were refactored and extended.
  • Rust server: Added GET /api/config that returns engine host/port, WS port, console port, and version; proxy and config wiring updated so the embedded frontend receives config from the server.
  • Docs and housekeeping: README API table updated (streams → states paths); .gitignore updated (e.g. AGENTS.md); bridge functions/triggers and route handlers aligned with new config and trace APIs.

Type of Change

  • Feature (new runtime config system, trace tree API, enhanced trace filters)
  • Bug fix
  • Refactor (non-breaking)
  • Documentation only
  • Other

Checklist

  • Code builds and runs locally
  • Lint/format checks pass (Biome)
  • Tests added/updated (no test framework configured per project docs)
  • README/docs updated where relevant

Additional Context

  • Config flow: ConfigProvider mounts first, fetches /api/config, calls setConfig(), then renders children. Loading and error states (spinner, retry with auto-retry) are handled in the provider. The Rust server serves /api/config with data from CLI/env so the same binary works in different environments.
  • Trace changes: fetchTraces now accepts filter/sort params; fetchTraceTree(traceId) and otelTraceTreeQuery(traceId) support the new tree endpoint. Traces UI and filters were refactored to use the new API shape and filter state.
  • Breaking: Any code that imported DEVTOOLS_API, MANAGEMENT_API, STREAMS_WS, ENGINE_HOST, etc. must switch to getDevtoolsApi(), getManagementApi(), getStreamsWs(), or getConfig(), and the app must be wrapped in ConfigProvider (already done in main.tsx).

Summary by CodeRabbit

  • New Features

    • Runtime configuration endpoint and in-app config provider for dynamic engine/port display
    • Trace tree API and hierarchical trace view with tree-based waterfall visualization
    • Redesigned trace filters and attribute editor with staged edits and apply/reset controls
  • Bug Fixes

    • Stronger validation for custom date ranges
  • Improvements

    • Footer and various UIs now show live engine/port values
    • Config fetch auto-retry with user-facing error/retry UI

Major changes:
- Implement runtime config system with /api/config endpoint and ConfigProvider
- Migrate all API modules from static constants to dynamic config getters
- Add trace tree visualization with SpanTreeNode and waterfall transformation
- Enhance trace filtering with draft state, apply button, and attribute suggestions
- Add pagination and advanced search to trace filters with click-outside detection
- Fix TimeRangeFilter validation to prevent NaN timestamps
- Fix Biome linting errors (variable ordering, React comment parsing)
- Update Rust server to serve runtime configuration

This establishes a proper configuration foundation and significantly improves
the trace analysis experience with better filtering and tree visualization.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

Replaces static frontend config with a runtime ConfigProvider and module-scoped ConsoleConfig; updates API modules to derive base URLs at runtime; adds OpenTelemetry trace-tree support end-to-end (new handler, trigger, types, fetchTraceTree, treeToWaterfallData); backend exposes /api/config and bridge trace-tree handler.

Changes

Cohort / File(s) Summary
Config runtime & provider
packages/console-frontend/src/api/config.ts, packages/console-frontend/src/api/config-provider.tsx, packages/console-frontend/src/api/index.ts, packages/console-frontend/src/main.tsx
Introduce ConsoleConfig, setConfig/getters (getDevtoolsApi/getManagementApi/getStreamsWs), ConfigProvider + useConfig, export adjustments, and wrap app with ConfigProvider.
API modules — base URL migration
packages/console-frontend/src/api/...
packages/console-frontend/src/api/alerts/*, .../events/*, .../observability/*, .../state/*, .../system/*, .../utils.ts, .../queries.ts
Replace DEVTOOLS_API/MANAGEMENT_API/STREAMS_WS constants with getter functions and update URL construction; add fetchTraceTree and otelTraceTreeQuery. One util (fetchWithFallback) behavior tightened to only call devtools.
Trace tree types & transforms
packages/console-frontend/src/api/observability/traces.ts, packages/console-frontend/src/lib/traceTransform.ts
Add SpanTreeNode and TraceTreeResponse types, fetchTraceTree API, and treeToWaterfallData transform (tree → waterfall flattening).
Traces UI & hooks
packages/console-frontend/src/routes/traces.tsx, packages/console-frontend/src/hooks/useTraceFilters.ts
Refactor traces page to use trace trees, treeToWaterfallData, pagination, fetchTraceTree; useTraceFilters memoizes apiParams and exposes getApiParams. Significant UI/flow changes.
Filters & attributes UX
packages/console-frontend/src/components/traces/TraceFilters.tsx, packages/console-frontend/src/components/traces/AttributesFilter.tsx, packages/console-frontend/src/components/filters/TimeRangeFilter.tsx
TraceFilters: popover-driven controls, search/stats props, active chips. AttributesFilter: draft state, suggestions, apply flow. TimeRangeFilter: validation and layout change (removed defaultPreset prop).
Routes & pages config usage
packages/console-frontend/src/routes/config.tsx, .../handlers.tsx, .../index.tsx, .../streams.tsx, .../states.tsx
Pages and handlers switched to use useConfig/getConfig for dynamic engine/ws ports and endpoints; streams subscription keying changed to composite key; some cleanup of query client usage.
Frontend minor updates
packages/console-frontend/src/components/layout/Sidebar.tsx, .../traces/SpanLogsTab.tsx, .../traces/TraceMap.tsx, packages/console-frontend/vite.config.ts, packages/console-frontend/src/vite-env.d.ts
Sidebar footer shows dynamic version/host:port; trace timestamp scaled; TraceMap optimized with spanById; vite import modernized; VITE env names renamed to VITE_III_*.
Backend: config & trace-tree
packages/console-rust/src/server.rs, packages/console-rust/src/bridge/functions.rs, packages/console-rust/src/bridge/triggers.rs, packages/console-rust/src/main.rs
Add /api/config endpoint and serve_config handler; remove handle_config; add handle_otel_traces_tree and register console.otel_traces_tree trigger; add bridge_port CLI arg; tighten CORS origins.
Docs & ignore
README.md, .gitignore
README API endpoint paths updated (/ _console/streams → /_console/states) and .gitignore adds AGENTS.md.

Sequence Diagram(s)

sequenceDiagram
    participant App as React App
    participant CP as ConfigProvider
    participant API as Server (/api/config)
    participant Context as ConfigContext
    participant Component as Consumer

    App->>CP: Mount ConfigProvider
    CP->>API: GET /api/config (with timeout/abort)
    API-->>CP: 200 JSON config
    CP->>CP: setConfig(...) & set loaded
    CP->>Context: provide config
    Component->>Context: useConfig()
    Context-->>Component: ConsoleConfig
Loading
sequenceDiagram
    participant UI as Trace Detail View
    participant Hook as useTraceFilters
    participant API as frontend fetchTraceTree
    participant Backend as Server (/otel/traces/tree)
    participant Transform as treeToWaterfallData

    UI->>Hook: request apiParams / trigger fetch
    UI->>API: fetchTraceTree(traceId)
    API->>Backend: POST /_console/otel/traces/tree (traceId)
    Backend-->>API: TraceTreeResponse (SpanTreeNode[] roots)
    API-->>UI: TraceTreeResponse
    UI->>Transform: treeToWaterfallData(roots)
    Transform-->>UI: WaterfallData
    UI->>UI: render waterfall
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped from constants into runtime light,
Fetching configs by day and retrying by night.
Traces now branch from root to leaf so free,
A rabbit applauds this dynamic, lively tree! 🌿✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately reflects the two main changes: introducing a runtime configuration system and enhancing trace filtering capabilities.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/improvements

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/console-frontend/src/api/observability/metrics.ts (1)

145-165: ⚠️ Potential issue | 🟠 Major

Redundant fallback: both try and catch paths call the same API.

In fetchRollups, both the try block (line 146) and the fallback (line 158) call getManagementApi(). This appears to be a copy-paste error — the try block should likely use getDevtoolsApi() to match the fallback pattern used in other functions like fetchDetailedMetrics.

🐛 Proposed fix
   try {
-    const res = await fetch(`${getManagementApi()}/rollups`, {
+    const res = await fetch(`${getDevtoolsApi()}/rollups`, {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify(body),
     })
     if (res.ok) {
       return unwrapResponse<RollupsResponse>(res)
     }
   } catch {
-    // Fall through to management API
+    // Fall through to management API
   }
🤖 Fix all issues with AI agents
In `@packages/console-frontend/src/api/config-provider.tsx`:
- Around line 60-87: The auto-retry useEffect can call setError, setConfig and
setConfigState after the component unmounts; add a cancellation guard and proper
cleanup: inside the effect create a boolean (e.g., isMounted or cancelled) and
set it to false in the cleanup, call controller.abort() and clear both timer and
timeout in cleanup, and before any state updates in the fetch handlers check the
flag (only call setError, setConfig, setConfigState when not cancelled). Ensure
you still use AbortController for the fetch and clear the request timeout
(timeout) when the request completes or is aborted to avoid leaked timers.

In `@packages/console-frontend/src/api/utils.ts`:
- Line 29: The current fallback retry uses getManagementApi() which returns the
same URL as getDevtoolsApi(), making the fallback useless; update the fetch
logic in the utility that calls getDevtoolsApi() (the request that assigns to
`res` and the subsequent fallback branch) to either call a truly distinct
endpoint from getManagementApi() (if intended) or remove the fallback/retry
entirely so the original error is surfaced. Locate the fetch call that uses
getDevtoolsApi() and the fallback branch that invokes getManagementApi(), then
either change getManagementApi() to return the correct alternate endpoint or
delete the fallback branch and propagate the initial fetch error.

In `@packages/console-frontend/src/components/traces/TraceFilters.tsx`:
- Around line 146-150: In handleDurationApply validate the parsed integers from
tempMinDuration and tempMaxDuration before calling onFilterChange: parse each
with Number.parseInt (or Number()) into local variables, check Number.isFinite
or !Number.isNaN, and only call onFilterChange('minDurationMs' /
'maxDurationMs', parsedValue) when valid otherwise pass null; keep
setOpenPopover(null) behavior. Ensure you reference the existing
handleDurationApply function, tempMinDuration/tempMaxDuration inputs, and
onFilterChange handler when making this change.
- Around line 99-103: The temporary input state (tempServiceName,
tempOperationName, tempMinDuration, tempMaxDuration) isn’t updated when the
filters prop changes, causing stale values; update these temp state values
whenever filters change or when the popover opens: either add a useEffect that
watches filters and calls setTempServiceName(filters.serviceName || ''),
setTempOperationName(filters.operationName || ''),
setTempMinDuration(filters.minDurationMs?.toString() || ''),
setTempMaxDuration(filters.maxDurationMs?.toString() || ''), or reset those
temps inside the existing togglePopover handler when opening a popover (check
openPopover !== popover) so inputs reflect the current filters before the user
edits them.

In `@packages/console-rust/src/server.rs`:
- Around line 134-144: The origins vector used to build the CorsLayer only
includes localhost/127.0.0.1 so requests from a configured host are blocked;
update the origins Vec (the variable origins used by
CorsLayer::new().allow_origin(...)) to also push formatted origins using
config.host and config.port (e.g. format!("http://{}:{}", config.host,
config.port) and the https variant) and handle the special case where
config.host is "0.0.0.0" by substituting a resolvable hostname such as
"localhost" or skipping adding that raw origin; ensure the new origins are
parsed into HeaderValue and included before calling .allow_origin(origins).
🧹 Nitpick comments (9)
packages/console-rust/src/main.rs (1)

33-36: LGTM! Consider logging the bridge port for operational visibility.

The new bridge_port CLI option correctly follows the existing pattern for port arguments and enables configurable bridge connectivity.

However, the startup log at lines 78-81 only displays engine_port and ws_port. Consider adding bridge_port to the log output for better operational visibility:

💡 Suggestion to improve operational logging
     info!(
-        "Connecting to engine at {}:{} (WS: {})",
-        args.engine_host, args.engine_port, args.ws_port
+        "Connecting to engine at {}:{} (WS: {}, Bridge: {})",
+        args.engine_host, args.engine_port, args.ws_port, args.bridge_port
     );
packages/console-frontend/src/hooks/useTraceFilters.ts (1)

215-217: Consider deriving warnings directly instead of syncing via effect.

The useEffect that syncs computedWarnings to validationWarnings state introduces an extra render cycle. Since computedWarnings is already memoized, you could either:

  1. Return computedWarnings directly from the hook (renaming as needed)
  2. Keep validationWarnings state only if external clearing via clearValidationWarnings is required

Current implementation is functionally correct but adds a minor render overhead.

♻️ Optional: Return computed warnings directly
-  const [validationWarnings, setValidationWarnings] = useState<{
-    durationSwapped?: boolean
-    timeRangeSwapped?: boolean
-  }>({})
+  const [manuallyCleared, setManuallyCleared] = useState(false)

...

-  useEffect(() => {
-    setValidationWarnings(computedWarnings)
-  }, [computedWarnings])

+  // Reset cleared state when filters change
+  useEffect(() => {
+    setManuallyCleared(false)
+  }, [filters])

+  const validationWarnings = manuallyCleared ? {} : computedWarnings

   const clearValidationWarnings = useCallback(() => {
-    setValidationWarnings({})
+    setManuallyCleared(true)
   }, [])
packages/console-frontend/src/routes/streams.tsx (1)

177-181: Update stale comment to reflect composite key structure.

The comment on line 179 states Map<streamName, {groupId, subscriptionId}>, but the actual key is now a composite ${streamName}:${groupId}. This discrepancy could confuse future maintainers.

📝 Suggested comment fix
   const subscriptionsRef = useRef<Map<string, { groupId: string; subscriptionId: string }>>(
     new Map(),
-  ) // Map<streamName, {groupId, subscriptionId}>
+  ) // Map<`${streamName}:${groupId}`, {groupId, subscriptionId}>
packages/console-frontend/src/routes/handlers.tsx (1)

627-636: Consider caching getConfig() result to avoid redundant calls.

Lines 627-628 call getConfig() twice within the same JSX expression. While this works correctly, destructuring once improves readability and slight efficiency.

💡 Optional: Destructure config once in the render scope

Since this is inside an IIFE that already has access to trigger and other computed values, you could destructure once at the start of that block:

 {(() => {
   const trigger = getAssociatedTrigger(selectedFunction)
   const apiPath = getApiPath(selectedFunction)
+  const { engineHost, enginePort } = getConfig()
+  const baseUrl = `${window.location.protocol}//${engineHost}:${enginePort}`

   return (
     <>
       {/* ... */}
       <code className="flex-1 text-xs font-mono bg-black/40 text-cyan-400 px-3 py-2 rounded border border-cyan-500/20">
-        {getHttpMethod(selectedFunction)} {window.location.protocol}{'//'}
-        {getConfig().engineHost}:{getConfig().enginePort}/{apiPath}
+        {getHttpMethod(selectedFunction)} {baseUrl}/{apiPath}
       </code>
       <button
         onClick={() => {
-          const { engineHost, enginePort } = getConfig()
           copyToClipboard(
-            `${window.location.protocol}//${engineHost}:${enginePort}/${apiPath}`,
+            `${baseUrl}/${apiPath}`,
             'endpoint',
           )
         }}
packages/console-frontend/src/components/traces/TraceFilters.tsx (3)

63-76: Consider stabilizing the useClickOutside callback with useCallback.

The onClickOutside callback passed to useClickOutside is recreated on every render (e.g., () => openPopover === 'status' && setOpenPopover(null)). This causes the effect to re-run on each render since it's in the dependency array.

💡 Stabilize with useCallback or move condition inside the hook

Option 1: Move the condition inside the hook so consumers pass a stable setter:

-function useClickOutside(ref: React.RefObject<HTMLElement | null>, onClickOutside: () => void) {
+function useClickOutside(
+  ref: React.RefObject<HTMLElement | null>,
+  onClickOutside: () => void,
+  enabled: boolean = true
+) {
   useEffect(() => {
     function handleClickOutside(event: MouseEvent) {
-      if (ref.current && !ref.current.contains(event.target as Node)) {
+      if (enabled && ref.current && !ref.current.contains(event.target as Node)) {
         onClickOutside()
       }
     }
     // ...
-  }, [ref, onClickOutside])
+  }, [ref, onClickOutside, enabled])
 }

// Usage:
-useClickOutside(statusRef, () => openPopover === 'status' && setOpenPopover(null))
+useClickOutside(statusRef, () => setOpenPopover(null), openPopover === 'status')

Option 2: Wrap callbacks in useCallback where they're defined.


78-83: formatDuration is duplicated in traces.tsx.

This same function exists in packages/console-frontend/src/routes/traces.tsx (lines 73-78). Consider extracting to a shared utility module (e.g., @/lib/formatters or @/lib/traceTransform).


227-255: Consider memoizing activeFilters array.

The activeFilters array is reconstructed on every render. While the array is small, wrapping it in useMemo with appropriate dependencies would prevent unnecessary recalculations during frequent re-renders.

packages/console-frontend/src/routes/traces.tsx (1)

110-111: Consider reducing the default limit or implementing lazy loading.

A default limit of 10,000 traces could cause performance issues with memory usage and rendering time on the client. Consider:

  • A lower default (e.g., 1000) with load-more functionality
  • Server-side pagination
  • Virtual scrolling for large lists
packages/console-frontend/src/api/config.ts (1)

27-37: getDevtoolsApi and getManagementApi return identical URLs despite different usage contexts.

Both functions return ${protocol}//${c.engineHost}:${c.enginePort}/_console. While they're used across different modules (getDevtoolsApi more extensively, getManagementApi for specific operations), they produce the same result. If this semantic distinction is intentional, add a comment explaining the purpose. Otherwise, one could delegate to the other to avoid duplication.

…er and TraceFilters

- Improve error handling in ConfigProvider to prevent state updates after component unmount
- Synchronize temporary state in TraceFilters when external filters change
- Refactor fetchWithFallback to simplify error handling and remove unused management API fetch
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/console-frontend/src/api/utils.ts`:
- Around line 23-33: Rename the exported function fetchWithFallback to a clear
name like fetchFromDevtools and keep fetchWithFallback as a deprecated alias
that forwards to the new function for backward compatibility; remove the unused
_managementPath parameter from the new function signature and update internal
references to call unwrapResponse via the new function name, and mark the old
export (fetchWithFallback) with a deprecation comment and have it call
fetchFromDevtools so existing callers still work while signaling the new public
API.

In `@packages/console-frontend/src/components/traces/TraceFilters.tsx`:
- Around line 78-83: The function formatDuration treats 0 as falsy and returns
'-' for 0ms; change the initial falsy check to only detect undefined (or null)
so zero is handled correctly. In the formatDuration function replace the "if
(!ms) return '-'" check with a check for undefined (e.g., "if (ms === undefined)
return '-'") so formatDuration(0) returns "0ms" while preserving existing
behavior for undefined inputs.
🧹 Nitpick comments (3)
packages/console-frontend/src/api/utils.ts (1)

29-30: Consider including full URL in error message for easier debugging.

Since the base URL is now dynamic, logging only the path may make debugging harder. Including the full URL would provide better context.

🔧 Proposed improvement
+  const url = `${getDevtoolsApi()}${devtoolsPath}`
+  const res = await fetch(url, options)
-  const res = await fetch(`${getDevtoolsApi()}${devtoolsPath}`, options)
   if (!res.ok) {
-    throw new Error(`Failed to fetch from ${devtoolsPath}: ${res.status}`)
+    throw new Error(`Failed to fetch from ${url}: ${res.status}`)
   }
packages/console-frontend/src/components/traces/TraceFilters.tsx (2)

63-76: useClickOutside re-registers listeners on every render due to inline callbacks.

The hook includes onClickOutside in its dependency array. Since the callbacks passed on lines 121-126 are inline arrow functions, they're recreated on each render, causing the effect to repeatedly add/remove event listeners.

Consider either memoizing the callbacks or restructuring the hook to accept the popover type and close action separately:

♻️ Option 1: Restructure hook to avoid inline callbacks
-function useClickOutside(ref: React.RefObject<HTMLElement | null>, onClickOutside: () => void) {
+function useClickOutside(
+  ref: React.RefObject<HTMLElement | null>,
+  isActive: boolean,
+  onClickOutside: () => void
+) {
   useEffect(() => {
+    if (!isActive) return
+
     function handleClickOutside(event: MouseEvent) {
       if (ref.current && !ref.current.contains(event.target as Node)) {
         onClickOutside()
       }
     }

     document.addEventListener('mousedown', handleClickOutside)
     return () => {
       document.removeEventListener('mousedown', handleClickOutside)
     }
-  }, [ref, onClickOutside])
+  }, [ref, isActive, onClickOutside])
 }

Then use a stable callback:

const closePopover = useCallback(() => setOpenPopover(null), [])

useClickOutside(statusRef, openPopover === 'status', closePopover)
useClickOutside(timeRangeRef, openPopover === 'timeRange', closePopover)
// ...

Also applies to: 121-126


338-353: Popovers lack keyboard accessibility (Escape to close).

Users cannot close open popovers with the Escape key, which is a common accessibility expectation. Consider adding a keydown handler at the popover container level.

♿ Add Escape key handling
+  useEffect(() => {
+    const handleEscape = (e: KeyboardEvent) => {
+      if (e.key === 'Escape' && openPopover) {
+        setOpenPopover(null)
+      }
+    }
+    document.addEventListener('keydown', handleEscape)
+    return () => document.removeEventListener('keydown', handleEscape)
+  }, [openPopover])

Add this effect near the other useEffect hooks (around line 111).

Comment on lines 23 to 33
export async function fetchWithFallback<T>(
devtoolsPath: string,
managementPath: string,
_managementPath?: string,
options?: RequestInit,
): Promise<T> {
try {
const res = await fetch(`${DEVTOOLS_API}${devtoolsPath}`, options)
if (res.ok) {
return await unwrapResponse<T>(res)
}
} catch {
// Fall through to management API
const res = await fetch(`${getDevtoolsApi()}${devtoolsPath}`, options)
if (!res.ok) {
throw new Error(`Failed to fetch from ${devtoolsPath}: ${res.status}`)
}

const res = await fetch(`${MANAGEMENT_API}${managementPath}`, options)
if (!res.ok) throw new Error(`Failed to fetch from ${managementPath}`)
return await unwrapResponse<T>(res)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find all callers of fetchWithFallback outside utils.ts
rg -n 'fetchWithFallback' --type=ts -g '!**/utils.ts'

Repository: MotiaDev/iii-console

Length of output: 170


🏁 Script executed:

# Also check imports of the function to understand how it's being used
rg -n 'import.*fetchWithFallback' --type=ts

Repository: MotiaDev/iii-console

Length of output: 46


🏁 Script executed:

# Check if _managementPath is referenced anywhere in the codebase
rg -n '_managementPath' --type=ts

Repository: MotiaDev/iii-console

Length of output: 137


🏁 Script executed:

# Check for test files that might call fetchWithFallback
rg -n 'fetchWithFallback' --type=ts

Repository: MotiaDev/iii-console

Length of output: 260


🏁 Script executed:

# Check if there are calls to this function through string patterns or dynamic imports
rg 'fetchWithFallback' --type=ts -A 3

Repository: MotiaDev/iii-console

Length of output: 633


🏁 Script executed:

# Verify the exact location and context of the export in index.ts
cat -n packages/console-frontend/src/api/index.ts | head -120 | tail -20

Repository: MotiaDev/iii-console

Length of output: 720


Function name fetchWithFallback no longer reflects behavior, and the function is part of the public API.

The fallback logic has been removed (correctly addressing the previous review about identical endpoints), but the function name is now misleading since there's no fallback mechanism. Additionally, the _managementPath parameter is unused dead code that never appears elsewhere in the codebase.

Since this function is exported as part of the public API (via index.ts), renaming it with a deprecated alias for backward compatibility is the right approach. Consider renaming to fetchFromDevtools (or similar) and deprecating the old name rather than directly removing the parameter.

♻️ Proposed refactor
-export async function fetchWithFallback<T>(
-  devtoolsPath: string,
-  _managementPath?: string,
-  options?: RequestInit,
-): Promise<T> {
+export async function fetchFromDevtools<T>(
+  devtoolsPath: string,
+  options?: RequestInit,
+): Promise<T> {
   const res = await fetch(`${getDevtoolsApi()}${devtoolsPath}`, options)
   if (!res.ok) {
     throw new Error(`Failed to fetch from ${devtoolsPath}: ${res.status}`)
   }
   return await unwrapResponse<T>(res)
 }
+
+/** `@deprecated` Use fetchFromDevtools instead */
+export const fetchWithFallback = fetchFromDevtools
📝 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
export async function fetchWithFallback<T>(
devtoolsPath: string,
managementPath: string,
_managementPath?: string,
options?: RequestInit,
): Promise<T> {
try {
const res = await fetch(`${DEVTOOLS_API}${devtoolsPath}`, options)
if (res.ok) {
return await unwrapResponse<T>(res)
}
} catch {
// Fall through to management API
const res = await fetch(`${getDevtoolsApi()}${devtoolsPath}`, options)
if (!res.ok) {
throw new Error(`Failed to fetch from ${devtoolsPath}: ${res.status}`)
}
const res = await fetch(`${MANAGEMENT_API}${managementPath}`, options)
if (!res.ok) throw new Error(`Failed to fetch from ${managementPath}`)
return await unwrapResponse<T>(res)
}
export async function fetchFromDevtools<T>(
devtoolsPath: string,
options?: RequestInit,
): Promise<T> {
const res = await fetch(`${getDevtoolsApi()}${devtoolsPath}`, options)
if (!res.ok) {
throw new Error(`Failed to fetch from ${devtoolsPath}: ${res.status}`)
}
return await unwrapResponse<T>(res)
}
/** `@deprecated` Use fetchFromDevtools instead */
export const fetchWithFallback = fetchFromDevtools
🤖 Prompt for AI Agents
In `@packages/console-frontend/src/api/utils.ts` around lines 23 - 33, Rename the
exported function fetchWithFallback to a clear name like fetchFromDevtools and
keep fetchWithFallback as a deprecated alias that forwards to the new function
for backward compatibility; remove the unused _managementPath parameter from the
new function signature and update internal references to call unwrapResponse via
the new function name, and mark the old export (fetchWithFallback) with a
deprecation comment and have it call fetchFromDevtools so existing callers still
work while signaling the new public API.

Comment on lines +78 to +83
function formatDuration(ms: number | undefined): string {
if (!ms) return '-'
if (ms < 1) return '<1ms'
if (ms < 1000) return `${Math.round(ms)}ms`
return `${(ms / 1000).toFixed(2)}s`
}
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

formatDuration(0) returns '-' instead of '0ms'.

The falsy check !ms catches 0, returning '-'. If 0ms is a valid average duration (e.g., cached/instant responses), it should display as '0ms'.

🔧 Proposed fix
 function formatDuration(ms: number | undefined): string {
-  if (!ms) return '-'
+  if (ms === undefined || ms === null) return '-'
+  if (ms === 0) return '0ms'
   if (ms < 1) return '<1ms'
   if (ms < 1000) return `${Math.round(ms)}ms`
   return `${(ms / 1000).toFixed(2)}s`
 }
📝 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
function formatDuration(ms: number | undefined): string {
if (!ms) return '-'
if (ms < 1) return '<1ms'
if (ms < 1000) return `${Math.round(ms)}ms`
return `${(ms / 1000).toFixed(2)}s`
}
function formatDuration(ms: number | undefined): string {
if (ms === undefined || ms === null) return '-'
if (ms === 0) return '0ms'
if (ms < 1) return '<1ms'
if (ms < 1000) return `${Math.round(ms)}ms`
return `${(ms / 1000).toFixed(2)}s`
}
🤖 Prompt for AI Agents
In `@packages/console-frontend/src/components/traces/TraceFilters.tsx` around
lines 78 - 83, The function formatDuration treats 0 as falsy and returns '-' for
0ms; change the initial falsy check to only detect undefined (or null) so zero
is handled correctly. In the formatDuration function replace the "if (!ms)
return '-'" check with a check for undefined (e.g., "if (ms === undefined)
return '-'") so formatDuration(0) returns "0ms" while preserving existing
behavior for undefined inputs.

@andersonleal andersonleal merged commit 0c6022c into main Feb 9, 2026
2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant