Revenue Discrepancies | Blue Frog Docs

Revenue Discrepancies

Diagnosing and fixing differences between analytics revenue and actual sales data.

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:

  1. Order exists in analytics
  2. Transaction ID matches
  3. Revenue value is correct
  4. Items array is complete
  5. 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)
}));

Further Reading

// SYS.FOOTER