Shipping Information Tracking | Blue Frog Docs

Shipping Information Tracking

Diagnosing and fixing shipping tier, cost, and delivery tracking issues in GA4 to optimize logistics and understand shipping impact on conversions.

Shipping Information Tracking

What This Means

Shipping tracking issues occur when GA4 doesn't properly capture shipping methods, costs, delivery times, and shipping-related customer behavior. Without accurate shipping data, you can't optimize delivery options, measure free shipping promotions, or understand how shipping costs impact purchase decisions.

Key Shipping Events:

  • add_shipping_info - Shipping method selected during checkout
  • purchase - Should include shipping cost and method
  • Custom events for shipping-related interactions

Required Parameters:

  • shipping - Shipping cost in purchase event
  • shipping_tier - Shipping method name (e.g., "Standard", "Express", "Free Shipping")

Impact Assessment

Business Impact

  • Shipping Strategy Unknown: Can't measure which options customers prefer
  • Free Shipping ROI: Don't know if free shipping promotions drive conversions
  • Lost Optimization: Can't test shipping thresholds for maximum profit
  • Carrier Performance: Can't compare delivery methods
  • Geographic Insights Missing: Don't know shipping costs by region

Analytics Impact

  • Incomplete Purchase Data: Revenue reports missing shipping breakdown
  • Funnel Analysis Gaps: Can't identify shipping-related abandonment
  • Attribution Issues: Can't credit free shipping for conversions
  • Poor Segmentation: Can't analyze customers by preferred shipping method

Common Causes

Implementation Issues

  • add_shipping_info event not firing
  • Shipping cost missing from purchase event
  • shipping_tier parameter not included
  • Single-page checkouts skip shipping event

Technical Problems

  • Third-party shipping calculators bypass tracking
  • Real-time rates loaded after event fires
  • Multiple shipping methods not differentiated
  • Shipping costs calculated server-side but not tracked

Data Quality Issues

  • Shipping costs include tax/fees inconsistently
  • Free shipping not tracked with $0 value
  • Shipping tier names not standardized
  • International shipping not distinguished

How to Diagnose

Check for Shipping Events

// Monitor shipping-related events
const shippingEvents = ['add_shipping_info', 'purchase'];
const shippingLog = [];

window.dataLayer = window.dataLayer || [];
const originalPush = dataLayer.push;

dataLayer.push = function(...args) {
  args.forEach(event => {
    if (shippingEvents.includes(event.event)) {
      const shipping = event.ecommerce?.shipping;
      const shippingTier = event.ecommerce?.shipping_tier;

      shippingLog.push({
        event: event.event,
        shipping_cost: shipping,
        shipping_tier: shippingTier
      });

      console.log('🚚 Shipping Event:', event.event, {
        cost: shipping,
        tier: shippingTier,
        hasShipping: typeof shipping !== 'undefined',
        hasTier: !!shippingTier
      });

      // Validate required fields
      if (event.event === 'add_shipping_info' && !shippingTier) {
        console.error('⚠️ Missing shipping_tier in add_shipping_info event');
      }

      if (event.event === 'purchase' && typeof shipping === 'undefined') {
        console.error('⚠️ Missing shipping cost in purchase event');
      }
    }
  });
  return originalPush.apply(this, args);
};

// Summary after checkout
setTimeout(() => {
  console.log('Shipping Tracking Summary:');
  console.table(shippingLog);
}, 30000);

Validate Shipping in Purchase Event

// Check if purchase includes shipping data
function validateShippingInPurchase() {
  const purchases = dataLayer.filter(e => e.event === 'purchase');
  const latestPurchase = purchases[purchases.length - 1];

  if (!latestPurchase) {
    console.log('No purchase events found yet');
    return;
  }

  const ecom = latestPurchase.ecommerce;

  console.log('Purchase Shipping Validation:');
  console.log('- Shipping cost:', ecom?.shipping ?? '⚠️ MISSING');
  console.log('- Shipping tier:', ecom?.shipping_tier ?? '⚠️ MISSING');
  console.log('- Transaction total:', ecom?.value);

  // Check if shipping is included in total
  const itemsTotal = ecom?.items?.reduce((sum, item) =>
    sum + (item.price * item.quantity), 0
  ) || 0;
  const tax = ecom?.tax || 0;
  const shipping = ecom?.shipping || 0;
  const calculatedTotal = itemsTotal + tax + shipping;

  if (Math.abs(calculatedTotal - ecom.value) > 0.01) {
    console.warn('⚠️ Total mismatch - shipping may be incorrectly calculated');
    console.log(`Items: ${itemsTotal} + Tax: ${tax} + Shipping: ${shipping} = ${calculatedTotal}`);
    console.log(`Reported total: ${ecom.value}`);
  } else {
    console.log('✓ Shipping correctly included in transaction total');
  }
}

validateShippingInPurchase();

GA4 DebugView Checklist

  1. Shipping Selection: add_shipping_info fires when method selected
  2. Shipping Tier: Parameter includes method name
  3. Shipping Cost: Numeric value present
  4. Purchase Event: Includes both shipping and shipping_tier
  5. Free Shipping: Tracked with shipping: 0

General Fixes

1. Track Shipping Selection Event

// Fire when customer selects shipping method
function trackAddShippingInfo(shippingMethod, cartData) {
  dataLayer.push({ ecommerce: null });
  dataLayer.push({
    event: 'add_shipping_info',
    ecommerce: {
      currency: 'USD',
      value: cartData.total + shippingMethod.cost,
      shipping: shippingMethod.cost,
      shipping_tier: shippingMethod.name,
      items: cartData.items.map(item => ({
        item_id: item.id,
        item_name: item.name,
        price: item.price,
        quantity: item.quantity
      }))
    },
    // Also as top-level parameters for easier reporting
    shipping_cost: shippingMethod.cost,
    shipping_method: shippingMethod.name,
    shipping_carrier: shippingMethod.carrier,
    estimated_delivery: shippingMethod.estimatedDays
  });

  // Store for use in purchase event
  sessionStorage.setItem('selected_shipping', JSON.stringify(shippingMethod));
}

// Attach to shipping method selection
document.querySelectorAll('input[name="shipping_method"]').forEach(radio => {
  radio.addEventListener('change', (e) => {
    const shippingMethod = {
      id: e.target.value,
      name: e.target.dataset.shippingName,
      cost: parseFloat(e.target.dataset.shippingCost),
      carrier: e.target.dataset.carrier,
      estimatedDays: e.target.dataset.estimatedDays
    };

    const cartData = getCartData();
    trackAddShippingInfo(shippingMethod, cartData);
  });
});

2. Include Shipping in Purchase Event

// Track purchase with complete shipping information
function trackPurchase(orderData) {
  const shippingMethod = JSON.parse(sessionStorage.getItem('selected_shipping')) || {};

  dataLayer.push({ ecommerce: null });
  dataLayer.push({
    event: 'purchase',
    ecommerce: {
      transaction_id: orderData.id,
      value: orderData.total, // Total including shipping
      tax: orderData.tax,
      shipping: orderData.shippingCost,
      shipping_tier: shippingMethod.name || orderData.shippingMethod,
      currency: 'USD',
      items: orderData.items.map(item => ({
        item_id: item.id,
        item_name: item.name,
        price: item.price,
        quantity: item.quantity
      }))
    },
    // Additional shipping details
    shipping_method: shippingMethod.name || orderData.shippingMethod,
    shipping_carrier: shippingMethod.carrier || orderData.carrier,
    shipping_cost: orderData.shippingCost,
    delivery_type: orderData.shippingCost === 0 ? 'free_shipping' : 'paid_shipping'
  });

  // Clear stored shipping data
  sessionStorage.removeItem('selected_shipping');
}

// Call on order confirmation page
if (isOrderConfirmationPage()) {
  const orderData = getOrderData();
  trackPurchase(orderData);
}

3. Track Free Shipping Qualification

// Track when customer qualifies for free shipping
function checkFreeShippingThreshold(cartTotal, threshold = 50) {
  const previousTotal = parseFloat(sessionStorage.getItem('previous_cart_total')) || 0;
  const justQualified = previousTotal < threshold && cartTotal >= threshold;
  const justLost = previousTotal >= threshold && cartTotal < threshold;

  if (justQualified) {
    dataLayer.push({
      event: 'free_shipping_qualified',
      cart_total: cartTotal,
      threshold: threshold,
      amount_to_qualify: 0
    });

    // Show notification
    showNotification('🎉 You qualify for free shipping!');
  } else if (justLost) {
    dataLayer.push({
      event: 'free_shipping_lost',
      cart_total: cartTotal,
      threshold: threshold,
      amount_to_qualify: threshold - cartTotal
    });
  }

  sessionStorage.setItem('previous_cart_total', cartTotal.toString());

  // Return progress toward free shipping
  return {
    qualified: cartTotal >= threshold,
    amountRemaining: Math.max(0, threshold - cartTotal),
    percentage: Math.min(100, (cartTotal / threshold) * 100)
  };
}

// Update on cart changes
window.addEventListener('cartUpdated', (e) => {
  const cartTotal = e.detail.total;
  const freeShippingStatus = checkFreeShippingThreshold(cartTotal, 50);

  // Update UI
  updateFreeShippingProgress(freeShippingStatus);
});

4. Track Shipping Cost Impact

// Track when shipping cost influences behavior
function trackShippingDecision(action, shippingCost, context) {
  dataLayer.push({
    event: 'shipping_decision',
    decision_type: action, // 'selected', 'changed', 'abandoned'
    shipping_cost: shippingCost,
    context: context, // 'checkout', 'cart'
    cart_value: getCartTotal(),
    shipping_to_cart_ratio: shippingCost / getCartTotal()
  });
}

// Track when expensive shipping might cause abandonment
document.querySelectorAll('input[name="shipping_method"]').forEach(radio => {
  radio.addEventListener('change', (e) => {
    const shippingCost = parseFloat(e.target.dataset.shippingCost);
    const cartTotal = getCartTotal();

    // Flag potentially problematic shipping costs
    if (shippingCost > cartTotal * 0.3) { // Shipping > 30% of cart
      dataLayer.push({
        event: 'high_shipping_cost',
        shipping_cost: shippingCost,
        cart_total: cartTotal,
        shipping_ratio: shippingCost / cartTotal
      });
    }

    trackShippingDecision('selected', shippingCost, 'checkout');
  });
});

// Track if user abandons after seeing shipping costs
let shippingCostsSeen = false;

window.addEventListener('beforeunload', () => {
  if (shippingCostsSeen && !hasCompletedPurchase()) {
    const selectedShipping = getSelectedShippingCost();

    dataLayer.push({
      event: 'shipping_abandonment',
      shipping_cost: selectedShipping,
      cart_total: getCartTotal(),
      checkout_step: getCurrentCheckoutStep()
    });
  }
});

5. Standardize Shipping Tier Names

// Create consistent shipping tier naming
const SHIPPING_TIERS = {
  free: {
    name: 'Free Shipping',
    code: 'FREE',
    cost: 0,
    estimatedDays: '5-7'
  },
  standard: {
    name: 'Standard Shipping',
    code: 'STANDARD',
    cost: 5.99,
    estimatedDays: '3-5'
  },
  express: {
    name: 'Express Shipping',
    code: 'EXPRESS',
    cost: 14.99,
    estimatedDays: '1-2'
  },
  overnight: {
    name: 'Overnight Shipping',
    code: 'OVERNIGHT',
    cost: 24.99,
    estimatedDays: '1'
  },
  international: {
    name: 'International Shipping',
    code: 'INTERNATIONAL',
    cost: null, // Calculated
    estimatedDays: '7-14'
  }
};

// Get shipping tier by ID
function getShippingTier(tierId, calculatedCost = null) {
  const tier = SHIPPING_TIERS[tierId];
  return {
    ...tier,
    cost: calculatedCost ?? tier.cost
  };
}

// Usage
const selectedTier = getShippingTier('express');
trackAddShippingInfo(selectedTier, cartData);

6. Track Shipping Address Information

// Track shipping destination (anonymized)
function trackShippingAddress(addressData) {
  // Never send PII - only aggregate data
  const shippingInfo = {
    country: addressData.country,
    state: addressData.state,
    postal_code_prefix: addressData.postalCode?.substring(0, 3), // First 3 digits only
    is_international: addressData.country !== 'US',
    is_rural: isRuralArea(addressData.postalCode),
    is_po_box: addressData.address1?.toLowerCase().includes('po box')
  };

  dataLayer.push({
    event: 'shipping_address_entered',
    ...shippingInfo
  });

  // Calculate and track shipping zones
  const zone = getShippingZone(addressData);
  dataLayer.push({
    event: 'shipping_zone_determined',
    shipping_zone: zone,
    zone_type: zone.type, // 'local', 'regional', 'national', 'international'
    estimated_transit_days: zone.transitDays
  });

  return shippingInfo;
}

// Shipping form submission
document.querySelector('#shipping-form')?.addEventListener('submit', (e) => {
  const addressData = {
    country: document.querySelector('[name="country"]').value,
    state: document.querySelector('[name="state"]').value,
    postalCode: document.querySelector('[name="postal_code"]').value,
    address1: document.querySelector('[name="address1"]').value
  };

  trackShippingAddress(addressData);
});

7. Server-Side Shipping Event Tracking

// Node.js example - Track shipping from order processing
const express = require('express');
const router = express.Router();

// Calculate shipping rates
router.post('/api/shipping/calculate', async (req, res) => {
  const { address, cartItems, cartTotal } = req.body;

  const rates = await getShippingRates(address, cartItems);

  // Track that shipping rates were requested
  await sendToGA4({
    client_id: req.body.clientId,
    events: [{
      name: 'shipping_rates_requested',
      params: {
        country: address.country,
        state: address.state,
        cart_total: cartTotal,
        num_options: rates.length,
        lowest_rate: Math.min(...rates.map(r => r.cost)),
        highest_rate: Math.max(...rates.map(r => r.cost))
      }
    }]
  });

  res.json({ rates });
});

// Create shipping label
router.post('/api/shipping/create-label', async (req, res) => {
  const { orderId, shippingMethod } = req.body;

  const label = await createShippingLabel(orderId, shippingMethod);

  // Track shipping label creation
  await sendToGA4({
    client_id: 'server',
    events: [{
      name: 'shipping_label_created',
      params: {
        transaction_id: orderId,
        shipping_carrier: shippingMethod.carrier,
        shipping_service: shippingMethod.service,
        tracking_number: label.trackingNumber,
        label_cost: label.cost
      }
    }]
  });

  res.json({ label });
});

// Track shipment status updates
router.post('/api/shipping/track', async (req, res) => {
  const { trackingNumber } = req.body;

  const tracking = await getTrackingInfo(trackingNumber);

  // Track delivery status
  await sendToGA4({
    client_id: 'server',
    events: [{
      name: 'shipment_status_update',
      params: {
        tracking_number: trackingNumber,
        status: tracking.status, // 'in_transit', 'out_for_delivery', 'delivered', 'exception'
        location: tracking.currentLocation,
        estimated_delivery: tracking.estimatedDelivery
      }
    }]
  });

  res.json({ tracking });
});

// Track successful delivery
router.post('/webhook/shipment-delivered', async (req, res) => {
  const { orderId, trackingNumber, deliveredAt } = req.body;

  const order = await getOrder(orderId);

  await sendToGA4({
    client_id: 'server',
    events: [{
      name: 'shipment_delivered',
      params: {
        transaction_id: orderId,
        tracking_number: trackingNumber,
        delivered_at: deliveredAt,
        days_to_deliver: calculateDaysToDeliver(order.createdAt, deliveredAt),
        on_time: isOnTime(order.estimatedDelivery, deliveredAt)
      }
    }]
  });

  res.sendStatus(200);
});

async function sendToGA4(data) {
  const measurementId = process.env.GA4_MEASUREMENT_ID;
  const apiSecret = process.env.GA4_API_SECRET;

  await fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`, {
    method: 'POST',
    body: JSON.stringify(data)
  });
}

module.exports = router;

Platform-Specific Guides

Shopify

<!-- Checkout - can only be edited with Shopify Plus -->
<!-- Use checkout.liquid or Shopify Scripts -->

<script>
// Track shipping method selection
document.addEventListener('DOMContentLoaded', function() {
  // For standard Shopify checkout, use Shopify's checkout object
  if (typeof Shopify !== 'undefined' && Shopify.Checkout) {
    var shippingRate = Shopify.Checkout.shippingRate;

    if (shippingRate) {
      dataLayer.push({ ecommerce: null });
      dataLayer.push({
        event: 'add_shipping_info',
        ecommerce: {
          currency: Shopify.Checkout.currency,
          value: parseFloat(Shopify.Checkout.totalPrice),
          shipping: parseFloat(shippingRate.price),
          shipping_tier: shippingRate.name,
          items: Shopify.Checkout.lineItems.map(function(item, index) {
            return {
              item_id: item.sku || item.variant_id,
              item_name: item.title,
              price: parseFloat(item.price),
              quantity: item.quantity,
              index: index
            };
          })
        },
        shipping_method: shippingRate.name,
        shipping_carrier: shippingRate.source || 'Shopify'
      });
    }
  }
});

// Store shipping for confirmation page
sessionStorage.setItem('shopify_shipping', JSON.stringify({
  cost: parseFloat({{ checkout.shipping_price | money_without_currency | remove: ',' }}),
  method: '{{ checkout.shipping_method.title | escape }}'
}));
</script>

<!-- Thank You page - Edit via Settings > Checkout > Order status page -->
<script>
// Track purchase with shipping info
dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: 'purchase',
  ecommerce: {
    transaction_id: '{{ order.order_number }}',
    value: {{ order.total_price | money_without_currency | remove: ',' }},
    tax: {{ order.tax_price | money_without_currency | remove: ',' }},
    shipping: {{ order.shipping_price | money_without_currency | remove: ',' }},
    shipping_tier: '{{ order.shipping_method.title | escape }}',
    currency: '{{ order.currency }}',
    items: [
      {% for line_item in order.line_items %}
      {
        item_id: '{{ line_item.sku | default: line_item.product_id }}',
        item_name: '{{ line_item.title | escape }}',
        price: {{ line_item.price | money_without_currency | remove: ',' }},
        quantity: {{ line_item.quantity }}
      }{% unless forloop.last %},{% endunless %}
      {% endfor %}
    ]
  },
  shipping_method: '{{ order.shipping_method.title | escape }}',
  shipping_cost: {{ order.shipping_price | money_without_currency | remove: ',' }},
  delivery_type: {% if order.shipping_price == 0 %}'free_shipping'{% else %}'paid_shipping'{% endif %}
});
</script>

WooCommerce

// Add to functions.php or custom plugin

// Track shipping method selection
add_action('woocommerce_checkout_update_order_review', function($post_data) {
    parse_str($post_data, $data);

    if (isset($data['shipping_method']) && is_array($data['shipping_method'])) {
        $shipping_method_id = array_shift($data['shipping_method']);
        $shipping_methods = WC()->shipping()->get_shipping_methods();

        foreach (WC()->shipping()->get_packages() as $package) {
            foreach ($package['rates'] as $rate) {
                if ($rate->id === $shipping_method_id) {
                    ?>
                    <script>
                    dataLayer.push({ ecommerce: null });
                    dataLayer.push({
                        event: 'add_shipping_info',
                        ecommerce: {
                            currency: '<?php echo get_woocommerce_currency(); ?>',
                            value: <?php echo WC()->cart->get_total(''); ?>,
                            shipping: <?php echo $rate->cost; ?>,
                            shipping_tier: '<?php echo esc_js($rate->label); ?>',
                            items: [
                                <?php foreach (WC()->cart->get_cart() as $item): ?>
                                {
                                    item_id: '<?php echo $item['data']->get_sku() ?: $item['product_id']; ?>',
                                    item_name: '<?php echo esc_js($item['data']->get_name()); ?>',
                                    price: <?php echo $item['data']->get_price(); ?>,
                                    quantity: <?php echo $item['quantity']; ?>
                                },
                                <?php endforeach; ?>
                            ]
                        },
                        shipping_method: '<?php echo esc_js($rate->label); ?>',
                        shipping_carrier: '<?php echo esc_js($rate->method_id); ?>'
                    });
                    </script>
                    <?php
                    break;
                }
            }
        }
    }
});

// Track purchase with shipping
add_action('woocommerce_thankyou', function($order_id) {
    $order = wc_get_order($order_id);
    $shipping_methods = $order->get_shipping_methods();
    $shipping_method = !empty($shipping_methods) ? reset($shipping_methods) : null;
    ?>
    <script>
    dataLayer.push({ ecommerce: null });
    dataLayer.push({
        event: 'purchase',
        ecommerce: {
            transaction_id: '<?php echo $order->get_order_number(); ?>',
            value: <?php echo $order->get_total(); ?>,
            tax: <?php echo $order->get_total_tax(); ?>,
            shipping: <?php echo $order->get_shipping_total(); ?>,
            shipping_tier: '<?php echo $shipping_method ? esc_js($shipping_method->get_name()) : ''; ?>',
            currency: '<?php echo $order->get_currency(); ?>',
            items: [
                <?php foreach ($order->get_items() as $item): ?>
                {
                    item_id: '<?php echo $item->get_product()->get_sku() ?: $item->get_product_id(); ?>',
                    item_name: '<?php echo esc_js($item->get_name()); ?>',
                    price: <?php echo $item->get_total() / $item->get_quantity(); ?>,
                    quantity: <?php echo $item->get_quantity(); ?>
                },
                <?php endforeach; ?>
            ]
        },
        shipping_method: '<?php echo $shipping_method ? esc_js($shipping_method->get_name()) : ''; ?>',
        shipping_cost: <?php echo $order->get_shipping_total(); ?>,
        delivery_type: <?php echo $order->get_shipping_total() == 0 ? "'free_shipping'" : "'paid_shipping'"; ?>
    });
    </script>
    <?php
}, 10, 1);

// Track free shipping threshold
add_action('woocommerce_cart_updated', function() {
    $cart_total = WC()->cart->get_subtotal();
    $free_shipping_threshold = 50; // Configure this

    if ($cart_total >= $free_shipping_threshold) {
        ?>
        <script>
        dataLayer.push({
            event: 'free_shipping_qualified',
            cart_total: <?php echo $cart_total; ?>,
            threshold: <?php echo $free_shipping_threshold; ?>
        });
        </script>
        <?php
    }
});

BigCommerce

// Add to theme/assets/js/theme/global.js
import utils from '@bigcommerce/stencil-utils';

class ShippingTracking {
    constructor() {
        this.init();
    }

    init() {
        this.trackShippingSelection();
    }

    trackShippingSelection() {
        // Monitor shipping method changes
        $(document).on('change', 'input[name="shippingProviderName"]', (event) => {
            const selectedShipping = $(event.target);
            const shippingCost = parseFloat(selectedShipping.data('shippingCost'));
            const shippingName = selectedShipping.val();

            utils.api.cart.getCart({}, (err, response) => {
                if (response) {
                    dataLayer.push({ ecommerce: null });
                    dataLayer.push({
                        event: 'add_shipping_info',
                        ecommerce: {
                            currency: response.currency.code,
                            value: response.cartAmount + shippingCost,
                            shipping: shippingCost,
                            shipping_tier: shippingName,
                            items: response.lineItems.physicalItems.map(item => ({
                                item_id: item.sku,
                                item_name: item.name,
                                price: item.salePrice,
                                quantity: item.quantity
                            }))
                        },
                        shipping_method: shippingName,
                        shipping_cost: shippingCost
                    });

                    // Store for order confirmation
                    sessionStorage.setItem('bc_shipping', JSON.stringify({
                        cost: shippingCost,
                        method: shippingName
                    }));
                }
            });
        });
    }
}

export default new ShippingTracking();

Magento

<!-- Add to Magento_Checkout/templates/success.phtml -->
<?php
$order = $block->getOrder();
$shippingMethod = $order->getShippingMethod();
$shippingDescription = $order->getShippingDescription();
?>

<script>
require(['jquery'], function($) {
    dataLayer.push({ ecommerce: null });
    dataLayer.push({
        event: 'purchase',
        ecommerce: {
            transaction_id: '<?php echo $order->getIncrementId(); ?>',
            value: <?php echo $order->getGrandTotal(); ?>,
            tax: <?php echo $order->getTaxAmount(); ?>,
            shipping: <?php echo $order->getShippingAmount(); ?>,
            shipping_tier: '<?php echo esc_js($shippingDescription); ?>',
            currency: '<?php echo $order->getOrderCurrencyCode(); ?>',
            items: [
                <?php foreach ($order->getAllVisibleItems() as $item): ?>
                {
                    item_id: '<?php echo $item->getSku(); ?>',
                    item_name: '<?php echo esc_js($item->getName()); ?>',
                    price: <?php echo $item->getPrice(); ?>,
                    quantity: <?php echo $item->getQtyOrdered(); ?>
                },
                <?php endforeach; ?>
            ]
        },
        shipping_method: '<?php echo esc_js($shippingDescription); ?>',
        shipping_cost: <?php echo $order->getShippingAmount(); ?>,
        delivery_type: <?php echo $order->getShippingAmount() == 0 ? "'free_shipping'" : "'paid_shipping'"; ?>
    });
});
</script>

Testing & Validation

Shipping Tracking Checklist

  • Shipping Selection: add_shipping_info fires when method selected
  • Shipping Tier: Includes descriptive method name
  • Shipping Cost: Numeric cost value present
  • Purchase Event: Includes shipping and shipping_tier
  • Free Shipping: Tracked with shipping: 0
  • Value Calculation: Transaction total includes shipping
  • Threshold Events: Free shipping qualification tracked
  • Consistent Naming: Shipping tier names standardized

GA4 Reports to Check

  1. Monetization → E-commerce purchases

    • View "Shipping tier" dimension
    • Check revenue by shipping method
  2. Engagement → Events → add_shipping_info

    • Verify event count
    • Check shipping_tier parameter distribution
  3. Custom Report: Shipping Analysis

    • Compare conversion rates by shipping tier
    • Analyze shipping cost vs. cart value ratio

Console Validation

// Test shipping tracking implementation
function validateShippingTracking() {
  console.log('🚚 Shipping Tracking Validation\n');

  // Check add_shipping_info events
  const shippingInfoEvents = dataLayer.filter(e => e.event === 'add_shipping_info');
  console.log(`add_shipping_info events: ${shippingInfoEvents.length}`);

  if (shippingInfoEvents.length > 0) {
    const latest = shippingInfoEvents[shippingInfoEvents.length - 1];
    const ecom = latest.ecommerce;

    console.log('\nLatest add_shipping_info:');
    console.log('- Shipping cost:', ecom?.shipping ?? '⚠️ MISSING');
    console.log('- Shipping tier:', ecom?.shipping_tier ?? '⚠️ MISSING');
    console.log('- Total value:', ecom?.value);
  }

  // Check purchase events
  const purchases = dataLayer.filter(e => e.event === 'purchase');
  console.log(`\npurchase events: ${purchases.length}`);

  if (purchases.length > 0) {
    const latest = purchases[purchases.length - 1];
    const ecom = latest.ecommerce;

    console.log('\nLatest purchase:');
    console.log('- Shipping cost:', ecom?.shipping ?? '⚠️ MISSING');
    console.log('- Shipping tier:', ecom?.shipping_tier ?? '⚠️ MISSING');
    console.log('- Transaction total:', ecom?.value);
    console.log('- Tax:', ecom?.tax);

    // Validate total calculation
    const itemsTotal = ecom?.items?.reduce((sum, item) =>
      sum + (item.price * item.quantity), 0
    ) || 0;
    const calculatedTotal = itemsTotal + (ecom?.tax || 0) + (ecom?.shipping || 0);

    console.log('\nTotal validation:');
    console.log('- Items total:', itemsTotal.toFixed(2));
    console.log('- + Tax:', ecom?.tax?.toFixed(2));
    console.log('- + Shipping:', ecom?.shipping?.toFixed(2));
    console.log('- = Calculated:', calculatedTotal.toFixed(2));
    console.log('- Reported:', ecom?.value?.toFixed(2));

    if (Math.abs(calculatedTotal - ecom.value) < 0.01) {
      console.log('✓ Totals match');
    } else {
      console.error('✗ Total mismatch!');
    }
  }
}

validateShippingTracking();

Further Reading

// SYS.FOOTER