trailguide 0.1.31 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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
|