Here at Swiftype, we’re huge fans of Mixpanel. Mixpanel is a service that provides very easy, yet very scalable and powerful, user-centric analytics to Web and mobile applications — with just a few lines of code, you can be up and running, gaining deep insight into how your users are using your product. One of Mixpanel’s great strengths is how easy it is to get up and running; you can embed just a few lines of JavaScript and be analyzing your application in a few minutes. For example:
# app/views/layouts/application.html.erb:# app/views/pricing/show.html.erb:# app/views/pricing/paid.html.erb:
These few little snippets of code will allow you to track user progress through a paid-plan flow; you’ll be able to see which users are looking at which plans, breaking them down by their current plan and/or which plan they looked at, track conversion rates through the flow, and so on.
Mixpanel is centered around events (like User Looked at Pricing Plan
or User Signed Up for Pricing Plan
), which are emitted when you call mixpanel.track
(and which are the basic unit of pricing for Mixpanel), andproperties (like currentPlan
, newPlan
, or oldPlan
), which are included with events and which are free. One of the keys to a high-quality Mixpanel integration is to pass lots and lots of properties: the more properties you pass, the more ways you’ll have to analyze your data. This is particularly helpful when applied in a speculative fashion: if you work to pass lots of data now, then the number of historical analyses you’ll be able to do in a month, six months, or a year goes up greatly. When you’re staring at the data, trying to figure out what’s going on, it’s so much nicer to think “ah, let me look at X!” than to think “gee, I really wish I’d measured X — maybe if I add it now, I can answer this question in another two months”.
Expanding the implementation
Let’s expand our implementation. We might want to add a few more properties about the user; Mixpanel lets us do this using something called super properties (using the register
call), which are passed with every event once set:
# app/views/layouts/application.html.erb:
And let’s pass considerably more properties on each event. The price of a plan is a critical factor, as are its capabilities; we definitely want to record those as well as the plan name, as we might change a plan’s price or capabilities over time. We’ll also throw in plan IDs, since the names might change:
# app/views/pricing/show.html.erb:# app/views/pricing/paid.html.erb:
From a Mixpanel point of view, this is a lot more powerful: we can now do calculations based on differences in price, slice and dice by yearly and monthly prices, support type, max users, and so on. We’ll be able to answer questions like “what was the effect in upgrades when we increased the maximum number of users from 3 to 5 on the middle-level plan, for those users who were still on the basic plan?”. We’ll be able to be clear and consistent in our data, and observe historical prices and capabilities of plans, even if we change them later.
…And now, the problems ensue
However, from a code point of view, this is the leading edge of turning into what we technical folks call a big ol’ mess. Our code is verbose, it isn’t DRY at all, and, as a result, it’s very error-prone. (How many of you noticed that I accidentally passed the name of the old plan twice in the second example, rather than the ID and the name?) And this is with just two events — can you imagine what it’s going to look like when we have twenty, or fifty?
Further, maintaining this code in the long run is going to be a nightmare. If we add another property to plans that we’re interested in monitoring, we have to go update every single event and add that property, or we’ll have inconsistent data. If we want to change property names — again, we have to go update every single call site. We have two events right now; in a real production system, we might have thirty or fifty. Yuck.
Not quite as obvious, but perhaps even more important, is the fact that having a clean record of exactly what an event means — and changes that might affect that event! — is critical for correct analysis. For example, if we decide to show pricing plans to everybody directly on their home page, the number of events for User Looked at Pricing Plan
is going to skyrocket, and so the conversion rates to User Signed Up for Pricing Plan
are going to plummet. Sure,you might be able to remember this, right now — but in another year, when you have six more people looking at it, is everybody going to remember all of the fifteen different significant changes you made over that year when looking at your results? There has to be a better way, right?
It may help to consider, on a more theoretical level, what’s happening when you use Mixpanel effectively. The properties you pass are effectively a snapshot of various parts of your database; the user is likely the single most important part of that snapshot, but there are plenty of other objects that contribute, too. That might be a database row representing a user-to-user communication, a taxi ride, a stay overnight, or a search engine, depending on your domain, but essentially you are reflecting a denormalized snapshot of a chunk of your database to Mixpanel with each event — this is how it can be so effective for you. When you consider it this way, it becomes even more clear why adding some structure and mechanism can be of huge advantage: with the right framework, you ought to be able to gather that database information and pass it very easily, almost implicitly, rather than having to maintain huge lists of properties all over your application.
What About Super Properties? Mixpanel’s “super properties”, while incredibly useful, can also be problematic. The implementation of these is straightforward: Mixpanel’s library issues a permanent cookie to your end user that records the current set of “super properties” that is registered; when firing an event client-side, it simply merges these properties in with any specified in the event. This is a really simple, useful, and powerful model, and is great when you’re starting out. However, there are several caveats: perfect updating is required — if you change data server-side and forget to re-call Mixpanel.register
, that data will be perpetually incorrect in Mixpanel; inaccessible server-side — if you fire events server-side (and, in our experience, you inevitably will have to at some point, like email generation or background tasks), you simply won’t have access to that data at all; easy to tamper with — it’s really easy for users to change their own super properties. As you’ll see below, our MetaEvents library replaces “super properties” with implicit properties, which largely eliminate all these issues.
Introducing MetaEvents
We’d love to introduce you to Swiftype’s solution for all these problems: the MetaEvents RubyGem. Let’s take a look at what our code from the above example would look like using MetaEvents. First, we define methods on some of our models that convert them to properties, and set up MetaEvents in our ApplicationController
:
# app/models/user.rb
def to_event_properties
{ :signup_date => created_at, :account_type => account_type,
:signin_count => signin_count }
end
# app/models/plan.rb
def to_event_properties
{ :id => id, :name => name, :monthly_price => monthly_price,
:yearly_price => yearly_price, :max_users => max_users,
:support_type => support_type }
end
# app/controllers/application_controller.rb
def meta_events_tracker
@meta_events_tracker ||= MetaEvents::Tracker.new(
current_user.id, request.remote_ip, :current_user => current_user)
end
And now we can fire events from our controllers just this easily:
#app/controllers/plan.rb
def show
@plan = Plan.find(params[:id])
meta_events_tracker.event!(:plan, :show, :plan => @plan)
end
def pay
# ...
meta_events_tracker.event!(:plan, :paid,
:old_plan => @old_plan, :new_plan => @new_plan)
end
(Here, we’re firing events server-side; we’ve found this to be more flexible and consistent than client-side events, but it’s just as easy to fire the events client-side, if you prefer.)
Several interesting things are happening here:
- MetaEvents allows us to pass implicit properties on every single request (the
MetaEvents::Tracker.new
call); this is like Mixpanel’s “super properties”, only more reliable (because they’re guaranteed always up-to-date) and in your full control;
- MetaEvents lets us pass objects as properties; it expands them using their
#to_event_properties
method, and integrates them into events, prefixing them with whatever key you passed them in with;
- MetaEvents provides a flexible model for firing events server-side; it’s easy to fire them asynchronously using Resque or a similar system.
We’re still passing through every bit as much data as before, only now it’s completely DRY. Adding properties is a piece of cake, properties will be completely consistent across events, and 100% up-to-date information about the current user will be passed on every single event.
Finally, let’s look at what config/meta_events.rb
, which defines our events, might look like a year from now:
category :plan do
event :show, "2014-03-03", "user looks at a pricing plan" do
note "2014-06-14", "pburkart", "we moved plan display onto the dashboard...vast increase in displays"
note "2014-07-11", "lweyand", "moved off dashboard unless a user was over plan limits"
note "2014-10-17", "mvellez", "more aggressive visual display of plan on dashboard"
note "2014-12-09", "lweyand", "holiday promotion lightbox added"
note "2015-01-05", "mvellez", "holiday promotion lightbox removed"
end
event :paid, "2014-03-03", "user pays for a new pricing plan" do
note "2014-08-09", "mvellez", "removed street address from payment form -- turns out we don't need it"
note "2014-08-17", "lweyand", "added PayPal support"
note "2014-11-13", "pburkart", "upped free trial from 30 to 60 days"
note "2014-12-14", "pburkart", "reduced free trial back to 30...turns out 60 didn't make any difference"
end
end
Not only does this file become a canonical record of what events you’re firing (as adding an event here is required before you can fire it), it also becomes a historical record of changes to your events. This tree is even exposed via MetaEvents, so you could easily turn this into an HTML report for the pointy-haired bosses around.
MetaEvents, defined
MetaEvents is a RubyGem that provides a framework for structuring your events, efficiently exposing large numbers of properties, adding implicit properties based on the currently logged-in user and browser, and firing events either server-side or client-side. When used in a large-scale Ruby application:
- You’ll be able to understand your events — forever. MetaEvents provides a Ruby-based DSL to document your events; you will have a permanent record of what each event is for, when it was introduced, and any changes you’ve made. This alone will probably make your product managers and business folks very happy, if experience is any guide.
- You’ll pass far more properties, and they’ll be consistent across all events. MetaEvents encourages you to define
#to_event_properties
methods on your models, and then pass entire models to its methods; it then automatically merges all properties of those models into the event. Now, when you think “hey, I wonder if…”, you’re much more likely to already have that data in Mixpanel for weeks or months than to have to add it now.
- You’ll capture environmental/contextual data automatically. MetaEvents lets you define implicit properties, which are fired with every single event and are typically properties from the currently-logged-in user, browser, account, or so on. Better than Mixpanel’s “super properties” because they come from your database right now and work server-side, these further increase the axes along which you can do analysis.
- You’ll still reap these benefits when firing events from the client. MetaEvents provides very easy ways to define events server-side and fire them client-side, either automatically on links or via any mechanism you choose.
Implementing MetaEvents doesn’t take long at all, and it can happily coexist with your existing Mixpanel code — you should be up and running within an hour, tops, and be able to expand rapidly from there.
Get started with MetaEvents now!
If you need to track users who aren’t logged in (and who doesn’t?), you might also want to take a look at our WebServerUid RubyGem, which provides an easy way to generate a unique browser ID for visitors.
MetaEvents and a predecessor system have been used in two different large-scale Rails web applications, providing detailed analysis at scale: over 500,000 events per day, with between 20-50 properties each, all with extremely little maintenance or overhead. Although its release is recent, the ideas have proved themselves over the course of several years. It is also thoroughly tested and documented; we think you’ll find the code easy to read and well-structured.