Analytics System Documentation
This document covers all analytics functionality across the platform both Customer Analytics (call stats, campaign metrics) and Admin Analytics (token consumption tracking). Use this as the single reference for understanding data flows, API endpoints, MongoDB collections, and frontend integration.
Table of Contents
- Architecture Overview
- Data Sources (MongoDB Collections)
- Customer Analytics
- Admin Analytics (Token Consumption)
- Feature Gating System
- Frontend Integration
- API Reference
- Adding New Analytics
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (React) │
├─────────────────────────────────────────────────────────────────┤
│ AnalyticsPage │ CampaignsPage │ HistoryPage │
│ - Call stats │ - Token badges │ - Contact tokens │
│ - Daily volume │ - Campaign stats │ - Call metrics │
│ - Token chart │ │ │
├─────────────────────────────────────────────────────────────────┤
│ campaignService │ tokenAnalyticsService (silentApi) │
├─────────────────────────────────────────────────────────────────┤
│ Backend (Express) │
├─────────────────────────────────────────────────────────────────┤
│ campaign.controller │ tokenAnalytics.controller │
│ (getDashboardStats) │ (getCampaignTokens, getTimeline, │
│ │ getContactTokens) │
├─────────────────────────────────────────────────────────────────┤
│ MongoDB Collections │
│ Contact │ Campaign │ ContactObservability │ CreditTxn │
└─────────────────────────────────────────────────────────────────┘Data Sources
Contact Collection
Primary collection for all call/contact data. Key fields for analytics:
| Field | Type | Description |
|---|---|---|
contactUID | string | Unique contact identifier |
campaignUID | string | Parent campaign |
orgUID | string | Organization (tenant isolation) |
userId | string | Owner user |
AgentID | string | Agent used for the call |
status | enum | pending, in_progress, done, failed, skipped, stopped |
callStatus | string | Granular status: ended, not_picked, rejected, media_failure, busy, etc. |
callDuration | number | Actual call duration (seconds) |
billingDuration | number | Billed duration (seconds) |
callDirection | string | inbound or outbound |
analysis | object | Call analysis results (schema-dependent) |
createdAt | Date | Contact creation time |
updatedAt | Date | Last update time |
isTestCall | boolean | Excluded from analytics |
ContactObservability Collection
Stores AI model usage metrics per contact. Linked to Contact via contactUID.
{
contactUID: string; // Links to Contact.contactUID
observability: {
Metrics: {
model_usage: [ // Array of usage entries per AI call
{
type: string; // "llm_usage" | "tts_usage" | "stt_usage"
provider: string; // e.g. "openai", "elevenlabs", "deepgram"
model: string; // e.g. "gpt-4o", "eleven_multilingual_v2"
input_tokens: number; // LLM only
output_tokens: number; // LLM only
input_cached_tokens: number; // LLM only
characters_count: number; // TTS only
audio_duration: number; // TTS and STT (seconds)
}
]
}
};
session_report: object; // Full session report (not used in analytics)
}Campaign Collection
Campaign metadata. Key fields:
| Field | Type | Description |
|---|---|---|
campaignUID | string | Unique campaign identifier |
orgUID | string | Organization |
userId | string | Owner |
AgentID | string | Default agent |
isRunning | boolean | Currently active |
stopRequested | boolean | Stop in progress |
CreditTransaction Collection
Used for inbound call credit tracking in dashboard stats.
Customer Analytics
What It Shows
Available to all users with voice_campaign feature. No additional feature gate required.
Analytics Page (/analytics):
- Overview cards: total campaigns, active campaigns, total agents, total contacts, answer rate, avg call duration, total billing duration, credits consumed/balance
- Daily call volume chart (area or bar chart)
- Call status breakdown (pie chart with granular statuses)
- Inbound metrics: call count, duration, credits consumed
- Success rate per campaign (when campaign filter is active)
- Analysis schema filters (filter by call analysis results)
History Page (/history):
- Campaign-level stats: total contacts, answered, not answered, in progress, pending, stopped
- Per-contact details: call status, duration, recording, transcription, analysis
- Filterable by campaign, agent, status, direction, date range
Campaigns Page (/campaigns):
- Per-campaign stats in table: total contacts, done, failed, pending counts
Backend Endpoint
GET /dashboard-statsQuery Parameters:
| Param | Required | Description |
|---|---|---|
range | No | today, yesterday, 7d, 30d, custom (default: 7d) |
from | If custom | Start date YYYY-MM-DD |
to | If custom | End date YYYY-MM-DD |
campaignUID | No | Filter to specific campaign |
agentId | No | Filter by agent |
direction | No | inbound or outbound |
analysis.* | No | Dot-notation analysis filters |
Response shape:
{
"success": true,
"dashboard": {
"range": "custom",
"dateFrom": "ISO string",
"dateTo": "ISO string",
"overview": {
"totalCampaigns": 10,
"activeCampaigns": 2,
"totalAgents": 5,
"totalContacts": 1500,
"contactsCalled": 1200,
"contactsDone": 900,
"contactsFailed": 300,
"answerRate": 75,
"avgCallDurationSec": 45,
"totalBillingDurationSec": 54000,
"creditsConsumed": 250,
"creditBalance": 750
},
"dailyCallVolume": [
{ "date": "2025-01-01", "day": "Wed", "total": 50, "answered": 40, "failed": 10 }
],
"callStatusBreakdown": {
"ended": 500,
"not_picked": 200,
"rejected": 50
},
"inbound": {
"inboundCalls": 100,
"inboundDuration": 3600,
"inboundCredits": 50
}
}
}Data Flow
- Frontend
campaignService.getDashboardStats(params)calls the endpoint - Backend builds MongoDB aggregation pipeline on
Contactcollection - Filters by: user's campaigns, date range, agent, direction, analysis
- Aggregates: counts by status, sums durations, computes rates
- Joins
CreditTransactionfor inbound credit tracking - Returns structured dashboard object
Admin Analytics (Token Consumption)
What It Shows
Available only when admin_analytics feature is enabled. Shows AI model usage (LLM tokens, TTS characters, STT audio duration) aggregated from the ContactObservability collection.
Campaigns Page Token Usage Column:
- Compact inline badge per campaign row showing: LLM total tokens, TTS characters, STT duration
- Non-blocking fetch (doesn't delay row rendering)
Campaign Details Modal Token Consumption Section:
- Detailed breakdown by type (LLM, TTS, STT)
- Per provider/model: input tokens, output tokens, cached tokens (LLM); characters, audio duration (TTS); audio duration (STT)
History Page Contact Token Details:
- Same detailed breakdown but for a single contact
- Shown in the contact side panel's Details tab
Analytics Page Token Consumption Over Time Chart:
- Area chart showing token usage over time
- Filters: granularity (daily/weekly/monthly), date range (max 365 days), agent, campaign
- Default: last 30 days, daily granularity
Backend Service
File: backend/client_backend/src/services/tokenAnalytics.service.ts
Three main methods:
getCampaignTokenUsage(campaignUID, orgUID)
Aggregation pipeline:
$matchcontacts bycampaignUID+orgUID$lookupContactObservability viacontactUID$unwindobservability (preserveNullAndEmptyArrays: true)$unwindmodel_usage array (preserveNullAndEmptyArrays: true)$groupby{type, provider, model}, summing all metrics
Returns: CampaignTokenResponse with usage breakdown + computed totals.
getTimelineTokenUsage({ orgUID, granularity, from, to, agentId?, campaignUID? })
Same pipeline pattern but adds:
- Date range filter on
createdAt - Optional
AgentIDandcampaignUIDfilters (AND logic) - Groups by time bucket using
$dateToStringwith format based on granularity:- daily:
%Y-%m-%d - weekly:
%Y-W%V - monthly:
%Y-%m
- daily:
Returns: TimelineTokenResponse with sorted buckets.
getContactTokenUsage(contactUID, orgUID)
Same pipeline but matches a single contact. Verifies contact belongs to orgUID.
Returns: ContactTokenResponse with usage breakdown (no totals).
All pipelines use maxTimeMS: 5000 to prevent long-running queries.
Backend Routes
File: backend/client_backend/src/routes/tokenAnalytics.routes.ts
All routes use middleware chain: verifyJwt → requireFeatureAccess("admin_analytics")
GET /api/v1/token-analytics/campaign/:campaignUID
GET /api/v1/token-analytics/timeline?granularity=&from=&to=&agentId=&campaignUID=
GET /api/v1/token-analytics/contact/:contactUIDController Validation
File: backend/client_backend/src/controllers/tokenAnalytics.controller.ts
- Campaign endpoint: returns 404 if campaign not found in org
- Timeline endpoint: validates granularity (must be daily/weekly/monthly), date format, start < end, range ≤ 365 days
- Contact endpoint: returns 404 if contact not found in org
- All endpoints: return 403 if no orgUID on request
Feature Gating System
Backend (Two-Layer)
Middleware: backend/client_backend/src/middleware/feature.middleware.ts
requireFeatureAccess("admin_analytics")| Role | Layer 1 (Org Plan) | Layer 2 (User Features) |
|---|---|---|
| SuperAdmin | Bypassed | Bypassed |
| Admin | Checked | Bypassed |
| Customer | Checked | Checked |
- If org plan doesn't include the feature → 403
- If customer doesn't have the feature assigned → 403
- If feature service errors → 403 (deny by default)
Frontend
Hook: app/src/hooks/useFeature.ts
const { hasFeature } = useFeature();
const showTokens = hasFeature(FEATURES.ADMIN_ANALYTICS);Logic mirrors backend:
- SuperAdmin: always true
- Admin: checks
organization.plan.features - Customer: checks BOTH
organization.plan.featuresANDuser.features
Feature Registration
Backend: backend/client_backend/src/types/index.ts
AVAILABLE_FEATURESarray includes"admin_analytics"FEATURE_MAP.tokenAnalyticsmaps to["admin_analytics"]
Frontend: app/src/lib/features.ts
FEATURES.ADMIN_ANALYTICS = 'admin_analytics'UI_FEATURE_MAP.adminAnalytics = [FEATURES.ADMIN_ANALYTICS]ADMIN_FEATURES = [FEATURES.ADMIN_ANALYTICS](displayed separately in feature assignment UI)
Frontend Integration
Token Analytics Service
File: app/src/services/tokenAnalytics.service.ts
Uses a dedicated axios instance (silentApi) separate from the global api to:
- Avoid triggering auto-toast on 403/404 errors
- Avoid triggering 401 token refresh redirect
- Silently return
nullon any failure
import { tokenAnalyticsService } from '@/services/tokenAnalytics.service';
const data = await tokenAnalyticsService.getCampaignTokenUsage(campaignUID);
// Returns CampaignTokenResponse | nulluseTokenAnalytics Hook
File: app/src/hooks/useTokenAnalytics.ts
Provides:
fetchCampaignTokens(campaignUID)with in-memory cachefetchTimelineTokens(params)with cache keyed by filter paramsfetchContactTokens(contactUID)with in-memory cache- State objects:
campaign,timeline,contacteach with{ data, loading, error }
UI Components
| Component | Location | Purpose |
|---|---|---|
TokenSummaryBadge | app/src/components/shared/ | Compact inline display (LLM/TTS/STT totals) |
TokenDetailsSection | app/src/components/shared/ | Detailed breakdown card by type/provider/model |
TokenAnalyticsChart | app/src/components/shared/ | Timeline area chart with filters |
CampaignTokenCell | app/src/components/campaigns/ | Non-blocking cell for campaign table rows |
Integration Pattern
All token analytics UI is conditionally rendered:
const { hasFeature } = useFeature();
const showTokens = hasFeature(FEATURES.ADMIN_ANALYTICS);
// Only render when feature is enabled
{showTokens && <TokenDetailsSection usage={...} />}API Reference
Customer Analytics
| Method | Endpoint | Auth | Feature Gate | Description |
|---|---|---|---|---|
| GET | /dashboard-stats | JWT | voice_campaign | Global dashboard metrics |
| GET | /campaign-detail/:uid | JWT | voice_campaign | Campaign contacts + stats |
| GET | /contacts | JWT | voice_campaign | Paginated contact list |
| GET | /success-rate/:campaignUID | JWT | voice_campaign | Campaign success rate |
Admin Analytics (Token Consumption)
| Method | Endpoint | Auth | Feature Gate | Description |
|---|---|---|---|---|
| GET | /api/v1/token-analytics/campaign/:campaignUID | JWT | admin_analytics | Campaign token usage |
| GET | /api/v1/token-analytics/timeline | JWT | admin_analytics | Time-bucketed token usage |
| GET | /api/v1/token-analytics/contact/:contactUID | JWT | admin_analytics | Contact token usage |
Timeline Query Parameters
| Param | Required | Values | Description |
|---|---|---|---|
granularity | Yes | daily, weekly, monthly | Time bucket size |
from | Yes | YYYY-MM-DD | Start date |
to | Yes | YYYY-MM-DD | End date |
agentId | No | string | Filter by agent |
campaignUID | No | string | Filter by campaign |
Response Types
// Campaign token response
interface CampaignTokenResponse {
campaignUID: string;
usage: TokenUsageBreakdown;
totals: TokenUsageTotals;
}
// Timeline response
interface TimelineTokenResponse {
granularity: "daily" | "weekly" | "monthly";
from: string;
to: string;
buckets: TimelineBucket[];
}
// Contact token response
interface ContactTokenResponse {
contactUID: string;
usage: TokenUsageBreakdown;
}
// Shared types
interface TokenUsageBreakdown {
llm: LLMUsageEntry[];
tts: TTSUsageEntry[];
stt: STTUsageEntry[];
}
interface TokenUsageTotals {
llm_input_tokens: number;
llm_output_tokens: number;
llm_cached_tokens: number;
tts_characters: number;
tts_audio_duration: number;
stt_audio_duration: number;
}Adding New Analytics
Adding a New Metric to Customer Analytics
- Add the aggregation logic in
campaign.controller.ts→getDashboardStats - Include the new field in the response
dashboardobject - Add the corresponding type in
app/src/types/campaign.ts - Display it in
AnalyticsPage.tsx(stat card, chart, or table)
Adding a New Token Analytics Dimension
- Update the aggregation pipeline in
tokenAnalytics.service.ts - Add new fields to the types in
backend/client_backend/src/types/tokenAnalytics.ts - Mirror the types in
app/src/types/tokenAnalytics.ts - Update the UI components (
TokenDetailsSection,TokenSummaryBadge,TokenAnalyticsChart)
Adding a New Feature-Gated Analytics Section
- Register the feature in
backend/client_backend/src/types/index.ts(AVAILABLE_FEATURES+FEATURE_MAP) - Register in
app/src/lib/features.ts(FEATURES+UI_FEATURE_MAP) - Create backend service + controller + routes with
requireFeatureAccess("your_feature") - Create frontend service (use dedicated axios instance for silent errors)
- Create hook + UI components
- Gate rendering with
useFeature().hasFeature(FEATURES.YOUR_FEATURE)
Key Principles
- Tenant isolation: Always scope queries by
orgUID - Silent failures: Token analytics uses
silentApinever shows error toasts or triggers auth redirects - Non-blocking: Campaign row token fetches don't block row rendering
- Feature deny-by-default: If feature service errors, deny access (403)
- Aggregation timeout: All MongoDB aggregations use
maxTimeMS: 5000 - Frontend timeout: All API calls use 10-second timeout
- Caching:
useTokenAnalyticshook caches responses inuseRefMaps