boffin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +8 -0
- data/LICENSE +18 -0
- data/README.md +302 -0
- data/Rakefile +14 -0
- data/boffin.gemspec +29 -0
- data/lib/boffin.rb +87 -0
- data/lib/boffin/config.rb +91 -0
- data/lib/boffin/hit.rb +83 -0
- data/lib/boffin/keyspace.rb +147 -0
- data/lib/boffin/trackable.rb +66 -0
- data/lib/boffin/tracker.rb +229 -0
- data/lib/boffin/utils.rb +171 -0
- data/lib/boffin/version.rb +4 -0
- data/spec/boffin/config_spec.rb +65 -0
- data/spec/boffin/hit_spec.rb +42 -0
- data/spec/boffin/keyspace_spec.rb +85 -0
- data/spec/boffin/trackable_spec.rb +38 -0
- data/spec/boffin/tracker_spec.rb +162 -0
- data/spec/boffin/utils_spec.rb +158 -0
- data/spec/boffin_spec.rb +44 -0
- data/spec/spec_helper.rb +50 -0
- metadata +128 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (C) 2011 Carsten Nielsen
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,302 @@
|
|
1
|
+
Boffin
|
2
|
+
======
|
3
|
+
|
4
|
+
Hit tracking library for Ruby using [Redis](http://redis.io)
|
5
|
+
|
6
|
+
About
|
7
|
+
-----
|
8
|
+
|
9
|
+
Boffin is a library for tracking hits to things in your Ruby application. Things
|
10
|
+
can be IDs of records in a database, strings representing tags or topics, URLs
|
11
|
+
of webpages, names of places, whatever you desire. Boffin is able to provide
|
12
|
+
lists of those things based on most hits, least hits, it can even report on
|
13
|
+
weighted combinations of different types of hits.
|
14
|
+
|
15
|
+
Resources
|
16
|
+
---------
|
17
|
+
|
18
|
+
* [Source Code](https://github.com/heycarsten/boffin)
|
19
|
+
* [Issue Tracker](https://github.com/heycarsten/boffin/issues)
|
20
|
+
* [Test Suite](https://github.com/heycarsten/boffin/tree/master/spec)
|
21
|
+
* [License](https://github.com/heycarsten/boffin/blob/master/LICENSE)
|
22
|
+
|
23
|
+
Getting started
|
24
|
+
---------------
|
25
|
+
|
26
|
+
You need a functioning [Redis](http://redis.io) installation. Once Redis is
|
27
|
+
installed you can start it by running `redis-server`, this will run Redis in the
|
28
|
+
foreground.
|
29
|
+
|
30
|
+
You can use Boffin in many different contexts, but the most common one is
|
31
|
+
probably that of a Rails or Sinatra application. Just add `boffin` to your
|
32
|
+
[Gemfile](http://gembundler.com):
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
gem 'boffin'
|
36
|
+
```
|
37
|
+
|
38
|
+
For utmost performance on *nix-based systems, require
|
39
|
+
[hiredis](https://github.com/pietern/hiredis-rb) before you require Boffin:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
gem 'hiredis'
|
43
|
+
gem 'boffin'
|
44
|
+
```
|
45
|
+
|
46
|
+
Configuration
|
47
|
+
-------------
|
48
|
+
|
49
|
+
Most of Boffin's default configuration options are quite reasonable, but they
|
50
|
+
are easy to change if required:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
Boffin.configure do |c|
|
54
|
+
c.redis = MyApp.redis # Redis.connect by default
|
55
|
+
c.namespace = "tracking:#{MyApp.env}" # Redis key namespace
|
56
|
+
c.hours_window_secs = 3.days # Time to maintain hourly interval data
|
57
|
+
c.days_window_secs = 3.months # Time to maintain daily interval data
|
58
|
+
c.months_window_secs = 3.years # Time to maintain monthly interval data
|
59
|
+
c.cache_expire_secs = 15.minutes # Time to cache Tracker#top result sets
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
Tracking
|
64
|
+
--------
|
65
|
+
|
66
|
+
A Tracker is responsible for maintaining a namespace for hits. For our examples
|
67
|
+
we will have a model called `Listing` it represents a listing in our realty
|
68
|
+
web app. We want to track when someone likes, shares, or views a listing.
|
69
|
+
|
70
|
+
Our example web app uses [Sinatra](http://sinatrarb.com) as its framework, and
|
71
|
+
[Sequel](http://sequel.rubyforge.org)::Model as its ORM. It's important to note
|
72
|
+
that Boffin has no requirements on any of these things, it can be used to track
|
73
|
+
any Ruby class in any environment.
|
74
|
+
|
75
|
+
Start by telling Boffin to make the Listing model trackable:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
Boffin.track(Listing)
|
79
|
+
```
|
80
|
+
|
81
|
+
**_or_**
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
class Listing < Sequel::Model
|
85
|
+
include Boffin::Trackable
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
You can optionally specify the types of hits that are acceptable, this is good
|
90
|
+
practice and will save frustrating moments where you accidentally type `:view`
|
91
|
+
instead of `:views`, to do that:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
Boffin.track(Listing, [:likes, :shares, :views])
|
95
|
+
```
|
96
|
+
|
97
|
+
**_or_**
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class Listing < Sequel::Model
|
101
|
+
include Boffin::Trackable
|
102
|
+
boffin.hit_types = [:likes, :shares, :views]
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
**_or_**
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class Listing < Sequel::Model
|
110
|
+
Boffin.track(self, [:likes, :shares, :views])
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
Now to track hits on instances of the Listing model, simply:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
get '/listings/:id' do
|
118
|
+
@listing = Listing[params[:id]]
|
119
|
+
@listing.hit(:views)
|
120
|
+
haml :'listings/show'
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
However you will probably want to provide Boffin with some uniqueness to
|
125
|
+
identify hits from particular users or sessions:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
get '/listings/:id' do
|
129
|
+
@listing = Listing[params[:id]]
|
130
|
+
@listing.hit(:views, [current_user, session[:id]])
|
131
|
+
haml :'listings/show'
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
Boffin now adds uniqueness to the hit in the form of `current_user.id` if
|
136
|
+
available. If `current_user` is nil, Boffin then uses `session[:id]`. You can
|
137
|
+
provide as many uniquenesses as you'd like, the first one that is not blank
|
138
|
+
(`nil`, `false`, `[]`, `{}`, or `''`) will be used.
|
139
|
+
|
140
|
+
It could get a bit tedious having to add `[current_user, session[:id]]` whenever
|
141
|
+
we want to hit an instance, so let's create a helper:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
helpers do
|
145
|
+
def hit(trackable, type)
|
146
|
+
trackable.hit(type, [current_user, session[:id]])
|
147
|
+
end
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
For these examples we are in the context of a Sinatra application, but this is
|
152
|
+
applicable to a Rails application as well:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
class ApplicationController < ActionController::Base
|
156
|
+
protected
|
157
|
+
def hit(trackable, type)
|
158
|
+
trackable.hit(type, [current_user, session[:session_id]])
|
159
|
+
end
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
You get the idea, now storing a hit is as easy as:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
get '/listings/:id' do
|
167
|
+
@listing = Listing[params[:id]]
|
168
|
+
hit @listing, :views
|
169
|
+
haml :'listings/show'
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
Reporting
|
174
|
+
---------
|
175
|
+
|
176
|
+
After some hits have been tracked, you can start to do some queries:
|
177
|
+
|
178
|
+
**Get a count of all views for an instance**
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
@listing.hit_count(:views)
|
182
|
+
```
|
183
|
+
|
184
|
+
**Get count of unique views for an instance**
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
@listing.uhit_count(:views)
|
188
|
+
```
|
189
|
+
|
190
|
+
**Get IDs of the most viewed listings in the past 5 days**
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
Listing.top_ids(:views, days: 5)
|
194
|
+
```
|
195
|
+
|
196
|
+
**Get IDs of the least viewed listings (that were viewed) in the past 8 hours**
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
Listing.top_ids(:views, hours: 8, order: 'asc')
|
200
|
+
```
|
201
|
+
|
202
|
+
**Get IDs and hit counts of the most liked listings in the past 5 days**
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
Listing.top_ids(:likes, days: 5, counts: true)
|
206
|
+
```
|
207
|
+
|
208
|
+
**Get IDs of the most liked, viewed, and shared listings with likes weighted
|
209
|
+
higher than views in the past 12 hours**
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
Listing.top_ids({ likes: 2, views: 1, shares: 3 }, hours: 12)
|
213
|
+
```
|
214
|
+
|
215
|
+
**Get IDs and combined/weighted scores of the most liked, and viewed listings in
|
216
|
+
the past 7 days**
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
Listing.top_ids({ likes: 2, views: 1 }, hours: 12, counts: true)
|
220
|
+
```
|
221
|
+
|
222
|
+
Boffin records hits in time intervals: hours, days, and months. Each interval
|
223
|
+
has a window of time that it is available before it expires; these windows are
|
224
|
+
configurable. It's also important to note that the results returned by these
|
225
|
+
methods are cached for the duration of `Boffin.config.cache_expire_secs`. See
|
226
|
+
**Configuration** above.
|
227
|
+
|
228
|
+
More
|
229
|
+
====
|
230
|
+
|
231
|
+
Not just for models
|
232
|
+
-------------------
|
233
|
+
|
234
|
+
As stated before, you can use Boffin to track anything. Maybe you'd like to
|
235
|
+
track your friends' favourite and least favourite colours:
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
@tracker = Boffin::Tracker.new(:colours, [:faves, :unfaves])
|
239
|
+
|
240
|
+
@tracker.hit(:faves, 'red', ['lena'])
|
241
|
+
@tracker.hit(:unfaves, 'blue', ['lena'])
|
242
|
+
@tracker.hit(:faves, 'green', ['soren'])
|
243
|
+
@tracker.hit(:unfaves, 'red', ['soren'])
|
244
|
+
@tracker.hit(:faves, 'green', ['jens'])
|
245
|
+
@tracker.hit(:unfaves, 'yellow', ['jens'])
|
246
|
+
|
247
|
+
@tracker.top(:faves, months: 1)
|
248
|
+
```
|
249
|
+
|
250
|
+
Or, perhaps you'd like to clone Twitter? Using Boffin, all the work is
|
251
|
+
essentially done for you*:
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
WordsTracker = Boffin::Tracker.new(:words, [:searches, :tweets])
|
255
|
+
|
256
|
+
get '/search' do
|
257
|
+
@tweets = Tweet.search(params[:q])
|
258
|
+
params[:q].split.each { |word| WordsTracker.hit(:searches, word) }
|
259
|
+
haml :'search/show'
|
260
|
+
end
|
261
|
+
|
262
|
+
post '/tweets' do
|
263
|
+
@tweet = Tweet.create(params[:tweet])
|
264
|
+
if @tweet.valid?
|
265
|
+
@tweet.words.each { WordsTracker.hit(:tweets, word) }
|
266
|
+
redirect to("/tweets/#{@tweet.id}")
|
267
|
+
else
|
268
|
+
haml :'tweets/form'
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
get '/trends' do
|
273
|
+
@words = WordsTracker.top({ tweets: 3, searches: 1 }, hours: 5)
|
274
|
+
haml :'trends/index'
|
275
|
+
end
|
276
|
+
```
|
277
|
+
_*This is a joke._
|
278
|
+
|
279
|
+
TODO
|
280
|
+
----
|
281
|
+
|
282
|
+
* Ability to hit multiple instances in one command
|
283
|
+
* Ability to get hit-count range for an instance
|
284
|
+
* Some nice examples with pretty things
|
285
|
+
* ORM adapters for niceness and tighter integration
|
286
|
+
* Examples of how to turn IDs back into instances
|
287
|
+
* Reporting DSL thingy
|
288
|
+
* Web framework integration (helpers for tracking hits)
|
289
|
+
* Ability to blend unique hits with raw hits
|
290
|
+
* Ability to unhit an instance (if a model instance is destroyed for example)
|
291
|
+
|
292
|
+
FAQ
|
293
|
+
---
|
294
|
+
|
295
|
+
### What's with the name?
|
296
|
+
|
297
|
+
Well, it means [this](http://en.wikipedia.org/wiki/Boffin). For the purposes of
|
298
|
+
this project, its use is very tongue-in-cheek.
|
299
|
+
|
300
|
+
### Are you British?
|
301
|
+
|
302
|
+
No, I'm just weird, but [this guy](http://github.com/aanand) is a real British person.
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
require 'rake'
|
4
|
+
|
5
|
+
require 'rspec/core'
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
8
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'yard'
|
12
|
+
YARD::Rake::YardocTask.new
|
13
|
+
|
14
|
+
task default: :spec
|
data/boffin.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path('../lib/boffin/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'boffin'
|
5
|
+
s.version = Boffin::VERSION
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.date = Date.today.strftime('%F')
|
8
|
+
s.homepage = 'http://github.com/heycarsten/boffin'
|
9
|
+
s.author = 'Carsten Nielsen'
|
10
|
+
s.email = 'heycarsten@gmail.com'
|
11
|
+
s.summary = 'Hit tracking library for Ruby using Redis'
|
12
|
+
s.has_rdoc = 'yard'
|
13
|
+
s.rubyforge_project = 'boffin'
|
14
|
+
s.files = `git ls-files`.split(?\n)
|
15
|
+
s.test_files = `git ls-files -- spec/*`.split(?\n)
|
16
|
+
s.require_paths = ['lib']
|
17
|
+
|
18
|
+
s.add_dependency 'redis', '>= 2.2'
|
19
|
+
s.add_development_dependency 'rspec', '~> 2.6'
|
20
|
+
s.add_development_dependency 'timecop'
|
21
|
+
|
22
|
+
s.description = <<-END
|
23
|
+
Boffin is a library for tracking hits to things in your Ruby application. Things
|
24
|
+
can be IDs of records in a database, strings representing tags or topics, URLs
|
25
|
+
of webpages, names of places, whatever you desire. Boffin is able to provide
|
26
|
+
lists of those things based on most hits, least hits, it can even report on
|
27
|
+
weighted combinations of different types of hits.
|
28
|
+
END
|
29
|
+
end
|
data/lib/boffin.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'date'
|
3
|
+
require 'time'
|
4
|
+
require 'redis'
|
5
|
+
require 'boffin/version'
|
6
|
+
require 'boffin/utils'
|
7
|
+
require 'boffin/config'
|
8
|
+
require 'boffin/keyspace'
|
9
|
+
require 'boffin/tracker'
|
10
|
+
require 'boffin/hit'
|
11
|
+
require 'boffin/trackable'
|
12
|
+
|
13
|
+
# Boffin is a library for tracking hits to things in your Ruby application.
|
14
|
+
# Things can be IDs of records in a database, strings representing tags or
|
15
|
+
# topics, URLs of webpages, names of places, whatever you desire. Boffin is able
|
16
|
+
# to provide lists of those things based on most hits, least hits, it can even
|
17
|
+
# report on weighted combinations of different types of hits.
|
18
|
+
#
|
19
|
+
# Refer to the {file:README} for further information and examples.
|
20
|
+
module Boffin
|
21
|
+
# The member to use when no session identifier is available for unique hit
|
22
|
+
# tracking
|
23
|
+
NIL_SESSION_MEMBER = 'boffin:nilsession'
|
24
|
+
|
25
|
+
# The way Time should be formatted for each interval type
|
26
|
+
INTERVAL_FORMATS = {
|
27
|
+
hours: '%F-%H',
|
28
|
+
days: '%F',
|
29
|
+
months: '%Y-%m' }
|
30
|
+
|
31
|
+
# Different interval types
|
32
|
+
INTERVAL_TYPES = INTERVAL_FORMATS.keys
|
33
|
+
|
34
|
+
# Raised by Tracker when hit types are passed to it that are not included in
|
35
|
+
# its list of valid hit types.
|
36
|
+
class UndefinedHitTypeError < StandardError; end
|
37
|
+
|
38
|
+
# Set or get the default Config instance
|
39
|
+
# @param [Hash] opts
|
40
|
+
# (see {Config#initialize})
|
41
|
+
# @return [Config]
|
42
|
+
# the default Config instance
|
43
|
+
# @yield [Config.new]
|
44
|
+
# Passes the block to {Config#initialize}
|
45
|
+
# @example Getting the default config instance
|
46
|
+
# Boffin.config
|
47
|
+
# @example Setting the default config instance with a block
|
48
|
+
# Boffin.config do |conf|
|
49
|
+
# conf.namespace = 'something:special'
|
50
|
+
# end
|
51
|
+
# @example Setting the default config instance with a Hash
|
52
|
+
# Boffin.config(namespace: 'something:cool')
|
53
|
+
def self.config(opts = {}, &block)
|
54
|
+
@config ||= Config.new(opts, &block)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Creates a new Tracker instance. If passed a class, Trackable is injected
|
58
|
+
# into it.
|
59
|
+
# @param [Class, Symbol] class_or_ns
|
60
|
+
# A class or symbol to use as a namespace for the Tracker
|
61
|
+
# @param [optional Array <Symbol>] hit_types
|
62
|
+
# A list of valid hit types for the Tracker
|
63
|
+
# @return [Tracker]
|
64
|
+
# @example Tracking an ActiveRecord model
|
65
|
+
# Boffin.track(MyModel, [:views, :likes])
|
66
|
+
#
|
67
|
+
# # This does the same thing:
|
68
|
+
# class MyModel
|
69
|
+
# include Boffin::Tracker
|
70
|
+
# boffin.hit_types = [:views, :likes]
|
71
|
+
# end
|
72
|
+
# @example Creating a tracker without fancy injecting-ness
|
73
|
+
# ThingsTracker = Boffin.track(:things)
|
74
|
+
#
|
75
|
+
# # This does the same thing:
|
76
|
+
# ThingsTracker = Boffin::Tracker.new(:things)
|
77
|
+
def self.track(class_or_ns, hit_types = [])
|
78
|
+
case class_or_ns
|
79
|
+
when String, Symbol
|
80
|
+
Tracker.new(class_or_ns, hit_types)
|
81
|
+
else
|
82
|
+
class_or_ns.send(:include, Trackable)
|
83
|
+
class_or_ns.boffin.hit_types = hit_types
|
84
|
+
class_or_ns.boffin
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|