Drupal Data Layer for GTM
Overview
A properly structured data layer is essential for effective Google Tag Manager implementation. This guide covers using the Drupal DataLayer module and creating custom data layer implementations that leverage Drupal's entity system, user data, and commerce information.
Method 1: Using the DataLayer Module (Recommended)
Installation
# Install Data Layer module
composer require drupal/datalayer
# Enable the module
drush en datalayer -y
# Clear cache
drush cr
Configuration
Navigate to Configuration → System → Data Layer (/admin/config/system/datalayer)
General Settings
Data Layer Name:
dataLayer
(Standard GTM data layer name)
Output Format:
- ✅ JSON (Recommended)
- ☐ JavaScript object
Include Default Data:
- ✅ Page metadata
- ✅ User information
- ✅ Entity data
- ✅ Language
- ✅ Term data
Entity-Specific Settings
Enable for Content Types:
☑ Article
☑ Page
☑ Product (if using Commerce)
☑ Custom content types
Fields to Include:
# Configure which entity fields to expose
- title
- created
- changed
- author
- taxonomy terms
- custom fields
User Data:
☑ User ID (anonymized)
☑ User roles
☐ Username (privacy concern)
☐ Email (privacy concern)
Default Data Layer Structure
The DataLayer module outputs data in this structure:
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'drupalLanguage': 'en',
'drupalCountry': 'US',
'siteName': 'My Drupal Site',
'entityType': 'node',
'entityBundle': 'article',
'entityId': '123',
'entityTitle': 'Article Title',
'entityCreated': '2024-01-15T10:30:00Z',
'entityChanged': '2024-01-20T14:45:00Z',
'entityUid': '5',
'entityLangcode': 'en',
'userUid': '1',
'userRoles': ['authenticated', 'content_editor'],
'userStatus': '1',
'taxonomyTerms': {
'field_category': ['Technology', 'Web Development'],
'field_tags': ['Drupal', 'GTM', 'Analytics']
}
});
Custom Data Layer Implementation
Method 1: Extending DataLayer Module
File: modules/custom/custom_datalayer/custom_datalayer.module
<?php
use Drupal\Core\Entity\EntityInterface;
use Drupal\node\NodeInterface;
use Drupal\commerce_product\Entity\ProductInterface;
/**
* Implements hook_datalayer_meta().
*/
function custom_datalayer_datalayer_meta() {
$meta = [];
// Add custom site-wide data
$config = \Drupal::config('system.site');
$meta['siteName'] = $config->get('name');
$meta['siteSlogan'] = $config->get('slogan');
// Add environment info
$meta['environment'] = getenv('PANTHEON_ENVIRONMENT') ?: 'local';
// Add user info
$current_user = \Drupal::currentUser();
$meta['userType'] = $current_user->isAnonymous() ? 'anonymous' : 'authenticated';
$meta['userRoles'] = $current_user->getRoles();
// Add page type
$route_match = \Drupal::routeMatch();
$route_name = $route_match->getRouteName();
$meta['pageType'] = _custom_datalayer_get_page_type($route_name);
return $meta;
}
/**
* Implements hook_datalayer_alter().
*/
function custom_datalayer_datalayer_alter(&$data_layer) {
$route_match = \Drupal::routeMatch();
// Add custom data for nodes
if ($node = $route_match->getParameter('node')) {
if ($node instanceof NodeInterface) {
// Add author information
$author = $node->getOwner();
$data_layer['contentAuthor'] = $author->getDisplayName();
// Add publish status
$data_layer['contentPublished'] = $node->isPublished();
// Add custom fields
if ($node->hasField('field_reading_time')) {
$data_layer['readingTime'] = $node->get('field_reading_time')->value;
}
// Add taxonomy data in structured format
if ($node->hasField('field_category')) {
$categories = [];
foreach ($node->get('field_category')->referencedEntities() as $term) {
$categories[] = $term->label();
}
$data_layer['categories'] = $categories;
}
}
}
// Add custom data for commerce products
if ($product = $route_match->getParameter('commerce_product')) {
if ($product instanceof ProductInterface) {
$variation = $product->getDefaultVariation();
if ($variation) {
$price = $variation->getPrice();
$data_layer['ecommerce'] = [
'productId' => $variation->getSku(),
'productName' => $product->label(),
'productPrice' => (float) $price->getNumber(),
'productCurrency' => $price->getCurrencyCode(),
];
// Add product category
if ($product->hasField('field_category')) {
$category = $product->get('field_category')->entity;
$data_layer['ecommerce']['productCategory'] = $category ? $category->label() : '';
}
// Add product brand
if ($product->hasField('field_brand')) {
$brand = $product->get('field_brand')->entity;
$data_layer['ecommerce']['productBrand'] = $brand ? $brand->label() : '';
}
}
}
}
}
/**
* Get page type from route.
*/
function _custom_datalayer_get_page_type($route_name) {
$page_types = [
'entity.node.canonical' => 'content',
'system.404' => 'error_404',
'system.403' => 'error_403',
'user.login' => 'login',
'user.register' => 'registration',
'view.search.page' => 'search',
'commerce_cart.page' => 'cart',
'commerce_checkout.form' => 'checkout',
];
return $page_types[$route_name] ?? 'other';
}
Method 2: Pure Custom Implementation
File: modules/custom/custom_datalayer/custom_datalayer.module
<?php
/**
* Implements hook_page_attachments().
*/
function custom_datalayer_page_attachments(array &$attachments) {
// Build data layer
$data_layer = _custom_datalayer_build();
// Attach to page
$attachments['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => 'window.dataLayer = window.dataLayer || []; dataLayer.push(' . json_encode($data_layer) . ');',
'#weight' => -1000, // Load early
],
'datalayer_init'
];
// Add cache metadata
$attachments['#cache']['contexts'][] = 'url.path';
$attachments['#cache']['contexts'][] = 'user.roles';
$attachments['#cache']['tags'][] = 'config:system.site';
}
/**
* Build the data layer array.
*/
function _custom_datalayer_build() {
$data_layer = [];
// Add page data
$data_layer = array_merge($data_layer, _custom_datalayer_page_data());
// Add user data
$data_layer = array_merge($data_layer, _custom_datalayer_user_data());
// Add entity data
$data_layer = array_merge($data_layer, _custom_datalayer_entity_data());
// Add commerce data
$data_layer = array_merge($data_layer, _custom_datalayer_commerce_data());
return $data_layer;
}
/**
* Get page data.
*/
function _custom_datalayer_page_data() {
$data = [];
$request = \Drupal::request();
$route_match = \Drupal::routeMatch();
// Basic page info
$data['pageUrl'] = $request->getSchemeAndHttpHost() . $request->getRequestUri();
$data['pagePath'] = $request->getPathInfo();
$data['pageTitle'] = \Drupal::service('title_resolver')->getTitle($request, $route_match->getRouteObject());
$data['pageLanguage'] = \Drupal::languageManager()->getCurrentLanguage()->getId();
$data['pageType'] = _custom_datalayer_get_page_type($route_match->getRouteName());
// Referrer
$data['pageReferrer'] = $request->server->get('HTTP_REFERER', '');
return $data;
}
/**
* Get user data.
*/
function _custom_datalayer_user_data() {
$data = [];
$current_user = \Drupal::currentUser();
$data['userId'] = $current_user->isAnonymous() ? null : $current_user->id();
$data['userType'] = $current_user->isAnonymous() ? 'anonymous' : 'authenticated';
$data['userRoles'] = $current_user->getRoles();
// Get primary role (highest permission level)
$data['userPrimaryRole'] = _custom_datalayer_get_primary_role($current_user);
return $data;
}
/**
* Get entity data.
*/
function _custom_datalayer_entity_data() {
$data = [];
$route_match = \Drupal::routeMatch();
// Check for node
if ($node = $route_match->getParameter('node')) {
if ($node instanceof \Drupal\node\NodeInterface) {
$data['entityType'] = 'node';
$data['entityBundle'] = $node->bundle();
$data['entityId'] = $node->id();
$data['entityTitle'] = $node->label();
$data['entityPublished'] = $node->isPublished();
$data['entityCreated'] = date('c', $node->getCreatedTime());
$data['entityChanged'] = date('c', $node->getChangedTime());
$data['entityAuthorId'] = $node->getOwnerId();
$data['entityAuthorName'] = $node->getOwner()->getDisplayName();
// Add taxonomy terms
$data['taxonomyTerms'] = _custom_datalayer_get_taxonomy_terms($node);
}
}
// Check for taxonomy term
if ($term = $route_match->getParameter('taxonomy_term')) {
$data['entityType'] = 'taxonomy_term';
$data['entityBundle'] = $term->bundle();
$data['entityId'] = $term->id();
$data['entityTitle'] = $term->label();
}
return $data;
}
/**
* Get commerce data.
*/
function _custom_datalayer_commerce_data() {
$data = [];
// Check if Commerce is installed
if (!\Drupal::moduleHandler()->moduleExists('commerce_product')) {
return $data;
}
$route_match = \Drupal::routeMatch();
// Product page
if ($product = $route_match->getParameter('commerce_product')) {
$variation = $product->getDefaultVariation();
if ($variation) {
$price = $variation->getPrice();
$data['ecommerce'] = [
'detail' => [
'products' => [
[
'id' => $variation->getSku(),
'name' => $product->label(),
'price' => (float) $price->getNumber(),
'currency' => $price->getCurrencyCode(),
'category' => _custom_datalayer_get_product_category($product),
'brand' => _custom_datalayer_get_product_brand($product),
]
]
]
];
}
}
// Cart page
if ($route_match->getRouteName() === 'commerce_cart.page') {
$cart_provider = \Drupal::service('commerce_cart.cart_provider');
$carts = $cart_provider->getCarts();
if (!empty($carts)) {
$cart = reset($carts);
$products = [];
foreach ($cart->getItems() as $order_item) {
$variation = $order_item->getPurchasedEntity();
$product = $variation->getProduct();
$products[] = [
'id' => $variation->getSku(),
'name' => $product->label(),
'price' => (float) $variation->getPrice()->getNumber(),
'quantity' => (float) $order_item->getQuantity(),
];
}
$data['ecommerce'] = [
'cart' => [
'products' => $products,
'totalValue' => (float) $cart->getTotalPrice()->getNumber(),
'currency' => $cart->getTotalPrice()->getCurrencyCode(),
]
];
}
}
return $data;
}
/**
* Get taxonomy terms from entity.
*/
function _custom_datalayer_get_taxonomy_terms($entity) {
$terms = [];
foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) {
if ($field_definition->getType() === 'entity_reference' &&
$field_definition->getSetting('target_type') === 'taxonomy_term') {
if (!$entity->get($field_name)->isEmpty()) {
$field_terms = [];
foreach ($entity->get($field_name)->referencedEntities() as $term) {
$field_terms[] = $term->label();
}
$terms[$field_name] = $field_terms;
}
}
}
return $terms;
}
/**
* Get primary user role.
*/
function _custom_datalayer_get_primary_role($user) {
if ($user->isAnonymous()) {
return 'anonymous';
}
$roles = $user->getRoles(TRUE); // Exclude authenticated role
$role_weights = [
'administrator' => 100,
'editor' => 50,
'content_creator' => 30,
'authenticated' => 1,
];
$highest_weight = 0;
$primary_role = 'authenticated';
foreach ($roles as $role) {
$weight = $role_weights[$role] ?? 10;
if ($weight > $highest_weight) {
$highest_weight = $weight;
$primary_role = $role;
}
}
return $primary_role;
}
/**
* Get product category.
*/
function _custom_datalayer_get_product_category($product) {
if ($product->hasField('field_category') && !$product->get('field_category')->isEmpty()) {
return $product->get('field_category')->entity->label();
}
return '';
}
/**
* Get product brand.
*/
function _custom_datalayer_get_product_brand($product) {
if ($product->hasField('field_brand') && !$product->get('field_brand')->isEmpty()) {
return $product->get('field_brand')->entity->label();
}
return '';
}
Event-Based Data Layer Updates
Push Events with JavaScript
File: js/datalayer-events.js
(function (Drupal, drupalSettings, once) {
'use strict';
/**
* Helper function to push to dataLayer.
*/
Drupal.dataLayerPush = function(data) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(data);
};
/**
* Track form interactions.
*/
Drupal.behaviors.dataLayerFormTracking = {
attach: function (context, settings) {
once('datalayer-form', 'form.webform-submission-form', context).forEach(function(form) {
// Form start
form.addEventListener('focusin', function() {
Drupal.dataLayerPush({
'event': 'formStart',
'formId': form.id,
'formName': form.querySelector('.webform-title')?.textContent || form.id
});
}, { once: true });
// Form submit
form.addEventListener('submit', function() {
Drupal.dataLayerPush({
'event': 'formSubmit',
'formId': form.id,
'formName': form.querySelector('.webform-title')?.textContent || form.id
});
});
});
}
};
/**
* Track video interactions.
*/
Drupal.behaviors.dataLayerVideoTracking = {
attach: function (context, settings) {
once('datalayer-video', 'video', context).forEach(function(video) {
var videoName = video.getAttribute('title') || video.currentSrc;
video.addEventListener('play', function() {
Drupal.dataLayerPush({
'event': 'videoStart',
'videoName': videoName,
'videoUrl': video.currentSrc
});
});
video.addEventListener('ended', function() {
Drupal.dataLayerPush({
'event': 'videoComplete',
'videoName': videoName,
'videoUrl': video.currentSrc
});
});
});
}
};
/**
* Track scroll depth.
*/
Drupal.behaviors.dataLayerScrollTracking = {
attach: function (context, settings) {
once('datalayer-scroll', 'body', context).forEach(function() {
var milestones = [25, 50, 75, 100];
var reached = {};
function checkScroll() {
var scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
var scrolled = window.scrollY;
var percent = Math.round((scrolled / scrollHeight) * 100);
milestones.forEach(function(milestone) {
if (percent >= milestone && !reached[milestone]) {
reached[milestone] = true;
Drupal.dataLayerPush({
'event': 'scrollDepth',
'scrollPercent': milestone,
'pagePath': window.location.pathname
});
}
});
}
var throttled = throttle(checkScroll, 500);
window.addEventListener('scroll', throttled);
function throttle(func, wait) {
var timeout;
return function() {
if (!timeout) {
timeout = setTimeout(function() {
timeout = null;
func();
}, wait);
}
};
}
});
}
};
})(Drupal, drupalSettings, once);
Drupal Commerce Data Layer
Enhanced E-commerce Data Layer
<?php
use Drupal\commerce_cart\Event\CartEvents;
use Drupal\commerce_cart\Event\CartEntityAddEvent;
/**
* Track add to cart in data layer.
*/
public function onCartEntityAdd(CartEntityAddEvent $event) {
$order_item = $event->getOrderItem();
$variation = $order_item->getPurchasedEntity();
$product = $variation->getProduct();
$data_layer_event = [
'event' => 'addToCart',
'ecommerce' => [
'currencyCode' => $variation->getPrice()->getCurrencyCode(),
'add' => [
'products' => [
[
'id' => $variation->getSku(),
'name' => $product->label(),
'price' => (float) $variation->getPrice()->getNumber(),
'brand' => _custom_datalayer_get_product_brand($product),
'category' => _custom_datalayer_get_product_category($product),
'variant' => $variation->label(),
'quantity' => (float) $order_item->getQuantity(),
]
]
]
]
];
// Store in session for next page load
$_SESSION['datalayer_events'][] = $data_layer_event;
}
/**
* Attach queued data layer events.
*/
function custom_datalayer_page_attachments(array &$attachments) {
if (!empty($_SESSION['datalayer_events'])) {
foreach ($_SESSION['datalayer_events'] as $event) {
$attachments['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => 'dataLayer.push(' . json_encode($event) . ');',
],
'datalayer_event_' . md5(json_encode($event))
];
}
// Clear events after adding
unset($_SESSION['datalayer_events']);
}
}
User Login/Registration Events
<?php
use Drupal\user\UserInterface;
/**
* Implements hook_user_login().
*/
function custom_datalayer_user_login(UserInterface $account) {
$_SESSION['datalayer_events'][] = [
'event' => 'userLogin',
'userId' => $account->id(),
'userRoles' => $account->getRoles(),
'loginMethod' => 'drupal'
];
}
/**
* Implements hook_user_insert().
*/
function custom_datalayer_user_insert(UserInterface $account) {
$_SESSION['datalayer_events'][] = [
'event' => 'userRegistration',
'userId' => $account->id(),
'registrationMethod' => 'drupal'
];
}
Testing Data Layer
1. Console Inspection
// View entire data layer
console.log(window.dataLayer);
// Monitor all pushes
var originalPush = dataLayer.push;
dataLayer.push = function() {
console.log('dataLayer.push:', arguments[0]);
return originalPush.apply(dataLayer, arguments);
};
2. GTM Preview Mode
- Open GTM → Preview
- Enter Drupal site URL
- View Data Layer tab
- Verify all expected variables present
3. Browser Extensions
- dataslayer - Chrome extension for viewing data layer
- Tag Assistant - Google's official debugging tool
Performance Considerations
1. Limit Data Layer Size
// Only include necessary fields
function custom_datalayer_datalayer_alter(&$data_layer) {
// Remove unnecessary data
unset($data_layer['entityNid']); // Redundant if entityId exists
// Limit array sizes
if (isset($data_layer['taxonomyTerms'])) {
foreach ($data_layer['taxonomyTerms'] as $key => $terms) {
// Limit to first 10 terms
$data_layer['taxonomyTerms'][$key] = array_slice($terms, 0, 10);
}
}
}
2. Cache Data Layer
$attachments['#cache']['tags'][] = 'node:' . $node->id();
$attachments['#cache']['contexts'][] = 'url.path';
3. Lazy Load Non-Critical Data
// Load additional data after page interactive
if ('requestIdleCallback' in window) {
requestIdleCallback(function() {
// Push non-critical data
dataLayer.push({
'additionalData': 'value'
});
});
}
Debugging Common Issues
Data Layer Not Populating
- Check module is enabled:
drush pm:list | grep datalayer - Clear cache:
drush cr - Verify JavaScript aggregation not breaking JSON
- Check browser console for errors
Duplicate Data
Ensure data layer push only happens once:
// Add flag to prevent duplicates
if (!isset($attachments['datalayer_added'])) {
$attachments['datalayer_added'] = TRUE;
// Add data layer
}
Data Layer Empty in GTM Preview
- Verify data layer name matches GTM configuration
- Check script load order (data layer must load before GTM)
- Ensure no JavaScript errors prevent execution