trailguide 0.1.31 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -8
  3. data/app/assets/javascripts/trail_guide/admin/application.js +20 -1
  4. data/app/assets/stylesheets/trail_guide/admin/application.css +22 -0
  5. data/app/assets/stylesheets/trail_guide/admin/experiments.css +36 -3
  6. data/app/controllers/trail_guide/admin/application_controller.rb +59 -8
  7. data/app/controllers/trail_guide/admin/experiments_controller.rb +209 -16
  8. data/app/controllers/trail_guide/admin/groups_controller.rb +34 -0
  9. data/app/controllers/trail_guide/experiments_controller.rb +1 -1
  10. data/app/views/layouts/trail_guide/admin/_calculator.erb +24 -0
  11. data/app/views/layouts/trail_guide/admin/_footer.html.erb +27 -0
  12. data/app/views/layouts/trail_guide/admin/_header.html.erb +147 -0
  13. data/app/views/layouts/trail_guide/admin/_import_modal.html.erb +45 -0
  14. data/app/views/layouts/trail_guide/admin/application.html.erb +17 -3
  15. data/app/views/trail_guide/admin/experiments/_alert_peek.html.erb +19 -0
  16. data/app/views/trail_guide/admin/experiments/_alert_state.html.erb +49 -0
  17. data/app/views/trail_guide/admin/experiments/_btn_analyze.html.erb +11 -0
  18. data/app/views/trail_guide/admin/experiments/_btn_analyze_goal.html.erb +5 -0
  19. data/app/views/trail_guide/admin/experiments/_btn_convert.html.erb +33 -0
  20. data/app/views/trail_guide/admin/experiments/_btn_enroll.html.erb +3 -0
  21. data/app/views/trail_guide/admin/experiments/_btn_join.html.erb +11 -0
  22. data/app/views/trail_guide/admin/experiments/_btn_leave.html.erb +7 -0
  23. data/app/views/trail_guide/admin/experiments/_btn_pause.html.erb +5 -0
  24. data/app/views/trail_guide/admin/experiments/_btn_peek.html.erb +13 -0
  25. data/app/views/trail_guide/admin/experiments/_btn_reset.html.erb +5 -0
  26. data/app/views/trail_guide/admin/experiments/_btn_restart.html.erb +5 -0
  27. data/app/views/trail_guide/admin/experiments/_btn_resume.html.erb +5 -0
  28. data/app/views/trail_guide/admin/experiments/_btn_schedule.html.erb +6 -0
  29. data/app/views/trail_guide/admin/experiments/_btn_start.html.erb +5 -0
  30. data/app/views/trail_guide/admin/experiments/_btn_stop.html.erb +5 -0
  31. data/app/views/trail_guide/admin/experiments/_experiment.html.erb +68 -172
  32. data/app/views/trail_guide/admin/experiments/_header.html.erb +87 -0
  33. data/app/views/trail_guide/admin/experiments/_start_modal.html.erb +57 -0
  34. data/app/views/trail_guide/admin/experiments/_tbody.html.erb +112 -0
  35. data/app/views/trail_guide/admin/experiments/_thead.html.erb +38 -0
  36. data/app/views/trail_guide/admin/experiments/index.html.erb +8 -16
  37. data/app/views/trail_guide/admin/experiments/show.html.erb +78 -0
  38. data/app/views/trail_guide/admin/groups/index.html.erb +40 -0
  39. data/app/views/trail_guide/admin/groups/show.html.erb +9 -0
  40. data/app/views/trail_guide/admin/orphans/_alert.html.erb +44 -0
  41. data/config/initializers/trailguide.rb +72 -9
  42. data/config/routes/admin.rb +19 -12
  43. data/lib/trail_guide/admin/engine.rb +2 -0
  44. data/lib/trail_guide/admin.rb +2 -0
  45. data/lib/trail_guide/calculators/bayesian.rb +98 -0
  46. data/lib/trail_guide/calculators/calculator.rb +58 -0
  47. data/lib/trail_guide/calculators/score.rb +68 -0
  48. data/lib/trail_guide/calculators.rb +8 -0
  49. data/lib/trail_guide/catalog.rb +134 -19
  50. data/lib/trail_guide/combined_experiment.rb +8 -3
  51. data/lib/trail_guide/config.rb +6 -1
  52. data/lib/trail_guide/engine.rb +2 -0
  53. data/lib/trail_guide/errors.rb +30 -1
  54. data/lib/trail_guide/experiment.rb +0 -1
  55. data/lib/trail_guide/experiments/base.rb +189 -53
  56. data/lib/trail_guide/experiments/config.rb +82 -13
  57. data/lib/trail_guide/experiments/participant.rb +21 -1
  58. data/lib/trail_guide/helper.rb +59 -32
  59. data/lib/trail_guide/metrics/checkpoint.rb +24 -0
  60. data/lib/trail_guide/metrics/config.rb +45 -0
  61. data/lib/trail_guide/metrics/funnel.rb +24 -0
  62. data/lib/trail_guide/metrics/goal.rb +89 -0
  63. data/lib/trail_guide/metrics.rb +9 -0
  64. data/lib/trail_guide/participant.rb +54 -21
  65. data/lib/trail_guide/variant.rb +34 -12
  66. data/lib/trail_guide/version.rb +2 -2
  67. data/lib/trailguide.rb +4 -0
  68. metadata +112 -7
  69. data/app/views/layouts/trail_guide/admin/_footer.erb +0 -10
  70. data/app/views/layouts/trail_guide/admin/_header.erb +0 -42
  71. data/app/views/trail_guide/admin/experiments/_combined_experiment.html.erb +0 -189
  72. 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 :metric, :algorithm, :control, :goals, :callbacks, :combined,
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
- started_at && started_at <= Time.now
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
- paused_at && paused_at <= Time.now
123
+ time = paused_at
124
+ time && time <= Time.now
97
125
  end
98
126
 
99
127
  def stopped?
100
- stopped_at && stopped_at <= Time.now
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
- TrailGuide.redis.hexists(storage_key, 'winner')
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
- configuration: {
171
- metric: metric,
172
- algorithm: algorithm.name,
173
- variants: variants.as_json,
174
- goals: goals.as_json,
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, :running?, :started?, :started_at, :start!,
196
- :start_manually?, :reset_manually?, :winner?, :allow_multiple_conversions?,
197
- :allow_multiple_goals?, :track_winner_conversions?, :callbacks, to: :class
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 winner
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 = winner
237
- variant.increment_participation! if track_winner_conversions?
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
- return control if !started? && configuration.start_manually
243
- start! unless started?
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
- variant_chosen!(variant, metadata: metadata)
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
- return false if !running? || (winner? && !track_winner_conversions?)
272
- return false unless participant.participating?
273
- raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.present? || goals.empty?
274
- raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.nil? || goals.any? { |goal| goal == checkpoint.to_s.underscore.to_sym }
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
- variant = participant.variant
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
- run_callbacks(:on_convert, variant, checkpoint, metadata)
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
- return true if callbacks[:allow_conversion].empty?
301
- run_callbacks(:allow_conversion, checkpoint, metadata)
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, :metric, :variants, :goals,
6
- :start_manually, :reset_manually, :store_override, :track_override,
7
- :combined, :allow_multiple_conversions, :allow_multiple_goals,
8
- :track_winner_conversions, :skip_request_filter, :target_sample_size,
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, :on_reset,
14
- :on_delete, :on_choose, :on_use, :on_convert, :on_redis_failover,
15
- :allow_participation, :allow_conversion, :rollout_winner
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 metric
85
- @metric ||= (self[:metric] || name).try(:to_s).try(:underscore).try(:to_sym)
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.to_s.underscore.to_sym
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 goals
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 ||= participant.converted?(experiment, checkpoint)
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