How to Choose and Track User Events to Build a Better Product
Tags: analytics, tracking, javascript
Published: 2019-10-09 16:12:13

Online products are unique in the richness of user data they have available. Every action you take, whether hovering over an ad, clicking a link, or making a keystroke, can be tracked. This one concept has led to an entire industry of “big data”, valued most highly by ad companies. It’s for this reason that the practice has been in the headlines and at the top of minds recently.

That being said, user activity is still a highly valuable source of information for applications not serving ads, but rather looking to improve their products for the sake of their users. This data is not only valuable for identifying user preferences, it is also key for understanding trends in user behavior. That’s why it’s important to still consider and implement event tracking when building a paid product.

<hr class="margin-double--ends">

<p class="text--medium text-weight--heavy">
  Monolist is the command center for engineers &mdash; tasks, pull requests, messages, docs — all in one place. <a href="/">Learn more</a> or <a href="https://app.monolist.co/login">try it for free</a>.
</p>

<hr class="margin-double--ends">

## 🎥 Tracking from the Get-Go

There are three main advantages to tracking user events as soon as you’re able.

- **Set a precedent.** By tracking the right user events early, you’re working to set a precedent that this should be followed for all future user-facing features being shipped. This is a good practice to maintain to avoid event tracking becoming an afterthought or post-launch effort.
- **Get the architecture in place.** Tackling event tracking early on and in a flexible manner means you’ll have the code and APIs in place for tracking events quickly, easily, and long into the future. We’ll go into more detail on this below.
- **Start learning ASAP.** This one should be obvious - the sooner you’re gathering the data, the sooner you can learn from it. If you’re tracking events from your first acquired user, you’re that much closer to improving the experience for your next user.

## 🔑 Deciding What to Track

There are two aspects to a user event that you track: the action the event represents (“user clicked on log in button”), and the data you choose to associate with that event (`{ user_id: 123 }`).

### What events should be tracked?

It’s important to carefully consider what events get tracked and persisted. There are a few questions you should ask when determining if something is worth tracking.

- **Is this an action the user took directly?** For example, did they interact with an element or trigger an update? If the event occurs automatically or passively, it should likely not be tracked.
- **Does this action happen at a reasonable interval?** An example of an event that a user may trigger quite often would be a mouseover or hover event on an element. In this case, the event should probably not be tracked as it’ll introduce noise to your data, and it really won’t tell you much.
- **Could this event tell us something about user behavior?** The answer to this question is most often yes, but it’s still a good one to consider. If you’re tracking an irrelevant (or likely inevitable) event like “user pressed the ‘y’ key”, it may not be valuable to track.

### What data should be included with an event?

This is when it’s most important to find the balance between user privacy and data availability. When attaching a data payload to a user event, it’s important to minimize (or ideally eliminate) any personal or identifying user about the info. The data should be pared down to the bare minimum needed to infer your learnings about the user’s experience. This is because you’ll most likely end up persisting your user events in a third-party service.

An example of a bad payload might look something like this:

```json
{
	"user_email": “john.doe@gmail.com”,
	"user_full_name": “John Doe”,
	"email_sent": “This is the body of an email I’m composing to a close friend.”
}
```

A good payload, on the other hand, might look something more like this:

```json
{
	"user_gid": “abc-123”,
	"sent_email_gid": “def-456”
}
```

Notice the difference? Rather than the raw data that is both identifying and personal, we only associate the event with the top-level (externalized) identifiers that we can use to then match to the proper entities internally.

## 🛠 Architecting Your Tracking Framework

Now that we’ve discussed how to select what events you’re tracking, along with what data they entail, what’s the best way to incorporate this into our application?

### Determine where the events take place

Here at Monolist, we use [React](https://reactjs.org/) for our UI and [Redux](https://redux.js.org/) for our data management. Together, these give us two fairly well-defined places that an event can occur: inside the UI, i.e. a React component, or inside a Redux action, i.e. when making an API call.

```typescript
function handleButtonClick() {
	// We could track this event
}
```

```typescript
function closeActionItem() {
	return (dispatch) => {
		// We could also track this event
	};
}
```

In general, we prefer the latter approach: centralizing our tracking in our Redux actions. This gives us one place and one common approach for tracking an event, making it easy to find and understand. It also allows us to easily track events across platforms since we share our Redux code between our web and react-native clients. However, tracking within a component is still sometimes necessary when we want insight into lighter actions that don’t necessarily update the Redux store.

### Determine where to send the events

The actual event tracking method we call is also a Redux action, providing us a familiar method of invocation. This looks something like this:

```typescript
export const trackUserEvent = (
  eventName: string,
  eventData?: object,
) => {
  return async () => {
    if (process.env.NODE_ENV === 'production') {
        await api.post(
          '/analytics/user-event',
          {
            eventName,
            eventData,
          },
        );
    }
  };
};
```

As you can see, it’s fairly straight forward: if we’re in the production environment, send the event and its data back to our API. We send the data back to our API (rather than directly to a third-party service for three reasons:

- This allows the API to carry out any additional data scrubbing we may want to define.
- This allows us to carry out the third-party pushing in an async queue, ensuring the event gets persisted regardless of further UI interaction (user unloads page, etc.)
- We now have one point of third-party contact to modify (in the case of a service provider switch, etc.)

The examples we provided earlier end up looking like this:

```typescript
async function handleOpenClick() {
	await props.onTrackUserEvent(‘open-item-click’);
}
```

```typescript
function closeActionItem() {
	Return (dispatch) => {
	dispatch(analyticsActions.trackUserEvent(‘close-action-item’));
	};
}
```

### Determine where to put the data

As we mentioned above, the primary reason for being strict about the data we persist for an event is that we may want to send our data to a third-party. There are many services dedicated to tracking your user events and helping you to analyze them (we use [Mixpanel](https://mixpanel.com/) here at Monolist). These are great for being able to easily parse and visualize your data without additional engineering work. This also means your PM can even dive into the data themselves.

Mixpanel has a relatively straight-forward API, which makes it easy for us to integrate from our Rails API. The endpoint that the above Redux action hits looks something like this:

```ruby
def track_user_event
    PushUserEventWorker.perform_async(user_event.id)
    head 200
end
```

As you can see, it does just one thing: creates an async job to push the event to our third-party (Mixpanel). That worker then makes a quick API call:

```ruby
class PushUserEventWorker
  include Sidekiq::Worker

  def perform(user_event_id)
    user_event = UserEvent.find(user_event_id)
    mixpanel = Mixpanel::Tracker.new(ENV["MIXPANEL_CLIENT_ID"])

    event_data = user_event.event_data || {}

    mixpanel.track(user_event.user.id, user_event.event_name, event_data, user_event.ip_address)
  end
end
```

This worker is now our single point of third-party contact if we ever decide to switch providers.

An alternative to this approach would be to use something like Redshift to store your data, and then write in-house tooling to use the data however you see fit. Although this is a much larger undertaking, it’ll likely provide you more control over your data and analysis (and save you a few dollars).

## 📊 Analyzing the Data

Once the data is stored, parsed, and visualized as we’ve described above, it’s up to you and your team to infer learnings from the data. Is that new signup flow seeing more engagement? Is the new feature you released last week resulting in any additional invites?

How has your team used event tracking to learn and implement product changes? [Let us know on Twitter](https://twitter.com/monolistapp)!


## ❗️ Are you a software engineer?

At Monolist, we’re focused on building the global command center for software engineers. If you want to try it for free, just [click here](https://app.monolist.co/login).