How to Implement Click Analytics with the Event Capture Phase

Recently, I worked on a story to add some additional user interaction telemetry to a web app. The client wanted to accumulate some real-world usage data from end-users to better inform an eventual product overhaul. Going into a rewrite, it would be great to know what features end-users are interacting with frequently, and which don’t add value.

Background

We started by logging HTTP request bodies on the back end. The way our application is set up, inspecting the requests that the web client makes to the back end answers a lot of questions. However, our app has a handful of features and interactions that don’t trigger HTTP requests. So, what could we do to track these interactions?

On the backend, we preferred a general approach. We would capture lots of information, and provide a mechanism to blocklist sensitive API endpoints. Then, we’d answer questions by post-processing the data. Approaching the problem this way meant our development effort could compound down the road. We were answering not-yet-asked questions as they came up without requiring further development effort or redeploys. We hoped to achieve something similar on the client’s side.

Our client application is an Ember app. After digging through the Ember docs, we thought a global DOM event listener would be the most straightforward. First, we would set up a click event listener on the window object. Then, we could parse the click events using Ember’s introspection APIs to generate log messages with info about what users clicked.

Our first attempt looked like this:

window.addEventListener("click", (ev) => {
analytics.log(analyticsForEvent(ev));
});

The Problem

Overall, the approach had a lot of merits. By doing things this way, we could answer questions about how users were interacting with the app that we hadn’t even thought of yet. But there was a problem. In our Ember app, like in many web apps, we often need to stop event propagation on click. If two HTML elements are clickable, you want to ensure that the element the user clicked on is the one that handles the click. Unfortunately, this prevented our top-level event handler from firing, meaning we wouldn’t track that interaction.

The Solution

Digging into the MDN Web Docs repository a bit more revealed the perfect workaround for our situation: the event capture phase. I always thought it was a bit strange that events bubbled out from the most to the least specific element on the page. It turns out this bubbling phase is the second half of the event handling story!

First, the browser triggers any capture phase listeners, starting with the largest element (the window and the document, in most cases) down to the smallest, and then the event bubbles from the smallest back to the top. To mount our analytics listener as a capture phase listener, all we needed to do was pass an extra parameter:

window.addEventListener("click", (ev) => {
analytics.log(analyticsForEvent(ev));
}, true);

Implementing Click Analytics with the Event Capture Phase

This way, we could leave our application code untouched and our event listener will still fire even if a component stops event propagation!

When working with frontend frameworks like Ember, React, or other options, it can be easy to forget about the great APIs built into the core of browsers. Don’t be afraid to dig around MDN for solutions to your next web dev problem!