MODX GA4 Event Tracking: Setup Guide | OpsBlu Docs

MODX GA4 Event Tracking: Setup Guide

Implement custom event tracking for GA4 on MODX including form submissions, downloads, and resource interactions.

Track user interactions on your MODX site with custom GA4 events. This guide covers MODX-specific event implementations using templates, plugins, and snippets.

Event Tracking Methods

Method 1: Template-Based Events (Direct Control)

Add event tracking directly to MODX templates.

Method 2: Plugin-Based Events (Automated)

Use MODX plugins to automatically track events.

Method 3: Snippet-Based Events (Reusable)

Create reusable snippets for event tracking.

Use Google Tag Manager for flexible event management.

Common MODX Events

Page View Events

Standard page views are tracked automatically with GA4 configuration. Enhance with MODX data:

<script>
  gtag('event', 'page_view', {
    'page_title': '[[*pagetitle]]',
    'page_location': '[[*uri:fullurl]]',
    'resource_id': '[[*id]]',
    'template': '[[*template]]',
    'parent_id': '[[*parent]]',
    'context': '[[!++context_key]]'
  });
</script>

Form Submission Events

FormIt Form Tracking

MODX's FormIt extra handles forms. Track submissions:

Method 1: Template-Based

Add to template after FormIt call:

[[!FormIt?
  &hooks=`email,FormItSaveForm,trackFormSubmission`
  &emailTo=`info@example.com`
  &emailSubject=`Contact Form Submission`
]]

<!-- Form HTML -->
<form action="[[~[[*id]]]]" method="post">
  [[!+fi.error_message]]

  <input type="text" name="name" value="[[!+fi.name]]" placeholder="Name">
  <input type="email" name="email" value="[[!+fi.email]]" placeholder="Email">
  <textarea name="message" placeholder="Message">[[!+fi.message]]</textarea>

  <button type="submit">Submit</button>
</form>

<!-- GA4 Event on Success -->
[[!+fi.successMessage:notempty=`
<script>
  gtag('event', 'form_submit', {
    'form_name': 'contact_form',
    'form_id': 'contact',
    'resource_id': '[[*id]]',
    'page_title': '[[*pagetitle]]'
  });
</script>
`]]

Method 2: Custom FormIt Hook

Create custom hook plugin:

<?php
/**
 * FormIt Hook: trackFormSubmission
 * Track form submissions to GA4
 */

$formName = $hook->getValue('form_name') ?: 'contact_form';
$resourceId = $modx->resource->get('id');
$pageTitle = $modx->resource->get('pagetitle');

$trackingScript = <<<HTML
<script>
  if (typeof gtag !== 'undefined') {
    gtag('event', 'form_submit', {
      'form_name': '{$formName}',
      'resource_id': '{$resourceId}',
      'page_title': '{$pageTitle}'
    });
  }
</script>
HTML;

$modx->setPlaceholder('ga4_form_tracking', $trackingScript);

return true;

Use in template:

[[!+ga4_form_tracking]]

Method 3: JavaScript-Based

Add to template with form:

<script>
  document.addEventListener('DOMContentLoaded', function() {
    const form = document.querySelector('form[action*="contact"]');

    if (form) {
      form.addEventListener('submit', function(e) {
        if (typeof gtag !== 'undefined') {
          gtag('event', 'form_submit', {
            'form_name': 'contact_form',
            'resource_id': '[[*id]]',
            'form_destination': form.action
          });
        }
      });
    }
  });
</script>

File Download Tracking

Track PDF, ZIP, and other file downloads:

Automatic Download Tracking:

<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Track all download links
    document.querySelectorAll('a[href$=".pdf"], a[href$=".zip"], a[href$=".doc"], a[href$=".docx"]').forEach(function(link) {
      link.addEventListener('click', function(e) {
        const fileUrl = this.href;
        const fileName = fileUrl.split('/').pop();
        const fileExtension = fileName.split('.').pop();

        if (typeof gtag !== 'undefined') {
          gtag('event', 'file_download', {
            'file_name': fileName,
            'file_extension': fileExtension,
            'file_url': fileUrl,
            'resource_id': '[[*id]]',
            'link_text': this.textContent
          });
        }
      });
    });
  });
</script>

MODX Plugin for Download Tracking:

<?php
/**
 * Download Tracking Plugin
 * Events: OnWebPagePrerender
 */

$downloadScript = <<<HTML
<script>
  (function() {
    const downloadExtensions = ['pdf', 'zip', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];

    document.addEventListener('click', function(e) {
      const link = e.target.closest('a');
      if (!link) return;

      const href = link.href;
      const extension = href.split('.').pop().toLowerCase().split('?')[0];

      if (downloadExtensions.includes(extension)) {
        if (typeof gtag !== 'undefined') {
          gtag('event', 'file_download', {
            'file_name': href.split('/').pop(),
            'file_extension': extension,
            'link_text': link.textContent.trim(),
            'resource_id': '{$modx->resource->get('id')}'
          });
        }
      }
    });
  })();
</script>
HTML;

$modx->resource->_output = str_replace('</body>', $downloadScript . '</body>', $modx->resource->_output);

Track clicks on external links:

<script>
  document.addEventListener('DOMContentLoaded', function() {
    const currentDomain = window.location.hostname;

    document.querySelectorAll('a[href^="http"]').forEach(function(link) {
      link.addEventListener('click', function(e) {
        const linkDomain = new URL(this.href).hostname;

        if (linkDomain !== currentDomain) {
          if (typeof gtag !== 'undefined') {
            gtag('event', 'click', {
              'event_category': 'outbound',
              'event_label': this.href,
              'link_text': this.textContent,
              'link_domain': linkDomain,
              'resource_id': '[[*id]]'
            });
          }
        }
      });
    });
  });
</script>

Video Tracking (YouTube)

Track YouTube video interactions embedded in MODX:

<script>
  // YouTube IFrame API
  var tag = document.createElement('script');
  tag.src = "https://www.youtube.com/iframe_api";
  var firstScriptTag = document.getElementsByTagName('script')[0];
  firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

  var players = [];

  function onYouTubeIframeAPIReady() {
    document.querySelectorAll('iframe[src*="youtube.com"]').forEach(function(iframe, index) {
      iframe.id = 'youtube-player-' + index;

      players[index] = new YT.Player(iframe.id, {
        events: {
          'onStateChange': function(event) {
            var videoTitle = event.target.getVideoData().title;
            var videoUrl = event.target.getVideoUrl();

            if (event.data == YT.PlayerState.PLAYING) {
              gtag('event', 'video_start', {
                'video_title': videoTitle,
                'video_url': videoUrl,
                'resource_id': '[[*id]]'
              });
            } else if (event.data == YT.PlayerState.ENDED) {
              gtag('event', 'video_complete', {
                'video_title': videoTitle,
                'video_url': videoUrl,
                'resource_id': '[[*id]]'
              });
            }
          }
        }
      });
    });
  }
</script>

Search Tracking

Track site searches (if using SimpleSearch or other search extra):

SimpleSearch Integration:

[[!SimpleSearch?
  &landing=`[[*id]]`
  &searchIndex=`search`
]]

<!-- Search form -->
<form action="[[~[[*id]]]]" method="get">
  <input type="text" name="search" value="[[!+search]]" placeholder="Search...">
  <button type="submit">Search</button>
</form>

<!-- Track search event -->
[[!+search:notempty=`
<script>
  gtag('event', 'search', {
    'search_term': '[[!+search]]',
    'resource_id': '[[*id]]',
    'results_count': '[[!+total]]'
  });
</script>
`]]

User Engagement Events

Scroll Depth Tracking

Track how far users scroll:

<script>
  (function() {
    var scrollDepths = [25, 50, 75, 100];
    var tracked = {};

    function trackScroll() {
      var scrollPercent = Math.round(
        (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100
      );

      scrollDepths.forEach(function(depth) {
        if (scrollPercent >= depth && !tracked[depth]) {
          tracked[depth] = true;

          if (typeof gtag !== 'undefined') {
            gtag('event', 'scroll', {
              'event_category': 'engagement',
              'event_label': depth + '%',
              'scroll_depth': depth,
              'resource_id': '[[*id]]',
              'page_title': '[[*pagetitle]]'
            });
          }
        }
      });
    }

    var timeout;
    window.addEventListener('scroll', function() {
      clearTimeout(timeout);
      timeout = setTimeout(trackScroll, 100);
    });
  })();
</script>

Time on Page Tracking

Track engagement time:

<script>
  (function() {
    var startTime = new Date();
    var timeThresholds = [30, 60, 120, 300]; // seconds
    var tracked = {};

    setInterval(function() {
      var secondsOnPage = Math.floor((new Date() - startTime) / 1000);

      timeThresholds.forEach(function(threshold) {
        if (secondsOnPage >= threshold && !tracked[threshold]) {
          tracked[threshold] = true;

          if (typeof gtag !== 'undefined') {
            gtag('event', 'time_on_page', {
              'event_category': 'engagement',
              'event_label': threshold + 's',
              'time_seconds': threshold,
              'resource_id': '[[*id]]'
            });
          }
        }
      });
    }, 5000); // Check every 5 seconds
  })();
</script>

Custom MODX Events

Resource Actions

Track when resources are created, updated, or deleted (in Manager):

<?php
/**
 * Resource Tracking Plugin
 * Events: OnDocFormSave
 */

// Only track in manager context
if ($modx->context->key !== 'mgr') return;

$resource = $modx->event->params['resource'];
$mode = $modx->event->params['mode'];

$action = ($mode == modSystemEvent::MODE_NEW) ? 'created' : 'updated';

// Log event (can send to GA4 Measurement Protocol)
$modx->log(modX::LOG_LEVEL_INFO, "Resource {$action}: ID {$resource->id}, Title: {$resource->pagetitle}");

// Send to GA4 via Measurement Protocol (server-side)
// Implementation depends on your setup

User Login/Registration

Track user logins:

<?php
/**
 * User Login Tracking Plugin
 * Events: OnWebAuthentication
 */

$user = $modx->event->params['user'];
$username = $user->get('username');
$userId = $user->get('id');

// Store in session to track on next page load
$_SESSION['modx_login_event'] = [
    'user_id' => $userId,
    'username' => $username,
    'login_time' => time()
];

Then in template:

[[!#modx_login_event:notempty=`
<script>
  gtag('event', 'login', {
    'method': 'MODX',
    'user_id': '[[!#modx_login_event.user_id]]'
  });
</script>
[[!#modx_login_event:remove]]
`]]

GTM-Based Event Tracking

For more flexible event tracking, use Google Tag Manager:

1. Push Events to Data Layer

In MODX template or plugin:

<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    'event': 'modx_form_submit',
    'formName': 'contact',
    'resourceId': '[[*id]]',
    'pageTitle': '[[*pagetitle]]'
  });
</script>

2. Create GTM Trigger

  • Type: Custom Event
  • Event name: modx_form_submit

3. Create GA4 Event Tag

  • Tag Type: Google Analytics: GA4 Event
  • Event Name: form_submit
  • Event Parameters:
    • form_name: \{\{DLV - Form Name\}\}
    • resource_id: \{\{DLV - Resource ID\}\}

See MODX Data Layer for full implementation.

Enhanced Ecommerce (for MODX Commerce)

If using MiniShop2 or SimpleCart:

Product Views

<!-- On product detail page -->
<script>
  gtag('event', 'view_item', {
    'currency': '[[++minishop2.currency]]',
    'value': [[*price:default=`0`]],
    'items': [{
      'item_id': '[[*id]]',
      'item_name': '[[*pagetitle]]',
      'item_category': '[[*parent:is=`5`:then=`Products`]]',
      'price': [[*price:default=`0`]]
    }]
  });
</script>

Add to Cart

<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Listen for MiniShop2 add to cart
    document.addEventListener('msminicart:add', function(e) {
      if (typeof gtag !== 'undefined') {
        gtag('event', 'add_to_cart', {
          'currency': '[[++minishop2.currency]]',
          'value': e.detail.price,
          'items': [{
            'item_id': e.detail.id,
            'item_name': e.detail.name,
            'price': e.detail.price,
            'quantity': e.detail.count
          }]
        });
      }
    });
  });
</script>

Purchase

<!-- On order confirmation page -->
[[!msOrder?
  &to=`orderComplete.tpl`
]]

<!-- In orderComplete.tpl chunk -->
<script>
  gtag('event', 'purchase', {
    'transaction_id': '[[+num]]',
    'value': [[+cost]],
    'currency': '[[++minishop2.currency]]',
    'tax': [[+tax:default=`0`]],
    'shipping': [[+shipping:default=`0`]],
    'items': [
      [[+products:notempty=`[[+products]]`]]
    ]
  });
</script>

Event Tracking Best Practices

1. Use Consistent Naming

Follow GA4 recommended event names:

  • form_submit (not form_submission or submit_form)
  • file_download (not download or file_click)
  • search (not site_search or search_query)

2. Include Resource Context

Always include MODX resource data:

{
  'resource_id': '[[*id]]',
  'page_title': '[[*pagetitle]]',
  'template': '[[*template]]'
}

3. Handle Missing Values

Use MODX output filters for fallbacks:

[[*tv_value:default=`Not Set`]]
[[*tv_value:notempty=`[[*tv_value]]`:default=`Default Value`]]

4. Test Events Thoroughly

  • Use GA4 DebugView
  • Check browser console for errors
  • Verify events fire correctly
  • Test across different resources

5. Document Custom Events

Keep track of custom events:

  • Event names
  • Parameters
  • Where implemented
  • Purpose/use case

Troubleshooting

Events Not Appearing in GA4

Check:

  • GA4 Measurement ID is correct
  • gtag is loaded (console.log(typeof gtag))
  • No JavaScript errors in console
  • Events appear in DebugView (enable debug mode)
  • Proper event name format (lowercase, underscores)

MODX Placeholders Not Resolving

Issue: Placeholders show as literal text in events.

Fix:

// Ensure proper MODX tag syntax
[[*id]]           // Resource field
[[+placeholder]]  // Placeholder
[[++setting]]     // System setting
[[!uncached]]     // Uncached call

Events Fire Multiple Times

Cause: Multiple event listeners or duplicate code.

Fix:

  • Check for duplicate tracking code in templates
  • Use event delegation instead of multiple listeners
  • Check plugins for conflicts

FormIt Events Not Tracking

Check:

  • FormIt hook is properly configured
  • Success message is showing (confirms submission)
  • JavaScript has no errors
  • gtag is loaded before event fires

Next Steps

For general GA4 event concepts, see GA4 Events Guide.