Revenue Discrepancies
What This Means
Revenue discrepancies occur when the revenue reported in analytics platforms doesn't match your actual sales data from your e-commerce platform, payment processor, or accounting system.
Common Discrepancy Causes:
- Duplicate purchase events
- Missing purchase events
- Incorrect price/quantity data
- Currency conversion issues
- Tax/shipping calculation differences
- Order modifications after tracking
Impact Assessment
Business Impact
- False Metrics: Revenue reports can't be trusted
- Bad Decisions: Marketing spend based on wrong data
- Audit Issues: Analytics can't reconcile with financials
- Trust Erosion: Stakeholders lose confidence in data
Common Discrepancy Ranges
- Under 5% Variance: Acceptable, often due to timing/refunds
- 5-15% Variance: Investigation needed
- Over 15% Variance: Critical issue requiring immediate fix
How to Diagnose
Step 1: Identify Discrepancy Pattern
// Compare analytics vs source of truth
const analyticsRevenue = 125000;
const actualRevenue = 150000;
const variance = ((analyticsRevenue - actualRevenue) / actualRevenue) * 100;
console.log(`Variance: ${variance.toFixed(2)}%`);
// Under-reporting: Analytics < Actual (missing events)
// Over-reporting: Analytics > Actual (duplicates)
Step 2: Check for Duplicates
// Look for duplicate transaction IDs
const transactions = dataLayer
.filter(e => e.event === 'purchase')
.map(e => e.ecommerce?.transaction_id);
const duplicates = transactions.filter(
(id, i) => transactions.indexOf(id) !== i
);
if (duplicates.length) {
console.error('Duplicate transactions:', duplicates);
}
Step 3: Validate Sample Orders
Pick 10 random orders and verify:
- Order exists in analytics
- Transaction ID matches
- Revenue value is correct
- Items array is complete
- Currency is correct
Step 4: Check Event Firing
// Monitor purchase events
let purchaseCount = 0;
dataLayer.push = function(event) {
if (event.event === 'purchase') {
purchaseCount++;
console.log('Purchase event #' + purchaseCount, event);
if (purchaseCount > 1) {
console.error('Multiple purchase events detected!');
}
}
return Array.prototype.push.apply(this, arguments);
};
General Fixes
1. Prevent Duplicate Purchase Events
// Use sessionStorage to prevent duplicates
function trackPurchase(orderData) {
const orderId = orderData.ecommerce.transaction_id;
const trackedOrders = JSON.parse(
sessionStorage.getItem('tracked_orders') || '[]'
);
if (trackedOrders.includes(orderId)) {
console.warn('Order already tracked:', orderId);
return;
}
dataLayer.push({ ecommerce: null });
dataLayer.push(orderData);
trackedOrders.push(orderId);
sessionStorage.setItem('tracked_orders', JSON.stringify(trackedOrders));
}
2. Correct Price Formatting
// WRONG: Price includes currency symbol or commas
{ price: '$29.99' }
{ price: '1,299.00' }
// CORRECT: Clean number
function cleanPrice(price) {
if (typeof price === 'number') return price;
return parseFloat(
String(price)
.replace(/[^0-9.-]/g, '')
);
}
{ price: cleanPrice('$1,299.00') } // 1299
3. Handle Tax/Shipping Correctly
// Include tax and shipping as separate fields
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: order.id,
value: order.subtotal, // Product value only
tax: order.tax, // Separate tax
shipping: order.shipping, // Separate shipping
currency: 'USD',
items: order.items
}
});
// OR include everything in value
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: order.id,
value: order.total, // Includes tax + shipping
currency: 'USD',
items: order.items
}
});
// Be consistent! Don't mix approaches
4. Handle Currency Correctly
// Always include currency
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: order.id,
value: order.total,
currency: order.currency, // 'USD', 'EUR', 'GBP', etc.
items: order.items.map(item => ({
...item,
price: item.price // In same currency as value
}))
}
});
// For multi-currency stores, convert to base currency
function toBaseCurrency(amount, fromCurrency) {
const rates = { EUR: 1.08, GBP: 1.26, CAD: 0.74 };
return amount * (rates[fromCurrency] || 1);
}
5. Server-Side Validation
// Validate on server before sending to GA4
async function trackPurchaseServerSide(order) {
// Validate order exists in database
const dbOrder = await Order.findById(order.id);
if (!dbOrder) {
throw new Error('Order not found');
}
// Validate amounts match
if (Math.abs(dbOrder.total - order.total) > 0.01) {
console.error('Amount mismatch', {
expected: dbOrder.total,
received: order.total
});
return;
}
// Send validated data
await sendToGA4({
event: 'purchase',
ecommerce: {
transaction_id: dbOrder.id,
value: dbOrder.total,
currency: dbOrder.currency,
items: dbOrder.items
}
});
}
6. Handle Refunds and Modifications
// Track refunds separately
dataLayer.push({
event: 'refund',
ecommerce: {
transaction_id: refund.originalOrderId,
value: refund.amount,
currency: 'USD',
items: refund.items // Optional: specific items refunded
}
});
// For partial refunds, include only refunded items
dataLayer.push({
event: 'refund',
ecommerce: {
transaction_id: 'ORDER_123',
value: 29.99,
currency: 'USD',
items: [{
item_id: 'SKU_456',
price: 29.99,
quantity: 1
}]
}
});
Reconciliation Process
Daily Reconciliation
-- Compare GA4 purchases vs Orders table
SELECT
DATE(created_at) as date,
COUNT(*) as order_count,
SUM(total) as actual_revenue
FROM orders
WHERE status = 'completed'
GROUP BY DATE(created_at)
ORDER BY date DESC;
-- Compare with GA4 data exports
-- Look for missing transaction_ids
Order-Level Audit
// Export for reconciliation
const auditLog = orders.map(order => ({
order_id: order.id,
analytics_tracked: checkIfTracked(order.id),
expected_value: order.total,
tracked_value: getTrackedValue(order.id),
variance: calculateVariance(order)
}));