Build a User Activity Tracker with Stimulus

Without hard data, it’s difficult to know if your app delivers a satisfactory user experience. Are most users dropping off halfway through your sales funnel? What CTA’s are they clicking? Are there users that begin to sign up for your mailing list, only stop when they see they need to register an account? In order to know the performance of pages, funnels, and flows within your app, you need to gather data on user interaction.

There are certainly many third-party services that can give you every dirty secret about users that come to your app, but some might find that gathering basic data is all that is needed to optimize UX. This can be done by integrating basic client-side tracking—a task that is easily accomplished using Stimulus.

What is Stimulus?

Stimulus is a javascript framework created by Basecamp. It was made to add bits of javascript into server-rendered HTML. Unlike most modern javascript frameworks, Stimulus focuses on simplicity and seamless integration into your markup, rather than taking over your client-side code. Whether or not Stimulus is the right choice for your application depends on the purpose javascript plays in your app and how it helps you achieve your business goals. But because it is a lightweight framework focused on DOM element interaction, it is ideal for making a simple interaction tracker.

To learn more about Stimulus, check out the Stimulus handbook.

Defining Project Goals

Let’s say we have an app with a single-page, multi-step signup form. Lately, Google Analytics has been reporting an abnormal amount of bounces and dropped sessions on the signup page, and we want to know exactly what step of the signup process we are losing our users. With this in mind, let’s define some questions we want our data to answer:

  1. How many users are landing on the signup page?
  2. How many users are completing the first part of the signup flow? (Email)
  3. How many users are selecting an option in the third step of the signup flow? (Account Type) What option are they selecting?
  4. How many users are completing the final step of the signup flow? (Credit card)

Now that we know what questions we want to answer, we can define what data we want to track, and how to track it.

  1. When a user visits a page, we should track that a user has entered the signup process.
  2. When a user enters their email, we should track the event.
  3. When a user selects an account type, we should track the event and which account type was selected.
  4. When a user enters their credit card information, we should track the event.
  5. When a user submits the form, we should track the event.
  6. All of the proceeding events should have some shared ID, so they can all be connected to the same page visit.

Let’s also set the additional goal of making this tracking controller reusable. We’ll want to use it in other parts of the app to track other interactions. With all that in mind, let’s build the Stimulus controller for tracking user interactions.

Building the Stimulus Controller

// tracking-controller.js

import Controller from 'stimulus'
import uuidv4 from 'uuid/v4'

export default class extends Controller {
  connect() {
    this.uuid = uuidv4()

    if ( this.context.element.dataset.trackEventName ) {
      const eventName = this.context.element.dataset.trackEventName
      const properties = this._getTrackingProperties(this.context.element)

      this._saveTrackingData(eventName, properties)
    }
  }

  trackEvent(event) {
    const eventName = event.target.dataset.trackEventName
    const properties = this._getTrackingProperties(event.target)
    if ( event.targe.value ) {
      Object.assign(properties, { value: event.target.value })
    }

    this._saveTrackingData(eventName, properties)
  }

  _getTrackingProperties(elem) {
    const properties = elem.dataset.trackProperties || "{}"
    return JSON.parse(properties)
  }

  _saveTrackingData(eventName, properties = {}) {
    Object.assign(properties, { interaction_id: this.uuid })
    // send event eventName and properties to server to be saved in database
  }
}

Some of this will make more sense when we implement it in the markup, but let’s go through this method by method to gain an understanding of how our data is getting tracked.

connect

This is a callback defined within the Stimulus lifecycle, and is called when our controller connects to the DOM. When it does, we want to set a uuid for this instance of the controller, with the help of the node-uuid library. This uuid will be shared between each interaction recorded by the instance of the controller, allowing us to know that these events all happened within the same interaction. And because this is isolated to the instance, we can instantiate multiple tracking controllers on the same page, allowing us to separately track different interactions simultaneously. If we want to track an event on the pageview itself, we can define a data-track-event-name attribute on the element that sets the controller.

trackEvent

This is the callback we will set in the DOM whenever we want to track an interaction. When it is set, it can be bound to any element that can trigger the event, and be triggered by multiple events. Like in connect, we set the event by setting a data-track-event-name attribute on the binding element, and track additional details with data-track-properties. Unlike connect, however, setting the data-track-event-name is mandatory. Finally, if the binding element has a value, we track what that value is. This will be useful for tracking changes in inputs.

_getTrackingProperties

If we want to track any additional information related to event, other than it’s name (and possibly the input’s value), we can set a data-track-properties attribute an any element that triggers a tracking event. This value should be stringified JSON, which we will parse before saving.

_saveTrackingData

Finally, once all the data is collected, we want to add the uuid of the interaction to the event’s properties. Then everything is saved to the database. This can be done through any number of means. At Zaarly, we use the ahoy gem to save and manage event data. In that case, our implementation would call ahoy.track(eventName, properties)

Now that we understand how the controller works, let’s implement it in our markup.

Using the Stimulus Controller

For this example, let’s assume we have a different javascript library helping us build this multi-step form—so innactive steps are obscured and cannot be seen until previous steps are completed. With that taken care of, here’s how we would want to integrate our tracking controller into our signup form:

<div data-controller="tracking" data-track-event-name="Started signup form">
  <form
    action="/signup"
    method="post"
    data-action="submit->tracking#trackEvent"
    data-track-event-name="Completed signup form"
  >
    <div class="step active">
      <label for="email">Email</label>
      <input
        name="email"
        type="email"
        data-action="change->tracking#trackEvent"
        data-track-event-name="Filled email field"
      />
      <button>Next</button>
    </div>

    <div class="step">
      <label>Select the Type of Account You Would Like</label>
      <input
        type="radio"
        name="account_type"
        value="basic"
        data-action="change->tracking#trackEvent"
        data-track-event-name="Selected account type"
      /> Basic ($1/mo - first month free)<br>
      <input
        type="radio"
        name="account_type"
        value="premium"
        data-action="change->tracking#trackEvent"
        data-track-event-name="Selected account type"
      /> Premium ($10/mo)<br>
      <input
        type="radio"
        name="account_type"
        value="super-user"
        data-action="change->tracking#trackEvent"
        data-track-event-name="Selected account type"
      /> Super-User ($20/mo)<br> 

      <button>Next</button>     
    </div>

    <div class="step">
      <label>Please enter your credit card details</label>
      <input type="text" name="cc[number]" placeholder="enter your card number">
      <input type="text" name="cc[security]" placeholder="###" />
      <input
        type="text"
        name="cc[expiry]"
        placeholder="MM/YY"
        data-action="change->tracking#trackEvent"
        data-track-event-name="credit card information entered"
      />

      <input type="submit" value="SIGN UP!" />   
    </div>    
  </form>
</div>

By binding stimulus controller actions to elements that trigger events, we are now tracking the user’s movement through the signup funnel. Now, when a user enters information into a critical input, or submits the form, that event and it’s corresponding properties are saved in our database (as a side note, you’ll notice that we bound the event credit card information entered to the input for the expiry date. This is because the expiry is the final input on that page, and is more likely to signal completion. More importantly, though, we don’t want to accidentally save users’ credit card numbers in our database in plain text).

Putting it All Together

So, in our theoretical scenario, can the data we’re saving be helpful? Since this data can tell us how far a user is getting in the funnel before dropping off, there are plenty of useful theories we can test with this data:

  1. Are users going so far as to select an account type, but dropping at the credit card phase? What account type are they choosing? If they are going for the basic plan, which offers the first month free, maybe it is time to start giving away that month without requiring a credit card.
  2. Are users entering their email, but dropping off when they get to the account type selection? Maybe it is time to reconsider pricing, or what benefits are given to each tier.
  3. Are users going so far as to enter their credit card information, but not submitting the form? Are they going so far as to submit the form, but you aren’t seeing a corresponding number of new accounts created? There may be a critical bug in your funnel that you should address right away.

As you can see, there is much to be learned from the simple act of tracking a user’s basic actions on your site. And Stimulus is a great tool for creating a simple, reusable activity tracker.