Single-Page App Analytics Pitfalls
The Fundamental Mismatch
Most analytics tools were designed around the assumption that a 'page' is a full document load. Single-page applications break that assumption by design — the browser loads one document and then rewrites the DOM and URL (via the History API) for every subsequent 'page.' Every analytics pitfall in SPA tracking traces back to this one structural mismatch between how the tool expects to be triggered and how the application actually behaves.
Pitfall 1: Missing Virtual Pageviews
The most common and most damaging mistake: installing GA4/GTM once at app load and never firing anything on subsequent route changes. This produces a GA4 property that reports a flood of session_start events on the entry page and almost nothing afterward — bounce rate looks artificially terrible, and engagement metrics are meaningless because 90% of actual user activity after the first page is invisible to analytics entirely.
Pitfall 2: Session and Engagement Time Miscalculation
GA4 calculates engaged sessions partly based on engagement_time_msec, which the GA4 JavaScript SDK tracks automatically based on tab visibility and activity — but only if it has an accurate signal of page/screen boundaries. SPAs that don't fire explicit page_view-equivalent events on route change can cause GA4 to attribute all engagement time to the first virtual page, making every subsequent 'page' look like a zero-engagement bounce even though the user spent 10 minutes on the site.
// Standard pattern: disable GTM's automatic history-based pageview
// detection and fire explicit, controlled virtual pageviews instead
declare global {
interface Window {
dataLayer: any[];
}
}
function sendVirtualPageview(path: string) {
window.dataLayer.push({
event: "page_view",
page_location: window.location.origin + path,
page_path: path,
page_title: document.title,
});
}
// In GTM: configure the GA4 Configuration tag with
// "Send a page view event when this configuration loads" UNCHECKED,
// and instead trigger a dedicated GA4 Event tag (event name: page_view)
// on a Custom Event trigger matching the dataLayer event above.Double-Counted Pageviews Are Just as Common as Missing Ones
Pitfall 3: Stale Variable Values After Route Change
GTM variables that read from the DOM or a JavaScript variable at container load time (rather than re-reading at event time) can report the first page's data on every subsequent virtual pageview — a classic symptom is every page in a GA4 report showing the same page title or content group, even though users clearly navigated elsewhere. Use variables that re-evaluate the dataLayer at trigger time (DOM Element or Custom JavaScript variables reading live window.location) rather than caching stale references.
Pitfall 4: Attribution Resetting Mid-Session
Some SPA routing implementations (particularly ones using window.location.replace or query-string-heavy state management) can trigger a session timeout or UTM parameter re-evaluation mid-session if the URL structure resembles a brand-new landing page to GA4's session logic, splitting what should be one session into several and fragmenting attribution for a single user journey.
Scroll, Outbound Click, and File Download Enhanced Measurement Can Misfire Too
What's Next?
These pitfalls apply across any SPA framework, but React and Next.js specifically have well-established, idiomatic patterns for solving them cleanly — worth covering as a dedicated, framework-specific deep dive.
I build these systems professionally.
Whether it's a RAG pipeline, analytics migration, or AI workflow — let's talk.