WooCommerce stores have an advantage over hosted platforms: you control the server, so you can output exactly the data layer you want on every page, including the thank-you page. The flip side is that nothing is set up for you, and a surprising number of stores run GA4 with only page views, or with a purchase event that fires twice per order.
This guide covers the full GA4 ecommerce setup for WooCommerce through Google Tag Manager, the choice between a plugin and a custom data layer, purchase deduplication, and sending the same events to Google Ads and Meta.
The Events That Matter
| Event | Fires when | Key data |
|---|---|---|
| view_item | Product page viewed | item_id, item_name, price, category |
| add_to_cart | Product added, including AJAX | item, quantity, value |
| begin_checkout | Checkout page loaded | cart items, value |
| add_shipping_info, add_payment_info | Optional checkout steps | items, value |
| purchase | Order received page, once | transaction_id, value, tax, shipping, currency, items |
Step 1: Install GTM
Use a lightweight plugin to insert the container, or add the GTM snippets to header.php and the body open hook of a child theme. Confirm the container loads on the storefront in Preview.
Step 2: Choose a Data Layer Method
Plugin method. Install GTM4WP, enable the WooCommerce integration and the GA4 events you need. It outputs a data layer for product, cart, checkout and order pages and handles AJAX add to cart. Fast and reliable for standard themes.
Custom method. Use WooCommerce hooks in a small plugin or your child theme to output data layer pushes. The purchase event is the most important one:
add_action('wp_head', function () {
if (!function_exists('is_order_received_page') || !is_order_received_page()) { return; }
$order_id = absint(get_query_var('order-received'));
$order = wc_get_order($order_id);
if (!$order || $order->get_meta('_ga4_purchase_tracked')) { return; }
$items = [];
foreach ($order->get_items() as $item) {
$product = $item->get_product();
$items[] = [
'item_id' => $product ? ($product->get_sku() ?: $product->get_id()) : $item->get_product_id(),
'item_name' => $item->get_name(),
'price' => (float) $order->get_item_total($item, false),
'quantity' => (int) $item->get_quantity(),
];
}
$payload = [
'event' => 'purchase',
'ecommerce' => [
'transaction_id' => (string) $order->get_order_number(),
'value' => (float) $order->get_total(),
'tax' => (float) $order->get_total_tax(),
'shipping' => (float) $order->get_shipping_total(),
'currency' => $order->get_currency(),
'coupon' => implode(',', $order->get_coupon_codes()),
'items' => $items,
],
'user_email' => strtolower($order->get_billing_email()),
'user_phone' => $order->get_billing_phone(),
];
echo '<script>window.dataLayer=window.dataLayer||[];window.dataLayer.push({ecommerce:null});window.dataLayer.push(' . wp_json_encode($payload) . ');</script>';
$order->update_meta_data('_ga4_purchase_tracked', 1);
$order->save();
});
The order meta flag is what makes the thank-you page safe to reload. For view_item, hook woocommerce_before_single_product and output the product data. For AJAX add_to_cart, add a small script that listens for WooCommerce's added_to_cart event:
<script>
jQuery(document.body).on('added_to_cart', function (e, fragments, hash, button) {
var $b = jQuery(button);
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'add_to_cart',
ecommerce: {
currency: window.wc_currency || 'USD',
value: parseFloat($b.data('price') || 0) * parseInt($b.data('quantity') || 1, 10),
items: [{
item_id: $b.data('product_sku') || String($b.data('product_id')),
item_name: $b.data('product_name') || $b.attr('aria-label') || '',
price: parseFloat($b.data('price') || 0),
quantity: parseInt($b.data('quantity') || 1, 10)
}]
}
});
});
</script>
Add data-price and data-product_name attributes to the add to cart button through the woocommerce_loop_add_to_cart_args filter if your theme does not already expose them.
Step 3: GTM Variables, Triggers and Tags
Variables for ecommerce.transaction_id, ecommerce.value, ecommerce.currency, ecommerce.items, user_email, user_phone. Custom Event triggers for each event name.
- Google tag on All Pages.
- GA4 Event tags for
view_item,add_to_cart,begin_checkoutandpurchase, reading ecommerce data from the data layer. - Google Ads Conversion Tracking on
purchasewith value, transaction ID and currency, Enhanced Conversions from the email and phone, and the Conversion Linker on All Pages. - Meta Pixel Purchase on
purchasewith value, currency, content IDs and an event ID equal to the order number.
Step 4: Payment Gateway Redirects
PayPal, Stripe hosted checkout and bank redirects send the customer away and back. Add the gateway domains to GA4's unwanted referrals list so the purchase keeps its original source. If the gateway returns the customer to the order received page twice, the order meta flag above prevents a second purchase push.
Step 5: Test
- GTM Preview. View a product, add to cart with AJAX, begin checkout. Confirm each event with items and values.
- Place a test order using cash on delivery or a test gateway. Confirm one
purchasewith the correct order number, total, tax and shipping. - Reload the order received page. No second purchase.
- Check GA4 DebugView and Monetisation reports, Meta Test Events, and the Google Ads request in a tag debugger such as the Insighter Debugger extension.
- After a few days, compare GA4 revenue with WooCommerce Analytics for the same period.
Common Problems
- Purchase fires but items missing. Hook ran before the order was loaded, or the SKU lookup failed. Log the payload.
- Revenue double. Plugin and custom data layer both active, or a theme-bundled GA4 integration. One source only.
- add_to_cart from product page missing. Non-AJAX single product forms post and reload. Track the resulting cart page or hook
woocommerce_add_to_cartserver-side and push on the next page load. - Wrong currency on multi-currency stores. Read the currency from the order object, never from a constant.
Outcome
GA4 shows the full WooCommerce funnel with accurate revenue, each order counted once. Google Ads and Meta receive the same purchase with value and customer data for matching. From there you can trust return on ad spend by channel, which is the whole point of the exercise.
