How To Add Google Analytics To A Next.js Application
March 4, 2022
Introduction
Google Analytics is a service that allows you to measure and analyze traffic that your web application receives. It provides many useful stats such as which pages are the most visited, what kind of device do most of your users use (desktop or mobile), device screen resolution, most popular browsers and many more!
In this blog post I'll cover how to add Google Analytics 4 to a website and more specifically a Next.js Application. I will also show you how to disable it while in development and how to manage user consent!
Getting Started
The first thing you have to do is log in to the Google Analytics page with your google account and create a new property. You will have be asked some generic stuff like your country and currency, and the size of your company.
After you create a property you will be asked to set up a data stream. Choose "Web" and fill in your website's URL and a name to identify the stream. After clicking "Create Stream" you will be take to the stream's details. There you will find your "Measurement ID". Go ahead and copy that!
Adding Google Analytics code to Next.js
Before adding the code you should first create an environment variable for your Measurement ID. If you don't already have a .env.local
file go ahead
and create it in your projects root directory and then add this line to it:
NEXT_PUBLIC_GA_ID="YOUR_MEASUREMENT_ID"
Make sure that it starts with NEXT_PUBLIC_
, otherwise Next.js will not be able to access the variable in the front-end.
Note: When you deploy your website, don't forget to set the environment variables in your hosting providers website!
Creating a Google Analytics Component
To keep your code clean and tidy we are going to create a new component that contains all that is needed to make Google Analytics work, plus you can reuse it for multiple projects. It's going to be a simple component that accepts a measurement ID and returns two Next.js Scripts.
import Script from "next/script"const GoogleAnalytics = ({ ga_id }) => {return (<><Scriptstrategy="afterInteractive"src={`https://www.googletagmanager.com/gtag/js?id=${ga_id}`}/><Script id="gtagInit" strategy="afterInteractive">{`window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('set', { 'cookie_flags': 'SameSite=None;Secure' });gtag('config', '${ga_id}');`}</Script></>);}export default GoogleAnalytics;
By using Next's Script component you can place a script anywhere in your application and not necessarily in the head. The strategy property determines
when the script is going to load. Setting it to afterInteractive
means that the script is only going to load after the rest of the app has finished
loading. We also secured our cookies by setting the secure flag to true.
All that's left to do is add the Google Analytics component to your _app.js
file and pass it your measurement ID as a prop:
import Layout from '../components/Layout'import GoogleAnalytics from '../components/GoogleAnalytics'function MyApp({ Component, pageProps }) {return (<><GoogleAnalytics ga_id={process.env.NEXT_PUBLIC_GA_ID} /><Layout><Component {...pageProps} /></Layout></>)}export default MyApp
And that's all the code you need to add basic Google Analytics functionality to your website! I've seen a lot of examples around that also add event handlers that send "page_view" events on route change but they aren't actually needed. Google Analytics 4 automatically detects when the browser's history has changed!
Enabling Debug Mode
You can easily enable debug mode for all the events sent during development by setting a parameter in your gtag configuration. To do that first detect
that you are in fact in development mode and then set in the parameters 'debug_mode': 1
.
import Script from "next/script";const GoogleAnalytics = ({ ga_id }) => {return (<><Scriptstrategy="afterInteractive"src={`https://www.googletagmanager.com/gtag/js?id=${ga_id}`}/><Script id="gtagInit" strategy="afterInteractive">{`window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('set', { 'cookie_flags': 'SameSite=None;Secure' });gtag('config', '${ga_id}', {${process.env.NODE_ENV === "development" ? "'debug_mode': 1" : ""}});`}</Script></>);}export default GoogleAnalytics;
After enabling it you can go to the Google Analytics website and navigate to Configure > DebugView. There you will see a detailed view of all the events sent by your debug devices.
Note: If you want to disable debug mode you have to completely omit the parameter. Setting 'debug_mode': 0
will not work.
Disabling Google Analytics in Development
To disable Google Analytics while in development all you have to do is conditionally render the Google Analytics component you created.
import Layout from '../components/Layout'import GoogleAnalytics from '../components/GoogleAnalytics'const isDevelopment = process.env.NODE_ENV === "development"function MyApp({ Component, pageProps }) {return (<>{!isDevelopment &&<GoogleAnalytics ga_id={process.env.NEXT_PUBLIC_GA_ID} />}<Layout><Component {...pageProps} /></Layout></>)}export default MyApp
If you are in development the component's code will not be executed at all. An alternative approach is to install an ad blocker extension that also blocks trackers. I have tried uBlock Origin and while it is active the script will fail to load.
Managing User Consent
To determine whether Google Analytics should collect data you first have to know if the user agrees with it. First we set the default consent to
"denied" by calling the gtag('consent', 'default', ...)
command.
Then, inside a useEffect hook we check for the users consent, and if it is equal to "granted" then we update the consent with the gtag command. In all other cases we leave the consent at the default value which we have set to "denied" previously.
import Script from "next/script";import { useEffect } from "react";const GoogleAnalytics = ({ ga_id }) => {useEffect(() => {const consent = localStorage.getItem('cookieConsent')if (consent === 'granted') {window.gtag('consent', 'update', {'analytics_storage': 'granted'})}}, [])return (<><Scriptstrategy="afterInteractive"src={`https://www.googletagmanager.com/gtag/js?id=${ga_id}`}/><Script id="gtagInit" strategy="afterInteractive">{`window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('set', { 'cookie_flags': 'SameSite=None;Secure' });window.gtag('consent', 'default', {'analytics_storage': 'denied','wait_for_update': 500});gtag('config', '${ga_id}', {${process.env.NODE_ENV === "development" ? "'debug_mode': 1" : ""}});`}</Script></>);}export default GoogleAnalytics;
I chose to store the user's consent in local storage because I don't need to use it server side, but if you have some other task on the server that requires that information you can use a cookie instead.
Creating a Consent Banner
We are going to start by creating a custom hook that contains all the logic required to read and write to local storage.
When the application loads the hook is going to read the local storage for the user's consent and save it into a state variable. Whenever that state changes the hook is going to update the local storage.
import { useEffect, useState } from "react"export default function useCookieConsent() {const [cookieConsent, setCookieConsent] = useState("")useEffect(() => {setCookieConsent(localStorage.getItem('cookieConsent'))},[])useEffect(() => {if (cookieConsent){localStorage.setItem('cookieConsent', cookieConsent)}}, [cookieConsent])return { cookieConsent, setCookieConsent }}
Next, we are going to create a simple consent banner component to get the user's input. It's going to have some text and two buttons. Styling is outside the scope of this post but you can go ahead and style it as you see fit. I recommend you place it in a fixed position.
const ConsentBanner = ({ setConsent }) => {const handleAccept = () => {window.gtag('consent', 'update', {'analytics_storage': 'granted'})window.gtag('event', 'page_view')setConsent('granted')}const handleRefuse = () => {setConsent('denied')}return (<div><p>Cookie Agreement Text</p><button onClick={handleRefuse}>Refuse</button><button onClick={handleAccept}>Accept</button></div>);}export default ConsentBanner;
When the user presses the accept button we are going to update the consent with the gtag command and save it to local storage with our custom hook.
The hook will be called one level up in the component tree and its setCookieConsent
function will be passed to our banner component through a prop.
This is done to enable conditional rendering of the component.
In our layout component (or in the _app.js
file in case you don't have one) we are going to import the hook and the banner component.
The banner component will only be rendered if the cookieConsent
variable we get from our hook doesn't have a value, meaning that the user hasn't set
their consent yet.
import Navbar from "./Navbar"import Footer from "./Footer"import useCookieConsent from "../hooks/useCookieConsent"import dynamic from "next/dynamic"const ConsentBanner = dynamic(() => import("./ConsentBanner"))const Layout = ({ children }) => {const { cookieConsent, setCookieConsent } = useCookieConsent()return (<><Navbar /><main>{children}</main><Footer />{!cookieConsent &&<ConsentBanner setConsent={setCookieConsent} />}</>);}export default Layout;
We use Next's dynamic
function to import the consent banner component. This way it only gets downloaded by the browser after the rest of the app has
loaded, and it won't be downloaded at all if the user has already set their consent!
Avoiding Inline Scripts
If for some reason you want to avoid the second inline script in the Google Analytics component you can accomplish that by extracting the script's code into a function and then calling that function when the first script loads:
import Script from "next/script";const initGtag = ga_id => {const params = {}if (process.env.NODE_ENV === "development") {params.debug_mode = 1}window.dataLayer = window.dataLayer || [];window.gtag = function gtag() { dataLayer.push(arguments); }window.gtag('js', new Date());window.gtag('set', { 'cookie_flags': 'SameSite=None;Secure' });window.gtag('consent', 'default', {'analytics_storage': 'denied','wait_for_update': 500});window.gtag('config', ga_id, params)const consent = localStorage.getItem('cookieConsent')if (consent === 'granted') {window.gtag('consent', 'update', {'analytics_storage': 'granted'})}}const GoogleAnalytics = ({ ga_id }) => {return (<><Scriptstrategy="afterInteractive"src={`https://www.googletagmanager.com/gtag/js?id=${ga_id}`}onLoad={() => initGtag(ga_id)}/></>);}export default GoogleAnalytics;
Making Sure Everything Works
The most straight forward way to make sure everything works is to go to your Google Analytics property and navigate to Report > Realtime. There you can see an overview of all the events that are being sent.
Another way to confirm everything is in order is to open the developer tools in your browser, type in "dataLayer" and hit enter. You should see an array containing all the gtag commands we have sent.
Whenever the route changes Google Analytics also automatically send a history change event. In the network tab you can also see the files being sent. They start with "collect" and have a status of 204.
Note: As mentioned previously, make sure that you don't have an ad blocker enabled as it could cause Google Analytics to fail. Also, if you are testing in development, make sure you remove the conditional rendering of the component we implemented in the previous step as it would cause Google Analytics not to run at all.
That's all for this post! I hope that you found this post helpful and that you have a fantastic day!