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 +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +4 -1
- data/lib/retained/tracker.rb +53 -8
- data/lib/retained/version.rb +1 -1
- data/retained.gemspec +7 -7
- data/test/tracker_spec.rb +80 -0
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d409f92d84ef7a75a997c20b70000222ad4387e1
|
4
|
+
data.tar.gz: f95025b82eb6e7176f73e715ed1806ad2ee84440
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b5191df944823da917a418e5f6606a39362bb2458a1c13b9ad2ddb769a833249f1022e9c80f2415b0afe69c2fb6f09a8acbc49d3857f9fdb7cf3ba7b85b2b4a
|
7
|
+
data.tar.gz: 207bca86cfea330de4315d5bf4ff2b1d2c8311547a3b2adad6da58e26a9e19902f3bfd00ed797ee6dc1fee8f591a2ceac4ab7f047aa717577e25998da0976fa6
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
data/lib/retained/tracker.rb
CHANGED
@@ -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
|
93
|
-
|
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
|
data/lib/retained/version.rb
CHANGED
data/retained.gemspec
CHANGED
@@ -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.
|
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.
|
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-
|
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.
|
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>, ["
|
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>, ["
|
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>, ["
|
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"])
|
data/test/tracker_spec.rb
CHANGED
@@ -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.
|
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-
|
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: '
|
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: '
|
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.
|
186
|
+
rubygems_version: 2.2.3
|
187
187
|
signing_key:
|
188
188
|
specification_version: 4
|
189
189
|
summary: Activity & Retention Tracking at Scale
|