There are a number of articles out there describing various methods of configuring google analytics
with next.js
-- all written with untyped JavaScript. This article aims to remedy the absence of a strongly typed reference. The official next.js example serves as a good reference, yet, it too lacks strong types as it is written with JavaScript.
Install @types/gtag.js
as a dev dependency
Open your terminal and run
yarn add -D @types/gtag.js
We will not be needing the vanilla (non-typed) gtag.js
package, the react-ga
package, or any other package for that matter. This is where declaration (**/*.d.ts
) files really shine! Before getting started, navigate to your tsconfig.json
file and ensure that the include
flag specifies the **/*.d.ts
glob pattern
"include": ["**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
Referencing Types in a local declaration file
First, create a root index.d.ts
file. This is where we will configure a triple-slash directive types-reference to the @types/gtag.js
dev-dependency. Once configured and declared, the types contained within @types/gtag.js
will be globally available for consumption -- no imports required. Sure is nice
From typescriptlang.org:
/// <reference types="..." />
"Similar to a
/// <reference path="..." />
directive, which serves as a declaration of dependency, a/// <reference types="..." />
directive declares a dependency on a package.The process of resolving these package names is similar to the process of resolving module names in an import statement. An easy way to think of triple-slash-reference-types directives are as an import for declaration packages."
// ..
"Use these directives only when youโre authoring a d.ts file by hand."
As the official TS Docs indicate one should only use these directives when authoring (or extracting from) a .d.ts file by hand -- which fits the bill for our use-case with @types/gtag.js
. I like to refer to unpaired or lone dev dependencies as stagpendencies -- they could use an introduction
With the formalities out of the way, add the following code to your index.d.ts
file to give this package a proper "intro":
/// <reference types="gtag.js" />
declare module 'gtag.js';
Google Analytics V4 -- Acquire a Tracking ID
Head over to google analytics and sign in. If you don't have an account, create one, then sign in. Once signed in and on the landing page, click on the Admin
gear icon in the bottom left corner then select + create property
as pictured below
Next, add a property name and do not select create a universal property
under advanced options. This writeup does not cover universal properties -- universal properties require the @types/google.analytics
dev dependency to be properly typed.
then provide business information about your new google analytics property. Since the property I'm creating is an example for this article, I've selected other
as the property type and only the top three options as being my intended use of google analytics. That said, if you are tracking a commerce site, for example, select additional desired options for your project.
Configure a Data Stream for your Property
Next, let's configure a data stream for our new property to start collecting data. Select web
as a platform then fill in the website url and stream-name fields appropriately. The website url field should be the primary url of your production landing page.
Once finished, click "Create stream". This should navigate you to the "Web Stream Details" view. Copy the Measurement ID
for your newly created property. We will be using this as an environmental variable. Note: do not use your stream id value. These two key-val pairs are not interchangeable. The measurement ID
is always prefixed with G-
in version 4 (as opposed to UA- in version 3) followed by a random 10-character-alphanumeric string (e.g., G-ABC4850XYZ
)
Back to your code editor
After copying the measurement ID for your new property, open your code editor, create a .env.local
file in the root directory, then add the following key-value pair
NEXT_PUBLIC_GA_TRACKING_ID=G-ABC4850XYZ
Next, create a root lib
directory and an analytics.ts
file therein. It is important to handle your measurement id
environmental variable as a conditionally undefined string (process.env.* values always resolve to string | undefined
)
@/lib/analytics.ts
export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_TRACKING_ID ?? '';
Consuming the globalized reference types
As mentioned previously, there is no need to import anything to consume the reference @types/gtag.js
types declared in the root index.d.ts
file. Let's start with pageview
:
export const pageview = (url: URL) => {
window.gtag('config', GA_TRACKING_ID, {
page_path: url
});
};
you should see the following intellisense definition when hovering over the appended gtag
of window.gtag
var gtag: Gtag.Gtag
(command: "config", targetId: string, config?: Gtag.ControlParams | Gtag.EventParams | Gtag.CustomParams | undefined) => void (+6 overloads)
if you Ctrl+click while hovering the window-appended gtag
, it will take you to the node_modules/@types/gtag.js
declaration file where you can view all of the type definitions provided by the @types/gtag.js
package.
Let's export one additional function to track events associated with pageviews:
export const event = (
action: Gtag.EventNames,
{ event_category, event_label, value }: Gtag.EventParams
) => {
window.gtag('event', action, {
event_category,
event_label,
value
});
};
The action
parameter measures user-initiated events. The destructured { event_category, event_label, value }
parameters capture relevant analytics data for each of the action
events.
The Gtag.EventNames
(user actions) corresponds to the following unions defined by the EventNames
type:
type EventNames =
| 'add_payment_info'
| 'add_to_cart'
| 'add_to_wishlist'
| 'begin_checkout'
| 'checkout_progress'
| 'exception'
| 'generate_lead'
| 'login'
| 'page_view'
| 'purchase'
| 'refund'
| 'remove_from_cart'
| 'screen_view'
| 'search'
| 'select_content'
| 'set_checkout_option'
| 'share'
| 'sign_up'
| 'timing_complete'
| 'view_item'
| 'view_item_list'
| 'view_promotion'
| 'view_search_results';
While we only used event_category
, event_label
, and value
in this writeup, the Gtag.EventParams
interface has the following shape
interface EventParams {
checkout_option?: string;
checkout_step?: number;
content_id?: string;
content_type?: string;
coupon?: string;
currency?: string;
description?: string;
fatal?: boolean;
items?: Item[];
method?: string;
number?: string;
promotions?: Promotion[];
screen_name?: string;
search_term?: string;
shipping?: Currency;
tax?: Currency;
transaction_id?: string;
value?: number;
event_label?: string;
event_category?: string;
}
any of these parameters can be used to track user-mediated events.
The contents of your @/lib/analytics.ts
file should now look as follows:
export const GA_TRACKING_ID =
process.env.NEXT_PUBLIC_GA_TRACKING_ID ?? '';
export const pageview = (url: URL) => {
window.gtag('config', GA_TRACKING_ID, {
page_path: url
});
};
export const event = (
action: Gtag.EventNames,
{ event_category, event_label, value }: Gtag.EventParams
) => {
window.gtag('event', action, {
event_category,
event_label,
value
});
};
pages/_document.tsx
Nearly finished. Navigate to pages/_document.tsx
and import the GA_TRACKING_ID
constant that we exported from @/lib/analytics.ts
import Document, {
Head,
Html,
Main,
NextScript,
DocumentContext,
DocumentProps,
DocumentInitialProps
} from 'next/document';
import { GA_TRACKING_ID } from '@/lib/analytics';
This file is important because it is used to augment the html, head, and body tags for all of the page files in our next.js repo. We will be injecting the Head
of _document.tsx
with two script
tags as follows:
<Head>
<meta charSet='utf-8' />
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
/>
<script
dangerouslySetInnerHTML={{
__html: `window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_TRACKING_ID}', {
page: window.location.pathname
});`
}}
/>
</Head>
For a holistic picture of _document.tsx
, I'll include the contents of my current working file:
import Document, {
Head,
Html,
Main,
NextScript,
DocumentContext,
DocumentProps,
DocumentInitialProps
} from 'next/document';
import { GA_TRACKING_ID } from '@/lib/analytics';
export default class FadeDocument extends Document<
DocumentProps | unknown
> {
static async getInitialProps(
ctx: DocumentContext
): Promise<DocumentInitialProps> {
const originalRenderPage = ctx.renderPage;
const initialProps = await Document.getInitialProps(ctx);
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => <App {...props} />
});
} catch (error) {
console.log(error);
}
return {
...initialProps,
styles: <>{initialProps.styles}</>
};
}
render() {
return (
<Html lang='en-US'>
<Head>
<meta charSet='utf-8' />
<link
rel='stylesheet'
href='https://rsms.me/inter/inter.css'
/>
<link rel='shortcut icon' href='/meta/favicon.ico' />
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
/>
<script
dangerouslySetInnerHTML={{
__html: `window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_TRACKING_ID}', {
page: window.location.pathname
});`
}}
/>
</Head>
<body className='loading'>
<Main />
<NextScript />
</body>
</Html>
);
}
}
Wrapping this up in the root _app.tsx
file
Navigate to the custom pages/_app.tsx
file and import gtag
as a wildcard (*) from @/lib/analytics
. We will also be needing useEffect
from React
and useRouter
from next/router
. Add the following code within the default export function but before the returned tsx in your _app.tsx
file:
const router = useRouter();
useEffect(() => {
const handleRouteChange = (url: URL) => {
gtag.pageview(url);
};
router.events.on(
'routeChangeComplete',
handleRouteChange
);
return () => {
router.events.off(
'routeChangeComplete',
handleRouteChange
);
};
}, [router.events]);
This code tracks pageview change events for the entirety of your app. I've included the full contents of my _app.tsx file below to provide a holistic view once more:
import '@/styles/index.css';
import '@/styles/chrome-bug.css';
import { AppProps, NextWebVitalsMetric } from 'next/app';
import { useEffect, FC } from 'react';
import { useRouter } from 'next/router';
import * as gtag from '@/lib/analytics';
const Noop: FC = ({ children }) => <>{children}</>;
export default function NextApp({
pageProps,
Component
}: AppProps) {
const LayoutNoop = (Component as any).LayoutNoop || Noop;
// remove chrome-bug.css loading class on FCP
useEffect(() => {
document.body.classList?.remove('loading');
}, []);
const router = useRouter();
useEffect(() => {
const handleRouteChange = (url: URL) => {
gtag.pageview(url);
};
router.events.on(
'routeChangeComplete',
handleRouteChange
);
return () => {
router.events.off(
'routeChangeComplete',
handleRouteChange
);
};
}, [router.events]);
return (
<>
<LayoutNoop pageProps={pageProps}>
<Component {...pageProps} />
</LayoutNoop>
</>
);
}
export function reportWebVitals(
metric: NextWebVitalsMetric
): void {
switch (metric.name) {
case 'FCP':
console.log('FCP: ', metric);
break;
case 'LCP':
console.log('LCP: ', metric);
break;
case 'CLS':
console.log('CLS: ', metric);
break;
case 'FID':
console.log('FID: ', metric);
break;
case 'TTFB':
console.log('TTFB: ', metric);
break;
case 'Next.js-hydration':
console.log('Next.js-hydration: ', metric);
break;
case 'Next.js-route-change-to-render':
console.log('Next.js-route-change-to-render: ', metric);
break;
case 'Next.js-render':
console.log('Next.js-render: ', metric);
break;
default:
break;
}
}
Push - Deploy - Profit
Ensure that your deployment environment is provided with the NEXT_PUBLIC_GA_TRACKING_ID
key-value pair, push your changes, successfully deploy, profit.
Check back in on google analytics after navigating around your newly deployed site to see if the data was successfully logged. That's all there is to incorporating strongly typed definitions into your next.js google analytics repo.
Top comments (1)
How to send reportWebVitals to GA?
RESP: See part II - Thanks!