I’m a firm believer that adding more options and controls often results in a degraded user experience, and so in order to make the determination of whether to keep certain filtering features on the Winter Light Festival, I need to collect user behavior information. So let’s create some custom events for those specific user actions, like clicking on filter checkboxs, select boxes, or buttons.

The first step is to create a separate Google Analytics 4 account for my local development environment. The live site uses the Google Site Kit plugin so that staff of the WLF can quickly see highlights. For my local environment, I want this deactivated (and other plugins that provide no benefit locally), and to just load the new GA4 script.

Because I define the 'WP_ENVIRONMENT_TYPE' constant in my wp-config.php file, I can add some code into my required functionality mu-plugin that only loads locally.

// when on local development we want the site to behave differently.
if ( defined( 'WP_ENVIRONMENT_TYPE' ) && WP_ENVIRONMENT_TYPE === 'local' ) {
	add_action( 'wp_footer', 'add_localdev_google_analytics' );

	// List of plugins to deactivate locally.
	$plugins_to_disable = [
		'google-site-kite/google-site-kite.php',
		'wordfence/wordfence.php',
		'wp-mail-smtp/wp_mail_smtp.php',
		'cloudflare/cloudflare.php',
		'litespeed-cache/litespeed-cache.php',
		'wp-mail-logging/wp-mail-logging.php',
		'wp-mail-log/wp-mail-log.php',
	];

	add_action(
		'plugins_loaded',
		function () use ( $plugins_to_disable ) {
			foreach ( $plugins_to_disable as $plugin ) {
				if ( is_plugin_active( $plugin ) ) {
					deactivate_plugins( $plugin );
				}
			}
		}
	);
}

/**
 * Adds the local development Google Analytics tracking code to the footer.
 */
function add_localdev_google_analytics() {
	?>
	<!-- Google tag (gtag.js) -->
	<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXX"></script>
	<script>
		console.log('The development Google Analytics tracking code is active.');
		window.dataLayer = window.dataLayer || [];
		function gtag(){dataLayer.push(arguments);}
		gtag('js', new Date());
		gtag('config', 'G-XXXXXXXXX');
	</script>
	<?php
}

After confirming that I’m sending data to the GA account, we’re ready to create some custom events

After inspecting the front end html output of our program and it’s filtering system, I collected a list of the elements that I want to track

Into my src/main.js file, I then added the following javascript to record buttons and filter usage. The filter type is accessed via the class and which particular filter is clicked is going to be recorded by way of the data value.

// Utility function to send data to GA4
function sendGAEvent(eventName, params) {
    gtag('event', eventName, params);
}

// Track button clicks
const buttons = document.querySelectorAll('.btn:not([data-track="false"]), a.button, button');

if ([...buttons].length > 0) {
    buttons.forEach(button => {

        button.addEventListener('click', e => {
            const buttonTitle = e.currentTarget.textContent.trim();
            const buttonId = e.currentTarget.id || null;
            const buttonType = e.currentTarget.getAttribute('data-type') || null;
            const buttonLink = e.currentTarget.href || null;

			//console.log('button has been clicked');

            sendGAEvent('wlf_btn_click', {
                event_category: 'click',
                page_title: document.title,
                page_location: window.location.href,
                btn_title: buttonTitle,
                btn_id: buttonId,
                btn_type: buttonType,
                btn_link_url: buttonLink,
            });
        });
    });
}

// Track FacetWP reset button clicks. This ensures that the reset button click is tracked even if it is dynamically added or updated by FacetWP.
document.addEventListener('click', function( e ) {
    const selectors = ['.facetwp-reset', 'button.facetwp-map-filtering'];
    if ( selectors.some(selector => e.target && e.target.matches(selector)) ) {
        const buttonTitle = e.target.textContent.trim();
        const buttonId = e.target.id || null;
        const buttonType = e.target.getAttribute('data-type') || null;
        const buttonLink = e.target.href || null;

        //alert('FacetWP reset button has been clicked');

        sendGAEvent('wlf_btn_click', {
            event_category: 'click',
            page_title: document.title,
            page_location: window.location.href,
            btn_title: buttonTitle,
            btn_id: buttonId,
            btn_type: buttonType,
            btn_link_url: buttonLink,
        });
    }
});

// Track FacetWP filtering interactions
function trackFacetInteractions() {
    document.querySelectorAll(
        '.facetwp-checkbox:not(.checked), .facetwp-radio:not(.checked)').forEach(facet => {
        facet.addEventListener('click', e => {

            const dataValue = e.currentTarget.getAttribute('data-value') || null;
            const facetType = e.currentTarget.className;
			//console.log('facet has been clicked that has a value of: ' + dataValue);

            sendGAEvent('facet_filter_click', {
                event_category: 'program filter',
                filter_type: facetType,
                filter_value: dataValue,
            });
        });
    });

	// create an event for when someone clicks into the input.facetwp-search and records the value of that input.
	document.querySelectorAll('.facetwp-facet.facetwp-type-search').forEach(function(facet) {
		const searchInput = facet.querySelector('.facetwp-search');
		const searchIcon = facet.querySelector('.facetwp-icon');

		// Event listener for Enter key press
		searchInput.addEventListener('keydown', function(e) {
			if ('Enter' === e.key) {
				const searchValue = e.currentTarget.value.trim();
				sendGAEvent('facet_search', {
					event_category: 'program filter',
					filter_type: 'search',
					filter_value: searchValue,
				});
			}
		});

		// Event listener for search icon clic
		searchIcon.addEventListener('click', function() {
			const searchValue = searchInput.value.trim();
			sendGAEvent('facet_search', {
				event_category: 'program filter',
				filter_type: 'search',
				filter_value: searchValue,
			});
		});
	});

}

// Listen for FacetWP updates and reattach event listeners
document.addEventListener('facetwp-loaded', trackFacetInteractions);

// Initial setup for the first page load
trackFacetInteractions();

One of the tweaks I’m doing here is only recording when a checkbox is checked, and not recording if it’s clicked again for the purpose of resetting that particular filter.
It’s important to ensure

Unfortunately because of the way that FacetWP handles the dropdown select menus, I’m omitting tracking of those for now.

Because it’s possible to track button clicks via other mechanisms like Google Tag Manager, I’m giving my events a specific name of wlf_btn_click. I can check the realtime view within GA to see if this info is getting transmitted properly:

1. Test Events in Realtime Report

  1. Go to Google Analytics 4.
  2. Navigate to Reports > Realtime.
  3. Click the Events card on the right sidebar.
  4. Interact with your website (e.g., click buttons or facets) and verify that your custom events (wlf_btn_click, facet_filter_click, and facet_search) appear in the Realtime report.

While we’re able to confirm these views, the data won’t be automatically categorized into meaningful reports unless we set up custom events.

Steps to Create Custom Events in GA4

2. Configure Custom Dimensions for Event Parameters

To track the additional parameters (btn_title, filter_value, etc.), you need to register them as custom dimensions.

Steps:

  1. In GA4, go to Admin > Custom Definitions.
  2. Click Create Custom Dimension.
  3. Fill out the fields:
    • Dimension Name: A descriptive name like Button Title.
    • Scope: Set to Event.
    • Event Parameter: Enter the exact parameter name (e.g., btn_title, filter_type, or filter_value). If you’ve already been sending data to GA4 to view in live mode, a lot of these parameter names should already be preloaded and available. (see screenshot below)
  4. Click Save.
  5. Repeat this process for all custom parameters you want to track.

Now I’ve got this collection of custom definitions:

Step 3. Verify Events in DebugView (Optional)

  1. Enable GA Debug Mode.
  2. Go to Admin > DebugView in GA4.
  3. Verify that your custom events and parameters are being sent as expected.

4. Create Custom Reports

  1. Navigate to Explore in GA4 and create a new report.
  2. Choose Free Form or Funnel visualization.
  3. Add your custom event names (wlf_btn_click, facet_filter_click, etc.) as the dimension under Rows.
  4. Include your custom parameters as metrics (e.g., filter_value, btn_title).
  5. Analyze interactions and filter data as needed.

It’s important to note that to get all the data from all the parameters, you’ll have to create the “custom exploration”. If you only use the views available through Reports > Engagements > Events, you’ll miss out on a lot, such as you can see in the following screenshot:

4. Profit!

So maybe you’ve followed this guide in order to optimize your sales funnel. Let me know if that helped you out. I’m implementing this in order to ascertain how our users actually use the site, and see if there’s anything I can clean up or out, which hopefully results in a more satisfied attendees. That’s a metric that’s a lot harder to track. After the festival is done, I’ll be making changes after a thorough analysis. I’m looking forward to updating this post in 2026 if anyone gives us feedback. UX is a constant process rather than a destination.

Bonus: Technical Notes about tracking user interactions with FacetWP

A note about the the Search Filter:

It’s important that you disable the “Auto refresh” option, which requires the user then to click the enter button or click the search icon.

This is because if you don’t, every keystroke is going to result in a single entry for every letter typed.

Leave a Reply

Your email address will not be published. Required fields are marked *

Post comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.