determinator 2.5.0 → 2.6.0
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 +4 -3
- data/lib/determinator/control.rb +110 -23
- data/lib/determinator/explainer.rb +61 -0
- data/lib/determinator/explainer/messages.rb +119 -0
- data/lib/determinator/feature.rb +14 -2
- data/lib/determinator/fixed_determination.rb +7 -2
- data/lib/determinator/serializers/json.rb +1 -0
- data/lib/determinator/target_group.rb +12 -4
- data/lib/determinator/version.rb +1 -1
- data/lib/rspec/determinator.rb +5 -2
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9fe6fd684f160e1d955e9528bd225942068320361d554102b63667125503e610
|
4
|
+
data.tar.gz: f70731d2a63bffa170ee0d545fa9efbc2271fc32995c6635fc4feea33ac38c65
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0debe679036a419091917171e90ce4360ec0ea8afdc3d2b0d536b1796cd93b2ffe1fe3b6dc933cfe03642be61bde94c12b471a6aca2d7a04c087f24e6b6b8e94
|
7
|
+
data.tar.gz: 423c16ff61e48e2da9e8883329b70a031819be4741b2f9031fe0cd6c1681e0df43d52f98028b5002a6e6ad82f6774aa24fed5ddf227b56a8d75bb59e3a81ccde
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,30 @@
|
|
1
|
+
# 2.6.0
|
2
|
+
|
3
|
+
Interface change:
|
4
|
+
- A `feature_cache` is now required to use Determinator. See the `examples/determinator-rails/config/initializers/determinator.rb` for a quick start.
|
5
|
+
|
6
|
+
# 2.5.4
|
7
|
+
|
8
|
+
Bug fix:
|
9
|
+
- Apply app_version logic to structured request.app_version too
|
10
|
+
|
11
|
+
# 2.5.3
|
12
|
+
|
13
|
+
Bug fix:
|
14
|
+
- Avoid errors when updating the gem and using persistent cache resulting in null fixed_determinations
|
15
|
+
|
16
|
+
# 2.5.2
|
17
|
+
|
18
|
+
Feature:
|
19
|
+
- Add structured_bucket to Feature
|
20
|
+
- Add `#retrieve` method to the Control
|
21
|
+
- Add optional `feature` argument to `feature_flag_on?` and `which_variant`, to reuse an existing feature
|
22
|
+
|
23
|
+
# 2.5.1
|
24
|
+
|
25
|
+
Feature:
|
26
|
+
- Add explain functionality for determinations
|
27
|
+
|
1
28
|
# 2.5.0
|
2
29
|
|
3
30
|
Feature:
|
data/lib/determinator.rb
CHANGED
@@ -3,6 +3,7 @@ require 'determinator/control'
|
|
3
3
|
require 'determinator/feature'
|
4
4
|
require 'determinator/target_group'
|
5
5
|
require 'determinator/fixed_determination'
|
6
|
+
require 'determinator/explainer'
|
6
7
|
require 'determinator/cache/fetch_wrapper'
|
7
8
|
require 'determinator/serializers/json'
|
8
9
|
require 'determinator/missing_response'
|
@@ -14,13 +15,13 @@ module Determinator
|
|
14
15
|
class << self
|
15
16
|
attr_reader :feature_cache, :retrieval
|
16
17
|
# @param :retrieval [Determinator::Retrieve::Routemaster] A retrieval instance for Features
|
18
|
+
# @param :feature_cache [#call] a caching proc, accepting a feature name, which will return the named feature or yield (and store) if not available
|
17
19
|
# @param :errors [#call, nil] a proc, accepting an error, which will be called with any errors which occur while determinating
|
18
20
|
# @param :missing_feature [#call, nil] a proc, accepting a feature name, which will be called any time a feature is requested but isn't available
|
19
|
-
|
20
|
-
def configure(retrieval:, errors: nil, missing_feature: nil, feature_cache: nil)
|
21
|
+
def configure(retrieval:, feature_cache:, errors: nil, missing_feature: nil)
|
21
22
|
self.on_error(&errors) if errors
|
22
23
|
self.on_missing_feature(&missing_feature) if missing_feature
|
23
|
-
@feature_cache = feature_cache
|
24
|
+
@feature_cache = feature_cache
|
24
25
|
@retrieval = retrieval
|
25
26
|
@instance = Control.new(retrieval: retrieval)
|
26
27
|
end
|
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,17 +93,19 @@ 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)
|
81
100
|
|
82
|
-
return feature
|
101
|
+
return override_value(feature, id) if feature_overridden?(feature, id)
|
83
102
|
|
84
103
|
fixed_determination = choose_fixed_determination(feature, properties)
|
85
104
|
# Given constraints have specified that this actor's determination should be fixed
|
86
105
|
if fixed_determination
|
87
|
-
return
|
88
|
-
|
89
|
-
|
106
|
+
return explainer.log(:chosen_fixed_determination, { fixed_determination: fixed_determination }) {
|
107
|
+
fixed_determination_value(feature, fixed_determination)
|
108
|
+
}
|
90
109
|
end
|
91
110
|
|
92
111
|
target_group = choose_target_group(feature, properties)
|
@@ -98,14 +117,18 @@ module Determinator
|
|
98
117
|
return false unless indicators
|
99
118
|
|
100
119
|
# Actor's indicator has excluded them from the feature
|
101
|
-
return false if indicators
|
120
|
+
return false if excluded_from_rollout?(indicators, target_group)
|
102
121
|
|
103
122
|
# Features don't need variant determination and, at this stage,
|
104
123
|
# they have been rolled out to.
|
105
|
-
|
124
|
+
# require_variant_determination?
|
125
|
+
return true unless require_variant_determination?(feature)
|
106
126
|
|
107
|
-
variant_for(feature, indicators.variant)
|
108
127
|
|
128
|
+
|
129
|
+
explainer.log(:chosen_variant) {
|
130
|
+
variant_for(feature, indicators.variant)
|
131
|
+
}
|
109
132
|
rescue ArgumentError
|
110
133
|
raise
|
111
134
|
|
@@ -114,25 +137,85 @@ module Determinator
|
|
114
137
|
false
|
115
138
|
end
|
116
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
|
+
|
117
176
|
def choose_fixed_determination(feature, properties)
|
177
|
+
return unless feature.fixed_determinations
|
178
|
+
|
118
179
|
# Keys and values must be strings
|
119
180
|
normalised_properties = normalise_properties(properties)
|
120
181
|
|
121
|
-
feature.fixed_determinations.find
|
122
|
-
|
123
|
-
|
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)
|
124
193
|
end
|
125
194
|
|
126
195
|
def choose_target_group(feature, properties)
|
127
196
|
# Keys and values must be strings
|
128
197
|
normalised_properties = normalise_properties(properties)
|
129
198
|
|
130
|
-
|
131
|
-
|
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
|
+
}
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def check_target_group(target_group, properties)
|
214
|
+
explainer.log(:check_target_group, { target_group: target_group })
|
215
|
+
|
216
|
+
return false unless target_group.rollout.between?(1, 65_536)
|
132
217
|
|
133
|
-
|
134
|
-
# Must choose target group deterministically, if more than one match
|
135
|
-
}.sort_by { |tg| tg.rollout }.last
|
218
|
+
matches_constraints(properties, target_group.constraints)
|
136
219
|
end
|
137
220
|
|
138
221
|
def matches_constraints(normalised_properties, constraints)
|
@@ -145,6 +228,7 @@ module Determinator
|
|
145
228
|
def matches_requirements?(scope, required, present)
|
146
229
|
case scope
|
147
230
|
when "app_version" then has_any_app_version?(required, present)
|
231
|
+
when "request.app_version" then has_any_app_version?(required, present)
|
148
232
|
else has_any?(required, present)
|
149
233
|
end
|
150
234
|
end
|
@@ -174,10 +258,12 @@ module Determinator
|
|
174
258
|
def actor_identifier(feature, id, guid)
|
175
259
|
case feature.bucket_type
|
176
260
|
when :id
|
261
|
+
explainer.log(:missing_identifier, { identifier_type: 'ID' }) unless id
|
177
262
|
id
|
178
263
|
when :guid
|
179
264
|
return guid if guid.to_s != ''
|
180
265
|
|
266
|
+
explainer.log(:missing_identifier, { identifier_type: 'GUID' })
|
181
267
|
raise ArgumentError, 'A GUID must always be given for GUID bucketed features'
|
182
268
|
when :fallback
|
183
269
|
identifier = (id || guid).to_s
|
@@ -187,6 +273,7 @@ module Determinator
|
|
187
273
|
when :single
|
188
274
|
SecureRandom.hex(64)
|
189
275
|
else
|
276
|
+
explainer.log(:unknown_bucket, { feature: feature } )
|
190
277
|
Determinator.notice_error "Cannot process the '#{feature.bucket_type}' bucket type found in #{feature.name}"
|
191
278
|
end
|
192
279
|
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,9 +3,9 @@ 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, :fixed_determinations, :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:, fixed_determinations: [], 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
10
|
@identifier = identifier.to_s
|
11
11
|
@variants = variants
|
@@ -14,6 +14,7 @@ module Determinator
|
|
14
14
|
@winning_variant = parse_outcome(winning_variant, allow_exclusion: false)
|
15
15
|
@active = active
|
16
16
|
@bucket_type = bucket_type.to_sym
|
17
|
+
@structured_bucket = structured_bucket
|
17
18
|
|
18
19
|
# To prevent confusion between actor id data types
|
19
20
|
@overrides = overrides.each_with_object({}) do |(identifier, outcome), hash|
|
@@ -36,6 +37,11 @@ module Determinator
|
|
36
37
|
variants.empty?
|
37
38
|
end
|
38
39
|
|
40
|
+
# @return [true,false] Is this feature using structured identification?
|
41
|
+
def structured?
|
42
|
+
!!structured_bucket && !structured_bucket.empty?
|
43
|
+
end
|
44
|
+
|
39
45
|
# Is this feature overridden for the given actor id?
|
40
46
|
#
|
41
47
|
# @return [true,false] Whether this feature is overridden for this actor
|
@@ -58,6 +64,10 @@ module Determinator
|
|
58
64
|
Marshal.dump(self) == Marshal.dump(other)
|
59
65
|
end
|
60
66
|
|
67
|
+
def to_explain_params
|
68
|
+
{ name: name, identifier: identifier, bucket_type: bucket_type }
|
69
|
+
end
|
70
|
+
|
61
71
|
private
|
62
72
|
|
63
73
|
attr_reader :overrides
|
@@ -72,6 +82,7 @@ module Determinator
|
|
72
82
|
constraints = target_group['constraints'].to_h
|
73
83
|
|
74
84
|
TargetGroup.new(
|
85
|
+
name: target_group['name'],
|
75
86
|
rollout: target_group['rollout'].to_i,
|
76
87
|
constraints: parse_constraints(constraints)
|
77
88
|
)
|
@@ -97,6 +108,7 @@ module Determinator
|
|
97
108
|
constraints = fixed_determination['constraints'].to_h
|
98
109
|
|
99
110
|
FixedDetermination.new(
|
111
|
+
name: fixed_determination['name'],
|
100
112
|
feature_on: fixed_determination['feature_on'],
|
101
113
|
variant: variant,
|
102
114
|
constraints: parse_constraints(constraints)
|
@@ -1,8 +1,9 @@
|
|
1
1
|
module Determinator
|
2
2
|
class FixedDetermination
|
3
|
-
attr_reader :feature_on, :variant, :constraints
|
3
|
+
attr_reader :name, :feature_on, :variant, :constraints
|
4
4
|
|
5
|
-
def initialize(feature_on:, variant:, constraints: {})
|
5
|
+
def initialize(feature_on:, variant:, name: '', constraints: {})
|
6
|
+
@name = name
|
6
7
|
@feature_on = feature_on
|
7
8
|
@variant = variant
|
8
9
|
@constraints = constraints
|
@@ -12,6 +13,10 @@ module Determinator
|
|
12
13
|
"<feature_on: #{feature_on}, variant: #{variant}, constraints: #{constraints}"
|
13
14
|
end
|
14
15
|
|
16
|
+
def to_explain_params
|
17
|
+
{ name: name }
|
18
|
+
end
|
19
|
+
|
15
20
|
def ==(other)
|
16
21
|
return false unless other.is_a?(self.class)
|
17
22
|
other.feature_on == feature_on && other.variant == variant && other.constraints == constraints
|
@@ -15,6 +15,7 @@ module Determinator
|
|
15
15
|
name: obj['name'],
|
16
16
|
identifier: obj['identifier'],
|
17
17
|
bucket_type: obj['bucket_type'],
|
18
|
+
structured_bucket: obj['structured_bucket'],
|
18
19
|
active: (obj['active'] === true),
|
19
20
|
target_groups: obj['target_groups'],
|
20
21
|
fixed_determinations: obj['fixed_determinations'].to_a,
|
@@ -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)
|
data/lib/determinator/version.rb
CHANGED
data/lib/rspec/determinator.rb
CHANGED
@@ -3,6 +3,9 @@ require_relative '../determinator/retrieve/in_memory_retriever'
|
|
3
3
|
|
4
4
|
module RSpec
|
5
5
|
module Determinator
|
6
|
+
|
7
|
+
DO_NOT_USE_IN_PRODUCTION_CODE_NULL_FEATURE_CACHE = -> (name, &block) { block.call(name) }
|
8
|
+
|
6
9
|
def self.included(by)
|
7
10
|
by.extend(DSL)
|
8
11
|
|
@@ -12,10 +15,10 @@ module RSpec
|
|
12
15
|
old_retriever = ::Determinator.instance.retrieval
|
13
16
|
begin
|
14
17
|
fake_retriever.clear!
|
15
|
-
::Determinator.configure(retrieval: fake_retriever)
|
18
|
+
::Determinator.configure(retrieval: fake_retriever, feature_cache: DO_NOT_USE_IN_PRODUCTION_CODE_NULL_FEATURE_CACHE)
|
16
19
|
example.run
|
17
20
|
ensure
|
18
|
-
::Determinator.configure(retrieval: old_retriever)
|
21
|
+
::Determinator.configure(retrieval: old_retriever, feature_cache: DO_NOT_USE_IN_PRODUCTION_CODE_NULL_FEATURE_CACHE)
|
19
22
|
end
|
20
23
|
end
|
21
24
|
end
|
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.6.0
|
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:
|
11
|
+
date: 2021-01-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -209,6 +209,8 @@ 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
|
213
215
|
- lib/determinator/fixed_determination.rb
|
214
216
|
- lib/determinator/missing_response.rb
|
@@ -247,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
247
249
|
- !ruby/object:Gem::Version
|
248
250
|
version: '0'
|
249
251
|
requirements: []
|
250
|
-
rubygems_version: 3.0.
|
252
|
+
rubygems_version: 3.0.3
|
251
253
|
signing_key:
|
252
254
|
specification_version: 4
|
253
255
|
summary: Determine which experiments and features a specific actor should see.
|