Overview
The analytics page loads data from a singleGET /analytics/dashboard endpoint that returns all Call-table-backed metrics (scalars + charts) in one request. A set of shared CTEs (Common Table Expressions) define base filtering once — all metric aggregations run against them.
8 additional endpoints serve data from non-Call-table sources (SMS, time saved, candidate satisfaction, campaigns launched, credits per candidate).
Key Files
| File | Purpose |
|---|---|
server/analytics/analytics_api.py | Dashboard endpoint + Pydantic response models |
server/analytics/analytics_filters.py | Shared filter parsing (ResolvedFilters) |
server/dao/call_dao/call_dao_analytics_base.py | 3 shared CTE builders |
server/dao/call_dao/call_dao_analytics_metrics.py | Metric DAO methods (counts, rates, charts) |
server/dao/call_dao/call_dao_analytics_passthrough.py | Passthrough rate + thumbs up/down DAO methods |
Shared CTEs
CallDaoAnalyticsBase provides 3 CTE builders that all analytics DAO methods inherit:
| CTE | Base Tables | Key Columns | Used By |
|---|---|---|---|
| Call | Call JOIN Campaign | call_id, candidate_id, campaign_id, call_status, call_length_sec, question_completion_rate, created | Total calls, answer rate, call completion, interviews, call length |
| CandidateInfo | CandidateCampaignUserReview JOIN Call JOIN Campaign | candidate_id, campaign_id, feedback, max_score | Passthrough rate, thumbs up/down, qualified candidates |
| Review | CandidateCampaignUserReview JOIN Call JOIN Campaign | candidate_id, feedback, call_last_reviewed_at | Thumbs up/down grouped by time |
org_id, date_range, campaign_ids, candidate_filter. All queries use the read-only replica via self.read_only_session().
Dashboard Endpoint
GET /analytics/dashboard returns an AnalyticsDashboardResponse containing:
- Scalar metrics: answer rate, call completion rate, total calls, interviews, people contacted, call length, avg interview length, avg time to first interview, opt-outs, qualified candidates, passthrough rate, thumbs up/down
- Chart data: 6 typed time-series charts (calls, candidates, interviews, thumbs up, thumbs down, passthrough rate) — each chart has its own Pydantic model (e.g.
CallsChartResponse,PassthroughRateChartResponse) - Withdrawal & accommodation: withdrawal rate, withdrawal reasons breakdown, accommodation rate
asyncio.gather, except qualified_candidates which runs sequentially (needs the passing score resolved first).
Per-Campaign Passing Scores
When filtering to a single campaign, the dashboard looks up that campaign’s custom passing score from Cosmos viacampaign_scripts_cosmos_dao.get_passing_scores(). This matches the behavior of the passthrough rate DAO, which also uses per-campaign scores internally.
Frontend
The frontend uses Orval-generated API client functions with Pydantic-backed TypeScript types. Each dashboard tile is fetched with an SWR hook keyed by[tile, orgId + serialized filters]. Changing filters produces a new cache key, so a stale in-flight response is automatically discarded by SWR rather than overwriting newer data, with no request-id guards needed.
Custom AI Dashboard Sharing
Custom AI dashboards can be visible to everyone in the org, visible only to the owner, or shared with selected users in the same org. Selected users can view the dashboard and its AI charts, but they cannot rename it, delete it, change its visibility, add charts, or reorder/resize charts. Sharing is only meaningful for private custom dashboards. Org-visible dashboards are already visible to everyone in the org, and the system Overview dashboard follows the org-wide dashboard permission rules instead of per-user sharing.Individual Endpoints
These 8 endpoints are separate from the dashboard because they use different data sources:| Endpoint | Data Source |
|---|---|
/analytics/sms_sent | SMS helper (Cosmos + external) |
/analytics/total_sms | SMS helper |
/analytics/time_saved | Calls + SMS + web calls + resume screening + notetaking |
/analytics/total_time_saved | Calls + SMS + web calls + resume screening + notetaking (scalar) |
/analytics/candidate_satisfaction | Call reviews (1-5 rating) |
/analytics/average_candidate_satisfaction | Same as above (scalar) |
/analytics/campaigns_launched_per_week | Campaign table (no filters) |
/analytics/credits_per_qualified_candidate | Credits + qualified candidates |
Adding a New Filter
A new filter only needs 3 touch-points — no per-metric changes:Parse in resolve_filters
Add the query param to
server/analytics/analytics_filters.py and include it in ResolvedFilters.Apply in one CTE builder
Add a
where clause in the relevant CTE method in server/dao/call_dao/call_dao_analytics_base.py. All metrics using that CTE now respect it automatically.