Strongly Typed Google Analytics V4 with Nextjs
Tags: typescript, analytics, nextjs, react
Published: 2021-06-04 09:21:15

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](https://github.com/vercel/next.js/tree/canary/examples/with-google-analytics) 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 

```git
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

```json
"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](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-):

#### `/// <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": 

```ts
/// <reference types="gtag.js" />

declare module 'gtag.js';
```

### Google Analytics V4 -- Acquire a Tracking ID
Head over to [google analytics](https://analytics.google.com) 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

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/39d3l7429uk79ukqbsml.png)

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.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7zval351m3ayvtlnae5r.png)
 
 Click `next`   ![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/apk96fy0ksdw6722hfqh.png)

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.

![analytics options](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zn1zvmzz0dl85lncjetl.png)

### Configure a Data Stream for your Property
![web platform](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f9spdky5x82uwx4o3sye.png)

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.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3a9x6w26dpwnbepzo5u5.png)

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`)
 
![web stream details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9w1zsjvqdet0pdcjcg4e.png)

### 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

```env
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`
```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`:

```ts
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` 

```ts
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](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gtag.js/index.d.ts) provided by the `@types/gtag.js` package.

Let's export one additional function to track events associated with pageviews:

```ts
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:

```ts
  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

```ts
  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:

```ts
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`

```tsx
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_](https://nextjs.org/docs/advanced-features/custom-document) 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:

```tsx 
<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:

```tsx
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:

```tsx
	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:

```tsx
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.
 
![production-key-val-pair](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yswj62eyikskzafptyuz.png)
 
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.

![example_analytics](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vd7ehybj8cvb9sw7vn78.png)