Growth & Marketing AI

Adding a Google Tag (GA4) to Your Site

Result:

The Google tag renders in the built HTML of every page after one edit to the site's shared layout, backed by a live GA4 property, using the same script-in-the-head pattern this cookbook already runs for PostHog.

Part of a series: this is the Google Analytics counterpart promised in Tracking Who’s Visiting Your Site, which covers the same setup for PostHog.

Shortcut

Paste this into Claude Code, pointed at your own site’s repo, once you have the Measurement ID below.

Requires manual input

A Google Analytics account and a GA4 property, which gives you a Measurement ID (the G-XXXXXXXXXX string). That happens inside an account only you can log into. Step 1 below covers where to find it.

Add Google's gtag.js tag for GA4 to my site's shared layout:

1. In the file that renders my <head> on every page, add the standard
   Google tag snippet: an async script tag loading
   https://www.googletagmanager.com/gtag/js?id=[my Measurement ID], plus
   an inline script that initializes window.dataLayer, defines gtag() to
   push onto it, and calls gtag('js', new Date()) and
   gtag('config', '[my Measurement ID]').
2. Confirm my build command still completes cleanly and the Measurement
   ID appears in the built HTML output.

Problem#

Tracking Who’s Visiting Your Site covers the general problem: an attribution model, a growth dashboard, or even a simple before/after check on a change all need one input underneath them, real traffic data, and that recipe walks through PostHog as one way to get it. Google Analytics, specifically GA4, the current version, is the other default, still the most widely deployed web analytics tool by a wide margin, and free at any volume that matters to an individual site or small team.

Past the shared job of counting a pageview, the two aren’t interchangeable. They store data differently, they report on it differently, and they resolve an anonymous visitor into a known person differently. Here’s the comparison, before the setup steps for GA4 specifically.

GA4 vs. PostHog, at a glance#

Google Analytics (GA4)PostHog
Free tierUnlimited events; standard reports start sampling past roughly 10M events processed in the date range you’re viewing1M events/month, unsampled
Data hostedGoogle’s infrastructure onlyPostHog Cloud (US or EU region) or self-hosted
Core productAcquisition and channel reporting, tightly integrated with Google Ads and Search ConsoleProduct analytics, session replay, feature flags, and experiments in one suite
SetupA snippet in your <head>, configured from analytics.google.comAn npm package (posthog-js), configured in code
Identity modelPer-device Client ID by default; User-ID and Google Signals are additive, opt-in layersAnonymous distinct_id by default; identify() resolves it to a full person profile
Rage clicksNot trackedAutocaptured — flags repeated rapid clicks on the same element as a frustration signal

Identity resolution: GA4#

By default, GA4 identifies a visitor by Client ID, a random value gtag.js generates and stores in a first-party cookie (_ga), scoped to one browser on one device. Clear that cookie, or open the site in a second browser or on a phone, and GA4 counts a new visitor, even though it’s the same person.

Two additive layers pull that back together. User-ID is a non-PII identifier your own system already has, a database primary key rather than an email, that you pass once you know it, typically at login. It has to be unique, persistent, and under 256 characters, and once it’s flowing, GA4 de-duplicates that person’s sessions across devices in reporting. Google Signals is the second layer: an opt-in setting that lets Google use its own signed-in-account data to associate multiple devices with one real person, when that person is signed into a Google account with Ads Personalization on. It only surfaces as aggregate cross-device reporting and remarketing audiences, never a lookup you can run on an individual visitor, and as of February 2024 it no longer even factors into GA4’s reporting-identity priority order (User-ID, then Device ID, then modeled data).

What GA4 doesn’t give you is an arbitrary profile to query. Its equivalent of a person property, called a user property, is capped at 25 custom entries per standard property, each name under 24 characters and value under 36, a limit that doesn’t free up even after you archive one.

Identity resolution: PostHog#

PostHog starts anonymous the same way, an auto-generated distinct_id in a cookie, but turning that into a known person is an explicit call in your own code: identify(), run at the exact moment you have a real value, a login or a form submission. Turning a Form Submission Into a Real Person in PostHog is this cookbook’s own recipe for that moment. Once called, PostHog merges every event that visitor generated before identifying with everything after, under one person, and lets you attach person properties, arbitrary key-value traits like plan tier or lifetime value, that update over time and stay directly queryable, with no 25-property ceiling. A parallel group() call resolves identity one level up, at the account or company level, for products where the unit that matters is the customer, not the individual visitor.

Pattern#

  1. Create a GA4 property. In analytics.google.com, use Admin to create a property, then add a web data stream for your domain. GA4 hands you a Measurement ID (G-XXXXXXXXXX) and, under “View tag instructions,” the exact snippet below with your ID already filled in.

  2. Add the snippet to your site’s shared layout, once. Whatever file renders your <head> on every page gets an async script tag plus a small inline script:

    <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag() { dataLayer.push(arguments); }
      gtag('js', new Date());
      gtag('config', 'G-XXXXXXXXXX');
    </script>

    A TypeScript project needs the callback typed as a variadic function (function gtag(...args: unknown[])) instead of relying on the untyped arguments object; the behavior is identical.

  3. Confirm the build. Run your framework’s build command and check the output for your Measurement ID or the string googletagmanager, confirming the script landed in the shipped HTML rather than sitting unused in a source file.

  4. Deploy, then confirm a real event. Visit your live site, then open GA4’s Reports, Realtime view, and look for your own pageview. That’s the same confirmation step PostHog’s Activity view serves in the companion recipe: nothing downstream is trustworthy until one real event shows up.

Output#

This is what’s now in BaseLayout.astro on this site’s own repo, next to the existing PostHog init, confirmed to render in the built HTML of every page. Once that change is live, the same Measurement ID will start showing real pageviews in GA4’s Realtime report, the way PostHog’s Activity view already does for this site, without needing a second tracking library or a different place in the codebase to maintain it.

Running the pattern above gets you GA4’s acquisition and channel reporting, out of the box, alongside whatever PostHog already tracks. What it doesn’t get you automatically is cross-device identity: that’s still a separate step, wiring up User-ID at login, covered in the Variations below.

Variations#

  • Gate it behind a build-time environment variable. This cookbook’s PostHog recipe keeps its script out of local development and unconfigured preview builds by checking an environment variable before initializing. The Measurement ID above isn’t a secret, it’s visible in any page’s source anyway, but gating it the same way keeps dev and preview traffic out of your real GA4 property’s numbers.
  • Turn on User-ID once you have logins. The setup above only wires up the anonymous Client-ID layer. Once your site has real accounts, passing a user_id at login turns on the cross-device de-duplication described in the identity section above.
  • Run both. Nothing about wiring one tag into a shared layout’s <head> conflicts with wiring in the other. A common split is GA4 for acquisition and channel reporting, given how tightly it integrates with Google Ads and Search Console, alongside PostHog for identity-linked product analytics, session replay, and feature flags.

Nathaniel Stich writes Exit Velocities — recipes drawn from AI/growth systems shipped in production, not opinion pieces.

About the author