Building a Shipping Analytics Dashboard with Tracking Data
Raw tracking events are a goldmine of shipping intelligence — if you know how to analyze them. By building an analytics dashboard on top of your tracking data, you can identify underperforming carriers, optimize shipping routes, and reduce delivery times. This tutorial walks you through building a practical shipping analytics system.
What to Measure
Before writing code, define the metrics that matter:
Core Delivery Metrics
| Metric | Formula | Why It Matters |
|---|---|---|
| Average transit time | Sum of transit days / Total shipments | Baseline for delivery speed |
| On-time delivery rate | On-time deliveries / Total deliveries × 100 | Carrier reliability indicator |
| Exception rate | Shipments with exceptions / Total shipments × 100 | Quality of shipping experience |
| First-attempt success | Delivered on first try / Total deliveries × 100 | Last-mile efficiency |
Business Metrics
| Metric | Formula | Why It Matters |
|---|---|---|
| WISMO rate | Tracking inquiries / Total shipments × 100 | Customer experience quality |
| Carrier cost per delivery | Total carrier costs / Successful deliveries | Cost efficiency |
| Return shipping rate | Return shipments / Total shipments × 100 | Product or fulfillment issues |
Collecting Tracking Data
Use WhereParcel webhooks to stream tracking events into your analytics database:
// Webhook receiver - store events for analytics
app.post('/webhooks/tracking', async (req, res) => {
const event = req.body;
// Store raw event for detailed analysis
await db.trackingEvents.insert({
trackingNumber: event.data.trackingNumber,
carrier: event.data.carrier,
status: event.data.latestEvent.status,
location: event.data.latestEvent.location,
timestamp: event.data.latestEvent.timestamp,
receivedAt: new Date(),
});
// Update shipment summary record
await updateShipmentSummary(event.data);
res.status(200).json({ received: true });
});
async function updateShipmentSummary(data) {
const summary = await db.shipmentSummaries.findOne({
trackingNumber: data.trackingNumber,
});
const updates = {
currentStatus: data.latestEvent.status,
lastUpdated: new Date(),
eventCount: (summary?.eventCount || 0) + 1,
};
// Record milestone timestamps
if (data.latestEvent.status === 'picked_up' && !summary?.pickedUpAt) {
updates.pickedUpAt = data.latestEvent.timestamp;
}
if (data.latestEvent.status === 'delivered') {
updates.deliveredAt = data.latestEvent.timestamp;
updates.transitDays = daysBetween(summary?.pickedUpAt, data.latestEvent.timestamp);
}
await db.shipmentSummaries.upsert(
{ trackingNumber: data.trackingNumber },
{ $set: updates }
);
}
Building the Analytics Queries
Carrier Performance Comparison
async function getCarrierPerformance(dateRange) {
const results = await db.shipmentSummaries.aggregate([
{
$match: {
deliveredAt: { $gte: dateRange.start, $lte: dateRange.end },
},
},
{
$group: {
_id: '$carrier',
totalShipments: { $sum: 1 },
avgTransitDays: { $avg: '$transitDays' },
onTimeCount: {
$sum: { $cond: [{ $lte: ['$transitDays', '$estimatedDays'] }, 1, 0] },
},
exceptionCount: {
$sum: { $cond: ['$hasException', 1, 0] },
},
},
},
{
$project: {
carrier: '$_id',
totalShipments: 1,
avgTransitDays: { $round: ['$avgTransitDays', 1] },
onTimeRate: {
$round: [{ $multiply: [{ $divide: ['$onTimeCount', '$totalShipments'] }, 100] }, 1],
},
exceptionRate: {
$round: [{ $multiply: [{ $divide: ['$exceptionCount', '$totalShipments'] }, 100] }, 1],
},
},
},
{ $sort: { onTimeRate: -1 } },
]);
return results;
}
Transit Time by Route
async function getTransitTimeByRoute(carrier, dateRange) {
return db.shipmentSummaries.aggregate([
{
$match: {
carrier,
deliveredAt: { $gte: dateRange.start, $lte: dateRange.end },
},
},
{
$group: {
_id: {
origin: '$originCountry',
destination: '$destinationCountry',
},
avgDays: { $avg: '$transitDays' },
minDays: { $min: '$transitDays' },
maxDays: { $max: '$transitDays' },
p90Days: { $percentile: { input: '$transitDays', p: [0.9], method: 'approximate' } },
count: { $sum: 1 },
},
},
{ $match: { count: { $gte: 10 } } }, // Only statistically significant routes
{ $sort: { avgDays: 1 } },
]);
}
Daily Delivery Trends
async function getDailyDeliveryTrends(dateRange) {
return db.shipmentSummaries.aggregate([
{
$match: {
deliveredAt: { $gte: dateRange.start, $lte: dateRange.end },
},
},
{
$group: {
_id: { $dateToString: { format: '%Y-%m-%d', date: '$deliveredAt' } },
delivered: { $sum: 1 },
avgTransitDays: { $avg: '$transitDays' },
exceptions: { $sum: { $cond: ['$hasException', 1, 0] } },
},
},
{ $sort: { _id: 1 } },
]);
}
Dashboard Layout
Organize your dashboard into logical sections:
Overview Section
- Total shipments (this period vs previous)
- Average transit time trend
- Overall on-time delivery rate
- Active exception count
Carrier Performance Section
- Side-by-side carrier comparison table
- Transit time distribution chart per carrier
- Exception rate by carrier over time
Route Analysis Section
- Heat map of transit times by origin/destination
- Top 10 fastest and slowest routes
- Route-level exception patterns
Exception Monitoring Section
- Active exceptions by type and severity
- Exception resolution time trends
- Carrier-specific exception patterns
Practical Insights from Analytics
Here are real patterns you might discover:
1. Carrier Selection Optimization
Carrier A: Avg 3.2 days, 94% on-time, $8.50/shipment
Carrier B: Avg 2.8 days, 91% on-time, $12.00/shipment
Carrier C: Avg 4.1 days, 97% on-time, $6.00/shipment
For time-sensitive shipments, Carrier B is fastest. For cost optimization with reliability, Carrier C offers the best value. These decisions become data-driven rather than assumption-based.
2. Route-Specific Carrier Performance
A carrier might be excellent domestically but poor internationally. Route-level analysis reveals this:
Carrier A: US → US: 2.1 days avg (excellent)
Carrier A: US → KR: 8.5 days avg (poor — competitors average 5.2 days)
3. Seasonal Patterns
Holiday seasons, monsoon periods, and major sales events all affect transit times. Historical analytics help you plan for:
- Peak season carrier capacity
- Customer communication during delays
- Inventory positioning to reduce transit distances
Getting Started
- Set up webhook collection — Start capturing tracking events today
- Define your metrics — Choose 3-5 key metrics to track first
- Build basic queries — Carrier comparison and daily trends are the highest value
- Iterate — Add more dimensions as you identify patterns worth tracking
The data you need is already flowing through your tracking integration. Turning it into analytics is the step that transforms shipping from a cost center into a competitive advantage.
For setting up data collection via webhooks, see our Webhook Best Practices guide.