retained 0.2.1 → 0.2.2

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