lib/unleash/feature_toggle.rb in unleash-4.4.4 vs lib/unleash/feature_toggle.rb in unleash-4.5.0
- old
+ new
@@ -7,10 +7,12 @@
module Unleash
class FeatureToggle
attr_accessor :name, :enabled, :strategies, :variant_definitions
+ FeatureEvaluationResult = Struct.new(:enabled?, :strategy)
+
def initialize(params = {}, segment_map = {})
params = {} if params.nil?
self.name = params.fetch('name', nil)
self.enabled = params.fetch('enabled', false)
@@ -35,51 +37,61 @@
def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant)
raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant'
context = ensure_valid_context(context)
- toggle_enabled = am_enabled?(context)
- variant = resolve_variant(context, toggle_enabled)
+ evaluation_result = evaluate(context)
- choice = toggle_enabled ? :yes : :no
+ group_id = evaluation_result[:strategy]&.params.to_h['groupId'] || self.name
+
+ variant = resolve_variant(context, evaluation_result, group_id)
+
+ choice = evaluation_result[:enabled?] ? :yes : :no
Unleash.toggle_metrics.increment_variant(self.name, choice, variant.name) unless Unleash.configuration.disable_metrics
variant
end
def self.disabled_variant
Unleash::Variant.new(name: 'disabled', enabled: false)
end
private
- def resolve_variant(context, toggle_enabled)
- return Unleash::FeatureToggle.disabled_variant unless toggle_enabled
- return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
+ def resolve_variant(context, evaluation_result, group_id)
+ variant_definitions = evaluation_result[:strategy]&.variant_definitions
+ variant_definitions = self.variant_definitions if variant_definitions.nil? || variant_definitions.empty?
+ return Unleash::FeatureToggle.disabled_variant unless evaluation_result[:enabled?]
+ return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights(variant_definitions) <= 0
- variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
+ variant_from_override_match(context, variant_definitions) ||
+ variant_from_weights(context, resolve_stickiness(variant_definitions), variant_definitions, group_id)
end
- def resolve_stickiness
- self.variant_definitions&.map(&:stickiness)&.compact&.first || "default"
+ def resolve_stickiness(variant_definitions)
+ variant_definitions&.map(&:stickiness)&.compact&.first || "default"
end
# only check if it is enabled, do not do metrics
def am_enabled?(context)
- result =
- if self.enabled
- self.strategies.empty? ||
- self.strategies.any? do |s|
- strategy_enabled?(s, context) && strategy_constraint_matches?(s, context)
- end
+ evaluate(context)[:enabled?]
+ end
+
+ def evaluate(context)
+ evaluation_result =
+ if !self.enabled
+ FeatureEvaluationResult.new(false, nil)
+ elsif self.strategies.empty?
+ FeatureEvaluationResult.new(true, nil)
else
- false
+ strategy = self.strategies.find{ |s| strategy_enabled?(s, context) && strategy_constraint_matches?(s, context) }
+ FeatureEvaluationResult.new(!strategy.nil?, strategy)
end
Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \
- "and Strategies combined with contraints returned #{result})"
+ "and Strategies combined with contraints returned #{evaluation_result})"
- result
+ evaluation_result
end
def strategy_enabled?(strategy, context)
r = Unleash.strategies.fetch(strategy.name).is_enabled?(strategy.params, context)
Unleash.logger.debug "Unleash::FeatureToggle.strategy_enabled? Strategy #{strategy.name} returned #{r} with context: #{context}"
@@ -90,12 +102,12 @@
return false if strategy.disabled
strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
end
- def sum_variant_defs_weights
- self.variant_definitions.map(&:weight).reduce(0, :+)
+ def sum_variant_defs_weights(variant_definitions)
+ variant_definitions.map(&:weight).reduce(0, :+)
end
def variant_salt(context, stickiness = "default")
begin
return context.get_by_name(stickiness) if !context.nil? && stickiness != "default"
@@ -108,22 +120,26 @@
return context.remote_address unless context&.remote_address.to_s.empty?
SecureRandom.random_number
end
- def variant_from_override_match(context)
- variant = self.variant_definitions.find{ |vd| vd.override_matches_context?(context) }
- return nil if variant.nil?
+ def variant_from_override_match(context, variant_definitions)
+ variant_definition = variant_definitions.find{ |vd| vd.override_matches_context?(context) }
+ return nil if variant_definition.nil?
- Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload)
+ Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
end
- def variant_from_weights(context, stickiness)
- variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context, stickiness), self.name, sum_variant_defs_weights)
+ def variant_from_weights(context, stickiness, variant_definitions, group_id)
+ variant_weight = Unleash::Strategy::Util.get_normalized_number(
+ variant_salt(context, stickiness),
+ group_id,
+ sum_variant_defs_weights(variant_definitions)
+ )
prev_weights = 0
- variant_definition = self.variant_definitions
+ variant_definition = variant_definitions
.find do |v|
res = (prev_weights + v.weight >= variant_weight)
prev_weights += v.weight
res
end
@@ -146,12 +162,27 @@
.select{ |s| s.has_key?('name') && Unleash.strategies.includes?(s['name']) }
.map do |s|
ActivationStrategy.new(
s['name'],
s['parameters'],
- resolve_constraints(s, segment_map)
+ resolve_constraints(s, segment_map),
+ resolve_variants(s)
)
end || []
+ end
+
+ def resolve_variants(strategy)
+ strategy.fetch("variants", [])
+ .select{ |variant| variant.is_a?(Hash) && variant.has_key?("name") }
+ .map do |variant|
+ VariantDefinition.new(
+ variant.fetch("name", ""),
+ variant.fetch("weight", 0),
+ variant.fetch("payload", nil),
+ variant.fetch("stickiness", nil),
+ variant.fetch("overrides", [])
+ )
+ end
end
def resolve_constraints(strategy, segment_map)
segment_constraints = (strategy["segments"] || []).map do |segment_id|
segment_map[segment_id]&.fetch("constraints")