Learn how to track events specific to Strapi-powered sites, including content views, user interactions, and custom events based on your Strapi content structure.
Important: Track in Frontend Framework
Remember that Strapi is headless, so all event tracking happens in your frontend framework (Next.js, Gatsby, Nuxt.js, etc.), not in Strapi itself.
Strapi Content Events
Content View Tracking
Track when users view content from Strapi.
Next.js Example
// app/articles/[slug]/page.tsx
'use client';
import { useEffect } from 'react';
export default function ArticlePage({ article }: { article: Article }) {
useEffect(() => {
// Track content view with Strapi metadata
window.gtag('event', 'view_content', {
content_type: article.attributes.__component || 'article',
content_id: article.id,
items: [{
item_id: article.id,
item_name: article.attributes.title,
item_category: article.attributes.category?.data?.attributes?.name || 'uncategorized',
}],
// Strapi-specific metadata
author: article.attributes.author?.data?.attributes?.name,
published_date: article.attributes.publishedAt,
locale: article.attributes.locale,
});
}, [article]);
return (
<article>
<h1>{article.attributes.title}</h1>
{/* Article content */}
</article>
);
}
Gatsby Example
// src/templates/article.js
import React, { useEffect } from 'react';
import { graphql } from 'gatsby';
const ArticleTemplate = ({ data }) => {
const article = data.strapiArticle;
useEffect(() => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'view_content', {
content_type: 'article',
content_id: article.strapiId,
items: [{
item_id: article.strapiId,
item_name: article.title,
item_category: article.category?.name || 'uncategorized',
}],
author: article.author?.name,
published_date: article.publishedAt,
});
}
}, [article]);
return (
<article>
<h1>{article.title}</h1>
{/* Article content */}
</article>
);
};
export const query = graphql`
query($slug: String!) {
strapiArticle(slug: { eq: $slug }) {
strapiId
title
publishedAt
category {
name
}
author {
name
}
}
}
`;
export default ArticleTemplate;
Nuxt Example
<!-- pages/articles/[slug].vue -->
<script setup>
const route = useRoute();
const { data: article } = await useFetch(`${useRuntimeConfig().public.strapiUrl}/api/articles/${route.params.slug}?populate=*`);
onMounted(() => {
if (process.client && window.gtag && article.value) {
window.gtag('event', 'view_content', {
content_type: 'article',
content_id: article.value.data.id,
items: [{
item_id: article.value.data.id,
item_name: article.value.data.attributes.title,
item_category: article.value.data.attributes.category?.data?.attributes?.name || 'uncategorized',
}],
author: article.value.data.attributes.author?.data?.attributes?.name,
published_date: article.value.data.attributes.publishedAt,
locale: article.value.data.attributes.locale,
});
}
});
</script>
<template>
<article v-if="article">
<h1>{{ article.data.attributes.title }}</h1>
<!-- Article content -->
</article>
</template>
Collection View Tracking
Track when users view collection pages (e.g., all blog posts, products).
// Track collection view
window.gtag('event', 'view_item_list', {
item_list_id: 'articles',
item_list_name: 'All Articles',
items: articles.map((article, index) => ({
item_id: article.id,
item_name: article.attributes.title,
item_category: article.attributes.category?.data?.attributes?.name,
index: index,
})),
});
Search Tracking
Track Strapi content search:
// When user searches Strapi content
function handleSearch(query, results) {
window.gtag('event', 'search', {
search_term: query,
content_type: 'strapi_search',
results_count: results.length,
});
}
Category/Filter Tracking
Track when users filter or navigate categories:
// Track category selection
window.gtag('event', 'select_content', {
content_type: 'category',
item_id: category.id,
item_name: category.attributes.name,
});
// Track filter application
window.gtag('event', 'filter_content', {
filter_type: 'category',
filter_value: selectedCategory,
results_count: filteredArticles.length,
});
User Interaction Events
Content Engagement
Track scroll depth to measure content engagement:
// lib/analytics.js
export function trackScrollDepth() {
let maxScroll = 0;
const milestones = [25, 50, 75, 90, 100];
const handleScroll = () => {
const scrollPercent = Math.round(
(window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100
);
if (scrollPercent > maxScroll) {
maxScroll = scrollPercent;
milestones.forEach(milestone => {
if (scrollPercent >= milestone && !window[`milestone_${milestone}`]) {
window[`milestone_${milestone}`] = true;
window.gtag('event', 'scroll_depth', {
percent_scrolled: milestone,
page_location: window.location.pathname,
});
}
});
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}
// Use in component
useEffect(() => {
const cleanup = trackScrollDepth();
return cleanup;
}, []);
Social Sharing
Track when users share Strapi content:
// Share button click handler
function handleShare(platform, article) {
window.gtag('event', 'share', {
method: platform, // 'twitter', 'facebook', 'linkedin', etc.
content_type: 'article',
item_id: article.id,
content_title: article.attributes.title,
});
}
Newsletter Signup
Track newsletter subscriptions:
// Newsletter form submission
function handleNewsletterSubmit(email) {
window.gtag('event', 'sign_up', {
method: 'newsletter',
location: window.location.pathname,
});
}
Download Tracking
Track when users download files from Strapi:
// Track file downloads
function trackDownload(file) {
window.gtag('event', 'file_download', {
file_name: file.attributes.name,
file_extension: file.attributes.ext,
file_size: file.attributes.size,
link_url: file.attributes.url,
});
}
// Example: Download button
<button => {
trackDownload(strapiFile);
window.open(strapiFile.attributes.url, '_blank');
}}>
Download PDF
</button>
Video/Media Tracking
Track video engagement from Strapi media library:
// Video play tracking
function handleVideoPlay(video) {
window.gtag('event', 'video_start', {
video_title: video.attributes.name,
video_url: video.attributes.url,
video_provider: 'strapi_upload',
});
}
// Video complete tracking
function handleVideoComplete(video, duration) {
window.gtag('event', 'video_complete', {
video_title: video.attributes.name,
video_duration: duration,
});
}
Dynamic Zone Tracking
Track interactions with Strapi Dynamic Zones:
// Track which components users interact with
function trackDynamicZoneInteraction(component) {
window.gtag('event', 'component_interaction', {
component_type: component.__component,
component_id: component.id,
page_location: window.location.pathname,
});
}
// Example: Image gallery component
{article.attributes.content?.map((component) => {
if (component.__component === 'content.image-gallery') {
return (
<ImageGallery
images={component.images} => trackDynamicZoneInteraction(component)}
/>
);
}
})}
Strapi Relations Tracking
Track navigation through related content:
// Track related article clicks
function trackRelatedArticleClick(currentArticle, relatedArticle) {
window.gtag('event', 'select_content', {
content_type: 'related_article',
promotion_id: currentArticle.id,
promotion_name: currentArticle.attributes.title,
items: [{
item_id: relatedArticle.id,
item_name: relatedArticle.attributes.title,
}],
});
}
Multi-Language Tracking (Strapi i18n)
Track language preferences and content in different locales:
// Track language selection
function trackLanguageChange(locale) {
window.gtag('event', 'language_change', {
previous_language: currentLocale,
new_language: locale,
});
// Update GA4 user properties
window.gtag('set', 'user_properties', {
preferred_language: locale,
});
}
// Track content in specific locale
window.gtag('event', 'view_content', {
content_type: 'article',
content_id: article.id,
content_locale: article.attributes.locale,
content_title: article.attributes.title,
});
Custom Content Type Events
Track custom Strapi content types:
// Example: Product from Strapi
function trackProductView(product) {
window.gtag('event', 'view_item', {
currency: 'USD',
value: product.attributes.price,
items: [{
item_id: product.id,
item_name: product.attributes.name,
item_brand: product.attributes.brand?.data?.attributes?.name,
item_category: product.attributes.category?.data?.attributes?.name,
price: product.attributes.price,
}],
});
}
// Example: Event from Strapi
function trackEventRegistration(event) {
window.gtag('event', 'event_registration', {
event_id: event.id,
event_name: event.attributes.title,
event_date: event.attributes.date,
event_location: event.attributes.location,
});
}
// Example: Course from Strapi
function trackCourseEnrollment(course) {
window.gtag('event', 'course_enrollment', {
course_id: course.id,
course_name: course.attributes.title,
course_category: course.attributes.category?.data?.attributes?.name,
instructor: course.attributes.instructor?.data?.attributes?.name,
});
}
Server-Side Event Tracking
For Next.js API routes or server actions:
// app/api/track-conversion/route.ts
export async function POST(request: Request) {
const { eventName, params } = await request.json();
// Send to GA4 Measurement Protocol
const measurementId = process.env.GA_MEASUREMENT_ID!;
const apiSecret = process.env.GA_API_SECRET!;
const response = await fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`,
{
method: 'POST',
body: JSON.stringify({
client_id: params.client_id,
events: [{
name: eventName,
params: params,
}],
}),
}
);
return Response.json({ success: response.ok });
}
Creating a Reusable Analytics Hook (React)
// hooks/useAnalytics.ts
import { useEffect } from 'react';
export function usePageView() {
useEffect(() => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('config', process.env.NEXT_PUBLIC_GA_ID!, {
page_path: window.location.pathname,
});
}
}, []);
}
export function useContentView(content: any) {
useEffect(() => {
if (typeof window !== 'undefined' && window.gtag && content) {
window.gtag('event', 'view_content', {
content_type: content.__component || 'article',
content_id: content.id,
items: [{
item_id: content.id,
item_name: content.attributes?.title || content.title,
}],
});
}
}, [content]);
}
export function useTrackEvent() {
return (eventName: string, params?: any) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', eventName, params);
}
};
}
// Usage in components
function Article({ article }) {
useContentView(article);
const trackEvent = useTrackEvent();
return (
<article>
<h1>{article.attributes.title}</h1>
<button => trackEvent('share', { method: 'twitter' })}>
Share
</button>
</article>
);
}
GTM Data Layer (Recommended)
Instead of direct gtag.js calls, use GTM data layer for better organization:
// Push to data layer
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'view_content',
contentType: article.__component,
contentId: article.id,
contentTitle: article.attributes.title,
contentCategory: article.attributes.category?.data?.attributes?.name,
author: article.attributes.author?.data?.attributes?.name,
publishedDate: article.attributes.publishedAt,
locale: article.attributes.locale,
});
See GTM Data Layer Structure for complete implementation.
Testing & Debugging
1. Use GA4 DebugView
Enable debug mode:
// In development
gtag('config', process.env.NEXT_PUBLIC_GA_ID!, {
debug_mode: process.env.NODE_ENV === 'development',
});
2. Console Logging
Add logging for development:
const trackEvent = (eventName, params) => {
if (process.env.NODE_ENV === 'development') {
console.log('GA4 Event:', eventName, params);
}
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', eventName, params);
}
};
3. Verify Event Parameters
Check in GA4 DebugView:
- Event name follows GA4 conventions
- Parameters are correctly formatted
- Item arrays have required fields
- Values are numeric (not strings)
4. Test Across Content Types
Test events for each Strapi content type:
- Collection types (articles, products, etc.)
- Single types (homepage, about, etc.)
- Relations (categories, authors, etc.)
- Dynamic zones
- Multi-language content
Common Issues
Events Not Firing
Cause: SSR/SSG timing issues or missing window check.
Solution: Always use useEffect or onMounted and check for window:
useEffect(() => {
if (typeof window !== 'undefined' && window.gtag) {
// Track event
}
}, []);
Missing Content Data
Cause: Strapi API response not populated.
Solution: Use populate parameter in API calls:
fetch(`${STRAPI_URL}/api/articles/${slug}?populate=*`)
Or populate specific fields:
fetch(`${STRAPI_URL}/api/articles/${slug}?populate[0]=category&populate[1]=author`)
Duplicate Events
Cause: Multiple useEffect dependencies or event listeners.
Solution: Properly manage dependencies:
// Only track once when content loads
useEffect(() => {
trackContentView(article);
}, [article.id]); // Only re-track if ID changes
Next Steps
- Set up GTM - For easier tag management
- Configure Data Layer - Structure your tracking data
- Troubleshoot Events - Debug tracking issues
For general event tracking concepts, see GA4 Event Tracking Guide.