determinator 2.4.4 → 2.5.4

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: 1b5c2494d530c31a915321c8dbf47713e4ddaed50a0d5858a285e020a87bccf1
4
- data.tar.gz: 4e9cad995c047a7c8d12aa7dc8228cddfc4a84891c2fc9283b4d41eb26b3bbdf
3
+ metadata.gz: 7e5cd27cbcca013e18d0a941c9a196e9b8e7e908a97d67b857514f58d35def5d
4
+ data.tar.gz: 18c86d3448b136bb996702d0bbcdba97f5280f496bd580daf145013b2dd054f8
5
5
  SHA512:
6
- metadata.gz: 0727e1e39289a3f89b0d6e20da1139130ca329cdd786af73b7190c3df3f3e958f31d2f92eb09593e00c2503fdb09adf499ac81006baac28094b377e59f7be452
7
- data.tar.gz: ce5085f303357a3d3e70e082b99f6ef986ee1f5c0a27fee7c0b89d7e6196c6702955e2c70db1897f003316cfcbf3934d99a9379cfa1b11a9659321832141a091
6
+ metadata.gz: f892fed72664d8dd47fb50d710678ff33ebe274f25fa8885ab048a13a7b3db5e496b7e39d67ce1fcea14c805bfaa390cc905241fb6e3e63405687d0ea2fe8355
7
+ data.tar.gz: a9c104b252ba508445957ee1f249f39ace127f294104693153e96afae9f3903c7dcf497ead2d1b255b1ff429fde9340db37551d82ecee6888576bae01eea6395
@@ -1,3 +1,30 @@
1
+ # 2.5.4
2
+
3
+ Bug fix:
4
+ - Apply app_version logic to structured request.app_version too
5
+
6
+ # 2.5.3
7
+
8
+ Bug fix:
9
+ - Avoid errors when updating the gem and using persistent cache resulting in null fixed_determinations
10
+
11
+ # 2.5.2
12
+
13
+ Feature:
14
+ - Add structured_bucket to Feature
15
+ - Add `#retrieve` method to the Control
16
+ - Add optional `feature` argument to `feature_flag_on?` and `which_variant`, to reuse an existing feature
17
+
18
+ # 2.5.1
19
+
20
+ Feature:
21
+ - Add explain functionality for determinations
22
+
23
+ # 2.5.0
24
+
25
+ Feature:
26
+ - Add fixed determinations
27
+
1
28
  # 2.4.4
2
29
 
3
30
  Bug fix:
@@ -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'
@@ -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 = Determinator.with_retrieval_cache(name) { retrieval.retrieve(name) }
79
+ def determinate_and_notice(name, id:, guid:, properties:, feature: nil)
80
+ feature ||= retrieve(name)
64
81
 
65
82
  if feature.nil? || feature.is_a?(ErrorResponse) || feature.is_a?(MissingResponse)
66
83
  Determinator.notice_missing_feature(name)
@@ -76,10 +93,20 @@ module Determinator
76
93
  # Calling method can place constraints on the feature, eg. experiment only
77
94
  return false if block_given? && !yield(feature)
78
95
 
96
+ explainer.log(:start, { feature: feature } )
97
+
79
98
  # Inactive features are always, always off
80
- return false unless feature.active?
99
+ return false unless feature_active?(feature)
100
+
101
+ return override_value(feature, id) if feature_overridden?(feature, id)
81
102
 
82
- return feature.override_value_for(id) if feature.overridden_for?(id)
103
+ fixed_determination = choose_fixed_determination(feature, properties)
104
+ # Given constraints have specified that this actor's determination should be fixed
105
+ if fixed_determination
106
+ return explainer.log(:chosen_fixed_determination, { fixed_determination: fixed_determination }) {
107
+ fixed_determination_value(feature, fixed_determination)
108
+ }
109
+ end
83
110
 
84
111
  target_group = choose_target_group(feature, properties)
85
112
  # Given constraints have excluded this actor from this experiment
@@ -90,14 +117,18 @@ module Determinator
90
117
  return false unless indicators
91
118
 
92
119
  # Actor's indicator has excluded them from the feature
93
- return false if indicators.rollout >= target_group.rollout
120
+ return false if excluded_from_rollout?(indicators, target_group)
94
121
 
95
122
  # Features don't need variant determination and, at this stage,
96
123
  # they have been rolled out to.
97
- return true unless feature.experiment?
124
+ # require_variant_determination?
125
+ return true unless require_variant_determination?(feature)
126
+
98
127
 
99
- variant_for(feature, indicators.variant)
100
128
 
129
+ explainer.log(:chosen_variant) {
130
+ variant_for(feature, indicators.variant)
131
+ }
101
132
  rescue ArgumentError
102
133
  raise
103
134
 
@@ -106,26 +137,98 @@ module Determinator
106
137
  false
107
138
  end
108
139
 
140
+ def feature_active?(feature)
141
+ explainer.log(:feature_active) {
142
+ feature.active?
143
+ }
144
+ end
145
+
146
+ def feature_overridden?(feature, id)
147
+ explainer.log(:feature_overridden_for) {
148
+ feature.overridden_for?(id)
149
+ }
150
+ end
151
+
152
+ def override_value(feature, id)
153
+ explainer.log(:override_value, { id: id }) {
154
+ feature.override_value_for(id)
155
+ }
156
+ end
157
+
158
+ def excluded_from_rollout?(indicators, target_group)
159
+ explainer.log(:excluded_from_rollout, { target_group: target_group } ) {
160
+ indicators.rollout >= target_group.rollout
161
+ }
162
+ end
163
+
164
+ def require_variant_determination?(feature)
165
+ explainer.log(:require_variant_determination) {
166
+ feature.experiment?
167
+ }
168
+ end
169
+
170
+ def fixed_determination_value(feature, fixed_determination)
171
+ return false unless fixed_determination.feature_on
172
+ return true unless feature.experiment?
173
+ return fixed_determination.variant
174
+ end
175
+
176
+ def choose_fixed_determination(feature, properties)
177
+ return unless feature.fixed_determinations
178
+
179
+ # Keys and values must be strings
180
+ normalised_properties = normalise_properties(properties)
181
+
182
+ feature.fixed_determinations.find do |fd|
183
+ explainer.log(:possible_match_fixed_determination, { fixed_determination: fd }) {
184
+ check_fixed_determination(fd, normalised_properties)
185
+ }
186
+ end
187
+ end
188
+
189
+ def check_fixed_determination(fixed_determination, properties)
190
+ explainer.log(:check_fixed_determination, { fixed_determination: fixed_determination })
191
+
192
+ matches_constraints(properties, fixed_determination.constraints)
193
+ end
194
+
109
195
  def choose_target_group(feature, properties)
110
196
  # Keys and values must be strings
111
- normalised_properties = properties.each_with_object({}) do |(name, values), hash|
112
- hash[name.to_s] = [*values].map(&:to_s)
197
+ normalised_properties = normalise_properties(properties)
198
+
199
+ # Must choose target group deterministically, if more than one match
200
+ explainer.log(:chosen_target_group) {
201
+ filtered_target_groups(feature, normalised_properties).sort_by { |tg| tg.rollout }.last
202
+ }
203
+ end
204
+
205
+ def filtered_target_groups(feature, properties)
206
+ feature.target_groups.select do |tg|
207
+ explainer.log(:possible_match_target_group, { target_group: tg }) {
208
+ check_target_group(tg, properties)
209
+ }
113
210
  end
211
+ end
114
212
 
115
- feature.target_groups.select { |tg|
116
- next false unless tg.rollout.between?(1, 65_536)
213
+ def check_target_group(target_group, properties)
214
+ explainer.log(:check_target_group, { target_group: target_group })
117
215
 
118
- tg.constraints.reduce(true) do |fit, (scope, *required)|
119
- present = [*normalised_properties[scope]]
120
- fit && matches_requirements?(scope, required, present)
121
- end
122
- # Must choose target group deterministically, if more than one match
123
- }.sort_by { |tg| tg.rollout }.last
216
+ return false unless target_group.rollout.between?(1, 65_536)
217
+
218
+ matches_constraints(properties, target_group.constraints)
219
+ end
220
+
221
+ def matches_constraints(normalised_properties, constraints)
222
+ constraints.reduce(true) do |fit, (scope, *required)|
223
+ present = [*normalised_properties[scope]]
224
+ fit && matches_requirements?(scope, required, present)
225
+ end
124
226
  end
125
227
 
126
228
  def matches_requirements?(scope, required, present)
127
229
  case scope
128
230
  when "app_version" then has_any_app_version?(required, present)
231
+ when "request.app_version" then has_any_app_version?(required, present)
129
232
  else has_any?(required, present)
130
233
  end
131
234
  end
@@ -155,10 +258,12 @@ module Determinator
155
258
  def actor_identifier(feature, id, guid)
156
259
  case feature.bucket_type
157
260
  when :id
261
+ explainer.log(:missing_identifier, { identifier_type: 'ID' }) unless id
158
262
  id
159
263
  when :guid
160
264
  return guid if guid.to_s != ''
161
265
 
266
+ explainer.log(:missing_identifier, { identifier_type: 'GUID' })
162
267
  raise ArgumentError, 'A GUID must always be given for GUID bucketed features'
163
268
  when :fallback
164
269
  identifier = (id || guid).to_s
@@ -168,6 +273,7 @@ module Determinator
168
273
  when :single
169
274
  SecureRandom.hex(64)
170
275
  else
276
+ explainer.log(:unknown_bucket, { feature: feature } )
171
277
  Determinator.notice_error "Cannot process the '#{feature.bucket_type}' bucket type found in #{feature.name}"
172
278
  end
173
279
  end
@@ -205,5 +311,13 @@ module Determinator
205
311
 
206
312
  raise ArgumentError, "A variant should have been found by this point, there is a bug in the code."
207
313
  end
314
+
315
+ private
316
+
317
+ def normalise_properties(properties)
318
+ properties.each_with_object({}) do |(name, values), hash|
319
+ hash[name.to_s] = [*values].map(&:to_s)
320
+ end
321
+ end
208
322
  end
209
323
  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
@@ -3,16 +3,18 @@ module Determinator
3
3
  #
4
4
  # @attr_reader [nil,Hash<String,Integer>] variants The variants for this experiment, with the name of the variant as the key and the weight as the value. Will be nil for non-experiments.
5
5
  class Feature
6
- attr_reader :name, :identifier, :bucket_type, :variants, :target_groups, :active, :winning_variant
6
+ attr_reader :name, :identifier, :bucket_type, :structured_bucket, :variants, :target_groups, :fixed_determinations, :active, :winning_variant
7
7
 
8
- def initialize(name:, identifier:, bucket_type:, target_groups:, variants: {}, overrides: {}, active: false, winning_variant: nil)
8
+ def initialize(name:, identifier:, bucket_type:, target_groups:, structured_bucket: nil, fixed_determinations: [], variants: {}, overrides: {}, active: false, winning_variant: nil)
9
9
  @name = name.to_s
10
- @identifier = (identifier || name).to_s
10
+ @identifier = identifier.to_s
11
11
  @variants = variants
12
12
  @target_groups = parse_target_groups(target_groups)
13
+ @fixed_determinations = parse_fixed_determinations(fixed_determinations)
13
14
  @winning_variant = parse_outcome(winning_variant, allow_exclusion: false)
14
15
  @active = active
15
16
  @bucket_type = bucket_type.to_sym
17
+ @structured_bucket = structured_bucket
16
18
 
17
19
  # To prevent confusion between actor id data types
18
20
  @overrides = overrides.each_with_object({}) do |(identifier, outcome), hash|
@@ -35,6 +37,11 @@ module Determinator
35
37
  variants.empty?
36
38
  end
37
39
 
40
+ # @return [true,false] Is this feature using structured identification?
41
+ def structured?
42
+ !!structured_bucket && !structured_bucket.empty?
43
+ end
44
+
38
45
  # Is this feature overridden for the given actor id?
39
46
  #
40
47
  # @return [true,false] Whether this feature is overridden for this actor
@@ -57,6 +64,10 @@ module Determinator
57
64
  Marshal.dump(self) == Marshal.dump(other)
58
65
  end
59
66
 
67
+ def to_explain_params
68
+ { name: name, identifier: identifier, bucket_type: bucket_type }
69
+ end
70
+
60
71
  private
61
72
 
62
73
  attr_reader :overrides
@@ -71,15 +82,46 @@ module Determinator
71
82
  constraints = target_group['constraints'].to_h
72
83
 
73
84
  TargetGroup.new(
85
+ name: target_group['name'],
74
86
  rollout: target_group['rollout'].to_i,
75
- constraints: constraints.each_with_object({}) do |(key, value), hash|
76
- hash[key.to_s] = [*value].map(&:to_s)
77
- end
87
+ constraints: parse_constraints(constraints)
78
88
  )
79
89
 
80
90
  # Invalid target groups are ignored
81
91
  rescue
82
92
  nil
83
93
  end
94
+
95
+ def parse_fixed_determinations(fixed_determinations)
96
+ fixed_determinations.map(&method(:parse_fixed_determination)).compact
97
+ end
98
+
99
+ def parse_fixed_determination(fixed_determination)
100
+ return fixed_determination if fixed_determination.is_a? FixedDetermination
101
+
102
+ variant = fixed_determination['variant']
103
+ return nil if variant && !variants.keys.include?(variant)
104
+
105
+ # if a variant is present the fixed determination should always be on
106
+ return nil if variant && !fixed_determination['feature_on']
107
+
108
+ constraints = fixed_determination['constraints'].to_h
109
+
110
+ FixedDetermination.new(
111
+ name: fixed_determination['name'],
112
+ feature_on: fixed_determination['feature_on'],
113
+ variant: variant,
114
+ constraints: parse_constraints(constraints)
115
+ )
116
+ # Invalid fixed determinations are ignored
117
+ rescue
118
+ nil
119
+ end
120
+
121
+ def parse_constraints(constraints)
122
+ constraints.each_with_object({}) do |(key, value), hash|
123
+ hash[key.to_s] = [*value].map(&:to_s)
124
+ end
125
+ end
84
126
  end
85
127
  end
@@ -0,0 +1,25 @@
1
+ module Determinator
2
+ class FixedDetermination
3
+ attr_reader :name, :feature_on, :variant, :constraints
4
+
5
+ def initialize(feature_on:, variant:, name: '', constraints: {})
6
+ @name = name
7
+ @feature_on = feature_on
8
+ @variant = variant
9
+ @constraints = constraints
10
+ end
11
+
12
+ def inspect
13
+ "<feature_on: #{feature_on}, variant: #{variant}, constraints: #{constraints}"
14
+ end
15
+
16
+ def to_explain_params
17
+ { name: name }
18
+ end
19
+
20
+ def ==(other)
21
+ return false unless other.is_a?(self.class)
22
+ other.feature_on == feature_on && other.variant == variant && other.constraints == constraints
23
+ end
24
+ end
25
+ end
@@ -12,14 +12,16 @@ module Determinator
12
12
  obj = string_or_hash.is_a?(Hash) ? string_or_hash : ::JSON.parse(string_or_hash)
13
13
 
14
14
  Determinator::Feature.new(
15
- name: obj['name'],
16
- identifier: obj['identifier'],
17
- bucket_type: obj['bucket_type'],
18
- active: (obj['active'] === true),
19
- target_groups: obj['target_groups'],
20
- variants: obj['variants'].to_h,
21
- overrides: obj['overrides'].to_h,
22
- winning_variant: obj['winning_variant'].to_s,
15
+ name: obj['name'],
16
+ identifier: obj['identifier'],
17
+ bucket_type: obj['bucket_type'],
18
+ structured_bucket: obj['structured_bucket'],
19
+ active: (obj['active'] === true),
20
+ target_groups: obj['target_groups'],
21
+ fixed_determinations: obj['fixed_determinations'].to_a,
22
+ variants: obj['variants'].to_h,
23
+ overrides: obj['overrides'].to_h,
24
+ winning_variant: obj['winning_variant'].to_s,
23
25
  )
24
26
  end
25
27
  end
@@ -1,8 +1,9 @@
1
1
  module Determinator
2
2
  class TargetGroup
3
- attr_reader :rollout, :constraints
3
+ attr_reader :name, :rollout, :constraints
4
4
 
5
- def initialize(rollout:, constraints: {})
5
+ def initialize(rollout:, name: '', constraints: {})
6
+ @name = name
6
7
  @rollout = rollout
7
8
  @constraints = constraints
8
9
  end
@@ -15,9 +16,16 @@ module Determinator
15
16
  Rational(rollout, 65_536)
16
17
  end
17
18
 
19
+ def humanize_percentage
20
+ (rollout_percent * 100).to_f.round(1)
21
+ end
22
+
18
23
  def inspect
19
- 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.4.4'
2
+ VERSION = '2.5.4'
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.4.4
4
+ version: 2.5.4
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-05-20 00:00:00.000000000 Z
11
+ date: 2020-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -209,7 +209,10 @@ files:
209
209
  - lib/determinator/cache/fetch_wrapper.rb
210
210
  - lib/determinator/control.rb
211
211
  - lib/determinator/error_response.rb
212
+ - lib/determinator/explainer.rb
213
+ - lib/determinator/explainer/messages.rb
212
214
  - lib/determinator/feature.rb
215
+ - lib/determinator/fixed_determination.rb
213
216
  - lib/determinator/missing_response.rb
214
217
  - lib/determinator/retrieve/dynaconf.rb
215
218
  - lib/determinator/retrieve/file.rb