unleash 5.1.1 → 6.0.9
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 +30 -0
- data/README.md +94 -120
- 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/util/http.rb +1 -0
- 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
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unleash
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 6.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Renato Arruda
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-11-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: yggdrasil-engine
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.0.9
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.0.9
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -166,7 +166,6 @@ files:
|
|
166
166
|
- examples/extending_unleash_with_opentelemetry.rb
|
167
167
|
- examples/simple.rb
|
168
168
|
- lib/unleash.rb
|
169
|
-
- lib/unleash/activation_strategy.rb
|
170
169
|
- lib/unleash/bootstrap/configuration.rb
|
171
170
|
- lib/unleash/bootstrap/handler.rb
|
172
171
|
- lib/unleash/bootstrap/provider/base.rb
|
@@ -174,36 +173,22 @@ files:
|
|
174
173
|
- lib/unleash/bootstrap/provider/from_url.rb
|
175
174
|
- lib/unleash/client.rb
|
176
175
|
- lib/unleash/configuration.rb
|
177
|
-
- lib/unleash/constraint.rb
|
178
176
|
- lib/unleash/context.rb
|
179
|
-
- lib/unleash/feature_toggle.rb
|
180
|
-
- lib/unleash/metrics.rb
|
181
177
|
- lib/unleash/metrics_reporter.rb
|
182
178
|
- lib/unleash/scheduled_executor.rb
|
183
179
|
- lib/unleash/spec_version.rb
|
184
180
|
- lib/unleash/strategies.rb
|
185
|
-
- lib/unleash/strategy/application_hostname.rb
|
186
|
-
- lib/unleash/strategy/base.rb
|
187
|
-
- lib/unleash/strategy/default.rb
|
188
|
-
- lib/unleash/strategy/flexible_rollout.rb
|
189
|
-
- lib/unleash/strategy/gradual_rollout_random.rb
|
190
|
-
- lib/unleash/strategy/gradual_rollout_sessionid.rb
|
191
|
-
- lib/unleash/strategy/gradual_rollout_userid.rb
|
192
|
-
- lib/unleash/strategy/remote_address.rb
|
193
|
-
- lib/unleash/strategy/user_with_id.rb
|
194
|
-
- lib/unleash/strategy/util.rb
|
195
181
|
- lib/unleash/toggle_fetcher.rb
|
196
182
|
- lib/unleash/util/http.rb
|
197
183
|
- lib/unleash/variant.rb
|
198
|
-
- lib/unleash/variant_definition.rb
|
199
|
-
- lib/unleash/variant_override.rb
|
200
184
|
- lib/unleash/version.rb
|
201
185
|
- unleash-client.gemspec
|
186
|
+
- v6_MIGRATION_GUIDE.md
|
202
187
|
homepage: https://github.com/unleash/unleash-client-ruby
|
203
188
|
licenses:
|
204
189
|
- Apache-2.0
|
205
190
|
metadata: {}
|
206
|
-
post_install_message:
|
191
|
+
post_install_message:
|
207
192
|
rdoc_options: []
|
208
193
|
require_paths:
|
209
194
|
- lib
|
@@ -211,15 +196,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
211
196
|
requirements:
|
212
197
|
- - ">="
|
213
198
|
- !ruby/object:Gem::Version
|
214
|
-
version: '2.
|
199
|
+
version: '2.7'
|
215
200
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
216
201
|
requirements:
|
217
202
|
- - ">="
|
218
203
|
- !ruby/object:Gem::Version
|
219
204
|
version: '0'
|
220
205
|
requirements: []
|
221
|
-
rubygems_version: 3.5.
|
222
|
-
signing_key:
|
206
|
+
rubygems_version: 3.5.6
|
207
|
+
signing_key:
|
223
208
|
specification_version: 4
|
224
209
|
summary: Unleash feature toggle client.
|
225
210
|
test_files: []
|
@@ -1,44 +0,0 @@
|
|
1
|
-
module Unleash
|
2
|
-
class ActivationStrategy
|
3
|
-
attr_accessor :name, :params, :constraints, :disabled, :variant_definitions
|
4
|
-
|
5
|
-
def initialize(name, params, constraints = [], variant_definitions = [])
|
6
|
-
self.name = name
|
7
|
-
self.disabled = false
|
8
|
-
|
9
|
-
if params.is_a?(Hash)
|
10
|
-
self.params = params
|
11
|
-
elsif params.nil?
|
12
|
-
self.params = {}
|
13
|
-
else
|
14
|
-
Unleash.logger.warn "Invalid params provided for ActivationStrategy (params:#{params})"
|
15
|
-
self.params = {}
|
16
|
-
end
|
17
|
-
|
18
|
-
if constraints.is_a?(Array) && constraints.all?{ |c| c.is_a?(Constraint) }
|
19
|
-
self.constraints = constraints
|
20
|
-
else
|
21
|
-
Unleash.logger.warn "Invalid constraints provided for ActivationStrategy (constraints: #{constraints})"
|
22
|
-
self.disabled = true
|
23
|
-
self.constraints = []
|
24
|
-
end
|
25
|
-
|
26
|
-
self.variant_definitions = valid_variant_definitions(variant_definitions)
|
27
|
-
end
|
28
|
-
|
29
|
-
def matches_context?(context)
|
30
|
-
self.constraints.any?{ |c| c.matches_context? context }
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def valid_variant_definitions(variant_definitions)
|
36
|
-
if variant_definitions.is_a?(Array) && variant_definitions.all?{ |variant_definition| variant_definition.is_a?(VariantDefinition) }
|
37
|
-
variant_definitions
|
38
|
-
else
|
39
|
-
Unleash.logger.warn "Invalid variant_definitions provided for ActivationStrategy (variant_definitions: #{variant_definitions})"
|
40
|
-
[]
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
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
|