GA4 Event Tracking for Sanity | Blue Frog Docs

GA4 Event Tracking for Sanity

Implement custom event tracking for Sanity-powered sites including content engagement, user interactions, GROQ queries, and custom dimensions.

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

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

  1. Go to GA4 AdminCustom definitions
  2. Click Create custom dimensions
  3. Add dimensions:
    • content_type (Event-scoped)
    • content_author (Event-scoped)
    • content_tags (Event-scoped)
    • preferred_content_type (User-scoped)

Enhanced Measurement

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:

  1. Go to GA4 AdminDebugView
  2. Navigate your site
  3. 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 views
  • search - Searches
  • sign_up - User signups
  • generate_lead - Lead generation
  • view_item - Product/content views
  • file_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',
})

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

For general GA4 event concepts, see GA4 Event Tracking Guide.

// SYS.FOOTER