determinator 2.4.3 → 2.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/lib/determinator.rb +2 -0
- data/lib/determinator/control.rb +135 -22
- data/lib/determinator/explainer.rb +61 -0
- data/lib/determinator/explainer/messages.rb +119 -0
- data/lib/determinator/feature.rb +48 -6
- data/lib/determinator/fixed_determination.rb +25 -0
- data/lib/determinator/serializers/json.rb +10 -8
- data/lib/determinator/target_group.rb +12 -4
- data/lib/determinator/tracking/determination.rb +6 -0
- data/lib/determinator/tracking/tracker.rb +9 -7
- data/lib/determinator/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 826a70be2194ebbfeaf540a68e08e9af45bcbea63ed43870f7745d85d6d7f726
|
4
|
+
data.tar.gz: 07af2da7a269b2d4627fa4a40d3562f0cea4711a9a7310cecc3116d406d77514
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 37c98b7a87b13b40cf98e741b20d72a131228bc1c61bd6ec51d07dff0f39eefc599fe30b4fd5ed3bf07e5acf4726bb9df322953c59c02f10f7c2833eb6d53294
|
7
|
+
data.tar.gz: 4bc7f61ba341e57a50c505b7083b585cda3999bb6f6c44e4b64a4435aeca18b8bf1225d199042c2a47fa94fad16567bd3525f48cd3df491b4c870ca21fe37b68
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,30 @@
|
|
1
|
+
# 2.5.3
|
2
|
+
|
3
|
+
Bug fix:
|
4
|
+
- Avoid errors when updating the gem and using persistent cache resulting in null fixed_determinations
|
5
|
+
|
6
|
+
# 2.5.2
|
7
|
+
|
8
|
+
Feature:
|
9
|
+
- Add structured_bucket to Feature
|
10
|
+
- Add `#retrieve` method to the Control
|
11
|
+
- Add optional `feature` argument to `feature_flag_on?` and `which_variant`, to reuse an existing feature
|
12
|
+
|
13
|
+
# 2.5.1
|
14
|
+
|
15
|
+
Feature:
|
16
|
+
- Add explain functionality for determinations
|
17
|
+
|
18
|
+
# 2.5.0
|
19
|
+
|
20
|
+
Feature:
|
21
|
+
- Add fixed determinations
|
22
|
+
|
23
|
+
# 2.4.4
|
24
|
+
|
25
|
+
Bug fix:
|
26
|
+
- Count repeated determinations instead of tracking separately
|
27
|
+
|
1
28
|
# 2.4.3
|
2
29
|
|
3
30
|
Feature:
|
data/lib/determinator.rb
CHANGED
@@ -2,6 +2,8 @@ require 'determinator/version'
|
|
2
2
|
require 'determinator/control'
|
3
3
|
require 'determinator/feature'
|
4
4
|
require 'determinator/target_group'
|
5
|
+
require 'determinator/fixed_determination'
|
6
|
+
require 'determinator/explainer'
|
5
7
|
require 'determinator/cache/fetch_wrapper'
|
6
8
|
require 'determinator/serializers/json'
|
7
9
|
require 'determinator/missing_response'
|
data/lib/determinator/control.rb
CHANGED
@@ -5,10 +5,11 @@ require 'securerandom'
|
|
5
5
|
|
6
6
|
module Determinator
|
7
7
|
class Control
|
8
|
-
attr_reader :retrieval
|
8
|
+
attr_reader :retrieval, :explainer
|
9
9
|
|
10
10
|
def initialize(retrieval:)
|
11
11
|
@retrieval = retrieval
|
12
|
+
@explainer = Determinator::Explainer.new
|
12
13
|
end
|
13
14
|
|
14
15
|
# Creates a new determinator instance which assumes the actor id, guid and properties given
|
@@ -29,10 +30,11 @@ module Determinator
|
|
29
30
|
# @param :id [#to_s] The id of the actor being determinated for
|
30
31
|
# @param :guid [#to_s] The Anonymous id of the actor being determinated for
|
31
32
|
# @param :properties [Hash<Symbol,String>] The properties of this actor which will be used for including this actor or not
|
33
|
+
# @param :feature [Feature] The feature to use instead of retrieving one
|
32
34
|
# @raise [ArgumentError] When the arguments given to this method aren't ever going to produce a useful response
|
33
35
|
# @return [true,false] Whether the feature is on (true) or off (false) for this actor
|
34
|
-
def feature_flag_on?(name, id: nil, guid: nil, properties: {})
|
35
|
-
determinate_and_notice(name, id: id, guid: guid, properties: properties) do |feature|
|
36
|
+
def feature_flag_on?(name, id: nil, guid: nil, properties: {}, feature: nil)
|
37
|
+
determinate_and_notice(name, id: id, guid: guid, properties: properties, feature: feature) do |feature|
|
36
38
|
feature.feature_flag?
|
37
39
|
end
|
38
40
|
end
|
@@ -43,14 +45,29 @@ module Determinator
|
|
43
45
|
# @param :id [#to_s] The id of the actor being determinated for
|
44
46
|
# @param :guid [#to_s] The Anonymous id of the actor being determinated for
|
45
47
|
# @param :properties [Hash<Symbol,String>] The properties of this actor which will be used for including this actor or not
|
48
|
+
# @param :feature [Feature] The feature to use instead of retrieving one
|
46
49
|
# @raise [ArgumentError] When the arguments given to this method aren't ever going to produce a useful response
|
47
50
|
# @return [false,String] Returns false, if the actor is not in this experiment, or otherwise the variant name.
|
48
|
-
def which_variant(name, id: nil, guid: nil, properties: {})
|
49
|
-
determinate_and_notice(name, id: id, guid: guid, properties: properties) do |feature|
|
51
|
+
def which_variant(name, id: nil, guid: nil, properties: {}, feature: nil)
|
52
|
+
determinate_and_notice(name, id: id, guid: guid, properties: properties, feature: feature) do |feature|
|
50
53
|
feature.experiment?
|
51
54
|
end
|
52
55
|
end
|
53
56
|
|
57
|
+
def explain_determination(name, id: nil, guid: nil, properties: {})
|
58
|
+
explainer.explain do
|
59
|
+
determinate_and_notice(name, id: id, guid: guid, properties: properties)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Uses the retrieval (and a cache if set on the Determinator config) to fetch a feature definition.
|
64
|
+
#
|
65
|
+
# @param name [#to_s] The name of the experiment being checked
|
66
|
+
# @return [Feature, MissingResponse] Returns the Feature object, or MissingResponse if the feature is not found.
|
67
|
+
def retrieve(name)
|
68
|
+
Determinator.with_retrieval_cache(name) { retrieval.retrieve(name) }
|
69
|
+
end
|
70
|
+
|
54
71
|
def inspect
|
55
72
|
'#<Determinator::Control>'
|
56
73
|
end
|
@@ -59,8 +76,8 @@ module Determinator
|
|
59
76
|
|
60
77
|
Indicators = Struct.new(:rollout, :variant)
|
61
78
|
|
62
|
-
def determinate_and_notice(name, id:, guid:, properties:)
|
63
|
-
feature
|
79
|
+
def determinate_and_notice(name, id:, guid:, properties:, feature: nil)
|
80
|
+
feature ||= retrieve(name)
|
64
81
|
|
65
82
|
if feature.nil? || feature.is_a?(ErrorResponse) || feature.is_a?(MissingResponse)
|
66
83
|
Determinator.notice_missing_feature(name)
|
@@ -76,10 +93,20 @@ module Determinator
|
|
76
93
|
# Calling method can place constraints on the feature, eg. experiment only
|
77
94
|
return false if block_given? && !yield(feature)
|
78
95
|
|
96
|
+
explainer.log(:start, { feature: feature } )
|
97
|
+
|
79
98
|
# Inactive features are always, always off
|
80
|
-
return false unless feature
|
99
|
+
return false unless feature_active?(feature)
|
100
|
+
|
101
|
+
return override_value(feature, id) if feature_overridden?(feature, id)
|
81
102
|
|
82
|
-
|
103
|
+
fixed_determination = choose_fixed_determination(feature, properties)
|
104
|
+
# Given constraints have specified that this actor's determination should be fixed
|
105
|
+
if fixed_determination
|
106
|
+
return explainer.log(:chosen_fixed_determination, { fixed_determination: fixed_determination }) {
|
107
|
+
fixed_determination_value(feature, fixed_determination)
|
108
|
+
}
|
109
|
+
end
|
83
110
|
|
84
111
|
target_group = choose_target_group(feature, properties)
|
85
112
|
# Given constraints have excluded this actor from this experiment
|
@@ -90,14 +117,18 @@ module Determinator
|
|
90
117
|
return false unless indicators
|
91
118
|
|
92
119
|
# Actor's indicator has excluded them from the feature
|
93
|
-
return false if indicators
|
120
|
+
return false if excluded_from_rollout?(indicators, target_group)
|
94
121
|
|
95
122
|
# Features don't need variant determination and, at this stage,
|
96
123
|
# they have been rolled out to.
|
97
|
-
|
124
|
+
# require_variant_determination?
|
125
|
+
return true unless require_variant_determination?(feature)
|
126
|
+
|
98
127
|
|
99
|
-
variant_for(feature, indicators.variant)
|
100
128
|
|
129
|
+
explainer.log(:chosen_variant) {
|
130
|
+
variant_for(feature, indicators.variant)
|
131
|
+
}
|
101
132
|
rescue ArgumentError
|
102
133
|
raise
|
103
134
|
|
@@ -106,21 +137,92 @@ module Determinator
|
|
106
137
|
false
|
107
138
|
end
|
108
139
|
|
140
|
+
def feature_active?(feature)
|
141
|
+
explainer.log(:feature_active) {
|
142
|
+
feature.active?
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
def feature_overridden?(feature, id)
|
147
|
+
explainer.log(:feature_overridden_for) {
|
148
|
+
feature.overridden_for?(id)
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
def override_value(feature, id)
|
153
|
+
explainer.log(:override_value, { id: id }) {
|
154
|
+
feature.override_value_for(id)
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
def excluded_from_rollout?(indicators, target_group)
|
159
|
+
explainer.log(:excluded_from_rollout, { target_group: target_group } ) {
|
160
|
+
indicators.rollout >= target_group.rollout
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
def require_variant_determination?(feature)
|
165
|
+
explainer.log(:require_variant_determination) {
|
166
|
+
feature.experiment?
|
167
|
+
}
|
168
|
+
end
|
169
|
+
|
170
|
+
def fixed_determination_value(feature, fixed_determination)
|
171
|
+
return false unless fixed_determination.feature_on
|
172
|
+
return true unless feature.experiment?
|
173
|
+
return fixed_determination.variant
|
174
|
+
end
|
175
|
+
|
176
|
+
def choose_fixed_determination(feature, properties)
|
177
|
+
return unless feature.fixed_determinations
|
178
|
+
|
179
|
+
# Keys and values must be strings
|
180
|
+
normalised_properties = normalise_properties(properties)
|
181
|
+
|
182
|
+
feature.fixed_determinations.find do |fd|
|
183
|
+
explainer.log(:possible_match_fixed_determination, { fixed_determination: fd }) {
|
184
|
+
check_fixed_determination(fd, normalised_properties)
|
185
|
+
}
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def check_fixed_determination(fixed_determination, properties)
|
190
|
+
explainer.log(:check_fixed_determination, { fixed_determination: fixed_determination })
|
191
|
+
|
192
|
+
matches_constraints(properties, fixed_determination.constraints)
|
193
|
+
end
|
194
|
+
|
109
195
|
def choose_target_group(feature, properties)
|
110
196
|
# Keys and values must be strings
|
111
|
-
normalised_properties = properties
|
112
|
-
|
197
|
+
normalised_properties = normalise_properties(properties)
|
198
|
+
|
199
|
+
# Must choose target group deterministically, if more than one match
|
200
|
+
explainer.log(:chosen_target_group) {
|
201
|
+
filtered_target_groups(feature, normalised_properties).sort_by { |tg| tg.rollout }.last
|
202
|
+
}
|
203
|
+
end
|
204
|
+
|
205
|
+
def filtered_target_groups(feature, properties)
|
206
|
+
feature.target_groups.select do |tg|
|
207
|
+
explainer.log(:possible_match_target_group, { target_group: tg }) {
|
208
|
+
check_target_group(tg, properties)
|
209
|
+
}
|
113
210
|
end
|
211
|
+
end
|
114
212
|
|
115
|
-
|
116
|
-
|
213
|
+
def check_target_group(target_group, properties)
|
214
|
+
explainer.log(:check_target_group, { target_group: target_group })
|
117
215
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
216
|
+
return false unless target_group.rollout.between?(1, 65_536)
|
217
|
+
|
218
|
+
matches_constraints(properties, target_group.constraints)
|
219
|
+
end
|
220
|
+
|
221
|
+
def matches_constraints(normalised_properties, constraints)
|
222
|
+
constraints.reduce(true) do |fit, (scope, *required)|
|
223
|
+
present = [*normalised_properties[scope]]
|
224
|
+
fit && matches_requirements?(scope, required, present)
|
225
|
+
end
|
124
226
|
end
|
125
227
|
|
126
228
|
def matches_requirements?(scope, required, present)
|
@@ -155,10 +257,12 @@ module Determinator
|
|
155
257
|
def actor_identifier(feature, id, guid)
|
156
258
|
case feature.bucket_type
|
157
259
|
when :id
|
260
|
+
explainer.log(:missing_identifier, { identifier_type: 'ID' }) unless id
|
158
261
|
id
|
159
262
|
when :guid
|
160
263
|
return guid if guid.to_s != ''
|
161
264
|
|
265
|
+
explainer.log(:missing_identifier, { identifier_type: 'GUID' })
|
162
266
|
raise ArgumentError, 'A GUID must always be given for GUID bucketed features'
|
163
267
|
when :fallback
|
164
268
|
identifier = (id || guid).to_s
|
@@ -168,6 +272,7 @@ module Determinator
|
|
168
272
|
when :single
|
169
273
|
SecureRandom.hex(64)
|
170
274
|
else
|
275
|
+
explainer.log(:unknown_bucket, { feature: feature } )
|
171
276
|
Determinator.notice_error "Cannot process the '#{feature.bucket_type}' bucket type found in #{feature.name}"
|
172
277
|
end
|
173
278
|
end
|
@@ -205,5 +310,13 @@ module Determinator
|
|
205
310
|
|
206
311
|
raise ArgumentError, "A variant should have been found by this point, there is a bug in the code."
|
207
312
|
end
|
313
|
+
|
314
|
+
private
|
315
|
+
|
316
|
+
def normalise_properties(properties)
|
317
|
+
properties.each_with_object({}) do |(name, values), hash|
|
318
|
+
hash[name.to_s] = [*values].map(&:to_s)
|
319
|
+
end
|
320
|
+
end
|
208
321
|
end
|
209
322
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'determinator/explainer/messages'
|
2
|
+
|
3
|
+
module Determinator
|
4
|
+
class Explainer
|
5
|
+
attr_accessor :enabled
|
6
|
+
attr_reader :logs
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@logs = []
|
10
|
+
@enabled = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def explain
|
14
|
+
@enabled = true
|
15
|
+
{ outcome: yield, explanation: @logs }
|
16
|
+
ensure
|
17
|
+
@logs = []
|
18
|
+
@enabled = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def log(type, args = {})
|
22
|
+
result = block_given? ? yield : nil
|
23
|
+
|
24
|
+
return result unless @enabled
|
25
|
+
|
26
|
+
result.tap do |r|
|
27
|
+
add(type, r, args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def add(type, result, args = {})
|
34
|
+
template = MESSAGES[type].fetch(result.to_s) { MESSAGES[type][:default] }
|
35
|
+
return unless template
|
36
|
+
|
37
|
+
args = convert_hash(
|
38
|
+
args.merge(result: result)
|
39
|
+
.transform_values { |v| v.respond_to?(:to_explain_params) ? v.to_explain_params : v }
|
40
|
+
)
|
41
|
+
|
42
|
+
@logs << template.dup.tap do |m|
|
43
|
+
m[:title] = format(m[:title], args)
|
44
|
+
m[:subtitle] = format(m[:subtitle], args) if m[:subtitle]
|
45
|
+
end
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def convert_hash(hsh, path = "")
|
50
|
+
hsh.each_with_object({}) do |(k, v), ret|
|
51
|
+
key = "#{path}#{k}"
|
52
|
+
|
53
|
+
if v.is_a?(Hash)
|
54
|
+
ret.merge! convert_hash(v, "#{key}.")
|
55
|
+
else
|
56
|
+
ret[key.to_sym] = v
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Determinator
|
2
|
+
class Explainer
|
3
|
+
MESSAGES = {
|
4
|
+
start: {
|
5
|
+
default: {
|
6
|
+
type: :start,
|
7
|
+
title: 'Determinating %<feature.name>s',
|
8
|
+
subtitle: 'The given ID, GUID and properties will be used to determine which target groups this actor is in, and will deterministically return an outcome for the visibility of this feature for them.'
|
9
|
+
}
|
10
|
+
},
|
11
|
+
feature_active: {
|
12
|
+
'true' => { type: :continue, title: 'Feature is active' },
|
13
|
+
'false' => { type: :fail, title: 'Feature is inactive', subtitle: 'Every actor is excluded' }
|
14
|
+
},
|
15
|
+
feature_overridden_for: {
|
16
|
+
'false' => { type: :pass, title: 'No matching override found' }
|
17
|
+
},
|
18
|
+
override_value: {
|
19
|
+
default: {
|
20
|
+
type: :success,
|
21
|
+
title: 'Matching override found for this actor with id: %<id>s',
|
22
|
+
subtitle: 'Determinator will return "%<result>s" for this actor and this feature in any system that is correctly set up.'
|
23
|
+
}
|
24
|
+
},
|
25
|
+
excluded_from_rollout: {
|
26
|
+
'true' => {
|
27
|
+
type: :fail,
|
28
|
+
title: 'Determinated to be outside the %<target_group.rollout_percent>s',
|
29
|
+
subtitle: 'This actor is excluded'
|
30
|
+
},
|
31
|
+
'false' => {
|
32
|
+
type: :continue,
|
33
|
+
title: 'Determinated to be inside the %<target_group.rollout_percent>s',
|
34
|
+
subtitle: 'This actor is included'
|
35
|
+
}
|
36
|
+
},
|
37
|
+
require_variant_determination: {
|
38
|
+
'false' => {
|
39
|
+
type: :success,
|
40
|
+
title: 'Feature flag on for this actor',
|
41
|
+
subtitle: 'Determinator will return true for this actor and this feature in any system that is correctly set up.'
|
42
|
+
}
|
43
|
+
},
|
44
|
+
missing_identifier: {
|
45
|
+
default: {
|
46
|
+
type: :fail,
|
47
|
+
title: 'No %<identifier_type>s given, cannot determinate',
|
48
|
+
subtitle: 'For %<identifier_type>s bucketed features an %<identifier_type>s must be given to have the possibility of being included.'
|
49
|
+
}
|
50
|
+
},
|
51
|
+
chosen_variant: {
|
52
|
+
default: {
|
53
|
+
type: :success,
|
54
|
+
title: 'In the "%<result>s" variant',
|
55
|
+
subtitle: 'Determinator will return "%<result>s" for this actor and this feature in any system that is correctly set up.'
|
56
|
+
}
|
57
|
+
},
|
58
|
+
unknown_bucket: {
|
59
|
+
default: {
|
60
|
+
type: :fail,
|
61
|
+
title: 'Unknown bucket type',
|
62
|
+
subtitle: 'The bucket type "%<feature.bucket_type>s" is not understood by Determinator. All actors will be excluded.'
|
63
|
+
}
|
64
|
+
},
|
65
|
+
check_target_group: {
|
66
|
+
default: {
|
67
|
+
type: :target_group,
|
68
|
+
title: 'Checking "%<target_group.name>s" target group',
|
69
|
+
subtitle: 'An actor must match at least one non-zero target group in order to be included.'
|
70
|
+
}
|
71
|
+
},
|
72
|
+
possible_match_target_group: {
|
73
|
+
'true' => {
|
74
|
+
type: :continue,
|
75
|
+
title: 'Matches the "%<target_group.name>s" target group',
|
76
|
+
subtitle: 'Matching this target group allows this actor a %<target_group.rollout_percent>s percent chance of being included.'
|
77
|
+
},
|
78
|
+
'false' => {
|
79
|
+
type: :pass,
|
80
|
+
title: 'Didn\'t match the "%<target_group.name>s" target group',
|
81
|
+
subtitle: 'Actor can\'t be included as part of this target group.'
|
82
|
+
}
|
83
|
+
},
|
84
|
+
chosen_target_group: {
|
85
|
+
'' => {
|
86
|
+
type: :fail,
|
87
|
+
title: 'No matching target groups',
|
88
|
+
subtitle: 'No matching target groups have a rollout larger than 0 percent. This actor is excluded.'
|
89
|
+
},
|
90
|
+
default: {
|
91
|
+
type: :info,
|
92
|
+
title: '%<result.rollout_percent>s percent chance of being included',
|
93
|
+
subtitle: 'The largest matching rollout percentage is %<result.rollout_percent>s, giving this actor a percent chance of being included.'
|
94
|
+
}
|
95
|
+
},
|
96
|
+
check_fixed_determination: {
|
97
|
+
default: {
|
98
|
+
type: :target_group,
|
99
|
+
title: 'Checking "%<fixed_determination.name>s" fixed determination',
|
100
|
+
subtitle: 'Matching an actor based on the constraints provided.'
|
101
|
+
}
|
102
|
+
},
|
103
|
+
possible_match_fixed_determination: {
|
104
|
+
'false' => {
|
105
|
+
type: :pass,
|
106
|
+
title: 'Didn\'t match the "%<fixed_determination.name>s" fixed determination',
|
107
|
+
subtitle: 'Actor can\'t be included as part of this fixed determination.'
|
108
|
+
}
|
109
|
+
},
|
110
|
+
chosen_fixed_determination: {
|
111
|
+
default: {
|
112
|
+
type: :success,
|
113
|
+
title: 'Matching fixed determination found for this actor with name: "%<fixed_determination.name>s"',
|
114
|
+
subtitle: 'Determinator will return "%<result>s" for this actor and this feature in any system that is correctly set up.'
|
115
|
+
},
|
116
|
+
}
|
117
|
+
}.freeze
|
118
|
+
end
|
119
|
+
end
|
data/lib/determinator/feature.rb
CHANGED
@@ -3,16 +3,18 @@ module Determinator
|
|
3
3
|
#
|
4
4
|
# @attr_reader [nil,Hash<String,Integer>] variants The variants for this experiment, with the name of the variant as the key and the weight as the value. Will be nil for non-experiments.
|
5
5
|
class Feature
|
6
|
-
attr_reader :name, :identifier, :bucket_type, :variants, :target_groups, :active, :winning_variant
|
6
|
+
attr_reader :name, :identifier, :bucket_type, :structured_bucket, :variants, :target_groups, :fixed_determinations, :active, :winning_variant
|
7
7
|
|
8
|
-
def initialize(name:, identifier:, bucket_type:, target_groups:, variants: {}, overrides: {}, active: false, winning_variant: nil)
|
8
|
+
def initialize(name:, identifier:, bucket_type:, target_groups:, structured_bucket: nil, fixed_determinations: [], variants: {}, overrides: {}, active: false, winning_variant: nil)
|
9
9
|
@name = name.to_s
|
10
|
-
@identifier =
|
10
|
+
@identifier = identifier.to_s
|
11
11
|
@variants = variants
|
12
12
|
@target_groups = parse_target_groups(target_groups)
|
13
|
+
@fixed_determinations = parse_fixed_determinations(fixed_determinations)
|
13
14
|
@winning_variant = parse_outcome(winning_variant, allow_exclusion: false)
|
14
15
|
@active = active
|
15
16
|
@bucket_type = bucket_type.to_sym
|
17
|
+
@structured_bucket = structured_bucket
|
16
18
|
|
17
19
|
# To prevent confusion between actor id data types
|
18
20
|
@overrides = overrides.each_with_object({}) do |(identifier, outcome), hash|
|
@@ -35,6 +37,11 @@ module Determinator
|
|
35
37
|
variants.empty?
|
36
38
|
end
|
37
39
|
|
40
|
+
# @return [true,false] Is this feature using structured identification?
|
41
|
+
def structured?
|
42
|
+
!!structured_bucket && !structured_bucket.empty?
|
43
|
+
end
|
44
|
+
|
38
45
|
# Is this feature overridden for the given actor id?
|
39
46
|
#
|
40
47
|
# @return [true,false] Whether this feature is overridden for this actor
|
@@ -57,6 +64,10 @@ module Determinator
|
|
57
64
|
Marshal.dump(self) == Marshal.dump(other)
|
58
65
|
end
|
59
66
|
|
67
|
+
def to_explain_params
|
68
|
+
{ name: name, identifier: identifier, bucket_type: bucket_type }
|
69
|
+
end
|
70
|
+
|
60
71
|
private
|
61
72
|
|
62
73
|
attr_reader :overrides
|
@@ -71,15 +82,46 @@ module Determinator
|
|
71
82
|
constraints = target_group['constraints'].to_h
|
72
83
|
|
73
84
|
TargetGroup.new(
|
85
|
+
name: target_group['name'],
|
74
86
|
rollout: target_group['rollout'].to_i,
|
75
|
-
constraints: constraints
|
76
|
-
hash[key.to_s] = [*value].map(&:to_s)
|
77
|
-
end
|
87
|
+
constraints: parse_constraints(constraints)
|
78
88
|
)
|
79
89
|
|
80
90
|
# Invalid target groups are ignored
|
81
91
|
rescue
|
82
92
|
nil
|
83
93
|
end
|
94
|
+
|
95
|
+
def parse_fixed_determinations(fixed_determinations)
|
96
|
+
fixed_determinations.map(&method(:parse_fixed_determination)).compact
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_fixed_determination(fixed_determination)
|
100
|
+
return fixed_determination if fixed_determination.is_a? FixedDetermination
|
101
|
+
|
102
|
+
variant = fixed_determination['variant']
|
103
|
+
return nil if variant && !variants.keys.include?(variant)
|
104
|
+
|
105
|
+
# if a variant is present the fixed determination should always be on
|
106
|
+
return nil if variant && !fixed_determination['feature_on']
|
107
|
+
|
108
|
+
constraints = fixed_determination['constraints'].to_h
|
109
|
+
|
110
|
+
FixedDetermination.new(
|
111
|
+
name: fixed_determination['name'],
|
112
|
+
feature_on: fixed_determination['feature_on'],
|
113
|
+
variant: variant,
|
114
|
+
constraints: parse_constraints(constraints)
|
115
|
+
)
|
116
|
+
# Invalid fixed determinations are ignored
|
117
|
+
rescue
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
|
121
|
+
def parse_constraints(constraints)
|
122
|
+
constraints.each_with_object({}) do |(key, value), hash|
|
123
|
+
hash[key.to_s] = [*value].map(&:to_s)
|
124
|
+
end
|
125
|
+
end
|
84
126
|
end
|
85
127
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Determinator
|
2
|
+
class FixedDetermination
|
3
|
+
attr_reader :name, :feature_on, :variant, :constraints
|
4
|
+
|
5
|
+
def initialize(feature_on:, variant:, name: '', constraints: {})
|
6
|
+
@name = name
|
7
|
+
@feature_on = feature_on
|
8
|
+
@variant = variant
|
9
|
+
@constraints = constraints
|
10
|
+
end
|
11
|
+
|
12
|
+
def inspect
|
13
|
+
"<feature_on: #{feature_on}, variant: #{variant}, constraints: #{constraints}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_explain_params
|
17
|
+
{ name: name }
|
18
|
+
end
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
return false unless other.is_a?(self.class)
|
22
|
+
other.feature_on == feature_on && other.variant == variant && other.constraints == constraints
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -12,14 +12,16 @@ module Determinator
|
|
12
12
|
obj = string_or_hash.is_a?(Hash) ? string_or_hash : ::JSON.parse(string_or_hash)
|
13
13
|
|
14
14
|
Determinator::Feature.new(
|
15
|
-
name:
|
16
|
-
identifier:
|
17
|
-
bucket_type:
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
15
|
+
name: obj['name'],
|
16
|
+
identifier: obj['identifier'],
|
17
|
+
bucket_type: obj['bucket_type'],
|
18
|
+
structured_bucket: obj['structured_bucket'],
|
19
|
+
active: (obj['active'] === true),
|
20
|
+
target_groups: obj['target_groups'],
|
21
|
+
fixed_determinations: obj['fixed_determinations'].to_a,
|
22
|
+
variants: obj['variants'].to_h,
|
23
|
+
overrides: obj['overrides'].to_h,
|
24
|
+
winning_variant: obj['winning_variant'].to_s,
|
23
25
|
)
|
24
26
|
end
|
25
27
|
end
|
@@ -1,8 +1,9 @@
|
|
1
1
|
module Determinator
|
2
2
|
class TargetGroup
|
3
|
-
attr_reader :rollout, :constraints
|
3
|
+
attr_reader :name, :rollout, :constraints
|
4
4
|
|
5
|
-
def initialize(rollout:, constraints: {})
|
5
|
+
def initialize(rollout:, name: '', constraints: {})
|
6
|
+
@name = name
|
6
7
|
@rollout = rollout
|
7
8
|
@constraints = constraints
|
8
9
|
end
|
@@ -15,9 +16,16 @@ module Determinator
|
|
15
16
|
Rational(rollout, 65_536)
|
16
17
|
end
|
17
18
|
|
19
|
+
def humanize_percentage
|
20
|
+
(rollout_percent * 100).to_f.round(1)
|
21
|
+
end
|
22
|
+
|
18
23
|
def inspect
|
19
|
-
|
20
|
-
|
24
|
+
"<TG name:'#{name}': #{humanize_percentage}% of those matching: #{constraints}>"
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_explain_params
|
28
|
+
{ name: name, rollout_percent: humanize_percentage }
|
21
29
|
end
|
22
30
|
|
23
31
|
def ==(other)
|
@@ -13,6 +13,12 @@ module Determinator
|
|
13
13
|
def ==(other)
|
14
14
|
id == other.id && guid == other.guid && feature_id == other.feature_id && determination == other.determination
|
15
15
|
end
|
16
|
+
|
17
|
+
alias eql? ==
|
18
|
+
|
19
|
+
def hash
|
20
|
+
[id, guid, feature_id, determination].hash
|
21
|
+
end
|
16
22
|
end
|
17
23
|
end
|
18
24
|
end
|
@@ -7,19 +7,21 @@ module Determinator
|
|
7
7
|
attr_reader :type, :determinations
|
8
8
|
|
9
9
|
def initialize(type)
|
10
|
-
@determinations =
|
10
|
+
@determinations = Hash.new(0)
|
11
11
|
@type = type
|
12
12
|
@monotonic_start = now
|
13
13
|
@start = Time.now
|
14
14
|
end
|
15
15
|
|
16
16
|
def track(id, guid, feature, determination)
|
17
|
-
determinations
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
determinations[
|
18
|
+
Determinator::Tracking::Determination.new(
|
19
|
+
id: id,
|
20
|
+
guid: guid,
|
21
|
+
feature_id: feature.identifier,
|
22
|
+
determination: determination
|
23
|
+
)
|
24
|
+
] += 1
|
23
25
|
end
|
24
26
|
|
25
27
|
def finish!(endpoint:, error:, **attributes)
|
data/lib/determinator/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: determinator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.5.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- JP Hastings-Spital
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-07-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -209,7 +209,10 @@ files:
|
|
209
209
|
- lib/determinator/cache/fetch_wrapper.rb
|
210
210
|
- lib/determinator/control.rb
|
211
211
|
- lib/determinator/error_response.rb
|
212
|
+
- lib/determinator/explainer.rb
|
213
|
+
- lib/determinator/explainer/messages.rb
|
212
214
|
- lib/determinator/feature.rb
|
215
|
+
- lib/determinator/fixed_determination.rb
|
213
216
|
- lib/determinator/missing_response.rb
|
214
217
|
- lib/determinator/retrieve/dynaconf.rb
|
215
218
|
- lib/determinator/retrieve/file.rb
|