retained 0.3.1 → 0.3.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: 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