GA4 Event Tracking on Craft CMS | OpsBlu Docs

GA4 Event Tracking on Craft CMS

Implement custom GA4 event tracking in Craft CMS using Twig templates, JavaScript, and Craft-specific data points.

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

Resources