GA4 Event Implementation Reference
Complete reference for implementing Google Analytics 4 events: automatically collected, enhanced measurement, recommended, and custom events with exact parameter lists, code examples, and validation techniques.
Event Model Overview
Google Analytics 4 uses a fundamentally different data model from Universal Analytics (UA). Where UA tracked discrete hit types (pageview, event, transaction, social, timing), GA4 treats everything as an event. There are no hit types — a page view is an event, a purchase is an event, and a scroll is an event.
Key differences from Universal Analytics
| Concept | Universal Analytics | GA4 |
|---|---|---|
| Data model | Hit-based (pageview, event, transaction) | Event-based (everything is an event) |
| Event structure | Category / Action / Label / Value | Event name + parameters (key-value pairs) |
| Sessions | Server-defined, 30-min timeout | Derived from session_start event, configurable timeout |
| Pageviews | Dedicated hit type | page_view event with page_location parameter |
| E-commerce | Enhanced Ecommerce plugin | Built-in recommended events with items array |
| Custom data | Custom dimensions/metrics (index-based) | Event parameters + custom dimensions/metrics (name-based) |
| User identity | Client ID + User ID | Client ID (user_pseudo_id) + User ID (user_id) + Google Signals |
Event anatomy
Every GA4 event consists of:
- Event name: A string identifier (e.g.,
page_view,purchase,sign_up) - Event parameters: Key-value pairs attached to the event (e.g.,
page_location,transaction_id) - User properties: Attributes of the user that persist across events (e.g.,
membership_tier)
// Conceptual structure of a GA4 event
{
event_name: "purchase",
event_params: {
transaction_id: "T12345",
value: 99.99,
currency: "USD",
items: [{ item_id: "SKU123", item_name: "Widget", price: 99.99 }]
},
user_properties: {
membership_tier: "gold"
},
// Automatically attached:
event_timestamp: 1707612345000000,
user_pseudo_id: "abc123.def456",
device: { category: "desktop", browser: "Chrome" },
geo: { country: "United States", city: "San Francisco" }
}
Automatically Collected Events
These events are collected automatically by the GA4 tag with no additional configuration. You cannot disable them.
Web (gtag.js / GTM)
| Event | Trigger | Key Parameters |
|---|---|---|
first_visit | First time a user visits the site (new user_pseudo_id cookie created) | None (user-level event) |
session_start | New session begins (30-minute inactivity timeout or midnight boundary) | ga_session_id, ga_session_number |
page_view | Every page load (or history.pushState in SPAs when enhanced measurement is on) | page_location, page_title, page_referrer |
user_engagement | When the page is in focus for at least 1 second and user has interacted | engagement_time_msec |
Mobile (Firebase SDK)
| Event | Trigger | Key Parameters |
|---|---|---|
first_open | First time a user opens the app after install or reinstall | previous_gmp_app_id, updated_with_analytics |
session_start | New session begins | ga_session_id, ga_session_number |
screen_view | Screen transition or logEvent call with screen_view | firebase_screen, firebase_screen_class, firebase_screen_id, firebase_previous_screen |
user_engagement | App is in the foreground for at least 1 second | engagement_time_msec |
app_update | App is updated to a new version and launched | previous_app_version |
app_remove | App package is removed (Android only) | None |
os_update | Device OS is updated | previous_os_version |
app_clear_data | User resets/clears app data | None |
app_exception | App crashes or throws an uncaught exception | fatal, timestamp |
Automatically attached parameters
Every event (automatic or otherwise) includes these parameters by default:
language— User's language settingpage_location— Full URL (web) or screen name (app)page_referrer— Previous page URL (web)page_title— HTML document title (web)screen_resolution— Display resolutionga_session_id— Session identifierga_session_number— Sequential session count for the userengagement_time_msec— Engagement time in milliseconds
Enhanced Measurement Events
Enhanced measurement events are collected automatically when enabled in the GA4 data stream settings (Admin > Data Streams > Web > Enhanced Measurement). Each event type can be toggled on or off individually.
| Event | Trigger | Key Parameters | Toggle |
|---|---|---|---|
scroll | User scrolls past 90% of page height | percent_scrolled (always 90) | Scrolls |
click | User clicks a link that navigates away from the current domain | link_url, link_domain, link_classes, link_id, outbound (true) | Outbound clicks |
view_search_results | Page loads with a URL query parameter matching a search term pattern (default: q, s, search, query, keyword) | search_term | Site search |
video_start | Embedded YouTube video starts playing | video_url, video_title, video_provider, video_current_time, visible | Video engagement |
video_progress | YouTube video reaches 10%, 25%, 50%, or 75% | video_url, video_title, video_provider, video_percent, video_current_time, visible | Video engagement |
video_complete | YouTube video reaches end | video_url, video_title, video_provider, video_current_time, visible | Video engagement |
file_download | User clicks a link to a file (extensions: pdf, xls, xlsx, doc, docx, txt, rtf, csv, exe, key, pps, ppt, pptx, 7z, pkg, rar, gz, zip, avi, mov, mp4, mpeg, wmv, midi, mp3, wav, wma) | file_name, file_extension, link_url, link_text, link_domain | File downloads |
form_start | User first interacts with a form (focus on an input field) | form_id, form_name, form_destination | Form interactions |
form_submit | User submits a form | form_id, form_name, form_destination, form_submit_text | Form interactions |
Configuring enhanced measurement
Enhanced measurement is enabled by default for new web data streams. To configure:
- Navigate to Admin > Data Streams > select your web stream
- Click Enhanced measurement toggle section
- Enable or disable individual event types
- For site search, click the gear icon to configure custom query parameters
// You can also configure enhanced measurement via gtag.js
gtag('config', 'G-XXXXXXXXXX', {
// Disable specific enhanced measurement events
send_page_view: false // disable automatic page_view on config
});
Note: Video tracking only works for embedded YouTube videos using the JS API (enablejsapi=1). Self-hosted or third-party video players require custom event implementation.
Recommended Events
Recommended events have predefined names and parameter lists that unlock built-in GA4 reports and features (e.g., e-commerce reports, monetization reports). You must implement these manually, but using the exact event names and parameter names enables GA4 to populate standard reports.
All Properties
These recommended events apply to any type of website or app.
login
Sent when a user logs in.
gtag('event', 'login', {
method: 'Google' // STRING — login method (e.g., 'Google', 'Email', 'Facebook')
});
sign_up
Sent when a user registers an account.
gtag('event', 'sign_up', {
method: 'Email' // STRING — registration method
});
share
Sent when a user shares content.
gtag('event', 'share', {
method: 'Twitter', // STRING — sharing method
content_type: 'article', // STRING — type of shared content
item_id: 'article_12345' // STRING — identifier for the shared content
});
search
Sent when a user performs a search.
gtag('event', 'search', {
search_term: 'running shoes' // STRING — the search query
});
select_content
Sent when a user selects content.
gtag('event', 'select_content', {
content_type: 'product', // STRING — type of content selected
content_id: 'P12345' // STRING — identifier for the content
});
E-commerce
These events power GA4's built-in e-commerce reports. Implement them in sequence to see full funnel data.
view_item_list
Sent when a user views a list of items (e.g., category page, search results).
gtag('event', 'view_item_list', {
item_list_id: 'category_123', // STRING — list identifier
item_list_name: 'Running Shoes', // STRING — list display name
items: [
{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
item_brand: 'RunCo',
item_category: 'Shoes',
item_category2: 'Running',
item_variant: 'Blue',
price: 129.99,
currency: 'USD',
index: 0,
item_list_id: 'category_123',
item_list_name: 'Running Shoes'
},
{
item_id: 'SKU_456',
item_name: 'Road Runner Elite',
item_brand: 'RunCo',
item_category: 'Shoes',
item_category2: 'Running',
item_variant: 'Red',
price: 149.99,
currency: 'USD',
index: 1,
item_list_id: 'category_123',
item_list_name: 'Running Shoes'
}
]
});
select_item
Sent when a user selects an item from a list.
gtag('event', 'select_item', {
item_list_id: 'category_123',
item_list_name: 'Running Shoes',
items: [{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
item_brand: 'RunCo',
item_category: 'Shoes',
price: 129.99,
currency: 'USD',
index: 0,
item_list_id: 'category_123',
item_list_name: 'Running Shoes'
}]
});
view_item
Sent when a user views a product detail page.
gtag('event', 'view_item', {
currency: 'USD',
value: 129.99,
items: [{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
item_brand: 'RunCo',
item_category: 'Shoes',
item_category2: 'Running',
item_variant: 'Blue',
price: 129.99,
currency: 'USD',
quantity: 1
}]
});
add_to_cart
Sent when a user adds an item to the cart.
gtag('event', 'add_to_cart', {
currency: 'USD',
value: 129.99,
items: [{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
item_brand: 'RunCo',
item_category: 'Shoes',
item_variant: 'Blue',
price: 129.99,
currency: 'USD',
quantity: 1
}]
});
remove_from_cart
Sent when a user removes an item from the cart.
gtag('event', 'remove_from_cart', {
currency: 'USD',
value: 129.99,
items: [{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
price: 129.99,
currency: 'USD',
quantity: 1
}]
});
view_cart
Sent when a user views their cart.
gtag('event', 'view_cart', {
currency: 'USD',
value: 259.98,
items: [
{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
price: 129.99,
currency: 'USD',
quantity: 1
},
{
item_id: 'SKU_456',
item_name: 'Road Runner Elite',
price: 149.99,
currency: 'USD',
quantity: 1
}
]
});
begin_checkout
Sent when a user begins checkout.
gtag('event', 'begin_checkout', {
currency: 'USD',
value: 259.98,
coupon: 'SUMMER20', // STRING — coupon code applied at checkout level
items: [
{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
price: 129.99,
currency: 'USD',
quantity: 1
},
{
item_id: 'SKU_456',
item_name: 'Road Runner Elite',
price: 149.99,
currency: 'USD',
quantity: 1
}
]
});
add_shipping_info
Sent when a user submits shipping information during checkout.
gtag('event', 'add_shipping_info', {
currency: 'USD',
value: 259.98,
coupon: 'SUMMER20',
shipping_tier: 'Express', // STRING — shipping tier selected (e.g., 'Ground', 'Express', 'Next Day')
items: [
{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
price: 129.99,
currency: 'USD',
quantity: 1
}
]
});
add_payment_info
Sent when a user submits payment information during checkout.
gtag('event', 'add_payment_info', {
currency: 'USD',
value: 259.98,
coupon: 'SUMMER20',
payment_type: 'Credit Card', // STRING — payment method (e.g., 'Credit Card', 'PayPal', 'Apple Pay')
items: [
{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
price: 129.99,
currency: 'USD',
quantity: 1
}
]
});
purchase
Sent when a user completes a purchase. This is the most critical e-commerce event.
gtag('event', 'purchase', {
transaction_id: 'T12345', // STRING (REQUIRED) — unique transaction identifier
value: 259.98, // NUMBER (REQUIRED) — total transaction value
currency: 'USD', // STRING (REQUIRED) — ISO 4217 currency code
tax: 20.80, // NUMBER — tax amount
shipping: 9.99, // NUMBER — shipping cost
coupon: 'SUMMER20', // STRING — coupon code
items: [
{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
affiliation: 'Online Store',
coupon: 'ITEM10OFF',
discount: 13.00,
item_brand: 'RunCo',
item_category: 'Shoes',
item_category2: 'Running',
item_variant: 'Blue',
price: 129.99,
currency: 'USD',
quantity: 1
},
{
item_id: 'SKU_456',
item_name: 'Road Runner Elite',
affiliation: 'Online Store',
item_brand: 'RunCo',
item_category: 'Shoes',
item_category2: 'Running',
item_variant: 'Red',
price: 149.99,
currency: 'USD',
quantity: 1
}
]
});
Critical: The purchase event requires transaction_id, value, and currency. Without these, the event will not populate e-commerce reports correctly. GA4 deduplicates purchases by transaction_id within a 72-hour window.
refund
Sent when a refund is issued.
// Full refund
gtag('event', 'refund', {
transaction_id: 'T12345', // STRING (REQUIRED) — original transaction ID
value: 259.98,
currency: 'USD'
});
// Partial refund (include items being refunded)
gtag('event', 'refund', {
transaction_id: 'T12345',
value: 129.99,
currency: 'USD',
items: [{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
price: 129.99,
currency: 'USD',
quantity: 1
}]
});
Lead Generation
generate_lead
Sent when a user submits a lead form or completes a lead generation action.
gtag('event', 'generate_lead', {
currency: 'USD',
value: 50.00 // NUMBER — estimated value of the lead
});
Gaming
earn_virtual_currency
gtag('event', 'earn_virtual_currency', {
virtual_currency_name: 'Coins', // STRING — name of the virtual currency
value: 100 // NUMBER — amount earned
});
spend_virtual_currency
gtag('event', 'spend_virtual_currency', {
virtual_currency_name: 'Coins', // STRING — name of the virtual currency
value: 50, // NUMBER — amount spent
item_name: 'Power Boost' // STRING — item purchased
});
level_up
gtag('event', 'level_up', {
level: 5, // NUMBER — new level reached
character: 'Warrior' // STRING — character that leveled up
});
post_score
gtag('event', 'post_score', {
score: 15000, // NUMBER (REQUIRED) — the score
level: 5, // NUMBER — level where score was achieved
character: 'Warrior' // STRING — character used
});
tutorial_begin
gtag('event', 'tutorial_begin');
// No required parameters
tutorial_complete
gtag('event', 'tutorial_complete');
// No required parameters
unlock_achievement
gtag('event', 'unlock_achievement', {
achievement_id: 'first_blood' // STRING (REQUIRED) — achievement identifier
});
Item Parameter Reference
The items array used across e-commerce events supports these parameters per item:
| Parameter | Type | Description |
|---|---|---|
item_id | STRING | Item SKU/ID (recommended) |
item_name | STRING | Item display name (recommended) |
affiliation | STRING | Store or affiliation |
coupon | STRING | Item-level coupon code |
discount | NUMBER | Discount amount |
index | NUMBER | Position in list |
item_brand | STRING | Brand name |
item_category | STRING | Primary category |
item_category2 | STRING | Category level 2 |
item_category3 | STRING | Category level 3 |
item_category4 | STRING | Category level 4 |
item_category5 | STRING | Category level 5 |
item_list_id | STRING | List ID where item was shown |
item_list_name | STRING | List name where item was shown |
item_variant | STRING | Variant (e.g., color, size) |
location_id | STRING | Physical location associated with the item |
price | NUMBER | Item price |
currency | STRING | ISO 4217 currency code |
quantity | NUMBER | Item quantity |
promotion_id | STRING | Promotion ID |
promotion_name | STRING | Promotion name |
creative_name | STRING | Creative name associated with a promotion |
creative_slot | STRING | Creative slot associated with a promotion |
Note: At least one of item_id or item_name is required for each item. GA4 supports up to 200 items per event.
Custom Events
Use custom events when no automatically collected, enhanced measurement, or recommended event fits your use case.
Naming Rules
- Maximum 40 characters for event names
- Must start with an alphabetic character
- Only alphanumeric characters and underscores allowed (
[a-zA-Z][a-zA-Z0-9_]*) - Case sensitive:
Add_To_Cartandadd_to_cartare different events - Cannot use reserved prefixes:
firebase_,google_,ga_ - Cannot use reserved event names:
ad_activeview,ad_click,ad_exposure,ad_impression,ad_query,adunit_exposure,app_clear_data,app_install,app_update,app_remove,error,first_open,first_visit,in_app_purchase,notification_dismiss,notification_foreground,notification_open,notification_receive,os_update,session_start,screen_view,user_engagement
Parameter Limits
| Limit | Value |
|---|---|
| Unique event names per property | 500 (auto + enhanced + recommended + custom) |
| Parameters per event | 25 |
| Parameter name length | 40 characters |
| Parameter value length (string) | 100 characters |
| User property name length | 24 characters |
| User property value length (string) | 36 characters |
| User properties per project | 25 |
Custom Event Examples
// Newsletter signup
gtag('event', 'newsletter_signup', {
newsletter_type: 'weekly_digest',
signup_location: 'footer',
user_segment: 'returning_visitor'
});
// Feature usage tracking (SaaS)
gtag('event', 'feature_used', {
feature_name: 'export_csv',
feature_category: 'data_tools',
plan_tier: 'pro'
});
// Content engagement
gtag('event', 'article_read', {
article_id: 'post_12345',
article_category: 'technology',
read_time_seconds: 245,
scroll_depth: 85
});
// Error tracking
gtag('event', 'error_occurred', {
error_type: 'form_validation',
error_message: 'invalid_email',
page_section: 'checkout'
});
GTM dataLayer Implementation
When using Google Tag Manager, push events to the dataLayer:
// Standard custom event
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'newsletter_signup',
newsletter_type: 'weekly_digest',
signup_location: 'footer'
});
// E-commerce purchase via dataLayer
window.dataLayer.push({ ecommerce: null }); // Clear previous ecommerce data
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'T12345',
value: 259.98,
currency: 'USD',
tax: 20.80,
shipping: 9.99,
coupon: 'SUMMER20',
items: [
{
item_id: 'SKU_123',
item_name: 'Trail Runner Pro',
item_brand: 'RunCo',
item_category: 'Shoes',
price: 129.99,
quantity: 1
}
]
}
});
Important: Always push { ecommerce: null } before e-commerce events to clear stale data from the dataLayer. This prevents data leakage between events.
Custom Dimensions and Metrics
Custom dimensions and metrics let you extend GA4's data model by registering event parameters or user properties for reporting.
Registration
Register custom dimensions and metrics in Admin > Custom definitions.
- Custom dimension (event-scoped): Maps an event parameter to a reportable dimension
- Custom dimension (user-scoped): Maps a user property to a reportable dimension
- Custom metric: Maps a numeric event parameter to a reportable metric
Scoping
| Scope | Source | Use Case |
|---|---|---|
| Event-scoped dimension | Event parameter | Page-level or action-level attributes (e.g., content_type, form_name) |
| User-scoped dimension | User property | User-level attributes that persist (e.g., membership_tier, signup_source) |
| Custom metric | Event parameter (numeric) | Numeric values for aggregation (e.g., read_time_seconds, score) |
Quota Limits
| Resource | Standard Properties | Analytics 360 |
|---|---|---|
| Event-scoped custom dimensions | 50 | 125 |
| User-scoped custom dimensions | 25 | 100 |
| Custom metrics | 50 | 125 |
Processing Time
After registration, custom dimensions and metrics take 24-48 hours to begin populating in standard reports. They appear in Realtime and DebugView immediately.
Setting User Properties
// Set user properties (persist across sessions)
gtag('set', 'user_properties', {
membership_tier: 'gold',
signup_date: '2025-01-15',
preferred_language: 'en'
});
// Alternative: set individual user property
gtag('set', { user_properties: { membership_tier: 'gold' } });
Implementation Patterns
gtag.js (Global Site Tag)
The simplest implementation method — add the gtag.js snippet directly to your HTML.
<!-- 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>
Then send events anywhere in your code:
// Send a custom event
gtag('event', 'newsletter_signup', {
newsletter_type: 'weekly_digest'
});
// Send a recommended event
gtag('event', 'purchase', {
transaction_id: 'T12345',
value: 99.99,
currency: 'USD',
items: [{ item_id: 'SKU_123', item_name: 'Widget', price: 99.99, quantity: 1 }]
});
Google Tag Manager (GTM)
For GTM implementations, push events to the dataLayer and create corresponding GA4 Event tags in GTM.
// Push event to dataLayer
window.dataLayer.push({
event: 'cta_click',
cta_text: 'Start Free Trial',
cta_location: 'hero_section',
page_section: 'homepage'
});
In GTM, create:
- Trigger: Custom Event trigger matching
cta_click - Tag: GA4 Event tag with event name
cta_click - Parameters: Map dataLayer variables to event parameters
Measurement Protocol (Server-Side)
The GA4 Measurement Protocol sends events server-side via HTTPS POST. Useful for offline conversions, CRM events, and backend-triggered events.
# Endpoint
POST https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET
# Payload
{
"client_id": "abc123.def456",
"events": [{
"name": "purchase",
"params": {
"transaction_id": "T12345",
"value": 99.99,
"currency": "USD",
"items": [{
"item_id": "SKU_123",
"item_name": "Widget",
"price": 99.99,
"quantity": 1
}]
}
}]
}
# Python example
import requests
import json
MEASUREMENT_ID = 'G-XXXXXXXXXX'
API_SECRET = 'YOUR_API_SECRET'
url = f'https://www.google-analytics.com/mp/collect?measurement_id={MEASUREMENT_ID}&api_secret={API_SECRET}'
payload = {
'client_id': 'abc123.def456', # Must match an existing GA4 client_id
'events': [{
'name': 'offline_purchase',
'params': {
'transaction_id': 'OFFLINE_T789',
'value': 250.00,
'currency': 'USD',
'payment_type': 'invoice'
}
}]
}
response = requests.post(url, data=json.dumps(payload))
# Returns 204 No Content on success (does NOT validate payload)
Measurement Protocol limitations:
- No response body — events are not validated server-side (use Validation Server for debugging)
client_idmust match an existing GA4 cookie value to attribute events to users- Events are not visible in Realtime reports (30-minute processing delay)
- Cannot trigger
first_visitorsession_startevents
Validation Server (Debug Endpoint)
Use the validation endpoint during development to catch payload errors before sending to production:
POST https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET
# Returns validation messages instead of silently accepting
{
"validationMessages": [
{
"fieldPath": "events[0].params.currency",
"description": "currency is required when value is set",
"validationCode": "VALUE_REQUIRED"
}
]
}
Validation and Debugging
DebugView
GA4 DebugView (Admin > DebugView) shows events in near real-time from debug-enabled devices/browsers.
Enable debug mode:
// gtag.js — enable debug mode
gtag('config', 'G-XXXXXXXXXX', { debug_mode: true });
// Or per-event
gtag('event', 'purchase', {
debug_mode: true,
transaction_id: 'T12345',
value: 99.99,
currency: 'USD'
});
For GTM, install the Google Analytics Debugger Chrome extension, or add a debug_mode = true field to your GA4 Configuration tag.
DebugView shows:
- Event timeline with event names and timestamps
- Event parameters for each event (click to expand)
- User properties at the time of each event
- Conversion flag indicators
Realtime Report
The Realtime report (Reports > Realtime) shows aggregate data from the last 30 minutes. Use it to verify:
- Events are arriving with correct names
- User counts are reasonable
- Event parameters are populated
- Conversions are firing
BigQuery Validation Queries
Once events flow to BigQuery, use these queries to validate implementation quality.
Check event volume by name
SELECT
event_name,
COUNT(*) as event_count,
COUNT(DISTINCT user_pseudo_id) as unique_users,
MIN(TIMESTAMP_MICROS(event_timestamp)) as first_seen,
MAX(TIMESTAMP_MICROS(event_timestamp)) as last_seen
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
GROUP BY event_name
ORDER BY event_count DESC
Validate e-commerce funnel completeness
WITH funnel AS (
SELECT
event_name,
COUNT(*) as event_count,
COUNT(DISTINCT user_pseudo_id) as users
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
AND event_name IN (
'view_item_list', 'select_item', 'view_item',
'add_to_cart', 'remove_from_cart', 'view_cart',
'begin_checkout', 'add_shipping_info', 'add_payment_info',
'purchase', 'refund'
)
GROUP BY event_name
)
SELECT
event_name,
event_count,
users,
ROUND(SAFE_DIVIDE(users, MAX(users) OVER ()), 4) as pct_of_top
FROM funnel
ORDER BY event_count DESC
Detect missing required parameters
-- Check for purchase events missing transaction_id or value
SELECT
'purchase_missing_transaction_id' as issue,
COUNT(*) as count
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
AND event_name = 'purchase'
AND (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') IS NULL
UNION ALL
SELECT
'purchase_missing_value' as issue,
COUNT(*) as count
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
AND event_name = 'purchase'
AND (SELECT value.double_value FROM UNNEST(event_params) WHERE key = 'value') IS NULL
AND (SELECT value.float_value FROM UNNEST(event_params) WHERE key = 'value') IS NULL
AND (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'value') IS NULL
UNION ALL
SELECT
'purchase_missing_currency' as issue,
COUNT(*) as count
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
AND event_name = 'purchase'
AND (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'currency') IS NULL
Find duplicate transactions
SELECT
transaction_id,
COUNT(*) as duplicate_count,
COUNT(DISTINCT user_pseudo_id) as user_count,
MIN(TIMESTAMP_MICROS(event_timestamp)) as first_occurrence,
MAX(TIMESTAMP_MICROS(event_timestamp)) as last_occurrence
FROM (
SELECT
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') as transaction_id,
user_pseudo_id,
event_timestamp
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
AND event_name = 'purchase'
)
WHERE transaction_id IS NOT NULL
GROUP BY transaction_id
HAVING COUNT(*) > 1
ORDER BY duplicate_count DESC
Audit custom event naming
-- Find event names that violate naming conventions
SELECT
event_name,
COUNT(*) as event_count,
CASE
WHEN LENGTH(event_name) > 40 THEN 'exceeds_40_chars'
WHEN REGEXP_CONTAINS(event_name, r'^(firebase_|google_|ga_)') THEN 'reserved_prefix'
WHEN NOT REGEXP_CONTAINS(event_name, r'^[a-zA-Z][a-zA-Z0-9_]*$') THEN 'invalid_characters'
WHEN event_name != LOWER(event_name) AND event_name NOT IN (
'first_visit', 'session_start', 'page_view', 'user_engagement'
) THEN 'mixed_case_warning'
ELSE 'valid'
END as naming_issue
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
GROUP BY event_name
HAVING naming_issue != 'valid'
ORDER BY event_count DESC
Check parameter value truncation
-- Find parameters that might be truncated at the 100-character limit
SELECT
event_name,
ep.key as parameter_name,
MAX(LENGTH(ep.value.string_value)) as max_length,
COUNTIF(LENGTH(ep.value.string_value) >= 100) as at_limit_count,
COUNT(*) as total_count
FROM `project.analytics_123456789.events_*`,
UNNEST(event_params) as ep
WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
AND ep.value.string_value IS NOT NULL
GROUP BY event_name, ep.key
HAVING max_length >= 100
ORDER BY at_limit_count DESC
Common Pitfalls
1. Event name case sensitivity
GA4 event names are case sensitive. Purchase, purchase, and PURCHASE are three different events. Always use snake_case for consistency and to match recommended event names.
// WRONG — creates a separate event from the recommended 'purchase'
gtag('event', 'Purchase', { ... });
// CORRECT
gtag('event', 'purchase', { ... });
2. Parameter value truncation
String parameter values are truncated at 100 characters. URLs, long product names, or error messages may be silently cut off.
// WRONG — URL will be truncated
gtag('event', 'page_error', {
full_url: window.location.href // Could exceed 100 chars
});
// BETTER — hash or shorten the value
gtag('event', 'page_error', {
page_path: window.location.pathname, // Usually shorter
url_hash: hashFunction(window.location.href) // Fixed length
});
3. Exceeding custom dimension quotas
Standard GA4 properties allow 50 event-scoped and 25 user-scoped custom dimensions. Once you hit the limit, you cannot register new parameters for reporting without archiving existing ones. Plan your parameter taxonomy carefully.
4. Duplicate purchase events
Without proper deduplication, purchase events can fire multiple times (e.g., page refresh on thank-you page). GA4 deduplicates by transaction_id within 72 hours, but only if transaction_id is provided.
// WRONG — no dedup protection without transaction_id
gtag('event', 'purchase', { value: 99.99, currency: 'USD' });
// CORRECT — always include transaction_id
gtag('event', 'purchase', {
transaction_id: 'T12345',
value: 99.99,
currency: 'USD',
items: [...]
});
5. Missing currency with value
When you send a value parameter, you must also send currency. Without currency, the value is ignored in monetization reports.
6. Not clearing dataLayer ecommerce object
In GTM implementations, stale ecommerce data in the dataLayer can leak into subsequent events.
// ALWAYS clear before pushing ecommerce events
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'purchase',
ecommerce: { ... }
});
7. Sending PII in event parameters
GA4 Terms of Service prohibit sending personally identifiable information (PII) such as email addresses, phone numbers, or names in event parameters or user properties. Violations can result in data deletion or property suspension.
// WRONG — PII in parameters
gtag('event', 'sign_up', { email: 'user@example.com' });
// CORRECT — use a hashed identifier
gtag('event', 'sign_up', { method: 'email', user_type: 'new' });
8. Exceeding the 500-event-name limit
Each GA4 property supports a maximum of 500 unique event names (including automatically collected and enhanced measurement events). Dynamically generated event names (e.g., click_button_123) quickly exhaust this limit.
// WRONG — dynamic event names burn through quota
gtag('event', `click_${buttonId}`, {});
// CORRECT — single event name with parameter
gtag('event', 'button_click', { button_id: buttonId });
Resources
- GA4 Event Reference: https://developers.google.com/analytics/devguides/collection/ga4/reference/events
- Recommended Events: https://support.google.com/analytics/answer/9267735
- Custom Events: https://developers.google.com/analytics/devguides/collection/ga4/events
- Measurement Protocol: https://developers.google.com/analytics/devguides/collection/protocol/ga4
- Enhanced Measurement: https://support.google.com/analytics/answer/9216061
- Event Limits & Quotas: https://support.google.com/analytics/answer/9267744
- GA4 BigQuery Export Schema: https://cogny.com/docs/ga4-bigquery-export-schema
- Full Cogny Docs: https://cogny.com/docs/ga4-event-implementation
Claude Code Skill
This reference is available as a Claude Code skill for instant access during development:
Install by adding to your .claude/settings.json:
{
"permissions": {
"allow": [
"skill:ga4-events"
]
}
}