boffin 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .yardoc
2
+ doc
3
+ Gemfile.lock
4
+ pkg
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ -fs
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ -m markdown
2
+ --markup-provider redcarpet
3
+ --hide-void-return
4
+ --no-cache
5
+ -
6
+ README.md
7
+ CHANGELOG.md
8
+ LICENSE
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ 0.1.0
2
+
3
+ * Initial public release
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source :rubygems
2
+ gemspec
3
+
4
+ group :development do
5
+ gem 'rake'
6
+ gem 'redcarpet'
7
+ gem 'yard', git: 'https://github.com/lsegal/yard.git'
8
+ end
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