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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63baaf3ee379eef31694ec105e48407970d0e53e6906af8e9a1746cfac5ca5e4
4
- data.tar.gz: 88b70dc55dedc32c98dc1eb3619fd420471127c812a7cdda7086c13349d3bdca
3
+ metadata.gz: 4466edc3643125ca6b84591b4db51083d215c5c6ea8201694e99d16e403eab24
4
+ data.tar.gz: 326d45e3f0ac249e5a358ee3911c1da4da01db81c14ab1b3c827a8fd5d0f4ef9
5
5
  SHA512:
6
- metadata.gz: 1ea1835cd7e0953c5d6747bbcbddfc43804cd33ffc3ed136c72ede003c2a24c9357c7746081370404b9851788a0b64b9d45a633657365524ed77cf6a5a69bf67
7
- data.tar.gz: 2db13fd8793c724554c2ba4766e3e769eb5d9e4042481b153a205337c5b0cfaad1bd6a691b552ee9a25109f9b8bb7b6dc6837570e909bf5d60fab1f4fe056e86
6
+ metadata.gz: c83ad23860f0d5565618e3cfbf1f9975763f3f64c0db49668fa2259aae06c972689e000538a734571906edbf766c8d1d46e58ad2d7c657461c76759768f37bfa
7
+ data.tar.gz: 777556d4606d6a10514b5f7c6311e498117845f9bf5bc3bd1be7f8c47cee837fb17243348d11ee0f2fff3931493e97e65cb06608dcbc9b435916aabad07e69f1
@@ -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: this is implemented by keeping the list of requests in a per-request thread-local variable, which means that determinations will only be tracked on the main thread.
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
  ```
@@ -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
@@ -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.active?
89
+ return false unless feature_active?(feature)
90
+
91
+ return override_value(feature, id) if feature_overridden?(feature, id)
81
92
 
82
- return feature.override_value_for(id) if feature.overridden_for?(id)
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.rollout >= target_group.rollout
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
- return true unless feature.experiment?
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.each_with_object({}) do |(name, values), hash|
112
- hash[name.to_s] = [*values].map(&:to_s)
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
- feature.target_groups.select { |tg|
116
- next false unless tg.rollout.between?(1, 65_536)
201
+ def check_target_group(target_group, properties)
202
+ explainer.log(:check_target_group, { target_group: target_group })
117
203
 
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
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
@@ -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 = (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
@@ -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.each_with_object({}) do |(key, value), hash|
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[name.to_s]
13
+ @features.fetch(name.to_s, MissingResponse.new)
14
14
  end
15
15
 
16
16
  # @param feature [Determinator::Feature] The feature to store
@@ -10,6 +10,7 @@ module Determinator
10
10
  # The Control class will assume a nil return from this method
11
11
  # means the feature doesn't exist, so in turn will return `false`.
12
12
  def retrieve(_)
13
+ MissingResponse.new
13
14
  end
14
15
  end
15
16
  end
@@ -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: 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
+ 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
- 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)
@@ -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!(status: status, error: !!error)
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!(queue: queue, error: !!error)
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
- @start = now
12
+ @monotonic_start = now
13
+ @start = Time.now
13
14
  end
14
15
 
15
16
  def track(id, guid, feature, determination)
16
- determinations << Determinator::Tracking::Determination.new(
17
- id: id,
18
- guid: guid,
19
- feature_id: feature.identifier,
20
- determination: determination
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 - @start
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,
@@ -1,3 +1,3 @@
1
1
  module Determinator
2
- VERSION = '2.4.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.4.0
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-03-05 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
@@ -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.3
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: []