GA4 Event Tracking on Joomla | OpsBlu Docs

GA4 Event Tracking on Joomla

Implement custom event tracking for Joomla-specific interactions, forms, and user behavior

Track Joomla-specific user interactions beyond standard pageviews using GA4 custom events. This guide covers form submissions, component interactions, module clicks, and Joomla-specific user behaviors.

GA4 Event Structure

GA4 events consist of an event name and optional parameters:

gtag('event', 'event_name', {
    'parameter_1': 'value_1',
    'parameter_2': 'value_2'
});

Joomla-Specific Event Examples:

  • Article views
  • Category navigation
  • Search queries
  • Form submissions (Contact, RSForm, ChronoForms)
  • Module interactions
  • Component-specific actions

Automatic Events (Enhanced Measurement)

GA4's Enhanced Measurement automatically tracks certain events without code:

Enabled by default:

  • Page views
  • Scrolls (90% depth)
  • Outbound clicks
  • Site search
  • Video engagement
  • File downloads

Enable in GA4:

GA4 Property → Data Streams → Web → Enhanced Measurement
✓ Page views
✓ Scrolls
✓ Outbound clicks
✓ Site search (configure query parameter: searchword or filter[search])
✓ Video engagement
✓ File downloads

Joomla Search Parameters: Joomla uses different search parameters depending on component:

  • com_search: searchword
  • com_finder: q
  • Custom search extensions may vary

Configure in GA4:

Enhanced Measurement → Site search → Advanced
Query parameters: searchword,q,filter[search]

Joomla Article Tracking

Track Article Views

Add custom event when article is viewed:

Template Method:

<?php
// In your template's index.php or component override
defined('_JEXEC') or die;

$app = JFactory::getApplication();
$input = $app->input;

if ($input->get('option') === 'com_content' && $input->get('view') === 'article') {
    $doc = JFactory::getDocument();

    $article = JTable::getInstance('content');
    $article->load($input->get('id'));

    $trackingScript = "
        gtag('event', 'view_article', {
            'article_id': '{$article->id}',
            'article_title': " . json_encode($article->title) . ",
            'category': " . json_encode($article->catid) . ",
            'author': " . json_encode($article->created_by_alias) . "
        });
    ";

    $doc->addScriptDeclaration($trackingScript);
}
?>

System Plugin Method:

<?php
defined('_JEXEC') or die;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;

class PlgSystemArticleTracking extends CMSPlugin
{
    protected $app;

    public function onContentAfterDisplay($context, &$article, &$params, $limitstart = 0)
    {
        // Only track article views
        if ($context !== 'com_content.article') {
            return;
        }

        $doc = Factory::getDocument();

        if ($doc->getType() !== 'html') {
            return;
        }

        $trackingScript = "
            gtag('event', 'view_article', {
                'article_id': '{$article->id}',
                'article_title': " . json_encode($article->title) . ",
                'category_id': '{$article->catid}',
                'author_id': '{$article->created_by}'
            });
        ";

        $doc->addScriptDeclaration($trackingScript);
    }
}
?>

Track Article Engagement

Track reading depth and time on page:

<script>
// Track scroll depth on articles
if (typeof gtag !== 'undefined') {
    let scrollDepths = [25, 50, 75, 100];
    let triggered = [];

    window.addEventListener('scroll', function() {
        let scrollPercentage = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;

        scrollDepths.forEach(function(depth) {
            if (scrollPercentage >= depth && !triggered.includes(depth)) {
                triggered.push(depth);
                gtag('event', 'article_scroll', {
                    'scroll_depth': depth,
                    'article_id': '<?php echo $article->id; ?>'
                });
            }
        });
    });

    // Track time on page
    let startTime = Date.now();
    window.addEventListener('beforeunload', function() {
        let timeSpent = Math.round((Date.now() - startTime) / 1000);
        gtag('event', 'article_time_spent', {
            'time_seconds': timeSpent,
            'article_id': '<?php echo $article->id; ?>'
        });
    });
}
</script>

Form Tracking

Contact Component (com_contact)

Track Joomla core contact form submissions:

Create template override:

/templates/your-template/html/com_contact/contact/default.php

Add tracking code:

<?php
defined('_JEXEC') or die;

$doc = JFactory::getDocument();

$formScript = "
    document.addEventListener('DOMContentLoaded', function() {
        const contactForm = document.querySelector('.contact-form');

        if (contactForm) {
            contactForm.addEventListener('submit', function(e) {
                gtag('event', 'form_submit', {
                    'form_type': 'contact',
                    'form_name': 'Joomla Contact Form',
                    'contact_id': '{$this->contact->id}'
                });
            });
        }
    });
";

$doc->addScriptDeclaration($formScript);
?>

<!-- Original contact form template code continues... -->

RSForm Pro

Track RSForm submissions:

Method 1: JavaScript (Client-Side)

<script>
document.addEventListener('DOMContentLoaded', function() {
    // RSForm uses specific class names
    const rsForms = document.querySelectorAll('.rsform');

    rsForms.forEach(function(form) {
        form.addEventListener('submit', function(e) {
            const formId = form.querySelector('input[name="formId"]')?.value || 'unknown';
            const formName = form.getAttribute('data-rsform-name') || 'RSForm';

            gtag('event', 'form_submit', {
                'form_type': 'rsform',
                'form_id': formId,
                'form_name': formName
            });
        });
    });
});
</script>

Method 2: RSForm Script Called on Submission

RSForm Pro → Forms → Edit Form → Properties → Script Called on Form Submission

Add:
gtag('event', 'rsform_submit', {
    'form_id': '{formId}',
    'form_title': '{formTitle}'
});

ChronoForms

Track ChronoForms submissions:

<?php
// In ChronoForms custom code action
defined('_JEXEC') or die;

$doc = JFactory::getDocument();

$trackingScript = "
    gtag('event', 'form_submit', {
        'form_type': 'chronoforms',
        'form_name': '{$form->title}',
        'form_id': '{$form->id}'
    });
";

$doc->addScriptDeclaration($trackingScript);
?>

Or add to form HTML:

<script>
document.getElementById('chronoform-<?php echo $form->id; ?>').addEventListener('submit', function() {
    gtag('event', 'chronoform_submit', {
        'form_name': '<?php echo $form->title; ?>',
        'form_id': '<?php echo $form->id; ?>'
    });
});
</script>

Search Tracking

Track search queries and results:

Template override in com_search:

<?php
// /templates/your-template/html/com_search/search/default.php
defined('_JEXEC') or die;

$app = JFactory::getApplication();
$input = $app->input;
$searchword = $input->get('searchword', '', 'string');
$doc = JFactory::getDocument();

if (!empty($searchword)) {
    $trackingScript = "
        gtag('event', 'search', {
            'search_term': " . json_encode($searchword) . ",
            'search_component': 'com_search',
            'results_count': " . count($this->results) . "
        });
    ";

    $doc->addScriptDeclaration($trackingScript);
}
?>

Smart Search (com_finder)

Track Smart Search queries:

<?php
// /templates/your-template/html/com_finder/search/default.php
defined('_JEXEC') or die;

$app = JFactory::getApplication();
$input = $app->input;
$query = $input->get('q', '', 'string');
$doc = JFactory::getDocument();

if (!empty($query)) {
    $trackingScript = "
        gtag('event', 'search', {
            'search_term': " . json_encode($query) . ",
            'search_component': 'com_finder',
            'results_count': " . $this->total . "
        });
    ";

    $doc->addScriptDeclaration($trackingScript);
}
?>

Module Interactions

Track Module Clicks

Track clicks on specific modules (e.g., custom HTML, menus):

Via Template:

<script>
document.addEventListener('DOMContentLoaded', function() {
    // Track custom module clicks
    const customModules = document.querySelectorAll('.custom-module');

    customModules.forEach(function(module) {
        module.addEventListener('click', function(e) {
            const moduleId = this.getAttribute('data-module-id');
            const moduleTitle = this.getAttribute('data-module-title');

            gtag('event', 'module_click', {
                'module_id': moduleId,
                'module_title': moduleTitle,
                'element_clicked': e.target.tagName
            });
        });
    });
});
</script>

Add to module chrome:

<?php
// /templates/your-template/html/modules.php
defined('_JEXEC') or die;

function modChrome_tracked($module, &$params, &$attribs)
{
    echo '<div class="module-tracked" data-module-id="' . $module->id . '" data-module-title="' . htmlspecialchars($module->title) . '">';
    echo '<h3>' . $module->title . '</h3>';
    echo $module->content;
    echo '</div>';
}
?>

Track menu item clicks:

<script>
document.addEventListener('DOMContentLoaded', function() {
    const menuLinks = document.querySelectorAll('.nav.menu a');

    menuLinks.forEach(function(link) {
        link.addEventListener('click', function(e) {
            const menuText = this.textContent.trim();
            const menuHref = this.getAttribute('href');

            gtag('event', 'menu_click', {
                'menu_text': menuText,
                'menu_link': menuHref,
                'menu_level': this.closest('li').className.includes('deeper') ? 'submenu' : 'main'
            });
        });
    });
});
</script>

Component-Specific Events

Community Builder (CB)

Track CB profile views and interactions:

<script>
document.addEventListener('DOMContentLoaded', function() {
    // Track profile views
    if (document.body.classList.contains('com-comprofiler')) {
        const profileId = document.querySelector('[data-user-id]')?.getAttribute('data-user-id');

        if (profileId) {
            gtag('event', 'profile_view', {
                'component': 'community_builder',
                'profile_id': profileId
            });
        }
    }

    // Track CB form submissions
    const cbForms = document.querySelectorAll('.cbValidation');
    cbForms.forEach(function(form) {
        form.addEventListener('submit', function() {
            gtag('event', 'cb_form_submit', {
                'form_name': this.getAttribute('name') || 'cb_form'
            });
        });
    });
});
</script>

K2

Track K2 item views:

<?php
// In K2 item template override
defined('_JEXEC') or die;

$doc = JFactory::getDocument();

$trackingScript = "
    gtag('event', 'view_k2_item', {
        'item_id': '{$this->item->id}',
        'item_title': " . json_encode($this->item->title) . ",
        'category_id': '{$this->item->catid}',
        'k2_tags': " . json_encode(array_column($this->item->tags, 'name')) . "
    });
";

$doc->addScriptDeclaration($trackingScript);
?>

JomSocial

Track JomSocial social interactions:

<script>
document.addEventListener('DOMContentLoaded', function() {
    // Track activity stream interactions
    const likeButtons = document.querySelectorAll('.joms-like-button');

    likeButtons.forEach(function(button) {
        button.addEventListener('click', function() {
            gtag('event', 'social_interaction', {
                'component': 'jomsocial',
                'interaction_type': 'like',
                'content_type': this.getAttribute('data-type') || 'post'
            });
        });
    });

    // Track share actions
    const shareButtons = document.querySelectorAll('.joms-share-button');

    shareButtons.forEach(function(button) {
        button.addEventListener('click', function() {
            gtag('event', 'share', {
                'component': 'jomsocial',
                'content_type': this.getAttribute('data-type') || 'post',
                'method': 'jomsocial_share'
            });
        });
    });
});
</script>

User Registration and Login

Track User Registration

System plugin tracking user registration:

<?php
defined('_JEXEC') or die;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;

class PlgSystemUserTracking extends CMSPlugin
{
    protected $app;

    public function onUserAfterSave($user, $isNew, $success, $msg)
    {
        if (!$isNew || !$success) {
            return;
        }

        $doc = Factory::getDocument();

        if ($doc->getType() !== 'html') {
            return;
        }

        $trackingScript = "
            gtag('event', 'sign_up', {
                'method': 'joomla_registration',
                'user_id': '{$user['id']}'
            });
        ";

        $doc->addScriptDeclaration($trackingScript);
    }
}
?>

Track Login Events

public function onUserLogin($user, $options = array())
{
    $doc = Factory::getDocument();

    if ($doc->getType() !== 'html') {
        return;
    }

    $trackingScript = "
        gtag('event', 'login', {
            'method': 'joomla_login'
        });
    ";

    $doc->addScriptDeclaration($trackingScript);
}

Download Tracking

Track file downloads from Joomla:

<script>
document.addEventListener('DOMContentLoaded', function() {
    // Track all download links
    const downloadLinks = document.querySelectorAll('a[href$=".pdf"], a[href$=".zip"], a[href$=".doc"], a[href$=".docx"]');

    downloadLinks.forEach(function(link) {
        link.addEventListener('click', function(e) {
            const fileName = this.getAttribute('href').split('/').pop();
            const fileExtension = fileName.split('.').pop();

            gtag('event', 'file_download', {
                'file_name': fileName,
                'file_extension': fileExtension,
                'link_url': this.getAttribute('href')
            });
        });
    });
});
</script>

CTA and Button Tracking

Track call-to-action buttons and links:

<script>
document.addEventListener('DOMContentLoaded', function() {
    // Track CTA buttons
    const ctaButtons = document.querySelectorAll('.btn-cta, .call-to-action, .button-primary');

    ctaButtons.forEach(function(button) {
        button.addEventListener('click', function(e) {
            gtag('event', 'cta_click', {
                'button_text': this.textContent.trim(),
                'button_location': this.getAttribute('data-location') || 'unknown',
                'link_url': this.getAttribute('href') || 'button'
            });
        });
    });

    // Track "Read More" links
    const readMoreLinks = document.querySelectorAll('.readmore a');

    readMoreLinks.forEach(function(link) {
        link.addEventListener('click', function(e) {
            gtag('event', 'read_more_click', {
                'article_title': this.closest('article')?.querySelector('h2')?.textContent || 'unknown'
            });
        });
    });
});
</script>

Video Tracking

Track video interactions (if not using Enhanced Measurement):

<script>
document.addEventListener('DOMContentLoaded', function() {
    const videos = document.querySelectorAll('video');

    videos.forEach(function(video) {
        const videoSrc = video.querySelector('source')?.src || video.src;

        video.addEventListener('play', function() {
            gtag('event', 'video_start', {
                'video_title': this.getAttribute('title') || 'untitled',
                'video_url': videoSrc
            });
        });

        video.addEventListener('ended', function() {
            gtag('event', 'video_complete', {
                'video_title': this.getAttribute('title') || 'untitled',
                'video_url': videoSrc
            });
        });

        // Track 25%, 50%, 75% milestones
        let milestones = [25, 50, 75];
        let triggered = [];

        video.addEventListener('timeupdate', function() {
            const percentage = (video.currentTime / video.duration) * 100;

            milestones.forEach(function(milestone) {
                if (percentage >= milestone && !triggered.includes(milestone)) {
                    triggered.push(milestone);
                    gtag('event', 'video_progress', {
                        'video_title': video.getAttribute('title') || 'untitled',
                        'video_percent': milestone
                    });
                }
            });
        });
    });
});
</script>

Testing Event Tracking

1. Browser Console

// View dataLayer contents
console.log(window.dataLayer);

// Manually trigger test event
gtag('event', 'test_event', {'test_param': 'test_value'});

2. GA4 DebugView

Enable debug mode:

gtag('config', 'G-XXXXXXXXXX', {
    'debug_mode': true
});

Then view events in real-time:

GA4 Property → Configure → DebugView

3. Browser Extensions

4. Real-Time Reports

GA4 Property → Reports → Realtime → Event count by Event name

Trigger events on your site and verify they appear within seconds.

Common Issues

Events not firing:

  • Check gtag is loaded: console.log(window.gtag)
  • Verify Measurement ID is correct
  • Check browser console for JavaScript errors
  • Ensure event listener is attached after DOM loads

Events fire multiple times:

  • Remove duplicate tracking code
  • Check for event listener duplication
  • Verify no extension conflicts

Events missing parameters:

  • Check parameter values are not undefined
  • Verify JSON encoding for special characters
  • Check parameter naming (snake_case required)

See Tracking Troubleshooting for more debugging.

Next Steps