GA4 Custom Events: Complete Setup Guide (With Examples)

Learn how to create and implement custom events in GA4. Covers when to use custom vs recommended events, GTM setup, event parameters, and reporting.

GA4custom eventsGTMevent trackingGoogle Analytics

GA4’s event-based model gives you complete flexibility to track what matters to your business. But with that flexibility comes confusion: What should be a custom event? How do you set it up correctly? Why isn’t your event showing up in reports?

Here’s the complete guide to GA4 custom events.

Understanding GA4’s Event Types

GA4 has four categories of events:

1. Automatically Collected Events

GA4 tracks these without any configuration:

  • page_view
  • session_start
  • first_visit
  • user_engagement
  • scroll (90% scroll depth)
  • click (outbound links, with Enhanced Measurement)
  • file_download
  • video_start, video_progress, video_complete (YouTube embeds)

2. Enhanced Measurement Events

Enabled in GA4 settings, these track common interactions:

  • scroll
  • outbound_click
  • site_search
  • video_engagement
  • file_download
  • form_interaction

Google’s predefined events with expected parameters. Use these when they match your use case:

Ecommerce:

  • view_item, add_to_cart, begin_checkout, purchase
  • view_item_list, select_item, add_to_wishlist

Lead Generation:

  • generate_lead, sign_up, login

Content:

  • share, search, select_content

4. Custom Events

Events you define yourself. Use these when recommended events don’t fit.

  1. Your action matches a recommended event’s purpose
  2. You want automatic reporting features
  3. You want cross-platform comparability

Example: Tracking a purchase? Use purchase, not completed_order.

Use Custom Events When:

  1. No recommended event matches your action
  2. You have industry-specific interactions
  3. You need to track proprietary features

Example: User clicks “Compare Products” feature → Custom event compare_products.

Creating Custom Events in GTM

Here’s the complete process for creating custom events via Google Tag Manager.

Step 1: Define Your Event

Before touching GTM, define:

  • Event name: lowercase, underscores, descriptive (e.g., pdf_download, pricing_view, feature_toggle)
  • Event parameters: Additional context (e.g., pdf_name, plan_type, feature_name)
  • Trigger condition: When should this fire?

Step 2: Create a Data Layer Push (If Needed)

For complex triggers, push to the data layer:

// Example: Track when user views pricing page for 30+ seconds
let pricingTimeStart = null;

if (window.location.pathname === '/pricing') {
  pricingTimeStart = Date.now();

  setTimeout(function() {
    dataLayer.push({
      'event': 'pricing_engaged',
      'engagement_time': 30,
      'pricing_plan_viewed': 'enterprise'
    });
  }, 30000);
}

Step 3: Create GTM Variables

Create Data Layer Variables for your event parameters:

  1. Variables → New → Data Layer Variable
  2. Name: dlv_pricing_plan_viewed
  3. Data Layer Variable Name: pricing_plan_viewed

Step 4: Create the GA4 Event Tag

  1. Tags → New
  2. Tag Type: Google Analytics: GA4 Event
  3. Configuration Tag: Select your GA4 Config tag
  4. Event Name: pricing_engaged
  5. Event Parameters:
    • Parameter Name: engagement_time
    • Value: {{dlv_engagement_time}}
    • Parameter Name: plan_viewed
    • Value: {{dlv_pricing_plan_viewed}}

Step 5: Create the Trigger

  1. Triggers → New
  2. Trigger Type: Custom Event
  3. Event name: pricing_engaged (matches your dataLayer push)

Step 6: Test in GTM Preview

  1. Click Preview in GTM
  2. Navigate to your site
  3. Trigger the action
  4. Verify:
    • Custom event appears in event list
    • Tag fires correctly
    • Parameters have values

Event Naming Best Practices

Follow Google’s Conventions:

// GOOD: Lowercase with underscores
'event': 'video_completed'
'event': 'form_submitted'
'event': 'feature_used'

// BAD: Various other formats
'event': 'VideoCompleted'     // PascalCase
'event': 'video-completed'    // Hyphens
'event': 'Video Completed'    // Spaces
'event': 'VIDEOCOMPLETED'     // Uppercase

Be Descriptive But Concise:

// GOOD: Clear and specific
'event': 'newsletter_signup'
'event': 'product_comparison'
'event': 'chat_started'

// BAD: Too vague or too long
'event': 'signup'             // Which signup?
'event': 'user_clicked_the_newsletter_signup_button_in_footer'  // Too long

Use Consistent Prefixes for Categories:

// Group related events with prefixes
'event': 'video_start'
'event': 'video_complete'
'event': 'video_progress'

'event': 'form_start'
'event': 'form_submit'
'event': 'form_error'

'event': 'cta_click'
'event': 'cta_hover'

Event Parameters Best Practices

Parameter Limits:

  • 25 custom parameters per event
  • 50 characters max for parameter names
  • 100 characters max for parameter values
  • 500 unique event names per property

Required vs Optional Parameters:

// For a custom 'resource_download' event
dataLayer.push({
  'event': 'resource_download',
  // Required - you always need these
  'resource_type': 'whitepaper',    // What type of resource?
  'resource_name': 'SEO Guide 2024', // Which specific resource?

  // Optional - nice to have
  'resource_category': 'marketing', // Categorization
  'download_location': 'hero_cta',  // Where on page?
  'user_type': 'free'               // Logged in status
});

Use Consistent Parameter Names:

// GOOD: Same parameter name across events
'event': 'resource_download', 'content_type': 'whitepaper'
'event': 'resource_view', 'content_type': 'case_study'
'event': 'resource_share', 'content_type': 'infographic'

// BAD: Different names for same concept
'event': 'resource_download', 'type': 'whitepaper'
'event': 'resource_view', 'content_category': 'case_study'
'event': 'resource_share', 'resource_kind': 'infographic'

Common Custom Event Examples

Example 1: CTA Button Clicks

// Data layer push
document.querySelectorAll('[data-cta]').forEach(function(button) {
  button.addEventListener('click', function() {
    dataLayer.push({
      'event': 'cta_click',
      'cta_text': this.innerText,
      'cta_location': this.dataset.ctaLocation || 'unknown',
      'cta_destination': this.href
    });
  });
});

GTM Tag Configuration:

  • Event Name: cta_click
  • Parameters: cta_text, cta_location, cta_destination

Example 2: Scroll Milestones

// Track 25%, 50%, 75%, 100% scroll
let scrollMarks = [25, 50, 75, 100];
let firedMarks = [];

window.addEventListener('scroll', function() {
  let scrollPercent = Math.round(
    (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
  );

  scrollMarks.forEach(function(mark) {
    if (scrollPercent >= mark && !firedMarks.includes(mark)) {
      firedMarks.push(mark);
      dataLayer.push({
        'event': 'scroll_milestone',
        'scroll_depth': mark,
        'page_path': window.location.pathname
      });
    }
  });
});

Example 3: Video Engagement (Non-YouTube)

// For custom video players (Vimeo, self-hosted, etc.)
const video = document.querySelector('video');

video.addEventListener('play', function() {
  dataLayer.push({
    'event': 'video_play',
    'video_title': this.dataset.title,
    'video_duration': Math.round(this.duration)
  });
});

video.addEventListener('ended', function() {
  dataLayer.push({
    'event': 'video_complete',
    'video_title': this.dataset.title,
    'video_duration': Math.round(this.duration)
  });
});

Example 4: Feature Usage

// Track usage of a specific product feature
function trackFeatureUse(featureName, details) {
  dataLayer.push({
    'event': 'feature_used',
    'feature_name': featureName,
    'feature_details': details,
    'user_plan': getUserPlan() // Your function to get user's plan
  });
}

// Usage
document.getElementById('export-btn').addEventListener('click', function() {
  trackFeatureUse('data_export', { format: 'csv', rows: 500 });
});

Example 5: Form Field Interaction

// Track which form fields users interact with
const form = document.getElementById('contact-form');
const trackedFields = new Set();

form.querySelectorAll('input, textarea, select').forEach(function(field) {
  field.addEventListener('focus', function() {
    if (!trackedFields.has(this.name)) {
      trackedFields.add(this.name);
      dataLayer.push({
        'event': 'form_field_focus',
        'form_name': 'contact_form',
        'field_name': this.name,
        'field_type': this.type
      });
    }
  });
});

Viewing Custom Events in GA4

Step 1: Wait for Data

Custom events take 24-48 hours to appear in standard reports. For immediate testing, use:

  1. Realtime Report: Reports → Realtime → Event count
  2. DebugView: Configure → DebugView

Step 2: Find Events in Reports

  1. Reports → Engagement → Events
  2. Search for your custom event name
  3. Click the event to see parameters

Step 3: Register Custom Definitions

To use custom parameters in reports, register them:

  1. Admin → Custom definitions → Create custom dimensions
  2. Dimension name: Your parameter (e.g., “Resource Type”)
  3. Scope: Event
  4. Event parameter: resource_type

Important: You can only register 50 custom dimensions (free) or 125 (GA4 360).

Step 4: Build Custom Reports

Create explorations using your custom events:

  1. Explore → Create new exploration
  2. Add your custom event as a metric
  3. Add your custom dimensions
  4. Build your report

Debugging Custom Events

Issue: Event Not Appearing in DebugView

// Enable debug mode
gtag('config', 'G-XXXXXXX', { 'debug_mode': true });

// Or in GTM, add parameter to GA4 Config tag:
// debug_mode = true

Issue: Parameters Not Showing

  1. Verify parameter names in GTM match exactly
  2. Check that parameters have values (not undefined/null)
  3. Confirm custom definitions are registered
// Debug in console:
dataLayer.filter(d => d.event === 'your_event_name');
// Check that parameters have values

Issue: Event Fires Multiple Times

// Add deduplication
let eventFired = {};

function trackOnce(eventName, params) {
  const key = eventName + JSON.stringify(params);
  if (eventFired[key]) return;

  eventFired[key] = true;
  dataLayer.push({
    'event': eventName,
    ...params
  });
}

Custom Events Checklist

  • Event name follows naming conventions (lowercase, underscores)
  • Parameters are defined and have values
  • GTM tag is configured correctly
  • Trigger fires at the right time
  • Event appears in DebugView
  • Custom dimensions registered in GA4
  • Event appears in Reports → Events
  • No duplicate events firing

Advanced: Event-Scoped Custom Metrics

For numeric values you want to aggregate (totals, averages):

dataLayer.push({
  'event': 'quiz_completed',
  'quiz_score': 85,           // Register as custom metric
  'quiz_time_seconds': 240    // Register as custom metric
});

Register in GA4:

  1. Admin → Custom definitions → Create custom metrics
  2. Name: “Quiz Score”
  3. Event parameter: quiz_score
  4. Unit of measurement: Standard

Now you can calculate average quiz scores, total time spent, etc.

Need Help With Custom Events?

Custom events are powerful but easy to misconfigure. If your events aren’t showing up or parameters are missing, the issue is usually subtle—a typo, timing problem, or configuration mismatch.

Get a free tracking audit and we’ll review your custom event implementation, identify issues, and ensure your data is flowing correctly.