Google Analytics 4 with Next.js
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
.
// 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
.
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.
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.
action
says what the user did.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.
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.
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.
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.
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.
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.
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.
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:
- Internal traffic filters to ignore events when we see our production site from our own devices.
- 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.