determinator 2.5.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b505247b6902fb3768389da7b5a20e625f95e5ccaec778ae7fb52e1808286b73
4
- data.tar.gz: 659b93e86df258535f904bd90d91768767c5c1644cd578d3efe3f45a9c2aba6f
3
+ metadata.gz: 9fe6fd684f160e1d955e9528bd225942068320361d554102b63667125503e610
4
+ data.tar.gz: f70731d2a63bffa170ee0d545fa9efbc2271fc32995c6635fc4feea33ac38c65
5
5
  SHA512:
6
- metadata.gz: 753968e66ac00b3f0606eed33b5d1873ad23a89cce901b69e261f38263ff14f18b975a0f03e55146e8ea78dc397b1a48878f10c3b1fc6eadd4f278a78f2a7703
7
- data.tar.gz: 46d183bc1e87d5031b43115bb63573503725f441a44603577274fef9817cd20722471bcba1b46d018beab4828bbcbf738c4de742aff0de26578178330edd2885
6
+ metadata.gz: 0debe679036a419091917171e90ce4360ec0ea8afdc3d2b0d536b1796cd93b2ffe1fe3b6dc933cfe03642be61bde94c12b471a6aca2d7a04c087f24e6b6b8e94
7
+ data.tar.gz: 423c16ff61e48e2da9e8883329b70a031819be4741b2f9031fe0cd6c1681e0df43d52f98028b5002a6e6ad82f6774aa24fed5ddf227b56a8d75bb59e3a81ccde
@@ -1,3 +1,30 @@
1
+ # 2.6.0
2
+
3
+ Interface change:
4
+ - A `feature_cache` is now required to use Determinator. See the `examples/determinator-rails/config/initializers/determinator.rb` for a quick start.
5
+
6
+ # 2.5.4
7
+
8
+ Bug fix:
9
+ - Apply app_version logic to structured request.app_version too
10
+
11
+ # 2.5.3
12
+
13
+ Bug fix:
14
+ - Avoid errors when updating the gem and using persistent cache resulting in null fixed_determinations
15
+
16
+ # 2.5.2
17
+
18
+ Feature:
19
+ - Add structured_bucket to Feature
20
+ - Add `#retrieve` method to the Control
21
+ - Add optional `feature` argument to `feature_flag_on?` and `which_variant`, to reuse an existing feature
22
+
23
+ # 2.5.1
24
+
25
+ Feature:
26
+ - Add explain functionality for determinations
27
+
1
28
  # 2.5.0
2
29
 
3
30
  Feature:
@@ -3,6 +3,7 @@ require 'determinator/control'
3
3
  require 'determinator/feature'
4
4
  require 'determinator/target_group'
5
5
  require 'determinator/fixed_determination'
6
+ require 'determinator/explainer'
6
7
  require 'determinator/cache/fetch_wrapper'
7
8
  require 'determinator/serializers/json'
8
9
  require 'determinator/missing_response'
@@ -14,13 +15,13 @@ module Determinator
14
15
  class << self
15
16
  attr_reader :feature_cache, :retrieval
16
17
  # @param :retrieval [Determinator::Retrieve::Routemaster] A retrieval instance for Features
18
+ # @param :feature_cache [#call] a caching proc, accepting a feature name, which will return the named feature or yield (and store) if not available
17
19
  # @param :errors [#call, nil] a proc, accepting an error, which will be called with any errors which occur while determinating
18
20
  # @param :missing_feature [#call, nil] a proc, accepting a feature name, which will be called any time a feature is requested but isn't available
19
- # @param :feature_cache [#call, nil] a caching proc, accepting a feature name, which will return the named feature or yield (and store) if not available
20
- def configure(retrieval:, errors: nil, missing_feature: nil, feature_cache: nil)
21
+ def configure(retrieval:, feature_cache:, errors: nil, missing_feature: nil)
21
22
  self.on_error(&errors) if errors
22
23
  self.on_missing_feature(&missing_feature) if missing_feature
23
- @feature_cache = feature_cache if feature_cache.respond_to?(:call)
24
+ @feature_cache = feature_cache
24
25
  @retrieval = retrieval
25
26
  @instance = Control.new(retrieval: retrieval)
26
27
  end
@@ -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,17 +93,19 @@ module Determinator
76
93
  # Calling method can place constraints on the feature, eg. experiment only
77
94
  return false if block_given? && !yield(feature)
78
95
 
96
+ explainer.log(:start, { feature: feature } )
97
+
79
98
  # Inactive features are always, always off
80
- return false unless feature.active?
99
+ return false unless feature_active?(feature)
81
100
 
82
- return feature.override_value_for(id) if feature.overridden_for?(id)
101
+ return override_value(feature, id) if feature_overridden?(feature, id)
83
102
 
84
103
  fixed_determination = choose_fixed_determination(feature, properties)
85
104
  # Given constraints have specified that this actor's determination should be fixed
86
105
  if fixed_determination
87
- return false unless fixed_determination.feature_on
88
- return true unless feature.experiment?
89
- return fixed_determination.variant
106
+ return explainer.log(:chosen_fixed_determination, { fixed_determination: fixed_determination }) {
107
+ fixed_determination_value(feature, fixed_determination)
108
+ }
90
109
  end
91
110
 
92
111
  target_group = choose_target_group(feature, properties)
@@ -98,14 +117,18 @@ module Determinator
98
117
  return false unless indicators
99
118
 
100
119
  # Actor's indicator has excluded them from the feature
101
- return false if indicators.rollout >= target_group.rollout
120
+ return false if excluded_from_rollout?(indicators, target_group)
102
121
 
103
122
  # Features don't need variant determination and, at this stage,
104
123
  # they have been rolled out to.
105
- return true unless feature.experiment?
124
+ # require_variant_determination?
125
+ return true unless require_variant_determination?(feature)
106
126
 
107
- variant_for(feature, indicators.variant)
108
127
 
128
+
129
+ explainer.log(:chosen_variant) {
130
+ variant_for(feature, indicators.variant)
131
+ }
109
132
  rescue ArgumentError
110
133
  raise
111
134
 
@@ -114,25 +137,85 @@ module Determinator
114
137
  false
115
138
  end
116
139
 
140
+ def feature_active?(feature)
141
+ explainer.log(:feature_active) {
142
+ feature.active?
143
+ }
144
+ end
145
+
146
+ def feature_overridden?(feature, id)
147
+ explainer.log(:feature_overridden_for) {
148
+ feature.overridden_for?(id)
149
+ }
150
+ end
151
+
152
+ def override_value(feature, id)
153
+ explainer.log(:override_value, { id: id }) {
154
+ feature.override_value_for(id)
155
+ }
156
+ end
157
+
158
+ def excluded_from_rollout?(indicators, target_group)
159
+ explainer.log(:excluded_from_rollout, { target_group: target_group } ) {
160
+ indicators.rollout >= target_group.rollout
161
+ }
162
+ end
163
+
164
+ def require_variant_determination?(feature)
165
+ explainer.log(:require_variant_determination) {
166
+ feature.experiment?
167
+ }
168
+ end
169
+
170
+ def fixed_determination_value(feature, fixed_determination)
171
+ return false unless fixed_determination.feature_on
172
+ return true unless feature.experiment?
173
+ return fixed_determination.variant
174
+ end
175
+
117
176
  def choose_fixed_determination(feature, properties)
177
+ return unless feature.fixed_determinations
178
+
118
179
  # Keys and values must be strings
119
180
  normalised_properties = normalise_properties(properties)
120
181
 
121
- feature.fixed_determinations.find { |fd|
122
- matches_constraints(normalised_properties, fd.constraints)
123
- }
182
+ feature.fixed_determinations.find do |fd|
183
+ explainer.log(:possible_match_fixed_determination, { fixed_determination: fd }) {
184
+ check_fixed_determination(fd, normalised_properties)
185
+ }
186
+ end
187
+ end
188
+
189
+ def check_fixed_determination(fixed_determination, properties)
190
+ explainer.log(:check_fixed_determination, { fixed_determination: fixed_determination })
191
+
192
+ matches_constraints(properties, fixed_determination.constraints)
124
193
  end
125
194
 
126
195
  def choose_target_group(feature, properties)
127
196
  # Keys and values must be strings
128
197
  normalised_properties = normalise_properties(properties)
129
198
 
130
- feature.target_groups.select { |tg|
131
- next false unless tg.rollout.between?(1, 65_536)
199
+ # Must choose target group deterministically, if more than one match
200
+ explainer.log(:chosen_target_group) {
201
+ filtered_target_groups(feature, normalised_properties).sort_by { |tg| tg.rollout }.last
202
+ }
203
+ end
204
+
205
+ def filtered_target_groups(feature, properties)
206
+ feature.target_groups.select do |tg|
207
+ explainer.log(:possible_match_target_group, { target_group: tg }) {
208
+ check_target_group(tg, properties)
209
+ }
210
+ end
211
+ end
212
+
213
+ def check_target_group(target_group, properties)
214
+ explainer.log(:check_target_group, { target_group: target_group })
215
+
216
+ return false unless target_group.rollout.between?(1, 65_536)
132
217
 
133
- matches_constraints(normalised_properties, tg.constraints)
134
- # Must choose target group deterministically, if more than one match
135
- }.sort_by { |tg| tg.rollout }.last
218
+ matches_constraints(properties, target_group.constraints)
136
219
  end
137
220
 
138
221
  def matches_constraints(normalised_properties, constraints)
@@ -145,6 +228,7 @@ module Determinator
145
228
  def matches_requirements?(scope, required, present)
146
229
  case scope
147
230
  when "app_version" then has_any_app_version?(required, present)
231
+ when "request.app_version" then has_any_app_version?(required, present)
148
232
  else has_any?(required, present)
149
233
  end
150
234
  end
@@ -174,10 +258,12 @@ module Determinator
174
258
  def actor_identifier(feature, id, guid)
175
259
  case feature.bucket_type
176
260
  when :id
261
+ explainer.log(:missing_identifier, { identifier_type: 'ID' }) unless id
177
262
  id
178
263
  when :guid
179
264
  return guid if guid.to_s != ''
180
265
 
266
+ explainer.log(:missing_identifier, { identifier_type: 'GUID' })
181
267
  raise ArgumentError, 'A GUID must always be given for GUID bucketed features'
182
268
  when :fallback
183
269
  identifier = (id || guid).to_s
@@ -187,6 +273,7 @@ module Determinator
187
273
  when :single
188
274
  SecureRandom.hex(64)
189
275
  else
276
+ explainer.log(:unknown_bucket, { feature: feature } )
190
277
  Determinator.notice_error "Cannot process the '#{feature.bucket_type}' bucket type found in #{feature.name}"
191
278
  end
192
279
  end
@@ -0,0 +1,61 @@
1
+ require 'determinator/explainer/messages'
2
+
3
+ module Determinator
4
+ class Explainer
5
+ attr_accessor :enabled
6
+ attr_reader :logs
7
+
8
+ def initialize
9
+ @logs = []
10
+ @enabled = false
11
+ end
12
+
13
+ def explain
14
+ @enabled = true
15
+ { outcome: yield, explanation: @logs }
16
+ ensure
17
+ @logs = []
18
+ @enabled = false
19
+ end
20
+
21
+ def log(type, args = {})
22
+ result = block_given? ? yield : nil
23
+
24
+ return result unless @enabled
25
+
26
+ result.tap do |r|
27
+ add(type, r, args)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def add(type, result, args = {})
34
+ template = MESSAGES[type].fetch(result.to_s) { MESSAGES[type][:default] }
35
+ return unless template
36
+
37
+ args = convert_hash(
38
+ args.merge(result: result)
39
+ .transform_values { |v| v.respond_to?(:to_explain_params) ? v.to_explain_params : v }
40
+ )
41
+
42
+ @logs << template.dup.tap do |m|
43
+ m[:title] = format(m[:title], args)
44
+ m[:subtitle] = format(m[:subtitle], args) if m[:subtitle]
45
+ end
46
+ true
47
+ end
48
+
49
+ def convert_hash(hsh, path = "")
50
+ hsh.each_with_object({}) do |(k, v), ret|
51
+ key = "#{path}#{k}"
52
+
53
+ if v.is_a?(Hash)
54
+ ret.merge! convert_hash(v, "#{key}.")
55
+ else
56
+ ret[key.to_sym] = v
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,119 @@
1
+ module Determinator
2
+ class Explainer
3
+ MESSAGES = {
4
+ start: {
5
+ default: {
6
+ type: :start,
7
+ title: 'Determinating %<feature.name>s',
8
+ subtitle: 'The given ID, GUID and properties will be used to determine which target groups this actor is in, and will deterministically return an outcome for the visibility of this feature for them.'
9
+ }
10
+ },
11
+ feature_active: {
12
+ 'true' => { type: :continue, title: 'Feature is active' },
13
+ 'false' => { type: :fail, title: 'Feature is inactive', subtitle: 'Every actor is excluded' }
14
+ },
15
+ feature_overridden_for: {
16
+ 'false' => { type: :pass, title: 'No matching override found' }
17
+ },
18
+ override_value: {
19
+ default: {
20
+ type: :success,
21
+ title: 'Matching override found for this actor with id: %<id>s',
22
+ subtitle: 'Determinator will return "%<result>s" for this actor and this feature in any system that is correctly set up.'
23
+ }
24
+ },
25
+ excluded_from_rollout: {
26
+ 'true' => {
27
+ type: :fail,
28
+ title: 'Determinated to be outside the %<target_group.rollout_percent>s',
29
+ subtitle: 'This actor is excluded'
30
+ },
31
+ 'false' => {
32
+ type: :continue,
33
+ title: 'Determinated to be inside the %<target_group.rollout_percent>s',
34
+ subtitle: 'This actor is included'
35
+ }
36
+ },
37
+ require_variant_determination: {
38
+ 'false' => {
39
+ type: :success,
40
+ title: 'Feature flag on for this actor',
41
+ subtitle: 'Determinator will return true for this actor and this feature in any system that is correctly set up.'
42
+ }
43
+ },
44
+ missing_identifier: {
45
+ default: {
46
+ type: :fail,
47
+ title: 'No %<identifier_type>s given, cannot determinate',
48
+ subtitle: 'For %<identifier_type>s bucketed features an %<identifier_type>s must be given to have the possibility of being included.'
49
+ }
50
+ },
51
+ chosen_variant: {
52
+ default: {
53
+ type: :success,
54
+ title: 'In the "%<result>s" variant',
55
+ subtitle: 'Determinator will return "%<result>s" for this actor and this feature in any system that is correctly set up.'
56
+ }
57
+ },
58
+ unknown_bucket: {
59
+ default: {
60
+ type: :fail,
61
+ title: 'Unknown bucket type',
62
+ subtitle: 'The bucket type "%<feature.bucket_type>s" is not understood by Determinator. All actors will be excluded.'
63
+ }
64
+ },
65
+ check_target_group: {
66
+ default: {
67
+ type: :target_group,
68
+ title: 'Checking "%<target_group.name>s" target group',
69
+ subtitle: 'An actor must match at least one non-zero target group in order to be included.'
70
+ }
71
+ },
72
+ possible_match_target_group: {
73
+ 'true' => {
74
+ type: :continue,
75
+ title: 'Matches the "%<target_group.name>s" target group',
76
+ subtitle: 'Matching this target group allows this actor a %<target_group.rollout_percent>s percent chance of being included.'
77
+ },
78
+ 'false' => {
79
+ type: :pass,
80
+ title: 'Didn\'t match the "%<target_group.name>s" target group',
81
+ subtitle: 'Actor can\'t be included as part of this target group.'
82
+ }
83
+ },
84
+ chosen_target_group: {
85
+ '' => {
86
+ type: :fail,
87
+ title: 'No matching target groups',
88
+ subtitle: 'No matching target groups have a rollout larger than 0 percent. This actor is excluded.'
89
+ },
90
+ default: {
91
+ type: :info,
92
+ title: '%<result.rollout_percent>s percent chance of being included',
93
+ subtitle: 'The largest matching rollout percentage is %<result.rollout_percent>s, giving this actor a percent chance of being included.'
94
+ }
95
+ },
96
+ check_fixed_determination: {
97
+ default: {
98
+ type: :target_group,
99
+ title: 'Checking "%<fixed_determination.name>s" fixed determination',
100
+ subtitle: 'Matching an actor based on the constraints provided.'
101
+ }
102
+ },
103
+ possible_match_fixed_determination: {
104
+ 'false' => {
105
+ type: :pass,
106
+ title: 'Didn\'t match the "%<fixed_determination.name>s" fixed determination',
107
+ subtitle: 'Actor can\'t be included as part of this fixed determination.'
108
+ }
109
+ },
110
+ chosen_fixed_determination: {
111
+ default: {
112
+ type: :success,
113
+ title: 'Matching fixed determination found for this actor with name: "%<fixed_determination.name>s"',
114
+ subtitle: 'Determinator will return "%<result>s" for this actor and this feature in any system that is correctly set up.'
115
+ },
116
+ }
117
+ }.freeze
118
+ end
119
+ end
@@ -3,9 +3,9 @@ module Determinator
3
3
  #
4
4
  # @attr_reader [nil,Hash<String,Integer>] variants The variants for this experiment, with the name of the variant as the key and the weight as the value. Will be nil for non-experiments.
5
5
  class Feature
6
- attr_reader :name, :identifier, :bucket_type, :variants, :target_groups, :fixed_determinations, :active, :winning_variant
6
+ attr_reader :name, :identifier, :bucket_type, :structured_bucket, :variants, :target_groups, :fixed_determinations, :active, :winning_variant
7
7
 
8
- def initialize(name:, identifier:, bucket_type:, target_groups:, fixed_determinations: [], variants: {}, overrides: {}, active: false, winning_variant: nil)
8
+ def initialize(name:, identifier:, bucket_type:, target_groups:, structured_bucket: nil, fixed_determinations: [], variants: {}, overrides: {}, active: false, winning_variant: nil)
9
9
  @name = name.to_s
10
10
  @identifier = identifier.to_s
11
11
  @variants = variants
@@ -14,6 +14,7 @@ module Determinator
14
14
  @winning_variant = parse_outcome(winning_variant, allow_exclusion: false)
15
15
  @active = active
16
16
  @bucket_type = bucket_type.to_sym
17
+ @structured_bucket = structured_bucket
17
18
 
18
19
  # To prevent confusion between actor id data types
19
20
  @overrides = overrides.each_with_object({}) do |(identifier, outcome), hash|
@@ -36,6 +37,11 @@ module Determinator
36
37
  variants.empty?
37
38
  end
38
39
 
40
+ # @return [true,false] Is this feature using structured identification?
41
+ def structured?
42
+ !!structured_bucket && !structured_bucket.empty?
43
+ end
44
+
39
45
  # Is this feature overridden for the given actor id?
40
46
  #
41
47
  # @return [true,false] Whether this feature is overridden for this actor
@@ -58,6 +64,10 @@ module Determinator
58
64
  Marshal.dump(self) == Marshal.dump(other)
59
65
  end
60
66
 
67
+ def to_explain_params
68
+ { name: name, identifier: identifier, bucket_type: bucket_type }
69
+ end
70
+
61
71
  private
62
72
 
63
73
  attr_reader :overrides
@@ -72,6 +82,7 @@ module Determinator
72
82
  constraints = target_group['constraints'].to_h
73
83
 
74
84
  TargetGroup.new(
85
+ name: target_group['name'],
75
86
  rollout: target_group['rollout'].to_i,
76
87
  constraints: parse_constraints(constraints)
77
88
  )
@@ -97,6 +108,7 @@ module Determinator
97
108
  constraints = fixed_determination['constraints'].to_h
98
109
 
99
110
  FixedDetermination.new(
111
+ name: fixed_determination['name'],
100
112
  feature_on: fixed_determination['feature_on'],
101
113
  variant: variant,
102
114
  constraints: parse_constraints(constraints)
@@ -1,8 +1,9 @@
1
1
  module Determinator
2
2
  class FixedDetermination
3
- attr_reader :feature_on, :variant, :constraints
3
+ attr_reader :name, :feature_on, :variant, :constraints
4
4
 
5
- def initialize(feature_on:, variant:, constraints: {})
5
+ def initialize(feature_on:, variant:, name: '', constraints: {})
6
+ @name = name
6
7
  @feature_on = feature_on
7
8
  @variant = variant
8
9
  @constraints = constraints
@@ -12,6 +13,10 @@ module Determinator
12
13
  "<feature_on: #{feature_on}, variant: #{variant}, constraints: #{constraints}"
13
14
  end
14
15
 
16
+ def to_explain_params
17
+ { name: name }
18
+ end
19
+
15
20
  def ==(other)
16
21
  return false unless other.is_a?(self.class)
17
22
  other.feature_on == feature_on && other.variant == variant && other.constraints == constraints
@@ -15,6 +15,7 @@ module Determinator
15
15
  name: obj['name'],
16
16
  identifier: obj['identifier'],
17
17
  bucket_type: obj['bucket_type'],
18
+ structured_bucket: obj['structured_bucket'],
18
19
  active: (obj['active'] === true),
19
20
  target_groups: obj['target_groups'],
20
21
  fixed_determinations: obj['fixed_determinations'].to_a,
@@ -1,8 +1,9 @@
1
1
  module Determinator
2
2
  class TargetGroup
3
- attr_reader :rollout, :constraints
3
+ attr_reader :name, :rollout, :constraints
4
4
 
5
- def initialize(rollout:, constraints: {})
5
+ def initialize(rollout:, name: '', constraints: {})
6
+ @name = name
6
7
  @rollout = rollout
7
8
  @constraints = constraints
8
9
  end
@@ -15,9 +16,16 @@ module Determinator
15
16
  Rational(rollout, 65_536)
16
17
  end
17
18
 
19
+ def humanize_percentage
20
+ (rollout_percent * 100).to_f.round(1)
21
+ end
22
+
18
23
  def inspect
19
- 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.6.0'
3
3
  end
@@ -3,6 +3,9 @@ require_relative '../determinator/retrieve/in_memory_retriever'
3
3
 
4
4
  module RSpec
5
5
  module Determinator
6
+
7
+ DO_NOT_USE_IN_PRODUCTION_CODE_NULL_FEATURE_CACHE = -> (name, &block) { block.call(name) }
8
+
6
9
  def self.included(by)
7
10
  by.extend(DSL)
8
11
 
@@ -12,10 +15,10 @@ module RSpec
12
15
  old_retriever = ::Determinator.instance.retrieval
13
16
  begin
14
17
  fake_retriever.clear!
15
- ::Determinator.configure(retrieval: fake_retriever)
18
+ ::Determinator.configure(retrieval: fake_retriever, feature_cache: DO_NOT_USE_IN_PRODUCTION_CODE_NULL_FEATURE_CACHE)
16
19
  example.run
17
20
  ensure
18
- ::Determinator.configure(retrieval: old_retriever)
21
+ ::Determinator.configure(retrieval: old_retriever, feature_cache: DO_NOT_USE_IN_PRODUCTION_CODE_NULL_FEATURE_CACHE)
19
22
  end
20
23
  end
21
24
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: determinator
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Hastings-Spital
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-08 00:00:00.000000000 Z
11
+ date: 2021-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -209,6 +209,8 @@ files:
209
209
  - lib/determinator/cache/fetch_wrapper.rb
210
210
  - lib/determinator/control.rb
211
211
  - lib/determinator/error_response.rb
212
+ - lib/determinator/explainer.rb
213
+ - lib/determinator/explainer/messages.rb
212
214
  - lib/determinator/feature.rb
213
215
  - lib/determinator/fixed_determination.rb
214
216
  - lib/determinator/missing_response.rb
@@ -247,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
247
249
  - !ruby/object:Gem::Version
248
250
  version: '0'
249
251
  requirements: []
250
- rubygems_version: 3.0.6
252
+ rubygems_version: 3.0.3
251
253
  signing_key:
252
254
  specification_version: 4
253
255
  summary: Determine which experiments and features a specific actor should see.