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.
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
- Open Google Analytics → Admin → Create → Account (or start the guided setup).
-
Account name is only a label (for example
johna.kiwior your name). It is not a GCP project ID. - Create a property for the site (property name can match the domain).
- 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
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
- Choose platform Web (not Android / iOS).
-
Website URL: your apex, for example
https://johna.kiwi. -
Stream name: something clear, for example
johna.kiwi. - 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)
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>
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)
Example branch env:
GA_MEASUREMENT_ID=G-XXXXXXXX
SITE_ENV=production
Local check:
GA_MEASUREMENT_ID=G-XXXXXXXX npm run build
# confirm index.html / 404.html contain gtag and your G- id
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.
If your Amplify header still looks like this:
default-src 'self'; style-src 'self'; font-src 'self'; ...
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
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
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
- Hard-refresh the live site (cache can keep an old CSP).
- DevTools → Network:
gtag/js?id=G-...should be 200, not blocked (blocked requests often show provisional headers). - Analytics → Reports → Realtime, or the setup Retest button for your domain.
- If you use a theme toggle in inline JS, it should work once
script-srcallows'unsafe-inline'(or move scripts to same-origin.jsfiles).
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
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
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>
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 |


Top comments (0)