🚨 Note: All standard Universal Analytics properties will stop processing new hits on July 1, 2023. 360 Universal Analytics properties will stop processing new hits on October 1, 2023. That’s why it’s recommended to start using Google Analytics 4. We’ve also created a post on how to track single page applications with GA4 and GTM
Single Page Applications (SPAs) are built differently than regular websites, which causes some difficulties in tracking them correctly with Google Analytics.
Fortunately, it is possible to solve these issues with the help of Google Tag Manager.

Sign up to the FREE GTM for Beginners Course...
In this article, you will learn how to track Single Page Applications with the help of GTM.
💡 Top Tip: The best practice is to try out new strategies on your demo website before implementing them live on your website.
Links:
The Rogue Referral Problem (Simo Ahava)
Track Single Page Application (E-Nor)
What are Single Page Applications (SPAs)?
Single Page Applications (SPAs) are web applications that load a single HTML page and dynamically update the content as the user interacts with the application, without requiring a full page refresh.
This allows for a more seamless and responsive user experience.
Single Page Applications often look like traditional websites, but they work very differently. In order to achieve a super-fast performance, SPAs load all of the HTML, CSS, and JavaScript resources required to navigate the site with the initial browser request.
After the initial load is completed and the user starts navigating on the site, SPAs only update the required content within the established page, instead of loading complete new pages every time.
This makes them fast and responsive. Almost like a native App you’d see on iOS or Android phones.
To achieve the desired effect SPAs utilize modern JavaScript Frameworks such as Angular, React or Vue.
They are still fairly new, so you wouldn’t find them in combination with popular CMS such as Wordpress, Magento, Shopify or Joomla (although there are exceptions).
If you encounter an SPA it was most likely built from scratch by a developer or a team of developers.
This means SPAs are often not as “standard” as you might be used to and therefore Tracking also needs to be customized to work with these new types of websites.
Why is it hard to Track Single Page Applications?
On traditional websites, Google Analytics and other tracking tools work by executing their code snippets every time a new page is loaded by the browser. Single Page Applications, however, don’t load new pages via a full server roundtrip, they swap out the content of the initially established page and only load the new content parts. You can observe this when navigating on an SPA in the browser tab: there is no loading animation when you click from one page of an SPA to the next.

SPAs lack the browser events that analytics software like Google Analytics rely on – if you track an SPA, you will likely only see one single page view per session, as only one roundtrip to the server is done. Google Tag Manager also relies on entire pages being loaded, so there are similar issues when using GTM on an SPA.
Two Methods to Track Single Page Applications
Despite these challenges, there are several ways to properly track Single Page Applications from the web. This article will show you two ways how to do this: With the use of the history trigger, which is the easier option, and by adding a custom data layer.
History Change Trigger
When you click through an SPA with the GTM preview mode opened, you will see there’s no reload happening and no new events are shown. By the way, this is also a great way to test whether you are really dealing with a web SPA.
The first way to solve the tracking challenge is via the history listener API. As you may know, GTM allows us to access a vast number of browser APIs, like detecting a click or hovering over an element. The history API is one of the data sources we can listen to via GTM.
Provided that the developer of the SPA has implemented the browser history functionality, we can hook into the browser history with GTM and see different entries for each SPA page that was visited by the user.
To make use of this browser history information, we need to employ the corresponding trigger in GTM. It is called the History Change listener. You find it by clicking on Triggers → New and under Other you will find the History Change trigger.
Simply give this trigger a name and save it, then go check in the GTM preview pane whether this tracking works. Under summary, you should see a new History event whenever you click on a link of your SPA.

It’s worth noting that the history trigger doesn’t actually listen to a click. It only checks if anything was added or removed from the browser history API. Note that your SPA needs to make use of the history functionality of the browser. If that isn’t the case, this solution won’t work and you can move on to the Custom Data Layer section.
Let’s assume we’re dealing with a web site that enables us to use the history trigger in the desired way: What’s the next step we need to take in order to track our SPA correctly?
First, check one of the history events in the summary tab and click on the Variables tab. You should be able to see that the Page Path URL is changing with every single history event.

Next, we want to add some auto variables that are provided by the history change trigger. To do this, go to Variables → Configure and activate all checkboxes under History. Once enabled, you will see those history variables in the Variables tab in the GTM preview pane.

It depends on the architecture of your web SPA whether or not these history variables are useful for tracking. Any of the variables need to have been addressed by the developer of your web SPA.
In order to use the variables for our page view tracking, we need to head over to the Tags section in GTM. Create a new tag and and name it accordingly, i.e. “GA – Pageview – all pages”. Choose Google Analytics as the tag type and Page View as the track type. Make sure you’ve entered the Google Analytics tracking code in the Google Analytics Settings.
Just copy the tracking code over from your Google Analytics account and enter it by clicking Select Settings Variable → New Variable… and then pasting it into the Tracking ID field. Hit Save and then choose the History Change trigger as the trigger for your tag. To make sure that your new tag fires on all pages, you should also add the All Pages trigger to your tag.

Save your tag and refresh the preview console and click a few links on your web SPA. You should now see a new tag being fired every time there is a new page view or browser history event. This means that each time a tag is fired, the corresponding information will be sent over to Google Analytics.
You can check this by looking at the Google Analytics Debugger. Click again on a few links of your web site and see if you can find hit Type page view and a URL under location when looking at the browser console.

Alternatively, you can also check the GA Real-time reporting to see whether the information is passed on from GTM to Google Analytics. And presto, we’re now properly tracking all page views on our SPA website. Of course, don’t forget to submit the GTM changes you’ve made, so they get published to your website.
Custom Data Layer
As you saw, the history trigger makes it relatively easy to track Single Page Applications – but your web SPA needs to make use of the browser history API for this to work. What can you do if this isn’t the case? Don’t worry, there’s a solution for those cases as well – using the custom data layer push.
What is the custom data layer push? As you may know, GTM has a data layer, which is GTMs central repository of structured data. You can push data into this data layer with the help of some JavaScript. And this is what we will do for this second way of how to track Single Page Applications website traffic.
For the custom data layer push, you will need the help of a developer, unless you have coding skills yourself. Let’s assume you have access to a developer who can help you with this task.
What you want to add is a JavaScript code that looks like this
CODE:
dataLayer.push(❴’event’: “pageview”❵)
This snippet should be deployed with each new view visible to the user (what we would consider a pageview normally). Ideally, the developer should add more data, such as the hostname, the path, the URL (highly recommended), the query path and the page title (optional).
We’ll assume that the developer has now correctly implemented this JavaScript on your web SPA and move on with our tracking in GTM. In the preview pane, when you click on one of the pageview events in the Summary tab and then go to Data Layer, you should be able to see all the information that was implemented via the newJavaScript snippet that the developer inserted for you.

If this all works as intended, you can now use this Custom Data Layer to fire your tags. To do this, head over to GTM and Triggers → New, and give it a name, such as “Custom – pageview”. The Trigger Type we choose is Custom Event, and the Event name to fire the tag is “pageview”. That’s it, you can save your new custom trigger and connect it to a tag.

Go to Tags → New, name it “GA – Pageview – pageview”, choose Google Analytics as the Tag Type and Page View as the Track Type, select your GA tracking ID and as a trigger select the new custom pageview trigger we just created.

Do you need the All Pages Trigger here as well, as in our history listener tracking? Well, this depends on how your web SPA is built. If you notice that there’s an explicit page view event whenever you click through your web site, that means it’s not needed to add All Pages as an additional trigger.
As before, save your new tag and refresh your page. Click on some of the links and make sure the new tag is firing correctly each time you go to another page of your SPA website. If this all works as intended and you’ve verified the tracking in GA, go back to GTM and submit your changes.
Common Issues with Single Page Applications Tracking
While this process of implementing the tracking for web SPAs was fairly straightforward, there are some issues you can run into when you are trying to track your traffic.
URL issues
One of these issues is URL problems. URL problems can occur when the URL structure of your website is using anchor links (the hash symbol which is used to jump to specific parts of a page on traditional websites). Google Analytics will not track the part of the URL that follows the hash symbol – which, depending on your URL structure could mean that all tracked URLs look the same, even though users are effectively visiting different pages. Unfortunately, there are quite a few SPA websites that are using anchor links for their page URLs, so this is a common issue.
Assuming we have such a website and GA by default ignores any part of a URL that is following a hash sign, how can we still properly track the web SPA?
This is where the history auto event variables come in, which we added in the history trigger chapter, but didn’t use. The auto variables Old History Fragment and New History Fragment will help us here (of course, the developer needs to have chosen to populate the browser history API with this information). Old History Fragment will show us the previous page that was visited and the New History Fragment will show the one we are visiting now.
To add the missing URL information, we edit our existing “GA – Pageview – pageview” tag. First, activate the “Enable overriding settings in this tag” checkbox, click on More Settings → Fields to Set, + Add Field with the field name “page” and as a value we choose the “New History Fragment” variable. This variable value will be added to the data that gets sent by your tag over to GA and will then be recognized as part of the URL that is being visited.

Save your changes and go back to Google Analytics, which should now show the full URL path of your SPA website and not only the part until the anchor link.
If the developer of your site chose not to use the history trigger, you can revert to the Custom data layer solution. That’s why it was important to not just add the pageview event but some more information, such as the hostname, path and URL of the pages on your website.
If the developer has added the path as part of the Custom Data layer to your website, you can simply create a new Variable in GTM by going to Variables → New, naming the new variable (we choose “div – page”) , selecting “Data Layer Variable” as the Variable Type, “path” as the DataLayer Variable Name and select this newly created Variable in your “GA – Pageview – pageview” tag in the “Fields to Set” section as the new value instead of New History Fragment.


As you can see, when you are facing URL issues with your SPA, there are ways to transfer the missing parts of a URL over to Google Analytics, but the options you have depend on the setup of your web SPA. In case you are working with Custom Data Layers, make sure that your developer implements information such as URL path right away – it could save yourself valuable time afterwards
Rogue Referral Problem
Another issue that can arise when you track Single Page Applications is the so-called Rogue Referral Problem. This issue, and how to fix it, was first reported by Simo Ahava. It describes a wrong attribution of Google Ads traffic as organic traffic in Google Analytics. Detecting this issue isn’t easy. To do this, you need to go to the User Explorer of GA and look for users that have Google CPC as the original referral channel on the first page of their visit, but were attributed as organic traffic with their next and all following actions, resulting in their entire session being rewritten as organic.
What happens is that Google Analytics strips some of the original information once the user hits your web SPA. During the following page views, GA is looking at the user’s previous pages and “forgets” that they originally came from one of your ads, wrongly attributing the user session as organic.
How can you tackle this problem? The recommended fix by Simo Ahava is to insert the following code in the page HTML before the GTM container:
CODE:
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
originalLocation: document.location.protocol + '//' +
document.location.hostname +
document.location.pathname +
document.location.search
});
However, in case you don’t want or you can’t edit your HTML directly, you can also add this code via a custom HTML tag. Copy the code above, head over to GTM and go to Tags → New, name the new tag and choose custom HTML as Tag Type and paste the code into the HTML field, enclosed by <script> tags.

This will push the URL with all parameters into the data layer. It will persist there as long as the page or GTM don’t reload. Therefore the information about the original user source cannot get lost.
This tag then needs to be fired once per page and on all pages of our site, but it has to be fired before any other tag gets fired. We can establish this by going back to our “GA – Pageview – pageview” tag, scrolling to the Advanced Settings and choosing Tag Sequencing → Fire a tag before GA – Pageview – pageview fires, selecting our custom HTML tag and hit save.

To make sure this worked, open the GTM preview pane and select the Data Layer tab. You should see a new Layer called {originalLocation:} that delivers the URL of the page as its value. This new Data Layer can now be pulled into a new variable.
In GTM, go to Variables → New, name it “div – originalLocation” and select DataLayer Variable as Variable Type and “originalLocation” as the Data Layer Variable Name. Save and check it in the GTM preview. You should now see the new “div – original Location” tag being fired in the Variables tab, with the URL of the original location of the user as the value.

However, there is one caveat to the solution of inserting the custom HTML via GTM and not directly into the HTML: It is possible that the originalLocation variable is pushed into the dataLayer at the same time as the tags are trying to retrieve it. This results in a missing Document location fied! To prevent this from happening, copy the following custom JavaScript code.
CODE:
function() {
return {{Data Layer Variable - originalLocation}} || window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.search;
}
Go to GTM Variables → New, name it, choose Custom JavaScript as the VariableType and paste the above code into the Custom JavaScript field. Make sure that you change the reference from “Data Layer Variable – originalLocation” to the name of your Custom HTML Variable (“div – originalLocation” in our case).

Double check in the GTM preview pane under Variables that the customJavaScript is showing up. Next, we need to add this location to our “GA – Pageview – pageviews” tag, so edit this tag by adding a Field to Set with the Field Name “location” and the value of our newly created “cjs – originalLocation” variable. Save your edits and try if it works on your SPA.

You can simulate a visit from Google ads to your site by adding “?gclid=” to the browser address, followed by a random number. Confirm that both the custom HTML and the GA – Pageview – pageview tags get fired. If everything is set correctly, the session attribution of a user won’t change, even when the user is navigating on the page, since the location variable won’t drop the “gclid=” suffix.
Keep in mind: this is only an issue when you are running Google Ads. If you don’t, you can safely ignore this rather complex fix for the rogue referral issue.
Summary
When you start to track Single Page Applications correctly, it may seem difficult. However, there are several ways to accomplish this with the help of GTM, either via the History Trigger, which is fairly straightforward, or, if this isn’t possible, by adding Custom Layer Variables to the site with the help of a developer. Users should take a closer look at their analytics to make sure they aren’t experiencing any URL issues, which can be caused by anchor links. Anchor links are a common way to mimic the standard page navigation in the browser address field and can lead to page path information missing in Google Analytics. Fortunately, the stripped URL path information can be recovered by applying the New History Fragment Variables or by making use of the custom data layer.
Another potential issue when you track Single Page Applications is the Rogue Referrer problem, which can lead to wrongly attributed Google Ad traffic. Fixing this issue requires a bit of tweaking in GTM by adding additional custom HTML and custom JavaScript variables, but with the instructions in this blog, you should be able to master this as well.
🚨 Note: Another useful thing to track is the conversion value which helps you measure the financial impact of your marketing campaigns.
Keep in mind that the rouge Referrer problem can only occur with Google Ads traffic, so you can ignore this issue if you aren’t running any Google Ad campaigns. In case you are already using Google Ads, you may wonder how to properly connect Google Ads to Google analytics. Or perhaps you’re already using Google Ads and Google Analytics, but you don’t know which of the programs you should use to track your conversions? Then our video about both conversion tracking methods will help you.

Sign up to the FREE GTM for Beginners Course...
Hi Julian,
I have a question, how we can use GTM “timer” functionality using “history” event.
Thank you
you would need to build you own custom timer trigger as described here https://measureschool.com/delayed-timer-google-tag-manager/
Hi, Julian! I have the same situation, but there is a some problem. When i set custom timer 10 seconds on “history” event, it fires even when user is on the page less than 10 seconds and goes further, to next steps / pages. Here I need to have a normal story with default GTM timer but i can’t do it. The page doesn’t reload. Is there a decision how can I resolve that?
hi Julian, thanks for GTM knowledge regarding SPA. I am trying to track downloads in my SPA application with google tag. Any pointers how to do it?
not very different from the other methods that are out there. Either a custom dataLayer push or a Click Trigger
Hey, I’m having a similar issue I believe. When AJAX-loaded content is added to the page (like a download link) clicking on that link does not trigger a linkClick event in GTM. It looks like GTM doesn’t know about the newly added download link element since it wasn’t there on page load. Should it trigger a an event or am I doing something else wrong? Thank you!
you would need to use a different trigger in this case. Possibly build your own custom one
Hi Julian! I needed to create a few JavaScript Variables to collect data from the site code. The JavaScript Variables are not “refreshing” as I continue through the SAP – any ideas?
Hi Julian, Thank you so much for your awesome guide. I was looking for how to solve the Rogue Referral Problem for couple of weeks but couldn’t find anywhere give me step by step instructions like this. The implementation is working so far but I got stuck at this step “Make sure that you change the reference from “Data Layer Variable – originalLocation” to the name of your Custom HTML Variable (“div – originalLocation” in our case). “ I change the “Data Layer Variable – originalLocation” to “div – originalLocation”However, GTM kept saying that div – originalLocation is found in anotehr variable.… Read more »
sorry, there was an error in our post. it needs to be dlv – originalLocation
Hi, I have a question about tracking SPA with the new GTM Preview mode. It doesn’t collect information across pages. Do you know if there is a workaround? It’s much more difficult to track things now. Thank you!
I don’t understand – you mean going from one domain to another? Yes, there are still issues with the Preview Mode
Julian, do you think rough url problem can affect google ads conversions too ( not the ones imported from analyttics) ? According to me it should not, if we are using “first lick attribution model”.As even though gclid is not passed so second click, ideally it should consider 1st and second click as same user and attribute the conversion to first click”
no, it should not. The GAds conversion tracking works differently than Google Analytics
Hi Julian, first of all, thank you for the information. It really helps! I am running into an issue after I follow the workaround and trying to get the Rogue Referral Problem solved. Somehow the setup only works on the first page jump. When I monitor the traffic source in real time, the medium switch back to Organic after the second page visit. Landing – CPC First Page – CPC Second Page – Organic I checked the setup multiple time and can’t spot anything different than yours. I wonder if you can think something that’s related in your past experience.… Read more »
what do you mean by second page visit? Should that be a one page website?
Hi Julian,
Thanks for your awesome guide, When setup GTM according to guideline I have 2 questions
Thank you
Hi. I have a question about this part: “If everything is set correctly, the session attribution of a user won’t change, even when the user is navigating on the page, since the location variable won’t drop the “gclid=” suffix.”
I did everything as described. But I didn’t understand if attribution works correctly. When I test with “gclid =, the datalayer shows everything correctly, but when I go to another page, gclid = is lost. Is this normal or not?
Hi Julian,
Very helpful guide, thank you! The issue we are experiencing is that when we navigate from one page to the next within a single page application the URL is captured correctly by Universal Analytics but the page title is actually the page I came from and not the page I landed on.
Can you please suggest how we might solve this problem?
Thank you,
Beata
Hi! i have a problem with UTM hat does not appear in the analitycs. lo hice asi: store.domain.com/?utm_source=XXXk&utm_medium=XXX&utm_id=XXX#products the origial url is store.domain.com/#/products
could you help me?
Hi Julian,
Really nice article.
I noticed that the single-page-application tag doesn’t fire when I click on an internal link that has parameters (?utm_source=…). The links are internal, meaning that they have our webshop’s stucture before the parameters. Do you have any idea why this happens?
Can I use Google anylytics tracking code for a single page of any website?
Hey Evan, yes, it is possible, but it is recommended to have Analytics implemented on each page of a website.
Hi,
I would like to track some simple links on my SPA but when clicking then GTM does not recognize them as a click (click variable is enabled). Is there any best practice available?