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 definederrordocument is not definederror- 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 undefinederror- 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 definederrorfbq is not definederror- 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
- Open GTM workspace
- Click Preview
- Enter your site URL
- Navigate and check:
- Tags firing
- Variables populating with Sanity data
- Data layer updates
- Triggers activating
Browser Extensions
GA4:
- Google Analytics Debugger
- Enables GA debug mode
- Shows events in console
Meta Pixel:
- Meta Pixel Helper
- Shows pixel events
- Displays parameters
GTM:
- Tag Assistant Legacy
- Shows tags firing
- Validates setup
Data Layer:
- dataLayer Inspector
- Real-time data layer view
- Shows all pushes
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
windowexists before using analytics - Use
useEffectfor 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.