Single-page-app tracking gaps
In a single-page application, the browser loads once and the framework swaps views via the History API without a new document load. Analytics that depend on the load event therefore record only the first screen. This page explains the gaps — missing virtual pageviews, stale page paths, and broken referrer chains — and how SPA-aware tracking closes them.
Why SPAs break naive tracking
A multi-page site loads a new document per navigation, firing the analytics snippet each time. An SPA loads one document and then mutates the DOM and URL with the History API (pushState/replaceState) or hash changes. The page never reloads, so a load-only tag records one pageview for an entire session that may span dozens of views.
Closing the gaps
SPA-aware tracking sends a virtual pageview on each route change. GA4's enhanced measurement can capture history-based page changes automatically when 'page changes based on browser history events' is enabled; otherwise you send a manual page_view on the router's navigation hook.
Two follow-on bugs are common: the page_path may lag a render and report the previous URL, and the document.referrer stays the original external referrer for the whole session, so internal navigations can be misattributed unless the tag updates page location explicitly.
- Fire a virtual page_view on each pushState/replaceState/hash change
- GA4 enhanced measurement can capture history events when enabled
- Update page_location/page_path so it is not stale on the next view
- document.referrer does not change on in-app navigation
How it appears in analytics and logs
If an SPA shows almost no secondary pageviews, the tag likely fires only on initial load; route changes via pushState/replaceState are not being captured as virtual pageviews.
Diagnostic use case
Diagnose why an SPA reports far fewer pageviews than expected, or why every session looks like a single landing screen, by checking how route changes are instrumented.
What WebmasterID can help detect
WebmasterID records page_view events on client-side navigation when wired into the router, so SPA route changes appear as distinct pages rather than collapsing into the landing view.
Common mistakes
- Firing the tag only on initial document load in an SPA.
- Sending virtual pageviews before the route's URL has updated.
- Assuming enhanced measurement covers history events without enabling it.
Privacy and accuracy notes
Virtual pageview tracking observes route changes in the visitor's own session and needs no cross-site identifiers. This page is educational, not legal advice.
Related pages
- Double-counting pageviews
Double-counting happens when a single page load fires the analytics tag more than once. Two snippets on the page, a tag added in both the site and a tag manager, or an SPA that fires a virtual pageview on top of the full-load one all do it. The result inflates pageviews and drags engagement and bounce metrics. This page covers detection and the fixes.
- Redirect and referrer loss
The referrer tells analytics where a visit came from, but it is fragile. A redirect hop can replace the original referrer with the redirector's URL, and Referrer-Policy or HTTPS-to-HTTP downgrades can suppress it entirely. When the referrer is empty, the visit falls into direct; when it is the redirector's domain, it can look like a self-referral. This page explains referrer loss in transit.
- Pageviews: what the metric counts
A pageview is recorded when a page is loaded (or a virtual page is rendered in a single-page app). It is the oldest web-analytics metric and the easiest to misread: pageviews count loads, not people, and modern apps and prefetching can inflate or hide them. This page defines the metric and its caveats.
- Event tracking docs
Wire page_view events into your SPA router.
Sources and verification notes
- Google — [GA4] Enhanced measurement (page changes & history)
- MDN — History API (pushState/replaceState)
Last reviewed 2026-06-24. Facts are checked against primary/official sources where available; uncertain specifics are marked “Data not yet verified” rather than guessed.