Learn how to track custom events, user interactions, and Craft-specific actions in Google Analytics 4 using Twig templates and JavaScript.
Overview
Event tracking in Craft CMS allows you to measure user interactions with your content, forms, downloads, and other elements. This guide covers implementation patterns specific to Craft CMS architecture.
Standard GA4 Events in Craft
Page View Tracking
Track page views with Craft entry metadata:
{# templates/_analytics/page-view.twig #}
{% if entry is defined and entry %}
<script>
gtag('event', 'page_view', {
'page_title': '{{ entry.title|e('js') }}',
'page_location': '{{ entry.url }}',
'page_path': '{{ craft.app.request.pathInfo }}',
'entry_type': '{{ entry.type.handle }}',
'section': '{{ entry.section.handle }}',
'author_id': '{{ entry.author.id }}',
'publish_date': '{{ entry.postDate|date('Y-m-d') }}'
});
</script>
{% endif %}
Scroll Depth Tracking
Track how far users scroll through Craft entries:
{# templates/_analytics/scroll-tracking.twig #}
<script>
(function() {
var scrollDepths = [25, 50, 75, 90, 100];
var triggered = [];
window.addEventListener('scroll', function() {
var scrollPercent = Math.round(
(window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100
);
scrollDepths.forEach(function(depth) {
if (scrollPercent >= depth && triggered.indexOf(depth) === -1) {
gtag('event', 'scroll', {
'event_category': 'engagement',
'event_label': depth + '%',
'value': depth,
{% if entry is defined %}
'entry_id': '{{ entry.id }}',
'entry_type': '{{ entry.type.handle }}',
'section': '{{ entry.section.handle }}'
{% endif %}
});
triggered.push(depth);
}
});
});
})();
</script>
Craft-Specific Event Tracking
Entry View Tracking
Track views of specific entry types:
{# In your entry template #}
{% if entry is defined %}
<script>
gtag('event', 'view_entry', {
'entry_id': '{{ entry.id }}',
'entry_title': '{{ entry.title|e('js') }}',
'entry_type': '{{ entry.type.handle }}',
'section': '{{ entry.section.handle }}',
'category': '{{ entry.category.first().title ?? 'uncategorized' }}',
'author': '{{ entry.author.fullName|e('js') }}',
'publish_date': '{{ entry.postDate|date('Y-m-d') }}'
});
</script>
{% endif %}
Matrix Block Interaction Tracking
Track interactions with Matrix blocks:
{# templates/_components/matrix-blocks.twig #}
{% for block in entry.contentBlocks.all() %}
{% switch block.type.handle %}
{% case 'videoEmbed' %}
<div class="video-block" data-block-type="video" data-block-id="{{ block.id }}">
{{ block.videoUrl }}
<script>
document.querySelector('[data-block-id="{{ block.id }}"]').addEventListener('click', function() {
gtag('event', 'video_interaction', {
'event_category': 'matrix_block',
'event_label': '{{ block.type.handle }}',
'block_id': '{{ block.id }}',
'entry_id': '{{ entry.id }}'
});
});
</script>
</div>
{% case 'downloadableFile' %}
<a href="{{ block.file.first().url }}"
data-block-type="download"
data-file-name="{{ block.file.first().filename }}" 'file_download', {
'event_category': 'matrix_block',
'event_label': '{{ block.file.first().filename|e('js') }}',
'file_extension': '{{ block.file.first().extension }}',
'block_id': '{{ block.id }}',
'entry_id': '{{ entry.id }}'
});">
Download {{ block.file.first().title }}
</a>
{% case 'imageGallery' %}
<div class="gallery" data-block-id="{{ block.id }}">
{% for image in block.images.all() %}
<img src="{{ image.url }}"
alt="{{ image.title }}" 'image_view', {
'event_category': 'gallery',
'event_label': '{{ image.title|e('js') }}',
'image_id': '{{ image.id }}',
'block_id': '{{ block.id }}'
});">
{% endfor %}
</div>
{% endswitch %}
{% endfor %}
Form Submission Tracking
Track Craft form submissions (using Craft's built-in forms or Freeform plugin):
Native Craft Contact Form
{# templates/_forms/contact.twig #}
<form method="post" id="contact-form">
{{ csrfInput() }}
{{ actionInput('contact-form/send') }}
<input type="hidden" name="toEmail" value="{{ craft.app.systemSettings.getSettings('email').fromEmail }}">
<label for="name">Name</label>
<input type="text" id="name" name="fromName" required>
<label for="email">Email</label>
<input type="email" id="email" name="fromEmail" required>
<label for="message">Message</label>
<textarea id="message" name="message[body]" required></textarea>
<button type="submit">Send</button>
</form>
<script>
document.getElementById('contact-form').addEventListener('submit', function(e) {
gtag('event', 'form_submit', {
'event_category': 'contact',
'event_label': 'contact_form',
'form_name': 'Contact Form',
'page_location': '{{ craft.app.request.absoluteUrl }}'
});
});
</script>
Freeform Plugin Integration
{# templates/_forms/freeform-contact.twig #}
{{ craft.freeform.form("contactForm").render() }}
<script>
document.addEventListener('freeform-ready', function(event) {
var form = event.target.form;
form.addEventListener('freeform-on-submit', function(event) {
gtag('event', 'form_start', {
'event_category': 'freeform',
'event_label': '{{ form.handle }}',
'form_name': '{{ form.name }}'
});
});
form.addEventListener('freeform-on-success', function(event) {
gtag('event', 'form_submit', {
'event_category': 'freeform',
'event_label': '{{ form.handle }}',
'form_name': '{{ form.name }}',
'submission_id': event.detail.submissionId
});
});
});
</script>
Asset Download Tracking
Track downloads of Craft assets:
{# Track all downloadable assets #}
{% set downloadableAssets = entry.downloads.all() %}
{% for asset in downloadableAssets %}
<a href="{{ asset.url }}"
download 'file_download', {
'event_category': 'downloads',
'event_label': '{{ asset.title|e('js') }}',
'file_name': '{{ asset.filename|e('js') }}',
'file_type': '{{ asset.extension }}',
'file_size': {{ asset.size }},
'asset_id': '{{ asset.id }}',
'entry_id': '{{ entry.id }}'
});">
Download {{ asset.title }}
</a>
{% endfor %}
Search Tracking
Track Craft's native search functionality:
{# templates/search/index.twig #}
{% set searchQuery = craft.app.request.getParam('q') %}
{% set searchResults = craft.entries()
.search(searchQuery)
.all() %}
<script>
gtag('event', 'search', {
'search_term': '{{ searchQuery|e('js') }}',
'results_count': {{ searchResults|length }},
'has_results': {{ searchResults|length > 0 ? 'true' : 'false' }}
});
</script>
{# Track search result clicks #}
{% for entry in searchResults %}
<a href="{{ entry.url }}" 'select_content', {
'event_category': 'search',
'content_type': '{{ entry.type.handle }}',
'item_id': '{{ entry.id }}',
'search_term': '{{ searchQuery|e('js') }}'
});">
{{ entry.title }}
</a>
{% endfor %}
User Engagement Events
Category/Tag Filtering
Track when users filter content by categories:
{# templates/blog/index.twig #}
{% set selectedCategory = craft.app.request.getParam('category') %}
{% if selectedCategory %}
<script>
gtag('event', 'filter_content', {
'event_category': 'navigation',
'filter_type': 'category',
'filter_value': '{{ selectedCategory|e('js') }}',
'section': 'blog'
});
</script>
{% endif %}
Related Entry Clicks
Track clicks on related entries:
{# Display related entries #}
{% set relatedEntries = entry.relatedPosts.all() %}
{% for related in relatedEntries %}
<a href="{{ related.url }}" 'select_content', {
'event_category': 'related_content',
'content_type': '{{ related.type.handle }}',
'item_id': '{{ related.id }}',
'source_entry_id': '{{ entry.id }}'
});">
{{ related.title }}
</a>
{% endfor %}
Social Share Tracking
Track social media sharing:
{# templates/_components/social-share.twig #}
<div class="social-share">
<a href="https://twitter.com/intent/tweet?url={{ entry.url|url_encode }}&text={{ entry.title|url_encode }}"
target="_blank" 'share', {
'method': 'twitter',
'content_type': '{{ entry.type.handle }}',
'item_id': '{{ entry.id }}'
});">
Share on Twitter
</a>
<a href="https://www.facebook.com/sharer/sharer.php?u={{ entry.url|url_encode }}"
target="_blank" 'share', {
'method': 'facebook',
'content_type': '{{ entry.type.handle }}',
'item_id': '{{ entry.id }}'
});">
Share on Facebook
</a>
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{ entry.url|url_encode }}"
target="_blank" 'share', {
'method': 'linkedin',
'content_type': '{{ entry.type.handle }}',
'item_id': '{{ entry.id }}'
});">
Share on LinkedIn
</a>
</div>
Live Preview Exclusion
Prevent event tracking during Live Preview:
{# Base template wrapper for all event tracking #}
{% if not craft.app.request.isLivePreview %}
{# Your event tracking code here #}
{% endif %}
GraphQL API Event Tracking
For headless Craft implementations, return event tracking data:
query EntryData($id: Int!) {
entry(id: $id) {
id
title
typeHandle
sectionHandle
author {
id
fullName
}
postDate @formatDateTime(format: "Y-m-d")
}
}
Implement tracking in your frontend application:
// Frontend JavaScript
function trackEntryView(entryData) {
gtag('event', 'view_entry', {
'entry_id': entryData.id,
'entry_title': entryData.title,
'entry_type': entryData.typeHandle,
'section': entryData.sectionHandle,
'author': entryData.author.fullName,
'publish_date': entryData.postDate
});
}
Custom Event Helper Module
Create a Twig extension for cleaner event tracking:
<?php
// modules/analytics/twigextensions/AnalyticsTwigExtension.php
namespace modules\analytics\twigextensions;
use Craft;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class AnalyticsTwigExtension extends AbstractExtension
{
public function getFunctions()
{
return [
new TwigFunction('trackEvent', [$this, 'trackEvent'], ['is_safe' => ['html']]),
];
}
public function trackEvent(string $eventName, array $params = []): string
{
// Skip in Live Preview
if (Craft::$app->request->isLivePreview) {
return '';
}
$jsonParams = json_encode($params, JSON_HEX_APOS | JSON_HEX_QUOT);
return "<script>gtag('event', '{$eventName}', {$jsonParams});</script>";
}
}
Use in templates:
{# Simple event tracking #}
{{ trackEvent('custom_event', {
'event_category': 'engagement',
'event_label': 'button_click',
'value': 1
}) }}
Event Tracking Best Practices
1. Consistent Naming Conventions
{# Use snake_case for event names and parameters #}
gtag('event', 'view_entry', {
'entry_type': 'blog_post',
'content_category': 'web_development'
});
2. Parameter Validation
{# Ensure data exists before tracking #}
{% if entry is defined and entry.id %}
<script>
gtag('event', 'page_view', {
'entry_id': '{{ entry.id }}',
'entry_type': '{{ entry.type.handle ?? 'unknown' }}'
});
</script>
{% endif %}
3. Environment-Specific Tracking
{# Only track in production #}
{% if craft.app.config.general.environment == 'production' %}
{# Event tracking code #}
{% endif %}
Debugging Event Tracking
Enable debug mode in development:
{% if craft.app.config.general.devMode %}
<script>
// Log all events to console
window.dataLayer = window.dataLayer || [];
var originalPush = window.dataLayer.push;
window.dataLayer.push = function() {
console.log('GA4 Event:', arguments);
return originalPush.apply(window.dataLayer, arguments);
};
</script>
{% endif %}
Next Steps
- E-commerce Tracking - Track Craft Commerce transactions and products
- GTM Data Layer - Implement structured data layer for GTM
- Troubleshooting Events - Fix common tracking issues