GA4 Enhanced Ecommerce for Magento | Blue Frog Docs

GA4 Enhanced Ecommerce for Magento

Implement complete Google Analytics 4 enhanced eCommerce tracking for Magento 2, including data layer, product impressions, and transaction tracking.

GA4 Enhanced Ecommerce for Magento

Implement comprehensive Google Analytics 4 enhanced eCommerce tracking on your Magento 2 store with a complete data layer implementation. This guide covers product impressions, user interactions, checkout funnel, and transaction tracking with full integration into Magento's architecture.


Complete Data Layer Architecture

Data Layer Structure

The data layer serves as the bridge between Magento and GA4, providing structured eCommerce data.

Base Structure:

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
    'event': 'ecommerce_event',
    'ecommerce': {
        'currency': 'USD',
        'value': 0.00,
        'items': []
    }
});

Full Module Implementation

Module Structure

app/code/YourCompany/Ga4Ecommerce/
├── etc/
│   ├── module.xml
│   ├── config.xml
│   ├── adminhtml/
│   │   └── system.xml
│   └── frontend/
│       ├── events.xml
│       └── di.xml
├── Block/
│   ├── DataLayer.php
│   ├── Category.php
│   ├── Product.php
│   ├── Cart.php
│   ├── Checkout.php
│   └── Success.php
├── Helper/
│   └── Data.php
├── Observer/
│   ├── AddToCart.php
│   ├── RemoveFromCart.php
│   ├── BeginCheckout.php
│   └── Purchase.php
├── ViewModel/
│   └── EcommerceData.php
└── view/frontend/
    ├── layout/
    │   ├── default.xml
    │   ├── catalog_category_view.xml
    │   ├── catalog_product_view.xml
    │   ├── checkout_cart_index.xml
    │   ├── checkout_index_index.xml
    │   └── checkout_onepage_success.xml
    ├── templates/
    │   ├── datalayer.phtml
    │   ├── category.phtml
    │   ├── product.phtml
    │   ├── cart.phtml
    │   ├── checkout.phtml
    │   └── success.phtml
    └── web/
        └── js/
            └── ecommerce-tracker.js

Helper Class

File: app/code/YourCompany/Ga4Ecommerce/Helper/Data.php

<?php
namespace YourCompany\Ga4Ecommerce\Helper;

use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Helper\Context;
use Magento\Store\Model\ScopeInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Category;

class Data extends AbstractHelper
{
    const XML_PATH_ENABLED = 'google/ga4_ecommerce/enabled';
    const XML_PATH_MEASUREMENT_ID = 'google/ga4_ecommerce/measurement_id';

    protected $categoryRepository;
    protected $storeManager;

    public function __construct(
        Context $context,
        \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository,
        \Magento\Store\Model\StoreManagerInterface $storeManager
    ) {
        parent::__construct($context);
        $this->categoryRepository = $categoryRepository;
        $this->storeManager = $storeManager;
    }

    public function isEnabled()
    {
        return $this->scopeConfig->isSetFlag(
            self::XML_PATH_ENABLED,
            ScopeInterface::SCOPE_STORE
        );
    }

    public function getMeasurementId()
    {
        return $this->scopeConfig->getValue(
            self::XML_PATH_MEASUREMENT_ID,
            ScopeInterface::SCOPE_STORE
        );
    }

    public function getCurrency()
    {
        return $this->storeManager->getStore()->getCurrentCurrencyCode();
    }

    public function formatProductData(Product $product, $index = 1, $listName = '')
    {
        $categories = $this->getProductCategories($product);

        return [
            'item_id' => $product->getSku(),
            'item_name' => $product->getName(),
            'item_brand' => $product->getAttributeText('manufacturer') ?: '',
            'item_category' => $categories[0] ?? '',
            'item_category2' => $categories[1] ?? '',
            'item_category3' => $categories[2] ?? '',
            'item_list_name' => $listName,
            'price' => (float)$product->getFinalPrice(),
            'discount' => (float)($product->getPrice() - $product->getFinalPrice()),
            'index' => $index,
            'quantity' => 1
        ];
    }

    public function formatCartItemData($item)
    {
        $product = $item->getProduct();
        $categories = $this->getProductCategories($product);

        return [
            'item_id' => $item->getSku(),
            'item_name' => $item->getName(),
            'item_brand' => $product->getAttributeText('manufacturer') ?: '',
            'item_category' => $categories[0] ?? '',
            'price' => (float)$item->getPrice(),
            'discount' => (float)$item->getDiscountAmount(),
            'quantity' => (int)$item->getQty()
        ];
    }

    public function formatOrderItemData($item)
    {
        $product = $item->getProduct();
        $categories = $this->getProductCategories($product);

        return [
            'item_id' => $item->getSku(),
            'item_name' => $item->getName(),
            'item_brand' => $product ? ($product->getAttributeText('manufacturer') ?: '') : '',
            'item_category' => $categories[0] ?? '',
            'item_variant' => $this->getItemVariant($item),
            'price' => (float)$item->getPrice(),
            'discount' => (float)$item->getDiscountAmount(),
            'quantity' => (int)$item->getQtyOrdered(),
            'coupon' => $item->getOrder()->getCouponCode() ?: ''
        ];
    }

    protected function getProductCategories(Product $product)
    {
        $categories = [];
        $categoryIds = $product->getCategoryIds();

        foreach ($categoryIds as $categoryId) {
            try {
                $category = $this->categoryRepository->get($categoryId);
                $categories[] = $category->getName();
            } catch (\Exception $e) {
                continue;
            }
        }

        return $categories;
    }

    protected function getItemVariant($item)
    {
        $options = $item->getProductOptions();
        if (isset($options['attributes_info'])) {
            $variants = array_column($options['attributes_info'], 'value');
            return implode(' - ', $variants);
        }
        return '';
    }
}

Category Page Tracking (View Item List)

File: app/code/YourCompany/Ga4Ecommerce/Block/Category.php

<?php
namespace YourCompany\Ga4Ecommerce\Block;

use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
use Magento\Framework\Registry;
use YourCompany\Ga4Ecommerce\Helper\Data as Helper;

class Category extends Template
{
    protected $registry;
    protected $helper;

    public function __construct(
        Context $context,
        Registry $registry,
        Helper $helper,
        array $data = []
    ) {
        $this->registry = $registry;
        $this->helper = $helper;
        parent::__construct($context, $data);
    }

    public function getEcommerceData()
    {
        if (!$this->helper->isEnabled()) {
            return null;
        }

        $category = $this->registry->registry('current_category');
        $layer = $this->registry->registry('current_layer');

        if (!$category || !$layer) {
            return null;
        }

        $productCollection = $layer->getProductCollection();
        $items = [];
        $index = 1;

        foreach ($productCollection as $product) {
            $items[] = $this->helper->formatProductData(
                $product,
                $index++,
                $category->getName()
            );
        }

        return [
            'event' => 'view_item_list',
            'ecommerce' => [
                'item_list_id' => 'category_' . $category->getId(),
                'item_list_name' => $category->getName(),
                'items' => $items
            ]
        ];
    }
}

Template: view/frontend/templates/category.phtml

<?php
/** @var \YourCompany\Ga4Ecommerce\Block\Category $block */
$data = $block->getEcommerceData();
if ($data):
?>
<script>
    require(['jquery'], function($) {
        $(document).ready(function() {
            window.dataLayer = window.dataLayer || [];
            window.dataLayer.push({ ecommerce: null }); // Clear previous ecommerce data
            window.dataLayer.push(<?= $block->escapeJs(json_encode($data)) ?>);

            // Track product clicks
            $('.product-item-link').on('click', function() {
                var index = $(this).closest('.product-item').index();
                var item = <?= json_encode($data['ecommerce']['items']) ?>[index];

                window.dataLayer.push({ ecommerce: null });
                window.dataLayer.push({
                    'event': 'select_item',
                    'ecommerce': {
                        'item_list_id': '<?= $block->escapeJs($data['ecommerce']['item_list_id']) ?>',
                        'item_list_name': '<?= $block->escapeJs($data['ecommerce']['item_list_name']) ?>',
                        'items': [item]
                    }
                });
            });
        });
    });
</script>
<?php endif; ?>

Layout: view/frontend/layout/catalog_category_view.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block class="YourCompany\Ga4Ecommerce\Block\Category"
                   name="ga4.category"
                   template="YourCompany_Ga4Ecommerce::category.phtml"
                   after="-"/>
        </referenceContainer>
    </body>
</page>

Product Page Tracking (View Item)

File: app/code/YourCompany/Ga4Ecommerce/Block/Product.php

<?php
namespace YourCompany\Ga4Ecommerce\Block;

use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
use Magento\Framework\Registry;
use YourCompany\Ga4Ecommerce\Helper\Data as Helper;

class Product extends Template
{
    protected $registry;
    protected $helper;

    public function __construct(
        Context $context,
        Registry $registry,
        Helper $helper,
        array $data = []
    ) {
        $this->registry = $registry;
        $this->helper = $helper;
        parent::__construct($context, $data);
    }

    public function getEcommerceData()
    {
        if (!$this->helper->isEnabled()) {
            return null;
        }

        $product = $this->registry->registry('current_product');
        if (!$product) {
            return null;
        }

        $itemData = $this->helper->formatProductData($product);

        return [
            'event' => 'view_item',
            'ecommerce' => [
                'currency' => $this->helper->getCurrency(),
                'value' => $itemData['price'],
                'items' => [$itemData]
            ]
        ];
    }

    public function getProductJson()
    {
        $product = $this->registry->registry('current_product');
        if (!$product) {
            return '{}';
        }

        return json_encode([
            'sku' => $product->getSku(),
            'name' => $product->getName(),
            'price' => (float)$product->getFinalPrice(),
            'currency' => $this->helper->getCurrency()
        ]);
    }
}

Template: view/frontend/templates/product.phtml

<?php
/** @var \YourCompany\Ga4Ecommerce\Block\Product $block */
$data = $block->getEcommerceData();
if ($data):
?>
<script>
    require(['jquery'], function($) {
        $(document).ready(function() {
            // View item event
            window.dataLayer = window.dataLayer || [];
            window.dataLayer.push({ ecommerce: null });
            window.dataLayer.push(<?= $block->escapeJs(json_encode($data)) ?>);

            // Add to cart tracking
            var productData = <?= $block->getProductJson() ?>;

            $('#product-addtocart-button, .tocart').on('click', function(e) {
                var qty = parseInt($('[name="qty"]').val()) || 1;

                window.dataLayer.push({ ecommerce: null });
                window.dataLayer.push({
                    'event': 'add_to_cart',
                    'ecommerce': {
                        'currency': productData.currency,
                        'value': productData.price * qty,
                        'items': [{
                            'item_id': productData.sku,
                            'item_name': productData.name,
                            'price': productData.price,
                            'quantity': qty
                        }]
                    }
                });
            });
        });
    });
</script>
<?php endif; ?>

Cart Page Tracking (View Cart)

File: app/code/YourCompany/Ga4Ecommerce/Block/Cart.php

<?php
namespace YourCompany\Ga4Ecommerce\Block;

use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
use Magento\Checkout\Model\Session as CheckoutSession;
use YourCompany\Ga4Ecommerce\Helper\Data as Helper;

class Cart extends Template
{
    protected $checkoutSession;
    protected $helper;

    public function __construct(
        Context $context,
        CheckoutSession $checkoutSession,
        Helper $helper,
        array $data = []
    ) {
        $this->checkoutSession = $checkoutSession;
        $this->helper = $helper;
        parent::__construct($context, $data);
    }

    public function getEcommerceData()
    {
        if (!$this->helper->isEnabled()) {
            return null;
        }

        $quote = $this->checkoutSession->getQuote();
        if (!$quote || !$quote->getItemsCount()) {
            return null;
        }

        $items = [];
        foreach ($quote->getAllVisibleItems() as $item) {
            $items[] = $this->helper->formatCartItemData($item);
        }

        return [
            'event' => 'view_cart',
            'ecommerce' => [
                'currency' => $quote->getQuoteCurrencyCode(),
                'value' => (float)$quote->getGrandTotal(),
                'items' => $items
            ]
        ];
    }
}

Template: view/frontend/templates/cart.phtml

<?php
/** @var \YourCompany\Ga4Ecommerce\Block\Cart $block */
$data = $block->getEcommerceData();
if ($data):
?>
<script>
    require(['jquery', 'Magento_Customer/js/customer-data'], function($, customerData) {
        $(document).ready(function() {
            // View cart event
            window.dataLayer = window.dataLayer || [];
            window.dataLayer.push({ ecommerce: null });
            window.dataLayer.push(<?= $block->escapeJs(json_encode($data)) ?>);

            // Remove from cart tracking
            $(document).on('click', '.action-delete', function() {
                var $item = $(this).closest('tr.item-info');
                var itemData = {
                    'item_id': $item.data('product-sku'),
                    'item_name': $item.find('.product-item-name').text().trim(),
                    'price': parseFloat($item.find('.cart-price .price').attr('data-price-amount')) || 0,
                    'quantity': parseInt($item.find('.qty').val()) || 1
                };

                window.dataLayer.push({ ecommerce: null });
                window.dataLayer.push({
                    'event': 'remove_from_cart',
                    'ecommerce': {
                        'currency': '<?= $block->escapeJs($data['ecommerce']['currency']) ?>',
                        'value': itemData.price * itemData.quantity,
                        'items': [itemData]
                    }
                });
            });
        });
    });
</script>
<?php endif; ?>

Checkout Tracking

Begin Checkout

File: app/code/YourCompany/Ga4Ecommerce/Block/Checkout.php

<?php
namespace YourCompany\Ga4Ecommerce\Block;

use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
use Magento\Checkout\Model\Session as CheckoutSession;
use YourCompany\Ga4Ecommerce\Helper\Data as Helper;

class Checkout extends Template
{
    protected $checkoutSession;
    protected $helper;

    public function __construct(
        Context $context,
        CheckoutSession $checkoutSession,
        Helper $helper,
        array $data = []
    ) {
        $this->checkoutSession = $checkoutSession;
        $this->helper = $helper;
        parent::__construct($context, $data);
    }

    public function getEcommerceData()
    {
        if (!$this->helper->isEnabled()) {
            return null;
        }

        $quote = $this->checkoutSession->getQuote();
        if (!$quote || !$quote->getItemsCount()) {
            return null;
        }

        $items = [];
        foreach ($quote->getAllVisibleItems() as $item) {
            $items[] = $this->helper->formatCartItemData($item);
        }

        return [
            'event' => 'begin_checkout',
            'ecommerce' => [
                'currency' => $quote->getQuoteCurrencyCode(),
                'value' => (float)$quote->getGrandTotal(),
                'coupon' => $quote->getCouponCode() ?: '',
                'items' => $items
            ]
        ];
    }
}

Template: view/frontend/templates/checkout.phtml

<?php
/** @var \YourCompany\Ga4Ecommerce\Block\Checkout $block */
$data = $block->getEcommerceData();
if ($data):
?>
<script>
    require(['jquery', 'Magento_Checkout/js/model/step-navigator'], function($, stepNavigator) {
        $(document).ready(function() {
            // Begin checkout event
            window.dataLayer = window.dataLayer || [];
            window.dataLayer.push({ ecommerce: null });
            window.dataLayer.push(<?= $block->escapeJs(json_encode($data)) ?>);

            // Track checkout steps
            stepNavigator.steps.subscribe(function(steps) {
                steps.forEach(function(step, index) {
                    if (step.isVisible()) {
                        var stepName = step.title || step.code;

                        window.dataLayer.push({ ecommerce: null });
                        window.dataLayer.push({
                            'event': 'checkout_progress',
                            'ecommerce': {
                                'checkout_step': index + 1,
                                'checkout_option': stepName
                            }
                        });
                    }
                });
            });

            // Add shipping info event
            $(document).on('click', '#shipping-method-buttons-container button', function() {
                var shippingMethod = $('input[name="shipping_method"]:checked').val();

                window.dataLayer.push({ ecommerce: null });
                window.dataLayer.push({
                    'event': 'add_shipping_info',
                    'ecommerce': {
                        'currency': '<?= $block->escapeJs($data['ecommerce']['currency']) ?>',
                        'value': <?= $data['ecommerce']['value'] ?>,
                        'shipping_tier': shippingMethod,
                        'items': <?= $block->escapeJs(json_encode($data['ecommerce']['items'])) ?>
                    }
                });
            });

            // Add payment info event
            $(document).on('click', '.payment-method input[type="radio"]', function() {
                var paymentMethod = $(this).val();

                window.dataLayer.push({ ecommerce: null });
                window.dataLayer.push({
                    'event': 'add_payment_info',
                    'ecommerce': {
                        'currency': '<?= $block->escapeJs($data['ecommerce']['currency']) ?>',
                        'value': <?= $data['ecommerce']['value'] ?>,
                        'payment_type': paymentMethod,
                        'items': <?= $block->escapeJs(json_encode($data['ecommerce']['items'])) ?>
                    }
                });
            });
        });
    });
</script>
<?php endif; ?>

Purchase Tracking

File: app/code/YourCompany/Ga4Ecommerce/Observer/Purchase.php

<?php
namespace YourCompany\Ga4Ecommerce\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Checkout\Model\Session as CheckoutSession;
use YourCompany\Ga4Ecommerce\Helper\Data as Helper;

class Purchase implements ObserverInterface
{
    protected $checkoutSession;
    protected $helper;

    public function __construct(
        CheckoutSession $checkoutSession,
        Helper $helper
    ) {
        $this->checkoutSession = $checkoutSession;
        $this->helper = $helper;
    }

    public function execute(Observer $observer)
    {
        if (!$this->helper->isEnabled()) {
            return;
        }

        $order = $observer->getEvent()->getOrder();
        if (!$order) {
            return;
        }

        $items = [];
        foreach ($order->getAllVisibleItems() as $item) {
            $items[] = $this->helper->formatOrderItemData($item);
        }

        $purchaseData = [
            'event' => 'purchase',
            'ecommerce' => [
                'transaction_id' => $order->getIncrementId(),
                'affiliation' => $this->helper->getStoreName(),
                'value' => (float)$order->getGrandTotal(),
                'currency' => $order->getOrderCurrencyCode(),
                'tax' => (float)$order->getTaxAmount(),
                'shipping' => (float)$order->getShippingAmount(),
                'coupon' => $order->getCouponCode() ?: '',
                'items' => $items
            ]
        ];

        $this->checkoutSession->setGa4PurchaseData($purchaseData);
    }
}

Register: etc/frontend/events.xml

<event name="checkout_onepage_controller_success_action">
    <observer name="ga4_ecommerce_purchase"
              instance="YourCompany\Ga4Ecommerce\Observer\Purchase"/>
</event>

Success Page Block: Block/Success.php

<?php
namespace YourCompany\Ga4Ecommerce\Block;

use Magento\Framework\View\Element\Template;
use Magento\Checkout\Model\Session as CheckoutSession;

class Success extends Template
{
    protected $checkoutSession;

    public function __construct(
        Template\Context $context,
        CheckoutSession $checkoutSession,
        array $data = []
    ) {
        $this->checkoutSession = $checkoutSession;
        parent::__construct($context, $data);
    }

    public function getPurchaseData()
    {
        return $this->checkoutSession->getGa4PurchaseData();
    }

    public function clearPurchaseData()
    {
        $this->checkoutSession->unsGa4PurchaseData();
    }
}

Template: view/frontend/templates/success.phtml

<?php
/** @var \YourCompany\Ga4Ecommerce\Block\Success $block */
$data = $block->getPurchaseData();
if ($data):
?>
<script>
    require(['jquery'], function($) {
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({ ecommerce: null });
        window.dataLayer.push(<?= $block->escapeJs(json_encode($data)) ?>);
    });
</script>
<?php
    $block->clearPurchaseData();
endif;
?>

Server-Side Tracking (Optional)

For accurate tracking independent of client-side issues.

File: app/code/YourCompany/Ga4Ecommerce/Service/ServerSideTracker.php

<?php
namespace YourCompany\Ga4Ecommerce\Service;

use Magento\Framework\HTTP\Client\Curl;

class ServerSideTracker
{
    const MEASUREMENT_PROTOCOL_URL = 'https://www.google-analytics.com/mp/collect';

    protected $curl;
    protected $helper;

    public function __construct(
        Curl $curl,
        \YourCompany\Ga4Ecommerce\Helper\Data $helper
    ) {
        $this->curl = $curl;
        $this->helper = $helper;
    }

    public function trackPurchase($order, $clientId = null)
    {
        $measurementId = $this->helper->getMeasurementId();
        $apiSecret = $this->helper->getApiSecret(); // Add to config

        if (!$measurementId || !$apiSecret) {
            return false;
        }

        $clientId = $clientId ?: $this->generateClientId();

        $payload = [
            'client_id' => $clientId,
            'events' => [
                [
                    'name' => 'purchase',
                    'params' => [
                        'transaction_id' => $order->getIncrementId(),
                        'value' => (float)$order->getGrandTotal(),
                        'currency' => $order->getOrderCurrencyCode(),
                        'tax' => (float)$order->getTaxAmount(),
                        'shipping' => (float)$order->getShippingAmount(),
                        'items' => $this->getOrderItems($order)
                    ]
                ]
            ]
        ];

        $url = self::MEASUREMENT_PROTOCOL_URL
            . '?measurement_id=' . $measurementId
            . '&api_secret=' . $apiSecret;

        $this->curl->post($url, json_encode($payload));
        $this->curl->setOption(CURLOPT_HTTPHEADER, ['Content-Type: application/json']);

        return $this->curl->getStatus() === 204;
    }

    protected function getOrderItems($order)
    {
        $items = [];
        foreach ($order->getAllVisibleItems() as $item) {
            $items[] = $this->helper->formatOrderItemData($item);
        }
        return $items;
    }

    protected function generateClientId()
    {
        return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }
}

Testing & Validation

Data Layer Debugging

Add console logging:

window.dataLayerDebug = true;

window.dataLayer = window.dataLayer || [];
var originalPush = window.dataLayer.push;
window.dataLayer.push = function() {
    if (window.dataLayerDebug) {
        console.log('DataLayer Push:', arguments);
    }
    return originalPush.apply(this, arguments);
};

GA4 DebugView

  1. Enable debug mode:
gtag('config', 'G-XXXXXXXXXX', {'debug_mode': true});
  1. Access DebugView in GA4 Admin

  2. Verify all eCommerce events appear with correct parameters


Performance Optimization

Lazy Data Layer Loading

Load non-critical events after page load:

window.addEventListener('load', function() {
    // Process deferred events
    processDeferredEcommerceEvents();
});

Varnish & FPC Compatibility

Use private content sections:

<config>
    <action name="catalog/product/view">
        <section name="ga4-ecommerce"/>
    </action>
</config>

Next Steps


Additional Resources

// SYS.FOOTER