trailguide 0.1.31 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +73 -8
- data/app/assets/javascripts/trail_guide/admin/application.js +20 -1
- data/app/assets/stylesheets/trail_guide/admin/application.css +22 -0
- data/app/assets/stylesheets/trail_guide/admin/experiments.css +36 -3
- data/app/controllers/trail_guide/admin/application_controller.rb +59 -8
- data/app/controllers/trail_guide/admin/experiments_controller.rb +209 -16
- data/app/controllers/trail_guide/admin/groups_controller.rb +34 -0
- data/app/controllers/trail_guide/experiments_controller.rb +1 -1
- data/app/views/layouts/trail_guide/admin/_calculator.erb +24 -0
- data/app/views/layouts/trail_guide/admin/_footer.html.erb +27 -0
- data/app/views/layouts/trail_guide/admin/_header.html.erb +147 -0
- data/app/views/layouts/trail_guide/admin/_import_modal.html.erb +45 -0
- data/app/views/layouts/trail_guide/admin/application.html.erb +17 -3
- data/app/views/trail_guide/admin/experiments/_alert_peek.html.erb +19 -0
- data/app/views/trail_guide/admin/experiments/_alert_state.html.erb +49 -0
- data/app/views/trail_guide/admin/experiments/_btn_analyze.html.erb +11 -0
- data/app/views/trail_guide/admin/experiments/_btn_analyze_goal.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_convert.html.erb +33 -0
- data/app/views/trail_guide/admin/experiments/_btn_enroll.html.erb +3 -0
- data/app/views/trail_guide/admin/experiments/_btn_join.html.erb +11 -0
- data/app/views/trail_guide/admin/experiments/_btn_leave.html.erb +7 -0
- data/app/views/trail_guide/admin/experiments/_btn_pause.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_peek.html.erb +13 -0
- data/app/views/trail_guide/admin/experiments/_btn_reset.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_restart.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_resume.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_schedule.html.erb +6 -0
- data/app/views/trail_guide/admin/experiments/_btn_start.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_stop.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_experiment.html.erb +68 -172
- data/app/views/trail_guide/admin/experiments/_header.html.erb +87 -0
- data/app/views/trail_guide/admin/experiments/_start_modal.html.erb +57 -0
- data/app/views/trail_guide/admin/experiments/_tbody.html.erb +112 -0
- data/app/views/trail_guide/admin/experiments/_thead.html.erb +38 -0
- data/app/views/trail_guide/admin/experiments/index.html.erb +8 -16
- data/app/views/trail_guide/admin/experiments/show.html.erb +78 -0
- data/app/views/trail_guide/admin/groups/index.html.erb +40 -0
- data/app/views/trail_guide/admin/groups/show.html.erb +9 -0
- data/app/views/trail_guide/admin/orphans/_alert.html.erb +44 -0
- data/config/initializers/trailguide.rb +72 -9
- data/config/routes/admin.rb +19 -12
- data/lib/trail_guide/admin/engine.rb +2 -0
- data/lib/trail_guide/admin.rb +2 -0
- data/lib/trail_guide/calculators/bayesian.rb +98 -0
- data/lib/trail_guide/calculators/calculator.rb +58 -0
- data/lib/trail_guide/calculators/score.rb +68 -0
- data/lib/trail_guide/calculators.rb +8 -0
- data/lib/trail_guide/catalog.rb +134 -19
- data/lib/trail_guide/combined_experiment.rb +8 -3
- data/lib/trail_guide/config.rb +6 -1
- data/lib/trail_guide/engine.rb +2 -0
- data/lib/trail_guide/errors.rb +30 -1
- data/lib/trail_guide/experiment.rb +0 -1
- data/lib/trail_guide/experiments/base.rb +189 -53
- data/lib/trail_guide/experiments/config.rb +82 -13
- data/lib/trail_guide/experiments/participant.rb +21 -1
- data/lib/trail_guide/helper.rb +59 -32
- data/lib/trail_guide/metrics/checkpoint.rb +24 -0
- data/lib/trail_guide/metrics/config.rb +45 -0
- data/lib/trail_guide/metrics/funnel.rb +24 -0
- data/lib/trail_guide/metrics/goal.rb +89 -0
- data/lib/trail_guide/metrics.rb +9 -0
- data/lib/trail_guide/participant.rb +54 -21
- data/lib/trail_guide/variant.rb +34 -12
- data/lib/trail_guide/version.rb +2 -2
- data/lib/trailguide.rb +4 -0
- metadata +112 -7
- data/app/views/layouts/trail_guide/admin/_footer.erb +0 -10
- data/app/views/layouts/trail_guide/admin/_header.erb +0 -42
- data/app/views/trail_guide/admin/experiments/_combined_experiment.html.erb +0 -189
- data/config/routes.rb +0 -5
@@ -5,12 +5,16 @@ module TrailGuide
|
|
5
5
|
module Experiments
|
6
6
|
class Base
|
7
7
|
class << self
|
8
|
-
delegate :
|
8
|
+
delegate :groups, :algorithm, :control, :goals, :callbacks, :combined,
|
9
9
|
:combined?, :allow_multiple_conversions?, :allow_multiple_goals?,
|
10
10
|
:track_winner_conversions?, :start_manually?, :reset_manually?,
|
11
|
-
to: :configuration
|
11
|
+
:enable_calibration?, to: :configuration
|
12
12
|
alias_method :funnels, :goals
|
13
13
|
|
14
|
+
def register!
|
15
|
+
TrailGuide.catalog.register(self)
|
16
|
+
end
|
17
|
+
|
14
18
|
def configuration
|
15
19
|
@configuration ||= Experiments::Config.new(self)
|
16
20
|
end
|
@@ -31,6 +35,14 @@ module TrailGuide
|
|
31
35
|
end
|
32
36
|
end
|
33
37
|
|
38
|
+
def is_combined?
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
def combined_experiments
|
43
|
+
combined.map { |combo| TrailGuide.catalog.find(combo) }
|
44
|
+
end
|
45
|
+
|
34
46
|
def run_callbacks(hook, *args)
|
35
47
|
return unless callbacks[hook]
|
36
48
|
return args[0] if hook == :rollout_winner
|
@@ -52,6 +64,15 @@ module TrailGuide
|
|
52
64
|
started
|
53
65
|
end
|
54
66
|
|
67
|
+
def schedule!(start_at, stop_at=nil, context=nil)
|
68
|
+
return false if started?
|
69
|
+
save! unless persisted?
|
70
|
+
scheduled = TrailGuide.redis.hset(storage_key, 'started_at', start_at.to_i)
|
71
|
+
TrailGuide.redis.hset(storage_key, 'stopped_at', stop_at.to_i) if stop_at
|
72
|
+
run_callbacks(:on_schedule, start_at, stop_at, context)
|
73
|
+
scheduled
|
74
|
+
end
|
75
|
+
|
55
76
|
def pause!(context=nil)
|
56
77
|
return false unless running? && configuration.can_resume?
|
57
78
|
paused = TrailGuide.redis.hset(storage_key, 'paused_at', Time.now.to_i)
|
@@ -89,21 +110,33 @@ module TrailGuide
|
|
89
110
|
end
|
90
111
|
|
91
112
|
def started?
|
92
|
-
|
113
|
+
time = started_at
|
114
|
+
time && time <= Time.now
|
115
|
+
end
|
116
|
+
|
117
|
+
def scheduled?
|
118
|
+
time = started_at
|
119
|
+
time && time > Time.now
|
93
120
|
end
|
94
121
|
|
95
122
|
def paused?
|
96
|
-
|
123
|
+
time = paused_at
|
124
|
+
time && time <= Time.now
|
97
125
|
end
|
98
126
|
|
99
127
|
def stopped?
|
100
|
-
|
128
|
+
time = stopped_at
|
129
|
+
time && time <= Time.now
|
101
130
|
end
|
102
131
|
|
103
132
|
def running?
|
104
133
|
started? && !paused? && !stopped?
|
105
134
|
end
|
106
135
|
|
136
|
+
def calibrating?
|
137
|
+
enable_calibration? && start_manually? && !started?
|
138
|
+
end
|
139
|
+
|
107
140
|
def declare_winner!(variant, context=nil)
|
108
141
|
variant = variants.find { |var| var == variant } unless variant.is_a?(Variant)
|
109
142
|
run_callbacks(:on_winner, variant, context)
|
@@ -120,7 +153,11 @@ module TrailGuide
|
|
120
153
|
end
|
121
154
|
|
122
155
|
def winner?
|
123
|
-
|
156
|
+
if combined?
|
157
|
+
combined.all? { |combo| TrailGuide.catalog.find(combo).winner? }
|
158
|
+
else
|
159
|
+
TrailGuide.redis.hexists(storage_key, 'winner')
|
160
|
+
end
|
124
161
|
end
|
125
162
|
|
126
163
|
def persisted?
|
@@ -165,23 +202,14 @@ module TrailGuide
|
|
165
202
|
return false
|
166
203
|
end
|
167
204
|
|
205
|
+
# export the experiment state (not config) as json
|
168
206
|
def as_json(opts={})
|
169
207
|
{ experiment_name => {
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
start_manually: start_manually?,
|
176
|
-
reset_manually: reset_manually?,
|
177
|
-
allow_multiple_conversions: allow_multiple_conversions?,
|
178
|
-
allow_multiple_goals: allow_multiple_goals?
|
179
|
-
},
|
180
|
-
statistics: {
|
181
|
-
# TODO expand on this for variants/goals
|
182
|
-
participants: variants.sum(&:participants),
|
183
|
-
converted: variants.sum(&:converted)
|
184
|
-
}
|
208
|
+
started_at: started_at,
|
209
|
+
paused_at: paused_at,
|
210
|
+
stopped_at: stopped_at,
|
211
|
+
winner: winner.try(:name),
|
212
|
+
variants: variants.map(&:as_json).reduce({}) { |r,v| r.merge!(v) },
|
185
213
|
} }
|
186
214
|
end
|
187
215
|
|
@@ -192,9 +220,9 @@ module TrailGuide
|
|
192
220
|
|
193
221
|
attr_reader :participant
|
194
222
|
delegate :configuration, :experiment_name, :variants, :control, :goals,
|
195
|
-
:storage_key, :
|
196
|
-
:
|
197
|
-
:
|
223
|
+
:storage_key, :combined?, :start_manually?, :reset_manually?,
|
224
|
+
:allow_multiple_conversions?, :allow_multiple_goals?, :is_combined?,
|
225
|
+
:enable_calibration?, :track_winner_conversions?, :callbacks, to: :class
|
198
226
|
|
199
227
|
def initialize(participant)
|
200
228
|
@participant = TrailGuide::Experiments::Participant.new(self, participant)
|
@@ -204,15 +232,15 @@ module TrailGuide
|
|
204
232
|
@algorithm ||= self.class.algorithm.new(self)
|
205
233
|
end
|
206
234
|
|
207
|
-
def
|
208
|
-
run_callbacks(:rollout_winner, self.class.winner)
|
235
|
+
def winning_variant
|
236
|
+
run_callbacks(:rollout_winner, self.class.winner, participant)
|
209
237
|
end
|
210
238
|
|
211
239
|
def choose!(override: nil, metadata: nil, **opts)
|
212
240
|
return control if TrailGuide.configuration.disabled
|
213
241
|
|
214
242
|
variant = choose_variant!(override: override, metadata: metadata, **opts)
|
215
|
-
run_callbacks(:on_use, variant, metadata)
|
243
|
+
run_callbacks(:on_use, variant, participant, metadata)
|
216
244
|
variant
|
217
245
|
rescue Errno::ECONNREFUSED, Redis::BaseError, SocketError => e
|
218
246
|
run_callbacks(:on_redis_failover, e)
|
@@ -225,7 +253,7 @@ module TrailGuide
|
|
225
253
|
|
226
254
|
if override.present?
|
227
255
|
variant = variants.find { |var| var == override } || control
|
228
|
-
if running?
|
256
|
+
if running? && !is_combined?
|
229
257
|
variant.increment_participation! if configuration.track_override
|
230
258
|
participant.participating!(variant) if configuration.store_override
|
231
259
|
end
|
@@ -233,14 +261,36 @@ module TrailGuide
|
|
233
261
|
end
|
234
262
|
|
235
263
|
if winner?
|
236
|
-
variant =
|
237
|
-
|
264
|
+
variant = winning_variant
|
265
|
+
if track_winner_conversions? && running?
|
266
|
+
variant.increment_participation! unless participant.variant == variant
|
267
|
+
participant.exit! if participant.participating? && participant.variant != variant
|
268
|
+
participant.participating!(variant)
|
269
|
+
end
|
238
270
|
return variant
|
239
271
|
end
|
240
272
|
|
241
|
-
return control if excluded
|
242
|
-
|
243
|
-
|
273
|
+
return control if excluded || stopped?
|
274
|
+
|
275
|
+
if !started? && start_manually?
|
276
|
+
if enable_calibration?
|
277
|
+
unless participant.variant == control
|
278
|
+
control.increment_participation!
|
279
|
+
parent.control.increment_participation! if is_combined?
|
280
|
+
end
|
281
|
+
|
282
|
+
if participant.participating? && participant.variant != control
|
283
|
+
participant.exit!
|
284
|
+
parent.participant.exit! if is_combined?
|
285
|
+
end
|
286
|
+
|
287
|
+
participant.participating!(control)
|
288
|
+
parent.participant.participating!(parent.control) if is_combined?
|
289
|
+
end
|
290
|
+
return control
|
291
|
+
end
|
292
|
+
|
293
|
+
start! unless started? || scheduled?
|
244
294
|
return control unless running?
|
245
295
|
|
246
296
|
if participant.participating?
|
@@ -249,11 +299,13 @@ module TrailGuide
|
|
249
299
|
return variant
|
250
300
|
end
|
251
301
|
|
252
|
-
return control unless TrailGuide.configuration.allow_multiple_experiments == true || !participant.participating_in_active_experiments?(TrailGuide.configuration.allow_multiple_experiments == false)
|
302
|
+
return control unless is_combined? || TrailGuide.configuration.allow_multiple_experiments == true || !participant.participating_in_active_experiments?(TrailGuide.configuration.allow_multiple_experiments == false)
|
253
303
|
return control unless allow_participation?(metadata)
|
254
304
|
|
255
305
|
variant = algorithm_choose!(metadata: metadata)
|
256
|
-
|
306
|
+
variant.increment_participation!
|
307
|
+
participant.participating!(variant)
|
308
|
+
run_callbacks(:on_choose, variant, participant, metadata)
|
257
309
|
variant
|
258
310
|
end
|
259
311
|
|
@@ -261,30 +313,48 @@ module TrailGuide
|
|
261
313
|
algorithm.choose!(metadata: metadata)
|
262
314
|
end
|
263
315
|
|
264
|
-
def variant_chosen!(variant, metadata: nil)
|
265
|
-
variant.increment_participation!
|
266
|
-
participant.participating!(variant)
|
267
|
-
run_callbacks(:on_choose, variant, metadata)
|
268
|
-
end
|
269
|
-
|
270
316
|
def convert!(checkpoint=nil, metadata: nil)
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
317
|
+
if !started?
|
318
|
+
return false unless enable_calibration?
|
319
|
+
variant = participant.variant
|
320
|
+
return false unless variant.present? && variant == control
|
321
|
+
else
|
322
|
+
return false unless running?
|
323
|
+
variant = participant.variant
|
324
|
+
return false unless variant.present?
|
325
|
+
|
326
|
+
if winner?
|
327
|
+
return false unless track_winner_conversions? && variant == winner
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
if checkpoint.nil?
|
332
|
+
raise InvalidGoalError, "You must provide a valid goal checkpoint for `#{experiment_name}`." unless goals.empty?
|
333
|
+
else
|
334
|
+
goal = goals.find { |g| g == checkpoint }
|
335
|
+
raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." if goal.nil?
|
336
|
+
checkpoint = goal
|
337
|
+
end
|
338
|
+
|
275
339
|
# TODO eventually allow progressing through funnel checkpoints towards goals
|
276
340
|
if participant.converted?(checkpoint)
|
277
|
-
return false unless allow_multiple_conversions?
|
341
|
+
return false unless (checkpoint.nil? && allow_multiple_conversions?) || (checkpoint.present? && checkpoint.allow_multiple_conversions?)
|
278
342
|
elsif participant.converted?
|
279
343
|
return false unless allow_multiple_goals?
|
280
344
|
end
|
281
|
-
return false unless allow_conversion?(checkpoint, metadata)
|
345
|
+
return false unless allow_conversion?(variant, checkpoint, metadata)
|
282
346
|
|
283
|
-
|
347
|
+
# TODO only reset if !reset_manually? AND they've converted all goals if
|
348
|
+
# allow_multiple_goals? is set
|
349
|
+
# TODO what should happen when allow_multiple_conversions? and !reset_manually?
|
284
350
|
# TODO eventually only reset if we're at the final goal in a funnel
|
285
351
|
participant.converted!(variant, checkpoint, reset: !reset_manually?)
|
286
352
|
variant.increment_conversion!(checkpoint)
|
287
|
-
|
353
|
+
if checkpoint.nil?
|
354
|
+
run_callbacks(:on_convert, checkpoint, variant, participant, metadata)
|
355
|
+
else
|
356
|
+
checkpoint.run_callbacks(:on_convert, self, variant, participant, metadata)
|
357
|
+
end
|
288
358
|
variant
|
289
359
|
rescue Errno::ECONNREFUSED, Redis::BaseError, SocketError => e
|
290
360
|
run_callbacks(:on_redis_failover, e)
|
@@ -293,12 +363,16 @@ module TrailGuide
|
|
293
363
|
|
294
364
|
def allow_participation?(metadata=nil)
|
295
365
|
return true if callbacks[:allow_participation].empty?
|
296
|
-
run_callbacks(:allow_participation, metadata)
|
366
|
+
run_callbacks(:allow_participation, participant, metadata)
|
297
367
|
end
|
298
368
|
|
299
|
-
def allow_conversion?(checkpoint=nil, metadata=nil)
|
300
|
-
|
301
|
-
|
369
|
+
def allow_conversion?(variant, checkpoint=nil, metadata=nil)
|
370
|
+
if checkpoint.nil?
|
371
|
+
return true if callbacks[:allow_conversion].empty?
|
372
|
+
run_callbacks(:allow_conversion, checkpoint, variant, participant, metadata)
|
373
|
+
else
|
374
|
+
checkpoint.allow_conversion?(self, variant, metadata)
|
375
|
+
end
|
302
376
|
end
|
303
377
|
|
304
378
|
def run_callbacks(hook, *args)
|
@@ -322,6 +396,68 @@ module TrailGuide
|
|
322
396
|
end
|
323
397
|
end
|
324
398
|
end
|
399
|
+
|
400
|
+
def combined_experiments
|
401
|
+
@combined_experiments ||= self.class.combined_experiments.map do |combo|
|
402
|
+
combo.new(participant.participant)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
### MEMOIZATOIN ###
|
407
|
+
# This is a lot of seemingly unnecessary duplication, but it really helps
|
408
|
+
# to cut down on the number of redis requests while still being
|
409
|
+
# thread-safe by memoizing these methods/values here at the instance level
|
410
|
+
|
411
|
+
def start!
|
412
|
+
@started_at = nil
|
413
|
+
self.class.start!
|
414
|
+
end
|
415
|
+
|
416
|
+
def started_at
|
417
|
+
@started_at ||= self.class.started_at
|
418
|
+
end
|
419
|
+
|
420
|
+
def paused_at
|
421
|
+
@paused_at ||= self.class.paused_at
|
422
|
+
end
|
423
|
+
|
424
|
+
def stopped_at
|
425
|
+
@stopped_at ||= self.class.stopped_at
|
426
|
+
end
|
427
|
+
|
428
|
+
def winner
|
429
|
+
@winner ||= self.class.winner
|
430
|
+
end
|
431
|
+
|
432
|
+
def scheduled?
|
433
|
+
started_at && started_at > Time.now
|
434
|
+
end
|
435
|
+
|
436
|
+
def started?
|
437
|
+
started_at && started_at <= Time.now
|
438
|
+
end
|
439
|
+
|
440
|
+
def paused?
|
441
|
+
paused_at && paused_at <= Time.now
|
442
|
+
end
|
443
|
+
|
444
|
+
def stopped?
|
445
|
+
stopped_at && stopped_at <= Time.now
|
446
|
+
end
|
447
|
+
|
448
|
+
def running?
|
449
|
+
started? && !paused? && !stopped?
|
450
|
+
end
|
451
|
+
|
452
|
+
def calibrating?
|
453
|
+
enable_calibration? && start_manually? && !started?
|
454
|
+
end
|
455
|
+
|
456
|
+
def winner?
|
457
|
+
return @has_winner unless @has_winner.nil?
|
458
|
+
@has_winner = self.class.winner?
|
459
|
+
end
|
460
|
+
|
325
461
|
end
|
326
462
|
end
|
327
463
|
end
|
@@ -2,17 +2,18 @@ module TrailGuide
|
|
2
2
|
module Experiments
|
3
3
|
class Config < Canfig::Config
|
4
4
|
DEFAULT_KEYS = [
|
5
|
-
:name, :summary, :preview_url, :algorithm, :
|
6
|
-
:start_manually, :reset_manually, :
|
7
|
-
:
|
8
|
-
:
|
9
|
-
:can_resume
|
5
|
+
:name, :summary, :preview_url, :algorithm, :groups, :variants, :goals,
|
6
|
+
:start_manually, :reset_manually, :store_participation, :store_override,
|
7
|
+
:track_override, :combined, :allow_multiple_conversions,
|
8
|
+
:allow_multiple_goals, :track_winner_conversions, :skip_request_filter,
|
9
|
+
:target_sample_size, :can_resume, :enable_calibration
|
10
10
|
].freeze
|
11
11
|
|
12
12
|
CALLBACK_KEYS = [
|
13
|
-
:on_start, :on_stop, :on_pause, :on_resume, :on_winner,
|
14
|
-
:on_delete, :on_choose, :on_use, :on_convert,
|
15
|
-
:allow_participation, :allow_conversion,
|
13
|
+
:on_start, :on_schedule, :on_stop, :on_pause, :on_resume, :on_winner,
|
14
|
+
:on_reset, :on_delete, :on_choose, :on_use, :on_convert,
|
15
|
+
:on_redis_failover, :allow_participation, :allow_conversion,
|
16
|
+
:rollout_winner
|
16
17
|
].freeze
|
17
18
|
|
18
19
|
def default_config
|
@@ -57,6 +58,10 @@ module TrailGuide
|
|
57
58
|
!!reset_manually
|
58
59
|
end
|
59
60
|
|
61
|
+
def store_participation?
|
62
|
+
!!store_participation
|
63
|
+
end
|
64
|
+
|
60
65
|
def allow_multiple_conversions?
|
61
66
|
!!allow_multiple_conversions
|
62
67
|
end
|
@@ -77,12 +82,37 @@ module TrailGuide
|
|
77
82
|
!!can_resume
|
78
83
|
end
|
79
84
|
|
85
|
+
def enable_calibration?
|
86
|
+
!!enable_calibration
|
87
|
+
end
|
88
|
+
|
80
89
|
def name
|
81
90
|
@name ||= (self[:name] || experiment.name).try(:to_s).try(:underscore).try(:to_sym)
|
82
91
|
end
|
83
92
|
|
84
|
-
def
|
85
|
-
|
93
|
+
def groups(*grps)
|
94
|
+
self[:groups] ||= []
|
95
|
+
unless grps.empty?
|
96
|
+
self[:groups] = self[:groups].concat([grps].flatten.map { |g| g.to_s.underscore.to_sym })
|
97
|
+
end
|
98
|
+
self[:groups]
|
99
|
+
end
|
100
|
+
|
101
|
+
def groups=(*grps)
|
102
|
+
self[:groups] = [grps].flatten.map { |g| g.to_s.underscore.to_sym }
|
103
|
+
end
|
104
|
+
|
105
|
+
def group(grp=nil)
|
106
|
+
unless grp.nil?
|
107
|
+
groups << grp.to_s.underscore.to_sym
|
108
|
+
return groups.last
|
109
|
+
end
|
110
|
+
groups.first
|
111
|
+
end
|
112
|
+
|
113
|
+
def group=(grp)
|
114
|
+
groups.unshift(grp.to_s.underscore.to_sym)
|
115
|
+
groups.first
|
86
116
|
end
|
87
117
|
|
88
118
|
def algorithm
|
@@ -117,16 +147,50 @@ module TrailGuide
|
|
117
147
|
variant
|
118
148
|
end
|
119
149
|
|
120
|
-
def goal(name)
|
121
|
-
goals << name
|
150
|
+
def goal(name, **config, &block)
|
151
|
+
goals << Metrics::Goal.new(experiment, name, **config, &block)
|
122
152
|
end
|
123
153
|
alias_method :funnel, :goal
|
124
154
|
|
125
|
-
def
|
155
|
+
def goal=(name)
|
156
|
+
goals << Metrics::Goal.new(experiment, name)
|
157
|
+
end
|
158
|
+
alias_method :funnel=, :goal=
|
159
|
+
|
160
|
+
def goals=(*names)
|
161
|
+
self[:goals] = [names].flatten.map { |g| Metrics::Goal.new(experiment, g) }
|
162
|
+
end
|
163
|
+
alias_method :funnels=, :goals=
|
164
|
+
|
165
|
+
def goals(*names, **config, &block)
|
166
|
+
self[:goals] ||= []
|
167
|
+
unless names.empty?
|
168
|
+
self[:goals] = self[:goals].concat([names].flatten.map { |g| Metrics::Goal.new(experiment, g, **config, &block) })
|
169
|
+
end
|
126
170
|
self[:goals]
|
127
171
|
end
|
128
172
|
alias_method :funnels, :goals
|
129
173
|
|
174
|
+
def metric(name, **config, &block)
|
175
|
+
group(name)
|
176
|
+
goal(name, **config, &block)
|
177
|
+
end
|
178
|
+
|
179
|
+
def metric=(name)
|
180
|
+
self.group = name
|
181
|
+
self.goal = name
|
182
|
+
end
|
183
|
+
|
184
|
+
def metrics(*names, **config, &block)
|
185
|
+
groups(*names)
|
186
|
+
goals(*names, **config, &block)
|
187
|
+
end
|
188
|
+
|
189
|
+
def metrics=(*names)
|
190
|
+
self.groups = *names
|
191
|
+
self.goals = *names
|
192
|
+
end
|
193
|
+
|
130
194
|
def combined?
|
131
195
|
!combined.empty?
|
132
196
|
end
|
@@ -159,6 +223,11 @@ module TrailGuide
|
|
159
223
|
self[:on_start] << (meth || block)
|
160
224
|
end
|
161
225
|
|
226
|
+
def on_schedule(meth=nil, &block)
|
227
|
+
self[:on_schedule] ||= []
|
228
|
+
self[:on_schedule] << (meth || block)
|
229
|
+
end
|
230
|
+
|
162
231
|
def on_stop(meth=nil, &block)
|
163
232
|
self[:on_stop] ||= []
|
164
233
|
self[:on_stop] << (meth || block)
|
@@ -12,14 +12,34 @@ module TrailGuide
|
|
12
12
|
@participating ||= variant.present?
|
13
13
|
end
|
14
14
|
|
15
|
+
def participating!(variant)
|
16
|
+
@participating = true
|
17
|
+
@variant = variant
|
18
|
+
participant.participating!(variant) if experiment.configuration.store_participation?
|
19
|
+
end
|
20
|
+
|
15
21
|
def converted?(checkpoint=nil)
|
16
|
-
@converted ||=
|
22
|
+
@converted ||= {}
|
23
|
+
@converted[checkpoint || :converted] ||= participant.converted?(experiment, checkpoint)
|
24
|
+
end
|
25
|
+
|
26
|
+
def converted!(variant, checkpoint, reset: false)
|
27
|
+
@converted ||= {}
|
28
|
+
@converted[checkpoint || :converted] ||= true
|
29
|
+
participant.converted!(variant, checkpoint, reset: reset)
|
17
30
|
end
|
18
31
|
|
19
32
|
def variant
|
20
33
|
@variant ||= participant.variant(experiment)
|
21
34
|
end
|
22
35
|
|
36
|
+
def exit!
|
37
|
+
@participating = nil
|
38
|
+
@converted = nil
|
39
|
+
@variant = nil
|
40
|
+
participant.exit!(experiment)
|
41
|
+
end
|
42
|
+
|
23
43
|
def method_missing(meth, *args, &block)
|
24
44
|
return participant.send(meth, *args, &block) if participant.respond_to?(meth, true)
|
25
45
|
super
|