Reports & Analytics
Audience: Sales · Management · Support · QA · Engineering · Where in app: Sidebar → Reports (/reports) · Plan availability: All plans — gated by the reports.view permission
Restructured (2026-07, Agent K beta-gated).
/reportsrendersReportsHome.vue, which switches oncompany.agent_k_beta_enabled:
- Beta companies get the deep six-tab analytics (
frontend/src/views/Reports/Index.vue— Overview / Hot Leads / Conversions / Traffic Sources / Auto Engage (the hooks tab, relabeled) / Engagement), moved here from/dashboardwhen that became the "Today on Knock Knock" command center. Documented under Dashboard.- Non-beta companies keep the legacy Reports screen (
ReportsView.vue) documented below.
Legacy Reports is a single full-width screen that plots your core engagement metrics — missed calls, chats, video/audio calls, and unique visitors — as trending cards over a date range you choose (24h, 7/30/90 days, or a custom window). It is the "zoom in on one number over time" view, as opposed to the Dashboard, which is the multi-tab operational overview. Reports pulls all of its numbers from a single backend endpoint (GET /reports).
What it does
- Shows eight metric cards plus two large charts, each with a count for the selected period and a sparkline/trend.
- Lets the viewer change the date window (
24h,7 days,30 days,90 days, or a custom start/end range). All cards update together. - Persists the active filter in the URL query string (
?dateFilter=...&startDate=...&endDate=...), so a report view is shareable/bookmarkable and survives refresh. - Scopes automatically to the signed-in user's company (admins/impersonation can pass a
company_id).
This screen is read-only. It does not export files, schedule emails, or drill into individual records — for that, see Related.
How it works
The view (ReportsView.vue) calls the useReportsQuery composable, which:
- Converts the chosen
dateFilterinto an absolute{ start, end }ISO range (getDateRange). For the preset filters the range is "now minus N" to "now"; forcustomit uses the picked dates (falling back to 7 days if the custom range is incomplete). - Fetches
GET /reports?start_date=<iso>&end_date=<iso>viareportsRepository.getDashboardReports. - Caches the result for 5 minutes (
staleTime), keyed by the date range — re-selecting the same window does not re-hit the server. - Transforms each metric's time-series array into chart buckets (
transformTimeSeriesData) and builds x-axis labels (getChartCategories).
For 24h, data is bucketed into 8 three-hour slots (labels like 00:00, 03:00, …). For longer ranges, data is bucketed per day and the chart shows the last 8 days/labels when there are more than eight.
Metrics shown
Every card maps a value plus a *_stats time series from the /reports response:
| Card (UI label) | Response field | Meaning |
|---|---|---|
| Calls Missed | missed_calls | Visitor-initiated calls (created_by: 'user') that were never started/answered and were not chats. |
| Chat from User | user_messages | Conversations where a real visitor (not chatbot) sent a message. |
| Chatbot Conversation | chatbot_conversations | Conversations handled by the AI chatbot. |
| Forwarded Phone Calls | forwarded_as_phone_calls | Service rooms flagged forwarded_as_phone_call: true (calls handed off to an external number). |
| Video Calls | video_calls | Video calls. |
| Audio Calls | audio_calls | Voice-only calls. |
| Users | sessions | Visitor sessions in the period (labeled "Users" in the UI). |
| Total Chats (large chart) | user_messages + chatbot_conversations | Computed client-side by combining the two chat series. |
| Total Video Calls (large chart) | video_calls | Re-plots the video-call series at full width. |
The card titled Users is sourced from the session count (
sessions), not a distinct-person count. Treat it as "sessions in range." Whether it represents unique people is verify againstUserSessionModelsemantics.
Date controls
| Control | Behavior |
|---|---|
| Custom | Opens a DateRangePicker; applying sets dateFilter = 'custom' and stores start/end. |
| 24h / 7 days / 30 days / 90 days | Sets the preset and clears any custom range. |
| Default on load | 7days, unless a valid dateFilter is present in the URL. |
Configuration & options
There are no saved settings on this screen — every option is a transient view control:
| Option | Values | Notes for QA/Support |
|---|---|---|
| Date filter | 24h, 7days, 30days, 90days, custom | Invalid/unknown values in the URL are ignored; the screen falls back to 7days. |
| Custom range | any start/end date | If only a partial custom range is set, the query silently uses the last 7 days. |
States to verify when supporting a user:
- Loading — full-screen spinner ("Loading reports…") shown only on first load while
metricsis empty. - Error — a "Failed to load reports" panel with a Try Again button (
refetch). Triggered by a non-200 from/reports. - Empty data — cards render
0and flat charts rather than erroring.
Behaviors & edge cases
- 5-minute cache: counts can lag reality by up to ~5 minutes per the React-Query stale time; "Try Again" / re-navigating after the window refetches.
- 24h timezone handling: the 24h bucketing applies the browser's UTC offset to each stat's timestamp, so the hour slots reflect the viewer's local clock, not the company timezone — verify when a customer reports off-by-hours buckets.
- Last-8-points truncation: for ranges longer than 8 days the chart only labels/plots the last 8 daily buckets; the headline value is still the full-period total. This is expected, not a bug.
- Totals are derived client-side: "Total Chats" is
user_messages + chatbot_conversationscomputed in the browser; it is not a separate backend field. - Permission gate: the route requires
reports.view. A user without it should not see the Reports sidebar entry/route.
Plan & limits
Available on all plans, gated only by the reports.view role permission (see route meta in router/index.js). There are no per-plan caps on date range or metric availability in the code reviewed. Any plan-tier marketing claims beyond the permission gate are verify with product/pricing.
Technical implementation
- Route switcher:
frontend/src/views/ReportsHome.vue(beta →Reports/Index.vue, else legacy) - Legacy view:
frontend/src/views/ReportsView.vue - Data composable:
frontend/src/composables/useReportsQuery.js(getDateRange,getChartCategories,transformTimeSeriesData,combineChartData) - Repository:
frontend/src/repositories/Reports/index.js→GET /reports?start_date&end_date - Components:
StatCard.vue,LargeChart.vue,DateRangePicker.vue - Route: name
reports, path/reports, meta{ tab: 'reports', permission: 'reports.view' }(frontend/src/router/index.js) - Backend endpoint:
GET /reports→ReportController@index(backend/app/routes/reportRoutes.js,backend/app/controllers/ReportController.js). It counts documents and builds interval series (StatsHelper.intervalStats) overServiceRoomModel,ChatModel, andUserSessionModel, scoped bycompany_id. Admins must passcompany_id; web-owners/team members resolve the company from their user record. - Related backend routes (not used by this screen): the richer
/stats/*and StatsV2 endpoints (backend/app/routes/statsRoutes.js,statsV2Routes.js) power the Dashboard tabs, not Reports. See services/backend.
Naming note:
ReportController@indexis internally documented as the "Dashboard endpoint," but in the app it backs the Reports screen. The product Dashboard tabs use the/stats/dashboard/*endpoints instead.
Related
- Dashboard — multi-tab operational overview (different data source:
/stats/dashboard/*). - Calls and Live Chat — drill into the individual conversations behind these counts.
- Sessions and Leads — act on the visitors represented by the "Users"/sessions metric.
- services/backend — the report/stats controllers and
StatsHelper.