totalizer 0.0.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 +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +336 -0
- data/Rakefile +2 -0
- data/lib/totalizer/errors.rb +15 -0
- data/lib/totalizer/factory.rb +81 -0
- data/lib/totalizer/message.rb +67 -0
- data/lib/totalizer/metric.rb +66 -0
- data/lib/totalizer/step.rb +37 -0
- data/lib/totalizer/version.rb +3 -0
- data/lib/totalizer.rb +6 -0
- data/spec/lib/totalizer/factory_spec.rb +141 -0
- data/spec/lib/totalizer/metric_spec.rb +116 -0
- data/spec/lib/totalizer/step_spec.rb +48 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/support/factories.rb +11 -0
- data/spec/support/models.rb +5 -0
- data/spec/support/schema.rb +13 -0
- data/totalizer.gemspec +29 -0
- metadata +184 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 325cabe6e33e0ec8d6ff8f104b20cb50194f8ac0
|
|
4
|
+
data.tar.gz: 19e48c1f6977e45f048cfa9e4a6b9456b5d30ede
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 543b8a101ff5fcacdb94703e9372b5a3f51ea0546934b677e5ff7a940ed83600e3027fa3c257a92cacefa9d4e1407c897ae3a519b1adb3e49263815738650cba
|
|
7
|
+
data.tar.gz: 7a6495f66fc42afe02c187347a5b228959e0e5e38945ed6db666f18567a52233904937b69201be7441a0b79f10a0610c840f85c886852487643bf57d14eb44e5
|
data/.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
.bundle
|
|
4
|
+
.config
|
|
5
|
+
.yardoc
|
|
6
|
+
Gemfile.lock
|
|
7
|
+
InstalledFiles
|
|
8
|
+
_yardoc
|
|
9
|
+
coverage
|
|
10
|
+
doc/
|
|
11
|
+
lib/bundler/man
|
|
12
|
+
pkg
|
|
13
|
+
rdoc
|
|
14
|
+
spec/reports
|
|
15
|
+
test/tmp
|
|
16
|
+
test/version_tmp
|
|
17
|
+
tmp
|
|
18
|
+
*.bundle
|
|
19
|
+
*.so
|
|
20
|
+
*.o
|
|
21
|
+
*.a
|
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2014 Michael Dijkstra
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Totalizer - Calculate important metrics in your Rails application.
|
|
2
|
+
|
|
3
|
+
Provides tools to Ruby on Rails developers to create calculations for Acquisition, Activation, Engagement, Retention and Churn.
|
|
4
|
+
|
|
5
|
+
### Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
gem 'totalizer'
|
|
10
|
+
|
|
11
|
+
And then execute:
|
|
12
|
+
|
|
13
|
+
$ bundle
|
|
14
|
+
|
|
15
|
+
Or install it yourself as:
|
|
16
|
+
|
|
17
|
+
$ gem install totalizer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
***
|
|
22
|
+
|
|
23
|
+
## Factory
|
|
24
|
+
|
|
25
|
+
The Totalizer Factory makes it easy to report on Acquisition, Activation, Engagement, Retention and Churn.
|
|
26
|
+
|
|
27
|
+
By defining one growth metric, like new user creation, and one key activity metric, like creating a post, you can generate all five reports.
|
|
28
|
+
|
|
29
|
+
To create a Factory just use this in your Rails application:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
growth_metric = Totalizer::Metric.new(model: User)
|
|
33
|
+
activity_metric = Totalizer::Metric.new(model: Project, map: 'user_id')
|
|
34
|
+
factory = Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: activity_metric)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This will return a message object for each calculation.
|
|
38
|
+
|
|
39
|
+
#### Parameters
|
|
40
|
+
|
|
41
|
+
You can pass the following parameters into the Factory:
|
|
42
|
+
|
|
43
|
+
+ `growth_metric`: (required) a Totalizer metric representing growth, usually a User model.
|
|
44
|
+
+ `activity_metric`: (required) a Totalizer metric representing the key activity a user should do within your application.
|
|
45
|
+
+ `date`: When to start measuring your records from. Must be a DateTime. Default is `now`.
|
|
46
|
+
+ `acquisition_duration`: Duration (in days) to measure your records. Must be an integer. Default is `7`.
|
|
47
|
+
+ `activation_duration`: Duration (in days) to measure your activation. Must be an integer. Default is `7`.
|
|
48
|
+
+ `engagement_duration`: Duration (in days) to measure your engagement. Must be an integer. Default is `7`.
|
|
49
|
+
+ `retention_duration`: Duration (in days) to measure your retention. Must be an integer. Default is `30`.
|
|
50
|
+
+ `churn_duration`: Duration (in days) to measure your churn. Must be an integer. Default is `30`.
|
|
51
|
+
|
|
52
|
+
### Acquisition
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
growth_metric = Totalizer::Metric.new(model: User)
|
|
56
|
+
activity_metric = Totalizer::Metric.new(model: Project, map: 'user_id')
|
|
57
|
+
factory = Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: activity_metric)
|
|
58
|
+
acquisition = factory.acquisition
|
|
59
|
+
|
|
60
|
+
acquisition.title
|
|
61
|
+
#=> "Acquisition"
|
|
62
|
+
|
|
63
|
+
acquisition.pretext
|
|
64
|
+
#=> "Signed up in the last 7 days"
|
|
65
|
+
|
|
66
|
+
acquisition.text
|
|
67
|
+
#=> "74 (Growth rate: 10%)"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Activation
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
growth_metric = Totalizer::Metric.new(model: User)
|
|
74
|
+
activity_metric = Totalizer::Metric.new(model: Project, map: 'user_id')
|
|
75
|
+
factory = Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: activity_metric)
|
|
76
|
+
activation = factory.activation
|
|
77
|
+
|
|
78
|
+
activation.title
|
|
79
|
+
#=> "Activation"
|
|
80
|
+
|
|
81
|
+
activation.pretext
|
|
82
|
+
#=> "Signed up in the last 7 days and did key activity"
|
|
83
|
+
|
|
84
|
+
activation.text
|
|
85
|
+
#=> "63/90 (Conversion rate: 70%)"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Engagement
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
growth_metric = Totalizer::Metric.new(model: User)
|
|
92
|
+
activity_metric = Totalizer::Metric.new(model: Project, map: 'user_id')
|
|
93
|
+
factory = Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: activity_metric)
|
|
94
|
+
engagement = factory.engagement
|
|
95
|
+
|
|
96
|
+
engagement.title
|
|
97
|
+
#=> "Engagement"
|
|
98
|
+
|
|
99
|
+
retention.pretext
|
|
100
|
+
#=> "Signed up more than 7 days ago and did key activity in the last 7 days"
|
|
101
|
+
|
|
102
|
+
engagement.text
|
|
103
|
+
#=> "42/350 (Engagement rate: 12%)"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Retention
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
growth_metric = Totalizer::Metric.new(model: User)
|
|
110
|
+
activity_metric = Totalizer::Metric.new(model: Project, map: 'user_id')
|
|
111
|
+
factory = Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: activity_metric)
|
|
112
|
+
retention = factory.retention
|
|
113
|
+
|
|
114
|
+
retention.title
|
|
115
|
+
#=> "Retention"
|
|
116
|
+
|
|
117
|
+
retention.pretext
|
|
118
|
+
#=> "Did key activity more than 7 days ago and again in the last 7 days"
|
|
119
|
+
|
|
120
|
+
retention.text
|
|
121
|
+
#=> "42/75 56%"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Churn
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
growth_metric = Totalizer::Metric.new(model: User)
|
|
128
|
+
activity_metric = Totalizer::Metric.new(model: Project, map: 'user_id')
|
|
129
|
+
factory = Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: activity_metric)
|
|
130
|
+
churn = factory.churn
|
|
131
|
+
|
|
132
|
+
churn.title
|
|
133
|
+
#=> "Churn"
|
|
134
|
+
|
|
135
|
+
churn.pretext
|
|
136
|
+
#=> "Acquired more than 7 days ago and did not do key activity in last 7 days over total acquired"
|
|
137
|
+
|
|
138
|
+
churn.text
|
|
139
|
+
#=> "33/75 44%"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Make your metrics visible
|
|
143
|
+
|
|
144
|
+
Metrics are only worthwhile if the team actually sees them.
|
|
145
|
+
|
|
146
|
+
With the results of your Factory you can:
|
|
147
|
+
+ Post them to Slack
|
|
148
|
+
+ Send them via email
|
|
149
|
+
+ Create a dashboard view
|
|
150
|
+
|
|
151
|
+
***
|
|
152
|
+
|
|
153
|
+
You can also access the underlying objects directly.
|
|
154
|
+
|
|
155
|
+
### Metric
|
|
156
|
+
|
|
157
|
+
A Metric is a calculation based on one of your models for a duration of time. To create a Metric just use this in your Rails application:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
metric = Totalizer::Metric.new(model: User)
|
|
161
|
+
|
|
162
|
+
metric.value
|
|
163
|
+
#=> 10 (the number of records for the period)
|
|
164
|
+
|
|
165
|
+
metric.start
|
|
166
|
+
#=> 20 (the number of records before the period)
|
|
167
|
+
|
|
168
|
+
metric.finish
|
|
169
|
+
#=> 30 (the number of records at the end of the period)
|
|
170
|
+
|
|
171
|
+
metric.rate
|
|
172
|
+
#=> 0.5 (how much the number changed over the period)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
##### Parameters
|
|
176
|
+
|
|
177
|
+
You can pass the following parameters into the Metric:
|
|
178
|
+
+ `model` (required): The Rails model class that Totalizer will query.
|
|
179
|
+
+ `date`: When to start measuring your records. Default is `now`.
|
|
180
|
+
+ `duration`: Duration (in days) to measure your records from. Must be an integer. Default is `7`.
|
|
181
|
+
+ `filter`: Write a custom query to determine which records to use in calculation. For example to find all users who created a public response you could pass in: `filter: "is_public = true"`.
|
|
182
|
+
+ `map`: Which field to map records on. For example, to find unique users who did a response you could pass in: `map: 'user_id'`. Default is `id`.
|
|
183
|
+
|
|
184
|
+
### Step
|
|
185
|
+
|
|
186
|
+
A step allows you to easily compare two sets of ids to see who converted.
|
|
187
|
+
|
|
188
|
+
To create a Funnel just use this in your Rails application:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
first_metric = Totalizer::Metric.new(model: User)
|
|
192
|
+
second_metric = Totalizer::Metric.new(model: User, filter: "actions > 0")
|
|
193
|
+
step = Totalizer::Step.new first_step_metric.ids, second_step_metric.ids
|
|
194
|
+
|
|
195
|
+
step.start
|
|
196
|
+
#=> 20 (the number of records that started the step)
|
|
197
|
+
|
|
198
|
+
step.finish
|
|
199
|
+
#=> 10 (the number of records that finished the step)
|
|
200
|
+
|
|
201
|
+
step.rate
|
|
202
|
+
#=> 0.5 (the rate that converted from start to finish)
|
|
203
|
+
|
|
204
|
+
step.ids
|
|
205
|
+
#=> [1,2,3,4,5,6,7,8,9,10] (the ids of the records that finished)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
You can use the result of one step to feed into another step. Continuing on the example above, you could do the following:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
third_metric = Totalizer::Metric.new(model: User, filter: "actions > 5")
|
|
212
|
+
next_step = Totalizer::Step.new step.ids, third_metric.ids
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
***
|
|
216
|
+
|
|
217
|
+
## Manual Calculations
|
|
218
|
+
|
|
219
|
+
You can also report on Acquisition, Activation, Retention, Engagement and Churn yourself without using a Factory.
|
|
220
|
+
|
|
221
|
+
### Acquisition
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
acquisition = Totalizer::Metric.new(model: User, duration: 7)
|
|
225
|
+
|
|
226
|
+
acquisition.value
|
|
227
|
+
#=> 7
|
|
228
|
+
|
|
229
|
+
acquisition.rate
|
|
230
|
+
#=> 10
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Activation
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
sign_up = Totalizer::Metric.new(model: User, duration: 7)
|
|
237
|
+
do_action = Totalizer::Metric.new(model: User, filter: "actions > 0")
|
|
238
|
+
activation = Totalizer::Step.new sign_up.ids, do_action.ids
|
|
239
|
+
|
|
240
|
+
activation.start
|
|
241
|
+
#=> 10
|
|
242
|
+
|
|
243
|
+
activation.finish
|
|
244
|
+
#=> 5
|
|
245
|
+
|
|
246
|
+
activation.rate
|
|
247
|
+
#=> 0.5
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Engagement
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
sign_up = Totalizer::Metric.new(model: User, duration: 7)
|
|
254
|
+
do_action = Totalizer::Metric.new(model: User, filter: "actions > 0")
|
|
255
|
+
|
|
256
|
+
existing_active = (sign_up.start_ids & do_action.ids).size
|
|
257
|
+
#=> 14
|
|
258
|
+
|
|
259
|
+
"(existing_active.to_f / sign_up.start.to_f * 100).round(0)}%"
|
|
260
|
+
#=> 12%
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Retention
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
this_week = Totalizer::Metric.new(model: Post, map: 'user_id')
|
|
267
|
+
last_week = Totalizer::Metric.new(model: Post, map: 'user_id')
|
|
268
|
+
retention = Totalizer::Step.new this_week.ids, last_week.ids
|
|
269
|
+
|
|
270
|
+
retention.start
|
|
271
|
+
#=> 14
|
|
272
|
+
|
|
273
|
+
retention.finish
|
|
274
|
+
#=> 7
|
|
275
|
+
|
|
276
|
+
retention.rate
|
|
277
|
+
#=> 0.5
|
|
278
|
+
|
|
279
|
+
"#{(retention.rate * 100).round(0)}%"
|
|
280
|
+
#=> 50%
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Churn
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
acquisition = Totalizer::Metric.new(model: User, duration: 30)
|
|
287
|
+
active_last_period = Totalizer::Metric.new(model: Post, map: 'user_id', duration: 30, date: 30.days.ago)
|
|
288
|
+
active_this_period = Totalizer::Metric.new(model: Post, map: 'user_id', duration: 30)
|
|
289
|
+
|
|
290
|
+
new_and_existing_customers = acquisition.end_value
|
|
291
|
+
#=> 100
|
|
292
|
+
|
|
293
|
+
lost_existing_customers = (active_last_period.ids - active_this_period.ids).size
|
|
294
|
+
#=> 5
|
|
295
|
+
|
|
296
|
+
lost_existing_customers.to_f / (new_and_existing_customers - lost_existing_customers).to_f
|
|
297
|
+
#=> 0.04
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Contributing
|
|
301
|
+
|
|
302
|
+
1. Fork it ( https://github.com/micdijkstra/totalizer/fork )
|
|
303
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
304
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
305
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
306
|
+
5. Create a new Pull Request
|
|
307
|
+
|
|
308
|
+
## Publishing
|
|
309
|
+
|
|
310
|
+
1. Update the version number in `lib/totalizer/version.rb`
|
|
311
|
+
2. Run `gem build totalizer.gemspec`
|
|
312
|
+
3. Run `gem push totalizer-0.0.X.gem`
|
|
313
|
+
|
|
314
|
+
## Licence
|
|
315
|
+
|
|
316
|
+
The MIT License (MIT)
|
|
317
|
+
|
|
318
|
+
Copyright (c) 2014 Michael Dijkstra
|
|
319
|
+
|
|
320
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
321
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
322
|
+
in the Software without restriction, including without limitation the rights
|
|
323
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
324
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
325
|
+
furnished to do so, subject to the following conditions:
|
|
326
|
+
|
|
327
|
+
The above copyright notice and this permission notice shall be included in
|
|
328
|
+
all copies or substantial portions of the Software.
|
|
329
|
+
|
|
330
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
331
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
332
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
333
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
334
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
335
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
336
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module Totalizer
|
|
2
|
+
class Factory
|
|
3
|
+
attr_accessor :growth_metric, :activity_metric, :date, :acquisition_duration, :activation_duration, :engagement_duration, :retention_duration, :churn_duration
|
|
4
|
+
|
|
5
|
+
def initialize params
|
|
6
|
+
self.growth_metric = params[:growth_metric]
|
|
7
|
+
self.activity_metric = params[:activity_metric]
|
|
8
|
+
self.date = params[:date] || DateTime.now
|
|
9
|
+
self.acquisition_duration = params[:acquisition_duration] || 7
|
|
10
|
+
self.activation_duration = params[:activation_duration] || 7
|
|
11
|
+
self.engagement_duration = params[:engagement_duration] || 7
|
|
12
|
+
self.retention_duration = params[:retention_duration] || 30
|
|
13
|
+
self.churn_duration = params[:churn_duration] || 30
|
|
14
|
+
validate_attributes!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def acquisition
|
|
18
|
+
@acquisition ||= calculate_acquisition
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def activation
|
|
22
|
+
@activation ||= calculate_activation
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def engagement
|
|
26
|
+
@engagement ||= calculate_engagement
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def retention
|
|
30
|
+
@retention || calculate_retention
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def churn
|
|
34
|
+
@churn || calculate_churn
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def calculate_acquisition
|
|
40
|
+
growth_metric = Totalizer::Metric.new self.growth_metric.attributes.merge(date: date, duration: acquisition_duration)
|
|
41
|
+
AcqusitionMessage.new(growth_metric, acquisition_duration)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def calculate_activation
|
|
45
|
+
growth_metric = Totalizer::Metric.new self.growth_metric.attributes.merge(date: date, duration: activation_duration)
|
|
46
|
+
activity_metric = Totalizer::Metric.new self.activity_metric.attributes.merge(date: date, duration: activation_duration)
|
|
47
|
+
step = Totalizer::Step.new growth_metric.ids, activity_metric.ids
|
|
48
|
+
ActivationMessage.new(step, activation_duration)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def calculate_engagement
|
|
52
|
+
growth_metric = Totalizer::Metric.new self.growth_metric.attributes.merge(date: date, duration: engagement_duration)
|
|
53
|
+
activity_metric = Totalizer::Metric.new self.activity_metric.attributes.merge(date: date, duration: engagement_duration)
|
|
54
|
+
EngagementMessage.new(growth_metric, activity_metric, engagement_duration)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def calculate_retention
|
|
58
|
+
previous_period = Totalizer::Metric.new self.activity_metric.attributes.merge(date: date - retention_duration.days, duration: retention_duration)
|
|
59
|
+
this_period = Totalizer::Metric.new self.activity_metric.attributes.merge(date: date, duration: retention_duration)
|
|
60
|
+
step = Totalizer::Step.new previous_period.ids, this_period.ids
|
|
61
|
+
RetentionMessage.new(step, retention_duration)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def calculate_churn
|
|
65
|
+
growth_metric = Totalizer::Metric.new self.growth_metric.attributes.merge(date: date, duration: churn_duration)
|
|
66
|
+
previous_activity_metric = Totalizer::Metric.new self.activity_metric.attributes.merge(date: date - churn_duration.days, duration: churn_duration)
|
|
67
|
+
this_activity_metc = Totalizer::Metric.new self.activity_metric.attributes.merge(date: date, duration: churn_duration)
|
|
68
|
+
ChurnMessage.new(growth_metric, previous_activity_metric, this_activity_metc, churn_duration)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def validate_attributes!
|
|
72
|
+
raise Errors::InvalidData unless growth_metric.kind_of?(Totalizer::Metric)
|
|
73
|
+
raise Errors::InvalidData unless activity_metric.kind_of?(Totalizer::Metric)
|
|
74
|
+
raise Errors::InvalidDate unless date.kind_of?(DateTime)
|
|
75
|
+
raise Errors::InvalidDuration unless acquisition_duration.kind_of?(Integer)
|
|
76
|
+
raise Errors::InvalidDuration unless activation_duration.kind_of?(Integer)
|
|
77
|
+
raise Errors::InvalidDuration unless retention_duration.kind_of?(Integer)
|
|
78
|
+
raise Errors::InvalidDuration unless churn_duration.kind_of?(Integer)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Totalizer
|
|
2
|
+
class Message
|
|
3
|
+
attr_accessor :title, :pretext, :text, :duration
|
|
4
|
+
|
|
5
|
+
def days_string
|
|
6
|
+
"#{duration if duration != 1} #{'day'.pluralize(duration)}"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class AcqusitionMessage < Message
|
|
11
|
+
def initialize growth_metric, duration
|
|
12
|
+
self.duration = duration
|
|
13
|
+
self.title = 'Acqusition'
|
|
14
|
+
self.pretext = "Signed up in the last #{days_string}"
|
|
15
|
+
self.text = "#{growth_metric.value} (Growth rate: #{(growth_metric.rate * 100).round(0)}%)"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class StepMessage < Message
|
|
20
|
+
def initialize step, duration
|
|
21
|
+
self.duration = duration
|
|
22
|
+
self.text = "#{step.finish}/#{step.start} (Conversion rate: #{(step.rate * 100).round(0)}%)"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class EngagementMessage < Message
|
|
27
|
+
def initialize growth_metric, activity_metric, duration
|
|
28
|
+
self.duration = duration
|
|
29
|
+
self.title = 'Engagement'
|
|
30
|
+
|
|
31
|
+
existing_active = (growth_metric.start_ids & activity_metric.ids).size
|
|
32
|
+
|
|
33
|
+
self.pretext = "Signed up more than #{days_string} ago and did key activity in the last #{days_string}"
|
|
34
|
+
self.text = "#{existing_active}/#{growth_metric.start} (Engagement rate: #{(existing_active.to_f / growth_metric.start.to_f * 100).round(0)}%)"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class ActivationMessage < StepMessage
|
|
39
|
+
def initialize step, duration
|
|
40
|
+
super
|
|
41
|
+
self.title = 'Activation'
|
|
42
|
+
self.pretext = "Signed up in the last #{days_string} and did key activity"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class RetentionMessage < StepMessage
|
|
47
|
+
def initialize step, duration
|
|
48
|
+
super
|
|
49
|
+
self.title = 'Retention'
|
|
50
|
+
self.pretext = "Did key activity more than #{days_string} ago and again in the last #{days_string}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class ChurnMessage < Message
|
|
55
|
+
def initialize growth_metric, previous_activity_metric, this_activity_metc, duration
|
|
56
|
+
self.duration = duration
|
|
57
|
+
self.title = 'Churn'
|
|
58
|
+
self.pretext = "Acquired more than #{days_string} ago and did not do key activity in last #{days_string} over total acquired"
|
|
59
|
+
|
|
60
|
+
new_and_existing = growth_metric.finish
|
|
61
|
+
lost_existing = (previous_activity_metric.ids - this_activity_metc.ids).size
|
|
62
|
+
final = new_and_existing - lost_existing
|
|
63
|
+
|
|
64
|
+
self.text = "#{lost_existing}/#{new_and_existing} (Churn rate: #{(lost_existing.to_f / new_and_existing.to_f * 100).round(0)}%)"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Totalizer
|
|
2
|
+
class Metric
|
|
3
|
+
attr_accessor :model, :date, :duration, :filter, :map, :ids, :value
|
|
4
|
+
|
|
5
|
+
def initialize params
|
|
6
|
+
self.model = params[:model]
|
|
7
|
+
self.date = params[:date] || DateTime.now
|
|
8
|
+
self.duration = params[:duration] || 7
|
|
9
|
+
self.filter = params[:filter]
|
|
10
|
+
self.map = params[:map] || 'id'
|
|
11
|
+
validate_attributes!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def attributes
|
|
15
|
+
{ model: model, date: date, duration: duration, filter: filter, map: map }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def end_date
|
|
19
|
+
date
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def start_date
|
|
23
|
+
date - duration.days
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ids
|
|
27
|
+
@ids ||= calculate(created_at: start_date..end_date)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def value
|
|
31
|
+
ids.size
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def start_ids
|
|
35
|
+
@start_ids ||= calculate(["created_at < ?", start_date])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def start
|
|
39
|
+
start_ids.size
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def finish_ids
|
|
43
|
+
@finish_ids ||= calculate(["created_at < ?", end_date])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def finish
|
|
47
|
+
finish_ids.size
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def rate
|
|
51
|
+
(finish - start).to_f / start.to_f
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def calculate where
|
|
57
|
+
model.where(filter).where(where).map { |object| object.send(map) }.uniq
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_attributes!
|
|
61
|
+
raise Errors::InvalidModel if model.nil?
|
|
62
|
+
raise Errors::InvalidDate unless date.kind_of?(DateTime)
|
|
63
|
+
raise Errors::InvalidDuration unless duration.kind_of?(Integer)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Totalizer
|
|
2
|
+
class Step
|
|
3
|
+
attr_accessor :start_ids, :end_ids
|
|
4
|
+
|
|
5
|
+
def initialize start_ids, end_ids
|
|
6
|
+
self.start_ids = start_ids
|
|
7
|
+
self.end_ids = end_ids
|
|
8
|
+
validate!
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def rate
|
|
12
|
+
(finish.to_f/(start.to_f.nonzero? || 1)).round(2)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ids
|
|
16
|
+
@ids ||= calculate
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start
|
|
20
|
+
start_ids.size
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def finish
|
|
24
|
+
ids.size
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def validate!
|
|
30
|
+
raise Errors::InvalidData unless start_ids.kind_of?(Array) && end_ids.kind_of?(Array)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def calculate
|
|
34
|
+
start_ids & end_ids
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/totalizer.rb
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Totalizer::Factory do
|
|
4
|
+
let(:growth_metric) { Totalizer::Metric.new model: User }
|
|
5
|
+
let(:activity_metric) { Totalizer::Metric.new model: Post, map: 'user_id' }
|
|
6
|
+
let(:duration) { nil }
|
|
7
|
+
let(:date) { nil }
|
|
8
|
+
let(:factory) { Totalizer::Factory.new growth_metric: growth_metric, activity_metric: activity_metric, date: date, retention_duration: 7, churn_duration: 7 }
|
|
9
|
+
|
|
10
|
+
describe "Validate" do
|
|
11
|
+
it "requires metrics" do
|
|
12
|
+
expect{ Totalizer::Factory.new(growth_metric: 'fake', activity_metric: 'metric') }.to raise_exception(Totalizer::Errors::InvalidData)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "requires activity metric" do
|
|
16
|
+
expect{ Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: 'metric') }.to raise_exception(Totalizer::Errors::InvalidData)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "requires growth metric" do
|
|
20
|
+
expect{ Totalizer::Factory.new(growth_metric: 'fake', activity_metric: activity_metric) }.to raise_exception(Totalizer::Errors::InvalidData)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "requires a valid datetime" do
|
|
24
|
+
expect{ Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: activity_metric, date: 'Whenever') }.to raise_exception(Totalizer::Errors::InvalidDate)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "requires a acquisition duration" do
|
|
28
|
+
expect{ Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: activity_metric, acquisition_duration: 'Whatever') }.to raise_exception(Totalizer::Errors::InvalidDuration)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "requires a activation duration" do
|
|
32
|
+
expect{ Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: activity_metric, activation_duration: 'Whatever') }.to raise_exception(Totalizer::Errors::InvalidDuration)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "requires a retention duration" do
|
|
36
|
+
expect{ Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: activity_metric, retention_duration: 'Whatever') }.to raise_exception(Totalizer::Errors::InvalidDuration)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "requires a churn duration" do
|
|
40
|
+
expect{ Totalizer::Factory.new(growth_metric: growth_metric, activity_metric: activity_metric, churn_duration: 'Whatever') }.to raise_exception(Totalizer::Errors::InvalidDuration)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe "Initialize" do
|
|
45
|
+
it "defaults to today" do
|
|
46
|
+
expect(factory.date).to eq DateTime.now
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "defaults to 7 day acquisition duration" do
|
|
50
|
+
expect(factory.acquisition_duration).to eq 7
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe "Calculate" do
|
|
55
|
+
let!(:user) { FactoryGirl.create :user, created_at: 2.days.ago }
|
|
56
|
+
let(:user_2) { FactoryGirl.create :user, created_at: 4.days.ago }
|
|
57
|
+
let(:user_3) { FactoryGirl.create :user, created_at: 8.days.ago }
|
|
58
|
+
let(:user_4) { FactoryGirl.create :user, created_at: 10.days.ago }
|
|
59
|
+
let(:user_5) { FactoryGirl.create :user, created_at: 11.days.ago }
|
|
60
|
+
let(:user_6) { FactoryGirl.create :user, created_at: 12.days.ago }
|
|
61
|
+
before do
|
|
62
|
+
user
|
|
63
|
+
FactoryGirl.create :post, user_id: user_2.id, created_at: 4.days.ago
|
|
64
|
+
FactoryGirl.create :post, user_id: user_3.id, created_at: 8.days.ago
|
|
65
|
+
FactoryGirl.create :post, user_id: user_4.id, created_at: 8.days.ago
|
|
66
|
+
FactoryGirl.create :post, user_id: user_4.id, created_at: 2.days.ago
|
|
67
|
+
FactoryGirl.create :post, user_id: user_5.id, created_at: 11.days.ago
|
|
68
|
+
FactoryGirl.create :post, user_id: user_6.id, created_at: 12.days.ago
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe "Acquisition" do
|
|
72
|
+
it "returns title" do
|
|
73
|
+
expect(factory.acquisition.title).to eq 'Acqusition'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "returns pretext" do
|
|
77
|
+
expect(factory.acquisition.pretext).to eq 'Signed up in the last 7 days'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "returns text" do
|
|
81
|
+
expect(factory.acquisition.text).to eq "2 (Growth rate: 50%)"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe "Activation" do
|
|
86
|
+
it "returns title" do
|
|
87
|
+
expect(factory.activation.title).to eq 'Activation'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "returns pretext" do
|
|
91
|
+
expect(factory.activation.pretext).to eq 'Signed up in the last 7 days and did key activity'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "returns text" do
|
|
95
|
+
expect(factory.activation.text).to eq "1/2 (Conversion rate: 50%)"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe "Engagement" do
|
|
100
|
+
it "returns title" do
|
|
101
|
+
expect(factory.engagement.title).to eq 'Engagement'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "returns pretext" do
|
|
105
|
+
expect(factory.engagement.pretext).to eq 'Signed up more than 7 days ago and did key activity in the last 7 days'
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "returns text" do
|
|
109
|
+
expect(factory.engagement.text).to eq "1/4 (Engagement rate: 25%)"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe "Retention" do
|
|
114
|
+
it "returns title" do
|
|
115
|
+
expect(factory.retention.title).to eq 'Retention'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "returns pretext" do
|
|
119
|
+
expect(factory.retention.pretext).to eq 'Did key activity more than 7 days ago and again in the last 7 days'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "returns text" do
|
|
123
|
+
expect(factory.retention.text).to eq "1/4 (Conversion rate: 25%)"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
describe "Churn" do
|
|
128
|
+
it "returns title" do
|
|
129
|
+
expect(factory.churn.title).to eq 'Churn'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "returns pretext" do
|
|
133
|
+
expect(factory.churn.pretext).to eq 'Acquired more than 7 days ago and did not do key activity in last 7 days over total acquired'
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "returns text" do
|
|
137
|
+
expect(factory.churn.text).to eq "3/6 (Churn rate: 50%)"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Totalizer::Metric do
|
|
4
|
+
let(:metric) { Totalizer::Metric.new(model: User) }
|
|
5
|
+
|
|
6
|
+
describe "Validate" do
|
|
7
|
+
it "requires a model" do
|
|
8
|
+
expect{ Totalizer::Metric.new(model: nil) }.to raise_exception(Totalizer::Errors::InvalidModel)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "requires a valid datetime" do
|
|
12
|
+
expect{ Totalizer::Metric.new(model: User, date: 'Whenever') }.to raise_exception(Totalizer::Errors::InvalidDate)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "requires a duration" do
|
|
16
|
+
expect{ Totalizer::Metric.new(model: User, duration: 'Whatever') }.to raise_exception(Totalizer::Errors::InvalidDuration)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "Initialize" do
|
|
21
|
+
it "defaults to today" do
|
|
22
|
+
expect(metric.date).to eq DateTime.now
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "defaults to 7 day duration" do
|
|
26
|
+
expect(metric.duration).to eq 7
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "sets the range" do
|
|
30
|
+
expect(metric.start_date).to eq DateTime.now - 7.days
|
|
31
|
+
expect(metric.end_date).to eq DateTime.now
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "defaults map to id" do
|
|
35
|
+
expect(metric.map).to eq 'id'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
describe "Calculate" do
|
|
41
|
+
let!(:user_1) { FactoryGirl.create :user, created_at: 8.days.ago }
|
|
42
|
+
let!(:user_2) { FactoryGirl.create :user, created_at: 3.days.ago }
|
|
43
|
+
let!(:user_3) { FactoryGirl.create :user, created_at: 2.days.ago }
|
|
44
|
+
|
|
45
|
+
it "maps the ids for the duration" do
|
|
46
|
+
expect(metric.ids).to eq [user_2.id, user_3.id]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "counts the ids for duration" do
|
|
50
|
+
expect(metric.value).to eq 2
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "maps the ids for the start" do
|
|
54
|
+
expect(metric.start_ids).to eq [user_1.id]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "counts the ids for start" do
|
|
58
|
+
expect(metric.start).to eq 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "maps the ids for the end" do
|
|
62
|
+
expect(metric.finish_ids).to eq [user_1.id, user_2.id, user_3.id]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "counts the ids for end" do
|
|
66
|
+
expect(metric.finish).to eq 3
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "calculates the rate of rate" do
|
|
70
|
+
expect(metric.rate).to eq 2
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe 'with invalid filter' do
|
|
74
|
+
let(:metric) { Totalizer::Metric.new(model: User, date: DateTime.now, duration: 7, filter: 'non_existant_field = true') }
|
|
75
|
+
|
|
76
|
+
it "requires a valid filter" do
|
|
77
|
+
expect{ metric.value }.to raise_exception(ActiveRecord::StatementInvalid)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe "Filter" do
|
|
83
|
+
let!(:user_1) { FactoryGirl.create :user, created_at: 8.days.ago, actions: 2 }
|
|
84
|
+
let!(:user_2) { FactoryGirl.create :user, created_at: 3.days.ago, actions: 0 }
|
|
85
|
+
let!(:user_3) { FactoryGirl.create :user, created_at: 2.days.ago, actions: 2 }
|
|
86
|
+
let(:metric) { Totalizer::Metric.new(model: User, filter: "actions > 0") }
|
|
87
|
+
|
|
88
|
+
it "maps the ids for the duration" do
|
|
89
|
+
expect(metric.ids).to eq [user_3.id]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "counts the ids for duration" do
|
|
93
|
+
expect(metric.value).to eq 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "maps the ids for the start" do
|
|
97
|
+
expect(metric.start_ids).to eq [user_1.id]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "counts the ids for start" do
|
|
101
|
+
expect(metric.start).to eq 1
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "maps the ids for the end" do
|
|
105
|
+
expect(metric.finish_ids).to eq [user_1.id, user_3.id]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "counts the ids for end" do
|
|
109
|
+
expect(metric.finish).to eq 2
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "calculates the rate of rate" do
|
|
113
|
+
expect(metric.rate).to eq 1
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Totalizer::Step do
|
|
4
|
+
let(:metric_1) { Totalizer::Metric.new(model: User) }
|
|
5
|
+
let(:metric_2) { Totalizer::Metric.new(model: Post, map: 'user_id') }
|
|
6
|
+
|
|
7
|
+
describe "Validate" do
|
|
8
|
+
it "requires arrays" do
|
|
9
|
+
expect{ Totalizer::Step.new('fake', 'metric') }.to raise_exception(Totalizer::Errors::InvalidData)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "requires two arrays" do
|
|
13
|
+
expect{ Totalizer::Step.new(metric_1.ids, 'metric') }.to raise_exception(Totalizer::Errors::InvalidData)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "requires two arrays" do
|
|
17
|
+
expect{ Totalizer::Step.new('fake', metric_2.ids) }.to raise_exception(Totalizer::Errors::InvalidData)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe "Calculate" do
|
|
22
|
+
let(:user) { FactoryGirl.create :user, created_at: 2.days.ago }
|
|
23
|
+
let(:user_2) { FactoryGirl.create :user, created_at: 4.days.ago }
|
|
24
|
+
let(:user_3) { FactoryGirl.create :user, created_at: 8.days.ago }
|
|
25
|
+
let(:step) { Totalizer::Step.new metric_1.ids, metric_2.ids }
|
|
26
|
+
before do
|
|
27
|
+
user
|
|
28
|
+
FactoryGirl.create :post, user_id: user_2.id, created_at: 4.days.ago
|
|
29
|
+
FactoryGirl.create :post, user_id: user_3.id, created_at: 8.days.ago
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "maps the ids who finishd the step" do
|
|
33
|
+
expect(step.ids).to eq [user_2.id]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "counts the number who start the step" do
|
|
37
|
+
expect(step.start).to eq 2
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "counts the number who finishd the step" do
|
|
41
|
+
expect(step.finish).to eq 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "calculates the rate from previous step" do
|
|
45
|
+
expect(step.rate).to eq 0.5
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
|
4
|
+
# loaded once.
|
|
5
|
+
#
|
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
|
7
|
+
#
|
|
8
|
+
#
|
|
9
|
+
require 'totalizer'
|
|
10
|
+
require 'active_record'
|
|
11
|
+
require 'factory_girl'
|
|
12
|
+
require 'database_cleaner'
|
|
13
|
+
require 'timecop'
|
|
14
|
+
|
|
15
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
|
16
|
+
load File.dirname(__FILE__) + '/support/schema.rb'
|
|
17
|
+
require File.dirname(__FILE__) + '/support/models.rb'
|
|
18
|
+
require File.dirname(__FILE__) + '/support/factories.rb'
|
|
19
|
+
|
|
20
|
+
RSpec.configure do |config|
|
|
21
|
+
config.run_all_when_everything_filtered = true
|
|
22
|
+
config.filter_run :focus
|
|
23
|
+
|
|
24
|
+
# Run specs in random order to surface order dependencies. If you find an
|
|
25
|
+
# order dependency and want to debug it, you can fix the order by providing
|
|
26
|
+
# the seed, which is printed after each run.
|
|
27
|
+
# --seed 1234
|
|
28
|
+
config.order = 'random'
|
|
29
|
+
|
|
30
|
+
config.include FactoryGirl::Syntax::Methods
|
|
31
|
+
|
|
32
|
+
config.before(:suite) do
|
|
33
|
+
DatabaseCleaner.strategy = :truncation
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
config.before(:each) do
|
|
37
|
+
Timecop.freeze(Time.local(1990))
|
|
38
|
+
DatabaseCleaner.start
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
config.after(:each) do
|
|
42
|
+
Timecop.return
|
|
43
|
+
DatabaseCleaner.clean
|
|
44
|
+
end
|
|
45
|
+
end
|
data/totalizer.gemspec
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'totalizer/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "totalizer"
|
|
8
|
+
spec.version = Totalizer::VERSION
|
|
9
|
+
spec.authors = ["Michael Dijkstra"]
|
|
10
|
+
spec.email = ["micdijkstra@gmail.com"]
|
|
11
|
+
spec.summary = %q{Totalizer makes it easy to calculate important metrics in your Rails app.}
|
|
12
|
+
spec.description = %q{Provides tools to Ruby on Rails developers to create calculations for acquisiton, activation, engagement, retention and churn.}
|
|
13
|
+
spec.homepage = ""
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
|
+
spec.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
|
22
|
+
spec.add_development_dependency "rake"
|
|
23
|
+
spec.add_development_dependency "rspec", "~> 2.6"
|
|
24
|
+
spec.add_development_dependency "activerecord"
|
|
25
|
+
spec.add_development_dependency "sqlite3"
|
|
26
|
+
spec.add_development_dependency "factory_girl_rails"
|
|
27
|
+
spec.add_development_dependency "database_cleaner"
|
|
28
|
+
spec.add_development_dependency "timecop"
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: totalizer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michael Dijkstra
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2016-10-01 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.6'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.6'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.6'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.6'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: activerecord
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: sqlite3
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: factory_girl_rails
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: database_cleaner
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: timecop
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
125
|
+
description: Provides tools to Ruby on Rails developers to create calculations for
|
|
126
|
+
acquisiton, activation, engagement, retention and churn.
|
|
127
|
+
email:
|
|
128
|
+
- micdijkstra@gmail.com
|
|
129
|
+
executables: []
|
|
130
|
+
extensions: []
|
|
131
|
+
extra_rdoc_files: []
|
|
132
|
+
files:
|
|
133
|
+
- ".gitignore"
|
|
134
|
+
- Gemfile
|
|
135
|
+
- LICENSE.txt
|
|
136
|
+
- README.md
|
|
137
|
+
- Rakefile
|
|
138
|
+
- lib/totalizer.rb
|
|
139
|
+
- lib/totalizer/errors.rb
|
|
140
|
+
- lib/totalizer/factory.rb
|
|
141
|
+
- lib/totalizer/message.rb
|
|
142
|
+
- lib/totalizer/metric.rb
|
|
143
|
+
- lib/totalizer/step.rb
|
|
144
|
+
- lib/totalizer/version.rb
|
|
145
|
+
- spec/lib/totalizer/factory_spec.rb
|
|
146
|
+
- spec/lib/totalizer/metric_spec.rb
|
|
147
|
+
- spec/lib/totalizer/step_spec.rb
|
|
148
|
+
- spec/spec_helper.rb
|
|
149
|
+
- spec/support/factories.rb
|
|
150
|
+
- spec/support/models.rb
|
|
151
|
+
- spec/support/schema.rb
|
|
152
|
+
- totalizer.gemspec
|
|
153
|
+
homepage: ''
|
|
154
|
+
licenses:
|
|
155
|
+
- MIT
|
|
156
|
+
metadata: {}
|
|
157
|
+
post_install_message:
|
|
158
|
+
rdoc_options: []
|
|
159
|
+
require_paths:
|
|
160
|
+
- lib
|
|
161
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - ">="
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '0'
|
|
166
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
167
|
+
requirements:
|
|
168
|
+
- - ">="
|
|
169
|
+
- !ruby/object:Gem::Version
|
|
170
|
+
version: '0'
|
|
171
|
+
requirements: []
|
|
172
|
+
rubyforge_project:
|
|
173
|
+
rubygems_version: 2.6.6
|
|
174
|
+
signing_key:
|
|
175
|
+
specification_version: 4
|
|
176
|
+
summary: Totalizer makes it easy to calculate important metrics in your Rails app.
|
|
177
|
+
test_files:
|
|
178
|
+
- spec/lib/totalizer/factory_spec.rb
|
|
179
|
+
- spec/lib/totalizer/metric_spec.rb
|
|
180
|
+
- spec/lib/totalizer/step_spec.rb
|
|
181
|
+
- spec/spec_helper.rb
|
|
182
|
+
- spec/support/factories.rb
|
|
183
|
+
- spec/support/models.rb
|
|
184
|
+
- spec/support/schema.rb
|