If you use the default Google Analytics Global Site Tag (gtag.js) code on a Turbolinks-powered site, Analytics will only track the first page that a user visits. As a user navigates to other pages, none of the subsequent pages will be tracked.
This is because Google Analytics was designed for traditional multi-page websites, where each page navigation causes a complete reload of the page. When the new page loads, the Analytics code in the head will fire almost immediately.
When using Turbolinks, the code in the head only executes on the initial page load. This means we must do some extra work to ensure that Analytics is notified when navigating between pages.
Programmatically sending page views to Analytics using Turbolinks
Below is the Global Site Tag (gtag.js) code that Analytics provides in its dashboard under Admin > Tracking Info > Tracking Code.
Copy this code from Analytics and paste it as the first item inside your page's <head />
tag. In your code, {ANALYTICS_ID}
will be something similar to UA-123456789-1
.
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={ANALYTICS_ID}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{ANALYTICS_ID}');
</script>
If your script doesn't look like the one above, you may be using analytics.js or Google Tag Manager. Those are beyond the scope of this article, but you can read more about how to get these to work with single page applications here.
Immediately below the script tag above, add the following, replacing {ANALYTICS_ID}
with your site's Analytics ID:
<script type="module">
let isInitialLoad = true;
document.addEventListener('turbolinks:load', () => {
if (isInitialLoad){isInitialLoad = false; return;}
gtag('config', '{ANALYTICS_ID}', {
page_path: window.location.pathname,
});
});
</script>
The full solution
All together, your site's head will now look like this:
<head>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={ANALYTICS_ID}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{ANALYTICS_ID}');
</script>
<script type="module">
let isInitialLoad = true;
document.addEventListener('turbolinks:load', () => {
if (isInitialLoad){isInitialLoad = false; return;}
gtag('config', '{ANALYTICS_ID}', {
page_path: window.location.pathname,
});
});
</script>
<!-- More code below... -->
</head>
Once you've replaced {ANALYTICS_ID}
with your site's ID, Analytics will now be able to track subsequent page visits on your website. Keep reading if you want to understand how this code works, or feel free to copy it and be on your way!
How it works
Let's break this code apart, starting with the script tag that loads gtag.js
:
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={ANALYTICS_ID}"></script>
This loads the analytics script. The interesting part here is the async
attribute: this script will load asynchronously in the background while the other HTML and JS continues to be parsed.
Next, let's look at the boot-up script:
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{ANALYTICS_ID}');
</script>
The most important piece of this script is gtag('config', '{ANALYTICS_ID}')
. Calling the gtag()
function with 'config'
in its first argument tells Analytics that a new page view has occurred.
The gtag()
function is also interesting: it pushes any arguments that it receives to a global dataLayer
array. Once the async gtag.js script finishes loading, the items within the dataLayer
array will be pushed to Analytics.
Now lets turn our attention to Turbolinks:
<script type="module">
let isInitialLoad = true;
document.addEventListener('turbolinks:load', () => {
if (isInitialLoad){isInitialLoad = false; return;}
gtag('config', '{ANALYTICS_ID}', {
page_path: window.location.pathname,
});
});
</script>
On the <script />
tag, the type="module"
attribute does two things:
- It prevents variables within the script from leaking into the global scope, meaning we don't need to worry about other scripts overriding them.
- It defers the script, meaning that it will only fire after the document has been parsed, but before the
DOMContentLoaded
event is fired (MDN docs). This delayed execution is fine, because it will still load before Turbolinks initializes.
In the turbolinks:load
callback, we check to see if it is the initial page load, and if it is, we return early. The first visit is already tracked from the script we copied from the Analytics dashboard.
On page navigations, the turbolinks:load
callback will call the gtag()
function with "config"
as the first argument, telling Analytics that there was a new page view. Like before, the second argument is the Analytics ID. The third argument is something a new: a config object with a page_path
property.
The gtag('config', ...)
function requires the page_path
property in its configuration object to accurately track what page it's on when performing client-side navigation. Without page_path
, Analytics will register it as another page view for the page that the user initially loaded. You can view all of the properties that the config object accepts in the Analytics documentation.
Why not track all page views from within the turbolinks:loaded
callback?
You may be wondering why we don't just track all of our page views in the turbolinks:loaded
callback. After all, it would simplify the code by removing the gtag('config', ...)
call from the script we copied from the Analytics dashboard, and we would no longer have to check if it was the initial page load in the second script.
There's a good reason we want to separate these out. On a slow connection, it may take several seconds for the page to load and for Turbolinks to initialize. If a user is on a slow connection, waits for five seconds, and exits before Turbolinks loads, the gtag('config', ...)
function would never fire because Turbolinks would have never loaded. By having the first gtag()
function fire immediately when the page loads, it's much more likely that Analytics will capture the page view, even if the user bounces after a few seconds.
Additional reading
Here are several resources I found helpful while figuring out how to hook this up. Maybe you'll find them valuable too.
Google Analytics Documentation: Single Page Application Measurement. This was written for the old analytics.js script, but the section at the end that discusses
document.referrer
anddocument.location
seems like it is still relevant with gtag.js.Optimize Smart: Sending pageview data via measurement protocol in Google Analytics. This article demystifies the two letter abbreviations that the page is sending to Analytics on every request.
Top comments (2)
Great article! This helped a lot.
I just had to make a change since my project is using Turbo and not Turbolinks. The former is the replacement to the no longer maintained Turbolinks project.
turbo.hotwired.dev/reference/events
The event listener is simply turbo:load instead of turbolinks:load
document.addEventListener('turbo:load', () => {});
I'm glad you liked it!
I've been a little late to the party with Hotwire Turbo. I just started working on my first project with it a few days ago. So far so good, though a lot left to learn!