GA4 Event Tracking for Sanity
Track user interactions and content engagement on your Sanity-powered site. Since Sanity is headless, all event tracking is implemented in your frontend application code.
Sanity Content Events
Content View Events
Track when users view Sanity content:
// utils/analytics.ts
export function trackContentView(document: any) {
if (typeof window === 'undefined' || !window.gtag) return
window.gtag('event', 'content_view', {
content_type: document._type,
content_id: document._id,
title: document.title,
category: document.category?.title || 'uncategorized',
tags: document.tags?.map(t => t.title).join(',') || '',
author: document.author?.name || 'unknown',
publish_date: document.publishedAt || document._createdAt,
locale: document.language || 'en',
revision: document._rev,
})
}
// Usage in Next.js component
'use client'
import { useEffect } from 'react'
import { trackContentView } from '@/utils/analytics'
export function BlogPost({ post }) {
useEffect(() => {
trackContentView(post)
}, [post])
return <article>{/* Content */}</article>
}
Content Engagement Tracking
Track scroll depth and time on page:
// components/ContentEngagementTracker.tsx
'use client'
import { useEffect, useState } from 'react'
export function ContentEngagementTracker({ contentId, contentType }) {
const [maxScroll, setMaxScroll] = useState(0)
const [startTime] = useState(Date.now())
useEffect(() => {
const handleScroll = () => {
const scrollPercentage = Math.round(
(window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100
)
if (scrollPercentage > maxScroll) {
setMaxScroll(scrollPercentage)
// Track scroll milestones
if ([25, 50, 75, 90].includes(scrollPercentage)) {
window.gtag?.('event', 'scroll', {
content_id: contentId,
content_type: contentType,
percent_scrolled: scrollPercentage,
})
}
}
}
window.addEventListener('scroll', handleScroll, { passive: true })
// Track time on page when leaving
const handleBeforeUnload = () => {
const timeOnPage = Math.round((Date.now() - startTime) / 1000)
window.gtag?.('event', 'content_engagement', {
content_id: contentId,
content_type: contentType,
time_on_page: timeOnPage,
max_scroll_depth: maxScroll,
})
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => {
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('beforeunload', handleBeforeUnload)
}
}, [contentId, contentType, maxScroll, startTime])
return null
}
Content Search Events
Track searches in Sanity content:
// components/SearchBar.tsx
'use client'
import { useState } from 'react'
import { client } from '@/lib/sanity.client'
export function SearchBar() {
const [query, setQuery] = useState('')
const handleSearch = async (e: React.FormEvent) => {
e.preventDefault()
// Track search event
window.gtag?.('event', 'search', {
search_term: query,
search_type: 'sanity_content',
})
// Perform GROQ search
const results = await client.fetch(
`*[_type in ["post", "page"] && title match $query]`,
{ query: `${query}*` }
)
// Track search results
window.gtag?.('event', 'view_search_results', {
search_term: query,
results_count: results.length,
})
}
return (
<form onSubmit={handleSearch}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<button type="submit">Search</button>
</form>
)
}
User Interaction Events
Link Click Tracking (Portable Text)
Track clicks on Sanity Portable Text links:
// components/PortableText.tsx
import { PortableText as BasePortableText } from '@portabletext/react'
const components = {
marks: {
link: ({ value, children }) => {
const handleClick = () => {
window.gtag?.('event', 'click', {
link_url: value.href,
link_text: children,
link_type: value.href.startsWith('http') ? 'external' : 'internal',
event_category: 'portable_text_link',
})
}
return (
<a
href={value.href}
onClick={handleClick}
target={value.blank ? '_blank' : undefined}
rel={value.blank ? 'noopener noreferrer' : undefined}
>
{children}
</a>
)
},
},
}
export function PortableText({ value }) {
return <BasePortableText value={value} components={components} />
}
Video/Media Tracking (Sanity Assets)
Track Sanity media asset interactions:
// components/SanityVideo.tsx
'use client'
import { getImageDimensions, getFileAsset } from '@sanity/asset-utils'
export function SanityVideo({ asset, title }) {
const handlePlay = () => {
window.gtag?.('event', 'video_start', {
video_title: title || asset.originalFilename,
video_url: asset.url,
video_id: asset._id,
})
}
const handleProgress = (e: React.SyntheticEvent<HTMLVideoElement>) => {
const video = e.currentTarget
const percentWatched = Math.round((video.currentTime / video.duration) * 100)
if ([25, 50, 75].includes(percentWatched)) {
window.gtag?.('event', 'video_progress', {
video_title: title || asset.originalFilename,
percent_watched: percentWatched,
})
}
}
const handleComplete = () => {
window.gtag?.('event', 'video_complete', {
video_title: title || asset.originalFilename,
})
}
return (
<video
src={asset.url}
onPlay={handlePlay}
onTimeUpdate={handleProgress}
onEnded={handleComplete}
controls
/>
)
}
Download Tracking (Sanity Files)
Track Sanity file downloads:
// components/DownloadButton.tsx
'use client'
export function DownloadButton({ fileAsset, label }) {
const handleDownload = () => {
window.gtag?.('event', 'file_download', {
file_name: fileAsset.originalFilename,
file_extension: fileAsset.extension,
file_size: fileAsset.size,
file_id: fileAsset._id,
link_text: label,
link_url: fileAsset.url,
})
}
return (
<a
href={fileAsset.url}
download
onClick={handleDownload}
>
{label || `Download ${fileAsset.originalFilename}`}
</a>
)
}
Form Events
Newsletter Signup
Track newsletter signups from Sanity forms:
// components/NewsletterForm.tsx
'use client'
import { useState } from 'react'
export function NewsletterForm() {
const [email, setEmail] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Track form submission start
window.gtag?.('event', 'generate_lead', {
currency: 'USD',
value: 0,
form_name: 'newsletter_signup',
})
try {
await subscribeToNewsletter(email)
// Track success
window.gtag?.('event', 'sign_up', {
method: 'newsletter',
})
setEmail('')
} catch (error) {
// Track error
window.gtag?.('event', 'exception', {
description: 'newsletter_signup_failed',
fatal: false,
})
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
<button type="submit">Subscribe</button>
</form>
)
}
Contact Form Tracking
Track contact form submissions:
// components/ContactForm.tsx
'use client'
export function ContactForm() {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
window.gtag?.('event', 'generate_lead', {
value: 0,
currency: 'USD',
form_name: 'contact_form',
form_type: formData.get('subject'),
})
try {
await submitContactForm(formData)
window.gtag?.('event', 'form_submit', {
form_name: 'contact_form',
form_destination: 'sales_team',
success: true,
})
} catch (error) {
window.gtag?.('event', 'form_submit', {
form_name: 'contact_form',
success: false,
error_message: error.message,
})
}
}
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
</form>
)
}
E-commerce Events (Sanity + Commerce)
View Item (Product from Sanity)
If using Sanity for product content:
// Track product view
export function trackProductView(product: any) {
window.gtag?.('event', 'view_item', {
currency: 'USD',
value: product.price,
items: [
{
item_id: product.sku || product._id,
item_name: product.title,
item_brand: product.brand,
item_category: product.category?.title,
price: product.price,
},
],
})
}
// Usage in product page
'use client'
export function ProductPage({ product }) {
useEffect(() => {
trackProductView(product)
}, [product])
return <div>{/* Product details */}</div>
}
Add to Cart
export function AddToCartButton({ product, quantity = 1 }) {
const handleAddToCart = () => {
window.gtag?.('event', 'add_to_cart', {
currency: 'USD',
value: product.price * quantity,
items: [
{
item_id: product.sku || product._id,
item_name: product.title,
item_brand: product.brand,
item_category: product.category?.title,
price: product.price,
quantity: quantity,
},
],
})
// Add to cart...
}
return (
<button onClick={handleAddToCart}>
Add to Cart
</button>
)
}
Custom Dimensions & Metrics
Set Up Custom Dimensions
Track Sanity-specific data as custom dimensions:
// Set user properties based on content preferences
export function setUserPreferences(preferences: any) {
window.gtag?.('set', 'user_properties', {
preferred_content_type: preferences.contentType,
preferred_locale: preferences.locale,
subscription_status: preferences.subscriptionStatus,
user_segment: preferences.segment,
})
}
// Set custom dimensions on page view
export function trackPageWithDimensions(document: any) {
window.gtag?.('event', 'page_view', {
// Custom dimensions
content_author: document.author?.name,
content_publish_date: document.publishedAt,
content_update_date: document._updatedAt,
content_locale: document.language,
content_tags: document.tags?.map(t => t.title).join(','),
has_featured_image: !!document.mainImage,
word_count: calculateWordCount(document.body),
})
}
Configure in GA4
- Go to GA4 Admin → Custom definitions
- Click Create custom dimensions
- Add dimensions:
content_type(Event-scoped)content_author(Event-scoped)content_tags(Event-scoped)preferred_content_type(User-scoped)
Enhanced Measurement
Outbound Link Tracking
Auto-track all external links in Sanity content:
// components/ContentWrapper.tsx
'use client'
import { useEffect } from 'react'
export function ContentWrapper({ children }) {
useEffect(() => {
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement
const link = target.closest('a')
if (link && link.href) {
const url = new URL(link.href, window.location.href)
// Check if external
if (url.hostname !== window.location.hostname) {
window.gtag?.('event', 'click', {
event_category: 'outbound',
event_label: url.href,
transport_type: 'beacon',
})
}
}
}
document.addEventListener('click', handleClick)
return () => document.removeEventListener('click', handleClick)
}, [])
return <div>{children}</div>
}
File Download Auto-Tracking
Automatically track all file downloads:
// Auto-track downloads
useEffect(() => {
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement
const link = target.closest('a')
if (link?.href) {
const fileExtension = link.href.split('.').pop()?.toLowerCase()
const downloadableExtensions = ['pdf', 'zip', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']
if (downloadableExtensions.includes(fileExtension || '')) {
window.gtag?.('event', 'file_download', {
file_extension: fileExtension,
file_url: link.href,
link_text: link.textContent || '',
})
}
}
}
document.addEventListener('click', handleClick)
return () => document.removeEventListener('click', handleClick)
}, [])
Framework-Specific Implementations
Next.js Event Wrapper
Create a reusable event tracking hook:
// hooks/useAnalytics.ts
import { useCallback } from 'react'
export function useAnalytics() {
const trackEvent = useCallback((
eventName: string,
eventParams?: Record<string, any>
) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', eventName, eventParams)
}
}, [])
const trackContentView = useCallback((document: any) => {
trackEvent('content_view', {
content_type: document._type,
content_id: document._id,
title: document.title,
})
}, [trackEvent])
return { trackEvent, trackContentView }
}
// Usage
const { trackEvent, trackContentView } = useAnalytics()
useEffect(() => {
trackContentView(sanityDocument)
}, [sanityDocument, trackContentView])
Gatsby Event Plugin
// gatsby-browser.js
export const onRouteUpdate = ({ location, prevLocation }) => {
// Track page view
if (window.gtag) {
window.gtag('config', process.env.GA_MEASUREMENT_ID, {
page_path: location.pathname,
})
}
}
// Track Sanity content views
export const onSanityLoad = (sanityData) => {
if (window.gtag && sanityData) {
window.gtag('event', 'content_view', {
content_type: sanityData._type,
content_id: sanityData._id,
})
}
}
React Context for Analytics
// context/AnalyticsContext.tsx
'use client'
import { createContext, useContext, useCallback } from 'react'
const AnalyticsContext = createContext<any>(null)
export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
const track = useCallback((event: string, params?: any) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', event, params)
}
}, [])
return (
<AnalyticsContext.Provider value={{ track }}>
{children}
</AnalyticsContext.Provider>
)
}
export const useAnalytics = () => {
const context = useContext(AnalyticsContext)
if (!context) {
throw new Error('useAnalytics must be used within AnalyticsProvider')
}
return context
}
// Usage
const { track } = useAnalytics()
track('button_click', { button_name: 'subscribe' })
Testing Events
GA4 DebugView
Enable debug mode to test events:
// Enable debug mode in development
const debugMode = process.env.NODE_ENV === 'development'
gtag('config', GA_MEASUREMENT_ID, {
debug_mode: debugMode,
})
View events:
- Go to GA4 Admin → DebugView
- Navigate your site
- See events appear in real-time with full parameters
Console Logging
Log events during development:
function trackEvent(eventName: string, params?: any) {
if (process.env.NODE_ENV === 'development') {
console.log('📊 GA4 Event:', eventName, params)
}
if (window.gtag) {
window.gtag('event', eventName, params)
}
}
Measurement Protocol
Test events programmatically:
// Server-side event tracking
async function trackServerEvent(eventName: string, params: any) {
const response = await fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${API_SECRET}`,
{
method: 'POST',
body: JSON.stringify({
client_id: userId,
events: [
{
name: eventName,
params: params,
},
],
}),
}
)
return response
}
Best Practices
1. Consistent Event Naming
Use standard GA4 event names when possible:
page_view- Page viewssearch- Searchessign_up- User signupsgenerate_lead- Lead generationview_item- Product/content viewsfile_download- Downloads
2. Event Parameter Limits
GA4 limits:
- Max 25 unique event parameters per event
- Parameter names: 40 characters max
- Parameter values: 100 characters max
3. Avoid PII
Never send personally identifiable information:
// Bad
window.gtag('event', 'sign_up', {
email: user.email, // DON'T
name: user.name, // DON'T
phone: user.phone, // DON'T
})
// Good
window.gtag('event', 'sign_up', {
user_id: hashUserId(user.id), // Hashed
method: 'newsletter',
user_type: 'subscriber',
})
4. Batch Related Events
Group related tracking logic:
function trackSanityPageView(document: any) {
// Page view
window.gtag('event', 'page_view')
// Content view
window.gtag('event', 'content_view', {
content_type: document._type,
content_id: document._id,
})
// User properties
window.gtag('set', 'user_properties', {
preferred_locale: document.language,
})
}
Next Steps
- Set up GTM - Manage events via Tag Manager
- Create Data Layer - Custom data layer for Sanity
- Troubleshoot Events - Debug tracking issues
For general GA4 event concepts, see GA4 Event Tracking Guide.