retained 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2f5d6bcae30957db9c68732ac86d8ecaab0b87b8
4
- data.tar.gz: 7f9cd05317be943e80db30d0400f2e846c3953c9
3
+ metadata.gz: c5889b1f2f5d17776a5955402abf0bb72a628c1b
4
+ data.tar.gz: d0e5963b0989d0bdf257b4f6b1be551538dec580
5
5
  SHA512:
6
- metadata.gz: 5d4b7f865769cf26c8cf23f6863d9fc297319d02a95e2fba2e9012b4189e73a7440ea7b259da38f30f3d42348735b4f8880f6ad5dab2a9fee9fcc03e3de0518c
7
- data.tar.gz: da5f936e390f57a603f216c38dac7fb488b5cb81ecb46056b20b8500aea8a85edc90f0869ac8bd13693187184c2c9938360afa631a11a26c3102269eb4e6bba2
6
+ metadata.gz: 62d5f2c52a6286581a4924a3f23df5918450345e40088e3c0d012b24a9a53d3c505cd0199d0c93fb4a590237ad6e80f42b64a3821844defed9f81d83cb59d703
7
+ data.tar.gz: 9279624e9cc42bf400675110ddecddf97cba3c8436880abe5e17e571a7a6b0a9f728b02bd2611cd632ce55184cbdf983320a94c72063d03e1920a41ed7f50819
@@ -1,3 +1,9 @@
1
+ # 0.2.2
2
+
3
+ * Fixed default group. Was erroneously using an empty string
4
+ instead of the string 'default'.
5
+ * Added unique_active method
6
+
1
7
  # 0.2.1
2
8
 
3
9
  * Fixed ActiveSupport require declaration
data/Gemfile CHANGED
@@ -1,7 +1,7 @@
1
1
  source 'http://rubygems.org'
2
2
 
3
3
  gem 'redis', '~> 3.1.0'
4
- gem 'redis-bitops', '~> 0.2.1'
4
+ gem 'redis-bitops', github: 'msaffitz/redis-bitops', branch: 'watch_keys_fix'
5
5
  gem 'activesupport'
6
6
 
7
7
  group :development do
@@ -1,3 +1,11 @@
1
+ GIT
2
+ remote: git://github.com/msaffitz/redis-bitops.git
3
+ revision: a82f1bdc809e843ff297bcaf4c7664d3fc461772
4
+ branch: watch_keys_fix
5
+ specs:
6
+ redis-bitops (0.2.1)
7
+ redis
8
+
1
9
  GEM
2
10
  remote: http://rubygems.org/
3
11
  specs:
@@ -55,8 +63,6 @@ GEM
55
63
  rdoc (3.12.2)
56
64
  json (~> 1.4)
57
65
  redis (3.1.0)
58
- redis-bitops (0.2.1)
59
- redis
60
66
  simplecov (0.9.0)
61
67
  docile (~> 1.1.0)
62
68
  multi_json
@@ -78,7 +84,7 @@ DEPENDENCIES
78
84
  minitest
79
85
  rdoc (~> 3.12)
80
86
  redis (~> 3.1.0)
81
- redis-bitops (~> 0.2.1)
87
+ redis-bitops!
82
88
  simplecov
83
89
  timecop (~> 0.7.1)
84
90
  yard (~> 0.7)
data/README.md CHANGED
@@ -12,15 +12,15 @@ Retained makes it easy to track activity and retention at scale in daily, hourly
12
12
 
13
13
  Use RubyGems to install Retained:
14
14
 
15
- $ gem install retained
16
-
15
+ $ gem install retained
16
+
17
17
  If you are using Bundler, you can also install it by adding it to your Gemfile:
18
18
 
19
- gem 'retained'
20
-
19
+ gem 'retained'
20
+
21
21
  Run `bundle install` to install.
22
22
 
23
- ## Basic Usage
23
+ ## Basic Usage
24
24
 
25
25
  Retained works by tracking whether an **entity** was active within a reporting interval for a specific **group**. Entities can be anything you wish to track activity for, and are identified by a unique identifier that you provide to Retained. Groups provide a scope for the entities activity. For example, groups allow you to track activity by a User (entity) for a specific feature of your product. Like entities, groups are identified by a unique identifier that you provide to Retained.
26
26
 
@@ -32,19 +32,23 @@ Retained's default settings are to connect to Redis at `redis://localhost:6379`,
32
32
 
33
33
  To track an entity with an id of `entity_id` as active for the current day:
34
34
 
35
- Retained.retain('entity_id')
35
+ Retained.retain('entity_id')
36
36
 
37
37
  To track an entity with an id of `entity_id` as active on another day:
38
38
 
39
- Retained.retain('entity_id', period: Time.new(2013,10,1))
40
-
39
+ Retained.retain('entity_id', period: Time.new(2013,10,1))
40
+
41
41
  To query whether an entity was active on a given day:
42
42
 
43
- Retained.active?('entity_id', period: Time.new(2014,3,21))
43
+ Retained.active?('entity_id', period: Time.new(2014,3,21))
44
44
 
45
45
  To determine the total number of active entities on a given day:
46
46
 
47
- Retained.total_active(period: Time.new(2014,2,10))
47
+ Retained.total_active(period: Time.new(2014,2,10))
48
+
49
+ To determine the total number of unique entities over a range:
50
+
51
+ Retained.unique_active(start: Time.new(2014,2,10), stop: Time.new(2014,2,13)
48
52
 
49
53
  ## Advanced Usage
50
54
 
@@ -52,38 +56,38 @@ To determine the total number of active entities on a given day:
52
56
 
53
57
  Retained can be configured with an alternate Redis connection string as well as an alternate prefix for keys:
54
58
 
55
- Retained.configure do |config|
56
- config.redis = 'redis://example.org:6379'
57
- config.prefix = 'my_prefix',
58
- end
59
-
59
+ Retained.configure do |config|
60
+ config.redis = 'redis://example.org:6379'
61
+ config.prefix = 'my_prefix',
62
+ end
63
+
60
64
  You can also configure groups to use an alternate reporting interval-- either daily, hourly, or by minute.
61
65
 
62
66
  **Important!** Once set, a group's reporting interval **cannot** be changed.
63
67
 
64
68
  For example, to configure a 'generated_report' group to use an hourly interval:
65
69
 
66
- Retained.configure do |config|
67
- config.group('generated_report).do |group|
68
- group.reporting_interval = :hour
69
- end
70
- end
71
-
70
+ Retained.configure do |config|
71
+ config.group('generated_report).do |group|
72
+ group.reporting_interval = :hour
73
+ end
74
+ end
75
+
72
76
  If you need to have more control over the Redis connection (i.e. to use Sentinel), you can directly provide an already established connection:
73
77
 
74
- Retained.configure do |config|
75
- config.redis_connection = Redis.new
76
- end
78
+ Retained.configure do |config|
79
+ config.redis_connection = Redis.new
80
+ end
77
81
 
78
82
  ### Non-Singleton Use
79
83
 
80
84
  You can connect to multiple backends for Retained by instantiating a Retained tracker directly:
81
85
 
82
- tracker = Retained::Tracker.new
83
- tracker.configure do |config|
84
- config.redis = 'redis://other.example.org:6379'
85
- end
86
- tracker.retain('entity_id)
86
+ tracker = Retained::Tracker.new
87
+ tracker.configure do |config|
88
+ config.redis = 'redis://other.example.org:6379'
89
+ end
90
+ tracker.retain('entity_id)
87
91
 
88
92
  ## Contributing to retained
89
93
 
@@ -16,21 +16,34 @@ module Retained
16
16
 
17
17
  # Tracks the entity as active at the period, or now if no period
18
18
  # is provided.
19
- def retain(entity, group: group, period: Time.now)
19
+ def retain(entity, group: 'default', period: Time.now)
20
20
  index = entity_index(entity, group)
21
21
  bitmap = config.redis_connection.sparse_bitmap(key_period(group, period))
22
22
  bitmap[index] = true
23
23
  end
24
24
 
25
- # Total active entities in the period, or now if now period,
25
+ # Total active entities in the period, or now if no period,
26
26
  # is provided.
27
- def total_active(group: group, period: Time.now)
27
+ def total_active(group: 'default', period: Time.now)
28
28
  bitmap = config.redis_connection.sparse_bitmap(key_period(group, period))
29
29
  bitmap.bitcount
30
30
  end
31
31
 
32
+ # Returns the total number of unique active entities between
33
+ # the start and end periods (inclusive), or now if no stop
34
+ # period is provided.
35
+ def unique_active(group: 'default', start:, stop: Time.now)
36
+ bitmaps = []
37
+ while ( start <= stop)
38
+ bitmaps << config.redis_connection.sparse_bitmap(key_period(group, start))
39
+ start += seconds_in_reporting_interval(config.group(group).reporting_interval)
40
+ end
41
+ return 0 if bitmaps.length == 0
42
+ bitmaps.inject { |uniques,bitmap| uniques | bitmap }.bitcount
43
+ end
44
+
32
45
  # Returns true if the entity was active in the given period,
33
- # or now if now period is provided. If a group or an array of groups
46
+ # or now if no period is provided. If a group or an array of groups
34
47
  # is provided activity will only be considered based on those groups.
35
48
  def active?(entity, group: nil, period: Time.now)
36
49
  group = [group] if group.is_a?(String)
@@ -73,5 +86,15 @@ LUA
73
86
  period = period.utc.send("beginning_of_#{config.group(group).reporting_interval}")
74
87
  "#{config.prefix}:#{group}:#{period.to_i}"
75
88
  end
89
+
90
+ private
91
+ def seconds_in_reporting_interval(interval)
92
+ case(interval.to_sym)
93
+ when :day then 60*60*24
94
+ when :hour then 60*60
95
+ when :minute then 60
96
+ else fail "Unknown reporting interval: #{interval}"
97
+ end
98
+ end
76
99
  end
77
100
  end
@@ -2,7 +2,7 @@ module Retained
2
2
  module Version
3
3
  MAJOR = 0
4
4
  MINOR = 2
5
- PATCH = 1
5
+ PATCH = 2
6
6
  BUILD = nil
7
7
 
8
8
  STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: retained 0.2.1 ruby lib
5
+ # stub: retained 0.2.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "retained"
9
- s.version = "0.2.1"
9
+ s.version = "0.2.2"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib"]
13
13
  s.authors = ["Mike Saffitz"]
14
- s.date = "2014-10-13"
14
+ s.date = "2014-11-07"
15
15
  s.description = "\n Easy tracking of activity and retention at scale in daily, hourly, or minute intervals\n using sparse Redis bitmaps.\n "
16
16
  s.email = "m@saffitz.com"
17
17
  s.extra_rdoc_files = [
@@ -48,7 +48,7 @@ Gem::Specification.new do |s|
48
48
 
49
49
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
50
50
  s.add_runtime_dependency(%q<redis>, ["~> 3.1.0"])
51
- s.add_runtime_dependency(%q<redis-bitops>, ["~> 0.2.1"])
51
+ s.add_runtime_dependency(%q<redis-bitops>, [">= 0"])
52
52
  s.add_runtime_dependency(%q<activesupport>, [">= 0"])
53
53
  s.add_development_dependency(%q<minitest>, [">= 0"])
54
54
  s.add_development_dependency(%q<yard>, ["~> 0.7"])
@@ -59,7 +59,7 @@ Gem::Specification.new do |s|
59
59
  s.add_development_dependency(%q<timecop>, ["~> 0.7.1"])
60
60
  else
61
61
  s.add_dependency(%q<redis>, ["~> 3.1.0"])
62
- s.add_dependency(%q<redis-bitops>, ["~> 0.2.1"])
62
+ s.add_dependency(%q<redis-bitops>, [">= 0"])
63
63
  s.add_dependency(%q<activesupport>, [">= 0"])
64
64
  s.add_dependency(%q<minitest>, [">= 0"])
65
65
  s.add_dependency(%q<yard>, ["~> 0.7"])
@@ -71,7 +71,7 @@ Gem::Specification.new do |s|
71
71
  end
72
72
  else
73
73
  s.add_dependency(%q<redis>, ["~> 3.1.0"])
74
- s.add_dependency(%q<redis-bitops>, ["~> 0.2.1"])
74
+ s.add_dependency(%q<redis-bitops>, [">= 0"])
75
75
  s.add_dependency(%q<activesupport>, [">= 0"])
76
76
  s.add_dependency(%q<minitest>, [">= 0"])
77
77
  s.add_dependency(%q<yard>, ["~> 0.7"])
@@ -3,6 +3,8 @@ require 'securerandom'
3
3
  require 'timecop'
4
4
  Timecop.safe_mode = true
5
5
 
6
+ SECONDS_PER_HOUR = 60*60
7
+ SECONDS_PER_DAY = 24*SECONDS_PER_HOUR
6
8
 
7
9
  describe Retained::Tracker do
8
10
  let(:tracker) { Retained::Tracker.new }
@@ -28,6 +30,11 @@ describe Retained::Tracker do
28
30
  tracker.config.redis_connection.sparse_bitmap("#{prefix}:group_a:1409356800")[0]
29
31
  end
30
32
 
33
+ it 'retains using the "default" group' do
34
+ tracker.retain('entity_a')
35
+ tracker.groups.must_equal ['default']
36
+ end
37
+
31
38
  it 'retain without a period tracks the entity for the current period' do
32
39
  Timecop.freeze(Time.new(2014, 8, 29, 9, 03, 35)) do
33
40
  tracker.retain('entity_a', group: 'group_a')
@@ -37,12 +44,13 @@ describe Retained::Tracker do
37
44
  end
38
45
  end
39
46
 
40
- it 'total_active returns the number of active entities in the default group in the period' do
47
+ it 'total_active returns the number of active entities in the "default" group in the period' do
41
48
  period = Time.new(2014, 8, 30, 10, 47, 35)
42
49
  (count = rand(100)).times do |i|
43
50
  tracker.retain("entity_#{i}", period: period)
44
51
  end
45
52
  tracker.total_active(period: period).must_equal count
53
+ tracker.total_active(group: "default", period: period).must_equal count
46
54
  end
47
55
 
48
56
  it 'total_active returns the number of active entities in the period' do
@@ -60,6 +68,57 @@ describe Retained::Tracker do
60
68
  tracker.total_active(group: 'group_a').must_equal count
61
69
  end
62
70
 
71
+ describe "unique_active" do
72
+ before(:each) do
73
+ tracker.configure do |config|
74
+ config.group('hour') { |g| g.reporting_interval = :hour }
75
+ config.group('minute') { |g| g.reporting_interval = :minute }
76
+ config.group('day') { |g| g.reporting_interval = :day}
77
+ end
78
+ end
79
+
80
+ it 'properly tracks uniques when the reporting_interval is day' do
81
+ Timecop.freeze do
82
+ (1..4).each { |e| tracker.retain(e, group: 'day', period: Time.now)}
83
+ (2..4).each { |e| tracker.retain(e, group: 'day', period: Time.now - SECONDS_PER_DAY)}
84
+ (2..5).each { |e| tracker.retain(e, group: 'day', period: Time.now - 2*SECONDS_PER_DAY)}
85
+ (1..2).each { |e| tracker.retain(e, group: 'day', period: Time.now - 3*SECONDS_PER_DAY)}
86
+
87
+ tracker.unique_active(group: 'day', start: Time.now - 2*SECONDS_PER_DAY).must_equal 5
88
+ tracker.unique_active(group: 'day', start: Time.now - 2*SECONDS_PER_DAY, stop: Time.now - SECONDS_PER_DAY).must_equal 4
89
+ tracker.unique_active(group: 'day', start: Time.now - 4*SECONDS_PER_DAY, stop: Time.now - 5*SECONDS_PER_DAY).must_equal 0
90
+ tracker.unique_active(group: 'day', start: Time.now - 3*SECONDS_PER_DAY, stop: Time.now - 3*SECONDS_PER_DAY).must_equal 2
91
+ end
92
+ end
93
+
94
+ it 'properly tracks uniques when the reporting_interval is hour' do
95
+ Timecop.freeze do
96
+ (1..2).each { |e| tracker.retain(e, group: 'hour', period: Time.now)}
97
+ (3..4).each { |e| tracker.retain(e, group: 'hour', period: Time.now - 3*SECONDS_PER_HOUR)}
98
+ (5..6).each { |e| tracker.retain(e, group: 'hour', period: Time.now - 4*SECONDS_PER_HOUR)}
99
+ (6..8).each { |e| tracker.retain(e, group: 'hour', period: Time.now - 6*SECONDS_PER_HOUR)}
100
+
101
+ tracker.unique_active(group: 'hour', start: Time.now - 4*SECONDS_PER_HOUR).must_equal 6
102
+ tracker.unique_active(group: 'hour', start: Time.now - 2*SECONDS_PER_HOUR, stop: Time.now - SECONDS_PER_HOUR).must_equal 0
103
+ # tracker.unique_active(group: 'hour', start: Time.now - 6*SECONDS_PER_HOUR, stop: Time.now - 4*SECONDS_PER_HOUR).must_equal 4
104
+ # tracker.unique_active(group: 'hour', start: Time.now - 6*SECONDS_PER_HOUR, stop: Time.now - 3*SECONDS_PER_HOUR).must_equal 7
105
+ end
106
+ end
107
+
108
+ it 'properly tracks uniques when the reporting_interval is minute' do
109
+ Timecop.freeze do
110
+ (1..2).each { |e| tracker.retain(e, group: 'minute', period: Time.now)}
111
+ (3..5).each { |e| tracker.retain(e, group: 'minute', period: Time.now - 2*60)}
112
+ (2..3).each { |e| tracker.retain(e, group: 'minute', period: Time.now - 4*60)}
113
+ (6..7).each { |e| tracker.retain(e, group: 'minute', period: Time.now - 5*60)}
114
+
115
+ tracker.unique_active(group: 'minute', start: Time.now - 5*60).must_equal 7
116
+ tracker.unique_active(group: 'minute', start: Time.now - 2*60, stop: Time.now).must_equal 5
117
+ tracker.unique_active(group: 'minute', start: Time.now - 5*60, stop: Time.now - 4*60).must_equal 4
118
+ end
119
+ end
120
+ end
121
+
63
122
  describe 'active?' do
64
123
  it 'returns true when the entity is active' do
65
124
  Timecop.freeze do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: retained
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Saffitz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-13 00:00:00.000000000 Z
11
+ date: 2014-11-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: redis-bitops
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.2.1
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 0.2.1
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: activesupport
43
43
  requirement: !ruby/object:Gem::Requirement