Skip to content

CallDebugger Refactoring Design

Date: 2026-02-04 Status: Approved Branch: debugger-dashboard

Overview

Refactor the CallDebugger component to integrate with the new multi-sidebar dashboard, consolidating on a single clean implementation with improved URL structure and simplified data layer.

Goals

  • Remove redundant sidebar from CallDebugger (superseded by dashboard sidebars)
  • Clean, shareable URLs: /debug/call/<call_id>
  • Simplify data layer (remove CallStore, use hooks only)
  • Preserve mobile responsiveness with improved UX
  • Single roundtrip call lookup supporting both retell and arini IDs

Route Structure

Route Component Purpose
/debug CallsLandingPage Dashboard with org/location/call browsing
/debug/call/:callId CallDebugger Full-page call debugger
/d/:callId Server redirect Short URL → /debug/call/:callId

URL Patterns

Dashboard (browsing):

/debug                           → Today, prod (defaults)
/debug?env=dev                   → Today, dev
/debug?date=2026-02-03           → Specific date, prod
/debug?env=dev&date=2026-02-03   → Specific date, dev

Debugger (viewing call):

/debug/call/call_ee6fe8a59f...   → Canonical debugger URL

Short URL (sharing):

/d/call_ee6fe8a59f...            → Redirects to debugger
/d/0196003d-2f8a-7c29-...        → Arini call ID also works

  1. User on /debug clicks a call → navigates to /debug/call/<call_id>
  2. User clicks back → returns to /debug (dashboard state restored from sessionStorage)
  3. User visits /d/<call_id> → server redirects to /debug/call/<call_id>
  4. User visits /debug/call/<call_id> directly → lookup resolves env/date automatically

API Endpoint Changes

New Endpoint

GET /api/debug/db/calls/:id (auth required)

Returns call metadata, resolving either retell_call_id or arini_call_id:

{
  call_id: string           // retell_call_id
  arini_call_id: string     // arini internal ID
  date: string              // YYYY-MM-DD (for S3 artifact paths)
  env: "prod" | "dev"       // resolved environment
  agent_id: string | null
  agent_name: string | null
  direction: "inbound" | "outbound" | null
  created_at: string
}

SQL Query:

SELECT
  retell_call_id as call_id,
  arini_call_id,
  DATE(created_at) as date,
  'prod' as env,
  agent_id,
  agent_name,
  direction,
  created_at
FROM retell_calls
WHERE retell_call_id = :id OR arini_call_id = :id
LIMIT 1

Removed Endpoint

  • GET /api/debug/db/calls/lookup?id= - Superseded by new endpoint

Updated Public Paths

Remove /api/debug/db/calls/lookup from PUBLIC_PATHS in auth middleware. The redirect router calls the query function directly (server-side), no public endpoint needed.

CallDebugger Component

Removed

  • CallIdSidebar component and all sidebar state
  • useNavigate() for internal call selection
  • Legacy URL param handling (params.date, params.env)
  • handleSelectCall() callback
  • Sidebar toggle button, mobile overlay mask
  • useResizable hook for sidebar width

New Structure

function CallDebugger() {
  const { callId } = useParams<{ callId: string }>()

  // Single source: lookup call metadata from DB
  const { data: call, error, isLoading } = useCall(callId)

  // If not found in prod, show helpful error with dev suggestion
  if (error?.notFound) {
    return <CallNotFound callId={callId} />
  }

  // Once we have metadata, fetch call details
  const { env, date } = call
  const { allLogs, waterfallSpans, ... } = useCallData(callId, date, env)

  return (
    <div className="h-full flex flex-col">
      <CallDebuggerHeader
        call={call}
        collapsed={headerCollapsed}
        onToggle={() => setHeaderCollapsed(!headerCollapsed)}
      />
      <ViewTabs view={view} onViewChange={setView} />
      <div className="flex-1 overflow-hidden">
        {view === "turns" && <TurnExplorerView ... />}
        {view === "raw" && <RawLogsView ... />}
        {view === "waterfall" && <WaterfallView ... />}
        {view === "data" && <CallDataView ... />}
      </div>
    </div>
  )
}

Data Layer Simplification

Removed

  • CallDebuggerContext.tsx - Context provider
  • data/callStore.ts - LRU caches, deduplication logic
  • useCallDebuggerContext hook

New Utility

utils/deduplicatedFetch.ts:

const pending = new Map<string, Promise<unknown>>()

export async function deduplicatedFetch<T>(
  key: string,
  fetcher: () => Promise<T>
): Promise<T> {
  const existing = pending.get(key)
  if (existing) return existing as Promise<T>

  const promise = fetcher().finally(() => pending.delete(key))
  pending.set(key, promise)
  return promise
}

Hook Pattern

Each hook manages its own state, using deduplicatedFetch to prevent duplicate requests:

function useCall(callId: string) {
  const [data, setData] = useState<CallMetadata | null>(null)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    deduplicatedFetch(`call:${callId}`, () =>
      fetch(`/api/debug/db/calls/${callId}`).then(r => r.json())
    )
      .then(setData)
      .catch(setError)
  }, [callId])

  return { data, error, isLoading: !data && !error }
}

Dashboard State Preservation

Save dashboard context to sessionStorage before navigating to debugger:

utils/dashboardState.ts:

const STORAGE_KEY = 'debug:dashboard'

type DashboardState = {
  date: string
  env: Environment
  orgId: string | null
  locationId: string | null
  agentId: string | null
}

export function saveDashboardState(state: DashboardState) {
  sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state))
}

export function loadDashboardState(): DashboardState | null {
  const saved = sessionStorage.getItem(STORAGE_KEY)
  return saved ? JSON.parse(saved) : null
}

export function clearDashboardState() {
  sessionStorage.removeItem(STORAGE_KEY)
}

Restored on dashboard mount, cleared after restore (one-time).

Mobile Responsiveness

Collapsible Metadata Header (existing)

Keep current behavior - tap to expand/collapse call metadata on mobile.

Bottom Sheet for Turn Details (new)

On mobile, TurnExplorerView's detail panel becomes a swipe-up bottom sheet:

const isMobile = useIsMobile()

{selectedTurn && (
  isMobile ? (
    <BottomSheet open={!!selectedTurn} onClose={() => setSelectedTurn(null)}>
      <TurnDetailsPanel turn={selectedTurn} />
    </BottomSheet>
  ) : (
    <div className="w-1/2 border-l">
      <TurnDetailsPanel turn={selectedTurn} />
    </div>
  )
)}

BottomSheet features: - Swipe down to dismiss - Drag handle at top - Max height 80vh - Backdrop overlay

Error Handling

Call Not Found

When call isn't found in prod database, show helpful error with option to check dev:

function CallNotFound({ callId }: { callId: string }) {
  const [checkingDev, setCheckingDev] = useState(false)
  const [devResult, setDevResult] = useState<'found' | 'not-found' | null>(null)

  const handleTryDev = async () => {
    setCheckingDev(true)
    const exists = await checkDevCall(callId)
    setDevResult(exists ? 'found' : 'not-found')
    setCheckingDev(false)
  }

  return (
    <div className="flex flex-col items-center justify-center h-full gap-4">
      <h1>Call Not Found</h1>
      <p><code>{callId}</code> wasn't found in production.</p>

      {devResult === 'found' ? (
        <Button onClick={() => navigate(`/debug?env=dev&callId=${callId}`)}>
          View in Dev Environment →
        </Button>
      ) : devResult === 'not-found' ? (
        <p>Not found in dev either.</p>
      ) : (
        <Button onClick={handleTryDev} disabled={checkingDev}>
          {checkingDev ? 'Checking...' : 'Check Dev Environment'}
        </Button>
      )}

      <Button variant="ghost" onClick={() => navigate('/debug')}>
         Back to Dashboard
      </Button>
    </div>
  )
}

File Changes

Create

File Purpose
utils/deduplicatedFetch.ts Request deduplication utility
utils/dashboardState.ts sessionStorage helpers for dashboard state
components/BottomSheet.tsx Mobile bottom sheet for turn details
components/CallNotFound.tsx Error state with "try dev" option
hooks/useCall.ts Fetch single call metadata by ID

Modify

File Changes
pages/CallDebugger.tsx Remove sidebar, simplify to route-based, use useCall
pages/CallsLandingPage.tsx Navigate to /debug/call/:id, save/restore state
components/TurnExplorerView.tsx Add bottom sheet for mobile
services/CallDebuggerService.ts Add getCall(id), remove legacy functions
worker/modules/debug/router.ts Add GET /db/calls/:id, remove /db/calls/lookup
worker/middleware/auth.ts Remove /api/debug/db/calls/lookup from public paths
ui/routes.tsx Update route for /debug/call/:callId

Delete

File Reason
contexts/CallDebuggerContext.tsx Replaced by simple hooks
data/callStore.ts LRU cache no longer needed
components/CallIdSidebar.tsx Replaced by dashboard sidebars
components/CallIdSidebar.css Associated styles
hooks/useDBCalls.ts Only used by CallIdSidebar

What's Not Changing

  • View components (TurnExplorerView, RawLogsView, WaterfallView, CallDataView)
  • Filter components (FilterDropdown, LogSearch)
  • Audio player, metadata header internals
  • S3 artifact fetching logic
  • Short URL redirect mechanism (/d/:callId)