determinator 2.4.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 +36 -0
- data/README.md +7 -4
- data/lib/determinator.rb +2 -0
- data/lib/determinator/control.rb +117 -16
- data/lib/determinator/explainer.rb +61 -0
- data/lib/determinator/explainer/messages.rb +119 -0
- data/lib/determinator/feature.rb +42 -6
- data/lib/determinator/fixed_determination.rb +25 -0
- data/lib/determinator/retrieve/in_memory_retriever.rb +1 -1
- data/lib/determinator/retrieve/null_retriever.rb +1 -0
- data/lib/determinator/serializers/json.rb +9 -8
- data/lib/determinator/target_group.rb +12 -4
- data/lib/determinator/tracking.rb +14 -2
- data/lib/determinator/tracking/determination.rb +6 -0
- data/lib/determinator/tracking/rack/middleware.rb +20 -1
- data/lib/determinator/tracking/request.rb +4 -2
- data/lib/determinator/tracking/sidekiq/middleware.rb +5 -1
- data/lib/determinator/tracking/tracker.rb +15 -10
- data/lib/determinator/version.rb +1 -1
- metadata +10 -7
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
@@ -1,3 +1,39 @@
|
|
1
|
+
# 2.5.1
|
2
|
+
|
3
|
+
Feature:
|
4
|
+
- Add explain functionality for determinations
|
5
|
+
|
6
|
+
# 2.5.0
|
7
|
+
|
8
|
+
Feature:
|
9
|
+
- Add fixed determinations
|
10
|
+
|
11
|
+
# 2.4.4
|
12
|
+
|
13
|
+
Bug fix:
|
14
|
+
- Count repeated determinations instead of tracking separately
|
15
|
+
|
16
|
+
# 2.4.3
|
17
|
+
|
18
|
+
Feature:
|
19
|
+
- Add Sinatra endpoint tracking
|
20
|
+
|
21
|
+
Bug fix:
|
22
|
+
- Remove endpoint tracking of PATH_INFO
|
23
|
+
|
24
|
+
# 2.4.2
|
25
|
+
|
26
|
+
Feature:
|
27
|
+
- Add endpoint information to tracking request
|
28
|
+
|
29
|
+
Bug fix:
|
30
|
+
- Make tracking request "start" attribute an actual time
|
31
|
+
|
32
|
+
# 2.4.1
|
33
|
+
|
34
|
+
Bug fix:
|
35
|
+
- Update "fake" retrievers to match behaviour introduced in `v2.3.1` when a feature is missing
|
36
|
+
|
1
37
|
# 2.4.0
|
2
38
|
|
3
39
|
Feature:
|
data/README.md
CHANGED
@@ -289,12 +289,17 @@ end
|
|
289
289
|
require 'determinator/tracking'
|
290
290
|
|
291
291
|
Determinator::Tracking.on_request do |r|
|
292
|
-
Rails.logger.info("tag=determinator_request type=#{r.type} request_time=#{r.time} error=#{r.error?} response_status=#{r.attributes[:status]} sidekiq_queue=#{r.attributes[:queue]}")
|
292
|
+
Rails.logger.info("tag=determinator_request endpoint=#{r.endpoint} type=#{r.type} request_time=#{r.time} error=#{r.error?} response_status=#{r.attributes[:status]} sidekiq_queue=#{r.attributes[:queue]}")
|
293
293
|
r.determinations.each do |d|
|
294
294
|
Rails.logger.info("tag=determination id=#{d.id} guid=#{d.guid} flag=#{d.feature_id} result=#{d.determination}")
|
295
295
|
end
|
296
296
|
end
|
297
297
|
|
298
|
+
# The library sets the "endpoint" with information about the request or sidekiq job. If you
|
299
|
+
# have environment variables that further identify the service, e.g. ENV['APP_NAME'],
|
300
|
+
# you can configure the tracker to prepend it to the endpoint:
|
301
|
+
Determinator::Tracking.endpoint_env_vars = ['APP_NAME']
|
302
|
+
|
298
303
|
# If using an APM, you can provide trace information on the request by providing a get_context hook: e.g.
|
299
304
|
|
300
305
|
Determinator::Tracking.get_context do
|
@@ -310,9 +315,7 @@ Determinator::Tracking.get_context do
|
|
310
315
|
end
|
311
316
|
```
|
312
317
|
|
313
|
-
NOTE:
|
314
|
-
|
315
|
-
If your application is spinning out worker threads, you should make the determinations in the main thread if possible; or collect them from your worker threads and track them in the main thread with
|
318
|
+
NOTE: determinations will only be recorded on the threads where Determinator::Tracking is initialised via the middleware. If offloading work away from these thread (for example, by spinning up new threads within a Rack request or a Sidekiq worker), make the determinations before, and pass them through to the new threads; or, if it's not possible, collect them manually and track them in the request's thread with
|
316
319
|
```
|
317
320
|
Determinator::Tracking.track(id, guid, feature, determination)
|
318
321
|
```
|
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
|
@@ -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,10 +83,20 @@ 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)
|
90
|
+
|
91
|
+
return override_value(feature, id) if feature_overridden?(feature, id)
|
81
92
|
|
82
|
-
|
93
|
+
fixed_determination = choose_fixed_determination(feature, properties)
|
94
|
+
# Given constraints have specified that this actor's determination should be fixed
|
95
|
+
if fixed_determination
|
96
|
+
return explainer.log(:chosen_fixed_determination, { fixed_determination: fixed_determination }) {
|
97
|
+
fixed_determination_value(feature, fixed_determination)
|
98
|
+
}
|
99
|
+
end
|
83
100
|
|
84
101
|
target_group = choose_target_group(feature, properties)
|
85
102
|
# Given constraints have excluded this actor from this experiment
|
@@ -90,14 +107,18 @@ module Determinator
|
|
90
107
|
return false unless indicators
|
91
108
|
|
92
109
|
# Actor's indicator has excluded them from the feature
|
93
|
-
return false if indicators
|
110
|
+
return false if excluded_from_rollout?(indicators, target_group)
|
94
111
|
|
95
112
|
# Features don't need variant determination and, at this stage,
|
96
113
|
# they have been rolled out to.
|
97
|
-
|
114
|
+
# require_variant_determination?
|
115
|
+
return true unless require_variant_determination?(feature)
|
116
|
+
|
98
117
|
|
99
|
-
variant_for(feature, indicators.variant)
|
100
118
|
|
119
|
+
explainer.log(:chosen_variant) {
|
120
|
+
variant_for(feature, indicators.variant)
|
121
|
+
}
|
101
122
|
rescue ArgumentError
|
102
123
|
raise
|
103
124
|
|
@@ -106,21 +127,90 @@ module Determinator
|
|
106
127
|
false
|
107
128
|
end
|
108
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
|
+
|
166
|
+
def choose_fixed_determination(feature, properties)
|
167
|
+
# Keys and values must be strings
|
168
|
+
normalised_properties = normalise_properties(properties)
|
169
|
+
|
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)
|
181
|
+
end
|
182
|
+
|
109
183
|
def choose_target_group(feature, properties)
|
110
184
|
# Keys and values must be strings
|
111
|
-
normalised_properties = properties
|
112
|
-
|
185
|
+
normalised_properties = normalise_properties(properties)
|
186
|
+
|
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
|
+
}
|
113
198
|
end
|
199
|
+
end
|
114
200
|
|
115
|
-
|
116
|
-
|
201
|
+
def check_target_group(target_group, properties)
|
202
|
+
explainer.log(:check_target_group, { target_group: target_group })
|
117
203
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
204
|
+
return false unless target_group.rollout.between?(1, 65_536)
|
205
|
+
|
206
|
+
matches_constraints(properties, target_group.constraints)
|
207
|
+
end
|
208
|
+
|
209
|
+
def matches_constraints(normalised_properties, constraints)
|
210
|
+
constraints.reduce(true) do |fit, (scope, *required)|
|
211
|
+
present = [*normalised_properties[scope]]
|
212
|
+
fit && matches_requirements?(scope, required, present)
|
213
|
+
end
|
124
214
|
end
|
125
215
|
|
126
216
|
def matches_requirements?(scope, required, present)
|
@@ -155,10 +245,12 @@ module Determinator
|
|
155
245
|
def actor_identifier(feature, id, guid)
|
156
246
|
case feature.bucket_type
|
157
247
|
when :id
|
248
|
+
explainer.log(:missing_identifier, { identifier_type: 'ID' }) unless id
|
158
249
|
id
|
159
250
|
when :guid
|
160
251
|
return guid if guid.to_s != ''
|
161
252
|
|
253
|
+
explainer.log(:missing_identifier, { identifier_type: 'GUID' })
|
162
254
|
raise ArgumentError, 'A GUID must always be given for GUID bucketed features'
|
163
255
|
when :fallback
|
164
256
|
identifier = (id || guid).to_s
|
@@ -168,6 +260,7 @@ module Determinator
|
|
168
260
|
when :single
|
169
261
|
SecureRandom.hex(64)
|
170
262
|
else
|
263
|
+
explainer.log(:unknown_bucket, { feature: feature } )
|
171
264
|
Determinator.notice_error "Cannot process the '#{feature.bucket_type}' bucket type found in #{feature.name}"
|
172
265
|
end
|
173
266
|
end
|
@@ -205,5 +298,13 @@ module Determinator
|
|
205
298
|
|
206
299
|
raise ArgumentError, "A variant should have been found by this point, there is a bug in the code."
|
207
300
|
end
|
301
|
+
|
302
|
+
private
|
303
|
+
|
304
|
+
def normalise_properties(properties)
|
305
|
+
properties.each_with_object({}) do |(name, values), hash|
|
306
|
+
hash[name.to_s] = [*values].map(&:to_s)
|
307
|
+
end
|
308
|
+
end
|
208
309
|
end
|
209
310
|
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,13 +3,14 @@ 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, :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:, 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
|
@@ -57,6 +58,10 @@ module Determinator
|
|
57
58
|
Marshal.dump(self) == Marshal.dump(other)
|
58
59
|
end
|
59
60
|
|
61
|
+
def to_explain_params
|
62
|
+
{ name: name, identifier: identifier, bucket_type: bucket_type }
|
63
|
+
end
|
64
|
+
|
60
65
|
private
|
61
66
|
|
62
67
|
attr_reader :overrides
|
@@ -71,15 +76,46 @@ module Determinator
|
|
71
76
|
constraints = target_group['constraints'].to_h
|
72
77
|
|
73
78
|
TargetGroup.new(
|
79
|
+
name: target_group['name'],
|
74
80
|
rollout: target_group['rollout'].to_i,
|
75
|
-
constraints: constraints
|
76
|
-
hash[key.to_s] = [*value].map(&:to_s)
|
77
|
-
end
|
81
|
+
constraints: parse_constraints(constraints)
|
78
82
|
)
|
79
83
|
|
80
84
|
# Invalid target groups are ignored
|
81
85
|
rescue
|
82
86
|
nil
|
83
87
|
end
|
88
|
+
|
89
|
+
def parse_fixed_determinations(fixed_determinations)
|
90
|
+
fixed_determinations.map(&method(:parse_fixed_determination)).compact
|
91
|
+
end
|
92
|
+
|
93
|
+
def parse_fixed_determination(fixed_determination)
|
94
|
+
return fixed_determination if fixed_determination.is_a? FixedDetermination
|
95
|
+
|
96
|
+
variant = fixed_determination['variant']
|
97
|
+
return nil if variant && !variants.keys.include?(variant)
|
98
|
+
|
99
|
+
# if a variant is present the fixed determination should always be on
|
100
|
+
return nil if variant && !fixed_determination['feature_on']
|
101
|
+
|
102
|
+
constraints = fixed_determination['constraints'].to_h
|
103
|
+
|
104
|
+
FixedDetermination.new(
|
105
|
+
name: fixed_determination['name'],
|
106
|
+
feature_on: fixed_determination['feature_on'],
|
107
|
+
variant: variant,
|
108
|
+
constraints: parse_constraints(constraints)
|
109
|
+
)
|
110
|
+
# Invalid fixed determinations are ignored
|
111
|
+
rescue
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
|
115
|
+
def parse_constraints(constraints)
|
116
|
+
constraints.each_with_object({}) do |(key, value), hash|
|
117
|
+
hash[key.to_s] = [*value].map(&:to_s)
|
118
|
+
end
|
119
|
+
end
|
84
120
|
end
|
85
121
|
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
|
@@ -10,7 +10,7 @@ module Determinator
|
|
10
10
|
|
11
11
|
# @param name [string,symbol] The name of the feature to retrieve
|
12
12
|
def retrieve(name)
|
13
|
-
@features
|
13
|
+
@features.fetch(name.to_s, MissingResponse.new)
|
14
14
|
end
|
15
15
|
|
16
16
|
# @param feature [Determinator::Feature] The feature to store
|
@@ -12,14 +12,15 @@ 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
|
-
active:
|
19
|
-
target_groups:
|
20
|
-
|
21
|
-
|
22
|
-
|
15
|
+
name: obj['name'],
|
16
|
+
identifier: obj['identifier'],
|
17
|
+
bucket_type: obj['bucket_type'],
|
18
|
+
active: (obj['active'] === true),
|
19
|
+
target_groups: obj['target_groups'],
|
20
|
+
fixed_determinations: obj['fixed_determinations'].to_a,
|
21
|
+
variants: obj['variants'].to_h,
|
22
|
+
overrides: obj['overrides'].to_h,
|
23
|
+
winning_variant: obj['winning_variant'].to_s,
|
23
24
|
)
|
24
25
|
end
|
25
26
|
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)
|
@@ -4,6 +4,8 @@ require 'determinator/tracking/context'
|
|
4
4
|
module Determinator
|
5
5
|
module Tracking
|
6
6
|
class << self
|
7
|
+
attr_reader :endpoint_env_vars
|
8
|
+
|
7
9
|
def instance
|
8
10
|
Thread.current[:determinator_tracker]
|
9
11
|
end
|
@@ -12,9 +14,9 @@ module Determinator
|
|
12
14
|
Thread.current[:determinator_tracker] = Tracker.new(type)
|
13
15
|
end
|
14
16
|
|
15
|
-
def finish!(error:, **attributes)
|
17
|
+
def finish!(endpoint:, error:, **attributes)
|
16
18
|
return false unless started?
|
17
|
-
request = instance.finish!(error: error, **attributes)
|
19
|
+
request = instance.finish!(endpoint: endpoint, error: error, **attributes)
|
18
20
|
clear!
|
19
21
|
report(request)
|
20
22
|
request
|
@@ -57,6 +59,16 @@ module Determinator
|
|
57
59
|
@on_request = nil
|
58
60
|
@get_context = nil
|
59
61
|
end
|
62
|
+
|
63
|
+
def endpoint_env_vars=(vars)
|
64
|
+
@endpoint_env_vars = Array(vars)
|
65
|
+
end
|
66
|
+
|
67
|
+
def collect_endpoint_info(parts)
|
68
|
+
endpoint = Array(Determinator::Tracking.endpoint_env_vars).map{ |v| ENV[v] }
|
69
|
+
endpoint += Array(parts)
|
70
|
+
endpoint.reject{ |p| p.nil? || p == ''}.join(' ')
|
71
|
+
end
|
60
72
|
end
|
61
73
|
end
|
62
74
|
end
|
@@ -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
|
@@ -16,7 +16,26 @@ module Determinator
|
|
16
16
|
error = true
|
17
17
|
raise
|
18
18
|
ensure
|
19
|
-
Determinator::Tracking.finish!(
|
19
|
+
Determinator::Tracking.finish!(
|
20
|
+
status: status,
|
21
|
+
error: !!error,
|
22
|
+
endpoint: extract_endpoint(env)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def extract_endpoint(env)
|
29
|
+
parts = if params = env['action_dispatch.request.path_parameters']
|
30
|
+
[env['REQUEST_METHOD'], [params[:controller], params[:action]].join('#')]
|
31
|
+
elsif env['sinatra.route']
|
32
|
+
[env['sinatra.route']]
|
33
|
+
else
|
34
|
+
[env['REQUEST_METHOD']]
|
35
|
+
end
|
36
|
+
Determinator::Tracking.collect_endpoint_info(parts)
|
37
|
+
rescue
|
38
|
+
env['PATH_INFO']
|
20
39
|
end
|
21
40
|
end
|
22
41
|
end
|
@@ -1,14 +1,16 @@
|
|
1
1
|
module Determinator
|
2
2
|
module Tracking
|
3
3
|
class Request
|
4
|
-
attr_reader :type, :time, :error, :attributes, :determinations, :context
|
4
|
+
attr_reader :start, :type, :endpoint, :time, :error, :attributes, :determinations, :context
|
5
5
|
|
6
|
-
def initialize(type:, time:, error:, attributes:, determinations:, context: nil)
|
6
|
+
def initialize(start:, type:, endpoint:, time:, error:, attributes:, determinations:, context: nil)
|
7
|
+
@start = start
|
7
8
|
@type = type
|
8
9
|
@time = time
|
9
10
|
@error = error
|
10
11
|
@attributes = attributes
|
11
12
|
@determinations = determinations
|
13
|
+
@endpoint = endpoint
|
12
14
|
@context = context
|
13
15
|
end
|
14
16
|
|
@@ -18,7 +18,11 @@ module Determinator
|
|
18
18
|
error = true
|
19
19
|
raise
|
20
20
|
ensure
|
21
|
-
Determinator::Tracking.finish!(
|
21
|
+
Determinator::Tracking.finish!(
|
22
|
+
endpoint: Determinator::Tracking.collect_endpoint_info(worker.class.name),
|
23
|
+
queue: queue,
|
24
|
+
error: !!error
|
25
|
+
)
|
22
26
|
end
|
23
27
|
end
|
24
28
|
end
|
@@ -7,25 +7,30 @@ 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
|
+
@start = Time.now
|
13
14
|
end
|
14
15
|
|
15
16
|
def track(id, guid, feature, determination)
|
16
|
-
determinations
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
determinations[
|
18
|
+
Determinator::Tracking::Determination.new(
|
19
|
+
id: id,
|
20
|
+
guid: guid,
|
21
|
+
feature_id: feature.identifier,
|
22
|
+
determination: determination
|
23
|
+
)
|
24
|
+
] += 1
|
22
25
|
end
|
23
26
|
|
24
|
-
def finish!(error:, **attributes)
|
25
|
-
request_time = now - @
|
27
|
+
def finish!(endpoint:, error:, **attributes)
|
28
|
+
request_time = now - @monotonic_start
|
26
29
|
Determinator::Tracking::Request.new(
|
30
|
+
start: @start,
|
27
31
|
type: type,
|
28
32
|
time: request_time,
|
33
|
+
endpoint: endpoint,
|
29
34
|
error: error,
|
30
35
|
attributes: attributes,
|
31
36
|
determinations: determinations,
|
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.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- JP Hastings-Spital
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-06-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -150,7 +150,7 @@ dependencies:
|
|
150
150
|
- - ">="
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
|
-
description:
|
153
|
+
description:
|
154
154
|
email:
|
155
155
|
- jp@deliveroo.co.uk
|
156
156
|
executables: []
|
@@ -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
|
@@ -231,7 +234,7 @@ homepage: https://github.com/deliveroo/determinator
|
|
231
234
|
licenses:
|
232
235
|
- MIT
|
233
236
|
metadata: {}
|
234
|
-
post_install_message:
|
237
|
+
post_install_message:
|
235
238
|
rdoc_options: []
|
236
239
|
require_paths:
|
237
240
|
- lib
|
@@ -246,8 +249,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
246
249
|
- !ruby/object:Gem::Version
|
247
250
|
version: '0'
|
248
251
|
requirements: []
|
249
|
-
rubygems_version: 3.0.
|
250
|
-
signing_key:
|
252
|
+
rubygems_version: 3.0.6
|
253
|
+
signing_key:
|
251
254
|
specification_version: 4
|
252
255
|
summary: Determine which experiments and features a specific actor should see.
|
253
256
|
test_files: []
|