Concrete CMS GA4 Event Tracking | OpsBlu Docs

Concrete CMS GA4 Event Tracking

Track Concrete CMS-specific events in GA4 including form submissions, file downloads, and custom block interactions.

Learn how to track Concrete CMS-specific events in Google Analytics 4, including form submissions, file downloads, user interactions, and custom block events.

Event Tracking Methods

Using Direct gtag.js Implementation

Add event tracking directly in your theme templates or custom blocks.

GTM provides easier management and testing of events. See GTM Setup.

Common Concrete CMS Events

Form Submissions

Track form block submissions (most common use case).

Method 1: GTM Auto-Event Listener (Easiest)

If using GTM, create a form submission trigger:

  1. GTMTriggersNew
  2. Trigger Type: Form Submission
  3. Name: Concrete CMS - Form Submit
  4. This trigger fires on: All Forms (or Some Forms with filter)
  5. Save

Create GA4 event tag:

  1. GTMTagsNew
  2. Tag Configuration: Google Analytics: GA4 Event
  3. Event Name: form_submit
  4. Event Parameters:
    • form_name: \{\{Form ID\}\} or \{\{Form Classes\}\}
    • form_destination: \{\{Page URL\}\}
  5. Triggering: Concrete CMS - Form Submit
  6. Save and Publish

Method 2: Custom JavaScript in Theme

Add to your theme's main JavaScript file or page template:

<script>
document.addEventListener('DOMContentLoaded', function() {
  // Track Concrete CMS form blocks
  const forms = document.querySelectorAll('form[action*="/ccm/system/form/submit"]');

  forms.forEach(function(form) {
    form.addEventListener('submit', function(e) {
      const formName = form.getAttribute('data-form-name') ||
                       form.querySelector('.ccm-form-name')?.textContent ||
                       'Unknown Form';

      gtag('event', 'form_submit', {
        'form_name': formName,
        'form_id': form.id || 'no-id',
        'page_path': window.location.pathname
      });
    });
  });
});
</script>

Method 3: Form Block Template Override

Create custom form block template with tracking:

Location: /application/blocks/form/templates/custom_tracking.php

<?php
// Include default form template
$this->inc('view.php');
?>

<script>
document.addEventListener('DOMContentLoaded', function() {
  const form = document.querySelector('#<?php echo $form_instance_id; ?>');

  if (form) {
    form.addEventListener('submit', function(e) {
      gtag('event', 'form_submit', {
        'form_name': '<?php echo addslashes($form_name); ?>',
        'form_id': '<?php echo $form_instance_id; ?>',
        'event_category': 'Form',
        'event_label': '<?php echo addslashes($form_name); ?>'
      });
    });
  }
});
</script>

Then select "Custom Tracking" template when adding form block.

File Downloads

Track downloads from file manager and document library blocks.

GTM Click Trigger Method

  1. Create TriggerClick - All Elements
  2. Trigger Fires On: Some Clicks
  3. Conditions:
    • Click URL matches RegEx: \.(pdf|doc|docx|xls|xlsx|zip|mp4|mp3)$
  4. Name: Concrete CMS - File Download

Create GA4 Event Tag:

  1. Event Name: file_download
  2. Event Parameters:
    • file_name: \{\{Click Text\}\}
    • file_extension: Use custom JavaScript variable to extract extension
    • link_url: \{\{Click URL\}\}
  3. Trigger: Concrete CMS - File Download

Direct Implementation

<script>
document.addEventListener('DOMContentLoaded', function() {
  // Track file downloads
  const fileLinks = document.querySelectorAll('a[href*="/download_file/"]');

  fileLinks.forEach(function(link) {
    link.addEventListener('click', function(e) {
      const fileName = link.textContent.trim() || link.href.split('/').pop();
      const fileExtension = fileName.split('.').pop().toLowerCase();

      gtag('event', 'file_download', {
        'file_name': fileName,
        'file_extension': fileExtension,
        'link_url': link.href,
        'link_text': link.textContent.trim()
      });
    });
  });
});
</script>

Track when users click external links.

<script>
document.addEventListener('DOMContentLoaded', function() {
  const currentDomain = window.location.hostname;

  document.querySelectorAll('a[href^="http"]').forEach(function(link) {
    link.addEventListener('click', function(e) {
      const linkDomain = new URL(link.href).hostname;

      // Only track external links
      if (linkDomain !== currentDomain) {
        gtag('event', 'click', {
          'event_category': 'Outbound Link',
          'event_label': link.href,
          'link_text': link.textContent.trim(),
          'link_domain': linkDomain
        });
      }
    });
  });
});
</script>

Video Interactions (YouTube Block)

Track YouTube video blocks in Concrete CMS.

<script>
// Track YouTube video plays from Concrete CMS YouTube block
window.onYouTubeIframeAPIReady = function() {
  const players = [];

  document.querySelectorAll('iframe[src*="youtube.com"]').forEach(function(iframe, index) {
    const player = new YT.Player(iframe, {
      events: {
        'onStateChange': function(event) {
          const videoTitle = iframe.title || iframe.getAttribute('data-title') || 'Unknown Video';

          if (event.data === YT.PlayerState.PLAYING) {
            gtag('event', 'video_start', {
              'video_title': videoTitle,
              'video_provider': 'YouTube'
            });
          }

          if (event.data === YT.PlayerState.ENDED) {
            gtag('event', 'video_complete', {
              'video_title': videoTitle,
              'video_provider': 'YouTube'
            });
          }
        }
      }
    });

    players.push(player);
  });
};
</script>

Page Type Tracking

Track different page types as custom events:

<?php
if (!$c->isEditMode() && !$this->controller->isControllerTaskInstanceOf('DashboardPageController')) {
    $pageType = $c->getPageTypeHandle();
    $pageName = $c->getCollectionName();
    ?>
    <script>
      gtag('event', 'page_type_view', {
        'page_type': '<?php echo $pageType; ?>',
        'page_name': '<?php echo addslashes($pageName); ?>',
        'content_group': '<?php echo $pageType; ?>'
      });
    </script>
    <?php
}
?>

Blog Post Tracking

Track blog post views with custom dimensions:

<?php
if ($c->getPageTypeHandle() === 'blog_entry') {
    $author = $c->getVersionObject()->getVersionAuthorUserName();
    $datePublished = $c->getCollectionDatePublic('Y-m-d');
    ?>
    <script>
      gtag('event', 'view_item', {
        'item_id': '<?php echo $c->getCollectionID(); ?>',
        'item_name': '<?php echo addslashes($c->getCollectionName()); ?>',
        'item_category': 'Blog Post',
        'item_brand': 'Blog',
        'content_type': 'blog_post',
        'author': '<?php echo addslashes($author); ?>',
        'publish_date': '<?php echo $datePublished; ?>'
      });
    </script>
    <?php
}
?>

E-Commerce Tracking (Community Store)

If using the Community Store add-on for e-commerce:

Product View Event

Add to product page template or block:

<?php
// Assuming $product is the Community Store product object
if (isset($product) && !$c->isEditMode()) {
    ?>
    <script>
      gtag('event', 'view_item', {
        'currency': '<?php echo \Config::get('community_store.currency'); ?>',
        'value': <?php echo $product->getPrice(); ?>,
        'items': [{
          'item_id': '<?php echo $product->getID(); ?>',
          'item_name': '<?php echo addslashes($product->getName()); ?>',
          'item_category': '<?php echo addslashes($product->getGroupName()); ?>',
          'price': <?php echo $product->getPrice(); ?>,
          'quantity': 1
        }]
      });
    </script>
    <?php
}
?>

Add to Cart Event

<script>
document.addEventListener('DOMContentLoaded', function() {
  // Listen for Community Store add to cart
  document.querySelectorAll('form.store-form-add-to-cart').forEach(function(form) {
    form.addEventListener('submit', function(e) {
      const productId = form.querySelector('input[name="pID"]')?.value;
      const productName = form.querySelector('.product-name')?.textContent;
      const productPrice = form.querySelector('.product-price')?.getAttribute('data-price');
      const quantity = form.querySelector('input[name="quantity"]')?.value || 1;

      gtag('event', 'add_to_cart', {
        'currency': 'USD', // Set your currency
        'value': parseFloat(productPrice) * parseInt(quantity),
        'items': [{
          'item_id': productId,
          'item_name': productName,
          'quantity': parseInt(quantity),
          'price': parseFloat(productPrice)
        }]
      });
    });
  });
});
</script>

Purchase Event

Add to order confirmation page template:

<?php
// On order confirmation page
if (isset($order) && !$c->isEditMode()) {
    $items = [];
    foreach ($order->getOrderItems() as $item) {
        $items[] = [
            'item_id' => $item->getProductID(),
            'item_name' => $item->getProductName(),
            'price' => $item->getPrice(),
            'quantity' => $item->getQuantity()
        ];
    }
    ?>
    <script>
      gtag('event', 'purchase', {
        'transaction_id': '<?php echo $order->getOrderID(); ?>',
        'value': <?php echo $order->getTotal(); ?>,
        'currency': '<?php echo \Config::get('community_store.currency'); ?>',
        'tax': <?php echo $order->getTaxTotal(); ?>,
        'shipping': <?php echo $order->getShippingTotal(); ?>,
        'items': <?php echo json_encode($items); ?>
      });
    </script>
    <?php
}
?>

Custom Block Event Tracking

For custom blocks, add tracking to block's view template.

Example: Custom CTA Block

<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>

<div class="custom-cta-block" id="cta-<?php echo $bID; ?>">
    <a href="<?php echo $ctaUrl; ?>" class="btn btn-primary cta-button">
        <?php echo $ctaText; ?>
    </a>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
  const ctaButton = document.querySelector('#cta-<?php echo $bID; ?> .cta-button');

  if (ctaButton) {
    ctaButton.addEventListener('click', function(e) {
      gtag('event', 'cta_click', {
        'event_category': 'CTA',
        'event_label': '<?php echo addslashes($ctaText); ?>',
        'link_url': '<?php echo $ctaUrl; ?>',
        'block_id': '<?php echo $bID; ?>'
      });
    });
  }
});
</script>

User Registration and Login

Track user registrations and logins:

Registration Event

Add to registration page template or success page:

<?php
// After successful registration
if (!$c->isEditMode()) {
    ?>
    <script>
      gtag('event', 'sign_up', {
        'method': 'Concrete CMS Registration',
        'event_category': 'User',
        'event_label': 'New Registration'
      });
    </script>
    <?php
}
?>

Login Event

Add to login form or post-login page:

<script>
// Track login form submission
document.querySelector('form[action*="/login/authenticate"]')?.addEventListener('submit', function(e) {
  gtag('event', 'login', {
    'method': 'Concrete CMS Login',
    'event_category': 'User'
  });
});
</script>

Search Tracking

Track site searches using Concrete CMS search:

<script>
document.addEventListener('DOMContentLoaded', function() {
  // Check if this is a search results page
  const urlParams = new URLSearchParams(window.location.search);
  const searchQuery = urlParams.get('query') || urlParams.get('search');

  if (searchQuery) {
    gtag('event', 'search', {
      'search_term': searchQuery,
      'page_location': window.location.href
    });
  }

  // Track search form submissions
  document.querySelectorAll('form[action*="/search"]').forEach(function(form) {
    form.addEventListener('submit', function(e) {
      const searchInput = form.querySelector('input[name="query"], input[name="search"]');

      if (searchInput) {
        gtag('event', 'search', {
          'search_term': searchInput.value
        });
      }
    });
  });
});
</script>

Scroll Depth Tracking

Track how far users scroll on pages:

<script>
(function() {
  let scrollDepths = [25, 50, 75, 90, 100];
  let tracked = {};

  window.addEventListener('scroll', function() {
    const scrollPercent = Math.round(
      ((window.scrollY + window.innerHeight) / document.body.scrollHeight) * 100
    );

    scrollDepths.forEach(function(depth) {
      if (scrollPercent >= depth && !tracked[depth]) {
        tracked[depth] = true;

        gtag('event', 'scroll', {
          'event_category': 'Scroll Depth',
          'event_label': depth + '%',
          'value': depth,
          'page_path': window.location.pathname
        });
      }
    });
  });
})();
</script>

Testing & Debugging

1. Use GA4 DebugView

Enable debug mode:

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

Then check AdminDebugView in GA4.

2. Check Browser Console

Monitor data layer in browser console:

// View all data layer pushes
console.log(window.dataLayer);

// Listen for new pushes
const originalPush = window.dataLayer.push;
window.dataLayer.push = function() {
  console.log('Data Layer Push:', arguments[0]);
  originalPush.apply(window.dataLayer, arguments);
};

3. GTM Preview Mode

If using GTM:

  1. Click Preview in GTM
  2. Enter your Concrete CMS site URL
  3. Navigate and trigger events
  4. Verify events appear in Tag Assistant

4. Verify Event Parameters

In GA4 DebugView:

  • Check event name matches GA4 recommended events
  • Verify parameters are present and correctly formatted
  • Ensure values are numeric (no currency symbols)
  • Check that item arrays are properly structured

Common Issues

Events Fire in Edit Mode

Problem: Events tracking when editing pages.

Solution: Always check edit mode:

<?php if (!$c->isEditMode()) { ?>
  // Event tracking code
<?php } ?>

Form Submissions Not Tracked

Problem: Forms submit before event fires.

Solution: Add delay or use beacon:

form.addEventListener('submit', function(e) {
  e.preventDefault();

  gtag('event', 'form_submit', {
    'event_callback': function() {
      form.submit();
    }
  });
});

Cache Prevents Updated Tracking

Problem: Event code changes don't appear.

Solution: Clear Concrete CMS cache:

  • Dashboard → System & Settings → Optimization → Clear Cache

Next Steps

For general event tracking concepts, see GA4 Event Tracking Guide.