DEV Community

Tyler Smith
Tyler Smith

Posted on • Updated on

Using Google Analytics's gtag.js with Turbolinks

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.

Where to find your tracking code

Copy the 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

On the <script /> tag, the type="module" attribute does two things:

  1. It prevents variables within the script from leaking into the global scope, meaning we don't need to worry about other scripts overriding them.
  2. 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.

Discussion (0)