Drupal Data Layer for Google Tag Manager | Blue Frog Docs

Drupal Data Layer for Google Tag Manager

Implement comprehensive data layer integration for GTM using Drupal's datalayer module and custom solutions

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.


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:

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

  1. Open GTM → Preview
  2. Enter Drupal site URL
  3. View Data Layer tab
  4. 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

  1. Check module is enabled: drush pm:list | grep datalayer
  2. Clear cache: drush cr
  3. Verify JavaScript aggregation not breaking JSON
  4. 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

  1. Verify data layer name matches GTM configuration
  2. Check script load order (data layer must load before GTM)
  3. Ensure no JavaScript errors prevent execution

Next Steps


Resources

// SYS.FOOTER