Implementing Google Tag Manager with CSP in Next.js 13/14
Creating Tag for Google Analytics 4 in GTM
1. Get GA4 Measurement ID
- Click on the "Admin" gear icon located in the lower left corner of the screen
- Click "Data Streams" under the "Property" settings
- Select specific web or app data stream from the list that you wand work with
- Copy Measurement ID(G-XXXX)
2. Create tag for GA4
- Click on "Tags" in left menu
- Click the "New" button at the top right
- Fill in the title at the top left
- Tag Configuration: Google Analytics - Google Tag - Enter the measurement ID in the Tag ID field
- Choose Trigger
3. Preview and Testing Connection
- Click the "Preview" button at the top right
- Enter the URL in the URL field
- Click the "Connect" button at center of modal
- Check whether the Tags fired and tag assistant connected
4. Check whether the GA4 receive Data using Debugview
- Enter debugview in the search bar
- Check wheter events are recorded by time zone
When the events recorded in debugview?
In debugview in GA4, only the events triggered from devices or browsers where debug mode is specifically enabled are recorded. This means that the normal interactions form regular users who are not in debug mode will not appear in debugview
Setting CSP Headers
In Next.js the middleware.js will be invoked for every route in your project. Therefore, setting CSP headers in middleware.js is reasonable choice.
Here are the key points of setting CSP headers
- Generate nonce dynamically for each request
- In development the eval() function may cause error
- Ensure the nonce is transmitted back to the client(e.g. using 'x-nonce' header)
1import { NextResponse, NextRequest } from "next/server";
2
3const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
4const cspOptions = `
5 default-src 'self';
6 script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${process.env.NODE_ENV === "production" ? "": `'unsafe-eval'`};
7 style-src 'self' https://www.googletagmanager.com https://fonts.googleapis.com 'unsafe-inline';
8 img-src 'self' https://www.google-analytics.com https://www.googletagmanager.com https://fonts.gstatic.com data:;
9 font-src 'self' https://fonts.gstatic.com;
10 form-action 'self' https://accounts.google.com;
11 connect-src 'self' https://www.google-analytics.com;
12`;
13
14const headerOptions = {
15 "x-nonce": nonce,
16 "Content-Security-Policy": cspOptions.replace(/\s{2,}/g, " ").trim(),
17};
18
19export async function middleware(request) {
20 const response = NextResponse.next();
21 Object.entries(headerOptions).forEach(([key, value]) => {
22 response.headers.set(key, value);
23 });
24
25 return response;
26}
27
28export const config = {
29 matcher: "/:path*",
30};
Installing GTM to Project Using Script Tag
In this section, we'll proceed with the assumption that you already have the Measurement ID for Google Analytics 4(GA4) and the ID for Google Tag Manager(GTM)
1. Create GTM component
First, create component for GTM using <Script> tag and set nonce attribute to it.
1"use client";
2
3import Script from "next/script";
4import { pageview } from "./pageview.js";
5import { usePathname, useSearchParams } from "next/navigation";
6import { useEffect } from "react";
7
8export default function Analytics({ nonce }) { //Receive nonce for CSP
9 const pathname = usePathname();
10 const searchParams = useSearchParams();
11
12 useEffect(() => {
13 if (pathname) {
14 pageview(pathname); //This function made for tracking page views in Google Analytics
15 }
16 }, [pathname, searchParams]);
17
18 if (process.env.NODE_ENV === "development") {
19 return null;
20 }
21
22 return (
23 <>
24 <noscript>
25 <iframe
26 src={`https://www.googletagmanager.com/ns.html?id=${process.env.NEXT_PUBLIC_GTM_ID}`}
27 height="0"
28 width="0"
29 style={{ display: "none", visibility: "hidden" }}
30 />
31 </noscript>
32 <Script
33 id="gtm-script"
34 nonce={nonce} //set nonce attrubute
35 strategy="afterInteractive"
36 dangerouslySetInnerHTML={{
37 __html: `
38 (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
39 new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
40 j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
41 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
42 })(window,document,'script','dataLayer', "${process.env.NEXT_PUBLIC_GTM_ID}");
43 `,
44 }}
45 />46
47 );
48
The nonce value may be hidden in DevTools. In that case, check the properties tag of the script tag.
4. Create function for tracking page view
The pageview function is responsible for tracking page views in GA4.
1export const pageview = (url) => {
2 if (typeof window.dataLayer !== "undefined") {
3 //for production
4 window.dataLayer.push({
5 event: "pageview",
6 page: url,
7 });
8 } else {
9 //for development
10 console.log({
11 event: "pageview",
12 page: url,
13 });
14 }
15};
5. Import the component to root layout
Place the gtm component right before the </body> tag, and wrap it with a <Suspense> component to prevent the entire component tree from being rendered dynamically
1import Analytics from "@comps/gtm/gtm";
2import { headers } from "next/headers";
3import { Suspense } from "react";
4import "./globals.css";
5
6
7export default function RootLayout({ children }) {
8 const nonce = headers().get("x-nonce"); //Get nonce from header
9
10 return (
11 <html lang="en">
12 <body className={inter.className}>
13 <Suspense>
14 <Analytics nonce={nonce} />
15 </Suspense>
16 </body>
17 </html>
18 );
19}
20