GA4 Analytics skill for Codex
Events that fire once and match your backend. Codex implements GA4 from a tracking plan rather than ad hoc snippets: consent defaults before any tag, recommended event names and ecommerce parameters, outcome-based triggers, key events and custom dimensions registered in admin. It hunts down double installs and duplicate SPA page views, sends server-only events through the Measurement Protocol, and reconciles purchase counts with the order database.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- ga4-analytics Installs to ~/.agents/skills/ga4-analytics/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install ga4-analytics The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project ga4-analytics Installs to .agents/skills/ga4-analytics/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/ga4-analytics.zipunzip -o ga4-analytics.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) ga4-analytics PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $ga4-analytics, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Implements and audits Google Analytics 4 — gtag.js or Google Tag Manager dataLayer events, recommended event names and parameters, ecommerce purchase tracking, key events, custom dimensions, Consent Mode v2, SPA page views, cross-domain and referral exclusions, Measurement Protocol for server events, DebugView verification, and BigQuery export. Use when adding GA4 to a site or app, events are missing or duplicated, conversions do not match the backend, consent banners need wiring, or someone asks for a GA4 report or exploration.
Use it when
- Installing GA4 or migrating tracking to Google Tag Manager
- Events or conversions are missing, duplicated or do not match the backend
- Wiring a consent banner to Consent Mode v2
- Server-side events need to reach GA4
Use something else when
- You are designing the event taxonomy itself: use analytics-tracking-plan
- The product uses PostHog for product analytics: use posthog-analytics
How to check the result
Walk every tracking-plan row in DebugView, confirm consent denial sets no _ga cookie, and reconcile seven days of purchases against the order database.
The mistake it prevents
Sending manual page_view events on SPA route changes while enhanced measurement already tracks history changes, doubling every page view.
Done well: every event in GA4 maps to a line in a written tracking plan, uses recommended names where they exist, fires exactly once per user action, respects the user’s consent choice, and key event counts reconcile with the backend within an agreed tolerance.
Before writing code
- Read
AGENTS.md, then find existing tracking:grep -rnE "gtag\(|dataLayer|googletagmanager|G-[A-Z0-9]{6,}|GTM-[A-Z0-9]+" --exclude-dir=node_modules .Two tag installs (hard-coded gtag plus GTM, or GA4 via a CMS plugin plus code) is the most common cause of doubled page views. - Find the consent management platform (CMP) and how it exposes choices.
- Decide gtag.js vs GTM: GTM if marketers manage tags; gtag.js directly if engineering
owns everything. In GTM, the code only pushes to
dataLayer; tags live in the container. - Get or write the tracking plan (event taxonomy design belongs to
analytics-tracking-plan). Each row: event name, trigger, parameters with types, key event yes/no, owner.
Workflow
- [ ] 1. Base install with Consent Mode v2 defaults before any tag
- [ ] 2. Page views (correct for SPAs)
- [ ] 3. Events from the tracking plan, recommended names first
- [ ] 4. Ecommerce and key events
- [ ] 5. Register custom dimensions; admin settings
- [ ] 6. Server-side events via Measurement Protocol (if needed)
- [ ] 7. Verify in DebugView and reconcile with backend
1. Base install with consent defaults
Consent defaults must run before the GA tag loads:
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
wait_for_update: 500
});
</script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
When the user chooses, the CMP (or your code) calls:
gtag('consent', 'update', { analytics_storage: 'granted', ad_storage: 'denied',
ad_user_data: 'denied', ad_personalization: 'denied' });
- Consent Mode v2 (
ad_user_data,ad_personalization) is required by Google for ads features with EEA/UK users. Defaults can differ by region with theregionkey. - Basic mode: tags do not load until consent. Advanced mode: tags load and send cookieless pings when denied, enabling behavioral modeling once traffic thresholds are met. Which mode is lawful is a legal decision for the site owner; implement what they choose and do not decide it for them.
- Persist the choice and re-apply
updateon every page load before events fire.
2. Page views
- Enhanced measurement sends
page_viewon load and, if “page changes based on browser history events” is enabled, on SPA route changes. Do not also send manualpage_viewon route change, or every view counts twice. - If you disable history-based tracking, send manually after the new title is set:
gtag('event', 'page_view', { page_location: location.href, page_title: document.title });
3. Events
Use recommended event names and parameters where they fit; GA4 reports and integrations understand them:
| Action | Event | Key parameters |
|---|---|---|
| Account created | sign_up | method |
| Logged in | login | method |
| Lead form submitted | generate_lead | value, currency, lead_source (custom) |
| Site search | search | search_term |
| Product viewed | view_item | currency, value, items |
| Added to cart | add_to_cart | currency, value, items |
| Checkout started | begin_checkout | currency, value, items, coupon |
| Purchase | purchase | transaction_id, value, currency, tax, shipping, items |
Custom events: snake_case, verb_object (invite_teammate, export_report). Limits:
names ≤ 40 characters starting with a letter; ≤ 25 parameters per event; parameter names
≤ 40 characters and values ≤ 100 characters; reserved prefixes google_, ga_,
firebase_ are not allowed. Never send PII (emails, names, phone numbers) in any event,
parameter or URL; it breaks Google’s terms and privacy law.
Fire on the confirmed outcome, not the click: sign_up after the API returns success,
not when the button is pressed.
With GTM, push to the dataLayer and configure GA4 event tags on custom event triggers:
window.dataLayer.push({ ecommerce: null }); // clear previous ecommerce object
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: order.id, value: order.total, currency: order.currency,
tax: order.tax, shipping: order.shipping,
items: order.lines.map(l => ({ item_id: l.sku, item_name: l.name, price: l.unitPrice, quantity: l.qty }))
}
});
4. Ecommerce and key events
purchaseneeds a uniquetransaction_id; GA4 deduplicates purchases with the same ID for the same user, which protects against thank-you page reloads only if the ID is always sent.valueis a number (no currency symbol),currencyan ISO 4217 code; both required for revenue.- Mark business outcomes as key events in Admin → Events (key events replaced “conversions” in GA4 in 2024; Google Ads still imports them as conversions).
5. Admin settings that matter
- Data retention: set to 14 months (default is 2) for explorations; not retroactive.
- Register custom parameters as custom dimensions/metrics (Admin → Custom definitions) or they will not appear in reports; also not retroactive. Standard properties allow 50 event-scoped and 25 user-scoped custom dimensions.
- Define internal traffic and activate the filter; list unwanted referrals (payment providers, auth domains) so sessions are not restarted as referrals.
- Cross-domain measurement for multi-domain journeys (Admin → Data streams → Configure tag settings → Configure your domains).
- Custom channel group for AI assistants (source matches
a regex such as
chatgpt|perplexity|gemini|copilot, extended as new assistants appear) so they do not sit in Referral. - Link BigQuery export for raw event access (free, daily export limits apply to standard properties).
6. Measurement Protocol for server events
For events only the server knows (subscription renewals, refunds, offline conversions):
curl -s -X POST "https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=$GA_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{"client_id":"123456.7654321","events":[{"name":"purchase","params":{"transaction_id":"R-1001","value":49.0,"currency":"USD","session_id":"1730000000","engagement_time_msec":1}}]}'
client_idmust be the browser’s GA client ID (from the_gacookie, stored with the order) to join the user’s journey;user_idif you use User-ID. Respect consent: do not send for users who denied analytics.- Validate first against
https://www.google-analytics.com/debug/mp/collect, which returns validation messages (the real endpoint always returns 2xx). Keep the API secret server-side. Network access is required; ask before sending to a production property.
7. Verify
- DebugView: enable with
gtag('config', 'G-XXXXXXXXXX', { debug_mode: true })on a dev build, the Tag Assistant / GTM Preview, or the GA debugger extension. Walk each tracking-plan row and confirm event name, parameters and that it fires once. - Browser devtools: filter network by
collect?v=2; checken=(event name) andgcs=/gcd=(consent state) values. - Test consent: with denial, no
_gacookie is set; after granting, events and cookies appear. - Reconcile: compare GA4
purchasecount and revenue with the order database for the same days and timezone (property timezone). 5–10% under-count is normal with ad blockers and consent denial; over-count means duplicates.
Gotchas
- Reports have processing delays of up to about a day; Realtime and DebugView are not the same as finished reports.
- Thresholding hides rows with small user counts when Google signals is on; switch reporting identity to device-based to inspect.
(not set)landing pages often come from sessions started by events without apage_view, e.g. Measurement Protocol events withoutsession_id.- UTM-less links in emails and apps land in Direct or Unassigned; see
utm-attribution. - A SPA that changes the URL before updating
document.titlerecords the previous page’s title.
Output
**Install:** gtag via app/layout.tsx; duplicate WordPress plugin tag removed.
**Consent:** v2 defaults denied (EEA+UK region), CMP updates via onConsentChanged.
**Events:** 11 plan rows implemented (table with name, trigger file:line, params).
**Key events:** sign_up, generate_lead, purchase. Custom dims: plan_tier, lead_source.
**Verified:** DebugView walkthrough of all 11; purchases 7 days: GA4 412 vs DB 441 (-6.6%).
**Open:** server-side refund events via Measurement Protocol (needs API secret). What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "GA4 Analytics"
short_description: "Events that fire once and match your backend"
default_prompt: "$ga4-analytics GA4 shows twice as many page views as our server logs, find out why" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.