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
Additional Resources
- Single-Page Application Tracking - Google
- React Router Documentation
- Vue Router Documentation
- Angular Router Documentation
- Next.js Analytics
- Tracking Issues Overview