lib/verdict/segmenter.rb in verdict-0.1.1 vs lib/verdict/segmenter.rb in verdict-0.2.0

- old
+ new

@@ -1,78 +1,87 @@ require 'digest/md5' -module Verdict::Segmenter +# Base class of all segmenters. +# +# The segmenter is responsible for assigning subjects to groups. You can +# implement any assignment strategy you like by subclassing this class and +# using it in your experiment. +# +# - You should implement the register_group method for the experiment definition DSL +# to make the system aware of the groups that the segmenter could return. +# - The verify! method is called after all the groups have been defined, so it can +# detect internal inconsistencies in the group definitions. +# - The assign method is where your assignment magic lives. +class Verdict::Segmenter - class Base + # The experiment to which this segmenter is associated + attr_reader :experiment - attr_reader :experiment, :groups + # A hash of the groups that are defined in this experiment, indexed by their + # handle. The assign method should return one of the groups in this hash + attr_reader :groups - def initialize(experiment) - @experiment = experiment - @groups = {} - end + def initialize(experiment) + @experiment = experiment + @groups = {} + end - def verify! - end + # DSL method to register a group. It calls the register_group method of the + # segmenter implementation + def group(handle, *args, &block) + group = register_group(handle, *args) + @groups[group.handle] = group + group.instance_eval(&block) if block_given? + end - def group(identifier, subject, context) - raise NotImplementedError - end + # The group method is called from the experiment definition DSL. + # It should register a new group to the segmenter, with the given handle. + # + # - The handle parameter is a symbol that uniquely identifies the group within + # this experiment. + # - The return value of this method should be a Verdict::Group instance. + def register_group(handle, *args) + raise NotImplementedError end - class StaticPercentage < Base + # The verify! method is called after all the groups have been defined in the + # experiment definition DSL. You can run any consistency checks in this method, + # and if anything is off, you can raise a Verdict::SegmentationError to + # signify the problem. + def verify! + # noop by default + end - class Group < Verdict::Group + # The assign method is called to assign a subject to one of the groups that have been defined + # in the segmenter implementation. + # + # - The identifier parameter is a string that uniquely identifies the subject. + # - The subject paramater is the subject instance that was passed to the framework, + # when the application code calls Experiment#assign or Experiment#switch. + # - The context parameter is an object that was passed to the framework, you can use this + # object any way you like in your segmenting logic. + # + # This method should return the Verdict::Group instance to which the subject should be assigned. + # This instance should be one of the group instance that was registered in the definition DSL. + def assign(identifier, subject, context) + raise NotImplementedError + end - attr_reader :percentile_range - def initialize(experiment, handle, percentile_range) - super(experiment, handle) - @percentile_range = percentile_range - end - - def percentage_size - percentile_range.end - percentile_range.begin - end - - def to_s - "#{handle} (#{percentage_size}%)" - end - - def as_json(options = {}) - super(options).merge(percentage: percentage_size) - end - end - - def initialize(experiment) - super - @total_percentage_segmented = 0 - end - - def verify! - raise Verdict::SegmentationError, "Should segment exactly 100% of the cases, but segments add up to #{@total_percentage_segmented}%." if @total_percentage_segmented != 100 - end - - def group(handle, size, &block) - percentage = size.kind_of?(Hash) && size[:percentage] ? size[:percentage] : size - n = case percentage - when :rest; 100 - @total_percentage_segmented - when :half; 50 - when Integer; percentage - else Integer(percentage) - end - - group = Group.new(experiment, handle, @total_percentage_segmented ... (@total_percentage_segmented + n)) - @groups[group.handle] = group - @total_percentage_segmented += n - group.instance_eval(&block) if block_given? - return group - end - - def assign(identifier, subject, context) - percentile = Digest::MD5.hexdigest("#{@experiment.handle}#{identifier}").to_i(16) % 100 - _, group = groups.find { |_, group| group.percentile_range.include?(percentile) } - raise Verdict::SegmentationError, "Could not get segment for subject #{identifier.inspect}!" unless group - group - end + # This method is called whenever a subjects converts to a goal, i.e., when Experiment#convert + # is called. You can use this to implement a feedback loop in your segmenter. + # + # - The identifier parameter is a string that uniquely identifies the subject. + # - The subject paramater is the subject instance that was passed to the framework, + # when the application code calls Experiment#assign or Experiment#switch. + # - The conversion parameter is a Verdict::Conversion instance that describes what + # goal the subject converted to. + # + # The return value of this method is not used. + def conversion_feedback(identifier, subject, conversion) + # noop by default end end + +require 'verdict/static_segmenter' +require 'verdict/fixed_percentage_segmenter' +require 'verdict/rollout_segmenter' \ No newline at end of file