Track custom events and user interactions specific to your DatoCMS-powered site. This guide covers DatoCMS-specific event tracking patterns using Google Analytics 4.
DatoCMS-Specific Events
Content View Events
Track when users view DatoCMS content:
// Track content view with full metadata
function trackDatoCMSContentView(record: any) {
if (!window.gtag) return
window.gtag('event', 'datocms_content_view', {
content_id: record.id,
content_type: record._modelApiKey,
content_title: record.title,
content_slug: record.slug,
published_date: record._publishedAt,
updated_date: record._updatedAt,
locale: record._locales?.[0] || 'en',
has_structured_text: !!record.content,
})
}
Modular Content Block Tracking
Track engagement with DatoCMS modular content blocks:
// Track individual content blocks
function trackContentBlocks(content: any, parentId: string) {
content.blocks?.forEach((block: any, index: number) => {
window.gtag('event', 'content_block_impression', {
block_type: block._modelApiKey,
block_id: block.id,
block_position: index,
parent_content_id: parentId,
total_blocks: content.blocks.length,
})
})
}
// Usage in component
useEffect(() => {
if (post.content?.blocks) {
trackContentBlocks(post.content, post.id)
}
}, [post])
Structured Text Engagement
Track reading progress through DatoCMS structured text:
'use client'
import { useEffect, useRef } from 'react'
export function StructuredTextTracker({ content, contentId }) {
const contentRef = useRef<HTMLDivElement>(null)
const trackedDepths = useRef(new Set<number>())
useEffect(() => {
const handleScroll = () => {
if (!contentRef.current || !window.gtag) return
const element = contentRef.current
const scrollDepth = Math.round(
((window.scrollY + window.innerHeight - element.offsetTop) /
element.offsetHeight) *
100
)
// Track at 25%, 50%, 75%, 100%
const milestones = [25, 50, 75, 100]
milestones.forEach((milestone) => {
if (scrollDepth >= milestone && !trackedDepths.current.has(milestone)) {
trackedDepths.current.add(milestone)
window.gtag('event', 'structured_text_depth', {
content_id: contentId,
scroll_depth: milestone,
word_count: estimateWordCount(content),
})
}
})
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [content, contentId])
return <div ref={contentRef}>{/* Render content */}</div>
}
function estimateWordCount(content: any): number {
// Estimate word count from structured text
const text = JSON.stringify(content.value)
return text.split(/\s+/).length
}
Multi-locale Events
Track language switching and locale preferences:
// Track locale change
function trackLocaleChange(
contentId: string,
fromLocale: string,
toLocale: string
) {
window.gtag('event', 'locale_switch', {
content_id: contentId,
from_locale: fromLocale,
to_locale: toLocale,
available_locales: getAllLocales().join(','),
})
}
// Track locale-specific content view
function trackLocalizedContent(record: any, currentLocale: string) {
window.gtag('event', 'localized_content_view', {
content_id: record.id,
content_type: record._modelApiKey,
current_locale: currentLocale,
available_locales: record._allLocales?.join(',') || currentLocale,
is_fallback: record._locales?.[0] !== currentLocale,
})
}
Standard GA4 Events
Page Views
Track page views with DatoCMS context:
// Automatic page view tracking
useEffect(() => {
if (window.gtag) {
window.gtag('event', 'page_view', {
page_title: post.seo?.title || post.title,
page_location: window.location.href,
page_path: window.location.pathname,
content_id: post.id,
content_type: post._modelApiKey,
})
}
}, [post])
Scroll Depth
Track how far users scroll through DatoCMS content:
'use client'
import { useEffect, useRef } from 'react'
export function ScrollDepthTracker({ contentId }: { contentId: string }) {
const tracked = useRef(new Set<number>())
useEffect(() => {
const handleScroll = () => {
const scrollPercentage = Math.round(
((window.scrollY + window.innerHeight) / document.body.scrollHeight) * 100
)
const milestones = [25, 50, 75, 90, 100]
milestones.forEach((milestone) => {
if (scrollPercentage >= milestone && !tracked.current.has(milestone)) {
tracked.current.add(milestone)
if (window.gtag) {
window.gtag('event', 'scroll', {
percent_scrolled: milestone,
content_id: contentId,
})
}
}
})
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [contentId])
return null
}
Click Events
Track clicks on DatoCMS content links:
// Track outbound links
function trackOutboundClick(url: string, contentId: string) {
window.gtag('event', 'click', {
event_category: 'outbound',
event_label: url,
content_id: contentId,
transport_type: 'beacon',
})
}
// Track internal navigation
function trackInternalClick(destination: string, contentId: string) {
window.gtag('event', 'click', {
event_category: 'internal',
event_label: destination,
source_content_id: contentId,
})
}
// Usage in component
<a
href={externalLink} => trackOutboundClick(externalLink, post.id)}
target="_blank"
rel="noopener noreferrer"
>
External Link
</a>
File Downloads
Track downloads from DatoCMS assets:
// Track asset downloads
function trackAssetDownload(asset: any, contentId: string) {
window.gtag('event', 'file_download', {
file_name: asset.filename,
file_extension: asset.format,
file_size: asset.size,
link_url: asset.url,
content_id: contentId,
})
}
// Usage
<a
href={asset.url} => trackAssetDownload(asset, post.id)}
download
>
Download {asset.filename}
</a>
Video Events
Track video engagement from DatoCMS video blocks:
// Track video interactions
function trackVideoEvent(
action: 'play' | 'pause' | 'complete',
video: any,
contentId: string
) {
window.gtag('event', `video_${action}`, {
video_title: video.title,
video_provider: video.provider,
video_url: video.url,
content_id: contentId,
})
}
// Usage with video player
<video => trackVideoEvent('play', videoBlock, post.id)} => trackVideoEvent('pause', videoBlock, post.id)} => trackVideoEvent('complete', videoBlock, post.id)}
>
<source src={videoBlock.video.url} />
</video>
Form Events
Form Submissions
Track form submissions from DatoCMS form blocks:
// Track form submission
function trackFormSubmit(formData: any, formBlock: any, contentId: string) {
window.gtag('event', 'generate_lead', {
form_name: formBlock.formName,
form_id: formBlock.id,
content_id: contentId,
form_type: formBlock._modelApiKey,
})
}
// Usage in form component
const handleSubmit = async (data: any) => {
trackFormSubmit(data, formBlock, post.id)
// Submit form logic
await submitForm(data)
}
Newsletter Signups
Track newsletter subscriptions:
function trackNewsletterSignup(email: string, source: string) {
window.gtag('event', 'sign_up', {
method: 'newsletter',
source: source,
content_id: post.id,
})
}
Search Events
Track site search from DatoCMS content:
function trackSearch(query: string, resultsCount: number) {
window.gtag('event', 'search', {
search_term: query,
results_count: resultsCount,
})
}
// Track search result click
function trackSearchResultClick(result: any, position: number) {
window.gtag('event', 'select_content', {
content_type: result._modelApiKey,
content_id: result.id,
item_position: position,
})
}
E-commerce Events
For DatoCMS-powered e-commerce sites:
Product Views
function trackProductView(product: any) {
window.gtag('event', 'view_item', {
currency: 'USD',
value: product.price,
items: [{
item_id: product.id,
item_name: product.title,
item_category: product.category?.title,
item_variant: product.variant?.name,
price: product.price,
}],
})
}
Add to Cart
function trackAddToCart(product: any, quantity: number) {
window.gtag('event', 'add_to_cart', {
currency: 'USD',
value: product.price * quantity,
items: [{
item_id: product.id,
item_name: product.title,
item_category: product.category?.title,
price: product.price,
quantity: quantity,
}],
})
}
Purchase
function trackPurchase(order: any) {
window.gtag('event', 'purchase', {
transaction_id: order.id,
value: order.total,
currency: 'USD',
tax: order.tax,
shipping: order.shipping,
items: order.items.map((item: any) => ({
item_id: item.product.id,
item_name: item.product.title,
item_category: item.product.category?.title,
price: item.price,
quantity: item.quantity,
})),
})
}
Image Optimization Events
Track Imgix image optimization performance:
function trackImageOptimization(image: any, contentId: string) {
const { responsiveImage } = image
window.gtag('event', 'image_optimization', {
content_id: contentId,
image_format: responsiveImage.webpSrcSet ? 'webp' : 'original',
uses_responsive: !!responsiveImage.srcSet,
uses_blur_placeholder: !!responsiveImage.base64,
image_width: responsiveImage.width,
image_height: responsiveImage.height,
})
}
User Engagement Events
Time on Page
Track actual time spent on DatoCMS content:
'use client'
import { useEffect, useRef } from 'react'
export function TimeOnPageTracker({ contentId }: { contentId: string }) {
const startTime = useRef(Date.now())
const isTracked = useRef(false)
useEffect(() => {
const trackTimeOnPage = () => {
if (isTracked.current) return
isTracked.current = true
const timeSpent = Math.round((Date.now() - startTime.current) / 1000)
if (window.gtag && timeSpent > 10) {
window.gtag('event', 'time_on_page', {
content_id: contentId,
time_seconds: timeSpent,
time_bucket: getTimeBucket(timeSpent),
})
}
}
// Track on page leave
window.addEventListener('beforeunload', trackTimeOnPage)
return () => {
trackTimeOnPage()
window.removeEventListener('beforeunload', trackTimeOnPage)
}
}, [contentId])
return null
}
function getTimeBucket(seconds: number): string {
if (seconds < 30) return '0-30s'
if (seconds < 60) return '30-60s'
if (seconds < 180) return '1-3min'
if (seconds < 300) return '3-5min'
return '5min+'
}
Copy to Clipboard
Track when users copy content:
useEffect(() => {
const handleCopy = () => {
if (window.gtag) {
window.gtag('event', 'content_copy', {
content_id: post.id,
})
}
}
document.addEventListener('copy', handleCopy)
return () => document.removeEventListener('copy', handleCopy)
}, [post.id])
Custom Dimensions
Set custom dimensions for DatoCMS content:
// Configure custom dimensions in GA4
gtag('set', 'user_properties', {
content_preference: userPreferences.contentType,
preferred_locale: userPreferences.locale,
})
// Track with custom dimensions
gtag('event', 'page_view', {
custom_dimension_1: post._modelApiKey,
custom_dimension_2: post.category?.title,
custom_dimension_3: post.author?.name,
})
Event Debugging
Debug events in 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)
}
}
Best Practices
- Consistent Naming: Use consistent event names across your DatoCMS site
- Content Context: Always include
content_idandcontent_typein events - Privacy: Don't track PII in event parameters
- Performance: Batch events when possible to reduce network requests
- Testing: Use GA4 DebugView to verify events
- Documentation: Document custom events for your team
Next Steps
- Troubleshoot Tracking Issues - Debug event tracking
- Set up GTM - Manage tags with GTM
- Optimize Performance - Improve Core Web Vitals