Skip to content

Dashboard: 60s auto-refresh flashes skeletons and clears cards #175

@arniesaha

Description

@arniesaha

Symptom

Every ~60s the dashboard appears to "reset": stat cards (LLM Calls, Total Cost, Cache Hit Rate, Avg Latency, Total Routed, Redirected, Kept Original, Unique Reasons) drop back to ... placeholders, charts become skeletons, and the Routing Reasons list collapses to skeleton bars. After the fetch finishes, values reappear. This makes the dashboard feel broken during every background refresh.

Reproduces on Overview and Routing tabs; also affects Sessions / Replay data loads.

Root cause

In every Tempo/Prometheus hook (dashboard/src/hooks/useTempo.ts, dashboard/src/hooks/usePrometheus.ts), the refetch pattern is:

const fetch_ = useCallback(async () => {
  setLoading(true)
  setError(null)
  try {
    // ...
    setTraces(data.traces ?? [])
  } catch (e) {
    setError(...)
    setTraces([])
  } finally {
    setLoading(false)
  }
}, [query, timeRange, refreshKey])

Two issues:

  1. setLoading(true) fires on every refetch (including the 60s auto-refresh), not just the initial load.
  2. On error, setTraces([]) blows away the previously fetched data.

Consumers render empty/skeleton state whenever loading is true (App.tsx: const llmCallCount = tracesLoading ? null : traceRows.length), so every 60s the UI flashes through its empty state. Since several hooks (useTempoSearch x3 instances + usePrometheus*) refetch simultaneously on refreshKey bump from App.tsx:64-70, the whole page flashes together.

Proposed fix — stale-while-revalidate

Don't throw away data during background refetches:

  1. Split loading into two flags:
    • loading — true only while we have no data yet (initial load)
    • refetching — true during any in-flight refetch after we already have data
  2. Never clear traces/count on refetch start or on error — keep last-good data on screen.
  3. Consumers render skeletons only when loading && !hasData; during refetching show a subtle indicator (maybe the existing refresh button spinner) but keep the cards.
  4. Optionally: expose staleAt / refetchedAt so cards can show "last updated Xs ago" without forcing a loading state.

Affected files

  • dashboard/src/hooks/useTempo.tsuseTempoSearch, useTempoSearchCount, useSessionGraph
  • dashboard/src/hooks/usePrometheus.ts — both hooks
  • dashboard/src/App.tsx:88-165 — consumers using tracesLoading gates
  • StatCards and any component doing loading ? <Skeleton/> : <Data/> — should become (loading && !data) ? ...

Nice-to-have follow-ups

  • Consider a tiny SWR-style cache keyed on (query, timeRange) so tab switches don't force a full refetch either.
  • Align refresh cadence with user activity (e.g. pause polling when tab is hidden via document.visibilityState).

Discovered while debugging #174.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions