Hungry for some Elm right away? Skip straight to the code.
“What are our users doing?” is one of the most common, interesting, and infuriatingly complicated questions I’ve heard in my time writing software.
Knowing what our users do determines whether we succeed or fail. Are teachers assigning individualized lessons using the new features we rolled out? Do enough kids in 3rd grade depend on the text-to-speech feature to make reading aloud automatic? How does that student keep triggering that bug we can’t reproduce?
We can’t ship useful software if we can’t answer those questions, which often turn out to be more complicated than they might initially appear. To understand what our users are doing, we first have to decide what we mean by “doing”.
Consider this: a student watches an educational video and answers a question to show their understanding. Which of these best describes what they just did?
- Completed a video and took a quiz.
- Clicked play on the video player, watched to the end, and hit next; then selected a quiz answer and hit submit.
- Moved the mouse over multiple elements, clicked a button, watched 31 individual seconds of a video, moused over more elements, clicked another button, scrolled, selected an answer, moused over more elements, then clicked another button.
Do you see the problem?
All three of those are accurate explanations of what the student did. Which group of data is most useful depends on what question we want to answer.
Levels of Resolution
We can think of each of those different correct answers as being at different levels of resolution.
1. The level of things
The first level (“took a quiz, placed an order”) we can call the level of things: discrete, tangible objects. Thinking at this level is like understanding a classroom by counting the number of students, looking at the homework they turn in, examining the grades they get.
As a web developer, I’ve always found this level the simplest to work with because it corresponds nicely to the records in the database: number of quizzes completed, number of orders for a user. Data at this level can answer many important questions.
That simplicity is also its limit, though. Some questions and some bugs are easy to determine from this level, but others require an examination of history, which is missing if all you can see is the current state.
2. The level of actions
The second level (“clicked play on the video, selected an answer”) we can call the level of events: snapshots of actions framed in meaningful terms. Thinking at this level is like understanding a classroom by counting the different activities that happen in a day: who studied 1–1 with the teacher, how many times students worked together in small groups, what presentations the teacher gave.
Event logs like this represent a running account of all that happens in your application, stored in Redshift or ElasticSearch or whatever vacuum tube-powered monstrosity we’re using as a data warehouse that day. It’s often not practical to reconstruct the current state of affairs, but such logs can be tremendously helpful across a range of questions.
3. The level of particles
The third level of data detail (“moved the mouse, scrolled, clicked on a button”) we can call the level of particles: the low-level actions that, grouped together, make up the events that create the things. Thinking at this level is like understanding a classroom by measuring physiological movements: opening up school books, retrieving tablets from desks, walking across the room.
Thinking at this level has led to some amazing and transformative work in fields dear to my heart, and if you’re working on a game or on a massively popular application (the likes of Facebook, Google, Amazon), examining this level of detail must be worth the investment. Myself, I’ve never done it.
How do you reach the level of events?
Which level of resolution is right depends, as noted above, on the questions we want to answer. For me, the most interesting level is the middle one, the level of events:
- Events map directly to significant actions in our application, so unlike the level of particles it’s easy to make sense of.
- Events are useful in addressing both business questions and technical issues, helping to debug the complex problems our users encounter.
But how do we actually log business-level event data?
Tracking at the level of things and the level of particles both have straightforward technical solutions that scale easily as we add more data:
For the level of things, we can just look at the same database that our application uses. If we want to know how many order have been placed by a given user on a given day, that’s a simple SQL query.
For the level of particles, we can snag every browser event, API request, etc. It requires a bit more work than just querying the database, but there are still clear points to hook into — and once we’re hooked in, it doesn’t matter if we add new buttons or API calls, they’re recorded.
For event data, though, there’s no straightforward solution. The semantically-meaningful moments in our app don’t map cleanly to specific kinds of technical interactions. How do we know which clicks count? How do we give a meaningful name to each API call?
The best solution I’ve seen has been to build a simple utility to log events and manually sprinkle those statements throughout an application:
And this works! Except when you forget to instrument a new feature and then have no idea how it’s working three months in. 😬
Fundamentally, the problem is this approach depends on humans and, quite frankly, we have better things to do than remember to do something repetitive and easy to miss. That’s what computers are for, right?
Building a React app, we experimented with different ways to collect this data and pipe it to a business analytics tool, but kept running into implementation details:
Then we adopted Elm.
Logging Events in Elm
Here’s a simplified example of an Elm program. (If you’re not familiar with Elm, don’t worry if you don’t catch every detail — we’ll go over the important parts later.)
Let’s review. What we’re looking for is a way to
✅ “Tag the actions in our application with human-readable identifiers”: every action that changes state is a message and every message already has a semantically-meaningful identifier (e.g. one with clear human-readable meaning).
✅ “and to have a way to intercept and log those events automatically”: every message is processed by our update function, and we control that function.
This very last bit took me some time (and the help of the Chicago Elm meetup) to understand: the update function we write for an Elm program is a function like any other.
This may be self-evident, but to me the function always had a mysterious air about it. Because “update” is one of the magic record fields you must provide to define an Elm program, I always assumed that the update function itself was somehow special too. It’s not.
So, what does it mean that update is a function like any other?
It means we can manipulate it. We can wrap it in our own function. We can log what comes in and what comes out.
Logging the Update Function
Tracking all the actions a user takes in your Elm program is this simple:
Presto! Since the messages in our application are already human-friendly labels (like PlayVideo below), that’s basically all there is to logging (frontend) events. Everything else is just implementation details.
And you know what they say about implementation details
“Keep an eye out for the next blog post about the details of implementing event logging in Elm in a real application!”
Thanks to @ryan_nhg for originally suggesting this approach and @isaacseymour for pointing out Elm 0.19’s impact on Debug.toString!