unleash 5.1.1 → 6.0.0.pre
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/CHANGELOG.md +6 -0
- 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 +1 -0
- metadata +20 -22
- 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,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unleash
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 6.0.0.pre
|
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-09-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: murmurhash3
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 0.1.7
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: yggdrasil-engine
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.0.5
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.0.5
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: bundler
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -166,7 +180,6 @@ files:
|
|
166
180
|
- examples/extending_unleash_with_opentelemetry.rb
|
167
181
|
- examples/simple.rb
|
168
182
|
- lib/unleash.rb
|
169
|
-
- lib/unleash/activation_strategy.rb
|
170
183
|
- lib/unleash/bootstrap/configuration.rb
|
171
184
|
- lib/unleash/bootstrap/handler.rb
|
172
185
|
- lib/unleash/bootstrap/provider/base.rb
|
@@ -174,36 +187,21 @@ files:
|
|
174
187
|
- lib/unleash/bootstrap/provider/from_url.rb
|
175
188
|
- lib/unleash/client.rb
|
176
189
|
- lib/unleash/configuration.rb
|
177
|
-
- lib/unleash/constraint.rb
|
178
190
|
- lib/unleash/context.rb
|
179
|
-
- lib/unleash/feature_toggle.rb
|
180
|
-
- lib/unleash/metrics.rb
|
181
191
|
- lib/unleash/metrics_reporter.rb
|
182
192
|
- lib/unleash/scheduled_executor.rb
|
183
193
|
- lib/unleash/spec_version.rb
|
184
194
|
- 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
195
|
- lib/unleash/toggle_fetcher.rb
|
196
196
|
- lib/unleash/util/http.rb
|
197
197
|
- lib/unleash/variant.rb
|
198
|
-
- lib/unleash/variant_definition.rb
|
199
|
-
- lib/unleash/variant_override.rb
|
200
198
|
- lib/unleash/version.rb
|
201
199
|
- unleash-client.gemspec
|
202
200
|
homepage: https://github.com/unleash/unleash-client-ruby
|
203
201
|
licenses:
|
204
202
|
- Apache-2.0
|
205
203
|
metadata: {}
|
206
|
-
post_install_message:
|
204
|
+
post_install_message:
|
207
205
|
rdoc_options: []
|
208
206
|
require_paths:
|
209
207
|
- lib
|
@@ -218,8 +216,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
218
216
|
- !ruby/object:Gem::Version
|
219
217
|
version: '0'
|
220
218
|
requirements: []
|
221
|
-
rubygems_version: 3.5.
|
222
|
-
signing_key:
|
219
|
+
rubygems_version: 3.5.6
|
220
|
+
signing_key:
|
223
221
|
specification_version: 4
|
224
222
|
summary: Unleash feature toggle client.
|
225
223
|
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
|