ga4-events — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ga4-events (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
Full docs: https://cogny.com/docs/ga4-event-implementation
/ga4-events # Show event model overview
/ga4-events purchase # Show purchase event parameters and code
/ga4-events ecommerce # Full e-commerce event funnel reference
/ga4-events enhanced measurement # Enhanced measurement events list
/ga4-events custom events # Custom event naming rules and limits
/ga4-events validation # DebugView, Realtime, and BigQuery checks
/ga4-events gtm # GTM dataLayer implementation patterns
/ga4-events measurement protocol # Server-side Measurement Protocol examplesYou are a GA4 event implementation expert. Use this reference to help users implement, audit, and validate GA4 events correctly. Provide precise, copy-paste-ready code examples.
When the user asks a question, find the relevant section below and provide actionable answers with ready-to-use code. If GA4 MCP tools are available, use them to inspect the user's actual property configuration (custom dimensions, data streams, conversion events) for context-aware advice.
If the user provides a specific event name or topic as an argument, focus on that area. Otherwise, provide an overview of the event model.
Works with zero accounts: the reference below is complete on its own. When connected, these make the answer specific to the user's setup:
| Job | Primary | Enhanced when connected |
|---|---|---|
| What the property already collects | GA4 tool_get_metadata, tool_list_custom_dimensions, tool_list_conversion_events | — |
| Is the event arriving? | GA4 tool_run_realtime_report, tool_run_report | Segment get_events_volume (upstream of GA4) |
| Same action, same name everywhere | — | Segment list_tracking_plans → list_tracking_plan_rules; PostHog list_event_definitions |
| Experiment exposure event | — | GrowthBook list_experiments (tracking keys) |
See "Live validation" under Validation and Debugging.
GA4 uses an event-based data model where everything is an event. Unlike Universal Analytics hit types (pageview, event, transaction), GA4 has a single concept: events with parameters.
| 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 |
| 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) |
Every GA4 event has:
page_view, purchase)page_location, transaction_id)membership_tier)Collected automatically with no configuration. Cannot be disabled.
| Event | Trigger | Key Parameters |
|---|---|---|
first_visit | First time user visits (new cookie) | None |
session_start | New session begins (30-min timeout) | ga_session_id, ga_session_number |
page_view | Every page load (or history.pushState in SPAs) | page_location, page_title, page_referrer |
user_engagement | Page in focus for 1+ second with interaction | engagement_time_msec |
| Event | Trigger | Key Parameters |
|---|---|---|
first_open | First app open after install | previous_gmp_app_id, updated_with_analytics |
session_start | New session begins | ga_session_id, ga_session_number |
screen_view | Screen transition | firebase_screen, firebase_screen_class, firebase_screen_id |
user_engagement | App in foreground 1+ second | engagement_time_msec |
app_update | App updated and launched | previous_app_version |
app_remove | App removed (Android only) | None |
os_update | OS updated | previous_os_version |
language, page_location, page_referrer, page_title, screen_resolution, ga_session_id, ga_session_number, engagement_time_msec
Collected automatically when enabled in Admin > Data Streams > Enhanced Measurement. Each can be toggled individually.
| Event | Trigger | Key Parameters |
|---|---|---|
scroll | User scrolls past 90% of page | percent_scrolled (always 90) |
click | Outbound link click | link_url, link_domain, link_classes, link_id, outbound |
view_search_results | URL contains search query parameter | search_term |
video_start | YouTube embed starts playing | video_url, video_title, video_provider, video_current_time, visible |
video_progress | YouTube video reaches 10/25/50/75% | Same as video_start plus video_percent |
video_complete | YouTube video reaches end | Same as video_start |
file_download | Click on file link (pdf, xls, doc, zip, etc.) | file_name, file_extension, link_url, link_text, link_domain |
form_start | First interaction with a form | form_id, form_name, form_destination |
form_submit | Form submitted | form_id, form_name, form_destination, form_submit_text |
Note: Video tracking only works with YouTube embeds using enablejsapi=1.
| Event | Parameters |
|---|---|
login | method (STRING) |
sign_up | method (STRING) |
share | method (STRING), content_type (STRING), item_id (STRING) |
search | search_term (STRING) |
select_content | content_type (STRING), content_id (STRING) |
gtag('event', 'login', { method: 'Google' });
gtag('event', 'sign_up', { method: 'Email' });
gtag('event', 'search', { search_term: 'running shoes' });Implement in order for full funnel reporting.
gtag('event', 'view_item_list', {
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', item_category2: 'Running', item_variant: 'Blue',
price: 129.99, currency: 'USD', index: 0,
item_list_id: 'category_123', item_list_name: 'Running Shoes'
}]
});gtag('event', 'select_item', {
item_list_id: 'category_123', item_list_name: 'Running Shoes',
items: [{ item_id: 'SKU_123', item_name: 'Trail Runner Pro', price: 129.99, currency: 'USD', index: 0 }]
});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_variant: 'Blue', price: 129.99, currency: 'USD', quantity: 1 }]
});gtag('event', 'add_to_cart', {
currency: 'USD', value: 129.99,
items: [{ item_id: 'SKU_123', item_name: 'Trail Runner Pro', price: 129.99, currency: 'USD', quantity: 1 }]
});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 }]
});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 }
]
});gtag('event', 'begin_checkout', {
currency: 'USD', value: 259.98, coupon: 'SUMMER20',
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 }
]
});gtag('event', 'add_shipping_info', {
currency: 'USD', value: 259.98, coupon: 'SUMMER20', shipping_tier: 'Express',
items: [{ item_id: 'SKU_123', item_name: 'Trail Runner Pro', price: 129.99, currency: 'USD', quantity: 1 }]
});gtag('event', 'add_payment_info', {
currency: 'USD', value: 259.98, coupon: 'SUMMER20', payment_type: 'Credit Card',
items: [{ item_id: 'SKU_123', item_name: 'Trail Runner Pro', price: 129.99, currency: 'USD', quantity: 1 }]
});gtag('event', 'purchase', {
transaction_id: 'T12345', // REQUIRED — unique transaction ID
value: 259.98, // sum of price * quantity (no tax/shipping); needed for revenue
currency: 'USD', // ISO 4217 — REQUIRED whenever value is set
tax: 20.80,
shipping: 9.99,
coupon: 'SUMMER20',
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', price: 149.99, currency: 'USD', quantity: 1 }
]
});Critical: purchase requires transaction_id and items. value is needed for revenue reporting, and currency is required whenever value is set (without it revenue isn't computed). GA4 deduplicates by transaction_id within 72 hours.
// Full refund
gtag('event', 'refund', { transaction_id: 'T12345', value: 259.98, currency: 'USD' });
// Partial refund
gtag('event', 'refund', {
transaction_id: 'T12345', value: 129.99, currency: 'USD',
items: [{ item_id: 'SKU_123', price: 129.99, currency: 'USD', quantity: 1 }]
});gtag('event', 'generate_lead', { currency: 'USD', value: 50.00 });| Event | Parameters |
|---|---|
earn_virtual_currency | virtual_currency_name (STRING), value (NUMBER) |
spend_virtual_currency | virtual_currency_name (STRING), value (NUMBER), item_name (STRING) |
level_up | level (NUMBER), character (STRING) |
post_score | score (NUMBER, required), level (NUMBER), character (STRING) |
tutorial_begin | None |
tutorial_complete | None |
unlock_achievement | achievement_id (STRING, required) |
gtag('event', 'earn_virtual_currency', { virtual_currency_name: 'Coins', value: 100 });
gtag('event', 'spend_virtual_currency', { virtual_currency_name: 'Coins', value: 50, item_name: 'Power Boost' });
gtag('event', 'level_up', { level: 5, character: 'Warrior' });
gtag('event', 'post_score', { score: 15000, level: 5, character: 'Warrior' });
gtag('event', 'tutorial_begin');
gtag('event', 'tutorial_complete');
gtag('event', 'unlock_achievement', { achievement_id: 'first_blood' });The items array supports these parameters per item:
| Parameter | Type | Description |
|---|---|---|
item_id | STRING | SKU/ID (recommended) |
item_name | STRING | Display name (recommended) |
affiliation | STRING | Store or affiliation |
coupon | STRING | Item-level coupon |
discount | NUMBER | Discount amount |
index | NUMBER | Position in list |
item_brand | STRING | Brand name |
item_category | STRING | Primary category |
item_category2-5 | STRING | Category levels 2-5 |
item_list_id | STRING | List ID |
item_list_name | STRING | List name |
item_variant | STRING | Variant (color, size) |
location_id | STRING | Physical location |
price | NUMBER | Item price |
currency | STRING | ISO 4217 code |
quantity | NUMBER | Quantity |
promotion_id | STRING | Promotion ID |
promotion_name | STRING | Promotion name |
creative_name | STRING | Promotion creative |
creative_slot | STRING | Creative slot |
At least one of item_id or item_name is required. Max 200 items per event.
Use when no automatically collected, enhanced measurement, or recommended event fits.
[a-zA-Z][a-zA-Z0-9_]*Add_To_Cart and add_to_cart are different eventsfirebase_, google_, ga_first_visit, session_start, app_install, in_app_purchase, etc.)| Limit | Value |
|---|---|
| Distinct event names, web streams | No limit |
| Distinct event names, app streams | 500 per app user (auto-collected and enhanced measurement events excluded) |
| Parameters per event | 25 |
| Parameter name length | 40 characters |
| Parameter value (string) | 100 characters |
| User property name length | 24 characters |
| User property value (string) | 36 characters |
| User properties per project | 25 |
gtag('event', 'newsletter_signup', {
newsletter_type: 'weekly_digest', signup_location: 'footer'
});
gtag('event', 'feature_used', {
feature_name: 'export_csv', feature_category: 'data_tools', plan_tier: 'pro'
});
gtag('event', 'article_read', {
article_id: 'post_12345', article_category: 'technology', read_time_seconds: 245
});Register event parameters or user properties for reporting in Admin > Custom definitions.
| Scope | Source | Use Case |
|---|---|---|
| Event-scoped dimension | Event parameter | Page/action attributes |
| User-scoped dimension | User property | Persistent user attributes |
| Custom metric | Event parameter (numeric) | Numeric aggregation |
| Resource | Standard | Analytics 360 |
|---|---|---|
| Event-scoped custom dimensions | 50 | 125 |
| User-scoped custom dimensions | 25 | 100 |
| Custom metrics | 50 | 125 |
Processing time: 24-48 hours for standard reports (immediate in Realtime/DebugView).
// Set user properties
gtag('set', 'user_properties', {
membership_tier: 'gold', signup_date: '2025-01-15'
});// Custom event
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'newsletter_signup',
newsletter_type: 'weekly_digest',
signup_location: 'footer'
});
// E-commerce — ALWAYS clear ecommerce first
window.dataLayer.push({ ecommerce: null });
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 }]
}
});POST https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET
{
"client_id": "abc123.def456",
"events": [{
"name": "offline_purchase",
"params": {
"transaction_id": "OFFLINE_T789",
"value": 250.00,
"currency": "USD"
}
}]
}import requests, json
url = 'https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET'
payload = {
'client_id': 'abc123.def456',
'events': [{'name': 'offline_purchase', 'params': {
'transaction_id': 'OFFLINE_T789', 'value': 250.00, 'currency': 'USD'
}}]
}
response = requests.post(url, data=json.dumps(payload))Limitations: No response validation (use debug endpoint); events are joined to online activity via client_id / app_instance_id, so reuse the real _ga client ID; include session_id (within 24 hours of session start) to tie the event to a session; reserved names like first_visit/session_start can't be sent. MP is meant to supplement gtag/GTM/Firebase, not replace them. MP events do show in Realtime within seconds; add debug_mode: 1 and engagement_time_msec to see them in DebugView.
POST https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET
# Returns validation errors instead of silently acceptingEnable debug mode to see events in near real-time in Admin > DebugView:
gtag('config', 'G-XXXXXXXXXX', { debug_mode: true });
// Or per-event:
gtag('event', 'purchase', { debug_mode: true, transaction_id: 'T12345', value: 99.99, currency: 'USD' });If Cogny isn't connected, skip this and note what connecting adds.
tool_run_realtime_report (property_id, metrics: [eventCount],dimensions: [eventName]) right after a test, then tool_run_report over 7 days with dimensions: [eventName] to see steady-state volume.
get_events_volume (start_time,end_time, group_by: [eventName], or event_name: "Order Completed"). Events in Segment but missing in GA4 mean the GA4 destination mapping is wrong, not the site code.
list_tracking_plan_rules (tracking_plan_id fromlist_tracking_plans) and PostHog list_event_definitions (project_id, search). Map each to its GA4 name in one table (Segment Order Completed → purchase, Product Added → add_to_cart) and flag actions with no GA4 equivalent.
list_experiments (status: running) gives each test'stracking key. GA4 should receive experiment_viewed with experiment_id = that key and variation_id; register both as event-scoped custom dimensions, or the GA4 split is invisible.
Name the tool on every count; Segment counts calls, GA4 counts events after its own filters.
Event volume audit:
SELECT event_name, COUNT(*) as event_count, COUNT(DISTINCT user_pseudo_id) as unique_users
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
GROUP BY event_name ORDER BY event_count DESCMissing purchase parameters:
SELECT '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 'missing_currency', 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 NULLDuplicate transactions:
SELECT transaction_id, COUNT(*) as dupes
FROM (
SELECT (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') as transaction_id
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 dupes DESCNaming convention audit:
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) 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'Parameter truncation check:
SELECT event_name, ep.key, MAX(LENGTH(ep.value.string_value)) as max_len,
COUNTIF(LENGTH(ep.value.string_value) >= 100) as at_limit
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 1, 2 HAVING max_len >= 100Purchase != purchase. Always use snake_case.transaction_id for dedup (72-hour window).value without currency is ignored in monetization reports.{ ecommerce: null } before ecommerce events in GTM.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.