Introducing Tally

February 21, 2020

Last week I open sourced a new RubyGem called Tally. Tally was created over the past few years as a part of a number of products I’ve worked on and I’ve always wanted to open it up to the public for anyone else to use as they see fit.

Tally is a quick utility for collecting counters and stats throughout a Rails application. Technically I suppose it could be used outside of Rails in a standard Ruby app, but that’s not my use-case so I haven’t spent any time optimizing it for that.

Tally sits on top of Redis for fast collection of these counters. The goal of the stat collection was to make it as fast as possible so that counters could be incremented throughout your application code in real-time.

Periodically throughout the day the counters are extracted from Redis and archived into a standard ActiveRecord model within your application. There’s a single table added to your database that keeps track of the counters each day after they are archived.

That’s it. It’s a quick and simple way to get basic stats reporting into an application. There are certainly bigger players in this space, and I’ve used several before as well. StatsD is a great example of a much more robust and scalable tool for this sort of thing. But in my recent use cases, it’s just a bit overkill. Sure, I could spin up StatsD and get it all to work. But I had bigger areas that I wanted to focus on, so Tally is a great place to start.

Side note: if you’re wanting to collect millions and millions of data points, you probably need something different here. Tally can work, but there are better tools for that job.

I’m currently using Tally for few specific needs, which I think are perfect use cases for the project. Here are a few use cases so far…

Use Case 1: Tracking Ad Impressions

First, I run a private ad server for a current project. I didn’t want to use a third-party ad server for privacy concerns and because I generally don’t trust that industry. That’s another story, for another day. However, it is important to report to our brand advertisers on how well advertising campaigns are performing. This includes impressions (views of each ad) and clicks. For video-based ads I also track the number of plays and completion percentage to see how well everything is performing. All of this is done privately with no user or personal information whatsoever shared with anyone.

Here’s a quick mockup of how it works. There is an AdUnit model in our database. It just stores basic data about the ads. Click-through links, the artwork for the ad, video streaming locations, and that sort of thing. Within that model, we use the Tally::Countable concern:

# app/models/ad_unit.rb
class AdUnit < ApplicationRecord

  include Tally::Countable

  # ...

end

Then, in the controller that renders the ad unit, we simply increment the impressions counter each time it is displayed:

# app/controllers/ad_units_controller.rb
class AdUnitsController < ApplicationController

  # GET /ad-units/:id
  def show
    @ad_unit = AdUnit.find(params[:id])
    @ad_unit.increment_tally(:impressions)
  end

end

We have a similar process to handle tracking for when an ad is clicked. To preserve the privacy of our readers we don’t directly link to the advertiser’s site. When an ad is clicked the link first goes through our server to clean any identifiable information from the user. This is also a great place for me to increment the clicks counter so I can accurately track how many people clicked on each ad.

From these two Tally counters we can derive something that looks like the screen below and see how many impressions and clicks each ad unit received:

Tally counters for tracking ad impressions and clicks
(This data is all fake, don't read into it!)

The tracking for all of this is incredibly simple and with a few queries to get the data out of Tally, we have enough information to share with advertisers on how well their campaign is performing.

Use Case 2: Tracking Subscriber Counts over time

My second current use case is to keep track of the total number of subscribers (aka users) of my product on a daily basis. Since Tally is created specifically to track counts by day, this is a perfect way to collect data.

I could increment a counter of each time a new subscriber signs up. But that’s a bit unnecessary since each time someone signs-up there will be a new Subscriber record in my database. So I’m using Tally’s custom calculators feature to quickly count the number of subscribers and store it along with Tally’s other records.

Here’s a quick and dirty example of how I do this:

# in an initializer file,
# something like config/initializers/tally.rb

Tally.register_calculator "SubscribersCalculator"</code></pre>

Then, I have the calculator in a service class inside my app folder:

# in app/calculators/subscribers_calculator.rb
class SubscribersCalculator

  include Tally::Calculator

  def call
    start_at = day.beginning_of_day
    end_at = day.end_of_day

    count = Subscriber.where(created_at: start_at..end_at).count

    {
      key: :subscribers,
      value: count
    }
  end

end

The Tally::Calculator concern automatically sets up an initializer and the day variable is the current Date, or the date that we want collect data for. Each time the Tally archive process runs (I run it hourly) this calculator is run and the data is stored alongside the other Tally counts I’ve collected.

To make things a bit more detailed we can also store the number of signups for each referral source. This is helpful to know where subscribers are finding our site from when they sign up each day. (Are people finding us from search, social media, organic traffic, a paid traffic campaign, etc.) In my use case, I have a source enum value on the Subscriber model that stores this information when the subscriber registers:

class Subscriber < ApplicationRecord

  enum source: %w( organic search social paid )

  # ...

end

If we wanted to collect subscriber counts by source, we could modify the calculator to something like this:

class SubscribersCalculator

  include Tally::Calculator

  def call
    start_at = day.beginning_of_day
    end_at = day.end_of_day
    result = []

    scope = Subscriber.where(created_at: start_at..end_at)

    # store the total number of subscribers for this day
    result.push(
      key: :subscribers,
      value: scope.count
    )

    # store the number of subscribers for each source
    Subscriber.sources.keys.each do |source|
      count = scope.where(source: source).count

      result.push(
        key: "#{ source }_subscribers",
        value: count
      )
    end

    result
  end

end

This updated calculator will store the total number of subscribers each day, and also the total number of subscribers per referral source. After a few days of gathering data we can produce a simple report like this to show a graph of our new subscribers:

An example of Tally counters placed into some charts
(This data is all fake, don't read into it!)

So that’s the initial release of Tally. It’s a simple but very powerful tool for collecting stats in a Rails application. It’s been very useful for me over the years, and I hope it can be useful to others.

The full documentation and README for Tally is available on Github and I’m certainly open to Pull Requests, Issues, bug reports, or other feedback.