December 27, 2010

Making Statistical Social Validation Fast

One of the challenges we have at RecruitMilitary is that there is a perception that there are lots of companies out there that "prey on veterans" and there is skepticism that we are who we say we are.

Why do it?

We know we're for real, but how do we communicate that quickly and effectively?

I've been pushing us to just show the numbers... while we're always pushing for improvement, on the whole I'm proud of our activity and just showing visitors our numbers in real time, I believe, should be convincing. Here's a screenshot of the finished product:

RecruitMilitary Board Social Validation

You can visit the site (during business hours is best) to see the stats bar in action.

The technical nitty-gritty

We knew that we'd end up with people sitting and watching the stats increment, so we wanted to make sure the request was fast. We use Resque as our background processing system, and along with it came Redis. We basically took the ideas in Resque's stat module and implemented a very fast way to get the counters from our system.

However, the data is actually coming from a different Rails app than our marketing site, so we rolled a simple little jsonp service. Here's the controller:

module Api::V1
  class StatsController < BaseController
    caches_action :index, :expires_in => 10.seconds

    def index
      @stats = 
        Board::Stat::DAILY_STATS.inject({}) do |h, stat|
          h[stat] = Board::Stat[stat]
          h["#{stat}_today"] = Board::Stat["#{stat}_today"]
          h
        end

      render_json(@stats)
    end
  end
end

In the destination page, we drop in a simple div that is filled in with javascript:

<div id="stats" class="hidden wide bordered">
</div>

The controller response was quick, but we were getting so many hits to it, we had to optimize further.

We did two 2 things:

The refined javascript:

$.fn.boardStatisticize = function() {
  var stat_box = this;

  $.ajax({
    cache: true,
    dataType: "jsonp",
    jsonpCallback: "statsicizer",
    success: handleResponse,
    url: "https://board.recruitmilitary.com/api/v1/stats"
  });

  function handleResponse(data) {
    var stats = data;
    var displayed_stats = ["active_jobs", "job_views",
      "candidate_accounts", "candidate_profile_views"];

    for (i = 0; i < displayed_stats.length; i++) {
      insertStat(displayed_stats[i],
      stats[displayed_stats[i]]);
    }

    function insertStat(key, value) {
      $("<div></div>")
        .addClass("block")
        .append(blockLabel())
        .append(totalCount())
        .append(todayCount())
        .attr("id", key)
        .css("width", (stat_box.innerWidth() / 4) - 22)
        .appendTo(stat_box);

      function blockLabel() {
        return $("<h3></h3>")
          .addClass("label")
          .text(BR.board_stat_labels[key]);
      }

      function totalCount() {
        return $("<span></span>")
          .addClass("total-count")
          .data("count", value)
          .text(commafyNumber(value));
      }

      function todayCount() {
        return $("<span></span>")
          .addClass("today-count")
          .data("count", stats[key + "_today"])
          .text("+" + 
            commafyNumber(stats[key + "_today"]) +
            " today");
      }
    }

    $("#candidate-form .job-count")
      .text(commafyNumber(stats["active_jobs"]));

    stat_box.removeClass("hidden");
  }

  window.setInterval("updateBoardStats()", 10000);

  return this;
};

function updateBoardStats() {
  if ($("html").is(".blurred"))
    return;

  $.ajax({
    cache: true,
    dataType: "jsonp",
    jsonpCallback: "statsicizer",
    success: handleResponse,
    url: "https://board.recruitmilitary.com/api/v1/stats"
  });

  function handleResponse(data) {
    var stats = data;
    var displayed_stats = ["active_jobs", "job_views",
      "candidate_accounts", "candidate_profile_views"];

    for (i = 0; i < displayed_stats.length; i++) {
      updateStat(displayed_stats[i],
      stats[displayed_stats[i]]);
    }

    function updateStat(key, value) {
      var total = $("#" + key + " .total-count");
      var today = $("#" + key + " .today-count");

      if (total.data("count") !== value) {
        total
          .addClass("updated")
          .data("count", value)
          .text(commafyNumber(value))
          .switchClass("updated", "", 5000);
        today
          .addClass("updated")
          .data("count", stats[key + "_today"])
          .text("+" + commafyNumber(stats[key +
             "_today"]) + " today")
          .switchClass("updated", "", 5000);
      }
    }

    $("#candidate-form .job-count")
      .text(commafyNumber(stats["active_jobs"]));
  }
}

Great work from Michael and Jason on this, we continue to get daily complements on our new home page, and I know this feature is central to the great experience.