TYPO3 GA4 Event Tracking | OpsBlu Docs

TYPO3 GA4 Event Tracking

Implement custom events in TYPO3 using TypoScript, Fluid templates, and JavaScript for forms, downloads, and user interactions

Learn how to track TYPO3-specific user interactions with GA4 events, including form submissions (Powermail, Form Framework), content elements, downloads, and custom user journeys using TypoScript and Fluid.

Understanding TYPO3 Event Tracking

TYPO3 sites generate unique interaction patterns that require specialized tracking:

  • Form Framework submissions (TYPO3 Core forms)
  • Powermail forms (most popular form extension)
  • Content element interactions (accordions, tabs, sliders)
  • File downloads (FAL-managed files)
  • News/Blog interactions (EXT:news, EXT:blog)
  • Backend-to-Frontend transitions

Enhanced Measurement (Automatic Events)

GA4's Enhanced Measurement automatically tracks common events when enabled in your GA4 property settings.

Automatic Events Available

  • page_view - Every page load
  • scroll - 90% scroll depth
  • click - Outbound link clicks
  • view_search_results - TYPO3 indexed_search results
  • video_start, video_progress, video_complete - Embedded videos
  • file_download - FAL file downloads

TYPO3-Specific Search Tracking

# Track TYPO3 indexed_search
[request.getQueryParams()['tx_indexedsearch_pi2']['search']['sword'] != '']
page.jsFooterInline.110 = TEXT
page.jsFooterInline.110.stdWrap.dataWrap (
    gtag('event', 'search', {
        'search_term': '{request.getQueryParams()['tx_indexedsearch_pi2']['search']['sword']}'
    });
)
[END]

Form Framework (TYPO3 Core) Tracking

TYPO3's native Form Framework provides hooks for event tracking.

TypoScript Configuration

plugin.tx_form {
    settings {
        yamlConfigurations {
            100 = EXT:your_sitepackage/Configuration/Form/CustomSetup.yaml
        }
    }
}

Form YAML Configuration

Create Configuration/Form/CustomSetup.yaml:

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          finishersDefinition:
            GoogleAnalytics:
              implementationClassName: 'Vendor\Extension\Finisher\GoogleAnalyticsFinisher'

Custom Finisher (PHP)

Create Classes/Finisher/GoogleAnalyticsFinisher.php:

<?php
declare(strict_types=1);

namespace Vendor\Extension\Finisher;

use TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher;

class GoogleAnalyticsFinisher extends AbstractFinisher
{
    protected function executeInternal()
    {
        $formRuntime = $this->finisherContext->getFormRuntime();
        $formIdentifier = $formRuntime->getIdentifier();
        $formTitle = $formRuntime->getFormDefinition()->getLabel();

        // Add JavaScript to page for GA4 event
        $GLOBALS['TSFE']->additionalFooterData['ga4_form_' . $formIdentifier] = '
        <script>
            gtag("event", "form_submit", {
                "event_category": "TYPO3 Form",
                "event_label": "' . htmlspecialchars($formTitle) . '",
                "form_id": "' . htmlspecialchars($formIdentifier) . '",
                "form_name": "' . htmlspecialchars($formTitle) . '"
            });
        </script>';

        return null;
    }
}

JavaScript-Based Form Tracking

page.jsFooterInline.120 = TEXT
page.jsFooterInline.120.value (
// Track TYPO3 Form Framework submissions
document.addEventListener('DOMContentLoaded', function() {
    const forms = document.querySelectorAll('[data-type="finisher"]');

    forms.forEach(function(form) {
        form.addEventListener('submit', function(e) {
            const formId = this.getAttribute('id');
            const formName = this.querySelector('h2')?.textContent || 'Unknown Form';

            gtag('event', 'form_submit', {
                'event_category': 'TYPO3 Form Framework',
                'event_label': formName,
                'form_id': formId,
                'form_name': formName
            });
        });
    });
});
)

Powermail Form Tracking

Powermail is TYPO3's most popular form extension with extensive tracking capabilities.

JavaScript Event Listener

page.jsFooterInline.130 = TEXT
page.jsFooterInline.130.value (
// Track Powermail submissions
document.addEventListener('DOMContentLoaded', function() {
    const powermailForms = document.querySelectorAll('.powermail_form');

    powermailForms.forEach(function(form) {
        form.addEventListener('submit', function(e) {
            const formUid = this.getAttribute('data-powermail-uid');
            const formName = this.querySelector('.powermail_fieldset legend')?.textContent || 'Contact Form';

            gtag('event', 'form_submit', {
                'event_category': 'Powermail',
                'event_label': formName,
                'form_id': formUid,
                'form_name': formName,
                'value': 1
            });
        });
    });

    // Track field interactions
    document.querySelectorAll('.powermail_field input, .powermail_field select').forEach(function(field) {
        field.addEventListener('focus', function() {
            gtag('event', 'form_start', {
                'event_category': 'Powermail',
                'event_label': 'User Started Form',
                'form_id': this.closest('.powermail_form').getAttribute('data-powermail-uid')
            });
        }, {once: true});
    });
});
)

Powermail TypoScript Hook

plugin.tx_powermail {
    settings {
        setup {
            marketing {
                # Enable Google Analytics tracking
                googleAnalytics {
                    _enable = 1
                    measurementId = {$analytics.ga4.measurementId}
                }
            }
        }
    }
}

Ajax Form Submission Tracking

page.includeJSFooter.powermail = EXT:your_sitepackage/Resources/Public/JavaScript/powermail-ga4.js

Create Resources/Public/JavaScript/powermail-ga4.js:

// Track Powermail AJAX submissions
jQuery(document).on('powermail:submit:success', function(e, data) {
    const formUid = data.formUid || 'unknown';
    const formName = jQuery('[data-powermail-uid="' + formUid + '"]')
        .find('legend').first().text() || 'Contact Form';

    gtag('event', 'form_submit', {
        'event_category': 'Powermail AJAX',
        'event_label': formName,
        'form_id': formUid,
        'submission_type': 'ajax'
    });
});

// Track form errors
jQuery(document).on('powermail:submit:error', function(e, data) {
    gtag('event', 'form_error', {
        'event_category': 'Powermail',
        'event_label': 'Submission Error',
        'form_id': data.formUid || 'unknown'
    });
});

File Download Tracking (FAL)

Track downloads from TYPO3's File Abstraction Layer:

TypoScript Implementation

page.jsFooterInline.140 = TEXT
page.jsFooterInline.140.value (
// Track FAL file downloads
document.addEventListener('DOMContentLoaded', function() {
    document.querySelectorAll('a[href]').forEach(function(link) {
        const href = link.getAttribute('href');

        // Check for TYPO3 file links (fileadmin, _processed, etc.)
        const isFileLink = href && (
            href.includes('/fileadmin/') ||
            href.includes('/_processed/') ||
            /\.(pdf|zip|docx?|xlsx?|pptx?|txt|csv|mp4|mp3)$/i.test(href)
        );

        if (isFileLink) {
            link.addEventListener('click', function(e) {
                const fileName = href.split('/').pop().split('?')[0];
                const fileExtension = fileName.split('.').pop().toLowerCase();

                gtag('event', 'file_download', {
                    'event_category': 'Downloads',
                    'event_label': fileName,
                    'file_name': fileName,
                    'file_extension': fileExtension,
                    'link_url': href,
                    'link_text': this.textContent.trim()
                });
            });
        }
    });
});
)

FAL Metadata-Enhanced Tracking

# Create custom ViewHelper or DataProcessor to access FAL metadata
page = PAGE
page {
    10 = FLUIDTEMPLATE
    10 {
        dataProcessing {
            100 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
            100 {
                references.fieldName = media
                as = files
            }
        }
    }
}

In Fluid template:

<f:for each="{files}" as="file">
    <a href="{file.publicUrl}"
       data-file-type="{file.extension}"
       data-file-size="{file.size}"
       data-file-name="{file.name}"
       class="tracked-download">
        {file.title}
    </a>
</f:for>

<script>
document.querySelectorAll('.tracked-download').forEach(function(link) {
    link.addEventListener('click', function() {
        gtag('event', 'file_download', {
            'file_name': this.getAttribute('data-file-name'),
            'file_extension': this.getAttribute('data-file-type'),
            'file_size': this.getAttribute('data-file-size'),
            'file_title': this.textContent.trim()
        });
    });
});
</script>

News Extension (EXT:news) Tracking

Track interactions with Georg Ringer's news extension:

Article View Tracking

[request.getQueryParams()['tx_news_pi1']['news'] > 0]
page.jsFooterInline.150 = TEXT
page.jsFooterInline.150.dataWrap (
    gtag('event', 'view_item', {
        'event_category': 'News',
        'event_label': '{page:title}',
        'item_id': '{request.getQueryParams()['tx_news_pi1']['news']}',
        'content_type': 'article'
    });
)
[END]

Category Tracking

[request.getQueryParams()['tx_news_pi1']['overwriteDemand']['categories'] != '']
page.jsFooterInline.151 = TEXT
page.jsFooterInline.151.dataWrap (
    gtag('event', 'view_item_list', {
        'event_category': 'News',
        'event_label': 'Category View',
        'item_list_name': 'news_category_{request.getQueryParams()['tx_news_pi1']['overwriteDemand']['categories']}'
    });
)
[END]

Comment/Share Tracking via Fluid

<!-- In your News detail template -->
<f:if condition="{newsItem}">
    <script>
        // Track news article view
        gtag('event', 'view_item', {
            'content_type': 'news_article',
            'item_id': '{newsItem.uid}',
            'item_name': '{newsItem.title -> f:format.htmlentities()}',
            'author': '{newsItem.author -> f:format.htmlentities()}',
            'publish_date': '{newsItem.datetime -> f:format.date(format: "Y-m-d")}'
        });
    </script>

    <!-- Track share button clicks -->
    <div class="news-share">
        <button class="share-facebook" data-article-id="{newsItem.uid}">Share on Facebook</button>
        <button class="share-twitter" data-article-id="{newsItem.uid}">Share on Twitter</button>
    </div>

    <script>
        document.querySelectorAll('.news-share button').forEach(function(button) {
            button.addEventListener('click', function() {
                const platform = this.classList.contains('share-facebook') ? 'facebook' : 'twitter';
                gtag('event', 'share', {
                    'method': platform,
                    'content_type': 'news_article',
                    'item_id': this.getAttribute('data-article-id')
                });
            });
        });
    </script>
</f:if>

Custom Content Element Tracking

Accordion/Collapsible Tracking

page.jsFooterInline.160 = TEXT
page.jsFooterInline.160.value (
// Track accordion interactions
document.addEventListener('DOMContentLoaded', function() {
    document.querySelectorAll('.ce-accordion .accordion-toggle').forEach(function(toggle, index) {
        toggle.addEventListener('click', function() {
            const title = this.textContent.trim();
            const isExpanding = !this.classList.contains('active');

            gtag('event', isExpanding ? 'accordion_expand' : 'accordion_collapse', {
                'event_category': 'Content Elements',
                'event_label': title,
                'accordion_position': index + 1
            });
        });
    });
});
)

Tab Navigation Tracking

page.includeJSFooter.tabs = EXT:your_sitepackage/Resources/Public/JavaScript/tab-tracking.js

Resources/Public/JavaScript/tab-tracking.js:

document.addEventListener('DOMContentLoaded', function() {
    document.querySelectorAll('.ce-tabs .tab-navigation a').forEach(function(tabLink, index) {
        tabLink.addEventListener('click', function(e) {
            const tabName = this.textContent.trim();

            gtag('event', 'tab_click', {
                'event_category': 'Content Elements',
                'event_label': tabName,
                'tab_position': index + 1
            });
        });
    });
});

User Authentication Tracking

Frontend User Login

# After successful login
[frontend.user.isLoggedIn == true]
page.jsFooterInline.170 = TEXT
page.jsFooterInline.170.value (
    // Check if this is a new session
    if (!sessionStorage.getItem('typo3_login_tracked')) {
        gtag('event', 'login', {
            'method': 'TYPO3 Frontend'
        });
        sessionStorage.setItem('typo3_login_tracked', '1');
    }
)
[END]

User Registration Tracking

Create a custom Extbase controller action:

<?php
namespace Vendor\Extension\Controller;

class UserController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController
{
    public function createAction(\Vendor\Extension\Domain\Model\User $user)
    {
        $this->userRepository->add($user);

        // Add GA4 tracking script
        $GLOBALS['TSFE']->additionalFooterData['ga4_signup'] = '
        <script>
            gtag("event", "sign_up", {
                "method": "TYPO3 Frontend Registration"
            });
        </script>';

        $this->redirect('success');
    }
}

Search Query Tracking

[request.getQueryParams()['tx_indexedsearch_pi2']['search']['sword'] != '']
page.jsFooterInline.180 = TEXT
page.jsFooterInline.180.stdWrap {
    dataWrap (
        gtag('event', 'search', {
            'search_term': '{request.getQueryParams()['tx_indexedsearch_pi2']['search']['sword']}',
            'search_results': document.querySelectorAll('.tx-indexedsearch-res').length || 0
        });
    )
}
[END]

Search Result Clicks

page.jsFooterInline.181 = TEXT
page.jsFooterInline.181.value (
// Track search result clicks
document.querySelectorAll('.tx-indexedsearch-res a').forEach(function(link, index) {
    link.addEventListener('click', function() {
        const searchTerm = new URLSearchParams(window.location.search).get('tx_indexedsearch_pi2[search][sword]');

        gtag('event', 'select_content', {
            'content_type': 'search_result',
            'item_id': index + 1,
            'search_term': searchTerm || 'unknown'
        });
    });
});
)

Video Tracking (Custom Players)

Video.js Integration

page.includeJSFooter.videoTracking = EXT:your_sitepackage/Resources/Public/JavaScript/video-tracking.js

Resources/Public/JavaScript/video-tracking.js:

// Track custom video player events
document.addEventListener('DOMContentLoaded', function() {
    const videos = document.querySelectorAll('video');

    videos.forEach(function(video) {
        const videoSrc = video.querySelector('source')?.src || video.currentSrc || 'unknown';
        const videoTitle = video.getAttribute('title') || videoSrc.split('/').pop();

        let tracked25 = false, tracked50 = false, tracked75 = false;

        video.addEventListener('play', function() {
            gtag('event', 'video_start', {
                'video_title': videoTitle,
                'video_url': videoSrc
            });
        });

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

            if (percent >= 25 && !tracked25) {
                tracked25 = true;
                gtag('event', 'video_progress', {
                    'video_title': videoTitle,
                    'video_percent': 25
                });
            }
            if (percent >= 50 && !tracked50) {
                tracked50 = true;
                gtag('event', 'video_progress', {
                    'video_title': videoTitle,
                    'video_percent': 50
                });
            }
            if (percent >= 75 && !tracked75) {
                tracked75 = true;
                gtag('event', 'video_progress', {
                    'video_title': videoTitle,
                    'video_percent': 75
                });
            }
        });

        video.addEventListener('ended', function() {
            gtag('event', 'video_complete', {
                'video_title': videoTitle,
                'video_url': videoSrc
            });
        });
    });
});

Custom Button and CTA Tracking

TypoScript-Based Button Tracking

page.jsFooterInline.190 = TEXT
page.jsFooterInline.190.value (
// Track all buttons and CTAs
document.addEventListener('DOMContentLoaded', function() {
    // Track Fluid-rendered buttons
    document.querySelectorAll('.btn, .cta-button, .ce-button a').forEach(function(button) {
        button.addEventListener('click', function() {
            gtag('event', 'cta_click', {
                'event_category': 'CTA',
                'event_label': this.textContent.trim(),
                'button_text': this.textContent.trim(),
                'button_url': this.href || 'no-url',
                'page_location': window.location.pathname
            });
        });
    });

    // Track newsletter signups
    document.querySelectorAll('form[name="newsletter"] button[type="submit"]').forEach(function(button) {
        button.addEventListener('click', function(e) {
            gtag('event', 'newsletter_signup_attempt', {
                'event_category': 'Engagement',
                'event_label': 'Newsletter Form'
            });
        });
    });
});
)

Advanced Event Tracking with Data Attributes

Fluid Template with Data Attributes

<f:link.page
    pageUid="{page.uid}"
    class="tracked-link"
    data="{
        gaCategory: 'Internal Navigation',
        gaAction: 'click',
        gaLabel: page.title
    }"
>
    {page.title}
</f:link.page>

Generic Data Attribute Tracker

page.jsFooterInline.200 = TEXT
page.jsFooterInline.200.value (
// Track all elements with data-ga-* attributes
document.querySelectorAll('[data-ga-category]').forEach(function(element) {
    element.addEventListener('click', function() {
        const category = this.getAttribute('data-ga-category');
        const action = this.getAttribute('data-ga-action') || 'click';
        const label = this.getAttribute('data-ga-label') || this.textContent.trim();

        gtag('event', action, {
            'event_category': category,
            'event_label': label
        });
    });
});
)

User Properties (Custom Dimensions)

Set User Properties via TypoScript

[frontend.user.isLoggedIn == true]
page.jsFooterInline.210 = TEXT
page.jsFooterInline.210.dataWrap (
    gtag('set', 'user_properties', {
        'user_type': 'logged_in',
        'user_group': '{TSFE:fe_user|user|usergroup}',
        'member_since': 'frontend_user'
    });
)
[END]

[frontend.user.isLoggedIn == false]
page.jsFooterInline.210 = TEXT
page.jsFooterInline.210.value (
    gtag('set', 'user_properties', {
        'user_type': 'guest'
    });
)
[END]

Debugging Event Tracking

Enable GA4 Debug Mode

page.jsFooterInline.999 = TEXT
page.jsFooterInline.999.value (
    // Enable debug mode in development
    gtag('config', '{$analytics.ga4.measurementId}', {
        'debug_mode': true
    });

    // Log all dataLayer pushes
    const originalPush = window.dataLayer.push;
    window.dataLayer.push = function() {
        console.log('dataLayer.push:', arguments);
        return originalPush.apply(this, arguments);
    };
)

TYPO3 Admin Panel Debug

Enable in Backend → User Settings → Admin Panel:

  • Preview
  • Cache (disable for testing)
  • Info (see current TypoScript conditions)

Performance Considerations

Conditional Loading

# Only load event tracking scripts on specific pages
[page["uid"] in [10, 15, 20]]
    page.includeJSFooter.eventTracking = EXT:your_sitepackage/Resources/Public/JavaScript/events.js
[END]

Defer Non-Critical Tracking

page.includeJSFooter {
    ga4Events = EXT:your_sitepackage/Resources/Public/JavaScript/ga4-events.js
    ga4Events.defer = 1
}

Next Steps