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