<!-- Cogny documentation. Canonical page: https://cogny.com/docs/ga4-event-implementation -->

# 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.

**Author:** Cogny Team  
**Published:** 2025-02-11  
**Updated:** 2026-10-09  
**Canonical:** https://cogny.com/docs/ga4-event-implementation

## 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`)

```javascript
// 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 setting
- `page_location` — Full URL (web) or screen name (app)
- `page_referrer` — Previous page URL (web)
- `page_title` — HTML document title (web)
- `screen_resolution` — Display resolution
- `ga_session_id` — Session identifier
- `ga_session_number` — Sequential session count for the user
- `engagement_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:

1. Navigate to **Admin > Data Streams > select your web stream**
2. Click **Enhanced measurement** toggle section
3. Enable or disable individual event types
4. For site search, click the gear icon to configure custom query parameters

```javascript
// 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.

```javascript
gtag('event', 'login', {
  method: 'Google'  // STRING — login method (e.g., 'Google', 'Email', 'Facebook')
});
```

#### sign_up

Sent when a user registers an account.

```javascript
gtag('event', 'sign_up', {
  method: 'Email'  // STRING — registration method
});
```

#### share

Sent when a user shares content.

```javascript
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.

```javascript
gtag('event', 'search', {
  search_term: 'running shoes'  // STRING — the search query
});
```

#### select_content

Sent when a user selects content.

```javascript
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).

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
// 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.

```javascript
gtag('event', 'generate_lead', {
  currency: 'USD',
  value: 50.00   // NUMBER — estimated value of the lead
});
```

### Gaming

#### earn_virtual_currency

```javascript
gtag('event', 'earn_virtual_currency', {
  virtual_currency_name: 'Coins',  // STRING — name of the virtual currency
  value: 100                        // NUMBER — amount earned
});
```

#### spend_virtual_currency

```javascript
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

```javascript
gtag('event', 'level_up', {
  level: 5,             // NUMBER — new level reached
  character: 'Warrior'  // STRING — character that leveled up
});
```

#### post_score

```javascript
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

```javascript
gtag('event', 'tutorial_begin');
// No required parameters
```

#### tutorial_complete

```javascript
gtag('event', 'tutorial_complete');
// No required parameters
```

#### unlock_achievement

```javascript
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_Cart` and `add_to_cart` are 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

```javascript
// 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:

```javascript
// 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

```javascript
// 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.

```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:

```javascript
// 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.

```javascript
// 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:
1. **Trigger**: Custom Event trigger matching `cta_click`
2. **Tag**: GA4 Event tag with event name `cta_click`
3. **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.

```bash
# 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
# 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_id` must 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_visit` or `session_start` events

### Validation Server (Debug Endpoint)

Use the validation endpoint during development to catch payload errors before sending to production:

```bash
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:

```javascript
// 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

```sql
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

```sql
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

```sql
-- 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

```sql
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

```sql
-- 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

```sql
-- 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.

```javascript
// 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.

```javascript
// 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.

```javascript
// 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.

```javascript
// 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.

```javascript
// 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.

```javascript
// 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:

**[ga4-events Skill on GitHub](https://github.com/growthhackers-sthlm/claude-code-marketing-skills/tree/main/skills/ga4-events)**

Install by adding to your `.claude/settings.json`:

```json
{
  "permissions": {
    "allow": [
      "skill:ga4-events"
    ]
  }
}
```

---

In this section:

- Conversion Tracking Debugger Reference: https://cogny.com/docs/conversion-tracking-debugger
- Core Web Vitals Reference: https://cogny.com/docs/core-web-vitals
- GA4 BigQuery Export Schema Reference: https://cogny.com/docs/ga4-bigquery-export-schema
- **GA4 Event Implementation Reference** (this page): https://cogny.com/docs/ga4-event-implementation
- Google Ads Query Language (GAQL) Reference: https://cogny.com/docs/gaql-reference
- Google Ads Scripts Reference: https://cogny.com/docs/google-ads-scripts
- GTM Event Tracking & Setup Reference: https://cogny.com/docs/gtm-event-tracking
- Meta Conversions API (CAPI) Setup Reference: https://cogny.com/docs/meta-conversions-api
- Schema.org Structured Data Reference: https://cogny.com/docs/structured-data-reference
- UTM Parameter Strategy & Builder Reference: https://cogny.com/docs/utm-strategy

Source page: https://cogny.com/docs/ga4-event-implementation  
All documentation: https://cogny.com/docs  
Cogny for agents: https://cogny.com/llms.txt
