unleash 5.1.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/pull_request.yml +0 -2
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +10 -0
- data/README.md +93 -119
- data/lib/unleash/client.rb +21 -22
- data/lib/unleash/configuration.rb +2 -2
- data/lib/unleash/context.rb +35 -9
- data/lib/unleash/metrics_reporter.rb +12 -26
- data/lib/unleash/strategies.rb +14 -73
- data/lib/unleash/toggle_fetcher.rb +22 -56
- data/lib/unleash/variant.rb +6 -0
- data/lib/unleash/version.rb +1 -1
- data/lib/unleash.rb +1 -1
- data/unleash-client.gemspec +2 -2
- data/v6_MIGRATION_GUIDE.md +21 -0
- metadata +11 -26
- data/lib/unleash/activation_strategy.rb +0 -44
- data/lib/unleash/constraint.rb +0 -117
- data/lib/unleash/feature_toggle.rb +0 -253
- data/lib/unleash/metrics.rb +0 -41
- data/lib/unleash/strategy/application_hostname.rb +0 -26
- data/lib/unleash/strategy/base.rb +0 -16
- data/lib/unleash/strategy/default.rb +0 -13
- data/lib/unleash/strategy/flexible_rollout.rb +0 -64
- data/lib/unleash/strategy/gradual_rollout_random.rb +0 -24
- data/lib/unleash/strategy/gradual_rollout_sessionid.rb +0 -21
- data/lib/unleash/strategy/gradual_rollout_userid.rb +0 -21
- data/lib/unleash/strategy/remote_address.rb +0 -36
- data/lib/unleash/strategy/user_with_id.rb +0 -20
- data/lib/unleash/strategy/util.rb +0 -17
- data/lib/unleash/variant_definition.rb +0 -26
- data/lib/unleash/variant_override.rb +0 -44
data/lib/unleash/constraint.rb
DELETED
@@ -1,117 +0,0 @@
|
|
1
|
-
require 'date'
|
2
|
-
module Unleash
|
3
|
-
class Constraint
|
4
|
-
attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
|
5
|
-
|
6
|
-
OPERATORS = {
|
7
|
-
IN: ->(context_v, constraint_v){ constraint_v.include? context_v.to_s },
|
8
|
-
NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v.to_s },
|
9
|
-
STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
|
10
|
-
STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
|
11
|
-
STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
|
12
|
-
NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } },
|
13
|
-
NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } },
|
14
|
-
NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } },
|
15
|
-
NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } },
|
16
|
-
NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } },
|
17
|
-
DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } },
|
18
|
-
DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
|
19
|
-
SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
|
20
|
-
SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } },
|
21
|
-
SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } },
|
22
|
-
FALLBACK_VALIDATOR: ->(_context_v, _constraint_v){ false }
|
23
|
-
}.freeze
|
24
|
-
|
25
|
-
STRING_OPERATORS = [:STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
|
26
|
-
|
27
|
-
LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
|
28
|
-
|
29
|
-
def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
|
30
|
-
raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
|
31
|
-
|
32
|
-
unless OPERATORS.include? operator.to_sym
|
33
|
-
Unleash.logger.warn "Operator #{operator} is not a supported operator, " \
|
34
|
-
"falling back to FALLBACK_VALIDATOR which skips this constraint."
|
35
|
-
operator = "FALLBACK_VALIDATOR"
|
36
|
-
end
|
37
|
-
self.log_inconsistent_constraint_configuration(operator.to_sym, value)
|
38
|
-
|
39
|
-
self.context_name = context_name
|
40
|
-
self.operator = operator.to_sym
|
41
|
-
self.value = value
|
42
|
-
self.inverted = !!inverted
|
43
|
-
self.case_insensitive = !!case_insensitive
|
44
|
-
end
|
45
|
-
|
46
|
-
def matches_context?(context)
|
47
|
-
Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})"
|
48
|
-
return false if context.nil?
|
49
|
-
|
50
|
-
match = matches_constraint?(context)
|
51
|
-
self.inverted ? !match : match
|
52
|
-
rescue KeyError
|
53
|
-
Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
|
54
|
-
found on the context"
|
55
|
-
false
|
56
|
-
end
|
57
|
-
|
58
|
-
def self.on_valid_date(val1, val2)
|
59
|
-
val1 = DateTime.parse(val1)
|
60
|
-
val2 = val2.is_a?(DateTime) ? val2 : DateTime.parse(val2)
|
61
|
-
yield(val1, val2)
|
62
|
-
rescue ArgumentError, TypeError
|
63
|
-
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
64
|
-
or constraint_value (#{val2}) into a date. Returning false!"
|
65
|
-
false
|
66
|
-
end
|
67
|
-
|
68
|
-
def self.on_valid_float(val1, val2)
|
69
|
-
val1 = Float(val1)
|
70
|
-
val2 = Float(val2)
|
71
|
-
yield(val1, val2)
|
72
|
-
rescue ArgumentError
|
73
|
-
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
74
|
-
or constraint_value (#{val2}) into a number. Returning false!"
|
75
|
-
false
|
76
|
-
end
|
77
|
-
|
78
|
-
def self.on_valid_version(val1, val2)
|
79
|
-
val1 = Gem::Version.new(val1)
|
80
|
-
val2 = Gem::Version.new(val2)
|
81
|
-
yield(val1, val2)
|
82
|
-
rescue ArgumentError
|
83
|
-
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
84
|
-
or constraint_value (#{val2}) into a version. Return false!"
|
85
|
-
false
|
86
|
-
end
|
87
|
-
|
88
|
-
# This should be a private method but for some reason this fails on Ruby 2.5
|
89
|
-
def log_inconsistent_constraint_configuration(operator, value)
|
90
|
-
Unleash.logger.warn "value is a String, operator is expecting an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
|
91
|
-
Unleash.logger.warn "value is an Array, operator is expecting a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
|
92
|
-
end
|
93
|
-
|
94
|
-
private
|
95
|
-
|
96
|
-
def matches_constraint?(context)
|
97
|
-
Unleash.logger.debug "Unleash::Constraint matches_constraint? value: #{self.value} operator: #{self.operator} " \
|
98
|
-
" context.get_by_name(#{self.context_name})"
|
99
|
-
|
100
|
-
unless OPERATORS.include?(self.operator)
|
101
|
-
Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
|
102
|
-
false
|
103
|
-
end
|
104
|
-
|
105
|
-
# when the operator is NOT_IN and there is no data, return true. In all other cases the operator doesn't match.
|
106
|
-
return self.operator == :NOT_IN unless context.include?(self.context_name)
|
107
|
-
|
108
|
-
v = self.value.dup
|
109
|
-
context_value = context.get_by_name(self.context_name)
|
110
|
-
|
111
|
-
# always return false, if we are comparing a non string with a string operator:
|
112
|
-
return false if !context_value.is_a?(String) && STRING_OPERATORS.include?(self.operator)
|
113
|
-
|
114
|
-
OPERATORS[self.operator].call(*self.case_insensitive ? [context_value.upcase, v.map(&:upcase)] : [context_value, v])
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
@@ -1,253 +0,0 @@
|
|
1
|
-
require 'unleash/activation_strategy'
|
2
|
-
require 'unleash/constraint'
|
3
|
-
require 'unleash/variant_definition'
|
4
|
-
require 'unleash/variant'
|
5
|
-
require 'unleash/strategy/util'
|
6
|
-
require 'securerandom'
|
7
|
-
|
8
|
-
module Unleash
|
9
|
-
class FeatureToggle
|
10
|
-
attr_accessor :name, :enabled, :dependencies, :strategies, :variant_definitions
|
11
|
-
|
12
|
-
FeatureEvaluationResult = Struct.new(:enabled?, :strategy)
|
13
|
-
|
14
|
-
def initialize(params = {}, segment_map = {})
|
15
|
-
params = {} if params.nil?
|
16
|
-
|
17
|
-
self.name = params.fetch('name', nil)
|
18
|
-
self.enabled = params.fetch('enabled', false)
|
19
|
-
self.dependencies = params.fetch('dependencies', [])
|
20
|
-
|
21
|
-
self.strategies = initialize_strategies(params, segment_map)
|
22
|
-
self.variant_definitions = initialize_variant_definitions(params)
|
23
|
-
end
|
24
|
-
|
25
|
-
def to_s
|
26
|
-
"<FeatureToggle: name=#{name},enabled=#{enabled},strategies=#{strategies},variant_definitions=#{variant_definitions}>"
|
27
|
-
end
|
28
|
-
|
29
|
-
def is_enabled?(context)
|
30
|
-
result = am_enabled?(context)
|
31
|
-
|
32
|
-
choice = result ? :yes : :no
|
33
|
-
Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
|
34
|
-
|
35
|
-
result
|
36
|
-
end
|
37
|
-
|
38
|
-
def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant)
|
39
|
-
raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant'
|
40
|
-
|
41
|
-
context = ensure_valid_context(context)
|
42
|
-
|
43
|
-
evaluation_result = evaluate(context)
|
44
|
-
|
45
|
-
group_id = evaluation_result.strategy&.params.to_h['groupId'] || self.name
|
46
|
-
|
47
|
-
variant = resolve_variant(context, evaluation_result, group_id)
|
48
|
-
|
49
|
-
choice = evaluation_result.enabled? ? :yes : :no
|
50
|
-
Unleash.toggle_metrics.increment_variant(self.name, choice, variant.name) unless Unleash.configuration.disable_metrics
|
51
|
-
|
52
|
-
variant.feature_enabled = evaluation_result.enabled?
|
53
|
-
|
54
|
-
variant
|
55
|
-
end
|
56
|
-
|
57
|
-
def self.disabled_variant
|
58
|
-
Unleash::Variant.new(name: 'disabled', enabled: false, feature_enabled: false)
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
def resolve_variant(context, evaluation_result, group_id)
|
64
|
-
variant_strategy_stickiness = evaluation_result.strategy&.params.to_h['stickiness'] || 'default'
|
65
|
-
variant_definitions = evaluation_result.strategy&.variant_definitions
|
66
|
-
variant_definitions = self.variant_definitions if variant_definitions.nil? || variant_definitions.empty?
|
67
|
-
return Unleash::FeatureToggle.disabled_variant unless evaluation_result.enabled?
|
68
|
-
return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights(variant_definitions) <= 0
|
69
|
-
|
70
|
-
variant_from_override_match(context, variant_definitions) ||
|
71
|
-
variant_from_weights(context, resolve_stickiness(variant_definitions, variant_strategy_stickiness), variant_definitions, group_id)
|
72
|
-
end
|
73
|
-
|
74
|
-
def resolve_stickiness(variant_definitions, variant_strategy_stickiness)
|
75
|
-
variant_definitions&.map(&:stickiness)&.compact&.first || variant_strategy_stickiness
|
76
|
-
end
|
77
|
-
|
78
|
-
# only check if it is enabled, do not do metrics
|
79
|
-
def am_enabled?(context)
|
80
|
-
evaluate(context).enabled?
|
81
|
-
end
|
82
|
-
|
83
|
-
def parent_dependencies_satisfied?(context)
|
84
|
-
dependencies.empty? || dependencies.all?{ |parent| evaluate_parent(parent, context) }
|
85
|
-
end
|
86
|
-
|
87
|
-
def evaluate_parent(parent, context)
|
88
|
-
parent_toggle = get_parent(parent["feature"])
|
89
|
-
return false if parent_toggle.nil? || !parent_toggle.dependencies.empty?
|
90
|
-
|
91
|
-
evaluation_result = parent_toggle.is_enabled?(context)
|
92
|
-
return !evaluation_result if parent["enabled"] == false
|
93
|
-
|
94
|
-
return false unless evaluation_result
|
95
|
-
|
96
|
-
return evaluation_result if parent["variants"].nil? || parent["variants"].empty?
|
97
|
-
|
98
|
-
parent["variants"].include?(parent_toggle.get_variant(context).name)
|
99
|
-
end
|
100
|
-
|
101
|
-
def get_parent(feature)
|
102
|
-
toggle_as_hash = Unleash&.toggles&.find{ |toggle| toggle['name'] == feature }
|
103
|
-
if toggle_as_hash.nil?
|
104
|
-
Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found"
|
105
|
-
return nil
|
106
|
-
end
|
107
|
-
|
108
|
-
Unleash::FeatureToggle.new(toggle_as_hash, Unleash&.segment_cache)
|
109
|
-
end
|
110
|
-
|
111
|
-
def evaluate(context)
|
112
|
-
evaluation_result =
|
113
|
-
if !parent_dependencies_satisfied?(context)
|
114
|
-
FeatureEvaluationResult.new(false, nil)
|
115
|
-
elsif !self.enabled
|
116
|
-
FeatureEvaluationResult.new(false, nil)
|
117
|
-
elsif self.strategies.empty?
|
118
|
-
FeatureEvaluationResult.new(true, nil)
|
119
|
-
else
|
120
|
-
strategy = self.strategies.find{ |s| strategy_enabled?(s, context) && strategy_constraint_matches?(s, context) }
|
121
|
-
FeatureEvaluationResult.new(!strategy.nil?, strategy)
|
122
|
-
end
|
123
|
-
|
124
|
-
Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled}) " \
|
125
|
-
"and Strategies combined with constraints returned #{evaluation_result})"
|
126
|
-
evaluation_result
|
127
|
-
end
|
128
|
-
|
129
|
-
def strategy_enabled?(strategy, context)
|
130
|
-
r = Unleash.strategies.fetch(strategy.name).is_enabled?(strategy.params, context)
|
131
|
-
Unleash.logger.debug "Unleash::FeatureToggle.strategy_enabled? Strategy #{strategy.name} returned #{r} with context: #{context}"
|
132
|
-
r
|
133
|
-
end
|
134
|
-
|
135
|
-
def strategy_constraint_matches?(strategy, context)
|
136
|
-
return false if strategy.disabled
|
137
|
-
|
138
|
-
strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
|
139
|
-
end
|
140
|
-
|
141
|
-
def sum_variant_defs_weights(variant_definitions)
|
142
|
-
variant_definitions.map(&:weight).reduce(0, :+)
|
143
|
-
end
|
144
|
-
|
145
|
-
def variant_salt(context, stickiness = "default")
|
146
|
-
begin
|
147
|
-
return context.get_by_name(stickiness) if !context.nil? && stickiness != "default"
|
148
|
-
rescue KeyError
|
149
|
-
Unleash.logger.warn "Custom stickiness key (#{stickiness}) not found in the provided context #{context}. " \
|
150
|
-
"Falling back to default behavior."
|
151
|
-
end
|
152
|
-
return context.user_id unless context&.user_id.to_s.empty?
|
153
|
-
return context.session_id unless context&.session_id.to_s.empty?
|
154
|
-
return context.remote_address unless context&.remote_address.to_s.empty?
|
155
|
-
|
156
|
-
SecureRandom.random_number
|
157
|
-
end
|
158
|
-
|
159
|
-
def variant_from_override_match(context, variant_definitions)
|
160
|
-
variant_definition = variant_definitions.find{ |vd| vd.override_matches_context?(context) }
|
161
|
-
return nil if variant_definition.nil?
|
162
|
-
|
163
|
-
Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
|
164
|
-
end
|
165
|
-
|
166
|
-
def variant_from_weights(context, stickiness, variant_definitions, group_id)
|
167
|
-
variant_weight = Unleash::Strategy::Util.get_normalized_number(
|
168
|
-
variant_salt(context, stickiness),
|
169
|
-
group_id,
|
170
|
-
Unleash::Strategy::Util::VARIANT_NORMALIZER_SEED,
|
171
|
-
sum_variant_defs_weights(variant_definitions)
|
172
|
-
)
|
173
|
-
prev_weights = 0
|
174
|
-
|
175
|
-
variant_definition = variant_definitions
|
176
|
-
.find do |v|
|
177
|
-
res = (prev_weights + v.weight >= variant_weight)
|
178
|
-
prev_weights += v.weight
|
179
|
-
res
|
180
|
-
end
|
181
|
-
return self.disabled_variant if variant_definition.nil?
|
182
|
-
|
183
|
-
Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
|
184
|
-
end
|
185
|
-
|
186
|
-
def ensure_valid_context(context)
|
187
|
-
unless ['NilClass', 'Unleash::Context'].include? context.class.name
|
188
|
-
Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, " \
|
189
|
-
"please use Unleash::Context. Context set to nil."
|
190
|
-
context = nil
|
191
|
-
end
|
192
|
-
context
|
193
|
-
end
|
194
|
-
|
195
|
-
def initialize_strategies(params, segment_map)
|
196
|
-
(params.fetch('strategies', []) || [])
|
197
|
-
.select{ |s| s.has_key?('name') && Unleash.strategies.includes?(s['name']) }
|
198
|
-
.map do |s|
|
199
|
-
ActivationStrategy.new(
|
200
|
-
s['name'],
|
201
|
-
s['parameters'],
|
202
|
-
resolve_constraints(s, segment_map),
|
203
|
-
resolve_variants(s)
|
204
|
-
)
|
205
|
-
end || []
|
206
|
-
end
|
207
|
-
|
208
|
-
def resolve_variants(strategy)
|
209
|
-
(strategy.fetch("variants", []) || [])
|
210
|
-
.select{ |variant| variant.is_a?(Hash) && variant.has_key?("name") }
|
211
|
-
.map do |variant|
|
212
|
-
VariantDefinition.new(
|
213
|
-
variant.fetch("name", ""),
|
214
|
-
variant.fetch("weight", 0),
|
215
|
-
variant.fetch("payload", nil),
|
216
|
-
variant.fetch("stickiness", nil),
|
217
|
-
variant.fetch("overrides", [])
|
218
|
-
)
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
def resolve_constraints(strategy, segment_map)
|
223
|
-
segment_constraints = (strategy["segments"] || []).map do |segment_id|
|
224
|
-
segment_map[segment_id]&.fetch("constraints")
|
225
|
-
end
|
226
|
-
(strategy.fetch("constraints", []) + segment_constraints).flatten.map do |constraint|
|
227
|
-
return nil if constraint.nil?
|
228
|
-
|
229
|
-
Constraint.new(
|
230
|
-
constraint.fetch('contextName'),
|
231
|
-
constraint.fetch('operator'),
|
232
|
-
constraint.fetch('value', nil) || constraint.fetch('values', nil),
|
233
|
-
inverted: constraint.fetch('inverted', false),
|
234
|
-
case_insensitive: constraint.fetch('caseInsensitive', false)
|
235
|
-
)
|
236
|
-
end
|
237
|
-
end
|
238
|
-
|
239
|
-
def initialize_variant_definitions(params)
|
240
|
-
(params.fetch('variants', []) || [])
|
241
|
-
.select{ |v| v.is_a?(Hash) && v.has_key?('name') }
|
242
|
-
.map do |v|
|
243
|
-
VariantDefinition.new(
|
244
|
-
v.fetch('name', ''),
|
245
|
-
v.fetch('weight', 0),
|
246
|
-
v.fetch('payload', nil),
|
247
|
-
v.fetch('stickiness', nil),
|
248
|
-
v.fetch('overrides', [])
|
249
|
-
)
|
250
|
-
end || []
|
251
|
-
end
|
252
|
-
end
|
253
|
-
end
|
data/lib/unleash/metrics.rb
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
module Unleash
|
2
|
-
class Metrics
|
3
|
-
attr_accessor :features, :features_lock
|
4
|
-
|
5
|
-
def initialize
|
6
|
-
self.features = {}
|
7
|
-
self.features_lock = Mutex.new
|
8
|
-
end
|
9
|
-
|
10
|
-
def to_s
|
11
|
-
self.features_lock.synchronize do
|
12
|
-
return self.features.to_json
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def increment(feature, choice)
|
17
|
-
raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice
|
18
|
-
|
19
|
-
self.features_lock.synchronize do
|
20
|
-
self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
|
21
|
-
self.features[feature][choice] += 1
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def increment_variant(feature, choice, variant)
|
26
|
-
self.features_lock.synchronize do
|
27
|
-
self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
|
28
|
-
self.features[feature][choice] += 1
|
29
|
-
self.features[feature]['variants'] = {} unless self.features[feature].include? 'variants'
|
30
|
-
self.features[feature]['variants'][variant] = 0 unless self.features[feature]['variants'].include? variant
|
31
|
-
self.features[feature]['variants'][variant] += 1
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def reset
|
36
|
-
self.features_lock.synchronize do
|
37
|
-
self.features = {}
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
@@ -1,26 +0,0 @@
|
|
1
|
-
require 'socket'
|
2
|
-
|
3
|
-
module Unleash
|
4
|
-
module Strategy
|
5
|
-
class ApplicationHostname < Base
|
6
|
-
attr_accessor :hostname
|
7
|
-
|
8
|
-
PARAM = 'hostnames'.freeze
|
9
|
-
|
10
|
-
def initialize
|
11
|
-
self.hostname = Socket.gethostname || 'undefined'
|
12
|
-
end
|
13
|
-
|
14
|
-
def name
|
15
|
-
'applicationHostname'
|
16
|
-
end
|
17
|
-
|
18
|
-
# need: :params['hostnames']
|
19
|
-
def is_enabled?(params = {}, _context = nil)
|
20
|
-
return false unless params.is_a?(Hash) && params.has_key?(PARAM)
|
21
|
-
|
22
|
-
params[PARAM].split(",").map(&:strip).map(&:downcase).include?(self.hostname)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,16 +0,0 @@
|
|
1
|
-
module Unleash
|
2
|
-
module Strategy
|
3
|
-
class NotImplemented < RuntimeError
|
4
|
-
end
|
5
|
-
|
6
|
-
class Base
|
7
|
-
def name
|
8
|
-
raise NotImplemented, "Strategy is not implemented"
|
9
|
-
end
|
10
|
-
|
11
|
-
def is_enabled?(_params = {}, _context = nil)
|
12
|
-
raise NotImplemented, "Strategy is not implemented"
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
@@ -1,64 +0,0 @@
|
|
1
|
-
require 'unleash/strategy/util'
|
2
|
-
|
3
|
-
module Unleash
|
4
|
-
module Strategy
|
5
|
-
class FlexibleRollout < Base
|
6
|
-
def name
|
7
|
-
'flexibleRollout'
|
8
|
-
end
|
9
|
-
|
10
|
-
# need: params['percentage']
|
11
|
-
def is_enabled?(params = {}, context = nil)
|
12
|
-
return false unless params.is_a?(Hash)
|
13
|
-
|
14
|
-
stickiness = params.fetch('stickiness', 'default')
|
15
|
-
return false if context_invalid?(stickiness, context)
|
16
|
-
|
17
|
-
stickiness_id = resolve_stickiness(stickiness, context)
|
18
|
-
|
19
|
-
begin
|
20
|
-
percentage = Integer(params.fetch('rollout', 0))
|
21
|
-
percentage = 0 if percentage > 100 || percentage.negative?
|
22
|
-
rescue ArgumentError
|
23
|
-
return false
|
24
|
-
end
|
25
|
-
|
26
|
-
group_id = params.fetch('groupId', '')
|
27
|
-
normalized_number = Util.get_normalized_number(stickiness_id, group_id, 0)
|
28
|
-
|
29
|
-
return false if stickiness_id.nil?
|
30
|
-
|
31
|
-
(percentage.positive? && normalized_number <= percentage)
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def context_invalid?(stickiness, context)
|
37
|
-
return false if ['random', 'default'].include?(stickiness)
|
38
|
-
|
39
|
-
!context.instance_of?(Unleash::Context)
|
40
|
-
end
|
41
|
-
|
42
|
-
def random
|
43
|
-
Random.rand(0..10_000)
|
44
|
-
end
|
45
|
-
|
46
|
-
def resolve_stickiness(stickiness, context)
|
47
|
-
case stickiness
|
48
|
-
when 'random'
|
49
|
-
random
|
50
|
-
when 'default'
|
51
|
-
return random unless context.instance_of?(Unleash::Context)
|
52
|
-
|
53
|
-
context&.user_id || context&.session_id || random
|
54
|
-
else
|
55
|
-
begin
|
56
|
-
context.get_by_name(stickiness)
|
57
|
-
rescue KeyError
|
58
|
-
nil
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
require 'unleash/strategy/util'
|
2
|
-
|
3
|
-
module Unleash
|
4
|
-
module Strategy
|
5
|
-
class GradualRolloutRandom < Base
|
6
|
-
def name
|
7
|
-
'gradualRolloutRandom'
|
8
|
-
end
|
9
|
-
|
10
|
-
# need: params['percentage']
|
11
|
-
def is_enabled?(params = {}, _context = nil)
|
12
|
-
return false unless params.is_a?(Hash) && params.has_key?('percentage')
|
13
|
-
|
14
|
-
begin
|
15
|
-
percentage = Integer(params['percentage'] || 0)
|
16
|
-
rescue ArgumentError
|
17
|
-
return false
|
18
|
-
end
|
19
|
-
|
20
|
-
(percentage >= Random.rand(1..100))
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
require 'unleash/strategy/util'
|
2
|
-
|
3
|
-
module Unleash
|
4
|
-
module Strategy
|
5
|
-
class GradualRolloutSessionId < Base
|
6
|
-
def name
|
7
|
-
'gradualRolloutSessionId'
|
8
|
-
end
|
9
|
-
|
10
|
-
# need: params['percentage'], params['groupId'], context.user_id,
|
11
|
-
def is_enabled?(params = {}, context = nil)
|
12
|
-
return false unless params.is_a?(Hash) && params.has_key?('percentage')
|
13
|
-
return false unless context.instance_of?(Unleash::Context)
|
14
|
-
return false if context.session_id.nil? || context.session_id.empty?
|
15
|
-
|
16
|
-
percentage = Integer(params['percentage'] || 0)
|
17
|
-
(percentage.positive? && Util.get_normalized_number(context.session_id, params['groupId'] || "", 0) <= percentage)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
require 'unleash/strategy/util'
|
2
|
-
|
3
|
-
module Unleash
|
4
|
-
module Strategy
|
5
|
-
class GradualRolloutUserId < Base
|
6
|
-
def name
|
7
|
-
'gradualRolloutUserId'
|
8
|
-
end
|
9
|
-
|
10
|
-
# need: params['percentage'], params['groupId'], context.user_id,
|
11
|
-
def is_enabled?(params = {}, context = nil, _constraints = [])
|
12
|
-
return false unless params.is_a?(Hash) && params.has_key?('percentage')
|
13
|
-
return false unless context.instance_of?(Unleash::Context)
|
14
|
-
return false if context.user_id.nil? || context.user_id.empty?
|
15
|
-
|
16
|
-
percentage = Integer(params['percentage'] || 0)
|
17
|
-
(percentage.positive? && Util.get_normalized_number(context.user_id, params['groupId'] || "", 0) <= percentage)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
@@ -1,36 +0,0 @@
|
|
1
|
-
module Unleash
|
2
|
-
module Strategy
|
3
|
-
class RemoteAddress < Base
|
4
|
-
PARAM = 'IPs'.freeze
|
5
|
-
|
6
|
-
def name
|
7
|
-
'remoteAddress'
|
8
|
-
end
|
9
|
-
|
10
|
-
# need: params['IPs'], context.remote_address
|
11
|
-
def is_enabled?(params = {}, context = nil)
|
12
|
-
return false unless params.is_a?(Hash) && params.has_key?(PARAM)
|
13
|
-
return false unless params.fetch(PARAM, nil).is_a? String
|
14
|
-
return false unless context.instance_of?(Unleash::Context)
|
15
|
-
|
16
|
-
remote_address = ipaddr_or_nil_from_str(context.remote_address)
|
17
|
-
|
18
|
-
params[PARAM]
|
19
|
-
.split(',')
|
20
|
-
.map(&:strip)
|
21
|
-
.map{ |ipblock| ipaddr_or_nil_from_str(ipblock) }
|
22
|
-
.compact
|
23
|
-
.map{ |ipb| ipb.include? remote_address }
|
24
|
-
.any?
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def ipaddr_or_nil_from_str(ip)
|
30
|
-
IPAddr.new(ip)
|
31
|
-
rescue StandardError
|
32
|
-
nil
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
module Unleash
|
2
|
-
module Strategy
|
3
|
-
class UserWithId < Base
|
4
|
-
PARAM = 'userIds'.freeze
|
5
|
-
|
6
|
-
def name
|
7
|
-
'userWithId'
|
8
|
-
end
|
9
|
-
|
10
|
-
# requires: params['userIds'], context.user_id,
|
11
|
-
def is_enabled?(params = {}, context = nil)
|
12
|
-
return false unless params.is_a?(Hash) && params.has_key?(PARAM)
|
13
|
-
return false unless params.fetch(PARAM, nil).is_a? String
|
14
|
-
return false unless context.instance_of?(Unleash::Context)
|
15
|
-
|
16
|
-
params[PARAM].split(",").map(&:strip).include?(context.user_id)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
require 'murmurhash3'
|
2
|
-
|
3
|
-
module Unleash
|
4
|
-
module Strategy
|
5
|
-
module Util
|
6
|
-
module_function
|
7
|
-
|
8
|
-
NORMALIZER = 100
|
9
|
-
VARIANT_NORMALIZER_SEED = 86_028_157
|
10
|
-
|
11
|
-
# convert the two strings () into a number between 1 and base (100 by default)
|
12
|
-
def get_normalized_number(identifier, group_id, seed, base = NORMALIZER)
|
13
|
-
MurmurHash3::V32.str_hash("#{group_id}:#{identifier}", seed) % base + 1
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|