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):
Short URL (sharing):
Navigation Flow¶
- User on
/debugclicks a call → navigates to/debug/call/<call_id> - User clicks back → returns to
/debug(dashboard state restored from sessionStorage) - User visits
/d/<call_id>→ server redirects to/debug/call/<call_id> - 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¶
CallIdSidebarcomponent and all sidebar stateuseNavigate()for internal call selection- Legacy URL param handling (
params.date,params.env) handleSelectCall()callback- Sidebar toggle button, mobile overlay mask
useResizablehook 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 providerdata/callStore.ts- LRU caches, deduplication logicuseCallDebuggerContexthook
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)