Boffin
Hit tracking library for Ruby using Redis
About
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.
Resources
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'
Configuration
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
end
Tracking
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:
Boffin.track(Listing)
or
class Listing < Sequel::Model
include Boffin::Trackable
end
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])
or
class Listing < Sequel::Model
include Boffin::Trackable
boffin.hit_types = [:likes, :shares, :views]
end
or
class Listing < Sequel::Model
Boffin.track(self, [:likes, :shares, :views])
end
Now to track hits on instances of the Listing model, simply:
get '/listings/:id' do
@listing = Listing[params[:id]]
@listing.hit(:views)
erb :'listings/show'
end
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'
end
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]])
end
end
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
protected
def hit(trackable, type)
trackable.hit(type, unique: [current_user, session[:session_id]])
end
end
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'
end
Reporting
After some hits have been tracked, you can start to do some queries:
Get a count of all views for an instance
@listing.hit_count(:views)
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.
More
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'
end
post '/tweets' do
@tweet = Tweet.create(params[:tweet])
if @tweet.valid?
@tweet.words.each { |word| WordsTracker.hit(:tweets, word) }
redirect to("/tweets/#{@tweet.id}")
else
erb :'tweets/form'
end
end
get '/trends' do
@words = WordsTracker.top({ tweets: 3, searches: 1 }, hours: 5)
erb :'trends/index'
end
*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])
end
class Order < ActiveRecord::Base
after_create :track_sales
private
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
end
end
end
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
FAQ
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.