Drupal-Specific GA4 Event Tracking | Blue Frog Docs

Drupal-Specific GA4 Event Tracking

Implement custom GA4 event tracking for Drupal Views, Webforms, Commerce, and user interactions

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

<?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);
(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';
  }
}

JavaScript:

(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

  1. Open GA4 → Configure → DebugView
  2. Perform actions on your Drupal site
  3. 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);
  };
}

Next Steps

// SYS.FOOTER