Quick answer: In a single page application, the browser loads one page and then changes the content with JavaScript, so the standard Google Analytics snippet records only the first page view. To track properly, send a virtual page view each time the view changes: update the page path with ga('set', 'page', ...) and then call ga('send', 'pageview'). With Google Tag Manager, use a History Change trigger or push a custom event to the data layer. Then check that referrers, titles, goals and campaign data still come through correctly.
More and more web apps, dashboards and even marketing sites are built as single page applications using frameworks like AngularJS, Ember, Backbone or React. They feel fast because the page does not fully reload when you click around. That same feature confuses analytics. Many teams launch a new app, look at Google Analytics a week later, and find a single page with a huge bounce rate and almost no page views per session. The data is not wrong about what the snippet sees; the snippet just is not seeing the user move around.
Why standard tracking fails
The usual Universal Analytics snippet runs when a page loads. It creates a tracker and sends one page view with the current URL and title. On a traditional site, every click loads a new page, the snippet runs again, and a new page view is recorded.
In a single page application:
- The HTML page loads once.
- Clicking a link changes the URL (with the History API or a hash fragment) and swaps the content, but the page does not reload.
- The snippet does not run again, so no new page view is sent.
The result is inflated bounce rates, very short session durations, missing content reports and goals that never fire if they depend on reaching a particular URL.
Plan what to track first
Before writing code, decide what counts as a page and what counts as an event.
- Virtual page views suit changes that a user would think of as a new screen: moving from the product list to a product, from step one of a sign up to step two, or from the dashboard to settings.
- Events suit actions within a screen: opening a tab, playing a video, filtering a list, adding to cart, submitting a form.
Write these down with the path or event names you will use. A consistent naming plan makes reports far easier to read later.
Tracking virtual page views with Universal Analytics
The basic pattern
Each time the route changes, update the tracker’s page field, then send a page view:
ga('set', 'page', '/products/blue-widget');
ga('set', 'title', 'Blue Widget');
ga('send', 'pageview');
Using set rather than passing the path only in the send call matters. Once set, the page value is attached to any events sent afterwards, so events are reported against the right screen.
Where to put it
Most frameworks have a hook that runs whenever the route changes. In AngularJS, that is the $routeChangeSuccess or $stateChangeSuccess event, depending on your router. Backbone and Ember routers have similar hooks. Put the tracking call there, so every route change is covered without developers needing to remember it on each screen.
Community modules exist for most frameworks, such as Angulartics for AngularJS, which handle this for you. Check that they use the set then send pattern.
Hash based URLs
If your app uses URLs with a hash, such as /#/products, Google Analytics ignores everything after the hash by default. Send the full route as the page path yourself, for example by building it from location.pathname plus location.hash, with the hash character removed.
Tracking with Google Tag Manager
If you use Google Tag Manager, there are two good approaches.
The History Change trigger
Tag Manager can detect changes made through the History API. Create a trigger of type History Change, and fire a Universal Analytics page view tag on it. In the tag, set the page field to the new path, using the built in history and page variables.
This needs no changes to the app’s code, which makes it quick to set up. It does not work with hash routing in every case, and it can fire twice if the app updates the history more than once per screen, so test carefully.
A data layer event
The more reliable approach is to have the app push an event to the data layer on each route change:
dataLayer.push({
'event': 'virtualPageview',
'virtualPath': '/products/blue-widget',
'virtualTitle': 'Blue Widget'
});
In Tag Manager, create a Custom Event trigger for virtualPageview, data layer variables for the path and title, and a page view tag that uses them. Developers control exactly when the event fires; marketers control what happens in Tag Manager.
Problems to watch for
Lost campaign data
When the first page loads with UTM parameters, the session is attributed to that campaign. If you then set the page to a clean path and the location field to the current URL without the parameters, later hits may start a new session with a different source. Keep the original landing URL in the location field, or only change the page field.
Self referrals
If your app sends hits with a changing location but a stale referrer, or loads content through redirects, you may see your own domain appear as a referrer. Check the referral report after launch and add your domain to the referral exclusion list if needed.
Duplicate page views
Some setups send a page view in the snippet and again when the router first runs. Check the real time report while clicking through the app to confirm one page view per screen.
Titles that do not change
If the app updates the URL but not the document title, every page will show the same title in reports. Set the title explicitly in each tracking call.
Goals and funnels
URL destination goals work with virtual page views as long as the paths match. Update your goal definitions and funnel steps to use the virtual paths you chose.
Test before you trust the data
- Use the Real Time report in Google Analytics while you click through the app.
- Install the Google Analytics Debugger extension for Chrome, which logs every hit in the browser console.
- Use Tag Manager’s preview mode to check triggers fire once and with the right values.
- Use a test view with filters removed, so you can see raw data.
Once data is flowing, keep it clean. Spam hits can be especially confusing in an app where traffic patterns are new; our guide to referral spam explains how to filter them. If you are still on the older ga.js code, our Universal Analytics upgrade guide covers the move first.
FAQ
Does Google Analytics run JavaScript sites automatically?
The snippet runs once per full page load. It does not detect content changes made later, so you have to send those yourself.
Should every screen change be a page view?
Only changes that feel like a new page to the user. Small changes within a screen are better as events.
Will virtual page views affect bounce rate?
Yes, in a good way. A visitor who moves to a second screen will no longer count as a bounce, which gives you a realistic figure.
Want analytics you can trust?
We set up tracking for web apps and sites so the reports reflect what users actually do. See how we help you know what’s working.