Set up Google Analytics 4
A measurement id firing on your site, data visible in Realtime, and a numeric property id plus read access for anything that reads the property over the API — anacraft included. Every step below happens in Google's console, not in anacraft.
Working in Claude Code? The repo ships this guide as a skill: clone it and ask to configure Google Analytics, and Claude walks the steps with you.
01Create the account and property
Everything starts at analytics.google.com under Admin, the gear at the bottom left. GA4 nests Account → Property → Data stream; knowing which level a setting lives on saves most of the hunting.
An account is an organisation-level container — one is usually enough. Under it, Admin → Property → + Create → Property:
- Property name — name the site, not the company, if more will follow.
- Reporting time zone — this sets the day boundary for every report you will ever run. Changing it later does not restate history, so get it right now.
- Currency — same warning.
The business details and objectives screens only shape which reports appear by default. Nothing there is load-bearing.
02Add a web data stream
Admin → Data collection and modification → Data streams → Add stream → Web. Give it the
canonical origin, https:// included, and leave Enhanced measurement on — it collects
page views, scrolls, outbound clicks, site search, video engagement, file downloads and form
interactions without a line of code.
The stream page then shows your Measurement ID, G-XXXXXXXXXX. That is what the tag
needs. It is not the property id, which is the single most common mix-up when wiring up a tool —
see step 06.
03Install the tag
The cheapest correct install is the gtag.js snippet, in <head>, on every page:
<!-- Google tag (gtag.js) --> <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>
Two rules prevent most broken installs: paste the id in both places, and keep it to
one GA4 tag per page — a second config for the same id double-counts every view. If
Google Tag Manager is already on the site, configure GA4 inside GTM instead of adding this snippet, and
remember to publish the container.
Single-page apps need one addition. The snippet fires a pageview on load, and client-side navigation changes the URL without a reload, so every later route goes uncounted until you send it yourself:
// on every route change
gtag('event', 'page_view', {
page_path: location.pathname + location.search,
page_location: location.href,
page_title: document.title,
});
On Next.js, @next/third-parties handles both the placement and the route changes. On
WordPress, Site Kit or any GA4 plugin does; editing header.php works too, but only from a
child theme or the next update erases it.
If your site serves the EEA or UK, set Consent Mode v2 defaults before the tag loads and update them when the visitor chooses. A consent platform emits those calls for you; ordering is the whole game.
Or sidestep the banner entirely with gtag('config', 'G-XXXXXXXXXX', { client_storage: 'none' }).
GA4 then writes no cookie, so there is no device storage to ask consent for. You lose returning-visitor
and multi-day attribution — every visit counts as new — and keep page views, countries, referrers and
Realtime. This site does exactly that, which is why you are reading it without dismissing a banner.
Do not combine the two: an analytics_storage: 'denied' default downgrades every hit to a
consent ping that GA4 reports only through modelling, so a small site sees less data, not more.
04Verify data is arriving
In this order, because each rules out a different failure.
debug_mode: true in the config call.05Change these before you forget
Data retention. Admin → Data collection and modification → Data retention. Event data defaults to 2 months; set 14 months unless policy says otherwise. It governs explorations and funnels rather than the standard reports, and it is not retroactive — data already aged out is gone.
Internal traffic. Admin → Data streams → your stream → Configure tag settings → Define internal traffic, then Admin → Data filters. A new filter is created in Testing, where it does nothing; switch it to Active once you have confirmed it tags your own visits correctly.
Unwanted referrals. Same tag settings menu — add a payment or auth host such as Stripe or PayPal, which would otherwise break session attribution and inflate direct traffic.
Cross-domain measurement belongs there too, if one journey spans two domains. Google Signals, under Data collection, adds demographics and cross-device reporting at the cost of more thresholded rows — an opt-in decision, not a default.
06Key events, and the id that tools want
Key events are what GA4 called conversions before 2024. Mark one at Admin → Events with the Mark as
key event toggle, or create it by name at Admin → Key events if it has not fired yet. For a click or
form submit that enhanced measurement misses, send gtag('event', 'signup_complete') and mark
it once it shows up, which can take a day.
Your property id is at Admin → Property → Property details, top right: a bare number like
397412345. Three ids get confused constantly, and only one of them belongs in an API call.
| Value | Looks like | Belongs in |
|---|---|---|
| Property id | 397412345 | The Data and Admin APIs, and any reporting tool — craft use 397412345 |
| Measurement id | G-XXXXXXXXXX | The on-page tag, and nowhere else |
| Stream id | 1234567890 | Stream-level admin calls only |
07Grant access
Admin → Property access management → +, or Account access management to cover every property at once. Least privilege first:
| Role | Can |
|---|---|
| Viewer | See reports and explorations. The right role for a dashboard or a read-only API client. |
| Analyst | Viewer, plus create and edit shared explorations and audiences. |
| Editor | Analyst, plus edit property settings, streams and events. |
| Administrator | Editor, plus manage users. Keep this to one or two people. |
The No Cost Metrics and No Revenue Metrics restrictions apply on top of the role. People are added by email and see the property on their next sign-in.
08API access, when a tool needs it
Three things have to line up, and each missing one produces its own error.
- A Google Cloud project with both the Google Analytics Data API and Admin API enabled. A tool that fetches reports but cannot list properties has the first on and the second off.
- Credentials: an OAuth client of type Desktop app for a CLI that signs a person in, or a
service account for unattended and server-side use. The read-only scope
analytics.readonlyis enough for anything that only reports. - For a service account, its
…iam.gserviceaccount.comaddress added under Property access management as Viewer. Creating the account grants it nothing by itself, and this is the step people skip.
The Cloud project carries only the API enablement and its own quota — it does not own the Analytics data and need not sit in the same organisation. Running your own project is also how you stop sharing a rate limit with everyone else using the same tool.
Many tools ship their own OAuth client and need none of this. anacraft does:
craft login works with no Google Cloud project at all.
09When it is not working
| Symptom | Usual cause |
|---|---|
| Realtime empty | Tag not in the served HTML, wrong id, or an ad blocker, tracking prevention or CSP dropping the request. Check DevTools → Network for collect. |
| Empty only for you | An active internal-traffic filter is removing exactly the visits you are testing with. |
| Realtime fine, reports empty | The 24–48 hour processing delay, or a date range that excludes today. |
| Every metric doubled | Two GA4 tags on the page — typically gtag.js and GTM both firing. |
| Direct traffic inflated | A payment or auth host counted as a referrer. Add it to unwanted referrals. |
| Explorations missing old data | Data retention still at the 2-month default. |
| API 403 “has not been used” | The Data or Admin API is not enabled on that Cloud project. |
| API 403 permission denied | The account or service account lacks Viewer on that property. |
API 404 on a G- id | Measurement id used where the numeric property id belongs. |
| API 429 | Quota. Poll less often, or move to your own Cloud project. |
10Point anacraft at it
With the property collecting data and your Google account holding at least Viewer on it, three commands connect the dashboard:
# sign in — read-only, official builds carry their own OAuth client $ craft login # every GA4 property this account can read $ craft props # save one as the default, by numeric property id $ craft use 397412345
Then craft opens the dashboard on real numbers, and craft live is the
fastest confirmation that your tag is still firing. The
quick start covers the rest of the commands.