DEV Community

John  Ajera
John Ajera

Posted on

Set up Google Analytics 4 on a static site (gtag, CSP, and Amplify)

Set up Google Analytics 4 on a static site (gtag, CSP, and Amplify)

You want page views on a static personal site (for example an Amplify-hosted home page). Google Analytics 4 (GA4) is free and not a Google Cloud product - you sign in with a normal Google account, create an Analytics account and property, then put a Measurement ID (G-...) on the site.

This guide walks through console setup, a complete one-page smoke test you can deploy as-is, and the Amplify Content-Security-Policy gotcha that blocks fonts, theme toggles, and Analytics if you forget it.

GA4 request path: browser to CloudFront to Amplify to Google Analytics

Icons for Amplify and CloudFront come from AWS Icons (AWS Architecture Icons; use per AWS Trademark Guidelines). GA4 is not a Google Cloud product, so there is no matching icon in GCP Icons - the last hop is a plain GA4 label.


1. Overview

  • Create a GA4 account (label only) and property for your site.
  • Choose data-sharing options (optional extras for Google).
  • Pick business objective Understand web and/or app traffic.
  • Add a Web data stream and copy the Measurement ID.
  • Deploy a full HTML smoke-test page with gtag (then wire production the same way).
  • Allow Google Tag Manager, Analytics, and (if used) Google Fonts in your CSP.
  • Confirm with GA Realtime or the tag Retest UI.

2. Prerequisites

  • A public HTTPS host you control (this walkthrough uses AWS Amplify Hosting).
  • A Google account with access to Google Analytics.
  • Ability to publish a static HTML file and edit Amplify custom headers / CSP.
  • Optional: Terraform or Console access if Amplify headers are managed as infra.

3. Create the GA4 account and property

  1. Open Google AnalyticsAdminCreateAccount (or start the guided setup).
  2. Account name is only a label (for example johna.kiwi or your name). It is not a GCP project ID.
  3. Create a property for the site (property name can match the domain).
  4. Complete business details as prompted.
  Google Analytics hierarchy
  -------------------------
  Account          "johna.kiwi"          <-- folder / org label
    Property       "johna.kiwi"          <-- one site (or app)
      Data stream  Web / johna.kiwi      <-- where G-XXXXXXXX lives
Enter fullscreen mode Exit fullscreen mode

GA sits next to Ads / Tag Manager / Search Console. You do not need a Google Cloud billing account for basic page-view tracking.


4. Account data sharing settings

You will see checkboxes such as:

Setting Typical choice for a personal site
Google products and services Off
Modeling contributions and business insights Optional (on or off)
Technical support Off unless you need Google support access
Recommendations for your business Off

Turning all of them off still allows basic hits, pages, and referrers. These controls are extra sharing with Google, not a requirement to collect page views.


5. Business objectives

Select Understand web and/or app traffic.

Skip leads, sales, and engagement unless you care about those recommended report packs. You can change suggestions later; page views still work either way.

Then Create.


6. Start collecting data: Web stream

  1. Choose platform Web (not Android / iOS).
  2. Website URL: your apex, for example https://johna.kiwi.
  3. Stream name: something clear, for example johna.kiwi.
  4. Create the stream and copy the Measurement ID (G-XXXXXXXX).
  Stream URL says:     https://johna.kiwi
  Hits actually come:  wherever this exact G-XXXXXXXX script runs

  johna.kiwi          [tag]  --> counted
  staging.johna.kiwi  [   ]  --> not counted (unless you add the tag)
  labs / guides       [   ]  --> not counted (unless you add the tag)
Enter fullscreen mode Exit fullscreen mode

Subdomains are not auto-tracked from DNS alone. Same Measurement ID on each host = one property; different IDs = separate properties.


7. Smoke-test page (full HTML)

Do not start with a half snippet in an existing theme. Save this as index.html, replace G-XXXXXXXX, and deploy it (Amplify, S3 website, GitHub Pages, or any static host). Then open the live URL and check Realtime.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>GA4 smoke test</title>
    <meta name="description" content="Minimal page to verify Google Analytics 4 gtag.">
    <!-- Google tag (gtag.js) -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag() {
        dataLayer.push(arguments);
      }
      gtag("js", new Date());
      gtag("config", "G-XXXXXXXX");
    </script>
    <style>
      body {
        font-family: system-ui, sans-serif;
        max-width: 40rem;
        margin: 3rem auto;
        padding: 0 1rem;
        line-height: 1.5;
      }
    </style>
  </head>
  <body>
    <h1>GA4 smoke test</h1>
    <p>
      If this page is public over HTTPS and CSP allows
      <code>googletagmanager.com</code>, a hit should appear in
      Analytics Realtime within a minute.
    </p>
    <p>Measurement ID on this page: <code>G-XXXXXXXX</code></p>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

That is enough to prove the tag. After it works, copy the same <script> block into your real site's <head> (and 404.html if you want those hits).

7.1 Amplify: inject only on production

For a real app like johna.kiwi, keep the smoke-test pattern in spirit: put gtag in HTML at build time from an env var so staging stays clean.

  Amplify main branch env
  -----------------------
  GA_MEASUREMENT_ID = G-XXXXXXXX
  SITE_ENV          = production

  build step
  ----------
  if GA_MEASUREMENT_ID set  -->  write gtag into index.html / 404.html
  else                      -->  leave markers empty (staging / PR)
Enter fullscreen mode Exit fullscreen mode

Example branch env:

GA_MEASUREMENT_ID=G-XXXXXXXX
SITE_ENV=production
Enter fullscreen mode Exit fullscreen mode

Local check:

GA_MEASUREMENT_ID=G-XXXXXXXX npm run build
# confirm index.html / 404.html contain gtag and your G- id
Enter fullscreen mode Exit fullscreen mode

Unset the var and rebuild to clear markers for local preview.


8. Content-Security-Policy (the usual failure mode)

The HTML can be perfect and GA still says tag wasn't detected. Strict Amplify CSP is usually why - and it also breaks Google Fonts and inline theme scripts.

CSP compare: strict policy blocks gtag versus allow-list policy that works

If your Amplify header still looks like this:

default-src 'self'; style-src 'self'; font-src 'self'; ...
Enter fullscreen mode Exit fullscreen mode

you will hit exactly the left-hand failures above. Use an allow-list closer to this (adjust to your needs):

default-src 'self';
script-src 'self' 'unsafe-inline' https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://www.google-analytics.com https://www.googletagmanager.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://www.googletagmanager.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests
Enter fullscreen mode Exit fullscreen mode

Update Amplify App settings → Custom headers, then redeploy the branch so CloudFront picks up the new policy. Confirm with:

curl -sI https://your.domain/ | grep -i content-security-policy
Enter fullscreen mode Exit fullscreen mode

If you manage Amplify with Terraform, do not put custom_headers in lifecycle.ignore_changes unless you intentionally manage CSP only in the Console. Ignoring headers for "YAML vs JSON drift" means CSP fixes never apply via CI/CD.


9. Verify

  1. Hard-refresh the live site (cache can keep an old CSP).
  2. DevTools → Network: gtag/js?id=G-... should be 200, not blocked (blocked requests often show provisional headers).
  3. Analytics → Reports → Realtime, or the setup Retest button for your domain.
  4. If you use a theme toggle in inline JS, it should work once script-src allows 'unsafe-inline' (or move scripts to same-origin .js files).

It can take a short time for the first hit to show; Realtime is the quick check.


10. Summary: copy-paste

1. Measurement ID (Amplify main env):

GA_MEASUREMENT_ID=G-XXXXXXXX
Enter fullscreen mode Exit fullscreen mode

2. Full smoke-test page: use the index.html in section 7 (replace every G-XXXXXXXX).

3. Confirm CSP after deploy:

curl -sI https://your.domain/ | grep -i content-security-policy
Enter fullscreen mode Exit fullscreen mode

4. Optional: gtag-only fragment for an existing layout's <head>:

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

11. Troubleshooting

Symptom Likely cause Fix
GA: tag wasn't detected CSP blocks googletagmanager or inline script Loosen CSP; redeploy; hard-refresh
Fonts fail in Network CSP style-src / font-src self-only Allow fonts.googleapis.com and fonts.gstatic.com
Theme / dark mode toggle dead Inline script blocked by CSP Allow 'unsafe-inline' under script-src, or use an external JS file
Tag in HTML but Retest fails Old CSP still on CloudFront Redeploy branch after header change; wait for cache miss
Staging shows no gtag Env only on main Expected if you inject only when GA_MEASUREMENT_ID is set
Subdomain traffic missing No tag on that host Add the same Measurement ID to that site's HTML
Smoke page works, real site fails Real site CSP stricter than smoke host Align custom headers; compare curl -sI on both

12. References

Top comments (0)