Single Page Application (SPA) Tracking Issues
What This Means
Single Page Applications (SPAs) built with frameworks like React, Vue, Angular, or Next.js load content dynamically without full page refreshes. Traditional analytics implementations designed for multi-page websites don't capture route changes in SPAs, resulting in severely underreported page views, broken user journeys, and incomplete analytics data.
Common SPA Tracking Problems
Page Views Not Tracked:
- Only initial page load tracked
- Route changes not captured
- Virtual page views missing
- History pushState/replaceState not monitored
Analytics Configuration:
- Analytics loaded only once
- Configuration not updated on route change
- Tags firing on initial load only
- Missing page titles/paths
- Events attributed to wrong page
- User journey broken across routes
- Session context lost
- Duplicate events on route change
Data Layer Issues:
- Data layer not cleared between routes
- Variables not updated
- Old page context persists
- State management conflicts
Impact on Your Business
Analytics Accuracy:
- Massively underreported page views (90%+ missing)
- Incomplete user journeys (can't track multi-step flows)
- Wrong attribution (conversions credited to wrong pages)
- Broken funnels (steps appear skipped)
- Inaccurate engagement metrics (time on page, bounce rate)
Business Intelligence:
- Can't identify popular pages/features
- Funnel analysis broken
- A/B test results invalid
- User flow reports meaningless
- ROI calculations wrong
Marketing Impact:
- Can't optimize user journeys
- Wasted ad spend on unmeasured pages
- Poor remarketing audiences
- Lost conversion tracking
Business Consequences:
- Decisions based on incomplete data
- Missed optimization opportunities
- Wasted marketing budget
- Lost revenue from poor UX
How to Diagnose
Method 1: Navigate and Check Analytics
- Load your SPA
- Open browser console
- Navigate to different routes
- Check if analytics fires on each route change
Test:
// Listen for gtag calls
const originalGtag = window.gtag;
window.gtag = function(...args) {
console.log('gtag called:', args);
return originalGtag.apply(this, args);
};
// Navigate through your app
// Watch console for gtag calls
What to Look For:
- No gtag calls on route changes
- Page view only on initial load
- Events without updated page context
Method 2: Google Analytics DebugView
- Enable debug mode
- Navigate through your SPA
- Watch DebugView for page_view events
What to Look For:
- Single page_view on initial load
- No page_view events on route changes
- Missing page_title updates
- Incorrect page_location
Method 3: Google Tag Manager Preview
- Enable GTM Preview mode
- Navigate through SPA routes
- Watch for:
- History change events
- Data layer pushes
- Tag fires
What to Look For:
- No History Change trigger firing
- Data layer not updating
- Page view tags not firing on route change
Method 4: Network Tab Analysis
- Open DevTools Network tab
- Filter by "analytics" or "collect"
- Navigate through SPA
- Count requests
What to Look For:
- Only one analytics request on page load
- No subsequent requests on route changes
- URLs not updating in requests
Method 5: Check Real-Time Reports
What to Look For:
- Page doesn't change in real-time report
- Only initial page shown
- Users stuck on one page
General Fixes
Fix 1: Track Route Changes in React
React Router v6 example:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
useEffect(() => {
// Track page view on route change
gtag('config', 'G-XXXXXXXXXX', {
page_path: location.pathname + location.search,
page_title: document.title
});
}, [location]);
return (
// Your app content
);
}
React Router with custom hook:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function usePageTracking() {
const location = useLocation();
useEffect(() => {
// Send page view
gtag('config', 'G-XXXXXXXXXX', {
page_path: location.pathname,
page_title: document.title
});
// Or send as event
gtag('event', 'page_view', {
page_path: location.pathname,
page_title: document.title,
page_location: window.location.href
});
}, [location]);
}
// Use in your app
function App() {
usePageTracking();
return <div>{/* Your app */}</div>;
}
Fix 2: Track Route Changes in Vue
Vue Router:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
// Your routes
]
});
// Track page views on route change
router.afterEach((to, from) => {
// Update page title
document.title = to.meta.title || 'Default Title';
// Send page view to Google Analytics
gtag('config', 'G-XXXXXXXXXX', {
page_path: to.fullPath,
page_title: document.title
});
});
export default router;
Vue 3 Composition API:
import { watch } from 'vue';
import { useRoute } from 'vue-router';
export default {
setup() {
const route = useRoute();
watch(
() => route.fullPath,
(newPath) => {
gtag('config', 'G-XXXXXXXXXX', {
page_path: newPath,
page_title: route.meta.title || document.title
});
}
);
}
};
Fix 3: Track Route Changes in Angular
Angular Router:
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
declare let gtag: Function;
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
constructor(private router: Router) {}
ngOnInit() {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
gtag('config', 'G-XXXXXXXXXX', {
page_path: event.urlAfterRedirects,
page_title: this.getTitle()
});
});
}
getTitle(): string {
return document.title;
}
}
Fix 4: Use Google Tag Manager with History Change
Set up GTM for SPAs:
Enable Built-in Variables:
- History Source
- History Old URL Fragment
- History New URL Fragment
- History Change Source
Create History Change Trigger:
- Trigger Type: History Change
- Fire on: All History Changes
Update GA4 Tag:
- Add History Change trigger
- Update page_path variable
Data Layer push on route change:
// React Router example
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
useEffect(() => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'route_change',
page_path: location.pathname,
page_title: document.title,
page_location: window.location.href
});
}, [location]);
return <div>{/* App */}</div>;
}
Fix 5: Track Next.js Applications
Next.js with App Router:
// app/layout.js
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
export default function RootLayout({ children }) {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
const url = pathname + (searchParams.toString() ? `?${searchParams}` : '');
gtag('config', process.env.NEXT_PUBLIC_GA_ID, {
page_path: url,
page_title: document.title
});
}, [pathname, searchParams]);
return (
<html>
<body>{children}</body>
</html>
);
}
Next.js with Pages Router:
// pages/_app.js
import { useEffect } from 'react';
import { useRouter } from 'next/router';
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const handleRouteChange = (url) => {
gtag('config', process.env.NEXT_PUBLIC_GA_ID, {
page_path: url,
page_title: document.title
});
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
return <Component {...pageProps} />;
}
export default MyApp;
Fix 6: Handle Dynamic Page Titles
Update document title on route change:
// React with React Helmet
import { Helmet } from 'react-helmet-async';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function ProductPage({ product }) {
const location = useLocation();
useEffect(() => {
// Track after title is updated
setTimeout(() => {
gtag('config', 'G-XXXXXXXXXX', {
page_path: location.pathname,
page_title: document.title
});
}, 0);
}, [location]);
return (
<>
<Helmet>
<title>{product.name} - My Store</title>
</Helmet>
<div>{/* Product content */}</div>
</>
);
}
Fix 7: Clear Data Layer Between Routes
Prevent data pollution:
// Clear data layer on route change
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
useEffect(() => {
// Clear previous page data
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'route_change',
page_path: location.pathname,
page_title: document.title,
// Clear previous values
product_id: undefined,
product_name: undefined,
category: undefined,
// ... clear other page-specific variables
});
}, [location]);
return <div>{/* App */}</div>;
}
Platform-Specific Guides
Detailed implementation instructions for your specific platform:
Verification
After implementing SPA tracking:
Real-time testing:
- Open GA4 Real-time report
- Navigate through your SPA
- Verify page changes in real-time
- Check page titles update
Debug mode:
- Enable GA4 debug mode
- Navigate through routes
- Check DebugView for page_view events
- Verify page_path updates
GTM Preview:
- Enable GTM Preview
- Navigate routes
- Check History Change trigger fires
- Verify data layer updates
Network tab:
- Open DevTools Network
- Navigate routes
- Count analytics requests
- Should see one per route change
Historical data:
- Wait 24-48 hours
- Check page reports
- Verify all pages appear
- Check user flow reports
Common Mistakes
- Not tracking route changes - Only initial page view tracked
- Tracking too early - Before title updates
- Double tracking - Both automatic and manual tracking
- Not clearing data layer - Old data persists
- Missing page titles - Generic titles for all pages
- Ignoring query parameters - Different pages look identical
- Not testing - Assuming it works without verification
- Framework-specific issues - Not using framework hooks properly
- Race conditions - Tracking before page ready
- Not handling hash routing - Hash routes not tracked