How to measure SPA traffic with Google Tag Manager and Google Analytics

Ramón Saquete

Written by Ramón Saquete

Nowadays it’s getting increasingly common to run into websites designed using full-stack frameworks, such as Angular, React or Vue. These technologies facilitate the implementation of Single-Page Application (SPA) websites, on which, by default, tracking scripts only work when users arrive on the website from an external source, while remaining ignorant of any browsing happening within the page. In this post we are going to explore various possible solutions to this problem.

On an SPA website, as opposed to a classic Multiple Page Application (MPA), JavaScript code included on the pages isn’t executed each time the user visits a new URL. That is, it only runs once, when the page is loaded for the first time on a user’s browser.

Once the user is already on the website, the JavaScript doesn’t execute again while they’re browsing, unless there’s something in the code forcing it to do it. This is obviously an issue for tracking tools based on JavaScript, as their code is only executed once, during the initial download.

Not everyone realises that most page views do not get recorded on Google Analytics when the website is an SPA 😮Click To Tweet

Let’s see how we can solve this using two possible ways:

  1. By setting up Google Tag Manager.
  2. Without using tag manager, by adding additional JavaScript code.

Regardless of the chosen technique, we will have to capture –whether directly or indirectly– calls to the history.pushstate method, responsible for URL changes via JavaScript on an SPA.

So let’s look at how we can apply each solution to Google Analytics. In theory, the following explanation can be applied to other tracking scripts, too. However, keep in mind that each particular case might require something else to perform correctly.

Tracking an SPA using Google Tag Manager (GTM)

In the following example, we’re setting out from a scenario, where we have already created a Google Universal Analytics account, and installed a GTM container per Google’s instructions.

First, we need to create a trigger that will fire when it detects a page change on the SPA. This can happen in three situations:

  1. When the user clicks a link that results in a content change and JavaScript URL change.
  2. When the user navigates through history by using the browser’s buttons.
  3. When the user clicks on a button that results in a position change within history through programming.

The trigger type responsible for this is “History Change“, which we will configure the following way:

GTM trigger history changes
Google Tag Manager SPA trigger

We’ve named the trigger “SPA tracker” and chosen the option for it to fire on all history events. We hit “Save”, and then we need to add the Google Analytics identifier to a constant if we haven’t done it yet. This way, it will be available whenever we set up any tag.

Google Analytics ID constant variable
Constant declaration in GTM

Each time a history event is triggered, the Google Tag Manager built-in variable will get updated. It will give us the URL path of where we are, which we will see configured as “Page Path”:

Page Path built-in variable in GTM
GTM built-in variables

Now we’re going to create a “Google Analytics settings” variable, where we will assign the built-in variable “Page Path” to the “page” field. Then, we will assign the tracking ID to the previously declared constant:

Google Analytics configuration variable
GTM configuration variable, assigning the existing “Page Path” to the “page” field.

Save the variable as “Analytics SPA configuration”. Now we’re ready to create a “Google Analytics” tag type, to which we will assign the configuration variable we’ve created in the previous step, and the “SPA tracker” trigger.

SPA tracking tag for Google Analytics in GTM
Google Analytics SPA tracking tag in GTM

You can see that we’ve added the “All Pages” trigger as well, so that it also fires when we enter a page. If we already had previously set up another Google Analytics tag with the same tracking ID and “All Pages” trigger, we can either delete it, or leave it there, but remove the “All Pages” trigger from the new tracking tag. If we choose the latter, we can have a different configuration for MPA and SPA pages on our site, while if we set it up as shown on the screenshot, the configuration will be unified.

Finally, let’s give our tag a name. For example “Tracking analytics SPA”, and hit the “Preview” button (in the top-right corner in GTM). Make sure it works correctly on your website from the GTM console. Here, we will see that the tag is fired on each “Page View” event, thanks to the “All Pages” trigger, and each time we click on an SPA link, thanks to the “SPA tracker” trigger for “All history changes” we created.

Tags fired GTM
The GTM console shows us that a “Page View” event and two “History” ones fired, because we went on the SPA and visited two pages, which made our tag run three times.

If we wanted to transfer more information to Google Analytics, we would have to add additional variables to data created in the “Data Layer”, where the developer can add the necessary information. “Data Layer” could already have interesting information inside, which we can check by examining the gtm.newHistoryState object, where with each history change an object is stored, and is passed by the developer to the history.pushstate method, to see the state of the application and the viewed page.

Most of the time, we will only find the viewed page ID in gtm.newHistoryState, but sometimes it can contain interesting information.

Once the configuration has been finished and tested, we can submit the changes and publish the new version.

Tracking an SPA with additional JavaScript

Even though GTM is a fantastic tool to separate the tracking script installation from the website’s code, there are times when it’s better not to use it. For example, if we only intend to use Google Analytics, by installing its tracking ID directly, we are saving ourselves from loading a tag manager, which will be only used to load one tag.

Moreover, an analytics expert wouldn’t have to configure a tag manager. If, on top of that, the website has been built with a CMS, it probably has a plugin to easily install Google Analytics, without requiring the help of a developer. Sometimes, not using GTM turns out to be the easier path.

Another issue presented by GTM, is that to do certain things you need some elementary coding skills, besides being familiar with the tool itself. Usually, those who delve further into learning how to use this tool are analytics experts, who don’t have any coding knowledge, while developers usually don’t know how to use analytics tools. For that reason, we often choose to keep each professional’s tasks separate: the analytics experts send the developers a request to install the code with a link to the required documentation, and perhaps some code provided by the tool in question.

If we’re using a plugin, and it’s already prepared to track SPA pages, our problem is solved. If that’s not the case, we’ll have to let go of the plugin and ask a developer to help us out.

In the following sections, we will see how a developer should install Google Analytics so that it works with SPAs. This task requires knowing how to code in JavaScript.

Implementation without using additional libraries

Basically, what you need to do, is to execute Google Analytics code generating a new pageview with each call to the history.pushstate method. According to Google’s documentation for SPAs, the following function would the correct:

   function recordVisitToCurrentPage(){
       if (typeof(ga)!=="undefined"){
          ga('set', 'page', window.location.pathname);
          ga('send', 'pageview');

Inside the “recordVisitToCurrentPage()” function we verify that the “ga” object enabling us to make calls to Google Analytics has been loaded, and if it has, we let it know that the current page has been visited. Here we could keep any code to record visits from any tracking script type.

Our next challenge is to call this function each time history.pushstate is called. Presently, there is not a single native event that allows us to detect when a call to history.pushstate takes place. There is a onpopstate event, which we want to keep in mind, but it doesn’t get triggered with calls to history.pushstate, only with changes in history caused by a user or the programming, with calls to history.back(), history.forward() or history.go(-1).

One possible solution to this would be to add the call to function “recordVisittoCurrentPage()” in all places in our code where there is a call to history.pushstate. This, however, can be tedious, take too much time, or it can even be impossible if these calls are obfuscated inside a framework’s router core.

The recommendation in these cases to make the implementation as viable and clean as possible is to apply a hack overwriting the native history.pushstate method as shown below:

window.onpopstate = history.onpushstate = function(e) { 
           //here we can pass e.state to our function 
           //if we want to record some other information of the current state,
           //like page type
            //we store the original native method
            var pushState = history.pushState;  
            //we overwrite the native method
            history.pushState = function(state) {  
                //we call the original native method we previously stored
                pushState.apply(history, arguments);
                //the invented onpushstate event is executed
                //after calling the pushstate method
                if (typeof history.onpushstate == "function") {  
                   history.onpushstate({state: state});  

Evidently, we have to load the Google Analytics library with the correct ID beforehand, so that our code records the visits.

Implementation using Google’s autotrack library

Another way to implement this is with Google’s autotrack library, which uses the same hack but with some additional code, to include Google Analytics and track visits to the SPA.

Using this library, we only should use the following code with the correct Google Analytics ID:||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-XXXXX-Y', 'auto');
ga('require', 'urlChangeTracker');


Due to the absence of the onpushstate event, SPA tracking code implementation becomes unnecessarily complicated. WhatWG should add this method to its specs, and browsers should implement it too.

In the meantime, all library and Google Tag Manager implementations are based on the hack I’ve just explained in this post. This isn’t how it should be, because any browser update could render this hack obsolete at any given time. Fortunately for us, though, it’s been working for several years now, and it doesn’t look like it’s going away in the short term.

Ramón Saquete
Autor: Ramón Saquete
Web developer at Human Level Communications online marketing agency. He's an expert in WPO, PHP development and MySQL databases.

Leave a comment

Your email address will not be published. Required fields are marked *