Google Analytics Custom Events: A Complete JavaScript Setup Guide
Learn how to set up custom JavaScript events in Google Analytics 4 — from form submissions and CTA clicks to scroll depth and video tracking — plus how to automate reporting with Looker Studio.
Most people install Google Analytics, paste the tracking code, and call it done. They get pageviews, session duration, and bounce rate — the basics. But the real value comes from tracking the things that actually matter to your business: form submissions, button clicks, video plays, scroll depth, file downloads.
These are called custom events, and setting them up with JavaScript is simpler than most people think. Here's how to do it properly, plus how to automate the reporting so you actually use the data.
The two approaches: gtag vs GTM
There are two ways to send custom events to Google Analytics:
gtag (direct) — You call gtag('event', ...) directly in your JavaScript code. This is simpler, more explicit, and easier to debug. I prefer this for most sites.
Google Tag Manager (GTM) — You configure triggers and tags in the GTM interface, and GTM fires the events. This is more powerful for complex setups and lets non-developers add tracking without touching code. But it adds a layer of abstraction that can be frustrating to debug.
This guide covers the gtag approach because it's the one I use most often and it's easier to understand from a developer's perspective. The event names and parameters are the same either way.
Prerequisites
You need Google Analytics 4 (GA4) installed on your site. If you're still on Universal Analytics, you need to migrate — UA stopped processing data in July 2024.
Your GA4 tracking code should look something like this in your <head>:
``html <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> ``
Once that's in place, you can fire custom events from anywhere in your JavaScript.
Basic custom event structure
A custom event has a name and optional parameters:
``javascript gtag('event', 'event_name', { parameter_1: 'value_1', parameter_2: 'value_2' }); ``
The event name should use snake_case and describe what happened. Google has a list of recommended events that unlock built-in reports. Use those when they match your use case. For everything else, use descriptive custom names.
Essential custom events to track
1. Form submissions
The most important event for most business sites. Track when someone submits a contact form, newsletter signup, or quote request.
```javascript const contactForm = document.getElementById('contact-form');
contactForm.addEventListener('submit', (event) => { event.preventDefault();
// Send the event before the form submits gtag('event', 'generate_lead', { form_type: 'contact', form_location: window.location.pathname });
// Then actually submit the form contactForm.submit(); }); ```
For AJAX forms, fire the event on success:
``javascript fetch('/api/contact', { method: 'POST', body: new FormData(contactForm) }) .then((response) => { if (response.ok) { gtag('event', 'generate_lead', { form_type: 'contact', form_location: window.location.pathname }); } }); ``
2. Button clicks and CTAs
Track when people click your call-to-action buttons. This tells you which CTAs actually convert.
```javascript document.querySelectorAll('[data-ga-event]').forEach((element) => { element.addEventListener('click', () => { const eventName = element.dataset.gaEvent; const eventCategory = element.dataset.gaCategory || 'button'; const eventLabel = element.dataset.gaLabel || element.textContent.trim();
gtag('event', eventName, { event_category: eventCategory, event_label: eventLabel, page_location: window.location.pathname }); }); }); ```
Then in your HTML:
``html <button data-ga-event="cta_click" data-ga-category="pricing" data-ga-label="start-trial"> Start Free Trial </button> ``
This data-attribute approach is clean because you can add tracking to any element without writing new JavaScript for each one.
3. Scroll depth
Knowing how far people scroll tells you whether they're actually reading your content. Track 25%, 50%, 75%, and 100%.
```javascript function trackScrollDepth() { const milestones = [25, 50, 75, 100]; const tracked = new Set();
window.addEventListener('scroll', () => { const scrollTop = window.scrollY; const docHeight = document.documentElement.scrollHeight - window.innerHeight; const scrollPercent = Math.round((scrollTop / docHeight) * 100);
milestones.forEach((milestone) => { if (scrollPercent >= milestone && !tracked.has(milestone)) { tracked.add(milestone); gtag('event', 'scroll_depth', { percentage: milestone, page_location: window.location.pathname }); } }); }); }
trackScrollDepth(); ```
4. Video engagement
If you embed videos on your site, track when people play, pause, and complete them.
```javascript const video = document.getElementById('hero-video');
video.addEventListener('play', () => { gtag('event', 'video_start', { video_title: video.dataset.title || 'Hero Video', video_url: video.currentSrc }); });
video.addEventListener('ended', () => { gtag('event', 'video_complete', { video_title: video.dataset.title || 'Hero Video', video_url: video.currentSrc }); });
// Track 25%, 50%, 75% progress const progressTracked = new Set(); video.addEventListener('timeupdate', () => { const percent = Math.floor((video.currentTime / video.duration) * 100); [25, 50, 75].forEach((milestone) => { if (percent >= milestone && !progressTracked.has(milestone)) { progressTracked.add(milestone); gtag('event', 'video_progress', { video_title: video.dataset.title || 'Hero Video', percentage: milestone }); } }); }); ```
5. File downloads
Track when people download PDFs, whitepapers, or other resources.
```javascript document.addEventListener('click', (event) => { const link = event.target.closest('a'); if (!link) return;
const href = link.getAttribute('href'); if (!href) return;
const downloadExtensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.zip', '.csv']; const isDownload = downloadExtensions.some((ext) => href.toLowerCase().endsWith(ext));
if (isDownload) { gtag('event', 'file_download', { file_name: href.split('/').pop(), file_extension: href.split('.').pop().toLowerCase(), link_url: href, page_location: window.location.pathname }); } }); ```
6. Site search
If your site has a search feature, track what people search for. This is gold for content strategy.
```javascript const searchForm = document.getElementById('site-search');
searchForm.addEventListener('submit', (event) => { event.preventDefault(); const searchTerm = searchForm.querySelector('input').value.trim();
if (searchTerm) { gtag('event', 'search', { search_term: searchTerm }); }
searchForm.submit(); }); ```
7. Outbound link clicks
Track when people leave your site through external links — especially affiliate links or partner referrals.
```javascript document.addEventListener('click', (event) => { const link = event.target.closest('a'); if (!link) return;
const href = link.getAttribute('href'); if (!href) return;
const isExternal = href.startsWith('http') && !href.includes(window.location.hostname);
if (isExternal) { gtag('event', 'click', { link_url: href, link_text: link.textContent.trim(), outbound: true }); } }); ```
Organizing your events with a utility module
Instead of scattering gtag() calls everywhere, wrap them in a small utility. This gives you type safety, consistent naming, and a single place to debug.
```javascript // analytics.js const Analytics = { trackFormSubmission(formType, location) { gtag('event', 'generate_lead', { form_type: formType, form_location: location }); },
trackCTAClick(category, label) { gtag('event', 'cta_click', { event_category: category, event_label: label, page_location: window.location.pathname }); },
trackScrollDepth(percentage) { gtag('event', 'scroll_depth', { percentage, page_location: window.location.pathname }); },
trackFileDownload(fileName, fileUrl) { gtag('event', 'file_download', { file_name: fileName, link_url: fileUrl, file_extension: fileName.split('.').pop() }); },
trackSearch(searchTerm) { gtag('event', 'search', { search_term: searchTerm }); },
trackVideo(videoTitle, action, percentage) { gtag('event', video_${action}, { video_title: videoTitle, percentage }); } };
export default Analytics; ```
Now your application code stays clean:
```javascript import Analytics from './analytics';
contactForm.addEventListener('submit', () => { Analytics.trackFormSubmission('contact', window.location.pathname); }); ```
Testing your events
Before you trust your data, verify that events are firing correctly.
Use GA4 DebugView. Go to Admin > DebugView in your GA4 property. Events appear in real time as you trigger them on your site. This is the fastest way to confirm an event is working.
Use the browser console. Add a debug mode to your analytics utility:
```javascript const DEBUG = window.location.hostname === 'localhost';
const Analytics = { track(eventName, params) { if (DEBUG) { console.log([Analytics] ${eventName}, params); } gtag('event', event_name, params); } }; ```
Use the Google Analytics Debugger Chrome extension. It logs every event to the console with full parameter details.
Automated reporting with Looker Studio
Tracking events is only half the battle. If you have to log into GA4 every time you want to see your data, you'll stop checking after two weeks. Automated reporting solves this.
Option 1: Looker Studio (free, recommended)
Looker Studio (formerly Google Data Studio) connects directly to GA4 and lets you build dashboards that update automatically.
- Go to lookerstudio.google.com
- Create a new report and connect your GA4 property as a data source
- Build charts for your custom events:
Form submissions over time:
- Chart type: Time series
- Metric: Event count
- Filter: event_name = generate_lead
CTA click breakdown:
- Chart type: Bar chart
- Dimension: event_label
- Metric: Event count
- Filter: event_name = cta_click
Scroll depth distribution:
- Chart type: Pie chart
- Dimension: percentage (from your scroll_depth event)
- Metric: Event count
- Filter: event_name = scroll_depth
Top downloaded files:
- Chart type: Table
- Dimension: file_name
- Metric: Event count
- Filter: event_name = file_download
- Click "Share" and schedule email delivery. You can have the report emailed to you (and your team or clients) as a PDF every week or month.
Option 2: Google Sheets with the GA4 add-on
If you prefer spreadsheets, the Google Analytics add-on for Google Sheets pulls data from GA4 on a schedule.
- Install the Google Analytics add-on for Google Sheets
- Create a new report with the parameters you want
- Schedule it to run daily or weekly
This is useful if you want to combine GA4 data with other data sources (like your CRM or ad spend) in a single spreadsheet.
Option 3: Custom email reports with the GA4 Data API
For full control, use the GA4 Data API to pull event data and send custom email reports. Here's a Node.js example that runs on a schedule:
```javascript const { BetaAnalyticsDataClient } = require('@google-analytics/data');
const analyticsDataClient = new BetaAnalyticsDataClient();
async function getFormSubmissions(propertyId, startDate, endDate) { const [response] = await analyticsDataClient.runReport({ property: properties/${propertyId}, dateRanges: [{ startDate, endDate }], metrics: [{ name: 'eventCount' }], dimensions: [ { name: 'customEvent:form_type' }, { name: 'date' } ], dimensionFilter: { filter: { fieldName: 'eventName', stringFilter: { value: 'generate_lead' } } } });
return response.rows; } ```
You can run this on a cron job, AWS Lambda, or GitHub Actions and email the results using a service like SendGrid or Resend.
Setting up conversions
Custom events become more powerful when you mark them as conversions in GA4. This lets you see which traffic sources, campaigns, and pages drive the actions you care about.
- Go to Admin > Events > Conversions
- Click "New conversion event"
- Enter your event name (e.g.,
generate_lead) - Save
Now your form submissions appear in the Conversions report, and you can see which channels drive the most leads.
Common mistakes to avoid
Firing events on page load. If you fire a generate_lead event every time a thank-you page loads, you'll double-count if someone refreshes. Use a cookie or session storage to prevent duplicates.
Using spaces or special characters in event names. Stick to snake_case: video_start, not video start or VideoStart. GA4 is picky about this.
Not setting up conversions. Custom events are just noise until you mark the important ones as conversions. Do this early.
Tracking too much. Not every click needs an event. Focus on the actions that tie to business outcomes: leads, purchases, signups, downloads. Everything else is noise.
Forgetting to test. Always verify events in DebugView before trusting your reports. A typo in an event name means weeks of missing data.
The minimum viable setup
If you do nothing else, track these three things:
- Form submissions —
generate_leadwith aform_typeparameter - CTA clicks —
cta_clickwith aevent_labelparameter - Key page views as conversions — Mark your thank-you or confirmation page views as conversions
That alone will tell you more about your site's performance than pageviews ever will.
Wrapping up
Custom events turn Google Analytics from a vanity metric dashboard into a tool that actually informs business decisions. The setup takes an afternoon. The payoff is permanent.
Start with form submissions and CTA clicks. Add scroll depth and video tracking if they matter to your content strategy. Set up a Looker Studio dashboard and schedule it to land in your inbox every Monday morning. Within a month, you'll wonder how you made decisions without it.
If you want help setting up custom event tracking or automated reporting for your site, get in touch. I do this for client sites regularly and can usually have it running in a day.
Want help shipping this on your own site?
Free 30-minute consultation. No pressure either way.
