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.
Method 4: GTM-Based Events (Recommended)
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);
External Link Tracking
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(notform_submissionorsubmit_form)file_download(notdownloadorfile_click)search(notsite_searchorsearch_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
- Install GTM - For advanced event management
- MODX Data Layer - Structure events for GTM
- Events Not Firing - Debug tracking issues
For general GA4 event concepts, see GA4 Events Guide.