retained 0.3.1 → 0.3.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: da114b8e5259d29aa525e3cb168870de2ce80c33
4
- data.tar.gz: 4e1961abef8c3090e496f6061ef6281a51895a3c
3
+ metadata.gz: d409f92d84ef7a75a997c20b70000222ad4387e1
4
+ data.tar.gz: f95025b82eb6e7176f73e715ed1806ad2ee84440
5
5
  SHA512:
6
- metadata.gz: 68339dda5d5f9a276f2d72c08f1e90459a2214ed046ae316695b13f92b5ba2af4fae5fa9886d83d28c83e667da46bdec84d53906a2112b93454b7f050ac69b33
7
- data.tar.gz: 0e0e8ea4876a3e9f3db18bc0e86213cb16fa186130652c1ab9fc48f39a9858e4b07498af107456074d2a26ecc3db6621fbff274546f19f3a923c96f9314a59e2
6
+ metadata.gz: 1b5191df944823da917a418e5f6606a39362bb2458a1c13b9ad2ddb769a833249f1022e9c80f2415b0afe69c2fb6f09a8acbc49d3857f9fdb7cf3ba7b85b2b4a
7
+ data.tar.gz: 207bca86cfea330de4315d5bf4ff2b1d2c8311547a3b2adad6da58e26a9e19902f3bfd00ed797ee6dc1fee8f591a2ceac4ab7f047aa717577e25998da0976fa6
@@ -1,3 +1,9 @@
1
+ # 0.3.2
2
+
3
+ * Remove redis gem dependency version
4
+ * Added total_retained and retention methods for directly
5
+ calculating retention between to period ranges.
6
+
1
7
  # 0.3.1
2
8
 
3
9
  * Update redis gem dependency to ~3.2
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  source 'http://rubygems.org'
2
2
 
3
- gem 'redis', '~> 3.2'
3
+ gem 'redis'
4
4
  gem 'activesupport'
5
5
 
6
6
  group :development do
@@ -75,7 +75,10 @@ DEPENDENCIES
75
75
  jeweler (~> 2.0.1)
76
76
  minitest
77
77
  rdoc (~> 3.12)
78
- redis (~> 3.2)
78
+ redis
79
79
  simplecov
80
80
  timecop (~> 0.7.1)
81
81
  yard (~> 0.7)
82
+
83
+ BUNDLED WITH
84
+ 1.10.5
@@ -32,12 +32,7 @@ module Retained
32
32
  # the start and end periods (inclusive), or now if no stop
33
33
  # period is provided.
34
34
  def unique_active(group: 'default', start:, stop: Time.now)
35
- keys = []
36
- start = period_start(group, start)
37
- while (start <= stop)
38
- keys << key_period(group, start)
39
- start += seconds_in_reporting_interval(config.group(group).reporting_interval)
40
- end
35
+ keys = period_range_keys(group, start, stop)
41
36
  return 0 if keys.length == 0
42
37
 
43
38
  temp_bitmap do |key|
@@ -46,6 +41,46 @@ module Retained
46
41
  end
47
42
  end
48
43
 
44
+ # Returns the total number of unique active entities retained between
45
+ # an initial and a final period range. Each period range consists
46
+ # of a start period and an end period (inclusive). The final period
47
+ # range's starting period must be after the inital period range's ending
48
+ # period.
49
+ def total_retained(group: 'default', initial_start:, initial_stop:,
50
+ final_start: , final_stop:)
51
+ #raise ArgumentError, "final_start must be after initial_stop" if final_start <= initial_stop
52
+ initial_keys = period_range_keys(group, initial_start, initial_stop)
53
+ final_keys = period_range_keys(group, final_start, final_stop)
54
+
55
+ return 0 if initial_keys == 0 || final_keys == 0
56
+
57
+ temp_bitmap do |key|
58
+ temp_bitmap do |initial_key|
59
+ config.redis_connection.bitop 'OR', initial_key, *initial_keys
60
+ temp_bitmap do |final_key|
61
+ config.redis_connection.bitop 'OR', final_key, *final_keys
62
+ config.redis_connection.bitop 'AND', key, initial_key, final_key
63
+ config.redis_connection.bitcount key
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # Returns the percent retained (as a float) retained between
70
+ # an initial and a final period range. Each period range consists
71
+ # of a start period and an end period (inclusive). The final period
72
+ # range's starting period must be after the inital period range's ending
73
+ # period. If there are no entities in the initial period, Float::NAN is returned.
74
+ def retention(group: 'default', initial_start:, initial_stop:,
75
+ final_start: , final_stop:)
76
+ initial_count = unique_active(group: group, start: initial_start, stop: initial_stop)
77
+ retained = total_retained(group: group, initial_start: initial_start,
78
+ initial_stop: initial_stop,
79
+ final_start: final_start,
80
+ final_stop: final_stop)
81
+ return retained / initial_count.to_f
82
+ end
83
+
49
84
  # Returns true if the entity was active in the given period,
50
85
  # or now if no period is provided. If a group or an array of groups
51
86
  # is provided activity will only be considered based on those groups.
@@ -89,8 +124,18 @@ LUA
89
124
  end
90
125
 
91
126
  private
92
- def temp_bitmap
93
- temp_key = "#{config.prefix}:temp:#{SecureRandom.hex}"
127
+ def period_range_keys(group, start, stop)
128
+ keys = []
129
+ start = period_start(group, start)
130
+ while (start <= stop)
131
+ keys << key_period(group, start)
132
+ start += seconds_in_reporting_interval(config.group(group).reporting_interval)
133
+ end
134
+ keys
135
+ end
136
+
137
+ def temp_bitmap(temp_key=SecureRandom.hex)
138
+ temp_key = "#{config.prefix}:temp:#{temp_key}"
94
139
  begin
95
140
  yield temp_key
96
141
  ensure
@@ -2,7 +2,7 @@ module Retained
2
2
  module Version
3
3
  MAJOR = 0
4
4
  MINOR = 3
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.3.1 ruby lib
5
+ # stub: retained 0.3.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "retained"
9
- s.version = "0.3.1"
9
+ s.version = "0.3.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 = "2015-03-29"
14
+ s.date = "2015-07-02"
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 = [
@@ -40,14 +40,14 @@ Gem::Specification.new do |s|
40
40
  ]
41
41
  s.homepage = "http://github.com/msaffitz/retained"
42
42
  s.licenses = ["MIT"]
43
- s.rubygems_version = "2.4.6"
43
+ s.rubygems_version = "2.2.3"
44
44
  s.summary = "Activity & Retention Tracking at Scale"
45
45
 
46
46
  if s.respond_to? :specification_version then
47
47
  s.specification_version = 4
48
48
 
49
49
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
50
- s.add_runtime_dependency(%q<redis>, ["~> 3.2"])
50
+ s.add_runtime_dependency(%q<redis>, [">= 0"])
51
51
  s.add_runtime_dependency(%q<activesupport>, [">= 0"])
52
52
  s.add_development_dependency(%q<minitest>, [">= 0"])
53
53
  s.add_development_dependency(%q<yard>, ["~> 0.7"])
@@ -57,7 +57,7 @@ Gem::Specification.new do |s|
57
57
  s.add_development_dependency(%q<simplecov>, [">= 0"])
58
58
  s.add_development_dependency(%q<timecop>, ["~> 0.7.1"])
59
59
  else
60
- s.add_dependency(%q<redis>, ["~> 3.2"])
60
+ s.add_dependency(%q<redis>, [">= 0"])
61
61
  s.add_dependency(%q<activesupport>, [">= 0"])
62
62
  s.add_dependency(%q<minitest>, [">= 0"])
63
63
  s.add_dependency(%q<yard>, ["~> 0.7"])
@@ -68,7 +68,7 @@ Gem::Specification.new do |s|
68
68
  s.add_dependency(%q<timecop>, ["~> 0.7.1"])
69
69
  end
70
70
  else
71
- s.add_dependency(%q<redis>, ["~> 3.2"])
71
+ s.add_dependency(%q<redis>, [">= 0"])
72
72
  s.add_dependency(%q<activesupport>, [">= 0"])
73
73
  s.add_dependency(%q<minitest>, [">= 0"])
74
74
  s.add_dependency(%q<yard>, ["~> 0.7"])
@@ -119,6 +119,86 @@ describe Retained::Tracker do
119
119
  end
120
120
  end
121
121
 
122
+ describe "total_retained" do
123
+ before(:each) do
124
+ tracker.configure do |config|
125
+ config.group('hour') { |g| g.reporting_interval = :hour }
126
+ config.group('minute') { |g| g.reporting_interval = :minute }
127
+ config.group('day') { |g| g.reporting_interval = :day}
128
+ end
129
+ end
130
+
131
+ it 'properly tracks retention when the reporting_interval is day' do
132
+ Timecop.freeze do
133
+ # Initial Period
134
+ [1,2].each { |e| tracker.retain(e, group: 'day', period: Time.now - 4*SECONDS_PER_DAY)}
135
+ [3,4].each { |e| tracker.retain(e, group: 'day', period: Time.now - 3*SECONDS_PER_DAY)}
136
+
137
+ # Final Period
138
+ tracker.retain(2, group: 'day', period: Time.now)
139
+ tracker.retain(3, group: 'day', period: Time.now - 2*SECONDS_PER_DAY)
140
+
141
+ tracker.total_retained(group: 'day', initial_start: Time.now - 4*SECONDS_PER_DAY, initial_stop: Time.now - 3*SECONDS_PER_DAY,
142
+ final_start: Time.now - 2*SECONDS_PER_DAY, final_stop: Time.now ).must_equal 2
143
+ tracker.total_retained(group: 'day', initial_start: Time.now - 4*SECONDS_PER_DAY, initial_stop: Time.now - 3*SECONDS_PER_DAY,
144
+ final_start: Time.now , final_stop: Time.now ).must_equal 1
145
+ end
146
+ end
147
+
148
+ it 'properly tracks retention when the reporting_interval is hour' do
149
+ Timecop.freeze do
150
+ # Initial Period
151
+ [1,2].each { |e| tracker.retain(e, group: 'hour', period: Time.now - 4*SECONDS_PER_HOUR)}
152
+ [3,4].each { |e| tracker.retain(e, group: 'hour', period: Time.now - 3*SECONDS_PER_HOUR)}
153
+
154
+ # Final Period
155
+ tracker.retain(2, group: 'hour', period: Time.now)
156
+ tracker.retain(3, group: 'hour', period: Time.now - 2*SECONDS_PER_HOUR)
157
+
158
+ tracker.total_retained(group: 'hour', initial_start: Time.now - 4*SECONDS_PER_HOUR, initial_stop: Time.now - 3*SECONDS_PER_HOUR,
159
+ final_start: Time.now - 2*SECONDS_PER_HOUR, final_stop: Time.now ).must_equal 2
160
+ tracker.total_retained(group: 'hour', initial_start: Time.now - 4*SECONDS_PER_HOUR, initial_stop: Time.now - 3*SECONDS_PER_HOUR,
161
+ final_start: Time.now , final_stop: Time.now ).must_equal 1
162
+ end
163
+ end
164
+
165
+ it 'properly tracks retention when the reporting_interval is minute' do
166
+ Timecop.freeze do
167
+ # Initial Period
168
+ [1,2].each { |e| tracker.retain(e, group: 'minute', period: Time.now - 4*60)}
169
+ [3,4].each { |e| tracker.retain(e, group: 'minute', period: Time.now - 3*60)}
170
+
171
+ # Final Period
172
+ tracker.retain(2, group: 'minute', period: Time.now)
173
+ tracker.retain(3, group: 'minute', period: Time.now - 2*60)
174
+
175
+ tracker.total_retained(group: 'minute', initial_start: Time.now - 4*60, initial_stop: Time.now - 3*60,
176
+ final_start: Time.now - 2*60, final_stop: Time.now ).must_equal 2
177
+ tracker.total_retained(group: 'minute', initial_start: Time.now - 4*60, initial_stop: Time.now - 3*60,
178
+ final_start: Time.now , final_stop: Time.now ).must_equal 1
179
+ end
180
+ end
181
+ end
182
+
183
+ it 'retention properly returns retention percent' do
184
+ Timecop.freeze do
185
+ # Initial Period
186
+ [1,2].each { |e| tracker.retain(e, group: 'minute', period: Time.now - 4*SECONDS_PER_DAY)}
187
+ [3,4].each { |e| tracker.retain(e, group: 'minute', period: Time.now - 3*SECONDS_PER_DAY)}
188
+
189
+ # Final Period
190
+ tracker.retain(2, group: 'minute', period: Time.now)
191
+ tracker.retain(3, group: 'minute', period: Time.now - 2*SECONDS_PER_DAY)
192
+
193
+ tracker.retention(group: 'minute', initial_start: Time.now - 4*SECONDS_PER_DAY, initial_stop: Time.now - 3*SECONDS_PER_DAY,
194
+ final_start: Time.now - 2*SECONDS_PER_DAY, final_stop: Time.now ).must_equal 0.5
195
+
196
+ tracker.retention(group: 'minute', initial_start: Time.now - 4*SECONDS_PER_DAY, initial_stop: Time.now - 3*SECONDS_PER_DAY,
197
+ final_start: Time.now , final_stop: Time.now ).must_equal 0.25
198
+ end
199
+ end
200
+
201
+
122
202
  describe 'active?' do
123
203
  it 'returns true when the entity is active' do
124
204
  Timecop.freeze do
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: retained
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.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: 2015-03-29 00:00:00.000000000 Z
11
+ date: 2015-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3.2'
19
+ version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '3.2'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -183,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
183
  version: '0'
184
184
  requirements: []
185
185
  rubyforge_project:
186
- rubygems_version: 2.4.6
186
+ rubygems_version: 2.2.3
187
187
  signing_key:
188
188
  specification_version: 4
189
189
  summary: Activity & Retention Tracking at Scale