In order to continuously improve mabl, we need to collect and analyze lots of data about the use (and users) of our service. Some of the questions that we need to answer are:
In order to satisfy these requirements, we need to collect data on navigation, user actions, and user identities within our app. Collecting navigation data allows us to track user flow through the app, showing us frequent user flows and patterns. User actions data allow us to see what things our users do, like creating a record, deleting a record, acknowledging receipt of a notification, etc. And capturing identity data allows us to segment navigation and actions across discrete users.
At mabl, we use ReactJS as our primary front-end framework. If you have tried to instrument an application built on ReactJS (or a similar single-page app framework), you know that the model is a little different than what we are used to with multi-page apps; most instrumentation packages were designed to record events on page load, whereas most of the “action” in a single page app happens outside of the page load context. We wrote this post to help you get your single-page app instrumented.
First, the toolset: We use Google Analytics to understand top-level trends, such as summary hit and session data. We use Mixpanel to analyze funnel, segmentation, and churn data as well as user onboarding notifications. We plan to use a SaaS ticketing tool such as Zendesk to handle customer support. We are also evaluating Intercom and Drift for online customer interaction and other purposes.
Each of these tools requires some degree of instrumentation within our application, and there is often overlap in the data that they expect. Likewise, integrating each of Google Analytics, Mixpanel, Zendesk etc. would have been a very large task with custom code for each one. Instead, we decided to use Segment to instrument our application once, and to funnel the appropriate navigation, action, and identity data from Segment into our analytics tools. Segment gives us the ability to instrument once, then track in many places. The Segment team has been responsive to our questions, even though we’re a small startup.
Having instrumented multi-page apps many times previously, we wanted to ensure that what our single page app gave us in terms of user experience did not impede our ability to track and analyze usage.
Since our single page app is running React and Redux we found Ben Hoffman’s post on the topic, which was extremely helpful, but we knew we wanted to take a slightly more modern approach that takes advantage of the other architectural investments we already made in things like redux-router.
The effort consisted of four major steps:
Let’s go through each of these steps in a little more detail.
We chose React as our front end framework using Redux to manage application state. When it came time to integrate Segment into our existing stack, we turned to a project called redux-segment. The first step was to add redux-segment v1.6.2 to our project.
npm install --save redux-segment
In our store configuration, we simply added:
import { createTracker } from 'redux-segment';
//...
const tracker = createTracker();
//...
let middleware = [
thunkMiddleware,
tracker
];
That’s… it. The redux-segment tracker middleware is designed to inspect every action and look for anything it should track. If it finds something, it sends it off to Segment. Specifically it looks for a meta.analytics in every action. If it finds some – off it goes.
Now we have to actually instrument our app.
The first thing we wanted to track was navigation through our single page app. If we had a multi-page app this would just happen on each page load, but we have a single page app so we need to tell segment when we switch “page.”
In order to track navigation, we first needed to have a few things in place. We were already using react-router v4, so we simply added react-router-redux v5.0.0-alpha6 and history v4.6.3.
By simply attaching the Segment middleware to redux in the previous step and by having react-router-redux pushing our navigation history into the store, we were very quickly able to track all user navigation in the app, as shown here:\
{type: “@@router/LOCATION_CHANGE”, payload: {…}, meta: {…}}
meta:
analytics:
eventType:“page“
__proto__:Object
__proto__:Object
payload:
hash:“”
key:“pirlbs“
pathname:“/organizations/75fc5646-1167-4bd5-8257-5f07aea15586/applications“
search:“”
state:undefined
__proto__:Object
type:“@@router/LOCATION_CHANGE“
redux-segment automatically appends the meta.analytics.eventType to this action, which is then automatically picked up by the tracker middleware we added on the previous step to send the pathname and other data to segment. So any location change action goes right to Segment, and from there it goes to Google Analytics, Mixpanel, etc.
Because we are already using redux, our app has already defined a number of redux actions, many of which we wanted to instrument as events via Segment.io. With the redux-segment tracker middleware this could not have been any easier. The middleware simply looks for some structured data in your action data and passes that data to segment.io for dissemination.
So in our redux actions definitions we simply added:
import { EventTypes } from 'redux-segment';
which gives us the EventTypes we will need to instrument our actions with:
const EventTypes = {
identify: 'identify',
page: 'page',
track: 'track',
alias: 'alias',
group: 'group',
reset: 'reset',
};
In the navigation example you can see that redux-segment added the eventType of page to the @@router/LOCATION_CHANGE action. redux-segment took care of this one for us. For our own actions, we need to do this ourselves using the eventType of track. For example, one of our actions is when someone decides to log in:
function requestLogin(user) {
return {
type: REQUEST_LOGIN,
user
};
}
In order to instrument this existing action so that it may be tracked, we simply add:
function requestLogin(user) {
return {
type: REQUEST_LOGIN,
user,
meta: {
analytics: {
eventType: EventTypes.track,
eventPayload: {
event: 'Logging in'
}
}
}
};
}
Now we can track every attempted login action regardless of success or failure. Notice that the eventPayload contains an event which is simply a human friendly string describing this action. This shows up in segment as:
Notice however that we never explicitly called analytics.track() – that was redux-segment. Repeat for all actions that need tracking.
Tracking identity is basically the same thing as tracking other actions, but with a different EventType and some specific data to track.
We created a /self api call that returns data about the currently logged in user for which we already had a redux action. Here instead of EventTypes.track, we instead use EventTypes.identify. Then we populate an eventPayload that gives Segment a user id, in addition to a traits object that contains all the details about this user: name, email, even picture.
function receiveSelf(user) {
return {
type: RECEIVE_SELF,
self: user,
receivedAt: Date.now(),
meta: {
analytics: {
eventType: EventTypes.identify,
eventPayload: {
userId: user.id,
traits: {
avatar: user.picture,
createdAt: user.created_time,
email: user.email,
firstName: user.given_name,
id: user.id,
lastName: user.family_name,
name: user.name,
username: user.email
}
}
}
}
};
}
Now we can track navigation and actions by individual users.
In life, there are many things that claim to work that really just… don’t (I’m looking at you, elevator close button). But react + redux + redux-segment is one of those special things that really comes together very quickly and works very easily and well. In fact, it is so good, I think the biggest challenge we will face will be deciding what not to track, because now the temptation will be to instrument absolutely everything imaginable.
Good problem to have.