skies.dev

Google Analytics 4 with Next.js

8 min read

I'm going to show you how I set up Google Analytics 4 in a Next.js project. Before we dive in, make sure to set up a Google Analytics account.

Page views

Let's set up a file called client/analytics.ts to keep our code related to analytics.

The first thing we'll need to access in our code is our Google Analytics 4 tracking ID, which will have the format G-XXXXXXXX.

client/analytics.ts
// TOOD: replace this with your own ID
export const ANALYTICS_ID = 'G-PR00D6TRZC';

You could also keep your tracking ID as an environment variable, which I'd recommend if your site is open source so that people don't inadvertently copy your tracking ID. Either way, the analytics ID needs to accessible from the client.

Once the analytics ID is defined, we can add Google Analytics in a custom Document.

pages/_document.tsx
import {Html, Head, Main, NextScript} from 'next/document';
import {ANALYTICS_ID} from '@client/analytics';

export default function Document() {
  return (
    <Html>
      <Head>
        <script
          async
          src={`https://www.googletagmanager.com/gtag/js?id=${ANALYTICS_ID}`}
        />
        <script
          dangerouslySetInnerHTML={{
            __html: `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', '${ANALYTICS_ID}', {
              page_path: window.location.pathname,
            });
          `,
          }}
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

Let's unpack what's happening here.

The first <script> injects the gtag library. The second <script> is doing two things. First, it initializes gtag.js:

window.dataLayer = window.dataLayer || [];
function gtag() {
  dataLayer.push(arguments);
}
gtag('js', new Date());

The next part is for tracking page views.

gtag('config', '${ANALYTICS_ID}', {
  page_path: window.location.pathname,
});

And with that, you should be good to start using Google Analytics. Out of the box, Google Analytics will automatically collect events, so you'll get a lot of insight simply by enabling Google Analytics on your site.

However, you might be interested in adding custom events to track things unique to your site. Let's look at that next.

Custom events

Let's first get the type definitions for gtag.js.

yarn add -D @types/gtag.js

Going back to client/analytics.ts, we'll create a new function to track custom events.

client/analytics.ts
export const trackEvent = (action: string, params: Gtag.EventParams = {}) => {
  // don't do anything in case of a problem loading gtag.js
  if (typeof window.gtag === 'undefined') {
    return;
  }
  window.gtag('event', action, params);
};

This track function takes two parameters.

  1. action says what the user did.
  2. params is an optional parameter for metadata about the event. Google's documentation on measuring events goes into more depth on these.

So for example, let's say you want to track when someone signs up for your newsletter. We'll assume signUp is some asynchronous function that adds an email to the database. If the sign-up is successful, we'll send an event to Google Analytics.

components/SignUpButton.tsx
import {trackEvent} from '@client/analytics';
import {signUp} from '@lib/signUpUser';

export function SignUpButton({email}: {email: string}) {
  return (
    <button
      type="submit"
      onClick={(e) => {
        e.preventDefault();
        signUp(email).then(() => {
          trackEvent('sign_up');
        });
      }}
    >
      Sign up
    </button>
  );
}

But what happens if signUp throws an error? We'll want a way to track errors as well, which we'll look at next.

Errors and exceptions

To measure exceptions, we'll keep using window.gtag('event'). If you want, you could simply keep using trackEvent for this purpose.

components/SignUpButton.tsx
import {trackEvent} from '@client/analytics';
import {signUp} from '@lib/signUpUser';

export function SignUpButton({email}: {email: string}) {
  return (
    <button
      type="submit"
      onClick={(e) => {
        e.preventDefault();
        signUp(email)
          .then(() => {
            trackEvent('sign_up');
          })
          .catch((err) => {
            trackEvent('exception', {
              description: err.message,
              fatal: false,
            });
          });
      }}
    >
      Sign up
    </button>
  );
}

However, I think we can make this easier by making a function for handling errors.

client/analytics.ts
export function trackError(err: unknown, fatal = false) {
  let description;
  if (err instanceof Error) {
    description = err.message;
  } else {
    description = 'unknown_cause';
  }
  trackEvent('exception', {description, fatal});
}

We need to set err's type as unknown because in JavaScript, you can throw anything. This also gives us flexibility to handle different kinds of errors. For example, if you're using axios, you can add a check for that.

client/analytics.ts
import axios from 'axios';

export function trackError(err: unknown, fatal = false) {
  let description;
  if (err instanceof Error) {
    description = err.message;
  } else if (axios.isAxiosError(err)) {
    description = err.response?.data ?? err.message;
  } else {
    description = 'unknown_cause';
  }
  trackEvent('exception', {description, fatal});
}

The trackError function now makes it easy to track errors in catch blocks. We can update our SignUpButton as follows.

components/SignUpButton.tsx
import {trackEvent, trackError} from '@client/analytics';
import {signUp} from '@lib/signUpUser';

export function SignUpButton({email}: {email: string}) {
  return (
    <button
      type="submit"
      onClick={(e) => {
        e.preventDefault();
        signUp(email)
          .then(() => {
            trackEvent('sign_up');
          })
          .catch(trackError);
      }}
    >
      Sign up
    </button>
  );
}

Now we've set up our code to track page views and our custom events. We're almost ready for production, but before we go live, let's make sure everything is working.

Debugging

The way to debug Google Analytics events is by using DebugView, accessible within the Google Analytics dashboard under the Configure tab.

To send events to DebugView, we just need to set debug_mode: true for each event we emit. We'll set this up so that when we're running our Next.js app in development, the debug_mode property is automatically added to the properties we send.

Let's head back to our custom Document to send page views to DebugView.

pages/_document.tsx
import {Html, Head, Main, NextScript} from 'next/document';
import {ANALYTICS_ID} from '@client/analytics';

export default function Document() {
  return (
    <Html>
      <Head>
        <script
          async
          src={`https://www.googletagmanager.com/gtag/js?id=${ANALYTICS_ID}`}
        />
        <script
          dangerouslySetInnerHTML={{
            __html: `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', '${ANALYTICS_ID}', {
              page_path: window.location.pathname,${
                process.env.NODE_ENV === 'development' ? 'debug_mode: true' : ''
              }
            });
          `,
          }}
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

Next we'll update our trackEvent functions to append the debug property as well.

client/analytics.ts
export const trackEvent = (action: string, params: Gtag.EventParams = {}) => {
  // don't do anything in case of a problem loading gtag.js
  if (typeof window.gtag === 'undefined') {
    return;
  }
  if (process.env.NODE_ENV === 'development') {
    // @ts-ignore
    params.debug_mode = true;
  }
  window.gtag('event', action, params);
};

Note, we need to add @ts-ignore because unfortunately debug_mode is not part of the type definition for Gtag.EventParams at the time of writing this. Rest assured, it is a valid property per Google's documentation on monitoring events in debug mode.

With debug_mode: true added, when you run your app locally, you should be able to see page views and events in DebugView. In my experience, the events can take 10-30 seconds to show up.

Now that we verified our events are working as expected, we're almost ready for production. Before we deploy, I recommend setting up filters.

Filters

We'll use filters to avoid polluting our analytics with events while we're developing our site or viewing it from home. For this, we're going to set up two filters:

  1. Internal traffic filters to ignore events when we see our production site from our own devices.
  2. Developer traffic to filter out events emitted while developing the site.

First, let's define what constitutes as "internal traffic." For the purpose of this article, we'll define internal traffic as being traffic coming from our home's IP address. We'll define our internal traffic settings by going to Admin > Data Streams > YOUR_STREAM > More Tagging Settings > Define internal traffic. From here, you can create a new rule to filter out internal traffic. Google "what's my IP" to find your public IP address and set up your filter conditions accordingly.

Next we'll activate our data filters by going to Admin > Data Settings > Data Filters. You should see Google was nice enough to set up a filter for internal traffic. Change this filter's state to "active" to start ignoring internal traffic i.e. traffic coming from your IP address.

Create a new filter for filtering out developer traffic and set its state to "active". This will ignore events that contain the debug_mode property we added in the Debugging.

It's important to note that when the internal traffic filter is set to active, you won't be able to see events coming from your IP address in DebugView. If you need to use DebugView, you'll want to temporarily set the internal traffic filters to inactive.

At this point, we should be ready to go live in production. I hope this article helps you build a great user experience and makes your business prosper.

Hey, you! 🫵

Did you know I created a YouTube channel? I'll be putting out a lot of new content on web development and software engineering so make sure to subscribe.

(clap if you liked the article)

You might also like