Google Analytics 4 with Next.js
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.
export const ANALYTICS_ID = 'G-PR00D6TRZC';
Add the script in your 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>
);
}
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:
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:
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:
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});
}
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:
- Internal traffic, for visits from your office or home network.
- 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.