Hit tracking library for Ruby using Redis
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.



Hit tracking library for Ruby using Redis


Boffin is a library for tracking hits to things in your Ruby application. Things can be IDs of records in a database, strings representing tags or topics, URLs of webpages, names of places, whatever you desire. Boffin is able to provide lists of those things based on most hits, least hits, it can even report on weighted combinations of different types of hits.


Getting started

You need a functioning Redis installation. Once Redis is installed you can start it by running redis-server, this will run Redis in the foreground.

You can use Boffin in many different contexts, but the most common one is probably that of a Rails or Sinatra application. Just add boffin to your Gemfile:

gem 'boffin'


Most of Boffin's default configuration options are quite reasonable, but they are easy to change if required:

Boffin.config do |c|
  c.redis              = MyApp.redis             # Redis.connect by default
  c.namespace          = "tracking:#{MyApp.env}" # Redis key namespace
  c.hours_window_secs  = 3.days     # Time to maintain hourly interval data
  c.days_window_secs   = 3.months   # Time to maintain daily interval data
  c.months_window_secs = 3.years    # Time to maintain monthly interval data
  c.cache_expire_secs  = 15.minutes # Time to cache Tracker#top result sets


A Tracker is responsible for maintaining a namespace for hits. For our examples we will have a model called Listing it represents a listing in our realty web app. We want to track when someone likes, shares, or views a listing.

Our example web app uses Sinatra as its framework, and Sequel::Model as its ORM. It's important to note that Boffin has no requirements on any of these things, it can be used to track any Ruby class in any environment.

Start by telling Boffin to make the Listing model trackable:



class Listing < Sequel::Model
  include Boffin::Trackable

You can optionally specify the types of hits that are acceptable, this is good practice and will save frustrating moments where you accidentally type :view instead of :views, to do that:

Boffin.track(Listing, [:likes, :shares, :views])


class Listing < Sequel::Model
  include Boffin::Trackable
  boffin.hit_types = [:likes, :shares, :views]


class Listing < Sequel::Model
  Boffin.track(self, [:likes, :shares, :views])

Now to track hits on instances of the Listing model, simply:

get '/listings/:id' do
  @listing = Listing[params[:id]]
  erb :'listings/show'

However you will probably want to provide Boffin with some uniqueness to identify hits from particular users or sessions:

get '/listings/:id' do
  @listing = Listing[params[:id]]
  @listing.hit(:views, unique: [current_user, session[:id]])
  erb :'listings/show'

Boffin now adds uniqueness to the hit in the form of current_user.id if available. If current_user is nil, Boffin then uses session[:id]. You can provide as many unique factors as you'd like, the first one that is not blank (nil, false, [], {}, or '') will be used.

It could get a bit tedious having to add [current_user, session[:id]] whenever we want to hit an instance, so let's create a helper:

helpers do
  def hit(trackable, type)
    trackable.hit(type, unique: [current_user, session[:id]])

For these examples we are in the context of a Sinatra application, but this is applicable to a Rails application as well:

class ApplicationController < ActionController::Base
  def hit(trackable, type)
    trackable.hit(type, unique: [current_user, session[:session_id]])

You get the idea, now storing a hit is as easy as:

get '/listings/:id' do
  @listing = Listing[params[:id]]
  hit @listing, :views
  erb :'listings/show'


After some hits have been tracked, you can start to do some queries:

Get a count of all views for an instance


Get count of all unique views for an instance

@listing.hit_count(:views, unique: true)

Get count of unique views for a specific user

@listing.hit_count(:views, unique: current_user)

Get IDs of the most viewed listings in the past 5 days

Listing.top_ids(:views, days: 5)

Get IDs of the least viewed listings (that were viewed) in the past 8 hours

Listing.top_ids(:views, hours: 8, order: :asc)

Get IDs and hit counts of the most liked listings in the past 5 days

Listing.top_ids(:likes, days: 5, counts: true)

Get IDs of the most liked, viewed, and shared listings with likes weighted higher than views in the past 12 hours

Listing.top_ids({ likes: 2, views: 1, shares: 3 }, hours: 12)

Get IDs and combined/weighted scores of the most liked, and viewed listings in the past 7 days

Listing.top_ids({ likes: 2, views: 1 }, hours: 12, counts: true)

Boffin records hits in time intervals: hours, days, and months. Each interval has a window of time that it is available before it expires; these windows are configurable. It's also important to note that the results returned by these methods are cached for the duration of Boffin.config.cache_expire_secs. See Configuration above.


Not just for models

As stated before, you can use Boffin to track anything. Maybe you'd like to track your friends' favourite and least favourite colours:

@tracker = Boffin::Tracker.new(:colours, [:faves, :unfaves])

@tracker.hit(:faves,   'red',    unique: 'lena')
@tracker.hit(:unfaves, 'blue',   unique: 'lena')
@tracker.hit(:faves,   'green',  unique: 'soren')
@tracker.hit(:unfaves, 'red',    unique: 'soren')
@tracker.hit(:faves,   'green',  unique: 'jens')
@tracker.hit(:unfaves, 'yellow', unique: 'jens')

@tracker.top(:faves, days: 1)

Or, perhaps you'd like to clone Twitter? Using Boffin, all the work is essentially done for you*:

WordsTracker = Boffin::Tracker.new(:words, [:searches, :tweets])

get '/search' do
  @tweets = Tweet.search(params[:q])
  params[:q].split.each { |word| WordsTracker.hit(:searches, word) }
  erb :'search/show'

post '/tweets' do
  @tweet = Tweet.create(params[:tweet])
  if @tweet.valid?
    @tweet.words.each { |word| WordsTracker.hit(:tweets, word) }
    redirect to("/tweets/#{@tweet.id}")
    erb :'tweets/form'

get '/trends' do
  @words = WordsTracker.top({ tweets: 3, searches: 1 }, hours: 5)
  erb :'trends/index'

*This is a joke.

Custom increments

For some applications you might want to track something beyond simple hits. To accomodate this you can specify a custom increment to any hit you record. For example, if you run an ecommerce site it might be nice to know which products are your bestsellers:

class Product < ActiveRecord::Base
  Boffin.track(self, [:sales])

class Order < ActiveRecord::Base
  after_create :track_sales


  def track_sales
    line_items.each do |line_item|
      product = line_item.product
      amount  = product.amount.cents * line_item.quantity
      product.hit :sales, increment: amount

Then, when you want to check on your sales over the last day:

Product.top_ids(:sales, hours: 24, counts: true)

The Future™

  • Ability to hit multiple instances in one command
  • Ability to get hit-count range for an instance
  • Some nice examples with pretty things
  • Maybe ORM adapters for niceness and tighter integration
  • Examples of how to turn IDs back into instances
  • Reporting DSL thingy
  • Web framework integration (helpers for tracking hits, console type ditty.)
  • Ability to union on unique hits and raw hits


OMG haven't you heard of page caching?! How am I supposed to use this if my Ruby app doesn't get hit?

Make an XHR request to an endpoint which is soley responsible for tracking hits to stuff whenever a specific thing loads. My above examples are just to demonstrate how to use the APIs, you can use them wherever you want.

What's with the name?

Well, it means this. For the purposes of this project, its use is very tongue-in-cheek.

Are you British?

No, I'm just weird, but this guy is a real British person.