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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b505247b6902fb3768389da7b5a20e625f95e5ccaec778ae7fb52e1808286b73
4
- data.tar.gz: 659b93e86df258535f904bd90d91768767c5c1644cd578d3efe3f45a9c2aba6f
3
+ metadata.gz: 4466edc3643125ca6b84591b4db51083d215c5c6ea8201694e99d16e403eab24
4
+ data.tar.gz: 326d45e3f0ac249e5a358ee3911c1da4da01db81c14ab1b3c827a8fd5d0f4ef9
5
5
  SHA512:
6
- metadata.gz: 753968e66ac00b3f0606eed33b5d1873ad23a89cce901b69e261f38263ff14f18b975a0f03e55146e8ea78dc397b1a48878f10c3b1fc6eadd4f278a78f2a7703
7
- data.tar.gz: 46d183bc1e87d5031b43115bb63573503725f441a44603577274fef9817cd20722471bcba1b46d018beab4828bbcbf738c4de742aff0de26578178330edd2885
6
+ metadata.gz: c83ad23860f0d5565618e3cfbf1f9975763f3f64c0db49668fa2259aae06c972689e000538a734571906edbf766c8d1d46e58ad2d7c657461c76759768f37bfa
7
+ data.tar.gz: 777556d4606d6a10514b5f7c6311e498117845f9bf5bc3bd1be7f8c47cee837fb17243348d11ee0f2fff3931493e97e65cb06608dcbc9b435916aabad07e69f1
@@ -1,3 +1,8 @@
1
+ # 2.5.1
2
+
3
+ Feature:
4
+ - Add explain functionality for determinations
5
+
1
6
  # 2.5.0
2
7
 
3
8
  Feature:
@@ -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'
@@ -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.active?
89
+ return false unless feature_active?(feature)
81
90
 
82
- return feature.override_value_for(id) if feature.overridden_for?(id)
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 false unless fixed_determination.feature_on
88
- return true unless feature.experiment?
89
- return fixed_determination.variant
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.rollout >= target_group.rollout
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
- return true unless feature.experiment?
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 { |fd|
122
- matches_constraints(normalised_properties, fd.constraints)
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
- feature.target_groups.select { |tg|
131
- next false unless tg.rollout.between?(1, 65_536)
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
- matches_constraints(normalised_properties, tg.constraints)
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
@@ -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
- pc = (rollout_percent * 100).to_f.round(1)
20
- "<#{pc}% of those matching: #{constraints}>"
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)
@@ -1,3 +1,3 @@
1
1
  module Determinator
2
- VERSION = '2.5.0'
2
+ VERSION = '2.5.1'
3
3
  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.5.0
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-08 00:00:00.000000000 Z
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