Sanity Google Analytics Integration | Blue Frog Docs

Sanity Google Analytics Integration

Integrate Google Analytics 4 with Sanity-powered sites for comprehensive content and user behavior tracking.

Sanity Google Analytics Integration

Complete guide to setting up Google Analytics 4 (GA4) on your Sanity-powered website for comprehensive content tracking, user behavior analysis, and performance monitoring.

Getting Started

Choose the implementation approach that best fits your frontend framework:

GA4 Setup Guide

Step-by-step instructions for installing GA4 on Sanity-powered sites using Next.js, Gatsby, Remix, and other frameworks. Includes GROQ query integration and content tracking setup.

Custom Event Tracking

Track custom interactions with Sanity content including article reads, content downloads, Portable Text engagement, and custom content type interactions.

Why GA4 for Sanity?

GA4 provides powerful analytics capabilities for headless CMS implementations:

  • Content Performance Insights: Track which Sanity content types and documents drive engagement
  • User Journey Mapping: Understand how users navigate through your content
  • GROQ-Enhanced Tracking: Enrich events with content metadata from Sanity
  • Framework Compatibility: Works with Next.js, Gatsby, Remix, SvelteKit, and more
  • Real-time Content Analytics: Monitor content updates and their impact
  • Custom Dimensions: Track Sanity-specific metadata (document types, authors, categories)

Implementation Options

Method Best For Complexity Framework
Next.js Script Component Next.js sites (App/Pages Router) Simple Next.js
Gatsby Plugin Static Gatsby sites Simple Gatsby
GTM Implementation Complex tracking, multiple tools Moderate All frameworks
Custom Client Library Full control, custom requirements Advanced All frameworks
Server-Side Events API Privacy-focused, ad-blocker resistant Advanced All frameworks

Prerequisites

Before starting:

  1. Google Analytics 4 property created
  2. Sanity project with GROQ API access
  3. Frontend framework (Next.js, Gatsby, etc.) deployed
  4. Basic understanding of your content schema

Sanity-Specific Tracking Features

Content Metadata Tracking

Leverage Sanity's content structure for rich analytics:

// Fetch content with GROQ for tracking
const contentData = await client.fetch(`
  *[_type == "post" && slug.current == $slug][0]{
    _id,
    _type,
    _rev,
    title,
    "slug": slug.current,
    publishedAt,
    "author": author->name,
    "categories": categories[]->title,
    "readingTime": round(length(pt::text(body)) / 5 / 180)
  }
`, { slug })

// Track with Sanity metadata
gtag('event', 'view_item', {
  item_id: contentData._id,
  item_name: contentData.title,
  item_category: contentData.categories?.join(','),
  content_type: contentData._type,
  author: contentData.author,
  reading_time_minutes: contentData.readingTime
})

Portable Text Engagement

Track user interaction with rich content blocks:

// Track scroll depth through Portable Text blocks
function trackPortableTextEngagement(blockIndex, blockType) {
  gtag('event', 'content_block_view', {
    block_index: blockIndex,
    block_type: blockType,
    content_id: contentData._id
  })
}

Document Revision Tracking

Monitor content updates and their impact:

gtag('event', 'content_version_view', {
  document_id: contentData._id,
  document_revision: contentData._rev,
  published_date: contentData.publishedAt
})

Multi-Language Support

For internationalized Sanity projects:

// Track content language
const localizedContent = await client.fetch(`
  *[_type == "post" && slug.current == $slug][0]{
    _id,
    __i18n_lang,
    __i18n_refs
  }
`)

gtag('config', 'G-XXXXXXXXXX', {
  content_group: localizedContent.__i18n_lang
})

Framework-Specific Examples

Next.js App Router

// app/layout.tsx
import { GoogleAnalytics } from '@next/third-parties/google'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <GoogleAnalytics gaId="G-XXXXXXXXXX" />
      </body>
    </html>
  )
}

Next.js Pages Router

// pages/_app.js
import Script from 'next/script'

export default function App({ Component, pageProps }) {
  return (
    <>
      <Script
        src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
        strategy="afterInteractive"
      />
      <Script id="google-analytics" strategy="afterInteractive">
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', 'G-XXXXXXXXXX');
        `}
      </Script>
      <Component {...pageProps} />
    </>
  )
}

Gatsby

// gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-google-gtag`,
      options: {
        trackingIds: ['G-XXXXXXXXXX'],
        pluginConfig: {
          head: true,
          respectDNT: true,
          exclude: ['/preview/**'],
        },
      },
    },
  ],
}

Remix

// app/root.tsx
import { useLocation } from '@remix-run/react'
import { useEffect } from 'react'

export default function Root() {
  const location = useLocation()

  useEffect(() => {
    if (typeof window !== 'undefined' && window.gtag) {
      window.gtag('config', 'G-XXXXXXXXXX', {
        page_path: location.pathname
      })
    }
  }, [location])

  return (
    <html>
      <head>
        <script
          async
          src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
        />
        <script
          dangerouslySetInnerHTML={{
            __html: `
              window.dataLayer = window.dataLayer || [];
              function gtag(){dataLayer.push(arguments);}
              gtag('js', new Date());
              gtag('config', 'G-XXXXXXXXXX');
            `
          }}
        />
      </head>
      <body>
        <Outlet />
      </body>
    </html>
  )
}

GROQ Query Best Practices for Analytics

Optimize Query Performance

// Fetch only needed fields for tracking
const query = `*[_type == "post" && slug.current == $slug][0]{
  _id,
  _type,
  title,
  "categoryName": category->title
}`

// Instead of fetching entire document
const badQuery = `*[_type == "post" && slug.current == $slug][0]`

Use Projections

// Efficient content metadata query
const analyticsQuery = groq`{
  "id": _id,
  "type": _type,
  "title": title,
  "author": author->name,
  "tags": tags[]->name,
  "publishDate": publishedAt,
  "wordCount": length(pt::text(body))
}`

Handle Missing References

// Graceful fallback for missing refs
const safeQuery = groq`{
  _id,
  title,
  "author": coalesce(author->name, "Unknown"),
  "category": coalesce(category->title, "Uncategorized")
}`

Performance Considerations

Avoid Blocking Renders

Use framework-specific async loading:

// Next.js - defer analytics
<Script
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
  strategy="afterInteractive"  // Loads after page is interactive
/>

Cache GROQ Queries

// Use SWR or React Query for caching
import useSWR from 'swr'

function useContentAnalytics(slug) {
  const { data } = useSWR(
    ['analytics', slug],
    () => client.fetch(analyticsQuery, { slug }),
    { revalidateOnFocus: false }
  )
  return data
}

Batch Analytics Events

// Collect events, send in batches
const eventQueue = []

function queueEvent(eventName, params) {
  eventQueue.push({ eventName, params })

  if (eventQueue.length >= 10) {
    flushEvents()
  }
}

function flushEvents() {
  eventQueue.forEach(({ eventName, params }) => {
    gtag('event', eventName, params)
  })
  eventQueue.length = 0
}

Privacy and Compliance

Exclude Preview Mode

// Next.js - don't track preview sessions
import { draftMode } from 'next/headers'

export default async function Page() {
  const { isEnabled } = draftMode()

  return (
    <div>
      {!isEnabled && <Analytics />}
      <Content />
    </div>
  )
}
// Initialize with consent defaults
gtag('consent', 'default', {
  'analytics_storage': 'denied',
  'ad_storage': 'denied'
})

// Update after user consent
function handleConsent(granted) {
  gtag('consent', 'update', {
    'analytics_storage': granted ? 'granted' : 'denied'
  })
}

PII Protection

// Sanitize content before tracking
function sanitizeForTracking(content) {
  return {
    id: content._id,
    type: content._type,
    // Never include user emails, names from comments, etc.
    title: content.title,
    category: content.category?.title
  }
}

Common Issues and Solutions

Issue: Events Not Firing on Client-Side Navigation

Cause: SPA frameworks don't reload page on navigation

Solution: Track route changes manually

// Next.js example
import { useRouter } from 'next/router'
import { useEffect } from 'react'

export default function Analytics() {
  const router = useRouter()

  useEffect(() => {
    const handleRouteChange = (url) => {
      gtag('config', 'G-XXXXXXXXXX', {
        page_path: url
      })
    }

    router.events.on('routeChangeComplete', handleRouteChange)
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange)
    }
  }, [router.events])

  return null
}

Issue: Content Metadata Not Available

Cause: GROQ query executes before content is fetched

Solution: Use loading states or SSR

// Fetch content server-side
export async function getStaticProps({ params }) {
  const content = await client.fetch(query, { slug: params.slug })

  return {
    props: { content }
  }
}

// Track after content is available
export default function Page({ content }) {
  useEffect(() => {
    if (content) {
      trackContentView(content)
    }
  }, [content])

  return <Content data={content} />
}

Additional Resources

// SYS.FOOTER