Class: TrailGuide::Experiment

Inherits:
Object
  • Object
show all
Defined in:
lib/trail_guide/experiment.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(participant) ⇒ Experiment

Returns a new instance of Experiment.



140
141
142
# File 'lib/trail_guide/experiment.rb', line 140

def initialize(participant)
  @participant = participant
end

Instance Attribute Details

#participantObject (readonly)

Returns the value of attribute participant.



134
135
136
# File 'lib/trail_guide/experiment.rb', line 134

def participant
  @participant
end

Class Method Details

.as_json(opts = {}) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/trail_guide/experiment.rb', line 110

def as_json(opts={})
  { experiment_name => {
    configuration: {
      metric: metric,
      algorithm: algorithm.name,
      variants: variants.as_json,
      goals: goals.as_json,
      resettable: resettable?,
      allow_multiple_conversions: allow_multiple_conversions?,
      allow_multiple_goals: allow_multiple_goals?
    },
    statistics: {
      # TODO expand on this for variants/goals
      participants: variants.sum(&:participants),
      converted: variants.sum(&:converted)
    }
  } }
end

.configurationObject



14
15
16
# File 'lib/trail_guide/experiment.rb', line 14

def configuration
  @configuration ||= ExperimentConfig.new(self)
end

.configure(*args, &block) ⇒ Object



18
19
20
# File 'lib/trail_guide/experiment.rb', line 18

def configure(*args, &block)
  configuration.configure(*args, &block)
end

.declare_winner!(variant) ⇒ Object



74
75
76
77
# File 'lib/trail_guide/experiment.rb', line 74

def declare_winner!(variant)
  variant = variant.name if variant.is_a?(Variant)
  TrailGuide.redis.hset(storage_key, 'winner', variant.to_s.underscore)
end

.delete!Object



97
98
99
100
101
102
# File 'lib/trail_guide/experiment.rb', line 97

def delete!
  variants.each(&:delete!)
  deleted = TrailGuide.redis.del(storage_key)
  run_callbacks(:on_delete)
  deleted
end

.experiment_nameObject



26
27
28
# File 'lib/trail_guide/experiment.rb', line 26

def experiment_name
  configuration.name
end

.inherited(child) ⇒ Object



10
11
12
# File 'lib/trail_guide/experiment.rb', line 10

def inherited(child)
  TrailGuide::Catalog.register(child)
end

.persisted?Boolean

Returns:

  • (Boolean)


88
89
90
# File 'lib/trail_guide/experiment.rb', line 88

def persisted?
  TrailGuide.redis.exists(storage_key)
end

.reset!Object



104
105
106
107
108
# File 'lib/trail_guide/experiment.rb', line 104

def reset!
  reset = (delete! && save!)
  run_callbacks(:on_reset)
  reset
end

.resettable?Boolean

Returns:

  • (Boolean)


22
23
24
# File 'lib/trail_guide/experiment.rb', line 22

def resettable?
  !configuration.reset_manually
end

.run_callbacks(hook, *args) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
# File 'lib/trail_guide/experiment.rb', line 38

def run_callbacks(hook, *args)
  return unless callbacks[hook]
  args.unshift(self)
  callbacks[hook].each do |callback|
    if callback.respond_to?(:call)
      callback.call(*args)
    else
      send(callback, *args)
    end
  end
end

.save!Object



92
93
94
95
# File 'lib/trail_guide/experiment.rb', line 92

def save!
  variants.each(&:save!)
  TrailGuide.redis.hsetnx(storage_key, 'name', experiment_name)
end

.start!Object



50
51
52
53
54
55
56
# File 'lib/trail_guide/experiment.rb', line 50

def start!
  return false if started?
  save! unless persisted?
  started = TrailGuide.redis.hset(storage_key, 'started_at', Time.now.to_i)
  run_callbacks(:on_start)
  started
end

.started?Boolean

Returns:

  • (Boolean)


70
71
72
# File 'lib/trail_guide/experiment.rb', line 70

def started?
  !!started_at
end

.started_atObject



65
66
67
68
# File 'lib/trail_guide/experiment.rb', line 65

def started_at
  started = TrailGuide.redis.hget(storage_key, 'started_at')
  return Time.at(started.to_i) if started
end

.stop!Object



58
59
60
61
62
63
# File 'lib/trail_guide/experiment.rb', line 58

def stop!
  return false unless started?
  stopped = TrailGuide.redis.hdel(storage_key, 'started_at')
  run_callbacks(:on_stop)
  stopped
end

.storage_keyObject



129
130
131
# File 'lib/trail_guide/experiment.rb', line 129

def storage_key
  experiment_name
end

.variants(include_control = true) ⇒ Object



30
31
32
33
34
35
36
# File 'lib/trail_guide/experiment.rb', line 30

def variants(include_control=true)
  if include_control
    configuration.variants
  else
    configuration.variants.select { |var| !var.control? }
  end
end

.winnerObject



79
80
81
82
# File 'lib/trail_guide/experiment.rb', line 79

def winner
  winner = TrailGuide.redis.hget(storage_key, 'winner')
  return variants.find { |var| var == winner } if winner
end

.winner?Boolean

Returns:

  • (Boolean)


84
85
86
# File 'lib/trail_guide/experiment.rb', line 84

def winner?
  TrailGuide.redis.hexists(storage_key, 'winner')
end

Instance Method Details

#algorithmObject



144
145
146
# File 'lib/trail_guide/experiment.rb', line 144

def algorithm
  @algorithm ||= self.class.algorithm.new(self)
end

#choose!(override: nil, metadata: nil, **opts) ⇒ Object



148
149
150
151
152
153
154
155
# File 'lib/trail_guide/experiment.rb', line 148

def choose!(override: nil, metadata: nil, **opts)
  return control if TrailGuide.configuration.disabled

  variant = choose_variant!(override: override, metadata: , **opts)
  participant.participating!(variant) unless override.present? && !configuration.store_override
  run_callbacks(:on_use, variant, )
  variant
end

#choose_variant!(override: nil, excluded: false, metadata: nil) ⇒ Object



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/trail_guide/experiment.rb', line 157

def choose_variant!(override: nil, excluded: false, metadata: nil)
  return control if TrailGuide.configuration.disabled
  if override.present?
    variant = variants.find { |var| var == override }
    return variant unless configuration.track_override && started?
  else
    return winner if winner?
    return control if excluded
    return control if !started? && configuration.start_manually
    start! unless started?
    return variants.find { |var| var == participant[storage_key] } if participating?
    return control unless TrailGuide.configuration.allow_multiple_experiments == true || !participant.participating_in_active_experiments?(TrailGuide.configuration.allow_multiple_experiments == false) 

    variant = algorithm.choose!(metadata: )
  end

  variant.increment_participation!
  run_callbacks(:on_choose, variant, )
  variant
end

#convert!(checkpoint = nil, metadata: nil) ⇒ Object

Raises:



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/trail_guide/experiment.rb', line 178

def convert!(checkpoint=nil, metadata: nil)
  return false unless participating?
  raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.present? || funnels.empty?
  raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.nil? || funnels.any? { |funnel| funnel == checkpoint.to_s.underscore.to_sym }
  # TODO eventually allow progressing through funnel checkpoints towards goals
  if converted?(checkpoint)
    return false unless allow_multiple_conversions?
  elsif converted?
    return false unless allow_multiple_goals?
  end

  variant = variants.find { |var| var == participant[storage_key] }
  # TODO eventually only reset if we're at the final goal in a funnel
  participant.converted!(variant, checkpoint, reset: resettable?)
  variant.increment_conversion!(checkpoint)
  run_callbacks(:on_convert, variant, checkpoint, )
  variant
end

#converted?(checkpoint = nil) ⇒ Boolean

Returns:

  • (Boolean)


201
202
203
# File 'lib/trail_guide/experiment.rb', line 201

def converted?(checkpoint=nil)
  participant.converted?(self, checkpoint)
end

#participating?Boolean

Returns:

  • (Boolean)


197
198
199
# File 'lib/trail_guide/experiment.rb', line 197

def participating?
  participant.participating?(self)
end

#run_callbacks(hook, *args) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
# File 'lib/trail_guide/experiment.rb', line 205

def run_callbacks(hook, *args)
  return unless callbacks[hook]
  args.unshift(self)
  callbacks[hook].each do |callback|
    if callback.respond_to?(:call)
      callback.call(*args)
    else
      send(callback, *args)
    end
  end
end