GTM Data Layer for Magento | Blue Frog Docs

GTM Data Layer for Magento

Implement a comprehensive Google Tag Manager data layer for Magento 2, including eCommerce events, customer data, and product information.

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

  1. Open GTM Container
  2. Click Preview
  3. Navigate to Magento store
  4. 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


Additional Resources

// SYS.FOOTER