skies.dev

Google Analytics 4 with Next.js

3 min read

This article shows a practical way to wire Google Analytics 4 into a Next.js app. The setup has four parts:

  • load gtag.js
  • track page views
  • track custom events and exceptions
  • separate real traffic from development noise

Page views

Start with a small analytics module that holds your GA4 tracking ID. You can hard-code it or expose it through an environment variable, but it must be available to the client.

client/analytics.ts
export const ANALYTICS_ID = 'G-PR00D6TRZC';

Add the script in your 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>
  );
}

That is enough to start recording page views.

Custom events

Install the GA types so your event helpers can use Gtag.EventParams:

yarn add -D @types/gtag.js

Then add a small wrapper around window.gtag:

client/analytics.ts
export const trackEvent = (action: string, params: Gtag.EventParams = {}) => {
  if (typeof window.gtag === 'undefined') {
    return;
  }

  if (process.env.NODE_ENV === 'development') {
    // @ts-ignore
    params.debug_mode = true;
  }

  window.gtag('event', action, params);
};

Use it from your app code:

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>
  );
}

Errors and exceptions

For failures, send an exception event. A dedicated helper keeps the call sites clean:

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

export function trackError(err: unknown, fatal = false) {
  let description: string;

  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});
}
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>
  );
}

Debugging

GA4 has a DebugView in the Analytics UI. If you add debug_mode: true, events appear there instead of blending into production traffic.

In development, the helper above adds debug_mode automatically, and the Document snippet can do the same for page views.

A useful habit is to verify events locally before shipping. In practice, DebugView can lag by a few seconds, so do not expect instant feedback.

Filters

Once tracking works, set up filters so your own visits do not pollute the data.

Typical filters are:

  1. Internal traffic, for visits from your office or home network.
  2. Developer traffic, for events with debug_mode: true.

Configure internal traffic in Analytics under the stream tagging settings, then activate the matching data filter in Admin > Data Settings > Data Filters.

Create a second filter for developer traffic and enable it as well.

If the internal traffic filter is active, your own visits will stop showing up in DebugView. Temporarily disable it when you need to debug from your normal IP address.

That is the core setup: page views, custom events, exception tracking, debug mode, and filters.

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