Fix Events Not Firing on Sanity Sites | Blue Frog Docs

Fix Events Not Firing on Sanity Sites

Troubleshoot analytics tracking issues specific to Sanity-powered websites including SSR, client-side routing, GROQ data timing, and data layer problems.

Fix Events Not Firing on Sanity Sites

Analytics events failing to fire is a common issue on Sanity-powered sites due to SSR/CSR complexities, client-side routing, and data layer timing. This guide covers Sanity-specific troubleshooting.

For general event troubleshooting, see the global tracking issues guide.

Common Sanity-Specific Issues

1. Server-Side Rendering Issues

Problem: Analytics code trying to access window on the server.

Symptoms:

  • window is not defined error
  • document is not defined error
  • Analytics not initializing

Diagnosis:

Check browser console for errors:

ReferenceError: window is not defined
ReferenceError: document is not defined

Solutions:

A. Check for Browser Environment

// Wrong - will error on server
const analytics = window.gtag('event', 'page_view')

// Right - check for window first
if (typeof window !== 'undefined' && window.gtag) {
  window.gtag('event', 'page_view')
}

B. Use useEffect for Client-Side Code

'use client'

import { useEffect } from 'react'

export function AnalyticsTracker({ sanityData }) {
  useEffect(() => {
    // Only runs on client
    if (window.gtag) {
      window.gtag('event', 'content_view', {
        content_id: sanityData._id,
        content_type: sanityData._type,
      })
    }
  }, [sanityData])

  return null
}

C. Dynamic Imports for Analytics

// Only load analytics on client
if (typeof window !== 'undefined') {
  import('./analytics').then((analytics) => {
    analytics.initialize()
  })
}

D. Next.js Script Component

import Script from 'next/script'

export function Analytics() {
  return (
    <Script
      id="ga4"
      strategy="afterInteractive"  // Only loads on client
      dangerouslySetInnerHTML={{
        __html: `
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', 'G-XXXXXXXXXX');
        `,
      }}
    />
  )
}

2. GROQ Query Timing Issues

Problem: Sanity data not available when analytics fires.

Symptoms:

  • Events fire with undefined content data
  • Missing Sanity document fields in analytics
  • Intermittent tracking

Diagnosis:

// Check if Sanity data loaded
console.log('Sanity document:', sanityDocument)
console.log('Document ID:', sanityDocument?._id)

// Track after data loads
useEffect(() => {
  if (sanityDocument) {
    console.log('Ready to track')
  }
}, [sanityDocument])

Solutions:

A. Wait for Sanity Data

export function ContentTracker({ documentId }) {
  const [document, setDocument] = useState(null)

  useEffect(() => {
    // Fetch Sanity document
    client.fetch(`*[_id == $id][0]`, { id: documentId })
      .then(data => setDocument(data))
  }, [documentId])

  // Track only after data loads
  useEffect(() => {
    if (document && window.gtag) {
      window.gtag('event', 'content_view', {
        content_id: document._id,
        content_type: document._type,
        title: document.title,
      })
    }
  }, [document])  // Fires when document changes

  return null
}

B. Use Suspense for Data Loading

// app/blog/[slug]/page.tsx
import { Suspense } from 'react'
import { client } from '@/lib/sanity.client'

async function BlogContent({ slug }) {
  const query = `*[_type == "post" && slug.current == $slug][0]`
  const post = await client.fetch(query, { slug })

  return (
    <>
      <ContentTracker document={post} />
      <article>{/* Content */}</article>
    </>
  )
}

export default function BlogPage({ params }) {
  return (
    <Suspense fallback={<Loading />}>
      <BlogContent slug={params.slug} />
    </Suspense>
  )
}

3. Client-Side Routing Issues

Problem: Events not firing on SPA route changes.

Symptoms:

  • Page views only tracked on initial load
  • Events fire once, then stop
  • Data layer not updating on navigation

Diagnosis:

// Monitor route changes
console.log('Current route:', window.location.pathname)

// Check if analytics fires on navigation
window.addEventListener('popstate', () => {
  console.log('Route changed, analytics should fire')
})

Solutions:

A. Next.js App Router

'use client'

import { usePathname, useSearchParams } from 'next/navigation'
import { useEffect } from 'react'

export function PageViewTracker() {
  const pathname = usePathname()
  const searchParams = useSearchParams()

  useEffect(() => {
    if (window.gtag) {
      const url = pathname + (searchParams?.toString() ? `?${searchParams}` : '')

      window.gtag('config', process.env.NEXT_PUBLIC_GA_ID!, {
        page_path: url,
      })
    }
  }, [pathname, searchParams])

  return null
}

B. Next.js Pages Router

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function App({ Component, pageProps }) {
  const router = useRouter()

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

      if (window.dataLayer) {
        window.dataLayer.push({
          event: 'pageview',
          page: url,
        })
      }
    }

    router.events.on('routeChangeComplete', handleRouteChange)

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

  return <Component {...pageProps} />
}

C. Gatsby

// gatsby-browser.js
export const onRouteUpdate = ({ location, prevLocation }) => {
  if (typeof window !== 'undefined' && window.gtag) {
    window.gtag('config', 'G-XXXXXXXXXX', {
      page_path: location.pathname,
    })
  }

  // For GTM
  if (window.dataLayer) {
    window.dataLayer.push({
      event: 'pageview',
      page: location.pathname,
    })
  }
}

4. Data Layer Timing Issues

Problem: Pushing data to data layer before GTM loads.

Symptoms:

  • dataLayer is undefined error
  • Variables return undefined in GTM
  • Events don't reach GA4/Meta Pixel

Diagnosis:

// Check if dataLayer exists
console.log('dataLayer exists:', typeof window.dataLayer !== 'undefined')

// Check dataLayer contents
console.table(window.dataLayer)

// Monitor dataLayer pushes
const originalPush = window.dataLayer?.push
if (window.dataLayer) {
  window.dataLayer.push = function() {
    console.log('DataLayer push:', arguments[0])
    return originalPush?.apply(window.dataLayer, arguments)
  }
}

Solutions:

A. Initialize Data Layer Early

// app/layout.tsx - Before GTM script
export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `window.dataLayer = window.dataLayer || [];`,
          }}
        />
        {/* GTM script here */}
      </head>
      <body>{children}</body>
    </html>
  )
}

B. Wait for Data Layer to Exist

export function pushToDataLayer(data: any) {
  if (typeof window === 'undefined') return

  // Wait for dataLayer to exist
  const interval = setInterval(() => {
    if (window.dataLayer) {
      clearInterval(interval)
      window.dataLayer.push(data)
    }
  }, 100)

  // Stop after 5 seconds
  setTimeout(() => clearInterval(interval), 5000)
}

5. Sanity Draft Content Tracking

Problem: Analytics tracking draft/preview content.

Symptoms:

  • Test data in production analytics
  • Inflated metrics from content team
  • Duplicate events during preview

Diagnosis:

// Check if document is a draft
const isDraft = sanityDocument._id.startsWith('drafts.')
console.log('Is draft:', isDraft)

// Check for preview mode
const isPreview = searchParams.get('preview') === 'true'
console.log('Is preview:', isPreview)

Solutions:

A. Exclude Draft Documents

'use client'

export function Analytics({ document }) {
  const isDraft = document._id.startsWith('drafts.')

  if (isDraft) {
    console.log('Draft document, analytics disabled')
    return null
  }

  return <AnalyticsScript />
}

B. Check for Preview Mode

import { useSearchParams } from 'next/navigation'

export function Analytics() {
  const searchParams = useSearchParams()
  const isPreview = searchParams.get('preview') === 'true'

  if (isPreview) {
    console.log('Preview mode detected, analytics disabled')
    return null
  }

  return <AnalyticsScript />
}

C. Sanity Visual Editing Exclusion

// Detect Sanity Visual Editing mode
function isSanityVisualEditing(): boolean {
  if (typeof window === 'undefined') return false

  return window.location.search.includes('sanity-visual-editing=true') ||
         window.location.pathname.startsWith('/studio')
}

export function Analytics() {
  if (isSanityVisualEditing()) return null

  return <AnalyticsScript />
}

6. Missing Analytics Scripts

Problem: Analytics scripts not loading.

Symptoms:

  • gtag is not defined error
  • fbq is not defined error
  • No network requests to analytics domains

Diagnosis:

// Check if scripts loaded
console.log('gtag exists:', typeof window.gtag !== 'undefined')
console.log('fbq exists:', typeof window.fbq !== 'undefined')
console.log('dataLayer exists:', typeof window.dataLayer !== 'undefined')

// Check network tab for script requests
// Look for requests to:
// - googletagmanager.com
// - google-analytics.com
// - connect.facebook.net

Solutions:

A. Verify Script Tags

Check that scripts are present in HTML:

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        {/* Verify these scripts are present */}
        <script async src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`} />
        <script
          dangerouslySetInnerHTML={{
            __html: `
              window.dataLayer = window.dataLayer || [];
              function gtag(){dataLayer.push(arguments);}
              gtag('js', new Date());
              gtag('config', '${GA_ID}');
            `,
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

B. Check Environment Variables

// Verify environment variables are set
const GA_ID = process.env.NEXT_PUBLIC_GA_ID

if (!GA_ID) {
  console.error('GA_ID not found in environment variables')
}

console.log('GA_ID:', GA_ID?.substring(0, 5) + '...')  // Log partial ID

7. Duplicate Events

Problem: Events firing multiple times.

Symptoms:

  • Inflated metrics
  • Same event firing 2-3 times
  • Multiple analytics implementations

Diagnosis:

// Monitor all gtag calls
const originalGtag = window.gtag
window.gtag = function() {
  console.log('gtag call:', arguments)
  if (originalGtag) {
    return originalGtag.apply(window, arguments)
  }
}

// Monitor dataLayer pushes
const originalPush = window.dataLayer.push
window.dataLayer.push = function() {
  console.log('dataLayer push:', arguments[0])
  return originalPush.apply(window.dataLayer, arguments)
}

Solutions:

A. Prevent Multiple useEffect Calls

'use client'

import { useEffect, useRef } from 'react'

export function ContentTracker({ sanityDocument }) {
  const tracked = useRef(false)

  useEffect(() => {
    // Only track once
    if (tracked.current) return
    tracked.current = true

    if (window.gtag) {
      window.gtag('event', 'content_view', {
        content_id: sanityDocument._id,
      })
    }
  }, [sanityDocument._id])  // Only depend on ID

  return null
}

B. Check for Multiple Implementations

// Check if analytics already initialized
if (!window.analyticsInitialized) {
  window.analyticsInitialized = true

  // Initialize analytics
  window.gtag('config', 'G-XXXXXXXXXX')
}

Debugging Tools & Techniques

Browser Console Debugging

Check Analytics Objects:

// Check if analytics loaded
console.log({
  gtag: typeof window.gtag,
  fbq: typeof window.fbq,
  dataLayer: typeof window.dataLayer,
})

// View dataLayer
console.table(window.dataLayer)

// Monitor all events
window.dataLayer.push = function() {
  console.log('📊 Event:', arguments[0])
  return Array.prototype.push.apply(window.dataLayer, arguments)
}

Test Event Firing:

// Manually trigger event
if (window.gtag) {
  window.gtag('event', 'test_event', {
    test_param: 'test_value'
  })
  console.log('Test event sent')
}

// Check if it appears in Network tab

GTM Preview Mode

  1. Open GTM workspace
  2. Click Preview
  3. Enter your site URL
  4. Navigate and check:
    • Tags firing
    • Variables populating with Sanity data
    • Data layer updates
    • Triggers activating

Browser Extensions

GA4:

Meta Pixel:

GTM:

Data Layer:

Environment-Specific Issues

Development vs Production

Problem: Different behavior in dev vs prod.

Solutions:

// Use different analytics IDs
const GA_ID = process.env.NODE_ENV === 'production'
  ? process.env.NEXT_PUBLIC_GA_PRODUCTION_ID
  : process.env.NEXT_PUBLIC_GA_DEV_ID

// Enable debug mode in development
if (process.env.NODE_ENV === 'development' && window.gtag) {
  window.gtag('config', GA_ID, {
    debug_mode: true,
  })
}

// Log events in development
function trackEvent(event: string, params: any) {
  if (process.env.NODE_ENV === 'development') {
    console.log('📊 Track:', event, params)
  }

  if (window.gtag) {
    window.gtag('event', event, params)
  }
}

Build-Time vs Runtime Variables

Problem: Environment variables not available at runtime.

Solutions:

// Next.js: Use NEXT_PUBLIC_ prefix
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX  // Available at runtime

// Don't use
GA_ID=G-XXXXXXXXXX  // Only available at build time

// Check if variable exists
if (!process.env.NEXT_PUBLIC_GA_ID) {
  console.error('GA_ID not found')
}

Common Fixes Checklist

Quick checklist for most common issues:

  • Check window exists before using analytics
  • Use useEffect for client-side code
  • Track route changes in SPA
  • Initialize data layer before GTM
  • Exclude draft/preview Sanity documents
  • Wait for Sanity data before tracking
  • Verify environment variables are set
  • Check CSP headers allow analytics
  • Prevent duplicate useEffect calls
  • Test in incognito (no ad blockers)
  • Check browser console for errors
  • Verify scripts in network tab
  • Use GTM Preview mode
  • Test with browser extensions

Next Steps

For general tracking troubleshooting, see the global tracking issues guide.

// SYS.FOOTER