class TrailGuide::Catalog
Attributes
combined[R]
experiments[R]
Public Class Methods
catalog()
click to toggle source
# File lib/trail_guide/catalog.rb, line 6 def catalog @catalog ||= new end
combined_experiment(combined, name)
click to toggle source
# File lib/trail_guide/catalog.rb, line 70 def combined_experiment(combined, name) experiment = Class.new(TrailGuide::CombinedExperiment) experiment.configure combined.configuration.to_h.merge({ name: name.to_s.underscore.to_sym, parent: combined, combined: [], variants: combined.configuration.variants.map { |var| var.dup(experiment) }, goals: combined.configuration.goals.map { |goal| goal.dup(experiment) } }) experiment end
load_experiments!(configs: [], classes: [])
click to toggle source
# File lib/trail_guide/catalog.rb, line 10 def load_experiments!(configs: [], classes: []) @catalog = nil # Load experiments from YAML configs if any exists [configs].flatten.each do |path| Dir[Rails.root.join(path)].each { |f| load_yaml_experiments(f) if ['.yml', '.yaml'].include?(File.extname(f)) } end # Load experiments from ruby configs if any exist [configs].flatten.each do |path| Dir[Rails.root.join(path)].each { |f| DSL.instance_eval(File.read(f)) if File.extname(f) == '.rb' } end # Load any experiment classes defined in the app [classes].flatten.each do |path| Dir[Rails.root.join(path)].each { |f| load f } end end
load_yaml_experiments(file)
click to toggle source
# File lib/trail_guide/catalog.rb, line 29 def load_yaml_experiments(file) experiments = (YAML.load_file(file) || {} rescue {}) .symbolize_keys.map { |k,v| [k, v.symbolize_keys] }.to_h experiments.each do |name, options| expvars = options[:variants].map do |var| if var.is_a?(Array) [var[0], var[1].symbolize_keys] else [var] end end expgoals = options[:goals] # TODO is it worth parsing these out for complex funnels, etc.? or is # it better to just force use of the DSL? DSL.experiment(name) do |config| expvars.each do |expvar| variant *expvar end expgoals.each do |expgoal| goal expgoal end if expgoals.present? config.control = options[:control] if options[:control] config.groups = options[:groups] if options[:groups] config.algorithm = options[:algorithm] if options[:algorithm] config.combined = options[:combined] if options[:combined] config.reset_manually = options[:reset_manually] if options.key?(:reset_manually) config.start_manually = options[:start_manually] if options.key?(:start_manually) config.store_override = options[:store_override] if options.key?(:store_override) config.track_override = options[:track_override] if options.key?(:track_override) config.allow_multiple_conversions = options[:allow_multiple_conversions] if options.key?(:allow_multiple_conversions) config.allow_multiple_goals = options[:allow_multiple_goals] if options.key?(:allow_multiple_goals) # TODO need to remember to update this with all the new config vars end end end
new(experiments=[], combined=[])
click to toggle source
# File lib/trail_guide/catalog.rb, line 87 def initialize(experiments=[], combined=[]) @experiments = experiments @combined = combined end
Public Instance Methods
adopted(key)
click to toggle source
# File lib/trail_guide/catalog.rb, line 339 def adopted(key) TrailGuide.redis.del("orphans:#{key}") rescue Errno::ECONNREFUSED, Redis::BaseError, SocketError => e false end
all()
click to toggle source
# File lib/trail_guide/catalog.rb, line 107 def all exploded = experiments.map do |exp| if exp.combined? exp.combined.map { |name| combined_experiment(exp, name) } else exp end end.flatten new(exploded, @combined) end
by_started()
click to toggle source
# File lib/trail_guide/catalog.rb, line 155 def by_started scoped = to_a.sort do |a,b| # TODO finish implementing specs, then implement `experiment.fresh?`, then (maybe) re-work this all # into an experiment spaceship operator if !(a.started? || a.scheduled? || a.winner?) && !(b.started? || b.scheduled? || b.winner?) a.experiment_name.to_s <=> b.experiment_name.to_s elsif !(a.started? || a.scheduled? || a.winner?) -1 elsif !(b.started? || b.scheduled? || b.winner?) 1 else if a.winner? && !b.winner? 1 elsif !a.winner? && b.winner? -1 elsif a.winner? && b.winner? a.experiment_name.to_s <=> b.experiment_name.to_s elsif a.running? && !b.running? -1 elsif !a.running? && b.running? 1 elsif a.running? && b.running? if a.started_at == b.started_at a.experiment_name.to_s <=> b.experiment_name.to_s else a.started_at <=> b.started_at end elsif a.paused? && !b.paused? -1 elsif !a.paused? && b.paused? 1 elsif a.paused? && b.paused? if a.paused_at == b.paused_at a.experiment_name.to_s <=> b.experiment_name.to_s else a.paused_at <=> b.paused_at end elsif a.scheduled? && !b.scheduled? -1 elsif !a.scheduled? && b.scheduled? 1 elsif a.scheduled? && b.scheduled? if a.started_at == b.started_at a.experiment_name.to_s <=> b.experiment_name.to_s else a.started_at <=> b.started_at end elsif a.stopped? && !b.stopped? -1 # TODO remove unused case elsif !a.stopped? && b.stopped? 1 # TODO remove unused case elsif a.stopped? && b.stopped? if a.stopped_at == b.stopped_at a.experiment_name.to_s <=> b.experiment_name.to_s else a.stopped_at <=> b.stopped_at end else a.experiment_name.to_s <=> b.experiment_name.to_s end end end new(scoped, @combined) end
calibrating()
click to toggle source
# File lib/trail_guide/catalog.rb, line 119 def calibrating new(to_a.select(&:calibrating?), @combined) end
combined_experiment(exp, name)
click to toggle source
# File lib/trail_guide/catalog.rb, line 92 def combined_experiment(exp, name) combo = @combined.find do |cex| cex.experiment_name == name.to_s.underscore.to_sym && cex.parent.experiment_name == exp.experiment_name end return combo if combo.present? combo = self.class.combined_experiment(exp, name) @combined << combo combo end
deregister(key, remove_const=false)
click to toggle source
# File lib/trail_guide/catalog.rb, line 269 def deregister(key, remove_const=false) klass = find(key) return unless klass.present? experiments.delete(klass) return klass unless remove_const && klass.name.present? Object.send(:remove_const, :"#{klass.name}") return key end
ended()
click to toggle source
# File lib/trail_guide/catalog.rb, line 143 def ended new(to_a.select(&:winner?), @combined) end
export()
click to toggle source
# File lib/trail_guide/catalog.rb, line 278 def export map do |exp| if exp.combined? [exp.as_json].concat(exp.combined_experiments.map(&:as_json)) else exp.as_json end end.flatten.reduce({}) { |red,exp| red.merge!(exp) } end
find(name)
click to toggle source
# File lib/trail_guide/catalog.rb, line 221 def find(name) if name.is_a?(Class) experiments.find { |exp| exp == name } else experiment = experiments.find do |exp| exp.experiment_name == name.to_s.underscore.to_sym || exp.groups.include?(name.to_s.underscore.to_sym) || exp.name == name.to_s.classify end return experiment if experiment.present? combined = experiments.find do |exp| next unless exp.combined? exp.combined.any? { |combo| combo.to_s.underscore.to_sym == name.to_s.underscore.to_sym } end return nil unless combined.present? return combined_experiment(combined, name) end end
groups()
click to toggle source
# File lib/trail_guide/catalog.rb, line 103 def groups experiments.map(&:groups).flatten.uniq end
import(state)
click to toggle source
# File lib/trail_guide/catalog.rb, line 288 def import(state) state.each do |exp,est| experiment = find(exp) next unless experiment.present? experiment.reset! TrailGuide.redis.hsetnx(experiment.storage_key, 'name', experiment.experiment_name) TrailGuide.redis.hset(experiment.storage_key, 'started_at', DateTime.parse(est['started_at']).to_i) if est['started_at'].present? TrailGuide.redis.hset(experiment.storage_key, 'paused_at', DateTime.parse(est['paused_at']).to_i) if est['paused_at'].present? TrailGuide.redis.hset(experiment.storage_key, 'stopped_at', DateTime.parse(est['stopped_at']).to_i) if est['stopped_at'].present? TrailGuide.redis.hset(experiment.storage_key, 'winner', est['winner']) if est['winner'].present? est['variants'].each do |var,vst| variant = experiment.variants.find { |v| v == var } next unless variant.present? TrailGuide.redis.hincrby(variant.storage_key, 'participants', vst['participants'].to_i) if vst['participants'].to_i > 0 if vst['converted'].is_a?(Hash) vst['converted'].each do |goal,gct| TrailGuide.redis.hincrby(variant.storage_key, goal, gct.to_i) if gct.to_i > 0 end else TrailGuide.redis.hincrby(variant.storage_key, 'converted', vst['converted'].to_i) if vst['converted'].to_i > 0 end end end end
method_missing(meth, *args, &block)
click to toggle source
Calls superclass method
# File lib/trail_guide/catalog.rb, line 345 def method_missing(meth, *args, &block) return experiments.send(meth, *args, &block) if experiments.respond_to?(meth, true) super end
missing()
click to toggle source
# File lib/trail_guide/catalog.rb, line 316 def missing TrailGuide.redis.keys.select do |key| exp = key.split(':').first find(exp).nil? end end
not_running()
click to toggle source
# File lib/trail_guide/catalog.rb, line 151 def not_running new(to_a.select { |e| !e.running? }, @combined) end
orphaned(key, trace)
click to toggle source
# File lib/trail_guide/catalog.rb, line 323 def orphaned(key, trace) added = TrailGuide.redis.sadd("orphans:#{key}", trace) TrailGuide.redis.expire("orphans:#{key}", 15.minutes.seconds) added rescue Errno::ECONNREFUSED, Redis::BaseError, SocketError => e false end
orphans()
click to toggle source
# File lib/trail_guide/catalog.rb, line 331 def orphans TrailGuide.redis.keys("orphans:*").reduce({}) do |h,key| h.merge({ key.split(':').last => TrailGuide.redis.smembers(key) }) end rescue Errno::ECONNREFUSED, Redis::BaseError, SocketError => e {} end
paused()
click to toggle source
# File lib/trail_guide/catalog.rb, line 135 def paused new(to_a.select { |e| e.paused? && !e.winner? }, @combined) end
register(klass)
click to toggle source
# File lib/trail_guide/catalog.rb, line 264 def register(klass) experiments << klass unless experiments.any? { |exp| exp == klass } klass end
respond_to_missing?(meth, include_private=false)
click to toggle source
# File lib/trail_guide/catalog.rb, line 350 def respond_to_missing?(meth, include_private=false) experiments.respond_to?(meth, include_private) end
running()
click to toggle source
# File lib/trail_guide/catalog.rb, line 131 def running new(to_a.select { |e| e.running? && !e.winner? }, @combined) end
scheduled()
click to toggle source
# File lib/trail_guide/catalog.rb, line 127 def scheduled new(to_a.select { |e| e.scheduled? && !e.winner? }, @combined) end
select(name)
click to toggle source
# File lib/trail_guide/catalog.rb, line 242 def select(name) if name.is_a?(Class) selected = experiments.select { |exp| exp == name } else # TODO we can be more efficient than mapping twice here selected = experiments.select do |exp| exp.experiment_name == name.to_s.underscore.to_sym || exp.groups.include?(name.to_s.underscore.to_sym) || exp.name == name.to_s.classify || (exp.combined? && exp.combined.any? { |combo| combo.to_s.underscore.to_sym == name.to_s.underscore.to_sym }) end.map do |exp| if exp.combined? && exp.combined.any? { |combo| combo.to_s.underscore.to_sym == name.to_s.underscore.to_sym } combined_experiment(exp, name) else exp end end end new(selected, @combined) end
started()
click to toggle source
# File lib/trail_guide/catalog.rb, line 123 def started new(to_a.select { |e| e.started? && !e.winner? }, @combined) end
stopped()
click to toggle source
# File lib/trail_guide/catalog.rb, line 139 def stopped new(to_a.select { |e| e.stopped? && !e.winner? }, @combined) end
unstarted()
click to toggle source
# File lib/trail_guide/catalog.rb, line 147 def unstarted new(to_a.select { |e| !e.started? && !e.calibrating? && !e.scheduled? && !e.winner? }, @combined) end