Metrics, from a developer's perspective, are simultaneously one of the most compelling and one of the most boring things you can ever be asked to look into. There's an old adage that goes something like, "If you want to improve something, measure it." There's a certain amount of logic behind that. Our instincts and best guesses can take us pretty far when trying to develop useful and robust applications, but there's nothing quite like hard data to show us the real shape of the world.

Data can give you a ton of insight into your application and there's a certain amount of OCD-like satisfaction with creating the perfect six-table join statement in SQL that exactly captures the metric you were trying to shake out. However, it is a very particular kind of person that's going to be willing to do this day-to-day. It's one thing to export a CSV from the database, filter the content down, create a pivot table or pareto chart and email it out every once in a while. But the monotony of having to do that once per day or once per week will drive any developer to madness.

What we really want is a programmatic way to find out this information and share it in an easily consumable manner. Yes, we can generate CSV reports that get mailed out daily or look at slides at the quarterly company meeting; those can provide a lot of value. But for our daily lives, what we really want are metrics that people are going to want to look at. We want metrics that motivate people. We want metrics that inform you of your app's daily successes and failures. We want metrics that let you know when something has gone terribly, terribly wrong with your application. And we want metrics that you can read and understand at a glance.

Enter Status Board

What is Status Board

Conveniently named, Status Board is an iOS application written by a company named Panic that allows you to create custom status boards for anything you'd like. All you need is a TV, an iPad along with the right cables to connect the two and you're in business. Lucky for me, thanks to some careless luggage handlers at the Westin in Portland, OR, I had a slightly maimed iPad that was no longer suitable for personal use that I could sacrifice to the cause.

For getting live data into the app, there are basically two approaches you can take:

  1. You have your application upload a file with the data you want to a Dropbox account, then have the Status Board periodically pull the data down to be displayed. This method... has simplicity going for it, but that's about it. To get it to work, you're going to have to write some kind of CSV export and then have something like a cronjob running to push the file all day long. It works, but you're going to be really limited in what you're going to be able to display in the Status Board app, since it only allows you to present CSV data in a couple different formats.

  2. Rendering a web page outside of the app that the iPad can connect to, which gives you access to what they call the DIY panel. Basically, you give the app an HTML endpoint and it will pull down that page and render it in the app, every 10 minutes by default. Now that you're dealing in straight HTML, you can render almost anything you'd like. Tables, graphs, maps, images... anything you can render from the given web app, you can put straight onto your board.

Nitty gritty

At Avvo, I was lucky enough that we had a Rails app that was accessible from inside the office network that also had access to all the data I could conceivably want to show. For example, as app developers we might like to know what our daily sales stats were for the last week, so we can write a simple html page that shows the count for each of the last 7 days (with the previous week's sales for that day in parentheses, for comparison).

Presenter code:

class StatsPresenter
    def date(days_back)
        "#{days_back.days.ago.month}/#{days_back.days.ago.day}"
    end

    def day_of_week(days_back)
        return "Today" if days_back == 0
        Date::DAYNAMES[days_back.days.ago.beginning_of_day.wday]
    end

    def days_sales(days_back)
        Purchase.where(:created_at => days_back.days.ago.beginning_of_day..days_back.days.ago.end_of_day).count
    end
end

View code (written in Slim):

- stats_presenter = StatsPresenter.new

table
  - for days_back in 0..6
    tr
      td.date = stats_presenter.date(days_back)
      td.day = stats_presenter.day_of_week(days_back)
      td = "#{stats_presenter.days_sales(days_back)} (#{stats_presenter.days_sales(days_back + 7)})"

This gives you a simple table that Status Board can render; you just have to create a new DIY panel in the app and point it at this page:

DIY configuration

And voila:

Week-over-week sales stats

Note: I found it necessary to tell the controller to skip layout rendering, since we had some standard headers/footers and such in our layouts that we didn't want rendered in each panel. So you may want to add this to your stats controller:

layout: false

You can get pretty fancy with what you display. Let's take it one step further and use the Google Maps API to display some data. For my team's product, we roll out our product to certain states first, so we wanted a simple way to track state-by-state progress on how things were going. We decided to highlight each state with a specific color to indicate its status. A red state would indicate that there's a problem with the state and blue would indicate that the state is ready to go.

Embedding a Google map onto your status board is fairly trivial:

<div id="map"></div>

<script src="https://maps.googleapis.com/maps/api/js"></script>
<script type="text/javascript">
function initialize() {
    var mapCanvas = document.getElementById('map');
    var mapOptions = {
      center: new google.maps.LatLng(38.0000, -96.0000),
      zoom: 4,
      mapTypeId: google.maps.MapTypeId.ROADMAP,
      disableDefaultUI: true
    }
    var map = new google.maps.Map(mapCanvas, mapOptions);
}

google.maps.event.addDomListener(window, 'load', initialize);
</script>

This gets you a simple map of the United States.

Basic Google Map

Next, Google also allows you to overlay polygons of your own onto the map. You do this by passing in the latitude and longitude of each coordinate of the polygon, then Google strings them all together to get the desired shape in whatever color you specify. I found a rough listing of the necessary coordinates online and converted them into a yml file for the application to use. Now, you have to be able to show the polygon data in a format that is readable by the Google API. The simplest way I found was to create a separate XML endpoint that returns the polygons you want to display, along with their color, which can then be read via JavaScript and sent to Google.

Controller code:

  class StatsPresenter
    def state_status
      @states = []

      State.all.each do |state|
        state_data = {}

        state_data["name"] = state.name
        state_data["points"] = state_polygons[state.name.downcase.tr(" ", "_")]
        state_data["color"] = color_for_state(state)

        @states << state_data
      end

      @states
    end

    def state_polygons
      @state_polygons ||= YAML.load_file(Rails.root.join('db/', 'domain/', 'state_polygons.yml'))
    end

    def color_for_state (state)
      if state_functional? state
        return "#0000ff"
      else
        return "#ff0000"
      end
    end
  end

Along with an XML builder:

  xml.instruct!
  xml.states do
    @stats_presenter.state_status.each do |state|
      xml.state(:name => state["name"], :color => state["color"]) do
        state["points"].each do |point|
          xml.point(:lat => point["lat"], :lng => point["lng"])
        end
      end
    end
  end

This will create the XML output you need for the state polygons:

<?xml version="1.0" encoding="UTF-8"?>
<states>
  <state name="Arizona" color="#0000ff">
    <point lat="36.9993" lng="-112.5989"/>
    <point lat="37.0004" lng="-110.8630"/>
    <point lat="37.0004" lng="-109.0475"/>
    <point lat="31.3325" lng="-109.0503"/>
    <point lat="31.3325" lng="-111.0718"/>
    <point lat="32.4935" lng="-114.8126"/>
    <point lat="32.5184" lng="-114.8099"/>
    <point lat="32.5827" lng="-114.8044"/>
    <point lat="32.6246" lng="-114.7992"/>
    ...

Finally, you need the javascript to retrieve, process and send the polygons to Google. You do this with a simple jQuery call to your XML status endpoint, which you add to the Google Maps configuration from above:

<script src="https://maps.googleapis.com/maps/api/js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script type="text/javascript">
  function initialize() {
    var polys = [];
    var mapCanvas = document.getElementById('map');
    var mapOptions = {
      center: new google.maps.LatLng(38.0000, -96.0000),
      zoom: 4,
      mapTypeId: google.maps.MapTypeId.ROADMAP,
      disableDefaultUI: true
    }
    var map = new google.maps.Map(mapCanvas, mapOptions);

    jQuery.get("/api/1/stats/state_status.xml", {}, function (data) {
      jQuery(data).find("state").each(function () {
        var color = this.getAttribute('color');
        var points = this.getElementsByTagName("point");
        var pts = [];
        for (var i = 0; i < points.length; i++) {
          pts [i] = new google.maps.LatLng(parseFloat(points [i].getAttribute("lat")),
          parseFloat(points [i].getAttribute("lng")
        ))
          ;
        }
        var poly = new google.maps.Polygon({
          paths: pts,
          strokeColor: '#000000',
          strokeOpacity: 1,
          fillColor: color,
          fillOpacity: 0.35
        });
        polys.push(poly);
        poly.setMap(map);
      });
    });
  }

  google.maps.event.addDomListener(window, 'load', initialize);
</script>

And now you can watch the status of each of your states in near-realtime:

Map with status overlays

Using DIY panels along with the versatility of your web applications, you can make a rich set of panels and pages for your status board that let you see how your app is performing at any given time, at a glance.

Sample stats

(Note that all the data is faked out for the purposes of these screenshots)