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
- Enable debug mode:
gtag('config', 'G-XXXXXXXXXX', {'debug_mode': true});
Access DebugView in GA4 Admin
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
- GTM Data Layer - Integrate with Tag Manager
- Event Tracking Troubleshooting - Debug issues
- Meta Pixel Setup - Add Facebook tracking
Additional Resources
- GA4 Measurement Protocol - Server-side tracking
- GA4 Ecommerce Events - Event specifications
- Magento Data Layer - Dependency injection