How the Claude Code Analytics Dashboard Detects Active Conversations in Real-Time
The Claude Code Analytics Dashboard tracks live conversations through a lightweight telemetry pipeline that sends 30-second heartbeats from the CLI to a Supabase table, then streams changes to the UI via Supabase-Realtime subscriptions.
According to the davila7/claude-code-templates repository, the dashboard displays a real-time count of active Claude Code sessions by combining client-side instrumentation, database upserts, and WebSocket-based subscriptions. This architecture allows the Analytics Dashboard to reflect new sessions and drop-offs with sub-second latency.
The Telemetry Pipeline: CLI Heartbeats
Every interaction in Claude Code triggers a telemetry event that powers the live dashboard. When a user sends a request in the CLI, the trackWebsiteEvent function in api/track-website-events.js dispatches a heartbeat payload to the backend.
Session Identification and Payload Structure
The heartbeat function constructs a JSON payload containing a unique session_id (generated once per CLI start), an event type of "conversation_heartbeat", and a client-side timestamp. As implemented in api/track-website-events.js:
export async function trackWebsiteEvent(event, sessionId) {
const body = {
session_id: sessionId,
event,
timestamp: Date.now(),
// optional user meta …
};
await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/active_sessions`, {
method: "POST",
headers: {
apikey: process.env.SUPABASE_SERVICE_ROLE_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
}
If the user is authenticated, the payload also includes a hashed user_id for attribution without exposing personal data.
The Heartbeat Timeout Mechanism
The CLI enforces a 30-second activity window. The trackWebsiteEvent function fires on every user request (such as pressing Enter), but if a session remains idle for more than 30 seconds, the CLI stops transmitting heartbeats. This timeout threshold synchronizes with the dashboard’s filtering logic to ensure disconnected or inactive sessions drop from the live count precisely when they become stale.
Data Persistence with Supabase
The active session data resides in a dedicated Supabase table optimized for high-frequency updates and real-time broadcasting.
Table Schema and Security
The migration file supabase/migrations/2024-xx-xx_create_active_sessions.sql defines the storage structure:
create table public.active_sessions (
id uuid primary key default uuid_generate_v4(),
session_id uuid not null,
last_seen bigint not null,
user_hash text,
created_at timestamp with time zone default now()
);
A Row Level Security (RLS) policy restricts write operations to the service role key used by the CLI, while allowing the dashboard's public API to read the data for display purposes.
Upsert Strategy for Live Updates
Rather than inserting duplicate rows for every heartbeat, the endpoint uses an UPSERT pattern. The code in api/track-website-events.js updates the last_seen timestamp if the session_id already exists:
await supabase
.from("active_sessions")
.upsert({
session_id,
last_seen: Date.now(),
user_hash: hashUserId(userId),
})
.eq("session_id", sessionId);
This guarantees that the active_sessions table maintains exactly one row per active Claude Code session, with the last_seen column continuously refreshed to the current timestamp.
Real-Time Subscription in the Dashboard
The front-end detects changes instantly through Supabase-Realtime. The utility file dashboard/src/lib/realtime.ts establishes a WebSocket connection to the database and listens for all changes on the active_sessions table:
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export const subscribeActiveConversations = (onChange) => {
return supabase
.channel("public:active_sessions")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "active_sessions" },
(payload) => {
// payload.new contains the freshest row
onChange(payload);
}
)
.subscribe();
};
The subscription captures INSERT, UPDATE, and DELETE events as they occur. When a new heartbeat upserts a row, the Realtime channel pushes that change to every connected dashboard client within milliseconds.
Calculating the Live Count
The React component dashboard/src/components/ActiveConversations.tsx manages local state and derives the final metric by filtering for recent timestamps. It stores session data in a Map<session_id, last_seen> and reapplies the 30-second threshold:
const ACTIVE_TIMEOUT_MS = 30_000;
export const ActiveConversations = () => {
const [sessions, setSessions] = useState<Map<string, number>>(new Map());
useEffect(() => {
const sub = subscribeActiveConversations((payload) => {
const { session_id, last_seen } = payload.new;
setSessions((prev) => {
const copy = new Map(prev);
copy.set(session_id, last_seen);
return copy;
});
});
return () => supabase.removeChannel(sub);
}, []);
const now = Date.now();
const activeCount = Array.from(sessions.values()).filter(
(ts) => now - ts <= ACTIVE_TIMEOUT_MS
).length;
return <span>{activeCount} active conversation{activeCount !== 1 && "s"}</span>;
};
Because the data arrives via the Realtime subscription rather than polling, the UI updates instantaneously as soon as a heartbeat lands in the database or a session times out.
Maintenance and Cleanup
To prevent the active_sessions table from growing indefinitely, a serverless cron job handles garbage collection. The script in api/maintenance/cleanup-inactive-sessions.ts executes on a one-minute interval via Vercel cron, deleting rows older than the 30-second threshold:
const THRESHOLD = Date.now() - 30_000; // 30 seconds
await supabase
.from("active_sessions")
.delete()
.lte("last_seen", THRESHOLD);
This pruning ensures that stale sessions are removed from storage while the dashboard’s local filtering ensures they disappear from the UI immediately upon expiration.
Summary
- CLI Instrumentation: The
trackWebsiteEventfunction inapi/track-website-events.jstransmits a"conversation_heartbeat"payload containingsession_idandtimestampon every user request. - Database Schema: The
active_sessionstable stores the latest heartbeat per session with RLS policies securing write access. - Upsert Pattern: Each heartbeat updates the
last_seencolumn via UPSERT, maintaining a single canonical row per active session. - Real-Time Streaming: The dashboard subscribes to Supabase-Realtime in
dashboard/src/lib/realtime.ts, receiving immediate notifications of database changes. - Client-Side Filtering: The
ActiveConversationscomponent counts only sessions with timestamps newer than 30 seconds to derive the live metric. - Background Cleanup: A cron job in
api/maintenance/cleanup-inactive-sessions.tspurges expired rows to keep the dataset lightweight.
Frequently Asked Questions
How does the dashboard know when a conversation becomes inactive?
The dashboard filters the local session cache using the same 30-second timeout defined in the CLI. If the difference between Date.now() and a session's last_seen value exceeds 30,000 milliseconds, the component excludes that session from the active count, even if the database row persists temporarily before the cleanup cron job removes it.
Why does the system use Supabase-Realtime instead of polling?
Supabase-Realtime provides sub-second latency by pushing database changes over WebSockets rather than requiring the client to request updates periodically. This architecture reduces network overhead and allows the Analytics Dashboard to reflect new heartbeats instantly as they are upserted into the active_sessions table.
What prevents unauthorized clients from writing to the active sessions table?
The active_sessions table implements Row Level Security (RLS) policies that restrict insert and update operations to requests bearing the SUPABASE_SERVICE_ROLE_KEY. The dashboard client uses an anonymous public key with read-only permissions, ensuring only the CLI backend can modify session data.
How does the system handle clock drift between the CLI and server?
The heartbeat mechanism relies on client-generated timestamps (Date.now() in the CLI) stored as last_seen in the database. The dashboard applies the 30-second filter using its own local time reference. While minor drift exists, the 30-second window is sufficiently large to accommodate typical client-server time discrepancies without affecting accuracy.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →