Google Analytics with Next.js

This blog post is part a series of posts on developing a blog with Next.js and Prismic. In this post we will be adding Google Analytics to our blog by leveraging Next.js's custom _document.js file.

Set Up

If have been coding along in the series, we will be continuing on from where we finished up in my previous post, SEO in Next.js. If you haven't been coding along to date and want to you can find code the on GitHub (you will want to start at 02). You will also find the finished code in that repository if you would like to use it as a reference.

For the purpose of this post I am assuming you already have a Google Analytics account. If not, head on over to Google to set one up. To complete this part of the series, all you need is your tracking code which will begin with a UA- prefix. (UA-XXXXXXXX-X)

Custom Document

Ok let's get started. First up we are going to create a custom _document.js that will load our Google Analytics script for us. We will only load this script in production so that we don't track items locally. In the pages directory create a new file, _document.js and add the following code:

// pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document'
// We wrap our scripts below in Fragment to avoid unnecessary mark up
import { Fragment } from 'react'
export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    // Check if in production
    const isProduction = process.env.NODE_ENV === 'production'
    const initialProps = await Document.getInitialProps(ctx)
    // Pass isProduction flag back through props
    return { ...initialProps, isProduction }
  }

  // Function will be called below to inject
  // script contents onto page
  setGoogleTags() {
    return {
      __html: `
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', 'UA-XXXXXXXX-X');
      `,
    }
  }

  render() {
    const { isProduction } = this.props
    return (
      <html>
        <body>
          <Main />
          <NextScript />
          {/* We only want to add the scripts if in production */}
          {isProduction && (
            <Fragment>
              <script
                async
                src="https://www.googletagmanager.com/gtag/js?id=UA-XXXXXXXX-X"
              />
              {/* We call the function above to inject the contents of the script tag */}
              <script dangerouslySetInnerHTML={this.setGoogleTags()} />
            </Fragment>
          )}
        </body>
      </html>
    )
  }
}

Most of the code above is the default _document.js supplied by Next.js. So what have we added to it? In getInitialProps we are checking if we are in production. The _document.js is is only rendered on the server so we will always have access to this process. We then pass this flag through to the props.

Next we have the setGoogleTags function. All we are doing here is returning the contents we want to populate the script with via React's dangerouslySetInnerHTML. Towards the bottom we then have a check for production using our flag we created earlier. If in production we are going to render two script tags. One will load the required Google Tag manager library the other will inject our initialisation code. All that's left to do in this file is to update all references of UA-XXXXXXXX-X with your tracking code.

Tracking Page Views

Now that we are after initialising Google Analytics we need to ensure we are firing a page view when we navigate client side. To do this will make some updates to our custom _app.js. Before that let's create a function in our helpers file that will fire the page view event. Add the following function and don't forget to add it to the exports.

// helpers/index.js
function trackPageView(url) {
  try {
    window.gtag('config', 'UA-XXXXXXXX-X', {
      page_location: url,
    })
  } catch (error) {
    // silences the error in dev mode
    // and/or if gtag fails to load
  }
}

What we are doing here is calling gtag (that we bound to the window earlier) with a url to track the current page view. We also wrap this in try catch block so that we don't get any errors in development mode or in production if the library fails to load for any reason. Which can happen and has happened to me causing massive page load delays. You will also need to update your tracking code (UA-XXXXXXXX-X) above. Ideally you would have some sort of error notification added to the catch in production so that you know if Google Analytics is not running, although in a small blog I wouldn't count this as critical but it is worth mentioning.

Let's put that newly created function to use. Our goal: When a user navigates client side to a new route we want trigger a page view. To do this will monitor route changes in our _app.js and call our new function when a route change happens. To do this let's add the following two imports to your _app.js:

// pages/_app.js
import Router from 'next/router'
import { trackPageView } from '../helpers'

Here we are importing Next.js's router and also the helper function we just created. Next let's add a lifecycle hook where will check for the route change then track a page view:

// pages/_app.js
componentDidMount() {
  Router.onRouteChangeComplete = url => {
    trackPageView(url);
  };
}

Here we are leveraging Next.js Router's method to see when route change has been complete, which gives us the page's url.

Now all that is left to do is test it out. To do this we first want to run it in dev mode and ensure there are no errors and also calls going to Google (you can check in the network tab of developer tools in Chrome). Then let's run it in production mode, which is yarn build followed by yarn start. Here we want to see no errors and we do want to see some calls going to Google. Also if you now log into your Google Analytics dashboard you should see a Real Time user active. If you got stuck along the way you can check out the code here.

Conclusion

In this post we added some simple page view tracking. Although using the method bound to the window is not ideal for this simple use case it works. I would like to revisit this in the future and hopefully leverage some plug in's for it. If you need to track events you could easily add a trackEvent function the helpers file and use it in the same manner as the trackPageView.

As always if you enjoyed the post give it a share, or if you have any feedback, questions or corrections please give me a shout on Twitter.