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_viewsession_startfirst_visituser_engagementscroll(90% scroll depth)click(outbound links, with Enhanced Measurement)file_downloadvideo_start,video_progress,video_complete(YouTube embeds)
2. Enhanced Measurement Events
Enabled in GA4 settings, these track common interactions:
scrolloutbound_clicksite_searchvideo_engagementfile_downloadform_interaction
3. Recommended Events
Google’s predefined events with expected parameters. Use these when they match your use case:
Ecommerce:
view_item,add_to_cart,begin_checkout,purchaseview_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.
When to Use Custom vs Recommended Events
Use Recommended Events When:
- Your action matches a recommended event’s purpose
- You want automatic reporting features
- You want cross-platform comparability
Example: Tracking a purchase? Use purchase, not completed_order.
Use Custom Events When:
- No recommended event matches your action
- You have industry-specific interactions
- 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:
- Variables → New → Data Layer Variable
- Name:
dlv_pricing_plan_viewed - Data Layer Variable Name:
pricing_plan_viewed
Step 4: Create the GA4 Event Tag
- Tags → New
- Tag Type: Google Analytics: GA4 Event
- Configuration Tag: Select your GA4 Config tag
- Event Name:
pricing_engaged - Event Parameters:
- Parameter Name:
engagement_time - Value:
{{dlv_engagement_time}} - Parameter Name:
plan_viewed - Value:
{{dlv_pricing_plan_viewed}}
- Parameter Name:
Step 5: Create the Trigger
- Triggers → New
- Trigger Type: Custom Event
- Event name:
pricing_engaged(matches your dataLayer push)
Step 6: Test in GTM Preview
- Click Preview in GTM
- Navigate to your site
- Trigger the action
- 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:
- Realtime Report: Reports → Realtime → Event count
- DebugView: Configure → DebugView
Step 2: Find Events in Reports
- Reports → Engagement → Events
- Search for your custom event name
- Click the event to see parameters
Step 3: Register Custom Definitions
To use custom parameters in reports, register them:
- Admin → Custom definitions → Create custom dimensions
- Dimension name: Your parameter (e.g., “Resource Type”)
- Scope: Event
- 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:
- Explore → Create new exploration
- Add your custom event as a metric
- Add your custom dimensions
- 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
- Verify parameter names in GTM match exactly
- Check that parameters have values (not undefined/null)
- 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:
- Admin → Custom definitions → Create custom metrics
- Name: “Quiz Score”
- Event parameter:
quiz_score - 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.