Class: TrailGuide::Experiment
- Inherits:
-
Object
- Object
- TrailGuide::Experiment
- Defined in:
- lib/trail_guide/experiment.rb
Instance Attribute Summary collapse
-
#participant ⇒ Object
readonly
Returns the value of attribute participant.
Class Method Summary collapse
- .algorithm(algo = nil) ⇒ Object
- .allow_multiple_conversions(allow) ⇒ Object
- .allow_multiple_conversions? ⇒ Boolean
- .allow_multiple_goals(allow) ⇒ Object
- .allow_multiple_goals? ⇒ Boolean
- .as_json(opts = {}) ⇒ Object
- .callbacks ⇒ Object
- .config_algorithm ⇒ Object
- .control(name = nil) ⇒ Object
- .declare_winner!(variant) ⇒ Object
- .delete! ⇒ Object
-
.experiment_name(name = nil) ⇒ Object
TODO could probably move all this configuration stuff at the class level into a canfig object instead…?.
- .funnel(name) ⇒ Object (also: goal)
- .funnels(arr = nil) ⇒ Object (also: goals)
- .inherited(child) ⇒ Object
- .metric(key = nil) ⇒ Object
- .on_choose(meth = nil, &block) ⇒ Object
- .on_convert(meth = nil, &block) ⇒ Object
- .on_delete(meth = nil, &block) ⇒ Object
- .on_reset(meth = nil, &block) ⇒ Object
- .on_start(meth = nil, &block) ⇒ Object
- .on_stop(meth = nil, &block) ⇒ Object
- .on_use(meth = nil, &block) ⇒ Object
- .persisted? ⇒ Boolean
- .reset! ⇒ Object
- .resettable(reset) ⇒ Object
- .resettable? ⇒ Boolean
- .run_callbacks(hook, *args) ⇒ Object
- .save! ⇒ Object
- .start! ⇒ Object
- .started? ⇒ Boolean
- .started_at ⇒ Object
- .stop! ⇒ Object
- .storage_key ⇒ Object
- .variant(name, metadata: {}, weight: 1, control: false) ⇒ Object
- .variants(include_control = true) ⇒ Object
- .winner ⇒ Object
- .winner? ⇒ Boolean
Instance Method Summary collapse
- #algorithm ⇒ Object
- #choose!(metadata: nil, **opts) ⇒ Object
- #choose_variant!(override: nil, excluded: false, metadata: nil) ⇒ Object
- #convert!(checkpoint = nil, metadata: nil) ⇒ Object
- #converted?(checkpoint = nil) ⇒ Boolean
-
#initialize(participant) ⇒ Experiment
constructor
A new instance of Experiment.
- #participating? ⇒ Boolean
- #run_callbacks(hook, *args) ⇒ Object
Constructor Details
#initialize(participant) ⇒ Experiment
250 251 252 |
# File 'lib/trail_guide/experiment.rb', line 250 def initialize(participant) @participant = participant end |
Instance Attribute Details
#participant ⇒ Object (readonly)
Returns the value of attribute participant.
244 245 246 |
# File 'lib/trail_guide/experiment.rb', line 244 def participant @participant end |
Class Method Details
.algorithm(algo = nil) ⇒ Object
33 34 35 36 |
# File 'lib/trail_guide/experiment.rb', line 33 def algorithm(algo=nil) @algorithm = TrailGuide::Algorithms.algorithm(algo) unless algo.nil? @algorithm ||= TrailGuide::Algorithms.algorithm(TrailGuide.configuration.algorithm) end |
.allow_multiple_conversions(allow) ⇒ Object
100 101 102 |
# File 'lib/trail_guide/experiment.rb', line 100 def allow_multiple_conversions(allow) @allow_multiple_conversions = allow end |
.allow_multiple_conversions? ⇒ Boolean
104 105 106 |
# File 'lib/trail_guide/experiment.rb', line 104 def allow_multiple_conversions? !!@allow_multiple_conversions end |
.allow_multiple_goals(allow) ⇒ Object
108 109 110 |
# File 'lib/trail_guide/experiment.rb', line 108 def allow_multiple_goals(allow) @allow_multiple_goals = allow end |
.allow_multiple_goals? ⇒ Boolean
112 113 114 |
# File 'lib/trail_guide/experiment.rb', line 112 def allow_multiple_goals? !!@allow_multiple_goals end |
.as_json(opts = {}) ⇒ Object
230 231 232 233 234 235 236 237 |
# File 'lib/trail_guide/experiment.rb', line 230 def as_json(opts={}) # TODO fill in the rest of the values i've added { experiment_name: experiment_name, algorithm: algorithm, variants: variants.as_json } end |
.callbacks ⇒ Object
116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/trail_guide/experiment.rb', line 116 def callbacks @callbacks ||= begin callbacks = { on_choose: [TrailGuide.configuration.on_experiment_choose].compact, on_use: [TrailGuide.configuration.on_experiment_use].compact, on_convert: [TrailGuide.configuration.on_experiment_convert].compact, on_start: [TrailGuide.configuration.on_experiment_start].compact, on_stop: [TrailGuide.configuration.on_experiment_stop].compact, on_reset: [TrailGuide.configuration.on_experiment_reset].compact, on_delete: [TrailGuide.configuration.on_experiment_delete].compact, } end end |
.config_algorithm ⇒ Object
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/trail_guide/experiment.rb', line 16 def config_algorithm config_algo = TrailGuide.configuration.algorithm case config_algo when :weighted config_algo = TrailGuide::Algorithms::Weighted when :bandit config_algo = TrailGuide::Algorithms::Bandit when :distributed config_algo = TrailGuide::Algorithms::Distributed when :random config_algo = TrailGuide::Algorithms::Random else config_algo = config_algo.constantize if config_algo.is_a?(String) end config_algo end |
.control(name = nil) ⇒ Object
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/trail_guide/experiment.rb', line 67 def control(name=nil) return variants.find { |var| var.control? } || variants.first if name.nil? variants.each(&:variant!) var_idx = variants.index { |var| var == name } if var_idx.nil? variant = Variant.new(self, name, control: true) else variant = variants.slice!(var_idx, 1)[0] variant.control! end variants.unshift(variant) return variant end |
.declare_winner!(variant) ⇒ Object
194 195 196 197 |
# File 'lib/trail_guide/experiment.rb', line 194 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
217 218 219 220 221 222 |
# File 'lib/trail_guide/experiment.rb', line 217 def delete! variants.each(&:delete!) deleted = TrailGuide.redis.del(storage_key) run_callbacks(:on_delete) deleted end |
.experiment_name(name = nil) ⇒ Object
TODO could probably move all this configuration stuff at the class level into a canfig object instead…?
11 12 13 14 |
# File 'lib/trail_guide/experiment.rb', line 11 def experiment_name(name=nil) @experiment_name = name.to_s.underscore.to_sym unless name.nil? @experiment_name || self.name.try(:underscore).try(:to_sym) end |
.funnel(name) ⇒ Object Also known as: goal
84 85 86 |
# File 'lib/trail_guide/experiment.rb', line 84 def funnel(name) funnels << name.to_s.underscore.to_sym end |
.funnels(arr = nil) ⇒ Object Also known as: goals
89 90 91 92 |
# File 'lib/trail_guide/experiment.rb', line 89 def funnels(arr=nil) @funnels = arr unless arr.nil? @funnels ||= [] end |
.inherited(child) ⇒ Object
4 5 6 7 |
# File 'lib/trail_guide/experiment.rb', line 4 def inherited(child) # TODO allow inheriting algo, variants, goals, metrics, etc. TrailGuide::Catalog.register(child) end |
.metric(key = nil) ⇒ Object
95 96 97 98 |
# File 'lib/trail_guide/experiment.rb', line 95 def metric(key=nil) @metric = key.to_s.underscore.to_sym unless key.nil? @metric ||= experiment_name end |
.on_choose(meth = nil, &block) ⇒ Object
130 131 132 |
# File 'lib/trail_guide/experiment.rb', line 130 def on_choose(meth=nil, &block) callbacks[:on_choose] << (meth || block) end |
.on_convert(meth = nil, &block) ⇒ Object
138 139 140 |
# File 'lib/trail_guide/experiment.rb', line 138 def on_convert(meth=nil, &block) callbacks[:on_convert] << (meth || block) end |
.on_delete(meth = nil, &block) ⇒ Object
154 155 156 |
# File 'lib/trail_guide/experiment.rb', line 154 def on_delete(meth=nil, &block) callbacks[:on_delete] << (meth || block) end |
.on_reset(meth = nil, &block) ⇒ Object
150 151 152 |
# File 'lib/trail_guide/experiment.rb', line 150 def on_reset(meth=nil, &block) callbacks[:on_reset] << (meth || block) end |
.on_start(meth = nil, &block) ⇒ Object
142 143 144 |
# File 'lib/trail_guide/experiment.rb', line 142 def on_start(meth=nil, &block) callbacks[:on_start] << (meth || block) end |
.on_stop(meth = nil, &block) ⇒ Object
146 147 148 |
# File 'lib/trail_guide/experiment.rb', line 146 def on_stop(meth=nil, &block) callbacks[:on_stop] << (meth || block) end |
.on_use(meth = nil, &block) ⇒ Object
134 135 136 |
# File 'lib/trail_guide/experiment.rb', line 134 def on_use(meth=nil, &block) callbacks[:on_use] << (meth || block) end |
.persisted? ⇒ Boolean
208 209 210 |
# File 'lib/trail_guide/experiment.rb', line 208 def persisted? TrailGuide.redis.exists(storage_key) end |
.reset! ⇒ Object
224 225 226 227 228 |
# File 'lib/trail_guide/experiment.rb', line 224 def reset! reset = (delete! && save!) run_callbacks(:on_reset) reset end |
.resettable(reset) ⇒ Object
38 39 40 |
# File 'lib/trail_guide/experiment.rb', line 38 def resettable(reset) @resettable = reset end |
.resettable? ⇒ Boolean
42 43 44 45 46 47 48 |
# File 'lib/trail_guide/experiment.rb', line 42 def resettable? if @resettable.nil? !TrailGuide.configuration.reset_manually else !!@resettable end end |
.run_callbacks(hook, *args) ⇒ Object
158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/trail_guide/experiment.rb', line 158 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
212 213 214 215 |
# File 'lib/trail_guide/experiment.rb', line 212 def save! variants.each(&:save!) TrailGuide.redis.hsetnx(storage_key, 'name', experiment_name) end |
.start! ⇒ Object
170 171 172 173 174 175 176 |
# File 'lib/trail_guide/experiment.rb', line 170 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
190 191 192 |
# File 'lib/trail_guide/experiment.rb', line 190 def started? !!started_at end |
.started_at ⇒ Object
185 186 187 188 |
# File 'lib/trail_guide/experiment.rb', line 185 def started_at started = TrailGuide.redis.hget(storage_key, 'started_at') return Time.at(started.to_i) if started end |
.stop! ⇒ Object
178 179 180 181 182 183 |
# File 'lib/trail_guide/experiment.rb', line 178 def stop! return false unless started? stopped = TrailGuide.redis.hdel(storage_key, 'started_at') run_callbacks(:on_stop) stopped end |
.storage_key ⇒ Object
239 240 241 |
# File 'lib/trail_guide/experiment.rb', line 239 def storage_key experiment_name end |
.variant(name, metadata: {}, weight: 1, control: false) ⇒ Object
50 51 52 53 54 55 56 |
# File 'lib/trail_guide/experiment.rb', line 50 def variant(name, metadata: {}, weight: 1, control: false) raise ArgumentError, "The variant #{name} already exists in experiment #{experiment_name}" if variants.any? { |var| var == name } control = true if variants.empty? variant = Variant.new(self, name, metadata: , weight: weight, control: control) variants << variant variant end |
.variants(include_control = true) ⇒ Object
58 59 60 61 62 63 64 65 |
# File 'lib/trail_guide/experiment.rb', line 58 def variants(include_control=true) @variants ||= [] if include_control @variants else @variants.select { |var| !var.control? } end end |
.winner ⇒ Object
199 200 201 202 |
# File 'lib/trail_guide/experiment.rb', line 199 def winner winner = TrailGuide.redis.hget(storage_key, 'winner') return variants.find { |var| var == winner } if winner end |
.winner? ⇒ Boolean
204 205 206 |
# File 'lib/trail_guide/experiment.rb', line 204 def winner? !!winner end |
Instance Method Details
#algorithm ⇒ Object
254 255 256 |
# File 'lib/trail_guide/experiment.rb', line 254 def algorithm @algorithm ||= self.class.algorithm.new(self) end |
#choose!(metadata: nil, **opts) ⇒ Object
258 259 260 261 262 263 264 |
# File 'lib/trail_guide/experiment.rb', line 258 def choose!(metadata: nil, **opts) return control if TrailGuide.configuration.disabled variant = choose_variant!(metadata: , **opts) run_callbacks(:on_use, variant, ) variant end |
#choose_variant!(override: nil, excluded: false, metadata: nil) ⇒ Object
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/trail_guide/experiment.rb', line 266 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 TrailGuide.configuration.store_override && started? else return winner if winner? return control if excluded return control if !started? && TrailGuide.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 participant.participating!(variant) variant.increment_participation! run_callbacks(:on_choose, variant, ) variant end |
#convert!(checkpoint = nil, metadata: nil) ⇒ Object
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/trail_guide/experiment.rb', line 288 def convert!(checkpoint=nil, metadata: nil) return false unless participating? raise ArgumentError, "You must provide a valid goal checkpoint for #{experiment_name}" unless checkpoint.present? || funnels.empty? raise ArgumentError, "Unknown goal checkpoint: #{checkpoint}" 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
311 312 313 |
# File 'lib/trail_guide/experiment.rb', line 311 def converted?(checkpoint=nil) participant.converted?(self, checkpoint) end |
#participating? ⇒ Boolean
307 308 309 |
# File 'lib/trail_guide/experiment.rb', line 307 def participating? participant.participating?(self) end |
#run_callbacks(hook, *args) ⇒ Object
315 316 317 318 319 320 321 322 323 324 325 |
# File 'lib/trail_guide/experiment.rb', line 315 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 |