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 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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in totalizer.gemspec
4
+ gemspec
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,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,15 @@
1
+ module Totalizer
2
+ module Errors
3
+ class InvalidData < StandardError
4
+ end
5
+
6
+ class InvalidModel < InvalidData
7
+ end
8
+
9
+ class InvalidDate < InvalidData
10
+ end
11
+
12
+ class InvalidDuration < InvalidData
13
+ end
14
+ end
15
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Totalizer
2
+ VERSION = "0.0.2"
3
+ end
data/lib/totalizer.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "totalizer/version"
2
+ require 'totalizer/errors'
3
+ require 'totalizer/metric'
4
+ require 'totalizer/step'
5
+ require 'totalizer/factory'
6
+ require 'totalizer/message'
@@ -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
@@ -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
@@ -0,0 +1,11 @@
1
+ # Read about factories at https://github.com/thoughtbot/factory_girl
2
+
3
+ FactoryGirl.define do
4
+ factory :user do
5
+ end
6
+ end
7
+
8
+ FactoryGirl.define do
9
+ factory :post do
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class User < ActiveRecord::Base
2
+ end
3
+
4
+ class Post < ActiveRecord::Base
5
+ end
@@ -0,0 +1,13 @@
1
+ ActiveRecord::Schema.define do
2
+ self.verbose = false
3
+
4
+ create_table :users, force: true do |t|
5
+ t.integer :actions
6
+ t.timestamps
7
+ end
8
+
9
+ create_table :posts, force: true do |t|
10
+ t.integer :user_id
11
+ t.timestamps
12
+ end
13
+ 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