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.
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