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