Track user interactions on your PayloadCMS frontend including form submissions, content downloads, video plays, and custom events.
Prerequisites
- GA4 already installed on your PayloadCMS frontend (setup guide)
- Access to your frontend application code
- Understanding of React/Next.js event handling
Event Tracking Utility
Create a reusable event tracking utility:
File: lib/gtag.js
export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_ID;
// Track custom events
export const event = ({ action, category, label, value }) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', action, {
event_category: category,
event_label: label,
value: value,
});
}
};
// Track GA4 recommended events
export const trackEvent = (eventName, eventParams = {}) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', eventName, eventParams);
}
};
Form Tracking
Contact Form Submission
import { trackEvent } from '@/lib/gtag';
export default function ContactForm() {
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
// Submit to Payload API
const response = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/contact-submissions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
}),
});
if (response.ok) {
// Track successful submission
trackEvent('form_submission', {
form_name: 'contact',
form_id: 'contact_form',
submission_status: 'success',
});
alert('Form submitted successfully!');
}
} catch (error) {
// Track failed submission
trackEvent('form_submission', {
form_name: 'contact',
form_id: 'contact_form',
submission_status: 'error',
error_message: error.message,
});
}
};
return (
<form
<input type="text" name="name" required />
<input type="email" name="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
);
}
Newsletter Signup
import { trackEvent } from '@/lib/gtag';
const NewsletterForm = () => {
const handleSignup = async (e) => {
e.preventDefault();
const email = e.target.email.value;
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/newsletter-subscribers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
if (response.ok) {
trackEvent('sign_up', {
method: 'newsletter',
form_location: 'footer',
});
}
} catch (error) {
console.error('Signup error:', error);
}
};
return (
<form
<input type="email" name="email" required />
<button type="submit">Subscribe</button>
</form>
);
};
Content Interaction Tracking
Track Blog Post Reads
// pages/blog/[slug].js
import { useEffect, useRef } from 'react';
import { trackEvent } from '@/lib/gtag';
export default function BlogPost({ post }) {
const hasTrackedRead = useRef(false);
useEffect(() => {
// Track after 30 seconds on page
const timer = setTimeout(() => {
if (!hasTrackedRead.current) {
trackEvent('page_engagement', {
content_type: 'blog_post',
content_id: post.id,
content_title: post.title,
engagement_time: 30,
});
hasTrackedRead.current = true;
}
}, 30000);
return () => clearTimeout(timer);
}, [post]);
return <article>{/* Post content */}</article>;
}
Track File Downloads
import { trackEvent } from '@/lib/gtag';
const DownloadButton = ({ file }) => {
const handleDownload = () => {
trackEvent('file_download', {
file_name: file.filename,
file_type: file.mimeType,
file_size: file.filesize,
link_url: file.url,
});
// Trigger download
window.open(file.url, '_blank');
};
return (
<button
Download {file.filename}
</button>
);
};
Track Video Plays
import { useRef } from 'react';
import { trackEvent } from '@/lib/gtag';
const VideoPlayer = ({ video }) => {
const videoRef = useRef(null);
const hasTrackedPlay = useRef(false);
const hasTracked25 = useRef(false);
const hasTracked50 = useRef(false);
const hasTracked75 = useRef(false);
const handlePlay = () => {
if (!hasTrackedPlay.current) {
trackEvent('video_start', {
video_title: video.title,
video_url: video.url,
video_provider: 'self_hosted',
});
hasTrackedPlay.current = true;
}
};
const handleTimeUpdate = () => {
const video = videoRef.current;
const progress = (video.currentTime / video.duration) * 100;
if (progress >= 25 && !hasTracked25.current) {
trackEvent('video_progress', {
video_title: video.title,
video_percent: 25,
});
hasTracked25.current = true;
} else if (progress >= 50 && !hasTracked50.current) {
trackEvent('video_progress', {
video_title: video.title,
video_percent: 50,
});
hasTracked50.current = true;
} else if (progress >= 75 && !hasTracked75.current) {
trackEvent('video_progress', {
video_title: video.title,
video_percent: 75,
});
hasTracked75.current = true;
}
};
const handleComplete = () => {
trackEvent('video_complete', {
video_title: video.title,
video_url: video.url,
});
};
return (
<video
ref={videoRef}
controls
>
<source src={video.url} type="video/mp4" />
</video>
);
};
Navigation Tracking
Track External Link Clicks
import { trackEvent } from '@/lib/gtag';
const ExternalLink = ({ href, children }) => {
const handleClick = () => {
trackEvent('click', {
link_url: href,
link_domain: new URL(href).hostname,
outbound: true,
});
};
return (
<a href={href} target="_blank" rel="noopener noreferrer"
{children}
</a>
);
};
Track Navigation Menu Clicks
import Link from 'next/link';
import { trackEvent } from '@/lib/gtag';
const NavLink = ({ href, label }) => {
const handleClick = () => {
trackEvent('navigation_click', {
link_text: label,
link_url: href,
menu_location: 'primary_nav',
});
};
return (
<Link href={href}
{label}
</Link>
);
};
Search Tracking
Track Site Search
import { useState } from 'react';
import { useRouter } from 'next/router';
import { trackEvent } from '@/lib/gtag';
const SearchBar = () => {
const [query, setQuery] = useState('');
const router = useRouter();
const handleSearch = async (e) => {
e.preventDefault();
// Fetch results from Payload
const response = await fetch(
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/posts?where[title][like]=${query}`
);
const data = await response.json();
// Track search
trackEvent('search', {
search_term: query,
search_results: data.docs.length,
});
// Navigate to results
router.push(`/search?q=${query}`);
};
return (
<form
<input
type="search"
value={query} => setQuery(e.target.value)}
placeholder="Search..."
/>
<button type="submit">Search</button>
</form>
);
};
User Engagement Tracking
Track Scroll Depth
import { useEffect, useRef } from 'react';
import { trackEvent } from '@/lib/gtag';
export default function Post({ post }) {
const tracked25 = useRef(false);
const tracked50 = useRef(false);
const tracked75 = useRef(false);
const tracked100 = useRef(false);
useEffect(() => {
const handleScroll = () => {
const scrollPercent = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
if (scrollPercent >= 25 && !tracked25.current) {
trackEvent('scroll', { percent_scrolled: 25 });
tracked25.current = true;
} else if (scrollPercent >= 50 && !tracked50.current) {
trackEvent('scroll', { percent_scrolled: 50 });
tracked50.current = true;
} else if (scrollPercent >= 75 && !tracked75.current) {
trackEvent('scroll', { percent_scrolled: 75 });
tracked75.current = true;
} else if (scrollPercent >= 100 && !tracked100.current) {
trackEvent('scroll', { percent_scrolled: 100 });
tracked100.current = true;
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return <article>{/* Content */}</article>;
}
Track Time on Page
import { useEffect, useRef } from 'react';
import { trackEvent } from '@/lib/gtag';
export function usePageTimer(pageName) {
const startTime = useRef(Date.now());
useEffect(() => {
return () => {
const timeSpent = Math.round((Date.now() - startTime.current) / 1000);
trackEvent('page_time', {
page_name: pageName,
time_spent: timeSpent,
});
};
}, [pageName]);
}
// Usage in component
export default function BlogPost({ post }) {
usePageTimer(post.title);
return <article>{/* Content */}</article>;
}
Enhanced Measurement Events
Track Page Views with Custom Dimensions
import { useEffect } from 'react';
import { trackEvent } from '@/lib/gtag';
export default function Post({ post }) {
useEffect(() => {
trackEvent('page_view', {
page_title: post.title,
page_location: window.location.href,
content_type: 'blog_post',
content_category: post.category?.name,
author: post.author?.name,
publish_date: post.publishedDate,
});
}, [post]);
return <article>{/* Content */}</article>;
}
Server-Side Event Tracking
Track Form Submissions via Payload Hooks
// collections/ContactSubmissions.ts
import { CollectionConfig } from 'payload/types';
import { trackServerEvent } from '../lib/server-analytics';
const ContactSubmissions: CollectionConfig = {
slug: 'contact-submissions',
hooks: {
afterChange: [
async ({ doc, operation, req }) => {
if (operation === 'create') {
await trackServerEvent({
clientId: req.ip || 'server',
eventName: 'form_submission',
eventParams: {
form_name: 'contact',
submission_method: 'api',
user_agent: req.headers['user-agent'],
},
});
}
},
],
},
fields: [
{ name: 'name', type: 'text' },
{ name: 'email', type: 'email' },
{ name: 'message', type: 'textarea' },
],
};
export default ContactSubmissions;
Debugging Events
Enable Debug Mode
// In development
if (process.env.NODE_ENV === 'development') {
window.gtag('config', process.env.NEXT_PUBLIC_GA_ID, {
debug_mode: true,
});
}
Log Events to Console
export const trackEvent = (eventName, eventParams = {}) => {
if (typeof window !== 'undefined' && window.gtag) {
// Log in development
if (process.env.NODE_ENV === 'development') {
console.log('GA4 Event:', eventName, eventParams);
}
window.gtag('event', eventName, eventParams);
}
};
Best Practices
- Use Descriptive Event Names: Follow GA4 recommended event names when possible
- Include Context: Add relevant parameters to each event
- Avoid Over-Tracking: Don't track every minor interaction
- Test Events: Use DebugView to verify events before deployment
- Document Events: Maintain a list of custom events and their parameters
Next Steps
- Set up Google Tag Manager for more advanced tracking
- Configure Meta Pixel for Facebook ads tracking
- Troubleshoot Events Not Firing