Single-Page Applications (SPA) become more and more popular. They are built using Ember, React, Angular, Backbone, Meteor or something similar.

Bad news about SPA is that adding default Universal Analytics tracking code to such applications will not result in having any valuable data in the Google Analytics account. The reason is that SPA does not generate any pageviews by defaults and pageview hits are not send to Universal Analytics. If no customization is applied to the analytics tracking code one will have sessions with only one pageview and extremely high bounce rate.

Good news is that pageview hits can be easily configured within Google Tag Manager container and send to analytics.

There are several ways to do it.

History Change trigger

Although page does not get reloaded while user navigates SPA the url and browser history get updated. So each piece of content still has its unique url. This is important for search engines since they want to index pages with unique URL addresses, which each return a static, unique piece of content.  

History Change trigger monitors changes in browser history and can be used to fire the tags. There are also some build-in variables in GTM connected with this trigger. These are:

  • New History Fragment – returns a string containing the new URL fragment after a page history change auto-event action is registered
  • Old History Fragment – returns a string containing the previous URL fragment
  • New History State – returns an object containing the new history state after a pushState() has been registered
  • Old History State – returns an object containing the old history state
  • History Source – returns a string describing the event that initiated the history change (e.g. popstate or pushState)

If you configure pageviews with the help of History Change trigger you will probably use New History Fragment variable for page field in your Universal Analytics tag. After publishing container with this tag pageview hit will be send to analytics each time the url is changed in your browser.

DataLayer event

History Change trigger is fast and easy to configure solution for SPA but it is not acceptable in some cases.

Some SPA applications do not need to be indexed or addressed to particular pieces of content by urls and therefore do not update browser history. So History Change trigger just will not work.

In other case you may want to scrape some data from DOM structure after the page was updated but DOM is not ready on browser history change so you will need to customize your tracking code further by adding timeout. This is not best practise and may cause you running into more issues with your tracking code.

The best solution in this case is to push an event to dataLayer after the content is updated. Here is a sample code snippet to do this:


Any extra information about the page may be pushed together with this event. If you configure tracking for a real estate agency it makes sense to push some data about the property viewed along with ‘content-update’ event (zip code, property name, address, city, type of building, size of building, etc.) All these variables can be catched in GTM and send to Google Analytics, Facebook, etc. or used for dynamic remarketing configuration.

Custom event trigger for “content-update” event should be configured and another Universal Analytics tag should send pageview hit when this trigger fires.

DOM MutationObserver

If none of the proposed solutions is acceptable for you there is still a chance to track pageview events in SPA using DOM MutationObserver. Some JavaScript coding will be required to implement this solution.

First of all you need to figure out any element in your DOM structure that gets updated together with page content. This may be breadcrumbs or page title block.

Then you need to create custom tag in your container that will push ‘content-update’ event each time the block is updated

var target = document.getElementById('some-id');//get the element that gets updated
var observer = new MutationObserver(function(mutations) {
 mutations.forEach(function(mutation) {
   dataLayer.push({ ‘event’:’content-update’ ’})
var config = { attributes: true, childList: true, characterData: true }; //configure the observer
observer.observe(target, config);//attach the observer

After this is done you need to configure trigger for custom event “content-update” same way as you would do in case it is pushed from the source code.

Note that this is the most complicated solution to configure and least stable. It will stop working correctly in case any layout updates are applied to your application. So consider other opportunities first before using it.

Happy SPA tracking!