determinator 2.5.0 → 2.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/lib/determinator.rb +1 -0
- data/lib/determinator/control.rb +91 -17
- data/lib/determinator/explainer.rb +61 -0
- data/lib/determinator/explainer/messages.rb +119 -0
- data/lib/determinator/feature.rb +6 -0
- data/lib/determinator/fixed_determination.rb +7 -2
- data/lib/determinator/target_group.rb +12 -4
- data/lib/determinator/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4466edc3643125ca6b84591b4db51083d215c5c6ea8201694e99d16e403eab24
|
4
|
+
data.tar.gz: 326d45e3f0ac249e5a358ee3911c1da4da01db81c14ab1b3c827a8fd5d0f4ef9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c83ad23860f0d5565618e3cfbf1f9975763f3f64c0db49668fa2259aae06c972689e000538a734571906edbf766c8d1d46e58ad2d7c657461c76759768f37bfa
|
7
|
+
data.tar.gz: 777556d4606d6a10514b5f7c6311e498117845f9bf5bc3bd1be7f8c47cee837fb17243348d11ee0f2fff3931493e97e65cb06608dcbc9b435916aabad07e69f1
|
data/CHANGELOG.md
CHANGED
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'
|
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
|
@@ -51,6 +52,12 @@ module Determinator
|
|
51
52
|
end
|
52
53
|
end
|
53
54
|
|
55
|
+
def explain_determination(name, id: nil, guid: nil, properties: {})
|
56
|
+
explainer.explain do
|
57
|
+
determinate_and_notice(name, id: id, guid: guid, properties: properties)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
54
61
|
def inspect
|
55
62
|
'#<Determinator::Control>'
|
56
63
|
end
|
@@ -76,17 +83,19 @@ module Determinator
|
|
76
83
|
# Calling method can place constraints on the feature, eg. experiment only
|
77
84
|
return false if block_given? && !yield(feature)
|
78
85
|
|
86
|
+
explainer.log(:start, { feature: feature } )
|
87
|
+
|
79
88
|
# Inactive features are always, always off
|
80
|
-
return false unless feature
|
89
|
+
return false unless feature_active?(feature)
|
81
90
|
|
82
|
-
return feature
|
91
|
+
return override_value(feature, id) if feature_overridden?(feature, id)
|
83
92
|
|
84
93
|
fixed_determination = choose_fixed_determination(feature, properties)
|
85
94
|
# Given constraints have specified that this actor's determination should be fixed
|
86
95
|
if fixed_determination
|
87
|
-
return
|
88
|
-
|
89
|
-
|
96
|
+
return explainer.log(:chosen_fixed_determination, { fixed_determination: fixed_determination }) {
|
97
|
+
fixed_determination_value(feature, fixed_determination)
|
98
|
+
}
|
90
99
|
end
|
91
100
|
|
92
101
|
target_group = choose_target_group(feature, properties)
|
@@ -98,14 +107,18 @@ module Determinator
|
|
98
107
|
return false unless indicators
|
99
108
|
|
100
109
|
# Actor's indicator has excluded them from the feature
|
101
|
-
return false if indicators
|
110
|
+
return false if excluded_from_rollout?(indicators, target_group)
|
102
111
|
|
103
112
|
# Features don't need variant determination and, at this stage,
|
104
113
|
# they have been rolled out to.
|
105
|
-
|
114
|
+
# require_variant_determination?
|
115
|
+
return true unless require_variant_determination?(feature)
|
106
116
|
|
107
|
-
variant_for(feature, indicators.variant)
|
108
117
|
|
118
|
+
|
119
|
+
explainer.log(:chosen_variant) {
|
120
|
+
variant_for(feature, indicators.variant)
|
121
|
+
}
|
109
122
|
rescue ArgumentError
|
110
123
|
raise
|
111
124
|
|
@@ -114,25 +127,83 @@ module Determinator
|
|
114
127
|
false
|
115
128
|
end
|
116
129
|
|
130
|
+
def feature_active?(feature)
|
131
|
+
explainer.log(:feature_active) {
|
132
|
+
feature.active?
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
def feature_overridden?(feature, id)
|
137
|
+
explainer.log(:feature_overridden_for) {
|
138
|
+
feature.overridden_for?(id)
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
def override_value(feature, id)
|
143
|
+
explainer.log(:override_value, { id: id }) {
|
144
|
+
feature.override_value_for(id)
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
def excluded_from_rollout?(indicators, target_group)
|
149
|
+
explainer.log(:excluded_from_rollout, { target_group: target_group } ) {
|
150
|
+
indicators.rollout >= target_group.rollout
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def require_variant_determination?(feature)
|
155
|
+
explainer.log(:require_variant_determination) {
|
156
|
+
feature.experiment?
|
157
|
+
}
|
158
|
+
end
|
159
|
+
|
160
|
+
def fixed_determination_value(feature, fixed_determination)
|
161
|
+
return false unless fixed_determination.feature_on
|
162
|
+
return true unless feature.experiment?
|
163
|
+
return fixed_determination.variant
|
164
|
+
end
|
165
|
+
|
117
166
|
def choose_fixed_determination(feature, properties)
|
118
167
|
# Keys and values must be strings
|
119
168
|
normalised_properties = normalise_properties(properties)
|
120
169
|
|
121
|
-
feature.fixed_determinations.find
|
122
|
-
|
123
|
-
|
170
|
+
feature.fixed_determinations.find do |fd|
|
171
|
+
explainer.log(:possible_match_fixed_determination, { fixed_determination: fd }) {
|
172
|
+
check_fixed_determination(fd, normalised_properties)
|
173
|
+
}
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def check_fixed_determination(fixed_determination, properties)
|
178
|
+
explainer.log(:check_fixed_determination, { fixed_determination: fixed_determination })
|
179
|
+
|
180
|
+
matches_constraints(properties, fixed_determination.constraints)
|
124
181
|
end
|
125
182
|
|
126
183
|
def choose_target_group(feature, properties)
|
127
184
|
# Keys and values must be strings
|
128
185
|
normalised_properties = normalise_properties(properties)
|
129
186
|
|
130
|
-
|
131
|
-
|
187
|
+
# Must choose target group deterministically, if more than one match
|
188
|
+
explainer.log(:chosen_target_group) {
|
189
|
+
filtered_target_groups(feature, normalised_properties).sort_by { |tg| tg.rollout }.last
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
def filtered_target_groups(feature, properties)
|
194
|
+
feature.target_groups.select do |tg|
|
195
|
+
explainer.log(:possible_match_target_group, { target_group: tg }) {
|
196
|
+
check_target_group(tg, properties)
|
197
|
+
}
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def check_target_group(target_group, properties)
|
202
|
+
explainer.log(:check_target_group, { target_group: target_group })
|
203
|
+
|
204
|
+
return false unless target_group.rollout.between?(1, 65_536)
|
132
205
|
|
133
|
-
|
134
|
-
# Must choose target group deterministically, if more than one match
|
135
|
-
}.sort_by { |tg| tg.rollout }.last
|
206
|
+
matches_constraints(properties, target_group.constraints)
|
136
207
|
end
|
137
208
|
|
138
209
|
def matches_constraints(normalised_properties, constraints)
|
@@ -174,10 +245,12 @@ module Determinator
|
|
174
245
|
def actor_identifier(feature, id, guid)
|
175
246
|
case feature.bucket_type
|
176
247
|
when :id
|
248
|
+
explainer.log(:missing_identifier, { identifier_type: 'ID' }) unless id
|
177
249
|
id
|
178
250
|
when :guid
|
179
251
|
return guid if guid.to_s != ''
|
180
252
|
|
253
|
+
explainer.log(:missing_identifier, { identifier_type: 'GUID' })
|
181
254
|
raise ArgumentError, 'A GUID must always be given for GUID bucketed features'
|
182
255
|
when :fallback
|
183
256
|
identifier = (id || guid).to_s
|
@@ -187,6 +260,7 @@ module Determinator
|
|
187
260
|
when :single
|
188
261
|
SecureRandom.hex(64)
|
189
262
|
else
|
263
|
+
explainer.log(:unknown_bucket, { feature: feature } )
|
190
264
|
Determinator.notice_error "Cannot process the '#{feature.bucket_type}' bucket type found in #{feature.name}"
|
191
265
|
end
|
192
266
|
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
@@ -58,6 +58,10 @@ module Determinator
|
|
58
58
|
Marshal.dump(self) == Marshal.dump(other)
|
59
59
|
end
|
60
60
|
|
61
|
+
def to_explain_params
|
62
|
+
{ name: name, identifier: identifier, bucket_type: bucket_type }
|
63
|
+
end
|
64
|
+
|
61
65
|
private
|
62
66
|
|
63
67
|
attr_reader :overrides
|
@@ -72,6 +76,7 @@ module Determinator
|
|
72
76
|
constraints = target_group['constraints'].to_h
|
73
77
|
|
74
78
|
TargetGroup.new(
|
79
|
+
name: target_group['name'],
|
75
80
|
rollout: target_group['rollout'].to_i,
|
76
81
|
constraints: parse_constraints(constraints)
|
77
82
|
)
|
@@ -97,6 +102,7 @@ module Determinator
|
|
97
102
|
constraints = fixed_determination['constraints'].to_h
|
98
103
|
|
99
104
|
FixedDetermination.new(
|
105
|
+
name: fixed_determination['name'],
|
100
106
|
feature_on: fixed_determination['feature_on'],
|
101
107
|
variant: variant,
|
102
108
|
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
|
@@ -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
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.5.
|
4
|
+
version: 2.5.1
|
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-06-
|
11
|
+
date: 2020-06-11 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
|