Andrew Ross's Blog

Andrew Ross's Blog

Strongly Typed Google Analytics V4 with Next.js

Strongly Typed Google Analytics V4 with Next.js

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

image

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

Click next image

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

Configure a Data Stream for your Property

web platform

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

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

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.

production-key-val-pair

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

#nextjs#typescript#google#analytics#reactjs
 
Share this