DEV Community

Cover image for Strongly Typed GA4 with Nextjs Part II - Danger Zone
Andrew Ross
Andrew Ross

Posted on

Strongly Typed GA4 with Nextjs Part II - Danger Zone

In Part One of this series, prior to the release of Next v11, we covered configuring GA4 with Nextjs.

Since then, Nextjs has rolled out a next/script component which provides an alternative to dangerously escaping inner html -- a previously necessary evil when working with vanilla script tags.

Bonus: it's also more performant

danger-zone

Therefore, it's no longer necessary to live dangerously in the world of HTML -- semantics aside, we'll move the script tags we configured in _document.tsx in the first article over to _app.tsx.

Important Aside: Scripts must be instantiated above the Head (next/head) tag in Next.js pages and must never be used with the head in next/document

This syntactically sexy implementation can be executed as follows:

const Noop: FC = ({ children }) => <>{children}</>;

export default function NextApp({
  Component,
  pageProps
}: AppContext & AppInitialProps) {

  const LayoutNoop = (Component as any).LayoutNoop || Noop;

  const apolloClient = useApollo(pageProps);

  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 (
    <>
      <Script
        async
        strategy='lazyOnload'
        src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
      />
      <Script strategy='afterInteractive'>
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', '${GA_TRACKING_ID}', {
            page_path: window.location.pathname,
          });
        `}
      </Script>
      <Head nextSeoProps={NextSEO} />
      <ApolloProvider client={apolloClient}>
        <AuthProvider>
          <GoogleFacebookProvider>
            <LayoutNoop pageProps={pageProps}>
              <Component {...pageProps} />
            </LayoutNoop>
          </GoogleFacebookProvider>
        </AuthProvider>
      </ApolloProvider>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

For those of you curious about various methods available when leveraging the new <Script /> component, you can read more here

Before eagerly firing up dev or deploying these changes to preview/production, be sure to remove the following XSS honeypot from the Head of _document.tsx

// ...
        <Head>
          <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>
// ...
Enter fullscreen mode Exit fullscreen mode

Real Time Metric Reporting? Yes.

For those of you interested in sending real-time metrics to your google analytics property, you're in luck. Below your default export in _app.tsx, include the following function, and you'll have real-time metrics pinging in your GA4 dashboard before you know it

export function reportGAVitals({
  id,
  name,
  label,
  value
}: NextWebVitalsMetric) {
  if (typeof window !== 'undefined')
    window.gtag('event', name, {
      event_category: label === 'web-vital' ? 'Web Vitals' : 'Next.js custom metric',
      value: Math.round(name === 'CLS' ? value * 1000 : value), // values must be integers
      event_label: id, // id unique to current page load
      non_interaction: true // avoids affecting bounce rate.
    } as Gtag.EventParams);
}

Enter fullscreen mode Exit fullscreen mode

Recall that the Gtag.EventParams type from the @types/gtag.js package is globally available for consumption with 0 imports required -- configured in a root index.d.ts file as follows:

index.d.ts
/// <reference types="gtag.js" />

declare module 'gtag.js';
Enter fullscreen mode Exit fullscreen mode

That's it for now my fellow TypeScript fiends, but expect more strongly typed Nextjs articles to come! Feedback on topics you'd like to see covered with Next + TypeScript would be appreciated -- anything from graphql-codegen to apollo config to api routes to flawlessly typing dynamic routes with fallback set to true in a headless context and so on and so forth.

hate-surprises

Cheers

Discussion (1)

Collapse
drago profile image
Drago

Good article mate, set a GA in typescript is a pain, this articles answering a lot. One question, is there are any way to test GA in local environment or must be deployed to hosting first. This is cumbersome to deploy the code which possibly doesn't ready for production!