How to track Single Page Applications with GA4 and GTM?

Updated: Saturday, October 7, 2023
What is a Single Page Application (SPA)?
A Single Page Application is a type of web page that loads all the code required for navigation from the very first page load. As the name suggests, it’s an application that loads a single page. This type of application is very beneficial for the user experience, because once the first page is loaded, navigation is very fluid. It may look like the URL changes, but in fact you always stay on the same page.
SPAs are often created using the following JavaScript frameworks Angular, React and Vue.js.
However, this type of application is not suitable for SEO (Search Engine Optimization), because search engines have difficulties understanding the organization of the website, given that there is only one page.
Google Analytics 4 offers a default feature for tracking SPAs, but this may not work in your case. That’s why we’re going to look at 3 different methods in this article.
What is the History API in JavaScript?
Without going into the technical details, the idea here is to provide a clear understanding of how SPAs work.
The history object is a JavaScript global variable used by most SPAs to manipulate browsing history.

If you’re working on an older SPA, it may not be using the JavaScript History API, in this case you’ll need to install Google Analytics 4 via method 3.
3 methods to track SPAs
Before detailing the 3 methods, it’s important to understand what’s happening on a SPA. To do this, we’re going to use an instance of Angular in which I’ve installed the Google Tag Manager tracking code.
Here’s what my Angular application looks like. You can test the application here.

I create a History Change
trigger in Google Tag Manager to be able to view history change events in the data layer (this activates the history change listener).

Then, as soon as I navigate in the SPA, I receive History events (gtm.historyChange
) in the Google Tag Manager preview mode.
As mentioned above, GTM listens for calls to the JavaScript History API, which is why we get these gtm.historyChange
events.
![]() | ![]() |
Method 1: Keep GA4 with default settings
Now let’s see what happens if I set the Google Tag for a new GA4 property on all pages.
Now, I receive History events (gtm.historyChange-v2
) and History Change events (page_view
).
The gtm.historyChange-v2
event is triggered as many times as the gtm.historyChange
event because they listen to the same events (those from the History API).
However, gtm.historyChange-v2
is managed by GA4, whereas gtm.historyChange
is managed by GTM.
This is why gtm.historyChange-v2
triggers a page_view
event (named History Change in GTM).
This page_view
event tracks all pages viewed using the history API.
This feature is enabled by default in GA4 enhanced measurement, and can be disabled when necessary. If you wish to transmit more information in the page_view
event, you will need to use methods 2 or 3.

page_view
event handling on a GA4 propertyMethod 2: Track page views with History Change
trigger in GTM
To use this method, I disable page_view
tracking when changing the browsing history in GA4 enhanced measurement settings.

page_view
event from history changesNow, I only get the gtm.historyChange
event, as the gtm.historyChange-v2
event is no longer in the Data Layer.
I’m going to create a GA4 page_view
event in GTM that will be triggered on history changes.

page_view
event configuration from History Change triggerThen, you can change the page_location
and page_referrer
parameters to use the variables in the Data Layer. This will allow you to study the real user paths within your SPA.
Method 3: Track page views via Data Layer
Method 3 consists in asking a developer to send a page_view
event in the Data Layer when the user changes the view in the SPA.
Your developer probably doesn’t know anything about how Google Tag Manager and Google Analytics 4 work, so you will need to create a detailed data layer documentation explaning what you need to be implemented on the SPA.
dataLayer.push(
{
event: "page_view",
virtual_page_title: "Tour of Heroes - Dashboard",
virtual_page_location: "/dashboard",
virtual_page_referrer: "/heroes"
}
);

page_view
event configuration from the data layer