13 KiB
Data Layer for Single-Page Applications (SPAs)
Source: https://developers.google.com/analytics/devguides/collection/ga4/single-page-applications Last Updated: 2025-01-09
Overview
Single-Page Applications (SPAs) are websites that load an HTML document once and fetch additional content using JavaScript APIs. Unlike traditional multi-page applications, SPAs require special handling for tracking page views and user interactions because the browser doesn't reload the page when users navigate between different screens.
The key challenge with SPAs is that traditional page view tracking relies on full page loads, but in SPAs, navigation happens dynamically without triggering a new page load event.
Why SPAs Require Special Treatment
Traditional vs SPA Page Loads
Traditional Website:
- User clicks a link
- Browser requests a new HTML document
- Page fully reloads
- Analytics tags fire automatically on load
Single-Page Application:
- User clicks a link
- JavaScript updates the DOM
- URL may change (via History API)
- No page reload occurs
- Analytics tags don't automatically fire
Key Measurement Goals
To accurately track SPAs, you need to:
- Count page views for each screen a user interacts with
- Track the page referrer correctly to trace the user journey
- Maintain proper event sequencing as users navigate
- Clear previous page data to avoid data carryover
- Update page-specific parameters for each virtual page view
Implementation Methods
Method 1: Browser History Changes (Recommended)
Use this method if your SPA uses the History API, specifically the pushState() and replaceState() methods to update screens.
How It Works
GTM's History Change trigger listens for:
- Changes to the URL fragment (hash)
- Calls to
history.pushState() - Calls to
history.replaceState() - Browser back/forward button clicks (
popstateevent)
GTM Setup
-
Enable Built-in History Variables in GTM:
- History Old URL Fragment
- History New URL Fragment
- History Old State
- History New State
- History Source (pushState, replaceState, popstate, or hashchange)
-
Create a History Change Trigger:
- Go to Triggers > New
- Choose "History Change" as trigger type
- Configure any additional filters if needed
- This trigger fires whenever the URL changes without a page reload
-
Create a Virtual Page View Tag:
- Create a GA4 Event tag
- Event Name:
page_view - Set to fire on the History Change trigger
- Configure page parameters (page_location, page_title, etc.)
Example Implementation
// Your SPA framework (e.g., React Router) handles navigation
// It uses pushState internally:
history.pushState({ page: 'products' }, 'Products', '/products');
// GTM's History Change trigger automatically detects this
// and fires your configured tags
Data Layer Push Pattern
When using History Change triggers, push updated page data to the data layer:
// Clear previous page data and push new page data
window.dataLayer.push({
event: 'virtual_pageview',
page_path: '/new-page',
page_title: 'New Page Title',
page_location: window.location.href,
// Clear previous page-scoped variables
previous_page_data: undefined,
transaction_id: undefined // Clear transaction-specific data
});
Method 2: Custom Events
Use this method if your website uses the DocumentFragment object to render different screens, or if you need more control over when virtual page views fire.
How It Works
You manually push custom events to the data layer when screen changes occur, rather than relying on automatic History API detection.
Implementation
// In your SPA's route change handler
function onRouteChange(newRoute) {
// Update the DOM with new content
updateContent(newRoute);
// Push virtual pageview to data layer
window.dataLayer.push({
event: 'virtual_pageview',
page_path: newRoute.path,
page_title: newRoute.title,
page_location: window.location.href
});
}
GTM Setup
-
Create a Custom Event Trigger:
- Go to Triggers > New
- Choose "Custom Event" as trigger type
- Event name:
virtual_pageview
-
Create Variables for Page Data:
- Data Layer Variable for
page_path - Data Layer Variable for
page_title - Data Layer Variable for
page_location
- Data Layer Variable for
-
Create a GA4 Event Tag:
- Event Name:
page_view - Add event parameters using the variables created above
- Set to fire on the Custom Event trigger
- Event Name:
Data Layer Clearing Best Practices
Why Clear Data
In SPAs, data persists in the data layer until explicitly cleared. This can cause:
- Transaction data appearing on non-transaction pages
- User data from one session bleeding into another
- Incorrect attribution of events to pages
What to Clear
Page-Scoped Data (clear on every virtual pageview):
page_titlepage_pathpage_category- Custom page dimensions
Event-Scoped Data (clear after the event fires):
transaction_idecommerceobjects- Form submission data
- Click data
Persistent Data (keep across page views):
user_iduser_type- Session-level dimensions
Clearing Pattern
// Pattern 1: Clear and set in separate pushes
dataLayer.push(function() {
this.reset(); // Clear all data layer state
});
dataLayer.push({
event: 'virtual_pageview',
page_title: 'New Page',
page_path: '/new-page'
});
// Pattern 2: Clear and set in one push (set to undefined)
dataLayer.push({
event: 'virtual_pageview',
// Clear previous values
transaction_id: undefined,
ecommerce: undefined,
form_id: undefined,
// Set new values
page_title: 'New Page',
page_path: '/new-page'
});
Framework-Specific Implementation
React with React Router
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function Analytics() {
const location = useLocation();
useEffect(() => {
// Clear previous page data
window.dataLayer.push({
event: 'virtual_pageview',
page_path: location.pathname,
page_title: document.title,
page_location: window.location.href,
// Clear event-scoped data
transaction_id: undefined,
ecommerce: undefined
});
}, [location]);
return null;
}
export default Analytics;
Vue.js with Vue Router
// In your main.js or router configuration
import router from './router';
router.afterEach((to, from) => {
window.dataLayer.push({
event: 'virtual_pageview',
page_path: to.path,
page_title: to.meta.title || document.title,
page_location: window.location.href,
// Clear event-scoped data
transaction_id: undefined,
ecommerce: undefined
});
});
Angular with Angular Router
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
export class AppComponent {
constructor(private router: Router) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
window.dataLayer.push({
event: 'virtual_pageview',
page_path: event.urlAfterRedirects,
page_title: document.title,
page_location: window.location.href,
// Clear event-scoped data
transaction_id: undefined,
ecommerce: undefined
});
});
}
}
Next.js
// In pages/_app.js
import { useEffect } from 'react';
import { useRouter } from 'next/router';
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const handleRouteChange = (url) => {
window.dataLayer.push({
event: 'virtual_pageview',
page_path: url,
page_title: document.title,
page_location: window.location.href,
// Clear event-scoped data
transaction_id: undefined,
ecommerce: undefined
});
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
return <Component {...pageProps} />;
}
export default MyApp;
Testing and Debugging SPAs
Preview Mode in GTM
- Enable Preview Mode in your GTM container
- Navigate through your SPA
- Watch for:
- History Change events firing
- Custom Event triggers activating
- Data layer state changes
- Tag firing sequence
Debug Console Checklist
For each virtual page view, verify:
page_viewevent firespage_locationupdates correctlypage_titlereflects the new screen- Previous page's
page_locationbecomes currentpage_referrer - Event-scoped data is cleared
- User/session data persists
DebugView in GA4
-
Enable Debug Mode in GTM tags:
// Add to your data layer push window.dataLayer.push({ event: 'virtual_pageview', debug_mode: true, // ... other parameters }); -
Navigate through your SPA
-
Check in GA4 DebugView:
- Each screen change should create a new
page_viewevent - Event parameters should update correctly
- Event sequence should match user journey
- Each screen change should create a new
Common SPA Pitfalls
1. Data Carryover
Problem: Transaction data appears on non-checkout pages
Solution:
// Always clear ecommerce data after it's sent
dataLayer.push({
event: 'purchase',
ecommerce: { /* purchase data */ }
});
// On next page view, clear it
dataLayer.push({
event: 'virtual_pageview',
ecommerce: undefined, // Explicitly clear
transaction_id: undefined
});
2. Missing Virtual Pageviews
Problem: Some navigation doesn't trigger page views
Solution: Ensure all route changes push to data layer, including:
- Back/forward button navigation
- Hash changes
- Modal/dialog state changes (if tracking as pages)
3. Duplicate Page Views
Problem: Both real and virtual page views fire
Solution:
// Block the initial page_view from GA4 Config tag
// Use a Pageview trigger with "Fire on DOM Ready" exception
// Or configure GA4 tag to not send page_view on initial load
4. Incorrect Referrer
Problem: page_referrer doesn't reflect previous virtual page
Solution: GTM automatically handles this when using History Change triggers. For custom events, ensure you're not manually setting page_referrer.
Advanced Patterns
Conditional Virtual Pageviews
Only track certain route changes:
function shouldTrackPageview(newRoute) {
// Don't track hash-only changes
if (newRoute.startsWith('#')) return false;
// Don't track query parameter-only changes
if (isSamePath(currentRoute, newRoute)) return false;
return true;
}
if (shouldTrackPageview(newRoute)) {
dataLayer.push({
event: 'virtual_pageview',
page_path: newRoute
});
}
Enhanced Virtual Pageview Data
Include additional context with virtual pageviews:
dataLayer.push({
event: 'virtual_pageview',
page_path: '/products/category/shoes',
page_title: 'Shoes - Products',
page_location: window.location.href,
// Additional context
page_category: 'Products',
page_subcategory: 'Shoes',
content_group: 'Product Listing',
user_journey_step: 'Browse',
spa_route_type: 'category_page'
});
Verification Checklist
Before deploying your SPA tracking:
- History Change trigger configured (if using History API)
- Custom Event trigger configured (if using custom events)
- Data Layer Variables created for page parameters
- GA4 Event tag configured with
page_viewevent - Initial page load tracked correctly
- All navigation types trigger virtual pageviews:
- Forward navigation
- Back button
- Direct URL changes
- Hash changes (if applicable)
- Data clearing implemented for:
- Transaction data
- Event-scoped variables
- Page-scoped variables
- Tested in Preview Mode
- Verified in GA4 DebugView
- Event sequence matches user journey
- Page referrer updates correctly