GTM Data Layer for Magento
Build a comprehensive Google Tag Manager data layer for your Magento 2 store to track user behavior, product interactions, and eCommerce events. This guide provides complete implementations for all major page types and user actions.
Data Layer Architecture
Core Structure
The data layer serves as a standardized data format between Magento and GTM.
Base Structure:
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'pageView',
'pageType': 'homepage',
'pageCategory': '',
'userStatus': 'guest',
'userId': null,
'customerGroup': 'NOT LOGGED IN',
'storeView': 'default',
'currency': 'USD'
});
Complete Data Layer Module
Module Structure
app/code/YourCompany/DataLayer/
├── etc/
│ ├── module.xml
│ ├── config.xml
│ └── frontend/
│ ├── events.xml
│ └── sections.xml
├── Block/
│ └── DataLayer.php
├── CustomerData/
│ └── DataLayer.php
├── Helper/
│ └── Data.php
├── ViewModel/
│ ├── Category.php
│ ├── Product.php
│ ├── Cart.php
│ └── Checkout.php
└── view/frontend/
├── layout/
│ └── default.xml
└── templates/
└── datalayer.phtml
Helper Class
File: Helper/Data.php
<?php
namespace YourCompany\DataLayer\Helper;
use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Helper\Context;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Framework\App\Request\Http;
class Data extends AbstractHelper
{
protected $customerSession;
protected $storeManager;
protected $request;
public function __construct(
Context $context,
CustomerSession $customerSession,
StoreManagerInterface $storeManager,
Http $request
) {
parent::__construct($context);
$this->customerSession = $customerSession;
$this->storeManager = $storeManager;
$this->request = $request;
}
public function getBaseDataLayer()
{
return [
'event' => 'pageView',
'pageType' => $this->getPageType(),
'pageCategory' => $this->getPageCategory(),
'userStatus' => $this->getUserStatus(),
'userId' => $this->getUserId(),
'customerGroup' => $this->getCustomerGroup(),
'storeView' => $this->getStoreCode(),
'storeName' => $this->getStoreName(),
'currency' => $this->getCurrency(),
'locale' => $this->getLocale()
];
}
public function getPageType()
{
$fullActionName = $this->request->getFullActionName();
$pageTypeMap = [
'cms_index_index' => 'homepage',
'catalog_category_view' => 'category',
'catalog_product_view' => 'product',
'catalogsearch_result_index' => 'search',
'checkout_cart_index' => 'cart',
'checkout_index_index' => 'checkout',
'checkout_onepage_success' => 'purchase',
'customer_account_login' => 'login',
'customer_account_create' => 'register',
'customer_account_index' => 'account'
];
return $pageTypeMap[$fullActionName] ?? 'other';
}
public function getPageCategory()
{
// Override in specific implementations
return '';
}
public function getUserStatus()
{
return $this->customerSession->isLoggedIn() ? 'logged_in' : 'guest';
}
public function getUserId()
{
return $this->customerSession->isLoggedIn()
? $this->customerSession->getCustomerId()
: null;
}
public function getCustomerGroup()
{
if (!$this->customerSession->isLoggedIn()) {
return 'NOT LOGGED IN';
}
$groupId = $this->customerSession->getCustomerGroupId();
$groups = [
0 => 'NOT LOGGED IN',
1 => 'General',
2 => 'Wholesale',
3 => 'Retailer'
];
return $groups[$groupId] ?? 'General';
}
public function getStoreCode()
{
return $this->storeManager->getStore()->getCode();
}
public function getStoreName()
{
return $this->storeManager->getStore()->getName();
}
public function getCurrency()
{
return $this->storeManager->getStore()->getCurrentCurrencyCode();
}
public function getLocale()
{
return $this->scopeConfig->getValue(
'general/locale/code',
\Magento\Store\Model\ScopeInterface::SCOPE_STORE
);
}
}
Base Data Layer Block
File: Block/DataLayer.php
<?php
namespace YourCompany\DataLayer\Block;
use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
use YourCompany\DataLayer\Helper\Data as DataLayerHelper;
class DataLayer extends Template
{
protected $dataLayerHelper;
public function __construct(
Context $context,
DataLayerHelper $dataLayerHelper,
array $data = []
) {
$this->dataLayerHelper = $dataLayerHelper;
parent::__construct($context, $data);
}
public function getDataLayerJson()
{
$dataLayer = $this->dataLayerHelper->getBaseDataLayer();
return json_encode($dataLayer);
}
public function getDataLayer()
{
return $this->dataLayerHelper->getBaseDataLayer();
}
}
Category Page Data Layer
File: ViewModel/Category.php
<?php
namespace YourCompany\DataLayer\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Framework\Registry;
use Magento\Catalog\Model\Layer\Resolver;
use YourCompany\DataLayer\Helper\Data as DataLayerHelper;
class Category implements ArgumentInterface
{
protected $registry;
protected $layerResolver;
protected $dataLayerHelper;
public function __construct(
Registry $registry,
Resolver $layerResolver,
DataLayerHelper $dataLayerHelper
) {
$this->registry = $registry;
$this->layerResolver = $layerResolver;
$this->dataLayerHelper = $dataLayerHelper;
}
public function getCategoryDataLayer()
{
$category = $this->registry->registry('current_category');
if (!$category) {
return [];
}
$layer = $this->layerResolver->get();
$productCollection = $layer->getProductCollection();
$products = [];
$index = 1;
foreach ($productCollection as $product) {
$products[] = [
'name' => $product->getName(),
'id' => $product->getSku(),
'price' => (float)$product->getFinalPrice(),
'brand' => $product->getAttributeText('manufacturer') ?: '',
'category' => $category->getName(),
'variant' => '',
'list' => 'Category: ' . $category->getName(),
'position' => $index++
];
}
$dataLayer = $this->dataLayerHelper->getBaseDataLayer();
$dataLayer['pageCategory'] = $category->getName();
$dataLayer['ecommerce'] = [
'currencyCode' => $this->dataLayerHelper->getCurrency(),
'impressions' => $products
];
return $dataLayer;
}
}
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="before.body.end">
<block class="Magento\Framework\View\Element\Template"
name="datalayer.category"
template="YourCompany_DataLayer::category.phtml">
<arguments>
<argument name="view_model" xsi:type="object">YourCompany\DataLayer\ViewModel\Category</argument>
</arguments>
</block>
</referenceContainer>
</body>
</page>
Template: view/frontend/templates/category.phtml
<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \YourCompany\DataLayer\ViewModel\Category $viewModel */
$viewModel = $block->getData('view_model');
$dataLayer = $viewModel->getCategoryDataLayer();
?>
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(<?= $block->escapeJs(json_encode($dataLayer)) ?>);
// Product click tracking
require(['jquery'], function($) {
$('.product-item-link').on('click', function(event) {
var $item = $(this).closest('.product-item');
var position = $item.index() + 1;
var productName = $(this).text().trim();
window.dataLayer.push({
'event': 'productClick',
'ecommerce': {
'click': {
'actionField': {'list': '<?= $block->escapeJs($dataLayer['pageCategory']) ?>'},
'products': [{
'name': productName,
'position': position
}]
}
}
});
});
});
</script>
Product Page Data Layer
File: ViewModel/Product.php
<?php
namespace YourCompany\DataLayer\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Framework\Registry;
use Magento\Catalog\Api\CategoryRepositoryInterface;
use YourCompany\DataLayer\Helper\Data as DataLayerHelper;
class Product implements ArgumentInterface
{
protected $registry;
protected $categoryRepository;
protected $dataLayerHelper;
public function __construct(
Registry $registry,
CategoryRepositoryInterface $categoryRepository,
DataLayerHelper $dataLayerHelper
) {
$this->registry = $registry;
$this->categoryRepository = $categoryRepository;
$this->dataLayerHelper = $dataLayerHelper;
}
public function getProductDataLayer()
{
$product = $this->registry->registry('current_product');
if (!$product) {
return [];
}
$category = $this->getProductCategory($product);
$productData = [
'name' => $product->getName(),
'id' => $product->getSku(),
'price' => (float)$product->getFinalPrice(),
'brand' => $product->getAttributeText('manufacturer') ?: '',
'category' => $category,
'variant' => '',
'stock' => $product->getIsInStock() ? 'in stock' : 'out of stock'
];
$dataLayer = $this->dataLayerHelper->getBaseDataLayer();
$dataLayer['pageCategory'] = $category;
$dataLayer['ecommerce'] = [
'currencyCode' => $this->dataLayerHelper->getCurrency(),
'detail': [
'products' => [$productData]
]
];
return $dataLayer;
}
public function getProductData()
{
$product = $this->registry->registry('current_product');
if (!$product) {
return null;
}
return [
'sku' => $product->getSku(),
'name' => $product->getName(),
'price' => (float)$product->getFinalPrice(),
'currency' => $this->dataLayerHelper->getCurrency()
];
}
protected function getProductCategory($product)
{
$categoryIds = $product->getCategoryIds();
if (empty($categoryIds)) {
return '';
}
try {
$categoryId = reset($categoryIds);
$category = $this->categoryRepository->get($categoryId);
return $category->getName();
} catch (\Exception $e) {
return '';
}
}
}
Template: view/frontend/templates/product.phtml
<?php
/** @var \YourCompany\DataLayer\ViewModel\Product $viewModel */
$viewModel = $block->getData('view_model');
$dataLayer = $viewModel->getProductDataLayer();
$productData = $viewModel->getProductData();
?>
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(<?= $block->escapeJs(json_encode($dataLayer)) ?>);
// Add to cart tracking
require(['jquery'], function($) {
var productData = <?= json_encode($productData) ?>;
$('#product-addtocart-button, .tocart').on('click', function() {
var qty = parseInt($('[name="qty"]').val()) || 1;
window.dataLayer.push({
'event': 'addToCart',
'ecommerce': {
'currencyCode': productData.currency,
'add': {
'products': [{
'name': productData.name,
'id': productData.sku,
'price': productData.price,
'quantity': qty
}]
}
}
});
});
});
</script>
Cart Page Data Layer
File: ViewModel/Cart.php
<?php
namespace YourCompany\DataLayer\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Checkout\Model\Session as CheckoutSession;
use YourCompany\DataLayer\Helper\Data as DataLayerHelper;
class Cart implements ArgumentInterface
{
protected $checkoutSession;
protected $dataLayerHelper;
public function __construct(
CheckoutSession $checkoutSession,
DataLayerHelper $dataLayerHelper
) {
$this->checkoutSession = $checkoutSession;
$this->dataLayerHelper = $dataLayerHelper;
}
public function getCartDataLayer()
{
$quote = $this->checkoutSession->getQuote();
if (!$quote || !$quote->getItemsCount()) {
return [];
}
$products = [];
foreach ($quote->getAllVisibleItems() as $item) {
$product = $item->getProduct();
$products[] = [
'name' => $item->getName(),
'id' => $item->getSku(),
'price' => (float)$item->getPrice(),
'brand' => $product->getAttributeText('manufacturer') ?: '',
'category' => $this->getItemCategory($product),
'variant' => $this->getItemVariant($item),
'quantity' => (int)$item->getQty()
];
}
$dataLayer = $this->dataLayerHelper->getBaseDataLayer();
$dataLayer['ecommerce'] = [
'currencyCode' => $quote->getQuoteCurrencyCode(),
'checkout' => [
'actionField': {'step': 0},
'products' => $products
]
];
$dataLayer['cartTotal'] = (float)$quote->getGrandTotal();
$dataLayer['cartItemCount'] = (int)$quote->getItemsQty();
return $dataLayer;
}
protected function getItemCategory($product)
{
$categoryIds = $product->getCategoryIds();
if (empty($categoryIds)) {
return '';
}
return 'Category'; // Implement full category fetching if needed
}
protected function getItemVariant($item)
{
$options = $item->getProductOptions();
if (isset($options['attributes_info'])) {
$variants = array_column($options['attributes_info'], 'value');
return implode(' - ', $variants);
}
return '';
}
}
Template: view/frontend/templates/cart.phtml
<?php
/** @var \YourCompany\DataLayer\ViewModel\Cart $viewModel */
$viewModel = $block->getData('view_model');
$dataLayer = $viewModel->getCartDataLayer();
?>
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(<?= $block->escapeJs(json_encode($dataLayer)) ?>);
// Remove from cart tracking
require(['jquery'], function($) {
$(document).on('click', '.action-delete', function() {
var $row = $(this).closest('tr.item-info');
window.dataLayer.push({
'event': 'removeFromCart',
'ecommerce': {
'remove': {
'products': [{
'name': $row.find('.product-item-name').text().trim(),
'id': $row.data('product-sku'),
'price': parseFloat($row.find('.price').attr('data-price-amount')) || 0,
'quantity': parseInt($row.find('.qty').val()) || 1
}]
}
}
});
});
});
</script>
Checkout Data Layer
File: ViewModel/Checkout.php
<?php
namespace YourCompany\DataLayer\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Checkout\Model\Session as CheckoutSession;
use YourCompany\DataLayer\Helper\Data as DataLayerHelper;
class Checkout implements ArgumentInterface
{
protected $checkoutSession;
protected $dataLayerHelper;
public function __construct(
CheckoutSession $checkoutSession,
DataLayerHelper $dataLayerHelper
) {
$this->checkoutSession = $checkoutSession;
$this->dataLayerHelper = $dataLayerHelper;
}
public function getCheckoutDataLayer($step = 1)
{
$quote = $this->checkoutSession->getQuote();
if (!$quote || !$quote->getItemsCount()) {
return [];
}
$products = [];
foreach ($quote->getAllVisibleItems() as $item) {
$products[] = [
'name' => $item->getName(),
'id' => $item->getSku(),
'price' => (float)$item->getPrice(),
'quantity' => (int)$item->getQty()
];
}
$dataLayer = $this->dataLayerHelper->getBaseDataLayer();
$dataLayer['ecommerce'] = [
'currencyCode' => $quote->getQuoteCurrencyCode(),
'checkout' => [
'actionField' => ['step' => $step],
'products' => $products
]
];
return $dataLayer;
}
}
Purchase Data Layer (Success Page)
Observer: Observer/PurchaseDataLayer.php
<?php
namespace YourCompany\DataLayer\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Checkout\Model\Session as CheckoutSession;
class PurchaseDataLayer implements ObserverInterface
{
protected $checkoutSession;
public function __construct(CheckoutSession $checkoutSession)
{
$this->checkoutSession = $checkoutSession;
}
public function execute(Observer $observer)
{
$order = $observer->getEvent()->getOrder();
if (!$order) {
return;
}
$products = [];
foreach ($order->getAllVisibleItems() as $item) {
$products[] = [
'name' => $item->getName(),
'id' => $item->getSku(),
'price' => (float)$item->getPrice(),
'brand' => '',
'category' => '',
'variant' => '',
'quantity' => (int)$item->getQtyOrdered()
];
}
$purchaseData = [
'event' => 'purchase',
'ecommerce' => [
'purchase' => [
'actionField' => [
'id' => $order->getIncrementId(),
'affiliation' => 'Online Store',
'revenue' => (float)$order->getGrandTotal(),
'tax' => (float)$order->getTaxAmount(),
'shipping' => (float)$order->getShippingAmount(),
'coupon' => $order->getCouponCode() ?: ''
],
'products' => $products
]
],
'transactionId' => $order->getIncrementId(),
'transactionTotal' => (float)$order->getGrandTotal(),
'transactionTax' => (float)$order->getTaxAmount(),
'transactionShipping' => (float)$order->getShippingAmount(),
'transactionProducts' => count($products)
];
$this->checkoutSession->setDataLayerPurchase($purchaseData);
}
}
Register: etc/frontend/events.xml
<event name="checkout_onepage_controller_success_action">
<observer name="datalayer_purchase"
instance="YourCompany\DataLayer\Observer\PurchaseDataLayer"/>
</event>
Customer Data Section (Private Content)
For dynamic data with Full Page Cache enabled.
File: CustomerData/DataLayer.php
<?php
namespace YourCompany\DataLayer\CustomerData;
use Magento\Customer\CustomerData\SectionSourceInterface;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\Checkout\Model\Session as CheckoutSession;
class DataLayer implements SectionSourceInterface
{
protected $customerSession;
protected $checkoutSession;
public function __construct(
CustomerSession $customerSession,
CheckoutSession $checkoutSession
) {
$this->customerSession = $customerSession;
$this->checkoutSession = $checkoutSession;
}
public function getSectionData()
{
$quote = $this->checkoutSession->getQuote();
return [
'userId' => $this->customerSession->getCustomerId(),
'userStatus' => $this->customerSession->isLoggedIn() ? 'logged_in' : 'guest',
'customerGroup' => $this->getCustomerGroup(),
'cartItemCount' => $quote ? (int)$quote->getItemsQty() : 0,
'cartTotal' => $quote ? (float)$quote->getGrandTotal() : 0
];
}
protected function getCustomerGroup()
{
if (!$this->customerSession->isLoggedIn()) {
return 'NOT LOGGED IN';
}
return $this->customerSession->getCustomerGroupId();
}
}
Register Section: etc/frontend/sections.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer/etc/sections.xsd">
<action name="*/*">
<section name="datalayer"/>
</action>
</config>
Use in Template:
require(['Magento_Customer/js/customer-data'], function(customerData) {
var dataLayerData = customerData.get('datalayer');
dataLayerData.subscribe(function(data) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'customerDataUpdate',
'userId': data.userId,
'userStatus': data.userStatus,
'cartItemCount': data.cartItemCount
});
});
});
GTM Variables Configuration
Create Variables in GTM
1. Page Type Variable
- Variable Name: Page Type
- Variable Type: Data Layer Variable
- Data Layer Variable Name: pageType
2. User ID Variable
- Variable Name: User ID
- Variable Type: Data Layer Variable
- Data Layer Variable Name: userId
3. Transaction ID Variable
- Variable Name: Transaction ID
- Variable Type: Data Layer Variable
- Data Layer Variable Name: transactionId
4. E-commerce Variable
- Variable Name: Ecommerce
- Variable Type: Data Layer Variable
- Data Layer Variable Name: ecommerce
GTM Triggers Configuration
Custom Event Triggers
1. Add to Cart Trigger
- Trigger Type: Custom Event
- Event Name: addToCart
2. Product Click Trigger
- Trigger Type: Custom Event
- Event Name: productClick
3. Purchase Trigger
- Trigger Type: Custom Event
- Event Name: purchase
Testing & Debugging
Console Logging
// Add to any data layer template
console.log('DataLayer:', window.dataLayer);
GTM Preview Mode
- Open GTM Container
- Click Preview
- Navigate to Magento store
- Check Variables tab in debug panel
Data Layer Inspector
Install browser extension:
- Chrome: dataLayer Inspector+
- Firefox: Tag Inspector
Performance Optimization
Lazy Loading
window.addEventListener('load', function() {
// Load non-critical data layer events
require(['jquery'], function($) {
// Process deferred events
});
});
Event Batching
var eventQueue = [];
var flushInterval = 2000; // 2 seconds
function queueDataLayerEvent(event) {
eventQueue.push(event);
}
setInterval(function() {
if (eventQueue.length > 0) {
eventQueue.forEach(function(event) {
window.dataLayer.push(event);
});
eventQueue = [];
}
}, flushInterval);
Next Steps
- GA4 Enhanced Ecommerce - Integrate with Analytics
- Meta Pixel Events - Use data layer for Facebook tracking
- Troubleshooting - Debug data layer issues
Additional Resources
- GTM Data Layer Documentation - Official guide
- Enhanced Ecommerce Dev Guide - Ecommerce tracking
- Magento Customer Data - Private content sections