Free tool

Build a GA4 tracking plan for your business model

Pick e-commerce, SaaS, marketplace or lead-gen, untick the events you do not need and add your own. You get the events, when each fires, its parameters and the suggested key events, as a CSV for Sheets or Excel, as JSON, and as a BigQuery query that checks the plan against what your GA4 export actually received.

Business model
Events
Add an event of your own
Your BigQuery export

Only used to fill in the check query below. GA4 names the dataset analytics_ followed by the property ID. It is not put in the plan’s link.

Everything runs in this browser. The plan is copied into the page address, so a link reopens the same plan for whoever you send it to.

Your real events

Check the plan against your GA4 BigQuery export

The query lists, for the last 28 full days of the GA4 export’s daily events_* tables, every planned event and how often it fired, every planned parameter and how often it arrived, and every event that fired but is not in the plan. It follows the plan above as you change it. Paste it into the BigQuery console and replace the table with your own export.

BigQuery: your tracking plan against the GA4 export · vendor docs
-- GA4 tracking plan check: E-commerce, 18 events.
-- Plan: https://lp-data.cloud/tools/ga4-tracking-plan-generator
-- For the last 28 full days: did each planned event fire, did each planned
-- parameter arrive as the planned type, and which events fired that are not in
-- the plan. Reads event_name, event_params, items and privacy_info only.
-- Replace the table with your own GA4 export: <project>.analytics_<property id>.
WITH plan AS (
  SELECT * FROM UNNEST([
    STRUCT('view_item_list' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('item_list_id', 'string'), ('item_list_name', 'string'), ('items', 'items')] AS params),
    STRUCT('select_item' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('item_list_id', 'string'), ('item_list_name', 'string'), ('items', 'items')] AS params),
    STRUCT('view_item' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('currency', 'string'), ('value', 'number'), ('items', 'items')] AS params),
    STRUCT('search' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('search_term', 'string')] AS params),
    STRUCT('add_to_wishlist' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('currency', 'string'), ('value', 'number'), ('items', 'items')] AS params),
    STRUCT('add_to_cart' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('currency', 'string'), ('value', 'number'), ('items', 'items')] AS params),
    STRUCT('remove_from_cart' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('currency', 'string'), ('value', 'number'), ('items', 'items')] AS params),
    STRUCT('view_cart' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('currency', 'string'), ('value', 'number'), ('items', 'items')] AS params),
    STRUCT('begin_checkout' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('currency', 'string'), ('value', 'number'), ('coupon', 'string'), ('items', 'items')] AS params),
    STRUCT('add_shipping_info' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('currency', 'string'), ('value', 'number'), ('shipping_tier', 'string'), ('items', 'items')] AS params),
    STRUCT('add_payment_info' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('currency', 'string'), ('value', 'number'), ('payment_type', 'string'), ('items', 'items')] AS params),
    STRUCT('purchase' AS event_name, TRUE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('transaction_id', 'string'), ('currency', 'string'), ('value', 'number'), ('tax', 'number'), ('shipping', 'number'), ('coupon', 'string'), ('items', 'items')] AS params),
    STRUCT('refund' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('transaction_id', 'string'), ('currency', 'string'), ('value', 'number'), ('items', 'items')] AS params),
    STRUCT('view_promotion' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('promotion_id', 'string'), ('promotion_name', 'string'), ('creative_slot', 'string'), ('items', 'items')] AS params),
    STRUCT('select_promotion' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('promotion_id', 'string'), ('promotion_name', 'string'), ('creative_slot', 'string'), ('items', 'items')] AS params),
    STRUCT('sign_up' AS event_name, TRUE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('method', 'string')] AS params),
    STRUCT('login' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('method', 'string')] AS params),
    STRUCT('newsletter_signup' AS event_name, FALSE AS key_event, ARRAY<STRUCT<name STRING, type STRING>>[('form_location', 'string')] AS params)
  ])
),
ev AS (
  SELECT event_name, event_params, items, privacy_info.analytics_storage AS analytics_storage
  FROM `your-project.analytics_123456789.events_*`
  -- Daily tables only: the events_intraday_ suffixes sort after the dates.
  WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY))
    AND FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY))
),
event_counts AS (
  SELECT
    event_name,
    COUNT(*) AS events,
    COUNTIF(analytics_storage = 'No') AS consent_denied,
    COUNTIF(ARRAY_LENGTH(items) > 0) AS with_items
  FROM ev
  GROUP BY event_name
),
param_counts AS (
  SELECT
    e.event_name,
    ep.key AS param,
    COUNT(*) AS events_with_param,
    COUNTIF(ep.value.string_value IS NOT NULL) AS as_string,
    COUNTIF(ep.value.int_value IS NOT NULL OR ep.value.double_value IS NOT NULL OR ep.value.float_value IS NOT NULL) AS as_number
  FROM ev AS e, UNNEST(e.event_params) AS ep
  WHERE e.event_name IN (SELECT event_name FROM plan)
  GROUP BY 1, 2
),
checks AS (
  SELECT
    1 AS sort_order,
    'planned event' AS row_type,
    p.event_name,
    CAST(NULL AS STRING) AS parameter,
    p.key_event,
    IFNULL(c.events, 0) AS events_28d,
    IFNULL(c.consent_denied, 0) AS consent_denied_28d,
    IF(c.events IS NULL, 'MISSING: never fired', 'ok') AS status
  FROM plan AS p
  LEFT JOIN event_counts AS c USING (event_name)

  UNION ALL

  SELECT
    2,
    'planned parameter',
    p.event_name,
    prm.name,
    p.key_event,
    IFNULL(IF(prm.type = 'items', c.with_items, pc.events_with_param), 0),
    NULL,
    CASE
      WHEN c.events IS NULL THEN 'event never fired'
      WHEN prm.type = 'items' AND c.with_items = 0 THEN 'MISSING: items always empty'
      WHEN prm.type = 'items' AND c.with_items < c.events THEN FORMAT('PARTIAL: on %.0f%% of events', 100 * c.with_items / c.events)
      WHEN prm.type = 'items' THEN 'ok'
      WHEN pc.events_with_param IS NULL THEN 'MISSING: never sent'
      WHEN prm.type = 'number' AND pc.as_number < pc.events_with_param THEN 'WRONG TYPE: sent as text on some events'
      WHEN prm.type = 'string' AND pc.as_string < pc.events_with_param THEN 'WRONG TYPE: sent as a number on some events'
      WHEN pc.events_with_param < c.events THEN FORMAT('PARTIAL: on %.0f%% of events', 100 * pc.events_with_param / c.events)
      ELSE 'ok'
    END
  FROM plan AS p
  CROSS JOIN UNNEST(p.params) AS prm
  LEFT JOIN event_counts AS c ON c.event_name = p.event_name
  LEFT JOIN param_counts AS pc ON pc.event_name = p.event_name AND pc.param = prm.name

  UNION ALL

  SELECT
    IF(c.event_name IN UNNEST(automatic), 4, 3),
    IF(c.event_name IN UNNEST(automatic), 'automatic event', 'not in plan'),
    c.event_name,
    NULL,
    NULL,
    c.events,
    c.consent_denied,
    IF(c.event_name IN UNNEST(automatic), 'collected by GA4 itself', 'FIRED BUT NOT IN THE PLAN')
  FROM event_counts AS c
  CROSS JOIN (SELECT ['click', 'file_download', 'first_visit', 'form_start', 'form_submit', 'page_view', 'scroll', 'session_start', 'user_engagement', 'video_complete', 'video_progress', 'video_start', 'view_search_results'] AS automatic)
  WHERE c.event_name NOT IN (SELECT event_name FROM plan)
)
SELECT * EXCEPT (sort_order)
FROM checks
ORDER BY sort_order, event_name, parameter

Reading the result

  • MISSING: never fired on a planned event: it is not implemented, or it fires on a page or app the export does not cover.
  • MISSING: never sent on a parameter: the event fires without it. Ecommerce events without an items array do not reach the ecommerce reports.
  • WRONG TYPE: a number sent as text (a value of "49.00" in quotes) or an ID sent as a number. GA4 stores them in different columns, so sums and joins quietly break.
  • PARTIAL: the parameter arrives on some of the events only, usually because one of several places that fire the event was missed.
  • FIRED BUT NOT IN THE PLAN: an old event, a typo of a planned one, or something a tag manager added. Each one is either planned or removed.
  • consent_denied_28d counts events sent with analytics_storage denied under advanced consent mode. Events from automatic collection and enhanced measurement are labelled separately (Google’s list).

It reads four columns over 28 days, so it scans a fraction of the export; the BigQuery console shows the bytes before you run it. If a parameter you know you send shows as missing, check whether GA4 stored it in a column of its own instead, such as ecommerce.transaction_id.

Templates

The four starter GA4 tracking plans

65 events across four business models. Recommended events link to the section of Google’s reference they come from, with the parameters it lists for them; custom events and custom parameters are ours, and labelled as such.

E-commerce tracking plan

An online shop: browse, cart, checkout, purchase, refund. Open it in the generator.

EventWhen it firesParameters
view_item_list
recommended
A list of products is shown: a category page, search results or a carousel.
  • item_list_id · string e.g. summer_sale
  • item_list_name · string e.g. Summer sale
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
select_item
recommended
A product is clicked in a list.
  • item_list_id · string e.g. summer_sale
  • item_list_name · string e.g. Summer sale
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
view_item
recommended
A product page loads.
  • currency · string e.g. EUR
  • value · number e.g. 49.00
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
search
recommended
A site search returns results.
  • search_term · string e.g. linen shirt
add_to_wishlist
recommended
A product is saved to the wishlist.
  • currency · string e.g. EUR
  • value · number e.g. 49.00
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
add_to_cart
recommended
A product is added to the cart.
  • currency · string e.g. EUR
  • value · number e.g. 49.00
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
remove_from_cart
recommended
A product is removed from the cart.
  • currency · string e.g. EUR
  • value · number e.g. 49.00
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
view_cart
recommended
The cart page or drawer opens.
  • currency · string e.g. EUR
  • value · number e.g. 98.00
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
begin_checkout
recommended
The first checkout step loads.
  • currency · string e.g. EUR
  • value · number e.g. 98.00
  • coupon · string e.g. SUMMER10
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
add_shipping_info
recommended
A shipping method is chosen in checkout.
  • currency · string e.g. EUR
  • value · number e.g. 98.00
  • shipping_tier · string e.g. Express
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
add_payment_info
recommended
Payment details are submitted in checkout.
  • currency · string e.g. EUR
  • value · number e.g. 98.00
  • payment_type · string e.g. Credit card
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
purchase
recommended key event
The order is confirmed. Send it once per order, ideally server-side.
  • transaction_id · string e.g. T_100231
  • currency · string e.g. EUR
  • value · number e.g. 98.00
  • tax · number e.g. 18.33
  • shipping · number e.g. 4.90
  • coupon · string e.g. SUMMER10
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
refund
recommended
An order is refunded, in full or in part. Usually sent from the back end.
  • transaction_id · string e.g. T_100231
  • currency · string e.g. EUR
  • value · number e.g. 98.00
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
view_promotion
recommended
A promotion banner is shown.
  • promotion_id · string e.g. P_SUMMER
  • promotion_name · string e.g. Summer sale
  • creative_slot · string e.g. home_hero
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
select_promotion
recommended
A promotion banner is clicked.
  • promotion_id · string e.g. P_SUMMER
  • promotion_name · string e.g. Summer sale
  • creative_slot · string e.g. home_hero
  • items · array of items e.g. [{"item_id":"SKU_123","item_name":"Linen shirt","price":49,"quantity":1}]
sign_up
recommended key event
An account is created.
  • method · string e.g. email
login
recommended
A customer signs in.
  • method · string e.g. Google
newsletter_signup
custom
The newsletter form is submitted and accepted.
  • form_location · string e.g. footer

SaaS / subscription tracking plan

A product with sign-up, trial, activation and a paid plan. Open it in the generator.

EventWhen it firesParameters
sign_up
recommended key event
An account is created.
  • method · string e.g. email
  • plan · string custom e.g. free
login
recommended
A user signs in.
  • method · string e.g. Google
trial_start
custom key event
A free trial of a paid plan starts.
  • plan · string e.g. team
  • trial_days · number e.g. 14
tutorial_begin
recommended
Onboarding starts.none
onboarding_step_complete
custom
Each onboarding step is finished.
  • step_name · string e.g. connect_data
  • step_number · number e.g. 2
tutorial_complete
recommended
The last onboarding step is finished.none
activation
custom key event
The user first does the thing that predicts retention (define it once, in the plan).
  • activation_type · string e.g. first_report_shared
feature_used
custom
A feature you want adoption numbers for is used. One event, the feature in a parameter.
  • feature_name · string e.g. scheduled_export
invite_sent
custom
A user invites a teammate.
  • invite_count · number e.g. 3
join_group
recommended
An invited user joins a workspace.
  • group_id · string e.g. workspace_8812
search
recommended
An in-app search returns results.
  • search_term · string e.g. revenue dashboard
pricing_view
custom
The pricing page or the in-app plan picker is shown.
  • page_context · string e.g. upgrade_modal
begin_checkout
recommended
The upgrade or payment form opens.
  • currency · string e.g. EUR
  • value · number e.g. 480.00
  • coupon · string e.g. SUMMER10
  • items · array of items e.g. [{"item_id":"plan_team_annual","item_name":"Team, annual","price":480,"quantity":5}]
purchase
recommended key event
A paid subscription starts or renews, from the billing system webhook.
  • transaction_id · string e.g. in_1Q2w3E
  • currency · string e.g. EUR
  • value · number e.g. 2400.00
  • tax · number e.g. 18.33
  • shipping · number e.g. 4.90
  • coupon · string e.g. SUMMER10
  • items · array of items e.g. [{"item_id":"plan_team_annual","item_name":"Team, annual","price":480,"quantity":5}]
  • billing_cycle · string custom e.g. annual
subscription_upgrade
custom
A subscription moves to a higher plan or more seats.
  • plan_from · string e.g. starter
  • plan_to · string e.g. team
  • value · number e.g. 960.00
subscription_cancel
custom
A subscription is cancelled, from the billing system.
  • plan · string e.g. team
  • cancel_reason · string e.g. too_expensive
refund
recommended
A subscription payment is refunded, from the billing system.
  • transaction_id · string e.g. in_1Q2w3E
  • currency · string e.g. EUR
  • value · number e.g. 480.00
  • items · array of items e.g. [{"item_id":"plan_team_annual","item_name":"Team, annual","price":480,"quantity":5}]

Marketplace tracking plan

Two-sided: buyers search and book, sellers list and respond. Open it in the generator.

EventWhen it firesParameters
sign_up
recommended key event
An account is created, buyer or seller.
  • method · string e.g. email
  • user_role · string custom e.g. seller
login
recommended
A user signs in.
  • method · string e.g. Google
search
recommended
A listing search returns results.
  • search_term · string e.g. porto 2 bed
  • results_count · number custom e.g. 37
view_item_list
recommended
Search results or a category of listings is shown.
  • item_list_id · string e.g. search_results
  • item_list_name · string e.g. Search results
  • items · array of items e.g. [{"item_id":"L-2041","item_name":"Two-bed flat, Porto","price":95,"quantity":3}]
select_item
recommended
A listing is clicked in results.
  • item_list_id · string e.g. search_results
  • item_list_name · string e.g. Search results
  • items · array of items e.g. [{"item_id":"L-2041","item_name":"Two-bed flat, Porto","price":95,"quantity":3}]
view_item
recommended
A listing page loads.
  • currency · string e.g. EUR
  • value · number e.g. 285.00
  • items · array of items e.g. [{"item_id":"L-2041","item_name":"Two-bed flat, Porto","price":95,"quantity":3}]
add_to_wishlist
recommended
A listing is saved.
  • currency · string e.g. EUR
  • value · number e.g. 285.00
  • items · array of items e.g. [{"item_id":"L-2041","item_name":"Two-bed flat, Porto","price":95,"quantity":3}]
share
recommended
A listing is shared.
  • method · string e.g. copy_link
  • content_type · string e.g. listing
  • item_id · string e.g. L-2041
message_sent
custom
A buyer messages a seller, or a seller replies.
  • sender_role · string e.g. buyer
  • listing_id · string e.g. L-2041
begin_checkout
recommended
A buyer starts a booking or order.
  • currency · string e.g. EUR
  • value · number e.g. 285.00
  • coupon · string e.g. SUMMER10
  • items · array of items e.g. [{"item_id":"L-2041","item_name":"Two-bed flat, Porto","price":95,"quantity":3}]
purchase
recommended key event
A booking or order is confirmed. Send it from the server once payment clears.
  • transaction_id · string e.g. B_55120
  • currency · string e.g. EUR
  • value · number e.g. 285.00
  • tax · number e.g. 18.33
  • shipping · number e.g. 4.90
  • coupon · string e.g. SUMMER10
  • items · array of items e.g. [{"item_id":"L-2041","item_name":"Two-bed flat, Porto","price":95,"quantity":3}]
seller_onboarding_complete
custom key event
A seller finishes verification and can publish.
  • seller_type · string e.g. individual
listing_create
custom
A seller saves a new listing draft.
  • listing_id · string e.g. L-2041
  • listing_category · string e.g. apartment
listing_publish
custom key event
A listing goes live.
  • listing_id · string e.g. L-2041
  • listing_category · string e.g. apartment
  • price · number e.g. 95.00
review_submit
custom
A buyer or seller leaves a review.
  • reviewer_role · string e.g. buyer
  • rating · number e.g. 5
refund
recommended
A booking or order is refunded, from the back end.
  • transaction_id · string e.g. B_55120
  • currency · string e.g. EUR
  • value · number e.g. 285.00
  • items · array of items e.g. [{"item_id":"L-2041","item_name":"Two-bed flat, Porto","price":95,"quantity":3}]

Lead-gen / B2B content site tracking plan

Content and forms in, qualified pipeline out, with CRM stages sent back. Open it in the generator.

EventWhen it firesParameters
generate_lead
recommended key event
A contact, demo or quote form is submitted and accepted.
  • currency · string e.g. EUR
  • value · number e.g. 150.00
  • lead_source · string e.g. contact_form
  • form_name · string custom e.g. demo_request
book_meeting
custom key event
A meeting is booked in the embedded scheduler.
  • meeting_type · string e.g. intro_call
newsletter_signup
custom
The newsletter form is submitted and accepted.
  • form_location · string e.g. article_footer
content_download
custom key event
A gated asset is unlocked and downloaded.
  • content_id · string e.g. tracking-plan-template
  • content_type · string e.g. whitepaper
select_content
recommended
A case study, guide or other content card is clicked.
  • content_type · string e.g. case_study
  • content_id · string e.g. data-completeness-incident
search
recommended
A site search returns results.
  • search_term · string e.g. ga4 consent mode
share
recommended
An article is shared.
  • method · string e.g. copy_link
  • content_type · string e.g. article
  • item_id · string e.g. ga4-tracking-plan
cta_click
custom
A primary call to action is clicked.
  • cta_id · string e.g. hero_book_call
  • cta_location · string e.g. hero
pricing_view
custom
The pricing page is shown.
  • page_context · string e.g. nav
qualify_lead
recommended
The lead is marked qualified in the CRM. Sent back with the Measurement Protocol.
  • currency · string e.g. EUR
  • value · number e.g. 1500.00
working_lead
recommended
Sales starts working the lead in the CRM.
  • currency · string e.g. EUR
  • value · number e.g. 1500.00
  • lead_status · string e.g. meeting_booked
disqualify_lead
recommended
The lead is disqualified in the CRM.
  • currency · string e.g. EUR
  • value · number e.g. 0
  • disqualified_lead_reason · string e.g. no_budget
close_convert_lead
recommended key event
The deal is won in the CRM.
  • currency · string e.g. EUR
  • value · number e.g. 12000.00
close_unconvert_lead
recommended
The deal is lost in the CRM.
  • currency · string e.g. EUR
  • value · number e.g. 0
  • unconvert_lead_reason · string e.g. chose_competitor

Method

How the plans are built, and what this does not do

  • Every event Google already defines uses Google’s name and the parameters its recommended events reference lists for it, checked 2026-10-16.
  • Custom events fill the gaps a business model has and the reference does not: trials, activation, listings, meetings. They follow the same lowercase snake_case and the GA4 naming and size limits.
  • Events GA4 collects by itself, such as page_view, scroll and form_submit, are left out of the plans and labelled as automatic in the check query.
  • Back-end events (refunds, renewals, CRM lead stages) are marked as such. They are sent from the server or with the Measurement Protocol, not from the browser.

What it does not do. It is a starting point, not your plan: the right events are the ones tied to decisions your business actually makes. It is written for GA4 only, with no Amplitude, Mixpanel or Segment specifics. It does not model consent mode or estimate how many events consent costs you; the check query only counts what arrived. It does not write tag manager or code snippets, and it does not set up key events or custom dimensions in GA4 for you.

Questions

GA4 tracking plans

What naming convention should GA4 events use?

GA4 event and parameter names start with a letter, use only letters, digits and underscores, and are at most 40 characters. They are case-sensitive, so Sign_Up and sign_up are two different events, and names starting with firebase_, google_ or ga_ are reserved. Use lowercase snake_case everywhere, keep Google's names for anything it already has a recommended event for, and name your own events object_action or action_object consistently (this tool uses Google's verb-first style, such as listing_publish next to add_to_cart). Pick one style, write it in the plan, and reject anything that does not follow it.

Should I use GA4 recommended events or custom events?

Use a recommended event whenever one fits, with exactly Google's name and parameters. GA4's ecommerce and lead reports, and features built on them, only read the recommended names: an order sent as order_completed instead of purchase never shows as revenue. Use a custom event only when nothing in the reference fits, such as listing_publish on a marketplace or trial_start on a SaaS product. This tool marks every event and parameter as recommended (with a link to Google's reference) or custom, so you always know which is which.

How many parameters should a GA4 event have?

GA4 accepts up to 25 parameters on one event, with names up to 40 characters and values up to 100 characters (page_title, page_referrer and page_location allow more). Most events need two to five. Every parameter reaches the BigQuery export, but a custom one only shows in GA4 reports after you register it as a custom dimension or metric, and those are limited per property. So reuse the same parameter name everywhere it means the same thing (one plan, not plan_name on one event and plan_id on another) and put new ideas in a parameter of an existing event before adding a new event.

Which events should be key events?

The few that mean a business outcome: a purchase, a sign-up, a lead, a booked meeting, a published listing. The generator suggests one to four per model. A key event (GA4's name for a conversion) is a setting in GA4 Admin, not something the event sends, so marking or unmarking one changes nothing in the tracking code. If everything is a key event, the attribution reports stop meaning anything.

How does consent mode affect GA4 event counts?

With basic consent mode, nothing is sent until a visitor accepts, so declined visits are simply missing. With advanced consent mode, GA4 still receives cookieless pings from visitors who decline, without a client ID. GA4 reports fill the gap with behavioural and key event modelling, but none of the modelled data is in the BigQuery export, so the export and the GA4 interface show different numbers and neither is wrong. The check query counts events sent with analytics_storage denied in a separate column, so you can see how much of each event depends on consent.

Why check the plan in BigQuery instead of in the GA4 reports?

Because the export has every event and every parameter as it arrived, without thresholds, modelling or the need to register custom dimensions first. One query can then list what is missing, what is sent as the wrong type and what fires without being in the plan, which the reports cannot show at all. It needs the GA4 BigQuery link switched on; daily tables appear the day after, so the query reads the last 28 full days.

The Divergence Point

Where the numbers stop agreeing, and what to do about it. One email a month on data quality, reconciliation and building warehouses that survive contact with a real business. No recycled LinkedIn posts.

Want the tracking designed and checked for you?

A starter plan is the easy part. The product analytics engagement ties every event to a decision, implements or reviews it across web, app and server, and validates it before anyone trusts a number.