Tools: Complete Guide to Asterisk CDR Analysis — Extract Insights from Call Detail Records
Asterisk CDR Analysis — Extract Insights from Call Detail Records
Introduction
Prerequisites
Understanding Asterisk CDR Structure in ViciDial
Native Asterisk CDR vs. ViciDial Logging
Critical ViciDial Tables for CDR Analysis
Setting Up Your Analysis Environment
Create a Dedicated Reporting Database User
Create Helper Views for Common Queries
Core CDR Analysis Queries
1. Identifying Failed Calls and Bottlenecks
2. Agent Performance Benchmarking
3. Inbound vs. Outbound Call Analysis
4. Campaign Performance Comparison
5. Detecting Call Quality Issues
Real-Time Monitoring with Asterisk CLI
Monitor Active Calls
Check SIP Registration Status
Monitor CDR Recording
Create a Live Dashboard Script
Exporting and Processing CDR Data
Export to CSV for Excel/Sheets Analysis
Parse Raw Asterisk CDR Files
Advanced Analysis: Detecting Patterns and Anomalies
Finding Your Best Performing Time Windows
Identifying Caller ID Spoofing / Fraud
Measuring First Call Resolution (FCR)
Compliance and Call Recording Verification
Creating Automated Reports and Alerts
Daily Email Report Script
Alert on Anomalies
Troubleshooting CDR Issues
No CDR Records Being Generated
CDR Records Exist But Have NULL Fields
Database Queries Slow / Timeouts
Asterisk CLI Commands Failing (Permission Denied)
MySQL Connection Issues from Scripts
Recording Files Not Found Despite recording_id in Database
Summary Master the complete workflow for analyzing Asterisk Call Detail Records in production ViciDial environments—from database queries to real-time metrics and automated reporting. Call Detail Records (CDRs) are the backbone of telecom operations. Every call passing through your Asterisk system generates raw data that contains critical insights: which numbers convert, where calls fail, which agents perform best, and where money is being wasted. In production ViciDial environments, CDR data accumulates rapidly—thousands of records daily—making manual inspection impossible. This tutorial walks you through extracting, transforming, and analyzing CDR data using SQL queries, command-line tools, and custom scripts. You'll learn to identify trends, debug call failures, optimize routing, and generate compliance reports. These are skills you need right now if you're managing an Asterisk system under load. Before starting, verify you have: Verify database connectivity: If this fails, check /etc/asterisk/asterisk.conf for database credentials and verify the MySQL service is running. Asterisk generates CDRs natively via the CDR module, but ViciDial overlays custom logging on top. Understanding both is essential: ViciDial Enhanced Logging: vicidial_log — The primary call record table: vicidial_closer_log — Detailed disposition and result data: Includes agent notes, call outcomes, lead disposition changes. vicidial_carrier_log — Inbound/outbound carrier metrics (if applicable). Don't query production using root. Create a read-only user: Simplify repeated analysis with SQL views: This reveals which numbers aren't working and why. High "NO ANS" rates might indicate: Early call drops (< 5 seconds) indicate network issues, bad numbers, or hangup detection problems. Output includes current calls, channel types, and duration. Useful for: Ensure all configured trunks and agents are registered. Missing registrations explain inbound call failures. Verify CDR module is active. If output shows "CDR logging disabled," check /etc/asterisk/cdr.conf: If you're using CSV CDR storage: This reveals peak efficiency hours. Most campaigns see 20-30% variation across day parts. Concentrate calls during peak windows. Numbers appearing across multiple agents/campaigns with poor conversion and high volume are often fraud or spam sources. Add to DNC list immediately. Multiple calls to same lead = poor FCR. High numbers indicate ineffective agent training or bad lead qualification. Missing recordings on completed calls violate compliance in many jurisdictions. This query exposes gaps. Run every 15 minutes: Symptom: asterisk -rx "cdr show status" shows "CDR logging disabled" Symptom: Database shows talk_sec, hold_sec as NULL Reason: Variables not being set in dialplan. Asterisk requires explicit tracking via DIALPLAN() application. Solution: In /etc/asterisk/extensions-vicidial.conf, ensure: Symptom: Large SELECT queries hang or timeout after 30 seconds Solution: Add indexes: Verify indexes exist: Symptom: asterisk -rx returns "permission denied" Solution: Your user isn't in the asterisk group: Symptom: Bash scripts fail with "Access denied for user" Symptom: recording_id populated but actual audio files missing Reason: Recordings stored in separate location, deleted by cleanup script, or disabled Asterisk CDR analysis transforms raw call data into actionable business intelligence. The techniques in this tutorial enable you to: Operational improvements: Performance insights: The Asterisk CDR system is powerful but requires intentional design and discipline. Start with the core queries provided here, customize them to your specific campaigns and metrics, and gradually build a comprehensive reporting infrastructure. Your operations team and finance department will demand this data sooner or later—better to build it on your schedule. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to ? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
asterisk -rx
/var/log/asterisk/messages
mysql -u root -p asterisk -e "SELECT COUNT(*) FROM vicidial_log LIMIT 1;"
mysql -u root -p asterisk -e "SELECT COUNT(*) FROM vicidial_log LIMIT 1;"
mysql -u root -p asterisk -e "SELECT COUNT(*) FROM vicidial_log LIMIT 1;"
/etc/asterisk/asterisk.conf
/var/log/asterisk/cdr-csv/
vicidial_log
vicidial_closer_log
vicidial_carrier_log
DESCRIBE vicidial_log;
DESCRIBE vicidial_log;
DESCRIBE vicidial_log;
campaign_id
length_in_sec
phone_number
called_number
DESCRIBE vicidial_closer_log;
DESCRIBE vicidial_closer_log;
DESCRIBE vicidial_closer_log;
CREATE USER 'cdr_reporter'@'localhost' IDENTIFIED BY 'SecurePassword123!';
GRANT SELECT ON asterisk.* TO 'cdr_reporter'@'localhost';
GRANT SELECT ON asterisk.vicidial_log TO 'cdr_reporter'@'localhost';
GRANT SELECT ON asterisk.vicidial_closer_log TO 'cdr_reporter'@'localhost';
FLUSH PRIVILEGES;
CREATE USER 'cdr_reporter'@'localhost' IDENTIFIED BY 'SecurePassword123!';
GRANT SELECT ON asterisk.* TO 'cdr_reporter'@'localhost';
GRANT SELECT ON asterisk.vicidial_log TO 'cdr_reporter'@'localhost';
GRANT SELECT ON asterisk.vicidial_closer_log TO 'cdr_reporter'@'localhost';
FLUSH PRIVILEGES;
CREATE USER 'cdr_reporter'@'localhost' IDENTIFIED BY 'SecurePassword123!';
GRANT SELECT ON asterisk.* TO 'cdr_reporter'@'localhost';
GRANT SELECT ON asterisk.vicidial_log TO 'cdr_reporter'@'localhost';
GRANT SELECT ON asterisk.vicidial_closer_log TO 'cdr_reporter'@'localhost';
FLUSH PRIVILEGES;
CREATE VIEW cdr_daily_summary AS
SELECT DATE(call_date) AS call_day, campaign_id, COUNT(*) AS total_calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, SUM(talk_sec) AS total_talk_seconds, ROUND(AVG(talk_sec), 2) AS avg_talk_seconds, ROUND(AVG(length_in_sec), 2) AS avg_call_duration
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY DATE(call_date), campaign_id;
CREATE VIEW cdr_daily_summary AS
SELECT DATE(call_date) AS call_day, campaign_id, COUNT(*) AS total_calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, SUM(talk_sec) AS total_talk_seconds, ROUND(AVG(talk_sec), 2) AS avg_talk_seconds, ROUND(AVG(length_in_sec), 2) AS avg_call_duration
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY DATE(call_date), campaign_id;
CREATE VIEW cdr_daily_summary AS
SELECT DATE(call_date) AS call_day, campaign_id, COUNT(*) AS total_calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, SUM(talk_sec) AS total_talk_seconds, ROUND(AVG(talk_sec), 2) AS avg_talk_seconds, ROUND(AVG(length_in_sec), 2) AS avg_call_duration
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY DATE(call_date), campaign_id;
mysql -u cdr_reporter -p asterisk -e "SELECT * FROM cdr_daily_summary LIMIT 10;"
mysql -u cdr_reporter -p asterisk -e "SELECT * FROM cdr_daily_summary LIMIT 10;"
mysql -u cdr_reporter -p asterisk -e "SELECT * FROM cdr_daily_summary LIMIT 10;"
SELECT call_date, user, campaign_id, phone_number, status, length_in_sec, talk_sec
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 24 HOUR) AND status IN ('NO ANS', 'BUSY', 'NOT CALLABLE', 'DNC') AND campaign_id = 'YOUR_CAMPAIGN_ID'
ORDER BY call_date DESC
LIMIT 100;
SELECT call_date, user, campaign_id, phone_number, status, length_in_sec, talk_sec
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 24 HOUR) AND status IN ('NO ANS', 'BUSY', 'NOT CALLABLE', 'DNC') AND campaign_id = 'YOUR_CAMPAIGN_ID'
ORDER BY call_date DESC
LIMIT 100;
SELECT call_date, user, campaign_id, phone_number, status, length_in_sec, talk_sec
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 24 HOUR) AND status IN ('NO ANS', 'BUSY', 'NOT CALLABLE', 'DNC') AND campaign_id = 'YOUR_CAMPAIGN_ID'
ORDER BY call_date DESC
LIMIT 100;
SELECT user, COUNT(*) AS calls_handled, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec, SUM(talk_sec) AS total_talk_sec, ROUND(AVG(length_in_sec), 2) AS avg_call_length
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) AND campaign_id = 'YOUR_CAMPAIGN_ID' AND user NOT IN ('SYSTEM', 'TRANSFER')
GROUP BY user
ORDER BY conversion_rate DESC;
SELECT user, COUNT(*) AS calls_handled, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec, SUM(talk_sec) AS total_talk_sec, ROUND(AVG(length_in_sec), 2) AS avg_call_length
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) AND campaign_id = 'YOUR_CAMPAIGN_ID' AND user NOT IN ('SYSTEM', 'TRANSFER')
GROUP BY user
ORDER BY conversion_rate DESC;
SELECT user, COUNT(*) AS calls_handled, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec, SUM(talk_sec) AS total_talk_sec, ROUND(AVG(length_in_sec), 2) AS avg_call_length
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) AND campaign_id = 'YOUR_CAMPAIGN_ID' AND user NOT IN ('SYSTEM', 'TRANSFER')
GROUP BY user
ORDER BY conversion_rate DESC;
SELECT CASE WHEN direction = 'INBOUND' THEN 'Inbound' ELSE 'Outbound' END AS call_type, COUNT(*) AS total_calls, COUNT(CASE WHEN status IN ('SALE', 'XFER') THEN 1 END) AS conversions, ROUND(COUNT(CASE WHEN status IN ('SALE', 'XFER') THEN 1 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec, ROUND(AVG(length_in_sec), 2) AS avg_call_duration
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY direction;
SELECT CASE WHEN direction = 'INBOUND' THEN 'Inbound' ELSE 'Outbound' END AS call_type, COUNT(*) AS total_calls, COUNT(CASE WHEN status IN ('SALE', 'XFER') THEN 1 END) AS conversions, ROUND(COUNT(CASE WHEN status IN ('SALE', 'XFER') THEN 1 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec, ROUND(AVG(length_in_sec), 2) AS avg_call_duration
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY direction;
SELECT CASE WHEN direction = 'INBOUND' THEN 'Inbound' ELSE 'Outbound' END AS call_type, COUNT(*) AS total_calls, COUNT(CASE WHEN status IN ('SALE', 'XFER') THEN 1 END) AS conversions, ROUND(COUNT(CASE WHEN status IN ('SALE', 'XFER') THEN 1 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec, ROUND(AVG(length_in_sec), 2) AS avg_call_duration
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY direction;
SELECT campaign_id, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec, SUM(talk_sec) / 3600 AS hours_talk_time
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND user NOT IN ('SYSTEM', 'TRANSFER')
GROUP BY campaign_id
ORDER BY conversion_rate DESC;
SELECT campaign_id, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec, SUM(talk_sec) / 3600 AS hours_talk_time
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND user NOT IN ('SYSTEM', 'TRANSFER')
GROUP BY campaign_id
ORDER BY conversion_rate DESC;
SELECT campaign_id, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec, SUM(talk_sec) / 3600 AS hours_talk_time
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND user NOT IN ('SYSTEM', 'TRANSFER')
GROUP BY campaign_id
ORDER BY conversion_rate DESC;
SELECT DATE(call_date) AS call_date, COUNT(*) AS total_calls, COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) AS dropped_early, ROUND(COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) / COUNT(*) * 100, 2) AS drop_rate_pct, COUNT(CASE WHEN status IN ('NO ANS', 'BUSY') THEN 1 END) AS unreachable, ROUND(COUNT(CASE WHEN status IN ('NO ANS', 'BUSY') THEN 1 END) / COUNT(*) * 100, 2) AS unreachable_pct
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(call_date)
ORDER BY call_date DESC;
SELECT DATE(call_date) AS call_date, COUNT(*) AS total_calls, COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) AS dropped_early, ROUND(COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) / COUNT(*) * 100, 2) AS drop_rate_pct, COUNT(CASE WHEN status IN ('NO ANS', 'BUSY') THEN 1 END) AS unreachable, ROUND(COUNT(CASE WHEN status IN ('NO ANS', 'BUSY') THEN 1 END) / COUNT(*) * 100, 2) AS unreachable_pct
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(call_date)
ORDER BY call_date DESC;
SELECT DATE(call_date) AS call_date, COUNT(*) AS total_calls, COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) AS dropped_early, ROUND(COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) / COUNT(*) * 100, 2) AS drop_rate_pct, COUNT(CASE WHEN status IN ('NO ANS', 'BUSY') THEN 1 END) AS unreachable, ROUND(COUNT(CASE WHEN status IN ('NO ANS', 'BUSY') THEN 1 END) / COUNT(*) * 100, 2) AS unreachable_pct
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(call_date)
ORDER BY call_date DESC;
asterisk -rx "core show channels"
asterisk -rx "core show channels"
asterisk -rx "core show channels"
asterisk -rx "sip show peers"
asterisk -rx "sip show peers"
asterisk -rx "sip show peers"
asterisk -rx "cdr show status"
asterisk -rx "cdr show status"
asterisk -rx "cdr show status"
/etc/asterisk/cdr.conf
[general]
enable=yes
[general]
enable=yes
[general]
enable=yes
asterisk -rx "module reload cdr"
asterisk -rx "module reload cdr"
asterisk -rx "module reload cdr"
#!/bin/bash
# live_cdr_monitor.sh - Real-time call metrics while true; do clear echo "=== ViciDial CDR Live Monitor ===" echo "Time: $(date)" echo "" # Current active calls echo "--- Active Calls ---" asterisk -rx "core show channels" | grep -E "^SIP|^Agent" | head -5 # Calls in last hour echo "" echo "--- Last Hour Metrics ---" mysql -u cdr_reporter -p'SecurePassword123!' asterisk -e " SELECT COUNT(*) as calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) as conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as conv_rate FROM vicidial_log WHERE call_date >= DATE_SUB(NOW(), INTERVAL 1 HOUR); " sleep 30
done
#!/bin/bash
# live_cdr_monitor.sh - Real-time call metrics while true; do clear echo "=== ViciDial CDR Live Monitor ===" echo "Time: $(date)" echo "" # Current active calls echo "--- Active Calls ---" asterisk -rx "core show channels" | grep -E "^SIP|^Agent" | head -5 # Calls in last hour echo "" echo "--- Last Hour Metrics ---" mysql -u cdr_reporter -p'SecurePassword123!' asterisk -e " SELECT COUNT(*) as calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) as conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as conv_rate FROM vicidial_log WHERE call_date >= DATE_SUB(NOW(), INTERVAL 1 HOUR); " sleep 30
done
#!/bin/bash
# live_cdr_monitor.sh - Real-time call metrics while true; do clear echo "=== ViciDial CDR Live Monitor ===" echo "Time: $(date)" echo "" # Current active calls echo "--- Active Calls ---" asterisk -rx "core show channels" | grep -E "^SIP|^Agent" | head -5 # Calls in last hour echo "" echo "--- Last Hour Metrics ---" mysql -u cdr_reporter -p'SecurePassword123!' asterisk -e " SELECT COUNT(*) as calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) as conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as conv_rate FROM vicidial_log WHERE call_date >= DATE_SUB(NOW(), INTERVAL 1 HOUR); " sleep 30
done
chmod +x live_cdr_monitor.sh
./live_cdr_monitor.sh
chmod +x live_cdr_monitor.sh
./live_cdr_monitor.sh
chmod +x live_cdr_monitor.sh
./live_cdr_monitor.sh
#!/bin/bash
# export_cdr_csv.sh OUTPUT_FILE="/tmp/cdr_export_$(date +%Y%m%d_%H%M%S).csv" mysql -u cdr_reporter -p'SecurePassword123!' asterisk \ -e "SELECT call_date, user, campaign_id, phone_number, status, length_in_sec, talk_sec, hold_sec FROM vicidial_log WHERE call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) ORDER BY call_date DESC;" \ --batch --skip-column-names | \ sed 's/\t/,/g' > "$OUTPUT_FILE" echo "CDR export saved to: $OUTPUT_FILE"
ls -lh "$OUTPUT_FILE"
#!/bin/bash
# export_cdr_csv.sh OUTPUT_FILE="/tmp/cdr_export_$(date +%Y%m%d_%H%M%S).csv" mysql -u cdr_reporter -p'SecurePassword123!' asterisk \ -e "SELECT call_date, user, campaign_id, phone_number, status, length_in_sec, talk_sec, hold_sec FROM vicidial_log WHERE call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) ORDER BY call_date DESC;" \ --batch --skip-column-names | \ sed 's/\t/,/g' > "$OUTPUT_FILE" echo "CDR export saved to: $OUTPUT_FILE"
ls -lh "$OUTPUT_FILE"
#!/bin/bash
# export_cdr_csv.sh OUTPUT_FILE="/tmp/cdr_export_$(date +%Y%m%d_%H%M%S).csv" mysql -u cdr_reporter -p'SecurePassword123!' asterisk \ -e "SELECT call_date, user, campaign_id, phone_number, status, length_in_sec, talk_sec, hold_sec FROM vicidial_log WHERE call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) ORDER BY call_date DESC;" \ --batch --skip-column-names | \ sed 's/\t/,/g' > "$OUTPUT_FILE" echo "CDR export saved to: $OUTPUT_FILE"
ls -lh "$OUTPUT_FILE"
chmod +x export_cdr_csv.sh
./export_cdr_csv.sh
chmod +x export_cdr_csv.sh
./export_cdr_csv.sh
chmod +x export_cdr_csv.sh
./export_cdr_csv.sh
#!/bin/bash
# analyze_csv_cdr.sh CDR_DIR="/var/log/asterisk/cdr-csv" # Show recent CDR file structure
head -1 "$CDR_DIR"/*.csv | tail -1 # Count calls by status in last day
find "$CDR_DIR" -name "*.csv" -mtime -1 -exec cat {} \; | \ awk -F',' '{print $NF}' | sort | uniq -c | sort -rn
#!/bin/bash
# analyze_csv_cdr.sh CDR_DIR="/var/log/asterisk/cdr-csv" # Show recent CDR file structure
head -1 "$CDR_DIR"/*.csv | tail -1 # Count calls by status in last day
find "$CDR_DIR" -name "*.csv" -mtime -1 -exec cat {} \; | \ awk -F',' '{print $NF}' | sort | uniq -c | sort -rn
#!/bin/bash
# analyze_csv_cdr.sh CDR_DIR="/var/log/asterisk/cdr-csv" # Show recent CDR file structure
head -1 "$CDR_DIR"/*.csv | tail -1 # Count calls by status in last day
find "$CDR_DIR" -name "*.csv" -mtime -1 -exec cat {} \; | \ awk -F',' '{print $NF}' | sort | uniq -c | sort -rn
SELECT HOUR(call_date) AS hour_of_day, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND DAYOFWEEK(call_date) NOT IN (1, 7)
GROUP BY HOUR(call_date)
ORDER BY conversion_rate DESC;
SELECT HOUR(call_date) AS hour_of_day, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND DAYOFWEEK(call_date) NOT IN (1, 7)
GROUP BY HOUR(call_date)
ORDER BY conversion_rate DESC;
SELECT HOUR(call_date) AS hour_of_day, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate, ROUND(AVG(talk_sec), 2) AS avg_talk_sec
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND DAYOFWEEK(call_date) NOT IN (1, 7)
GROUP BY HOUR(call_date)
ORDER BY conversion_rate DESC;
SELECT phone_number, COUNT(*) AS call_count, COUNT(DISTINCT user) AS unique_agents, COUNT(DISTINCT campaign_id) AS unique_campaigns, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY phone_number
HAVING call_count > 100 AND conversion_rate < 5 AND unique_agents > 5
ORDER BY call_count DESC
LIMIT 20;
SELECT phone_number, COUNT(*) AS call_count, COUNT(DISTINCT user) AS unique_agents, COUNT(DISTINCT campaign_id) AS unique_campaigns, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY phone_number
HAVING call_count > 100 AND conversion_rate < 5 AND unique_agents > 5
ORDER BY call_count DESC
LIMIT 20;
SELECT phone_number, COUNT(*) AS call_count, COUNT(DISTINCT user) AS unique_agents, COUNT(DISTINCT campaign_id) AS unique_campaigns, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversion_rate
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY phone_number
HAVING call_count > 100 AND conversion_rate < 5 AND unique_agents > 5
ORDER BY call_count DESC
LIMIT 20;
SELECT vl1.lead_id, COUNT(*) AS call_attempts, vl1.status AS final_status, MIN(vl1.call_date) AS first_call, MAX(vl1.call_date) AS last_call, ROUND(TIMESTAMPDIFF(HOUR, MIN(vl1.call_date), MAX(vl1.call_date)), 1) AS hours_to_resolution
FROM vicidial_log vl1
WHERE vl1.call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND vl1.status IN ('SALE', 'XFER')
GROUP BY vl1.lead_id
HAVING call_attempts > 1
ORDER BY call_attempts DESC
LIMIT 50;
SELECT vl1.lead_id, COUNT(*) AS call_attempts, vl1.status AS final_status, MIN(vl1.call_date) AS first_call, MAX(vl1.call_date) AS last_call, ROUND(TIMESTAMPDIFF(HOUR, MIN(vl1.call_date), MAX(vl1.call_date)), 1) AS hours_to_resolution
FROM vicidial_log vl1
WHERE vl1.call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND vl1.status IN ('SALE', 'XFER')
GROUP BY vl1.lead_id
HAVING call_attempts > 1
ORDER BY call_attempts DESC
LIMIT 50;
SELECT vl1.lead_id, COUNT(*) AS call_attempts, vl1.status AS final_status, MIN(vl1.call_date) AS first_call, MAX(vl1.call_date) AS last_call, ROUND(TIMESTAMPDIFF(HOUR, MIN(vl1.call_date), MAX(vl1.call_date)), 1) AS hours_to_resolution
FROM vicidial_log vl1
WHERE vl1.call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND vl1.status IN ('SALE', 'XFER')
GROUP BY vl1.lead_id
HAVING call_attempts > 1
ORDER BY call_attempts DESC
LIMIT 50;
SELECT call_date, user, campaign_id, phone_number, status, recording_id, talk_sec, CASE WHEN recording_id IS NULL THEN 'NO RECORDING' WHEN talk_sec > 0 THEN 'RECORDED' ELSE 'NO CONVERSATION' END AS compliance_status
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 1 DAY) AND status IN ('SALE', 'XFER') AND recording_id IS NULL
ORDER BY call_date DESC;
SELECT call_date, user, campaign_id, phone_number, status, recording_id, talk_sec, CASE WHEN recording_id IS NULL THEN 'NO RECORDING' WHEN talk_sec > 0 THEN 'RECORDED' ELSE 'NO CONVERSATION' END AS compliance_status
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 1 DAY) AND status IN ('SALE', 'XFER') AND recording_id IS NULL
ORDER BY call_date DESC;
SELECT call_date, user, campaign_id, phone_number, status, recording_id, talk_sec, CASE WHEN recording_id IS NULL THEN 'NO RECORDING' WHEN talk_sec > 0 THEN 'RECORDED' ELSE 'NO CONVERSATION' END AS compliance_status
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 1 DAY) AND status IN ('SALE', 'XFER') AND recording_id IS NULL
ORDER BY call_date DESC;
#!/bin/bash
# daily_cdr_report.sh REPORT_DATE=$(date +"%Y-%m-%d")
RECIPIENT="[email protected]" mysql -u cdr_reporter -p'SecurePassword123!' asterisk > /tmp/daily_report.txt << EOF SELECT '=== DAILY CDR SUMMARY ===' as report;
SELECT CONCAT('Report Date: ', DATE(NOW()));
SELECT ''; SELECT 'CAMPAIGN PERFORMANCE' as section;
SELECT campaign_id, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conv_rate
FROM vicidial_log
WHERE DATE(call_date) = DATE(NOW()) - INTERVAL 1 DAY
GROUP BY campaign_id
ORDER BY conv_rate DESC; SELECT '';
SELECT 'TOP AGENTS' as section;
SELECT user, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(AVG(talk_sec), 2) AS avg_talk_sec
FROM vicidial_log
WHERE DATE(call_date) = DATE(NOW()) - INTERVAL 1 DAY AND user NOT IN ('SYSTEM', 'TRANSFER')
GROUP BY user
ORDER BY conversions DESC
LIMIT 10; SELECT '';
SELECT 'CALL QUALITY METRICS' as section;
SELECT COUNT(*) AS total_calls, COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) AS dropped_calls, ROUND(COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) / COUNT(*) * 100, 2) AS drop_rate
FROM vicidial_log
WHERE DATE(call_date) = DATE(NOW()) - INTERVAL 1 DAY; EOF mail -s "ViciDial CDR Report - $REPORT_DATE" "$RECIPIENT" < /tmp/daily_report.txt
#!/bin/bash
# daily_cdr_report.sh REPORT_DATE=$(date +"%Y-%m-%d")
RECIPIENT="[email protected]" mysql -u cdr_reporter -p'SecurePassword123!' asterisk > /tmp/daily_report.txt << EOF SELECT '=== DAILY CDR SUMMARY ===' as report;
SELECT CONCAT('Report Date: ', DATE(NOW()));
SELECT ''; SELECT 'CAMPAIGN PERFORMANCE' as section;
SELECT campaign_id, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conv_rate
FROM vicidial_log
WHERE DATE(call_date) = DATE(NOW()) - INTERVAL 1 DAY
GROUP BY campaign_id
ORDER BY conv_rate DESC; SELECT '';
SELECT 'TOP AGENTS' as section;
SELECT user, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(AVG(talk_sec), 2) AS avg_talk_sec
FROM vicidial_log
WHERE DATE(call_date) = DATE(NOW()) - INTERVAL 1 DAY AND user NOT IN ('SYSTEM', 'TRANSFER')
GROUP BY user
ORDER BY conversions DESC
LIMIT 10; SELECT '';
SELECT 'CALL QUALITY METRICS' as section;
SELECT COUNT(*) AS total_calls, COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) AS dropped_calls, ROUND(COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) / COUNT(*) * 100, 2) AS drop_rate
FROM vicidial_log
WHERE DATE(call_date) = DATE(NOW()) - INTERVAL 1 DAY; EOF mail -s "ViciDial CDR Report - $REPORT_DATE" "$RECIPIENT" < /tmp/daily_report.txt
#!/bin/bash
# daily_cdr_report.sh REPORT_DATE=$(date +"%Y-%m-%d")
RECIPIENT="[email protected]" mysql -u cdr_reporter -p'SecurePassword123!' asterisk > /tmp/daily_report.txt << EOF SELECT '=== DAILY CDR SUMMARY ===' as report;
SELECT CONCAT('Report Date: ', DATE(NOW()));
SELECT ''; SELECT 'CAMPAIGN PERFORMANCE' as section;
SELECT campaign_id, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conv_rate
FROM vicidial_log
WHERE DATE(call_date) = DATE(NOW()) - INTERVAL 1 DAY
GROUP BY campaign_id
ORDER BY conv_rate DESC; SELECT '';
SELECT 'TOP AGENTS' as section;
SELECT user, COUNT(*) AS calls, SUM(CASE WHEN status IN ('SALE', 'XFER') THEN 1 ELSE 0 END) AS conversions, ROUND(AVG(talk_sec), 2) AS avg_talk_sec
FROM vicidial_log
WHERE DATE(call_date) = DATE(NOW()) - INTERVAL 1 DAY AND user NOT IN ('SYSTEM', 'TRANSFER')
GROUP BY user
ORDER BY conversions DESC
LIMIT 10; SELECT '';
SELECT 'CALL QUALITY METRICS' as section;
SELECT COUNT(*) AS total_calls, COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) AS dropped_calls, ROUND(COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) / COUNT(*) * 100, 2) AS drop_rate
FROM vicidial_log
WHERE DATE(call_date) = DATE(NOW()) - INTERVAL 1 DAY; EOF mail -s "ViciDial CDR Report - $REPORT_DATE" "$RECIPIENT" < /tmp/daily_report.txt
crontab -e
0 8 * * * /root/daily_cdr_report.sh
0 8 * * * /root/daily_cdr_report.sh
0 8 * * * /root/daily_cdr_report.sh
#!/bin/bash
# cdr_anomaly_alert.sh THRESHOLD_DROP_RATE=15 # Alert if drop rate exceeds 15%
ALERT_EMAIL="[email protected]" DROP_RATE=$(mysql -u cdr_reporter -p'SecurePassword123!' asterisk -se " SELECT ROUND( COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) / COUNT(*) * 100, 2 ) FROM vicidial_log WHERE call_date >= DATE_SUB(NOW(), INTERVAL 1 HOUR);
") if (( $(echo "$DROP_RATE > $THRESHOLD_DROP_RATE" | bc -l) )); then echo "ALERT: Call drop rate is $DROP_RATE% (threshold: $THRESHOLD_DROP_RATE%)" | \ mail -s "CRITICAL: High Call Drop Rate Detected" "$ALERT_EMAIL"
fi
#!/bin/bash
# cdr_anomaly_alert.sh THRESHOLD_DROP_RATE=15 # Alert if drop rate exceeds 15%
ALERT_EMAIL="[email protected]" DROP_RATE=$(mysql -u cdr_reporter -p'SecurePassword123!' asterisk -se " SELECT ROUND( COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) / COUNT(*) * 100, 2 ) FROM vicidial_log WHERE call_date >= DATE_SUB(NOW(), INTERVAL 1 HOUR);
") if (( $(echo "$DROP_RATE > $THRESHOLD_DROP_RATE" | bc -l) )); then echo "ALERT: Call drop rate is $DROP_RATE% (threshold: $THRESHOLD_DROP_RATE%)" | \ mail -s "CRITICAL: High Call Drop Rate Detected" "$ALERT_EMAIL"
fi
#!/bin/bash
# cdr_anomaly_alert.sh THRESHOLD_DROP_RATE=15 # Alert if drop rate exceeds 15%
ALERT_EMAIL="[email protected]" DROP_RATE=$(mysql -u cdr_reporter -p'SecurePassword123!' asterisk -se " SELECT ROUND( COUNT(CASE WHEN length_in_sec < 5 THEN 1 END) / COUNT(*) * 100, 2 ) FROM vicidial_log WHERE call_date >= DATE_SUB(NOW(), INTERVAL 1 HOUR);
") if (( $(echo "$DROP_RATE > $THRESHOLD_DROP_RATE" | bc -l) )); then echo "ALERT: Call drop rate is $DROP_RATE% (threshold: $THRESHOLD_DROP_RATE%)" | \ mail -s "CRITICAL: High Call Drop Rate Detected" "$ALERT_EMAIL"
fi
*/15 * * * * /root/cdr_anomaly_alert.sh
*/15 * * * * /root/cdr_anomaly_alert.sh
*/15 * * * * /root/cdr_anomaly_alert.sh
asterisk -rx "cdr show status"
/etc/asterisk/cdr.conf
[general]
enable=yes
[general]
enable=yes
[general]
enable=yes
asterisk -rx "module reload cdr"
asterisk -rx "module reload cdr"
asterisk -rx "module reload cdr"
asterisk -rx "cel show status"
asterisk -rx "cel show status"
asterisk -rx "cel show status"
/etc/asterisk/extensions-vicidial.conf
exten => s,n,Set(CDR(userfield)=${CAMPAIGN_ID})
exten => s,n,Set(CHANNEL(language)=en)
exten => s,n,Set(CDR(userfield)=${CAMPAIGN_ID})
exten => s,n,Set(CHANNEL(language)=en)
exten => s,n,Set(CDR(userfield)=${CAMPAIGN_ID})
exten => s,n,Set(CHANNEL(language)=en)
ALTER TABLE vicidial_log ADD INDEX idx_call_date (call_date);
ALTER TABLE vicidial_log ADD INDEX idx_campaign_user (campaign_id, user);
ALTER TABLE vicidial_log ADD INDEX idx_status (status);
ALTER TABLE vicidial_log ADD INDEX idx_call_date (call_date);
ALTER TABLE vicidial_log ADD INDEX idx_campaign_user (campaign_id, user);
ALTER TABLE vicidial_log ADD INDEX idx_status (status);
ALTER TABLE vicidial_log ADD INDEX idx_call_date (call_date);
ALTER TABLE vicidial_log ADD INDEX idx_campaign_user (campaign_id, user);
ALTER TABLE vicidial_log ADD INDEX idx_status (status);
mysql -u root -p asterisk -e "SHOW INDEXES FROM vicidial_log;"
mysql -u root -p asterisk -e "SHOW INDEXES FROM vicidial_log;"
mysql -u root -p asterisk -e "SHOW INDEXES FROM vicidial_log;"
asterisk -rx
usermod -a -G asterisk $USER
newgrp asterisk
asterisk -rx "core show channels"
usermod -a -G asterisk $USER
newgrp asterisk
asterisk -rx "core show channels"
usermod -a -G asterisk $USER
newgrp asterisk
asterisk -rx "core show channels"
sudo asterisk -rx "core show channels"
sudo asterisk -rx "core show channels"
sudo asterisk -rx "core show channels"
cat > ~/.my.cnf << EOF
[client]
user=cdr_reporter
password=SecurePassword123!
host=localhost
database=asterisk
EOF chmod 600 ~/.my.cnf
cat > ~/.my.cnf << EOF
[client]
user=cdr_reporter
password=SecurePassword123!
host=localhost
database=asterisk
EOF chmod 600 ~/.my.cnf
cat > ~/.my.cnf << EOF
[client]
user=cdr_reporter
password=SecurePassword123!
host=localhost
database=asterisk
EOF chmod 600 ~/.my.cnf
mysql -e "SELECT COUNT(*) FROM vicidial_log;"
mysql -e "SELECT COUNT(*) FROM vicidial_log;"
mysql -e "SELECT COUNT(*) FROM vicidial_log;"
recording_id
grep -r "monitor_format" /etc/asterisk/*.conf | grep -v "^#"
grep -r "recording" /etc/asterisk/*.conf | grep -v "^#"
grep -r "monitor_format" /etc/asterisk/*.conf | grep -v "^#"
grep -r "recording" /etc/asterisk/*.conf | grep -v "^#"
grep -r "monitor_format" /etc/asterisk/*.conf | grep -v "^#"
grep -r "recording" /etc/asterisk/*.conf | grep -v "^#"
ls -lah /var/spool/asterisk/monitor/
ls -lah /var/spool/asterisk/vicidial/recordings/
ls -lah /var/spool/asterisk/monitor/
ls -lah /var/spool/asterisk/vicidial/recordings/
ls -lah /var/spool/asterisk/monitor/
ls -lah /var/spool/asterisk/vicidial/recordings/
mysql asterisk -e "SELECT variable_value FROM system_settings WHERE variable_name = 'recording_archive_days';"
mysql asterisk -e "SELECT variable_value FROM system_settings WHERE variable_name = 'recording_archive_days';"
mysql asterisk -e "SELECT variable_value FROM system_settings WHERE variable_name = 'recording_archive_days';"
vicidial_log
vicidial_closer_log - Access to the Asterisk server with root or sudo privileges
- MySQL/MariaDB client installed and credentials for the asterisk database
- Asterisk CLI access (typically via asterisk -rx command)
- ViciDial installation (version 2.14 or later, though techniques apply to 2.12+)
- Basic SQL knowledge (SELECT, WHERE, GROUP BY, JOIN)
- Understanding of Asterisk dialplan basics (extensions, applications, variables)
- Log file access at /var/log/asterisk/messages
- Cron or systemd timer for scheduled tasks (optional but recommended) - Stored in /var/log/asterisk/cdr-csv/ (CSV format)
- Fields: accountcode, src, dst, dcontext, channel, dstchannel, lastapp, lastdata, start, answer, end, duration, billsec, disposition, amaflags, uniqueid, userfield - Stored in MySQL tables: vicidial_log, vicidial_closer_log, vicidial_carrier_log
- Includes agent activity, campaign info, lead scoring, inbound routing
- Real-time accessible for live dashboards and compliance - uniqueid — Unique call identifier (matches Asterisk)
- lead_id — ViciDial lead identifier
- user — Agent username
- campaign_id — Campaign code
- call_date — Call timestamp
- length_in_sec — Total call duration
- talk_sec — Actual conversation time
- hold_sec — Time on hold
- status — Final disposition (SALE, XFER, NO ANS, etc.)
- phone_number — Dialed number
- called_number — Actual number called - Stale lead lists
- Calling outside business hours
- Network/codec issues on carrier side
- Asterisk routing misconfiguration - Detecting hung calls
- Verifying call routing during testing
- Identifying resource exhaustion - Check /etc/asterisk/cdr.conf: - Reload CDR module: - If using CEL (Channel Event Logging), verify it's enabled: - Create .my.cnf in home directory: - Now scripts work without -u and -p flags: - Check where recordings are stored: - Common locations: - Check ViciDial settings for recording retention: - Identify and fix call quality issues within hours, not weeks
- Optimize agent scheduling around peak conversion windows
- Detect fraud and spam sources automatically
- Verify compliance with recording and retention regulations - Benchmark agent and campaign performance objectively
- Correlate talk time, hold time, and conversions for each agent
- Track FCR (First Call Resolution) to identify training gaps
- Monitor real-time metrics with custom dashboards - Eliminate unprofitable campaigns and time windows
- Reduce waste on unreachable numbers and DNC violations
- Optimize trunk routing and carrier selection based on call success rates - Start with the raw data: vicidial_log and vicidial_closer_log contain everything you need
- Use database views: Create reusable views to simplify repeated analysis
- Establish baselines: Know your historical performance before chasing improvements
- Automate reporting: Scheduled queries and emails keep stakeholders aligned
- Monitor in real-time: Catch problems while they're happening, not in historical reports
- Index aggressively: Database performance degrades quickly without proper indexes
- Document your queries: Future you will thank present you