DatoCMS Event Tracking with GA4 | OpsBlu Docs

DatoCMS Event Tracking with GA4

Track custom events, content interactions, and user behavior on DatoCMS-powered sites with Google Analytics 4.

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

  1. Consistent Naming: Use consistent event names across your DatoCMS site
  2. Content Context: Always include content_id and content_type in events
  3. Privacy: Don't track PII in event parameters
  4. Performance: Batch events when possible to reduce network requests
  5. Testing: Use GA4 DebugView to verify events
  6. Documentation: Document custom events for your team

Next Steps