Mixpanel Analytics with Ruby

Today, we’re going to look at Mixpanel as an example of meaningful analytics. Meaningful analytics help us to solve user acquisition and retention problems. Three percent of visitors signing up for our application is an acquisition problem. However, having only seven percent of signed up users return to use it is a retention problem.

Analytics is like a debugger for your marketing and user satisfaction efforts. Events that take place in your software are like the critical breakpoints we set and the properties of these events are like watch variables.

Mixpanel

How Can Analytics Help Us?

Meaningful analytics take a look at real user events in our software systems to help us diagnose and solve problems, such as:

  • Where do my users stop in a series of steps?
  • How long does it take a user to complete a workflow?
  • Does my copywriting communicate effectively?
  • Do my users keep coming back to use core features?
  • Was a critical action completed?
  • Who uses my application?

On the free service tier, Mixpanel lets you track 25,000 events for free or 175,000 by installing a badge on your application. After you’ve reached the total number of events, old events will fall out of scope like a FIFO queue. Mixpanel has other paid plans to let you keep more data around every month. You can send events to Mixpanel from the client side in JavaScript or from the server side using Ruby. If you choose to use the client side library, events you send will automatically include a number of properties from the HTTP requests themselves such as your browser version, operating system, referrer, and location (if available). Additionally, you can set key value pairs that will be sent to Mixpanel with each request and are referred to as “super properties.” These are stored in a cookie on the client side:

// Super properties are set in a cookie
mixpanel.register({
  'user type': 'free trial',
  'source': 'email campaign',
  'preferred format': 'video',
}, 30);

// They are then sent with every request
mixpanel.track('Code Review');

Funnel Vision

One of the easiest concepts in Mixpanel is that of the funnel. A funnel is a series of steps that you desire your users to take in order to achieve a goal. Let’s imagine a website which allows you to sign up to take an online course. First, an email campaign is sent out to potential users that may attract them to a landing page for the product advertised. At the bottom of that page is a link to the pricing page. If the user finds the pricing agreeable, then they will click the link to the sign up page. Finally, if the sign up form was easily completed, then the user would complete the funnel by signing up and starting the course. Completing this series of steps would also be known as a conversion because a measurable predetermined goal was completed.

Mixpanel Flow

Here is a script you can use to generate some sample data in order to experiment with Mixpanel and see how its funnel feature works:

require 'faker'
require 'mixpanel-ruby'
require 'securerandom'

PROJECT_TOKEN = '539d98201fc65b215d25339537e4d945'
tracker = Mixpanel::Tracker.new(PROJECT_TOKEN)

def user_bounced
  @bounced = true
end

def user_continued
  @bounced == false && rand(2) == 1
end 

users = 10.times.map { SecureRandom.hex } 
users.each do |user| 
  @bounced = false 
  tracker.track(user, 'Landing Page', { campaign: 'Mailchimp Code Reviews' }) 
  user_continued ? tracker.track(user, 'Product Page') : user_bounced 
  user_continued ? tracker.track(user, 'Pricing Page') : user_bounced 
  user_continued ? tracker.track(user, 'Signup Page') : user_bounced   user_continued ? tracker.track(user, 'User Signed Up') : user_bounced 
  if user_continued 
    account = SecureRandom.hex 
    tracker.alias(account, user) 
    tracker.identify(account) 
    tracker.people.set(account, { 
      '$first_name' => Faker::Name.first_name, 
      '$last_name' => Faker::Name.last_name, 
      '$email' => Faker::Internet.email, 
      '$phone' => Faker::PhoneNumber.cell_phone 
    }) 
  end 
end

What Should I Track?

Now that you know how to track events, the next question is what should you track? Once we see the benefit of analytics our first instinct may be to track everything, but that won’t do you much good.

  mixpanel.track('Lions');
  mixpanel.track('Tigers');
  mixpanel.track('Bears');
  mixpanel.track('Wolf! Wolf!  No seriously help wolf yeoooowwww!!!');

When you track too many events without any context to real goals, the result is that events cannot be easily correlated and people will be overwhelmed with information that they cannot necessarily take action on. This will eventually cause your team to stop paying attention to the analytics altogether, thus diminishing its overall value. Mixpanel is really great at letting you break down data by properties:

  mixpanel.track('Predator Attack', { 'Animal': 'Lion' });
  mixpanel.track('Predator Attack', { 'Animal': 'Tiger' });
  mixpanel.track('Predator Attack', { 'Animal': 'Bear' });
  mixpanel.track('Predator Attack', { 'Animal': 'Wolf' });

A general rule of thumb is to track few events with many properties on each event:

# Not real code 
class Application 
  has_few :events
end

class Event
  has_many :properties
end

As developers, we like to create elegant solutions that are future proof. Therefore, our next thought may be to create a generic solution that tracks all controller events. At first glance this seems like a logical thing to do. In Rails we have RESTful actions that correlate to real world actions and are immediately recognizable to other developers if we follow the RESTful controller conventions. Another similar implementation idea is to generically attach tracking to ActiveRecord object lifecycle hooks. Both of these ideas are flawed for several reasons.

First, business logic is not always tied directly to controller actions or ActiveRecord object lifecycle hooks. If you practice keeping your code DRY, it is entirely possible that a particular event which you would like to track, is encapsulated by a service object or simple Ruby class and may be invoked in multiple controllers throughout your application. In this case you would end up with duplicate tracking code if you were to tie it directly to all controller actions.

Naming Events

The other problem with this approach has to do with the challenge of naming. Depending on the size of your organization, analytics may require the buy in and understanding of consumers and producers. In a larger organization, developers act as the producers. They define in code the events and properties which will be tracked by the analytics platform. Members of the marketing and sales team may act as the consumers, reading and interpreting the events as they are collected. While the names of controller actions may be perfectly understandable to your developer team, it is likely meaningless to anyone who is not a Rails developer.

tracker.track 'CodeReviews#index'
tracker.track 'CodeReviews#new'
tracker.track 'CodeReviews#create'
tracker.track 'CodeReviewMailer#notify'

Since successful products and organizations tend to grow, we can’t necessarily predict who the consumers of our tracking data will be in the future. That’s why it is important to use human readable event names that can be understood without the context of source code insight.

Tying events directly to controller names also makes the data meaningless as soon as we refactor our application, rename controllers, or switch to a different framework or technology in the years to come.

# No 
mixpanel.track('797128') # Magic Numbers 
mixpanel.track('sessions#create') # Controller Actions 
mixpanel.track('SU') # Insider Abbreviations 

# Yes 
mixpanel.track('User Signed Up') # No need to read code 
mixpanel.track('Begin Code Review') # Plain English names 
mixpanel.track('Social Media Referral') # High Level Tasks 

At this point you may be wondering what kind of events you should be tracking. You need to track your goals. Analytics is all about setting goals, testing and then making adjustments to achieve those goals. If you’re not sure where to start, take a look at Dave McClure’s AARRR! Framework. AARRR stands for Acquisition, Activation, Retention, Referral, and Revenue. You can start by creating one goal for each of those key points. In our example code review service it might be broken out like this:

  • Acquisition: “User Sign Up”
  • Activation: “User Submits a GitHub Repo”
  • Retention: “User Submits a GitHub Repo again within one month”
  • Referral: “User Clicks Social Share”
  • Revenue: “Users Credit Card Charged”

To read more about AARRR checkout the original post here or this Mixpanel specific post about it here.

require 'mixpanel-ruby' 
PROJECT_TOKEN = '539d98201fc65b215d25339537e4d945' 
tracker = Mixpanel::Tracker.new(PROJECT_TOKEN) 

Tracking API

Tracking an event is pretty easy. You specify the Mixpanel user ID, the event name and a hash of whatever extra properties you’d like to add to the event. The user ID is a unique string you must construct to tie events to a specific user. You should not use the simple database ID number of the user in your Rails project for the Mixpanel user_id as that is subject to change. Instead, you should generate a unique number based on SecureRandom or a secure hexdigest of some other unique data.

Mixpanel has the concept of anonymous versus registered users and its API also supports linking the two together when the anonymous user eventually signs up for your service and converts to a registered user. In the JavaScript client many properties of the HTTP request are automatically sent with each event, but in the Ruby version if you want to include these properties you will need to specify them manually.

tracker.track(user_id, event_name, properties_hash)

In order to identify a newly signed up user, use the “alias” method, not to be confused with Ruby’s own alias_method

# Identify a newly signed up user
tracker.alias(user_id, original_anonymous_id)

You can also track information about a user’s identity. This is useful for later contacting segments of your user base within the Mixpanel web user interface by email, phone, SMS, or push notifications.

# Track information about a User's Identity 
tracker.people.set(id, properties_hash) 

Mixpanel offers a specific API call in order to track revenue for a given user.

# Track Revenue 
tracker.people.track_charge(user_id, amount, properties_hash)

Performance Concerns

By default, events are sent synchronously. This is not going to scale well for you because now every time someone makes a request to your web application it will block waiting for the request to Mixpanel’s API to complete. One solution is to send all of your events from the client side using JavaScript. If you are going to use the Ruby client library then you will need to send your events to Mixpanel asynchronously by implementing some kind of background queue like Kestrel, Sidekiq or even a simple text file to store and read them from.

When using the JavaScript library there is a function called track_links() that will send link click events asynchronously and prevent problems with the page being reloaded before the Mixpanel event is sent.

mixpanel.track_links(
  '#subscribe a', 'User Subscribed', {
  'subscription_plan': 'Code Crafter',
});

Example Implementation

It’s nice to wrap any analytics tracker up in an abstraction so that you can swap it out if you later choose to implement a different analytics solution:

class Tracker 
  def initialize(user_id) 
    @user_id = user_id 
  end 

  def track(event, params = {}) 
    tracker.track(@user_id, event, params) 
  end 

  private 

  def tracker 
    @tracker ||= Mixpanel::Tracker.new(Rails.env['PROJECT_TOKEN'])
  end 
end 

You can also create a module to mix in to any controllers or service objects to give some convenience methods for tracking:

module Analyzable 
  private 

  def tracker
    @tracker ||= Tracker.new(user_id)
  end

  def user_id
    current_user.tracking_id
  end 
end 

If you want to support multiple analytics platforms you can use dependency injection to configure the tracking solution on the fly, like this:

class CodeReviewController < ApplicationController
  include Analyzable 

  def create
    report = code_review_service.generate_report(params[:repository]) 
    render :show, locals: { report: report } 
  end 

  private 

  def code_review_service 
    CodeReviewService.new do |config| 
      # Instance of Mixpanel::Tracker 
      # from the Analyzable module 
      config.tracker = tracker 
    end 
  end 
end

Finally, you could have your service objects run the domain logic and perform the event tracking:

class CodeReviewService 
  attr_writer :tracker 

  def initialize(repository, &amp;block) 
    self.repository = repository 
    yield self if block_given? 
  end 

  def generate_report 
    analyze_code 
    write_results 
    track_report_creation 
  end 

  private 

  attr_accessor :repository 
  attr_reader :tracker 

  def track_report_creation 
    tracker.track(CREATE_REPORT, repository: repository) if tracker 
  end 
end

Mixpanel slices and dices on properties, so be sure to give it a generous portion of properties with a limited amount of events tied to your application logic.

Mixpanel Dashboard

You should also define the event name strings themselves in a common place as constants or an enum because they are case sensitive. Accidentally tracking the same event as separate events due to a capitalization mistake can completely skew your results.

# Three 
tracker.track('Course Started') 

# Separate 
tracker.track('Course started') 

# Events 
tracker.track('course started') 

# (Be careful!)

Client Versus Server

When deciding whether to use client-side or server-side tracking there are a number of advantages to each. Default to client-side tracking because it gives you a whole grab bag of free:

  • free Async
  • free HTTP request Properties
  • More free Time
$(document).ready(function() {

    // Many HTTP Request Properties
    // are already sent by default
    mixpanel.track('Some Event', {
      some_property: 'value',
      some_property: 'value',
      some_property: 'value'
    });

  });

Server-side tracking is beneficial when you want to track actions taking place with shared services or using an API that is presented using multiple front ends. Be sure to include as many HTTP request properties in your events up front as you are able to.

Mixpanel allows you to define funnel, segmentation and retention goals at any point in the future. If you don’t track the data now, then you won’t be able to segment on it in the future.

Extra Help

I highly recommend you watch all of the demo videos on mixpanel.com, they are fairly short and informative. They cover the following really useful features:

  • Funnels
  • Segmentation
  • Retention
  • Live View
  • Notifications
  • Revenue
  • Bookmarks
  • People

For additional help implementing analytics it is important to coordinate with everyone who will be producing and consuming the metrics in order to define goals, track data consistently, and produce understandable and actionable results.

For more information about analytics in general, check out the Lean Analytics Book. If you intend to use a client-side implementation, be sure to check out Segment.io’s analytics.js and its documentation, which abstracts away from specific analytics implementations in case you want to change your provider or support multiple providers in the future.

I’m curious to know what problems you’re trying to solve with analytics. What lessons you have learned along the way?


Matt Closson
Matt Closson

Filed under: Strategy, Rescue

Next article →

Rake: File Tasks

Jacob Swanner Jacob Swanner