Hackathon: Building a Sales Leaderboard with RabbitFeed and Websockets

As mentioned in this post, we’ve been aligning our approach to business intelligence with our architectural principles. One of our hypotheses was that by taking an event-based approach to analytics we would be able to be more agile with our data. In order to test this hypothesis, we needed a ‘killer-app’ that would not only prove the approach, but also help us sell it to the rest of the business.

Finding a Killer-App

We sell our product via two primary channels: online and offline. Our offline channel is served by a contact centre in our Northampton office. Sales consultants call potential customers and attempt to make sales over the phone. Previously, whenever consultants made sales, they would have to get up from their desks, walk to their team’s white board, and manually record the sale.

Our idea for the ‘killer-app’ was to utilise the wall boards scattered about the contact centre for displaying a sales leaderboard fed by events from our back office system. Not only would this remove an inefficient manual step in the sales process, it would also foster a sense of friendly competition amongst consultants (we hoped).

The Leaderboard

The leaderboard is a simple ranking of consultants by the total value of the product that they have sold for the day. Rankings can be split out by teams to show consultants not only how they rank with the rest of the contact centre, but also within their own team. It looks something like this (note that this is sample data):

The leaderboard

How it Works

Whenever a consultant completes a sale on our back office system, it publishes a sales event. The leaderboard application subscribes to these sales events. Once a sales event is consumed, it is immediately pushed up to the leaderboard.

The Sales Event

The event publish/subscribe is performed using RabbitFeed.

Publishing

The event publish is done at the point the consultant completes a sale in the back office. This event is then available to any downstream application for consumption.

The sales event is defined in the RabbitFeed initialiser like this:

EventDefinitions do
  define_event('consultant_completes_sale', version: '0.0.1') do
    defined_as do
      %w{
        A consultant has completed a sale in the back office.
      }
    end
    payload_contains do
      field('consultant', type: 'string', definition: 'The name of the consultant')
      field('team', type: 'string', definition: 'The name of the team to which the consultant belongs')
      field('amount', type: 'string', definition: 'The amount of the sale, in dollars')
    end
  end
end

The event is published from the Rails controller:

class SalesController < ApplicationController

  def create
    sale = Sale.new sale_params
    if sale.save
      RabbitFeed::Producer.publish_event 'consultant_completes_sale', {
        consultant: sale.consultant.name,
        team:       sale.consultant.team,
        amount:     sale.amount,
      }
      ...
    else
      ...
    end
  end
end

Subscribing

A RabbitFeed consumer process runs in the Rails environment on the leaderboard server. This process will apply the sale to the day’s rankings and will push the update to the clients.

The subscription is configured in the RabbitFeed initialiser like this:

EventRouting do
  accept_from('back office') do
    event('consultant_completes_sale') do |event|
      (EventHander.new event).update_leaderboard
    end
  end
end

When an event is consumed, a consultant ranking record is found or initialised and the sale is applied to the consultant’s ranking. The updated ranking is then pushed up to the clients. The event handling code looks like this:

class EventHandler

  attr_reader :event

  def initialize event
    @event = event
  end

  def update_leaderboard
    consultant_ranking.record_sale! event.amount
    push_to_clients
  end

  private

  def consultant_ranking
    @consultant_ranking ||= ConsultantRanking.find_or_initialize_by(
      consultant: event.consultant,
      team:       event.team,
      date:       (Time.parse event.transacted_at_utc).to_date,
    )
  end

  def push_to_clients
    ...
  end
end

Updating the Leaderboard

At any one time, there will be 10-15 clients displaying the leaderboard in a browser window. There are a few options available for updating the leaderboard with new data and sales. We could simply trigger the browser to refresh the page at specified time intervals, but this method doesn’t afford any interactivity or excitement when new data is displayed. Alternatively, we could have each client continuously poll the server for updates, but this requires the client to maintain some state about the updates it has processed. We wanted a mechanism by which all clients would be updated simultaneously and instantaneously whenever a sale was made. This allows the client to apply any updates to the leaderboard with a flash! We were able to achieve this via the use of Web Sockets.

Configuration

First, we need to add the websocket-rails gem to our leaderboard application:

gem 'websocket-rails'

Next, we need to enable the RabbitFeed consumer process to trigger websocket events. Because the RabbitFeed consumer process is asynchronous, it runs as a separate process outside of the leaderboard web application. We needed a means for websocket events triggered within the RabbitFeed consumer process to be sent to clients connected to the leaderboard web application. Fortunately, websocket-rails has this capability built-in: Using Redis, a websocket event triggered on one process will be pushed onto Redis and synchronised to the other process.

Set this configuration option in the websocket-rails initialiser to enable websocket synchronisation:

# Change to true to enable channel synchronization between
# multiple server instances.
# * Requires Redis.
config.synchronize = true

The websocket event will be published to the consultant_ranking channel. The clients will subscribe to this channel. The event is published by calling trigger with the change event and a payload. The client handler will bind to this event type. The code to trigger the websocket event looks like this:

class EventHandler

  ...

  def push_to_clients
    WebsocketRails[:consultant_ranking].trigger :change, {
      consultant:    consultant_ranking.consultant,
      team:          consultant_ranking.team,
      units_sold:    consultant_ranking.units_sold,
      sales_revenue: consultant_ranking.sales_revenue,
      ranking:       consultant_ranking.ranking,
    }
  end
end

On the client side, we create a websocket pointing back to the websocket URL on our server. We then subscribe to the consultant_ranking channel and define a handler function for change events. The event handler will be called for every change event triggered on the server. The event handler will apply the change in ranking to the leaderboard with a flash. The code looks like this:

// Create the socket
// The socket URL looks something like this: http://localhost:3000/websocket
var dispatcher = new WebSocketRails(window.location.host+'/websocket');

// Subscribe to the websocket channel on which we publish the events
var channel = dispatcher.subscribe('consultant_ranking');

// Declare the event handler
// Bind to the 'change' channel event
channel.bind('change', function(consultant_ranking) {

  // This is what gets called when an event is received
  update_consultant_ranking(consultant_ranking);

};

Performance

Due to the distributed nature of the design, there are no bottlenecks between the back office application and the leaderboard. As soon as a sales event is published, the leaderboard subscriber consumes the event and pushes it up to the clients. In fact, the sale often registers on the leaderboard before the back office system can load the post-sale page!

Outcome

The leaderboard was an instant success: Sales consultants began vying for the top ranking on the leaderboard, team managers were able to get immediate feedback on the performance of their team members, and senior management gained improved insight on who the top performers in the contact centre were. The leaderboard proved that an event-based approach to analytics would allow us to be more agile with our data. Additional applications were soon built using the same approach. For example, we created a daily sales dashboard showing stats on the day’s sales, split by the online and offline channels as well as a business KPI monitoring and alerting system.

Find out more about how you can use RabbitFeed to unlock the potential of your data

Footer