boffin 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,7 +1,15 @@
1
- 0.2.0
1
+ **0.3.0**
2
+
3
+ * `Hit` can now accept a custom increment, this allows values such as cents to
4
+ be tracked. (Justin Giancola)
5
+ * Unique qualities are now passed in as a member in an options hash for
6
+ `Tracker#hit`, this deprecates the unique qualities argument and allows for
7
+ other options such as `:increment` to be passed as well. (Justin Giancola)
8
+
9
+ **0.2.0**
2
10
 
3
11
  * Support for Ruby 1.8.7 thanks to Justin Giancola
4
12
 
5
- 0.1.0
13
+ **0.1.0**
6
14
 
7
15
  * Initial public release
data/README.md CHANGED
@@ -51,7 +51,7 @@ Most of Boffin's default configuration options are quite reasonable, but they
51
51
  are easy to change if required:
52
52
 
53
53
  ```ruby
54
- Boffin.configure do |c|
54
+ Boffin.config do |c|
55
55
  c.redis = MyApp.redis # Redis.connect by default
56
56
  c.namespace = "tracking:#{MyApp.env}" # Redis key namespace
57
57
  c.hours_window_secs = 3.days # Time to maintain hourly interval data
@@ -128,7 +128,7 @@ identify hits from particular users or sessions:
128
128
  ```ruby
129
129
  get '/listings/:id' do
130
130
  @listing = Listing[params[:id]]
131
- @listing.hit(:views, [current_user, session[:id]])
131
+ @listing.hit(:views, unique: [current_user, session[:id]])
132
132
  haml :'listings/show'
133
133
  end
134
134
  ```
@@ -144,7 +144,7 @@ we want to hit an instance, so let's create a helper:
144
144
  ```ruby
145
145
  helpers do
146
146
  def hit(trackable, type)
147
- trackable.hit(type, [current_user, session[:id]])
147
+ trackable.hit(type, unique: [current_user, session[:id]])
148
148
  end
149
149
  end
150
150
  ```
@@ -156,7 +156,7 @@ applicable to a Rails application as well:
156
156
  class ApplicationController < ActionController::Base
157
157
  protected
158
158
  def hit(trackable, type)
159
- trackable.hit(type, [current_user, session[:session_id]])
159
+ trackable.hit(type, unique: [current_user, session[:session_id]])
160
160
  end
161
161
  end
162
162
  ```
@@ -238,14 +238,14 @@ track your friends' favourite and least favourite colours:
238
238
  ```ruby
239
239
  @tracker = Boffin::Tracker.new(:colours, [:faves, :unfaves])
240
240
 
241
- @tracker.hit(:faves, 'red', ['lena'])
242
- @tracker.hit(:unfaves, 'blue', ['lena'])
243
- @tracker.hit(:faves, 'green', ['soren'])
244
- @tracker.hit(:unfaves, 'red', ['soren'])
245
- @tracker.hit(:faves, 'green', ['jens'])
246
- @tracker.hit(:unfaves, 'yellow', ['jens'])
241
+ @tracker.hit(:faves, 'red', unique: ['lena'])
242
+ @tracker.hit(:unfaves, 'blue', unique: ['lena'])
243
+ @tracker.hit(:faves, 'green', unique: ['soren'])
244
+ @tracker.hit(:unfaves, 'red', unique: ['soren'])
245
+ @tracker.hit(:faves, 'green', unique: ['jens'])
246
+ @tracker.hit(:unfaves, 'yellow', unique: ['jens'])
247
247
 
248
- @tracker.top(:faves, months: 1)
248
+ @tracker.top(:faves, days: 1)
249
249
  ```
250
250
 
251
251
  Or, perhaps you'd like to clone Twitter? Using Boffin, all the work is
@@ -263,7 +263,7 @@ end
263
263
  post '/tweets' do
264
264
  @tweet = Tweet.create(params[:tweet])
265
265
  if @tweet.valid?
266
- @tweet.words.each { WordsTracker.hit(:tweets, word) }
266
+ @tweet.words.each { |word| WordsTracker.hit(:tweets, word) }
267
267
  redirect to("/tweets/#{@tweet.id}")
268
268
  else
269
269
  haml :'tweets/form'
@@ -277,18 +277,52 @@ end
277
277
  ```
278
278
  _*This is a joke._
279
279
 
280
- TODO
281
- ----
280
+
281
+ Custom increments
282
+ -----------------
283
+
284
+ For some applications you might want to track something beyond simple hits.
285
+ To accomodate this you can specify a custom increment to any hit you record.
286
+ For example, if you run an ecommerce site it might be nice to know which
287
+ products are your bestsellers:
288
+
289
+ ```ruby
290
+ class Product < ActiveRecord::Base
291
+ Boffin.track(self, [:sales])
292
+ end
293
+
294
+ class Order < ActiveRecord::Base
295
+ after_create :track_sales
296
+
297
+ private
298
+
299
+ def track_sales
300
+ line_items.each do |line_item|
301
+ product = line_item.product
302
+ amount = product.amount.cents * line_item.quantity
303
+ product.hit :sales, increment: amount
304
+ end
305
+ end
306
+ end
307
+ ```
308
+
309
+ Then, when you want to check on your sales over the last day:
310
+
311
+ ```ruby
312
+ Product.top_ids(:sales, hours: 24, counts: true)
313
+ ```
314
+
315
+ The Future&trade;
316
+ -----------------
282
317
 
283
318
  * Ability to hit multiple instances in one command
284
319
  * Ability to get hit-count range for an instance
285
320
  * Some nice examples with pretty things
286
- * ORM adapters for niceness and tighter integration
321
+ * Maybe ORM adapters for niceness and tighter integration
287
322
  * Examples of how to turn IDs back into instances
288
323
  * Reporting DSL thingy
289
- * Web framework integration (helpers for tracking hits)
290
- * Ability to blend unique hits with raw hits
291
- * Ability to unhit an instance (if a model instance is destroyed for example)
324
+ * Web framework integration (helpers for tracking hits, console type ditty.)
325
+ * Ability to union on unique hits and raw hits
292
326
 
293
327
  FAQ
294
328
  ---
data/lib/boffin/hit.rb CHANGED
@@ -12,10 +12,21 @@ module Boffin
12
12
  # @param [Object] instance
13
13
  # The instance that is being hit, any object that responds to
14
14
  # `#to_member`, `#id`, or `#to_s`
15
- # @param [Array] uniquenesses
16
- # An array of which the first object is used to generate a session
17
- # identifier for hit uniqueness
18
- def initialize(tracker, type, instance, uniquenesses = [])
15
+ # @param [Hash] options
16
+ # @option options [Array] :unique ([]) An array of which the first
17
+ # object is used to generate a session identifier for hit uniqueness
18
+ # @option options [Fixnum] :increment (1) The hit increment
19
+ def initialize(tracker, type, instance, opts = {})
20
+ if opts.is_a?(Array)
21
+ warn "This constructor is deprecated and will soon be unavailable\n" \
22
+ "please create Hits with: \n" \
23
+ "Hit.new(@tracker, :type, @instance, :unique => [1,2,3,4])\n"
24
+ uniquenesses = opts
25
+ @increment = 1
26
+ else
27
+ uniquenesses = opts.delete(:unique) || []
28
+ @increment = opts.delete(:increment) || 1
29
+ end
19
30
  @now = Time.now
20
31
  @sessid = Utils.uniquenesses_as_session_identifier(uniquenesses)
21
32
  @type = type
@@ -54,7 +65,7 @@ module Boffin
54
65
  # `true` if this hit is unique, `false` if it has been made before by the
55
66
  # same session identifer.
56
67
  def track_hit
57
- redis.incr(keyspace.hit_count(@type, @instance))
68
+ redis.incrby(keyspace.hit_count(@type, @instance), @increment)
58
69
  redis.zincrby(keyspace.hits(@type, @instance), 1, @sessid) == '1'
59
70
  end
60
71
 
@@ -75,7 +86,7 @@ module Boffin
75
86
  # Changes keyspace scope to keys under .uniq
76
87
  def set_window_interval(interval, uniq = false)
77
88
  key = keyspace(uniq).hits_time_window(@type, interval, @now)
78
- redis.zincrby(key, 1, @member)
89
+ redis.zincrby(key, @increment, @member)
79
90
  redis.expire(key, @tracker.config.send("#{interval}_window_secs"))
80
91
  end
81
92
 
@@ -40,8 +40,8 @@ module Boffin
40
40
 
41
41
  # @see Tracker#hit
42
42
  # @return [Hit]
43
- def hit(type, uniquenesses = [])
44
- self.class.boffin.hit(type, self, uniquenesses)
43
+ def hit(type, opts = {})
44
+ self.class.boffin.hit(type, self, opts)
45
45
  end
46
46
 
47
47
  # @see Tracker#hit_count
@@ -25,14 +25,16 @@ module Boffin
25
25
 
26
26
  # @param [Symbol] hit_type
27
27
  # @param [#as_member, #id, #to_s] instance
28
- # @param [Array] uniquenesses
28
+ # @param [Hash] options
29
+ # @option options [Array] :unique ([]) uniquenesses
30
+ # @option options [Fixnum] :increment (1) hit increment
29
31
  # @return [Hit]
30
32
  # @raise Boffin::UndefinedHitTypeError
31
33
  # Raised if a list of hit types is available and the provided hit type is
32
34
  # not in the list.
33
- def hit(hit_type, instance, uniquenesses = [])
35
+ def hit(hit_type, instance, opts = {})
34
36
  validate_hit_type(hit_type)
35
- Hit.new(self, hit_type, instance, uniquenesses)
37
+ Hit.new(self, hit_type, instance, opts)
36
38
  end
37
39
 
38
40
  # @param [Symbol] hit_type
data/lib/boffin/utils.rb CHANGED
@@ -54,7 +54,11 @@ module Boffin
54
54
  # NOTE: this feels like a hack. I'm sure there is a more elegant way
55
55
  # to determine whether the :id method is the built in Object#id but
56
56
  # I can't think of it
57
- obj.respond_to?(:id) and obj.id != obj.object_id
57
+ if RUBY_VERSION < "1.9"
58
+ obj.respond_to?(:id) and obj.id != obj.object_id
59
+ else
60
+ obj.respond_to?(:id)
61
+ end
58
62
  end
59
63
 
60
64
  # Pulls time interval information from a hash of options.
@@ -1,4 +1,4 @@
1
1
  module Boffin
2
2
  # Version of this Boffin release
3
- VERSION = '0.2.0'
3
+ VERSION = '0.3.0'
4
4
  end
@@ -14,7 +14,7 @@ describe Boffin::Hit, '::new' do
14
14
  end
15
15
 
16
16
  it 'stores hit data under the appropriate keys' do
17
- Boffin::Hit.new(@tracker, :tests, @ditty, [nil, @user])
17
+ Boffin::Hit.new(@tracker, :tests, @ditty, :unique => [nil, @user])
18
18
  [:hours, :days, :months].each do |interval|
19
19
  @tracker.top(:tests, interval => 1, :counts => true).
20
20
  should == [['1', 1]]
@@ -26,8 +26,8 @@ describe Boffin::Hit, '::new' do
26
26
  end
27
27
 
28
28
  it 'does not store data under unique keys if the hit is not unique' do
29
- Boffin::Hit.new(@tracker, :tests, @ditty, [nil, @user])
30
- Boffin::Hit.new(@tracker, :tests, @ditty, [nil, @user])
29
+ Boffin::Hit.new(@tracker, :tests, @ditty, :unique => [nil, @user])
30
+ Boffin::Hit.new(@tracker, :tests, @ditty, :unique => [nil, @user])
31
31
  [:hours, :days, :months].each do |interval|
32
32
  @tracker.top(:tests, interval => 1, :counts => true).
33
33
  should == [['1', 2]]
@@ -39,4 +39,14 @@ describe Boffin::Hit, '::new' do
39
39
  @tracker.uhit_count(:tests, @ditty).should == 1
40
40
  end
41
41
 
42
+ it 'allows arbitrary hit increments' do
43
+ Boffin::Hit.new(@tracker, :tests, @ditty, :unique => [nil, @user], :increment => 5)
44
+ Boffin::Hit.new(@tracker, :tests, @ditty, :unique => [nil, @user], :increment => 5)
45
+ [:hours, :days, :months].each do |interval|
46
+ @tracker.top(:tests, interval => 1, :counts => true).
47
+ should == [['1', 10]]
48
+ @tracker.top(:tests, interval => 1, :counts => true, :unique => true).
49
+ should == [['1', 5]]
50
+ end
51
+ end
42
52
  end
@@ -4,8 +4,8 @@ describe Boffin::Trackable do
4
4
  before :all do
5
5
  SpecHelper.flush_keyspace!
6
6
  @mock = MockTrackableInjected.new(1)
7
- @mock.hit(:views, ['sess.1'])
8
- @mock.hit(:views, ['sess.1'])
7
+ @mock.hit(:views, :unique => ['sess.1'])
8
+ @mock.hit(:views, :unique => ['sess.1'])
9
9
  end
10
10
 
11
11
  it 'can be included' do
@@ -14,43 +14,43 @@ describe Boffin::Tracker do
14
14
 
15
15
  Timecop.freeze(@date - 2) do
16
16
  @tracker.hit(:views, @instance3)
17
- @tracker.hit(:likes, @instance3, [@user1])
18
- @tracker.hit(:views, @instance3, ['sess.1'])
19
- @tracker.hit(:views, @instance3, ['sess.2'])
20
- @tracker.hit(:views, @instance3, [@user2])
21
- @tracker.hit(:likes, @instance1, [nil, nil])
22
- @tracker.hit(:views, @instance3, ['sess.4'])
23
- @tracker.hit(:views, @instance3, [@user1])
24
- @tracker.hit(:views, @instance2, [@user2])
25
- @tracker.hit(:views, @instance3, [@user2])
17
+ @tracker.hit(:likes, @instance3, :unique => [@user1])
18
+ @tracker.hit(:views, @instance3, :unique => ['sess.1'])
19
+ @tracker.hit(:views, @instance3, :unique => ['sess.2'])
20
+ @tracker.hit(:views, @instance3, :unique => [@user2])
21
+ @tracker.hit(:likes, @instance1, :unique => [nil, nil])
22
+ @tracker.hit(:views, @instance3, :unique => ['sess.4'])
23
+ @tracker.hit(:views, @instance3, :unique => [@user1])
24
+ @tracker.hit(:views, @instance2, :unique => [@user2])
25
+ @tracker.hit(:views, @instance3, :unique => [@user2])
26
26
  @tracker.hit(:likes, @instance3)
27
- @tracker.hit(:views, @instance3, ['sess.1'])
27
+ @tracker.hit(:views, @instance3, :unique => ['sess.1'])
28
28
  @tracker.hit(:views, @instance3)
29
- @tracker.hit(:likes, @instance3, [@user1])
30
- @tracker.hit(:views, @instance3, ['sess.1'])
29
+ @tracker.hit(:likes, @instance3, :unique => [@user1])
30
+ @tracker.hit(:views, @instance3, :unique => ['sess.1'], :increment => 2)
31
31
  end
32
32
 
33
33
  Timecop.freeze(@date - 1) do
34
34
  @tracker.hit(:views, @instance1)
35
- @tracker.hit(:likes, @instance2, [@user1])
36
- @tracker.hit(:views, @instance2, ['sess.4'])
37
- @tracker.hit(:views, @instance2, [nil, @user1])
38
- @tracker.hit(:views, @instance2, ['sess.3'])
39
- @tracker.hit(:views, @instance1, ['sess.3'])
40
- @tracker.hit(:views, @instance1, [@user1])
41
- @tracker.hit(:views, @instance2, ['sess.2'])
42
- @tracker.hit(:views, @instance1, [@user1])
43
- @tracker.hit(:views, @instance1, [@user2])
44
- end
45
-
46
- @tracker.hit(:views, @instance3, ['sess.2'])
47
- @tracker.hit(:views, @instance2, [@user2])
35
+ @tracker.hit(:likes, @instance2, :unique => [@user1])
36
+ @tracker.hit(:views, @instance2, :unique => ['sess.4'])
37
+ @tracker.hit(:views, @instance2, :unique => [nil, @user1])
38
+ @tracker.hit(:views, @instance2, :unique => ['sess.3'])
39
+ @tracker.hit(:views, @instance1, :unique => ['sess.3'])
40
+ @tracker.hit(:views, @instance1, :unique => [@user1])
41
+ @tracker.hit(:views, @instance2, :unique => ['sess.2'])
42
+ @tracker.hit(:views, @instance1, :unique => [@user1])
43
+ @tracker.hit(:views, @instance1, :unique => [@user2])
44
+ end
45
+
46
+ @tracker.hit(:views, @instance3, :unique => ['sess.2'])
47
+ @tracker.hit(:views, @instance2, :unique => [@user2])
48
48
  @tracker.hit(:likes, @instance2)
49
- @tracker.hit(:views, @instance2, [@user1])
50
- @tracker.hit(:views, @instance1, ['sess.4'])
51
- @tracker.hit(:views, @instance3, ['sess.3'])
52
- @tracker.hit(:views, @instance1, [@user1])
53
- @tracker.hit(:views, @instance1, [@user2])
49
+ @tracker.hit(:views, @instance2, :unique => [@user1])
50
+ @tracker.hit(:views, @instance1, :unique => ['sess.4'])
51
+ @tracker.hit(:views, @instance3, :unique => ['sess.3'])
52
+ @tracker.hit(:views, @instance1, :unique => [@user1])
53
+ @tracker.hit(:views, @instance1, :unique => [@user2])
54
54
  end
55
55
 
56
56
  describe '#hit' do
@@ -134,7 +134,7 @@ describe Boffin::Tracker do
134
134
  it 'returns ids and counts when passed { counts: true } as an option' do
135
135
  ids = @tracker.top(:views, :days => 3, :counts => true)
136
136
  ids.should == [
137
- ['300', 12],
137
+ ['300', 13],
138
138
  ['100', 8],
139
139
  ['200', 7]
140
140
  ]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: boffin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-09-08 00:00:00.000000000Z
12
+ date: 2011-09-12 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
16
- requirement: &70347564151120 !ruby/object:Gem::Requirement
16
+ requirement: &70276273568860 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '2.2'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70347564151120
24
+ version_requirements: *70276273568860
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &70347564150300 !ruby/object:Gem::Requirement
27
+ requirement: &70276273568280 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '2.6'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70347564150300
35
+ version_requirements: *70276273568280
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: timecop
38
- requirement: &70347564149720 !ruby/object:Gem::Requirement
38
+ requirement: &70276273567900 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70347564149720
46
+ version_requirements: *70276273567900
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bundler
49
- requirement: &70347564149100 !ruby/object:Gem::Requirement
49
+ requirement: &70276273567340 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,7 +54,7 @@ dependencies:
54
54
  version: 1.0.14
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70347564149100
57
+ version_requirements: *70276273567340
58
58
  description: ! 'Boffin is a library for tracking hits to things in your Ruby application.
59
59
  Things
60
60
 
@@ -111,7 +111,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
111
111
  version: '0'
112
112
  segments:
113
113
  - 0
114
- hash: 3845958252376638612
114
+ hash: -2879744131493119363
115
115
  required_rubygems_version: !ruby/object:Gem::Requirement
116
116
  none: false
117
117
  requirements:
@@ -120,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
120
  version: '0'
121
121
  segments:
122
122
  - 0
123
- hash: 3845958252376638612
123
+ hash: -2879744131493119363
124
124
  requirements: []
125
125
  rubyforge_project: boffin
126
126
  rubygems_version: 1.8.6