Most GA4 eCommerce implementations look complete on the surface but have silent gaps that corrupt conversion reporting. Purchases are double-counted. Revenue figures in GA4 differ from your payment processor by 15%. Add-to-cart events fire for out-of-stock items. Refund events never reach GA4. These problems are common, hard to spot without systematic auditing, and expensive when they cause misoptimization of paid campaigns. This guide covers the six most common GA4 eCommerce tracking gaps and the verification queries to find them in BigQuery.

Purchase Event Duplication

The most damaging eCommerce tracking gap is duplicate purchase events. This happens when your order confirmation page is visited more than once for the same order — a user refreshes the page, uses the browser back button, or your email confirmation links back to the order confirmation page. Each visit fires a new purchase event with the same or recalculated revenue.

The fix is deduplication using the transaction_id parameter. Every purchase event must include a unique transaction_id. In GA4, if two purchase events have the same transaction_id within a short time window, GA4’s deduplication logic may catch it — but this is not guaranteed, and it does not work across sessions. The reliable fix is server-side deduplication: store a record of processed transaction IDs and only push the purchase event to the dataLayer if the transaction_id has not been seen before in the current browser session.

// Client-side purchase deduplication
function trackPurchase(orderData) {
  const processedKey = 'processed_order_' + orderData.transaction_id;
  if (sessionStorage.getItem(processedKey)) {
    return; // Already tracked this order
  }
  sessionStorage.setItem(processedKey, '1');
  dataLayer.push({
    event: 'purchase',
    ecommerce: {
      transaction_id: orderData.transaction_id,
      value: orderData.revenue,
      tax: orderData.tax,
      shipping: orderData.shipping,
      currency: 'USD',
      items: orderData.items
    }
  });
}

Revenue Discrepancy Sources

GA4 revenue figures differ from payment processor reports for several reasons. First, currency: if your site sells in multiple currencies and you pass local currency values without consistent conversion, GA4 accumulates revenue in mixed currencies. Always pass the currency parameter with every purchase event and use a single reporting currency. Second, discount codes: if your purchase event value includes discounts but your payment processor reports pre-discount revenue, the two numbers will differ. Decide which number to track (post-discount is more accurate) and apply it consistently. Third, returns: GA4 refund events must be sent explicitly — they do not automatically sync from your payment processor.

Finding Duplicate Purchases in BigQuery

img
-- Detect duplicate transaction IDs
SELECT
  (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') AS transaction_id,
  COUNT(*) AS purchase_event_count,
  SUM((SELECT value.double_value FROM UNNEST(event_params) WHERE key = 'value')) AS total_revenue_recorded
FROM `your_project.analytics_XXXXXX.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20240101' AND '20240131'
  AND event_name = 'purchase'
GROUP BY transaction_id
HAVING COUNT(*) > 1
ORDER BY purchase_event_count DESC

Any row returned by this query is a duplicate purchase. The total_revenue_recorded column shows how much revenue was inflated by the duplication. If you find widespread duplication, calculate the true revenue by joining against your order management system’s transaction records.

Implementing GA4 Refund Events

GA4 refunds must be sent via the Measurement Protocol since refunds typically happen in your back-end after the user has left the site. The Measurement Protocol endpoint accepts refund events with the same transaction_id as the original purchase:

import requests

def send_refund_to_ga4(measurement_id, api_secret, client_id, transaction_id, refund_amount):
    endpoint = f'https://www.google-analytics.com/mp/collect?measurement_id={measurement_id}&api_secret={api_secret}'
    payload = {
        'client_id': client_id,
        'events': [{
            'name': 'refund',
            'params': {
                'transaction_id': transaction_id,
                'value': refund_amount,
                'currency': 'USD'
            }
        }]
    }
    response = requests.post(endpoint, json=payload)
    return response.status_code

You need the client_id (GA4’s device identifier, not the user ID) to associate the refund with the original session. Store the GA4 client_id alongside each order in your database at purchase time. Retrieve it when processing refunds. Without the client_id, GA4 cannot associate the refund with the original purchase session, though GA4 will still record the refund event and subtract the revenue from totals.

Item-Level Data Gaps

GA4’s items array in eCommerce events must be populated with accurate item_id, item_name, item_category, price, and quantity fields. Common gaps: item_category is missing for products that lack a category assignment in your CMS, price reflects post-discount price on add_to_cart but pre-discount on purchase creating inconsistent funnel metrics, or items array is empty on checkout events because cart data is not available in the checkout template. Audit your items array completeness in BigQuery by checking what percentage of purchase events have at least one item with a non-null item_id, and what percentage of item records have null values in each required field.

guide

Leave a Comment