Skip to content

Commit f3c9982

Browse files
authored
feat(analytics): add client-side gtag.js to root layout (#60)
Render the GA4 gtag.js snippet from app/layout.tsx so page views and client interactions land in the same GA4 property the server-side Measurement Protocol code already writes to. Complements the server-side trackEvent path in lib/analytics.ts — together they cover both surface areas (page views from the client, airdrop outcomes from the API route) without double-counting. Uses next/script with strategy="afterInteractive" rather than raw <script> tags so Next.js manages load timing and dedup across navigations. Driven by NEXT_PUBLIC_GA4_MEASUREMENT_ID. When the var is unset (e.g. local dev without analytics, or any environment we don't want tracking on), the GA4 <Script> tags don't render at all. The ID is public by design — every visitor receives it in the gtag.js src URL — so the NEXT_PUBLIC_ prefix is safe. Document the new env var in .env.example with a note to keep it in sync with the existing server-side GA4_MEASUREMENT_ID (same GA4 property), and add it to ProcessEnv in types.d.ts.
1 parent 3318dcf commit f3c9982

3 files changed

Lines changed: 36 additions & 0 deletions

File tree

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,11 @@ GA4_MEASUREMENT_ID=
116116
# Protocol API secrets. Leaking it lets anyone forge events into your GA4
117117
# property. Required only if GA4_MEASUREMENT_ID is set.
118118
GA4_API_SECRET=
119+
120+
# [PUBLIC] [optional] GA4 Measurement ID for client-side gtag.js page-view
121+
# tracking. Format: "G-XXXXXXXXXX". Inlined into the bundle at build time
122+
# via the NEXT_PUBLIC_ prefix — served to every visitor in the gtag.js
123+
# script src, so not sensitive. When unset, app/layout.tsx skips rendering
124+
# the GA4 <Script> tags entirely. Typically the same value as
125+
# GA4_MEASUREMENT_ID (same GA4 property) — keep them in sync.
126+
NEXT_PUBLIC_GA4_MEASUREMENT_ID=

app/layout.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from "next";
22
import { Inter } from "next/font/google";
3+
import Script from "next/script";
34
import { Header } from "@/components/header";
45
import { Footer } from "@/components/ui/footer";
56
import "./globals.css";
@@ -26,6 +27,8 @@ export default function RootLayout({
2627
}: {
2728
children: React.ReactNode;
2829
}) {
30+
const gaId = process.env.NEXT_PUBLIC_GA4_MEASUREMENT_ID;
31+
2932
return (
3033
<html lang="en">
3134
<body className={`${inter.className}`}>
@@ -36,6 +39,23 @@ export default function RootLayout({
3639
</section>
3740

3841
<Footer />
42+
43+
{gaId && (
44+
<>
45+
<Script
46+
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
47+
strategy="afterInteractive"
48+
/>
49+
<Script id="ga4-init" strategy="afterInteractive">
50+
{`
51+
window.dataLayer = window.dataLayer || [];
52+
function gtag(){dataLayer.push(arguments);}
53+
gtag('js', new Date());
54+
gtag('config', '${gaId}');
55+
`}
56+
</Script>
57+
</>
58+
)}
3959
</body>
4060
</html>
4161
);

types.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ declare namespace NodeJS {
4343
GA4_MEASUREMENT_ID: string;
4444
GA4_API_SECRET: string;
4545

46+
/**
47+
* Client-side analytics (gtag.js). Optional — the GA4 <Script>
48+
* tags in app/layout.tsx render only when this is set. Public by
49+
* design (served to every visitor in the script src), so safe to
50+
* expose with the NEXT_PUBLIC_ prefix.
51+
*/
52+
NEXT_PUBLIC_GA4_MEASUREMENT_ID: string;
53+
4654
/**
4755
* Auth related variables
4856
*/

0 commit comments

Comments
 (0)