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 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