
Analytics Insights
- 84 installs
- 67 repo stars
- Updated August 4, 2026
- hyperfx-ai/marketing-skills
Helps with ai & agent building tasks.
About
analytics-insights is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- analytics-insights
- AI & Agent Building
- AI-coding skill
Analytics Insights by the numbers
- 84 all-time installs (skills.sh)
- +9 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #5,107 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hyperfx-ai/marketing-skills --skill analytics-insightsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 84 |
|---|---|
| repo stars | ★ 67 |
| Last updated | August 4, 2026 |
| Repository | hyperfx-ai/marketing-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Analytics Insights
Operator skill for the Google measurement stack — GA4, GTM, Search Console, and BigQuery — driven directly from chat. Build a tracking plan, run reports, mark conversions, audit existing setup, and query the warehouse without leaving the conversation.
Out of scope — defer to other skills
| Request | Send them to |
|---|---|
| Keyword research, AI-search visibility, full SEO audit | seo-research (HyperSEO toolkit — broader and richer than GSC for keyword work) |
| Google Ads campaign performance | google-ads (campaign-level) — but GA4-side conversion attribution lives here |
| Meta / Facebook ads metrics | meta-ads |
| Email program metrics | email-lifecycle (provider-side) |
GSC and HyperSEO overlap on search-performance data. Rule of thumb: use GSC here for the user's own site's impression / click / position data. Use HyperSEO (in seo-research) for keyword research, competitor data, AI-search visibility.
Requirements
- Hyper MCP installed and connected. https://app.hyperfx.ai/mcp
- At least one of these connected at https://app.hyperfx.ai/integrations:
- Google Analytics — GA4 reports, custom metrics / dimensions, key-event (conversion) management.
- Google Tag Manager — tag / trigger / variable / workspace / version management.
- Google Search Console — search-performance data, sitemaps, URL inspection.
- BigQuery — SQL queries against the GA4 export (or any other dataset).
If google_analytics_run_ga4_report, gtm_tag, google_search_console_get_performance_data, and bigquery_execute_query are all missing from the agent's tool list, stop and tell the user to enable the Hyper MCP and connect at least one of these integrations.
Tool surface
| Group | Tools |
|---|---|
| GA4 — reporting | google_analytics_run_ga4_report, google_analytics_list_accounts, google_analytics_list_properties, google_analytics_get_property |
| GA4 — properties & data streams | google_analytics_create_ga4_property, google_analytics_update_property, google_analytics_delete_property, google_analytics_create_data_stream, google_analytics_list_data_streams, google_analytics_get_data_stream, google_analytics_update_data_stream, google_analytics_delete_data_stream, google_analytics_get_data_retention_settings (read-only — no update variant in MCP), google_analytics_acknowledge_user_data_collection |
| GA4 — key events (conversions) | google_analytics_create_key_event, google_analytics_list_key_events, google_analytics_get_key_event, google_analytics_update_key_event, google_analytics_delete_key_event |
| GA4 — custom metrics / dimensions | google_analytics_create_custom_metric, google_analytics_list_custom_metrics, google_analytics_get_custom_metric, google_analytics_update_custom_metric, google_analytics_archive_custom_metric, google_analytics_create_custom_dimension, google_analytics_list_custom_dimensions, google_analytics_get_custom_dimension, google_analytics_update_custom_dimension, google_analytics_archive_custom_dimension |
GTM (note: prefixed gtm_*, not google_tag_manager_*) | gtm_account, gtm_container, gtm_workspace, gtm_tag, gtm_trigger, gtm_variable, gtm_built_in_variable, gtm_folder, gtm_environment, gtm_version, gtm_version_header, gtm_user_permission, gtm_client, gtm_template, gtm_transformation, gtm_zone, gtm_destination |
| Google Search Console | google_search_console_get_performance_data, google_search_console_list_sites, google_search_console_list_sitemaps, google_search_console_get_sitemap, google_search_console_submit_sitemap, google_search_console_delete_sitemap, google_search_console_submit_url |
| BigQuery | bigquery_execute_query, bigquery_insert_rows |
Critical rules
1. GA4 property IDs — arg name differs by tool. Three patterns: (a) Reporting tools (run_ga4_report, get_property) take property_id="properties/123456789". (b) Create and list tools (create_key_event, list_key_events, create_custom_dimension, list_custom_dimensions, etc.) take parent="properties/123456789". (c) Get/update/delete tools operate on a specific resource and take name= with the full resource path (e.g. "properties/123456789/keyEvents/12345"). All three need the properties/ prefix in some form — passing a bare numeric ID silently fails. When in doubt, check the tool's schema for which arg is marked required. 2. Date ranges are inclusive on both ends. start_date="2026-04-01" and end_date="2026-04-30" returns 30 days, not 29. Same for relative dates: 7daysAgo to today is 8 days, not 7. 3. GA4 sampling kicks in above ~10M events. For high-volume properties, the GA4 API silently samples results. If precision matters (board reporting, financial attribution), use the BigQuery GA4 export instead — see `references/bigquery-ga4-export.md`. 4. Conversions in GA4 are "key events". GA4 renamed "conversions" to "key events" in 2024. The tools reflect this — use google_analytics_create_key_event to mark an event as a conversion. Don't get confused by older docs. 5. GTM changes need a workspace + version + publish. Tags / triggers / variables created in a workspace are not live until the workspace is committed to a new version and that version is published. Use gtm_workspace → modify → gtm_version (create) → publish. 6. GSC data has a 2–3 day lag. Don't query "yesterday" in GSC and expect data — query 3+ days back for stable numbers. GA4 has a 24-48h lag for some metrics. 7. Apple Mail Privacy Protection inflates GA4 "engaged" sessions from email. Don't trust email-driven engagement numbers in GA4 alone — cross-reference with the email provider's own click data.
Workflow — pick the right path
The skill covers four distinct jobs. Pick first; the workflows are different.
| The user wants… | Path | Reference |
|---|---|---|
| A report ("how did we do last month?") | Phase R | — |
| To set up tracking ("we need to measure X") | Phase T | `references/ga4-tracking-plan.md` |
| To audit existing GTM / GA4 ("why is conversion data wonky?") | Phase A | `references/gtm-audit.md` |
| Precise / unsampled / cross-source analysis | Phase B | `references/bigquery-ga4-export.md` |
Phase R — Run a report (most common path)
Quick-read option: For instant, no-sampling queries against cached GA4 data, try google_analytics_query_insights first — it's faster than the full API path and avoids the ~10M-event sampling threshold. Call it with a query= SQL string; the tool description lists available columns and the cached table name. If it returns "No data cached", read the suggestion field for the workspace-specific table name and retry. For cached GSC data, use google_search_console_query_insights with the same pattern. Fall through to Step 4 below for metrics not in the cache.
1. Confirm the property. google_analytics_list_accounts() → google_analytics_list_properties(filter="parent:accounts/<account_id>"). Ask the user to pick if there are multiple. Save the properties/<id> for the rest of the conversation. 2. Pick the date range. Always confirm. "Last 30 days" is start_date="30daysAgo", end_date="yesterday" (avoid today — partial-day data is unstable). 3. Pick metrics + dimensions. Don't blast 12 metrics × 6 dimensions in one report — the result is unreadable. Pick the 2–3 metrics that answer the user's question and the 1–2 dimensions that segment them meaningfully. 4. Run the report.
google_analytics_run_ga4_report(
property_id="properties/123456789",
start_date="30daysAgo",
end_date="yesterday",
metrics=["activeUsers", "sessions", "conversions", "totalRevenue"],
dimensions=["sessionDefaultChannelGroup", "deviceCategory"],
)5. Present results as a table. Always show the raw numbers alongside any interpretation. "Organic search drove 12,400 sessions (+18% MoM)" beats "organic was up." 6. One follow-on if the data flags it. If a metric stands out (e.g., conversion rate dropped 40% on mobile), run one targeted follow-up report — don't speculate.
Common GA4 metrics & dimensions
The GA4 API uses camelCase names. The most useful:
Metrics: activeUsers, sessions, screenPageViews, bounceRate, engagementRate, averageSessionDuration, conversions, eventCount, totalRevenue, transactions, purchaseRevenue, userEngagementDuration.
Dimensions: country, city, deviceCategory, operatingSystem, browser, sessionDefaultChannelGroup, sessionSource, sessionMedium, sessionCampaignName, pagePath, eventName, date, hour, landingPage.
For the full list, the user can browse the GA4 Data API reference.
Phase T — Set up tracking
The GA4 API can create properties, data streams, key events, custom metrics, and custom dimensions — but it can't deploy GTM tags into the page. That step is GTM-side (or hard-coded in the site). The skill workflow:
1. Tracking plan first. Define what events you need to fire, where they fire, and which ones are conversions (key events). Full template in `references/ga4-tracking-plan.md`. 2. GA4-side setup — create custom dimensions / metrics, mark key events:
google_analytics_create_custom_dimension(
parent="properties/123456789",
parameter_name="plan_tier",
display_name="Plan Tier",
scope="EVENT",
)
google_analytics_create_key_event(
parent="properties/123456789",
event_name="purchase",
counting_method="ONCE_PER_EVENT",
)3. GTM-side setup — create/update tags + triggers + variables in a workspace, then version + publish:
gtm_workspace(operation="create", account_id="...", container_id="...", name="purchase-tracking-v3")
gtm_tag(operation="create", workspace_path="...", tag_definition={...})
gtm_trigger(operation="create", workspace_path="...", trigger_definition={...})
gtm_variable(operation="create", workspace_path="...", variable_definition={...})
gtm_version(operation="create", workspace_path="...", version_name="purchase-tracking-v3")
# then publish via the GTM UI or version operation4. Validate — see Phase A.
Phase A — Audit (the existing setup is broken or suspect)
Most "our analytics is wrong" complaints are one of:
| Symptom | Likely cause | How to confirm |
|---|---|---|
| Conversion event not appearing in GA4 | Event firing in GTM but not reaching GA4 (wrong measurement ID, blocked by consent gate, ad blocker) | google_analytics_run_ga4_report for eventName=purchase over the last 7d → if 0, check GTM |
| Conversion count wildly off | Event firing on every page (not just confirmation), or duplicate tags | Audit GTM tags via gtm_tag(operation="list"), check for multiple tags firing on the same trigger |
| Revenue reported differently in GA4 vs the platform of record | Currency mismatch, refund handling, attribution window | Pull both side-by-side, look for refund / currency rows |
| Suddenly mobile traffic dropped to ~0 | Tag firing only on desktop trigger, or a recent GTM publish broke the mobile container | gtm_version(operation="list") to find recent publishes, diff with previous version |
| GSC clicks ≠ GA4 organic sessions | Always different — different definitions. Don't try to reconcile exactly. | Expected; document and move on |
Detailed audit walkthrough in `references/gtm-audit.md`.
Phase B — BigQuery GA4 export (precision / cross-source analysis)
For unsampled data, custom attribution, joining GA4 with order-DB / CRM data, or cohort analysis. Requires the GA4 → BigQuery export to be turned on in the GA4 admin (free for standard properties since 2023).
bigquery_execute_query(
query="""
SELECT
event_date,
COUNT(DISTINCT user_pseudo_id) AS users,
COUNTIF(event_name = 'purchase') AS purchases,
SUM(IF(event_name = 'purchase', ecommerce.purchase_revenue, 0)) AS revenue
FROM `your-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260401' AND '20260430'
GROUP BY event_date
ORDER BY event_date
"""
)Schema, common queries, and the full attribution-modeling workflow in `references/bigquery-ga4-export.md`.
Output standards
- Always present numbers as tables, not prose. "12,400 sessions, 8.2% bounce, 1.4% conversion" → markdown table.
- Always include the date range in the report header. "Apr 1–30, 2026" — undated numbers are useless.
- Always include the property / site the report came from. Multi-property organizations get burned by this constantly.
- Annotate sampling. If GA4 returns a
samplesReadCount < samplingSpaceSize, say so explicitly. The user needs to know whether to trust the number for finance / board reporting. - Distinguish "ratio metric" from "summable metric". Bounce rate, engagement rate, conversion rate are ratios and don't sum — present them as one number, not a column total.
Critical rules (recap)
1. GA4 property IDs: reporting tools use property_id=, create/list tools use parent=, get/update/delete tools use name= (full resource path). 2. Date ranges are inclusive on both ends. 3. Above ~10M events, GA4 API samples — switch to BigQuery export for precision. 4. Conversions are "key events" in GA4 (renamed in 2024). 5. GTM changes need workspace → version → publish to go live. 6. GSC has a 2–3 day data lag; GA4 has 24–48h. 7. MPP inflates GA4 email-engagement metrics.
Reference workflows
| Reference | When to read |
|---|---|
| `references/ga4-tracking-plan.md` | Designing what to track — recommended event schema, custom dimensions / metrics, key-event mapping, naming conventions |
| `references/gtm-audit.md` | Auditing an existing GTM container — finding duplicate tags, broken triggers, unused variables, missing consent gates |
| `references/bigquery-ga4-export.md` | Querying the GA4 BigQuery export — schema, common queries (DAU, funnel, attribution, cohort, retention), join patterns |
BigQuery GA4 Export
The GA4 → BigQuery export is the single most under-used analytics asset most teams have. It gives you:
- Unsampled data — no GA4 API sampling, no matter how big the property.
- Event-level granularity — every event row, queryable.
- Joinability — join GA4 data with your order DB, CRM, billing data.
- Custom attribution — any model you can write in SQL, not just the four GA4 ships.
- Cohort and retention queries — clean window functions instead of GA4's locked-in cohort report.
Free for standard GA4 properties since 2023. Configure in GA4 Admin → BigQuery Linking. After it's set up, every day's events arrive as a events_YYYYMMDD table in <project>.analytics_<property_id>.events_*.
Schema cheat sheet
The export has one row per event. Key columns:
| Column | Type | What |
|---|---|---|
event_date | STRING | YYYYMMDD (use _TABLE_SUFFIX for partition pruning) |
event_timestamp | INT64 | Microseconds since epoch (UTC) |
event_name | STRING | The event name (purchase, page_view, etc.) |
event_params | RECORD (REPEATED) | Array of {key, value: {string_value, int_value, double_value, float_value}} |
user_id | STRING | Set if you set it via gtag('config', '...', {user_id}) |
user_pseudo_id | STRING | GA4-assigned pseudo-anonymous client ID |
user_properties | RECORD (REPEATED) | Array of {key, value: ...} |
device.* | RECORD | category, mobile_brand_name, operating_system, browser |
geo.* | RECORD | country, region, city, continent |
traffic_source.* | RECORD | name, medium, source |
session_traffic_source_last_click.* | RECORD | The session's last-click attribution |
ecommerce.* | RECORD | purchase_revenue, transaction_id, total_item_quantity |
items | RECORD (REPEATED) | Cart contents — item_id, item_name, price, quantity |
Two patterns dominate:
Extract a parameter: unnest event_params and pick by key.
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_path') AS page_pathBuild session ID: GA4 doesn't ship a session ID column directly — derive it from user_pseudo_id + ga_session_id event param.
CONCAT(
user_pseudo_id,
'.',
CAST((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS STRING)
) AS session_idQuery templates (copy + adapt)
Replace your-project.analytics_123456789 with your actual project and property ID throughout.
1. Daily active users + sessions
SELECT
PARSE_DATE('%Y%m%d', event_date) AS date,
COUNT(DISTINCT user_pseudo_id) AS daily_active_users,
COUNT(DISTINCT CONCAT(
user_pseudo_id, '.',
CAST((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS STRING)
)) AS sessions
FROM `your-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260401' AND '20260430'
GROUP BY date
ORDER BY date;2. Top pages by views
SELECT
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_path') AS page_path,
COUNT(*) AS page_views,
COUNT(DISTINCT user_pseudo_id) AS unique_users
FROM `your-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260401' AND '20260430'
AND event_name = 'page_view'
GROUP BY page_path
ORDER BY page_views DESC
LIMIT 50;3. Conversion funnel (begin_checkout → purchase)
WITH steps AS (
SELECT
user_pseudo_id,
MAX(IF(event_name = 'view_item', 1, 0)) AS step_1_view,
MAX(IF(event_name = 'add_to_cart', 1, 0)) AS step_2_add,
MAX(IF(event_name = 'begin_checkout', 1, 0)) AS step_3_checkout,
MAX(IF(event_name = 'purchase', 1, 0)) AS step_4_purchase
FROM `your-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260401' AND '20260430'
GROUP BY user_pseudo_id
)
SELECT
SUM(step_1_view) AS viewed_item,
SUM(step_2_add) AS added_to_cart,
SUM(step_3_checkout) AS began_checkout,
SUM(step_4_purchase) AS purchased,
ROUND(SUM(step_4_purchase) / NULLIF(SUM(step_1_view), 0) * 100, 2) AS overall_conversion_pct
FROM steps;4. Revenue by source/medium (last-click attribution)
SELECT
session_traffic_source_last_click.manual_campaign.source AS source,
session_traffic_source_last_click.manual_campaign.medium AS medium,
COUNT(DISTINCT ecommerce.transaction_id) AS purchases,
SUM(ecommerce.purchase_revenue) AS revenue
FROM `your-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260401' AND '20260430'
AND event_name = 'purchase'
GROUP BY source, medium
ORDER BY revenue DESC;5. New vs returning users
WITH first_seen AS (
SELECT
user_pseudo_id,
MIN(PARSE_DATE('%Y%m%d', event_date)) AS first_date
FROM `your-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260430'
GROUP BY user_pseudo_id
)
SELECT
PARSE_DATE('%Y%m%d', e.event_date) AS date,
COUNT(DISTINCT IF(fs.first_date = PARSE_DATE('%Y%m%d', e.event_date), e.user_pseudo_id, NULL)) AS new_users,
COUNT(DISTINCT IF(fs.first_date < PARSE_DATE('%Y%m%d', e.event_date), e.user_pseudo_id, NULL)) AS returning_users
FROM `your-project.analytics_123456789.events_*` e
JOIN first_seen fs USING (user_pseudo_id)
WHERE _TABLE_SUFFIX BETWEEN '20260401' AND '20260430'
GROUP BY date
ORDER BY date;6. Weekly retention cohort (week-over-week)
WITH first_visit AS (
SELECT
user_pseudo_id,
DATE_TRUNC(PARSE_DATE('%Y%m%d', MIN(event_date)), WEEK) AS cohort_week
FROM `your-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260430'
GROUP BY user_pseudo_id
),
visits AS (
SELECT DISTINCT
user_pseudo_id,
DATE_TRUNC(PARSE_DATE('%Y%m%d', event_date), WEEK) AS visit_week
FROM `your-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260430'
)
SELECT
fv.cohort_week,
DATE_DIFF(v.visit_week, fv.cohort_week, WEEK) AS weeks_since_first_visit,
COUNT(DISTINCT v.user_pseudo_id) AS users
FROM first_visit fv
JOIN visits v USING (user_pseudo_id)
GROUP BY cohort_week, weeks_since_first_visit
ORDER BY cohort_week, weeks_since_first_visit;7. Joining with your order database
The most powerful query in the bunch. Assumes the site sets user_id on gtag('config', ..., {user_id}) and that ID matches your DB.
WITH ga AS (
SELECT
user_id,
DATE(TIMESTAMP_MICROS(event_timestamp)) AS visit_date,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_path') AS page_path
FROM `your-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260401' AND '20260430'
AND user_id IS NOT NULL
)
SELECT
ga.user_id,
ga.page_path,
COUNT(*) AS page_visits,
ANY_VALUE(orders.lifetime_revenue_cents) AS lifetime_revenue_cents,
ANY_VALUE(orders.plan_tier) AS plan_tier
FROM ga
LEFT JOIN `your-project.your_dataset.orders` orders USING (user_id)
GROUP BY user_id, page_path
ORDER BY page_visits DESC
LIMIT 100;This unlocks questions GA4 can never answer alone: "What's the LTV of users who hit our pricing page in the last 30 days?"
Running queries through the MCP
bigquery_execute_query(
query="""
SELECT event_date, COUNT(DISTINCT user_pseudo_id) AS dau
FROM `your-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260401' AND '20260430'
GROUP BY event_date
ORDER BY event_date
""",
# Optional: project_id, location, etc. — depends on Pipedream proxy config
)For inserting rows back into BigQuery (e.g., writing a derived metrics table):
bigquery_insert_rows(
table_id="your-project.your_dataset.daily_metrics",
rows=[
{"date": "2026-04-30", "metric_name": "dau", "value": 12400},
...
],
)Performance + cost rules
- Always partition-prune. Use
_TABLE_SUFFIX BETWEEN 'YYYYMMDD' AND 'YYYYMMDD'to limit which daily tables get scanned. A query without this against a year of GA4 events is expensive. - *Don't `SELECT `.** Pick the columns you need. GA4 events have ~30 nested fields; selecting all of them on a year of data is a wallet event.
- Cache exploratory queries. BigQuery caches identical queries for 24h at no cost. Iterating on a query? Don't rewrite the date range every time — cache hit gets you free re-runs.
- *Use the `intraday_
tables for today's data.** GA4 export populatesintraday_YYYYMMDDfor the current day, then collapses intoevents_YYYYMMDD~24h later. If you need today's data, query both withevents_*` glob.
Schema gotchas
- `event_date` is a STRING (`YYYYMMDD`). Convert with
PARSE_DATE('%Y%m%d', event_date)before doing date arithmetic. - `event_timestamp` is microseconds.
TIMESTAMP_MICROS(event_timestamp)to get a real timestamp. - Nested params require `UNNEST`. This is the #1 thing new GA4 BigQuery users get stuck on. Use the patterns above as templates.
- *Ecommerce data lives in `ecommerce.
anditems(repeated).**purchaseevents have populatedecommerce.purchase_revenue;add_to_cartevents have populateditemsbut noecommerce` revenue. - `user_id` is NULL for anonymous users. Don't filter on it unless you specifically want logged-in users.
GA4 Tracking Plan
A tracking plan is the contract between the product (what we instrument) and the analytics surface (what we report on). Build it before writing any GTM tags. Skipping this step is the #1 reason GA4 setups end up unusable six months in.
What a tracking plan contains
For each event you intend to fire:
1. Event name — snake_case, verb-led (e.g., purchase, start_checkout, play_video). 2. Trigger — when the event fires. Be precise: "click on [data-ev=cta-pricing]" beats "user clicks a button." 3. Parameters — the data you ship with the event. Each parameter has a name, type, source, example value. 4. Is it a key event (conversion)? — yes / no. 5. Where it's set up — GTM tag name, or hardcoded in the codebase. 6. Owner — who's responsible if it breaks.
Anything missing from this list will burn the team later.
Recommended event taxonomy
GA4 has recommended events that get special treatment in the UI (auto-populated reports, predictive audiences). Use them where they fit; only invent custom events for things they don't cover.
For all sites
| Event | When | Key event? |
|---|---|---|
page_view | Every page load (auto with GA4 base config) | No |
scroll | 90% scroll depth (auto with enhanced measurement) | No |
click | Outbound link click (auto with enhanced measurement) | No |
view_search_results | Site search (auto with enhanced measurement) | No |
file_download | PDF / docx / zip download (auto with enhanced measurement) | No |
form_submit (custom or auto) | Form submitted | Often (lead form) |
sign_up | Account creation completed | Yes |
login | Returning user logs in | No |
For ecommerce (use exactly these names — they unlock the GA4 Monetization reports)
| Event | When | Key event? |
|---|---|---|
view_item_list | Product list / collection page viewed | No |
view_item | Product detail page viewed | No |
select_item | Product clicked from a list | No |
add_to_cart | Item added to cart | No |
view_cart | Cart viewed | No |
begin_checkout | Checkout started | Often |
add_payment_info | Payment info added | No |
add_shipping_info | Shipping info added | No |
purchase | Order completed | Yes |
refund | Order refunded (full or partial) | No |
For the ecom events, GA4 expects specific parameter names — currency, value, items[] with item_id, item_name, price, quantity. Off-spec parameters silently fall out of the Monetization reports.
For SaaS / product-led
| Event | When | Key event? |
|---|---|---|
sign_up | Account created | Yes |
start_trial | Free trial started | Yes |
complete_onboarding | First-run setup finished | Yes (activation) |
feature_use_<name> | Specific feature used (rate-limit to once per session) | Sometimes |
upgrade_plan | Plan upgraded | Yes |
downgrade_plan | Plan downgraded | No (but track) |
cancel_subscription | Subscription canceled | No (but track) |
Custom dimensions vs custom metrics vs event params
GA4 distinguishes:
- Event parameters — data attached to a single event. Available in the API as
event_params.<name>in the BigQuery export. - User properties — attached to a user, persist across sessions. Use for plan tier, signup cohort, account type.
- Custom dimensions — register an event parameter or user property as a dimension you can group by in GA4 reports. Until you register it, you can't use it as a dimension in the GA4 UI or `run_ga4_report`.
- Custom metrics — register a numeric event parameter as a metric you can sum / average. Same deal: unregistered = unreportable.
Rule: every parameter you want to report on must be registered as either a custom dimension (string) or custom metric (number). Register them ahead of the event firing — backfill doesn't happen for older data.
google_analytics_create_custom_dimension(
parent="properties/123456789", # note: arg is "parent", not "property_id"
parameter_name="plan_tier", # the event param name
display_name="Plan Tier", # what shows in the UI
scope="EVENT", # or "USER" for user properties
description="Plan the user is on at time of event",
)
google_analytics_create_custom_metric(
parent="properties/123456789", # note: arg is "parent", not "property_id"
parameter_name="lesson_seconds",
display_name="Lesson Duration (s)",
measurement_unit="SECONDS",
scope="EVENT",
)GA4 limits: 50 custom event-scoped dimensions, 25 user-scoped, 50 custom metrics per property (standard tier). Use them sparingly — registered dimensions are forever (you can archive but not delete).
Naming conventions
A consistent naming scheme is the single highest-leverage thing in a tracking plan. Pick one and enforce.
- Events: `snake_case`, verb-led.
add_to_cart, notaddedToCartorAdd to Cart. Aligns with GA4 recommended events. - Parameters: `snake_case`, noun.
item_id,plan_tier,referrer_path. Avoid abbreviations. - Custom dimensions: human-readable display names. "Plan Tier", "Signup Cohort". The display name is what shows in reports —
plan_tieris fine for the param, "Plan Tier" is what the user sees. - Boolean params: prefix with `is_` or `has_`.
is_returning_user,has_active_subscription. - Don't use reserved names. GA4 reserves
ga_session_id,_session_id,firebase_*, etc. The list is in the GA4 docs.
Mapping the plan to key events
Not every event is a conversion. Mark only the ones that map to business outcomes:
| Site type | Typical key events |
|---|---|
| Ecommerce | purchase, sometimes begin_checkout (for funnel midpoint reporting) |
| SaaS | sign_up, start_trial, complete_onboarding, upgrade_plan |
| Lead-gen / B2B | form_submit (with form_id=demo-request), book_meeting |
| Content / publisher | subscribe_newsletter, time-on-site engagement (custom) |
Mark with google_analytics_create_key_event:
google_analytics_create_key_event(
parent="properties/123456789", # note: arg is "parent", not "property_id"
event_name="purchase",
counting_method="ONCE_PER_EVENT", # or ONCE_PER_SESSION
)counting_method matters:
ONCE_PER_EVENT— every fire counts. Use for purchases, form submits.ONCE_PER_SESSION— first fire in a session counts. Use for low-friction events like "viewed pricing page" you might mark as a soft conversion.
Example tracking plan (excerpt — SaaS)
| Event | Trigger | Params | Key event? | Setup | Owner |
|--------------------|-----------------------------------|-------------------------------------------------|-----------|--------------|-------|
| page_view | Every page | page_path, page_title | No | GA4 auto | growth |
| sign_up | After /api/users POST 201 | method (email|google), plan_tier_intent | Yes | GTM tag #14 | growth |
| start_trial | After /api/billing/trials POST | plan, trial_days | Yes | GTM tag #15 | growth |
| complete_onboarding| After last onboarding step | onboarding_seconds | Yes | GTM tag #16 | product |
| upgrade_plan | On Stripe webhook checkout.success| from_plan, to_plan, mrr_delta_cents | Yes | server-side | finance |
| feature_use_export | On click of [data-ev=export-csv] | report_type, row_count | No | GTM tag #22 | product |The "Setup" column tells the auditor where to look when something breaks. The "Owner" column tells you who to ping.
Anti-patterns
- One mega-event with 30 parameters. Splits the data badly and hits GA4 param limits. Split into multiple events with focused param sets.
- Same event name fired in multiple meanings.
clickwith no qualifying parameter is useless. Either name the events differently (click_cta_hero,click_cta_pricing) or attach acta_idparameter. - Sending PII as parameters. GA4 explicitly prohibits emails, phone numbers, full names. Hash or remove before sending.
- Forgetting to register the dimension. Param fires, never appears in the UI. Hours of confusion.
- Renaming events after launch. GA4 treats renamed events as new — old data won't merge. Pick names carefully the first time.
What to do with this plan
1. Review with engineering and product. They have to fire the events; sign-off is non-negotiable. 2. Implement in GTM first for marketing-page events, server-side for billing / auth events. 3. Register the custom dimensions and metrics in GA4 before any event ships. 4. QA in GA4 DebugView for the first week — every new event should show up there in real time. 5. Document the plan somewhere durable (Notion, Confluence, repo README) — the plan is the source of truth, not the GTM container.
GTM Audit
A standard auditing pass against an existing Google Tag Manager container. Run this when:
- Conversion data looks wrong in GA4.
- A recent GTM publish broke something and nobody is sure what.
- A new team has inherited a container someone else built.
- The container has > 30 tags and nobody can remember what they all do.
The Hyper MCP exposes the GTM API as gtm_* tools (note: the prefix is gtm_*, not google_tag_manager_*). Each tool is one entity with operation parameters (get, list, create, update, delete, etc.).
Audit checklist
The 8 checks below cover ~95% of GTM problems.
1. Inventory: how many tags, triggers, variables?
gtm_tag(operation="list", workspace_path="accounts/123/containers/456/workspaces/789")
gtm_trigger(operation="list", workspace_path="...")
gtm_variable(operation="list", workspace_path="...")Healthy ranges (rough — bigger sites legitimately have more):
| Entity | < 30 tags | 30–80 | > 80 |
|---|---|---|---|
| Tags | Easy to understand | Audit needed every quarter | Likely has duplicates / stale tags |
| Triggers | Probably fine | Look for unused | Most are unused — most teams over-create triggers |
| Variables | Fine | Some likely unused | Definitely has unused ones |
If totals are at the high end of any row, schedule a cleanup pass.
2. Find duplicate tags
The most common GTM problem: two tags fire on the same trigger because someone created a "v2" without disabling the original. Result: events fire twice → conversion counts double.
Pull all tags, group by type + the trigger they fire on. Anything firing the same event to the same destination on the same trigger is suspect.
# Get all tags
result = gtm_tag(operation="list", workspace_path="...")
# Look for duplicates: same type, same firing trigger, similar parameters
# (No SQL here — just inspect the response)Confirm against GA4 DebugView: trigger the page, see if the event fires once or twice.
3. Find tags with no firing triggers
A tag with firingTriggerId: [] is dead — it can never fire. Either delete it or attach the missing trigger.
Common cause: someone deleted the trigger but not the tag. The tag sits there confusing future readers.
4. Find triggers with no tags
A trigger nothing depends on is just clutter. Delete.
# After listing tags + triggers, build a set of trigger IDs in use:
used = set()
for tag in tags:
used.update(tag.firing_trigger_id or [])
used.update(tag.blocking_trigger_id or [])
unused_triggers = [t for t in triggers if t.trigger_id not in used]5. Find variables that aren't referenced
GTM variables are referenced from tags and triggers. If a variable name appears nowhere in any tag's parameters or trigger's filters, delete it.
# Search the JSON of every tag + trigger for {{Variable Name}} references6. Check for missing consent gates (GDPR / CCPA)
If the site collects EU traffic, every analytics + advertising tag should be gated on a consent variable. The pattern:
- A consent variable (
Cookiebot Consent State,OneTrust Active Groups, custom JS variable) returns the user's consent status. - Each marketing / analytics tag has a blocking trigger on
Consent != Granted.
Without this, GDPR fines are real.
# For every analytics / advertising tag, verify it has a blocking trigger
# on the consent variable. Tags missing the gate are flagged.GA4 has built-in consent mode that can do this differently — check whether the container is using consent mode v2 or manual gates. Either is fine, missing both is not.
7. Check for hard-coded measurement IDs / property IDs
Bad: GA4 measurement ID G-XXXXXXX typed directly into 14 different tags. Good: GA4 measurement ID stored in a single Constant variable, referenced from all tags.
Cleanup: create a Constant variable, replace every hard-coded reference. Critical when migrating between properties.
8. Check version history for recent changes
If something broke "around last Tuesday," the publish history is the prime suspect.
gtm_version(operation="list", container_path="accounts/123/containers/456")
gtm_version_header(operation="list", container_path="accounts/123/containers/456")Each version has a publishedTimestamp and a name. Find the version that was live during the breakage window. Diff against the previous version (the GTM UI does this best — the API can fetch both versions and you compare the JSON).
gtm_version(operation="get", version_path="accounts/.../versions/12")
gtm_version(operation="get", version_path="accounts/.../versions/11")If a tag / trigger / variable changed between v11 and v12 and v12 was the publish that broke things, you have your culprit.
The publish workflow (don't break this part)
GTM has a 4-stage workflow that exists for a reason — bypass it and you'll publish broken tags to production.
1. Workspace ← edit here (sandboxed, doesn't affect live container)
gtm_workspace(operation="create", name="audit-2026-q2", ...)
2. Preview ← test in GTM Preview mode with the page open
(UI-only — no API operation, but the agent can tell the user to do this)
3. Version ← snapshot the workspace into a versioned set of changes
gtm_version(operation="create", workspace_path="...", name="audit-cleanup", notes="removed 12 unused tags")
4. Publish ← make the version live
(UI or version operation with publish=true)Never edit directly in the default workspace if more than one person uses the container. Create a named workspace, do the work, version + publish, delete the workspace.
Common audit findings (what they usually mean)
| Finding | Likely cause | Fix |
|---|---|---|
4× the expected purchase events in GA4 | Two tags fire on the same trigger | Disable / delete the duplicate |
| Conversion event missing entirely | Tag exists but blocking trigger fires (consent, page exclusion) | Check blocking triggers, verify consent state |
| Events firing on the wrong domain | Trigger lacks a Page Hostname filter | Add hostname filter to trigger |
| Form submission tracked, but no form data captured | Form variables not registered as DataLayer Variables | Create DataLayer Variables for each field |
| Tag fires on every page instead of just confirmation | Trigger is All Pages instead of a URL-specific trigger | Tighten the trigger condition |
| Old tag firing a Universal Analytics event | Container never migrated when GA4 launched | Delete UA tags (UA was sunset July 2024) |
| New page type missing all tracking | Page template doesn't have the GTM container snippet | Verify the GTM script tag is on every page template |
Reporting the audit
Present findings as a table the team can act on:
| # | Finding | Severity | Action | Tag / Trigger ID |
|---|------------------------------------------|----------|------------------------------|------------------|
| 1 | Duplicate `purchase` event tag | High | Delete tag #47 | tag_47 |
| 2 | 14 unused triggers | Low | Delete | (list) |
| 3 | GA4 measurement ID hardcoded in 8 tags | Medium | Move to Constant variable | (list) |
| 4 | EU traffic, no consent gate on Meta tag | High | Add consent blocking trigger | tag_22 |Severity rubric:
- High = data is currently wrong / privacy non-compliant. Fix this week.
- Medium = will become a problem (migration risk, maintenance burden). Fix this month.
- Low = cleanup, no functional impact. Fix when convenient.
After the audit, the team should know exactly what to delete, what to fix, and in what order.