SilverStripe Event Tracking with Google Analytics 4 | OpsBlu Docs

SilverStripe Event Tracking with Google Analytics 4

Implement custom event tracking for SilverStripe including form submissions, file downloads, and user interactions.

Track user interactions on your SilverStripe site including form submissions, downloads, clicks, and custom events.

Prerequisites

  • GA4 already installed on SilverStripe (setup guide)
  • Understanding of SilverStripe templates and PHP
  • Access to SilverStripe template files

Basic Event Tracking

JavaScript Event Tracking

File: themes/yourtheme/javascript/analytics.js

// Track custom event
function trackEvent(eventName, params) {
  if (typeof gtag !== 'undefined') {
    gtag('event', eventName, params);
  }
}

// Export for use in templates
window.trackEvent = trackEvent;

Form Submission Tracking

Basic Form Tracking

File: app/src/PageController.php

<?php

namespace App;

use SilverStripe\Forms\Form;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\View\Requirements;

class PageController extends \PageController
{
    private static $allowed_actions = [
        'ContactForm',
    ];

    public function ContactForm()
    {
        $form = Form::create(
            $this,
            'ContactForm',
            FieldList::create(
                TextField::create('Name'),
                EmailField::create('Email'),
                TextareaField::create('Message')
            ),
            FieldList::create(
                FormAction::create('doSubmitContact', 'Send')
            ),
            RequiredFields::create(['Name', 'Email', 'Message'])
        );

        return $form;
    }

    public function doSubmitContact($data, $form)
    {
        // Process form data...

        // Track successful submission
        Requirements::customScript(<<<JS
if (typeof gtag !== 'undefined') {
    gtag('event', 'form_submission', {
        'form_name': 'contact',
        'form_id': 'ContactForm'
    });
}
JS
        );

        return $this->redirectBack()->with([
            'Message' => 'Thank you for your submission!'
        ]);
    }
}

Template-Based Form Tracking

File: themes/yourtheme/templates/Includes/ContactForm.ss

<form $FormAttributes 'contact')">
    $Fields
    $Actions
</form>

<script>
function trackFormSubmit(event, formName) {
    if (typeof gtag !== 'undefined') {
        gtag('event', 'form_submission', {
            'form_name': formName,
            'form_location': window.location.pathname
        });
    }
}
</script>

Download Tracking

Track File Downloads

File: themes/yourtheme/templates/Layout/Page.ss

<% loop $Content.Files %>
<a href="$URL" '{$Extension}', {$Size})"
   download>
    Download $Name
</a>
<% end_loop %>

<script>
function trackDownload(fileName, fileType, fileSize) {
    if (typeof gtag !== 'undefined') {
        gtag('event', 'file_download', {
            'file_name': fileName,
            'file_type': fileType,
            'file_size': fileSize,
            'link_url': event.target.href
        });
    }
}
</script>

PHP-Based Download Tracking

File: app/src/DownloadController.php

<?php

namespace App;

use SilverStripe\Control\Controller;
use SilverStripe\View\Requirements;

class DownloadController extends Controller
{
    private static $url_segment = 'download';

    private static $allowed_actions = [
        'file',
    ];

    public function file()
    {
        $fileID = $this->request->param('ID');
        // Fetch file and serve...

        // Track download server-side or via Requirements
        Requirements::customScript(<<<JS
if (typeof gtag !== 'undefined') {
    gtag('event', 'file_download', {
        'file_id': '{$fileID}',
        'download_method': 'controller'
    });
}
JS
        );

        return $this->serveFile($file);
    }
}

Click Tracking

File: themes/yourtheme/javascript/link-tracking.js

document.addEventListener('DOMContentLoaded', function() {
    // Track all external links
    document.querySelectorAll('a[href^="http"]').forEach(function(link) {
        if (link.hostname !== window.location.hostname) {
            link.addEventListener('click', function() {
                if (typeof gtag !== 'undefined') {
                    gtag('event', 'click', {
                        'link_domain': link.hostname,
                        'link_url': link.href,
                        'outbound': true
                    });
                }
            });
        }
    });
});

Content Interaction Tracking

Blog Post Read Time

File: themes/yourtheme/templates/Layout/BlogPost.ss

<article data-post-id="$ID">
    <h1>$Title</h1>
    $Content
</article>

<script>
(function() {
    var startTime = Date.now();
    var tracked30s = false;

    setTimeout(function() {
        if (!tracked30s && typeof gtag !== 'undefined') {
            gtag('event', 'page_engagement', {
                'content_type': 'blog_post',
                'content_id': '$ID',
                'content_title': '$Title.JS',
                'engagement_time': 30
            });
            tracked30s = true;
        }
    }, 30000);
})();
</script>

Search Tracking

File: app/src/SearchController.php

<?php

namespace App;

use SilverStripe\Control\HTTPRequest;
use SilverStripe\View\Requirements;

class SearchController extends \PageController
{
    public function index(HTTPRequest $request)
    {
        $query = $request->getVar('q');
        $results = $this->performSearch($query);

        // Track search
        Requirements::customScript(<<<JS
if (typeof gtag !== 'undefined') {
    gtag('event', 'search', {
        'search_term': '{$query}',
        'search_results': {$results->count()}
    });
}
JS
        );

        return [
            'Query' => $query,
            'Results' => $results,
        ];
    }
}

Video Tracking

File: themes/yourtheme/templates/Includes/VideoPlayer.ss

<video id="video-{$ID}" controls>
    <source src="$VideoURL" type="video/mp4">
</video>

<script>
(function() {
    var video = document.getElementById('video-{$ID}');
    var tracked = {
        play: false,
        25: false,
        50: false,
        75: false,
        complete: false
    };

    video.addEventListener('play', function() {
        if (!tracked.play && typeof gtag !== 'undefined') {
            gtag('event', 'video_start', {
                'video_title': '$Title.JS',
                'video_url': '$VideoURL'
            });
            tracked.play = true;
        }
    });

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

        if (percent >= 25 && !tracked[25]) {
            gtag('event', 'video_progress', { 'video_percent': 25 });
            tracked[25] = true;
        }
        // Similar for 50, 75...
    });
})();
</script>

Member-Specific Events

File: app/src/MemberExtension.php

<?php

namespace App;

use SilverStripe\ORM\DataExtension;
use SilverStripe\View\Requirements;

class MemberExtension extends DataExtension
{
    public function onAfterLogin()
    {
        Requirements::customScript(<<<JS
if (typeof gtag !== 'undefined') {
    gtag('event', 'login', {
        'method': 'password',
        'member_id': '{$this->owner->ID}'
    });
}
JS
        );
    }

    public function onAfterLogout()
    {
        Requirements::customScript(<<<JS
if (typeof gtag !== 'undefined') {
    gtag('event', 'logout');
}
JS
        );
    }
}

Register extension:

# app/_config/config.yml
SilverStripe\Security\Member:
  extensions:
    - App\MemberExtension

Scroll Depth Tracking

File: themes/yourtheme/javascript/scroll-tracking.js

(function() {
    var tracked = {
        25: false,
        50: false,
        75: false,
        100: false
    };

    window.addEventListener('scroll', function() {
        var scrollPercent = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;

        Object.keys(tracked).forEach(function(milestone) {
            if (scrollPercent >= milestone && !tracked[milestone]) {
                if (typeof gtag !== 'undefined') {
                    gtag('event', 'scroll', {
                        'percent_scrolled': parseInt(milestone)
                    });
                }
                tracked[milestone] = true;
            }
        });
    });
})();

Best Practices

  1. Use Consistent Event Names: Follow GA4 recommended events
  2. Include Context: Add relevant parameters to each event
  3. Avoid Over-Tracking: Don't track every minor interaction
  4. Test Events: Use DebugView to verify events before deployment

Next Steps


Additional Resources