Drupal-Specific GA4 Event Tracking
Overview
This guide covers implementing custom GA4 event tracking for Drupal-specific features including Views interactions, Webform submissions, user registrations, content interactions, and more. Learn how to leverage Drupal's APIs and hooks for comprehensive event tracking.
Core Event Tracking Patterns
JavaScript Event Tracking
Basic event syntax:
gtag('event', 'event_name', {
'event_category': 'category',
'event_label': 'label',
'value': 1
});
Drupal Behaviors pattern:
(function (Drupal, drupalSettings) {
'use strict';
Drupal.behaviors.customEventTracking = {
attach: function (context, settings) {
// Your event tracking code here
// context ensures proper handling with AJAX
}
};
})(Drupal, drupalSettings);
Webform Event Tracking
Method 1: Using Webform Module Hooks
File: modules/custom/custom_analytics/custom_analytics.module
<?php
use Drupal\webform\WebformSubmissionInterface;
/**
* Implements hook_webform_submission_insert().
*/
function custom_analytics_webform_submission_insert(WebformSubmissionInterface $submission) {
$webform = $submission->getWebform();
$webform_id = $webform->id();
$webform_title = $webform->label();
// Get submission data
$data = $submission->getData();
// Attach tracking JavaScript
$submission_page = \Drupal::request()->query->get('destination');
// Queue JavaScript for next page load
\Drupal::messenger()->addStatus('Form submitted successfully.');
// Store event in session for JS to pick up
$_SESSION['ga_events'][] = [
'event' => 'form_submit',
'parameters' => [
'form_id' => $webform_id,
'form_name' => $webform_title,
'event_category' => 'webform',
'event_label' => $webform_title,
],
];
}
/**
* Implements hook_page_attachments().
*/
function custom_analytics_page_attachments(array &$attachments) {
// Check for queued GA events
if (!empty($_SESSION['ga_events'])) {
$attachments['#attached']['drupalSettings']['gaEvents'] = $_SESSION['ga_events'];
$attachments['#attached']['library'][] = 'custom_analytics/ga_events';
unset($_SESSION['ga_events']);
}
}
File: modules/custom/custom_analytics/js/ga-events.js
(function (Drupal, drupalSettings) {
'use strict';
Drupal.behaviors.gaEventsTracker = {
attach: function (context, settings) {
if (settings.gaEvents && settings.gaEvents.length > 0) {
settings.gaEvents.forEach(function(eventData) {
gtag('event', eventData.event, eventData.parameters);
});
// Clear events after sending
delete drupalSettings.gaEvents;
}
}
};
})(Drupal, drupalSettings);
Method 2: Client-Side Webform Tracking
File: themes/custom/mytheme/js/webform-tracking.js
(function (Drupal, once) {
'use strict';
Drupal.behaviors.webformTracking = {
attach: function (context, settings) {
// Track all webform submissions
once('webform-ga-tracking', 'form.webform-submission-form', context).forEach(function(form) {
var webformId = form.getAttribute('data-webform-id') || form.id;
var webformTitle = form.querySelector('.webform-title')?.textContent || webformId;
// Track form start (first interaction)
var formStarted = false;
form.addEventListener('focusin', function() {
if (!formStarted) {
formStarted = true;
gtag('event', 'form_start', {
'event_category': 'webform',
'event_label': webformTitle,
'form_id': webformId
});
}
}, { once: true });
// Track form submission
form.addEventListener('submit', function(event) {
gtag('event', 'form_submit', {
'event_category': 'webform',
'event_label': webformTitle,
'form_id': webformId,
'transport_type': 'beacon'
});
});
// Track form abandonment
var formInteracted = false;
form.addEventListener('input', function() {
formInteracted = true;
}, { once: true });
window.addEventListener('beforeunload', function() {
if (formInteracted && !form.submitted) {
gtag('event', 'form_abandon', {
'event_category': 'webform',
'event_label': webformTitle,
'form_id': webformId,
'transport_type': 'beacon'
});
}
});
});
}
};
})(Drupal, once);
Library definition:
# themes/custom/mytheme/mytheme.libraries.yml
webform-tracking:
js:
js/webform-tracking.js: {}
dependencies:
- core/drupal
- core/once
Drupal Views Event Tracking
Track View Interactions
For exposed filters:
(function (Drupal, once) {
'use strict';
Drupal.behaviors.viewsFilterTracking = {
attach: function (context, settings) {
// Track Views exposed filter submissions
once('views-filter-tracking', 'form.views-exposed-form', context).forEach(function(form) {
var viewId = form.getAttribute('data-drupal-views-id') || 'unknown';
var displayId = form.getAttribute('data-drupal-views-display-id') || 'unknown';
form.addEventListener('submit', function() {
// Collect filter values
var formData = new FormData(form);
var filters = {};
formData.forEach(function(value, key) {
if (value && key !== 'form_build_id' && key !== 'form_id') {
filters[key] = value;
}
});
gtag('event', 'view_filter', {
'event_category': 'drupal_views',
'event_label': viewId + ':' + displayId,
'view_id': viewId,
'display_id': displayId,
'filters': JSON.stringify(filters)
});
});
});
}
};
})(Drupal, once);
Track View Item Clicks
(function (Drupal, once) {
'use strict';
Drupal.behaviors.viewsItemTracking = {
attach: function (context, settings) {
once('views-item-tracking', '.view-content .views-row', context).forEach(function(row) {
var links = row.querySelectorAll('a');
links.forEach(function(link) {
link.addEventListener('click', function() {
var itemTitle = row.querySelector('.views-field-title')?.textContent.trim() || 'unknown';
var itemType = row.closest('.view')?.classList[1] || 'unknown'; // e.g., view-articles
gtag('event', 'select_content', {
'content_type': itemType,
'item_id': link.href,
'event_category': 'drupal_views',
'event_label': itemTitle
});
});
});
});
}
};
})(Drupal, once);
Track AJAX View Updates
(function (Drupal) {
'use strict';
// Track Views AJAX pager clicks
Drupal.behaviors.viewsAjaxTracking = {
attach: function (context, settings) {
if (Drupal.views && Drupal.views.ajaxView) {
// Override Views AJAX success callback
var originalSuccess = Drupal.Ajax.prototype.success;
Drupal.Ajax.prototype.success = function(response, status) {
// Check if this is a Views AJAX call
if (this.element && this.element.closest('.view')) {
var view = this.element.closest('.view');
var viewId = view.getAttribute('data-view-id');
var displayId = view.getAttribute('data-view-display-id');
gtag('event', 'view_ajax_update', {
'event_category': 'drupal_views',
'event_label': viewId + ':' + displayId,
'action_type': this.element.classList.contains('pager') ? 'pagination' : 'filter'
});
}
// Call original success handler
originalSuccess.apply(this, arguments);
};
}
}
};
})(Drupal);
User Behavior Tracking
User Registration
<?php
use Drupal\user\UserInterface;
/**
* Implements hook_user_insert().
*/
function custom_analytics_user_insert(UserInterface $account) {
// Track new user registration
$_SESSION['ga_events'][] = [
'event' => 'sign_up',
'parameters' => [
'method' => 'drupal_registration',
'event_category' => 'user',
'event_label' => 'user_registration'
],
];
}
User Login
<?php
use Drupal\user\UserInterface;
/**
* Implements hook_user_login().
*/
function custom_analytics_user_login(UserInterface $account) {
$_SESSION['ga_events'][] = [
'event' => 'login',
'parameters' => [
'method' => 'drupal',
'event_category' => 'user',
'user_role' => implode(',', $account->getRoles(TRUE))
],
];
}
Comment Submission
<?php
use Drupal\comment\CommentInterface;
/**
* Implements hook_comment_insert().
*/
function custom_analytics_comment_insert(CommentInterface $comment) {
$entity = $comment->getCommentedEntity();
$_SESSION['ga_events'][] = [
'event' => 'comment_submit',
'parameters' => [
'event_category' => 'engagement',
'event_label' => $entity->getEntityTypeId() . ':' . $entity->id(),
'content_type' => $entity->bundle(),
'content_title' => $entity->label()
],
];
}
Content Interaction Tracking
Track Reading Progress (Scroll Depth)
(function (Drupal, once) {
'use strict';
Drupal.behaviors.scrollDepthTracking = {
attach: function (context, settings) {
// Only track on full node pages
if (!document.body.classList.contains('page-node-type-article')) {
return;
}
once('scroll-depth', 'body', context).forEach(function() {
var milestones = [25, 50, 75, 100];
var reached = {};
var contentTitle = document.querySelector('.page-title')?.textContent || 'unknown';
var nodeId = document.body.className.match(/page-node-(\d+)/)?.[1] || 'unknown';
function checkScrollDepth() {
var scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
var scrolled = window.scrollY;
var percentScrolled = Math.round((scrolled / scrollHeight) * 100);
milestones.forEach(function(milestone) {
if (percentScrolled >= milestone && !reached[milestone]) {
reached[milestone] = true;
gtag('event', 'scroll', {
'event_category': 'engagement',
'event_label': contentTitle,
'percent_scrolled': milestone,
'page_location': window.location.href,
'node_id': nodeId
});
}
});
}
var throttledCheck = throttle(checkScrollDepth, 500);
window.addEventListener('scroll', throttledCheck);
});
// Throttle function
function throttle(func, wait) {
var timeout;
return function() {
if (!timeout) {
timeout = setTimeout(function() {
timeout = null;
func();
}, wait);
}
};
}
}
};
})(Drupal, once);
Track Video Engagement (Media Entity)
(function (Drupal, once) {
'use strict';
Drupal.behaviors.mediaVideoTracking = {
attach: function (context, settings) {
once('media-video-tracking', 'video, .media--type-video video', context).forEach(function(video) {
var videoTitle = video.getAttribute('title') || video.closest('.media')?.querySelector('.media__title')?.textContent || 'unknown';
var videoSrc = video.currentSrc || video.querySelector('source')?.src || 'unknown';
var tracked = {
play: false,
progress_25: false,
progress_50: false,
progress_75: false,
complete: false
};
video.addEventListener('play', function() {
if (!tracked.play) {
tracked.play = true;
gtag('event', 'video_start', {
'event_category': 'media',
'event_label': videoTitle,
'video_url': videoSrc
});
}
});
video.addEventListener('timeupdate', function() {
var percent = (video.currentTime / video.duration) * 100;
if (percent >= 25 && !tracked.progress_25) {
tracked.progress_25 = true;
gtag('event', 'video_progress', {
'event_category': 'media',
'event_label': videoTitle,
'video_percent': 25
});
}
if (percent >= 50 && !tracked.progress_50) {
tracked.progress_50 = true;
gtag('event', 'video_progress', {
'event_category': 'media',
'event_label': videoTitle,
'video_percent': 50
});
}
if (percent >= 75 && !tracked.progress_75) {
tracked.progress_75 = true;
gtag('event', 'video_progress', {
'event_category': 'media',
'event_label': videoTitle,
'video_percent': 75
});
}
});
video.addEventListener('ended', function() {
if (!tracked.complete) {
tracked.complete = true;
gtag('event', 'video_complete', {
'event_category': 'media',
'event_label': videoTitle,
'video_url': videoSrc
});
}
});
});
}
};
})(Drupal, once);
Search Tracking
Drupal Core Search
<?php
/**
* Implements hook_page_attachments().
*/
function custom_analytics_page_attachments(array &$attachments) {
$route_name = \Drupal::routeMatch()->getRouteName();
// Track search pages
if ($route_name === 'search.view') {
$keys = \Drupal::request()->query->get('keys');
$type = \Drupal::request()->query->get('type', 'all');
if ($keys) {
$attachments['#attached']['drupalSettings']['searchTracking'] = [
'search_term' => $keys,
'search_type' => $type,
];
$attachments['#attached']['library'][] = 'custom_analytics/search_tracking';
}
}
}
File: modules/custom/custom_analytics/js/search-tracking.js
(function (Drupal, drupalSettings) {
'use strict';
Drupal.behaviors.searchTracking = {
attach: function (context, settings) {
if (settings.searchTracking) {
gtag('event', 'search', {
'search_term': settings.searchTracking.search_term,
'event_category': 'search',
'event_label': settings.searchTracking.search_type
});
}
}
};
})(Drupal, drupalSettings);
Search API / Views Search
(function (Drupal, once) {
'use strict';
Drupal.behaviors.searchApiTracking = {
attach: function (context, settings) {
once('search-api-tracking', 'form.views-exposed-form[id*="search"]', context).forEach(function(form) {
form.addEventListener('submit', function() {
var searchInput = form.querySelector('input[type="search"], input[name*="keys"]');
var searchTerm = searchInput ? searchInput.value : '';
if (searchTerm) {
gtag('event', 'search', {
'search_term': searchTerm,
'event_category': 'site_search',
'event_label': form.id
});
}
});
});
}
};
})(Drupal, once);
CTA & Button Tracking
Track All CTA Buttons
(function (Drupal, once) {
'use strict';
Drupal.behaviors.ctaTracking = {
attach: function (context, settings) {
// Track buttons with .cta class or specific patterns
once('cta-tracking', 'a.cta, a.btn, .button, [class*="call-to-action"]', context).forEach(function(button) {
button.addEventListener('click', function() {
var buttonText = this.textContent.trim();
var buttonUrl = this.href || 'no-url';
var buttonLocation = this.closest('section, .region, .block')?.className || 'unknown';
gtag('event', 'cta_click', {
'event_category': 'engagement',
'event_label': buttonText,
'button_text': buttonText,
'button_url': buttonUrl,
'button_location': buttonLocation
});
});
});
}
};
})(Drupal, once);
Error Page Tracking
<?php
/**
* Implements hook_page_attachments().
*/
function custom_analytics_page_attachments(array &$attachments) {
$route_name = \Drupal::routeMatch()->getRouteName();
// Track 404 errors
if ($route_name === 'system.404') {
$current_path = \Drupal::request()->getPathInfo();
$referer = \Drupal::request()->server->get('HTTP_REFERER');
$attachments['#attached']['drupalSettings']['errorTracking'] = [
'error_type' => '404',
'page_path': $current_path,
'referrer': $referer,
];
$attachments['#attached']['library'][] = 'custom_analytics/error_tracking';
}
// Track 403 errors (access denied)
if ($route_name === 'system.403') {
$current_path = \Drupal::request()->getPathInfo();
$attachments['#attached']['drupalSettings']['errorTracking'] = [
'error_type' => '403',
'page_path': $current_path,
];
$attachments['#attached']['library'][] = 'custom_analytics/error_tracking';
}
}
(function (Drupal, drupalSettings) {
'use strict';
Drupal.behaviors.errorTracking = {
attach: function (context, settings) {
if (settings.errorTracking) {
gtag('event', 'exception', {
'description': 'HTTP ' + settings.errorTracking.error_type,
'fatal': false,
'event_category': 'error',
'page_path': settings.errorTracking.page_path,
'referrer': settings.errorTracking.referrer || 'direct'
});
}
}
};
})(Drupal, drupalSettings);
Performance Monitoring
Track BigPipe Performance
(function (Drupal) {
'use strict';
// Track BigPipe placeholder revelations
if (Drupal.behaviors.bigPipe) {
document.addEventListener('DOMContentLoaded', function() {
var placeholderCount = document.querySelectorAll('[data-big-pipe-placeholder-id]').length;
if (placeholderCount > 0) {
gtag('event', 'bigpipe_placeholders', {
'event_category': 'performance',
'event_label': window.location.pathname,
'value': placeholderCount
});
}
});
}
})(Drupal);
Testing Event Tracking
1. Enable GA4 Debug Mode
gtag('config', 'G-XXXXXXXXXX', {
'debug_mode': true
});
2. Use DebugView in GA4
- Open GA4 → Configure → DebugView
- Perform actions on your Drupal site
- View events in real-time with parameters
3. Browser Console Logging
// Log all events to console (development only)
if (drupalSettings.environment === 'development') {
var originalGtag = window.gtag;
window.gtag = function() {
console.log('GA4 Event:', arguments);
originalGtag.apply(this, arguments);
};
}