determinator 0.12.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitmodules +3 -0
- data/CHANGELOG.md +12 -0
- data/Guardfile +1 -0
- data/README.md +27 -8
- data/circle.yml +3 -0
- data/examples/determinator-rails/Gemfile.lock +1 -1
- data/examples/determinator-rails/app/controllers/index_controller.rb +0 -4
- data/examples/determinator-rails/config/initializers/determinator.rb +19 -7
- data/lib/determinator.rb +78 -27
- data/lib/determinator/cache/fetch_wrapper.rb +20 -0
- data/lib/determinator/control.rb +57 -25
- data/lib/determinator/feature.rb +39 -8
- data/lib/determinator/retrieve/file.rb +24 -0
- data/lib/determinator/retrieve/routemaster.rb +4 -69
- data/lib/determinator/serializers/json.rb +28 -0
- data/lib/determinator/version.rb +1 -1
- metadata +6 -3
- data/lib/determinator/retrieve/null_cache.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b01c8573e10fbc219e4cd44c85696a8c34f4283
|
4
|
+
data.tar.gz: f7a43c6e60f22ef9947fbe04e5313db4b28982c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66c567515540a0cc5adc1b9978b2a33b57d7f66135a0d6413847bccc68ee69d7a0954979333dda96bad701eba3879b1ac9e4c38785f18a4bf5c9ddc0f51ac668
|
7
|
+
data.tar.gz: 3329ee1b0ea3c1f59dc010675e2e2f89173dfc7c346b06ccb4248cb67e8c29b1ff13041a969f0400911a41fa93e0656b921ec5c9d9984e7d44f4634cec2c1a59
|
data/.gitmodules
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
# v1.0.0 (2018-02-05)
|
2
|
+
|
3
|
+
⚠️ This release includes breaking changes ⚠️
|
4
|
+
|
5
|
+
Breaking change:
|
6
|
+
- Changes retrieval caching so that 404s are cached as well as positive feature retrievals. This means that an unconfigured feature won't result in a thundering herd of requests to Florence. (#46)
|
7
|
+
|
8
|
+
Feature:
|
9
|
+
- Changes tests to use the [Determinator standard tests](https://github.com/deliveroo/determinator-standard-tests) (#41)
|
10
|
+
- Supports the use of the 'single' bucket type for features, which allows (only) feature flags to be on or off, without any `id` or `guid` specified. (#41)
|
11
|
+
- Adds Determintion callbacks: trigger a block when a determination is made, allowing for experiment tracking. (#45)
|
12
|
+
|
1
13
|
# v0.12.1 (2018-02-01)
|
2
14
|
|
3
15
|
Bug Fix:
|
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -81,19 +81,34 @@ Determinator requires a initialiser block somewhere in your application's boot p
|
|
81
81
|
# config/initializers/determinator.rb
|
82
82
|
|
83
83
|
require 'determinator/retrieve/routemaster'
|
84
|
+
require 'active_support/cache'
|
85
|
+
|
84
86
|
Determinator.configure(
|
85
|
-
retrieval: Determinator::Retrieve::Routemaster.new(
|
86
|
-
|
87
|
-
|
88
|
-
)
|
89
|
-
errors: -> error { NewRelic::Agent.notice_error(error) },
|
90
|
-
missing_features: -> feature_name { STATSD.increment 'determinator.missing_feature', tags: ["feature:#{name}"] }
|
87
|
+
retrieval: Determinator::Retrieve::Routemaster.new(discovery_url: 'https://flo.dev/'),
|
88
|
+
feature_cache: Determinator::Cache::FetchWrapper.new(
|
89
|
+
ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
|
90
|
+
)
|
91
91
|
)
|
92
|
+
Determinator.on_error(NewRelic::Agent.method(:notice_error))
|
93
|
+
Determinator.on_missing_feature do |feature_name|
|
94
|
+
STATSD.increment 'determinator.missing_feature', tags: ["feature:#{name}"]
|
95
|
+
end
|
96
|
+
|
97
|
+
Determinator.on_determination do |id, guid, feature, determination|
|
98
|
+
if feature.experiment? && determination != false
|
99
|
+
YourTrackingSolution.record_variant_viewing(
|
100
|
+
user_id: id,
|
101
|
+
experiment_name: feature.name,
|
102
|
+
variant: determination
|
103
|
+
)
|
104
|
+
end
|
105
|
+
end
|
92
106
|
```
|
93
107
|
|
94
108
|
This configures the `Determinator.instance` with:
|
95
109
|
|
96
110
|
- What **retrieval** mechanism should be used to get feature details
|
111
|
+
- (recommended) How features should be **cached** as they're retrieved. This mechanism allows caching features _and_ missing features, so when a cache is configured a determination request for a missing feature on busy machines won't result in a thundering herd.
|
97
112
|
- (optional) How **errors** should be reported
|
98
113
|
- (optional) How **missing features** should be monitored (as they indicate something's up with your code or your set up!)
|
99
114
|
|
@@ -175,9 +190,13 @@ end
|
|
175
190
|
|
176
191
|
* Check out [the specs for `RSpec::Determinator`](spec/rspec/determinator_spec.rb) to find out what you can do!
|
177
192
|
|
178
|
-
|
193
|
+
## Testing this library
|
179
194
|
|
180
|
-
|
195
|
+
This library makes use of the [Determinator Standard Tests](https://github.com/deliveroo/determinator-standard-tests) to ensure that it conforms to the same specification as determinator libraries in other languages. The standard tests can be updated to the latest ones available by updating the submodule:
|
196
|
+
|
197
|
+
```bash
|
198
|
+
git submodule foreach git pull origin master
|
199
|
+
```
|
181
200
|
|
182
201
|
## Contributing
|
183
202
|
|
data/circle.yml
CHANGED
@@ -3,10 +3,6 @@ class IndexController < ApplicationController
|
|
3
3
|
is_colloquial = determinator.feature_flag_on?(:colloquial_welcome)
|
4
4
|
emoji = determinator.which_variant(:welcome_emoji)
|
5
5
|
|
6
|
-
if emoji
|
7
|
-
# TODO: Track that this user saw a variant of this experiment
|
8
|
-
end
|
9
|
-
|
10
6
|
message = [
|
11
7
|
is_colloquial ? "hi world" : "hello world",
|
12
8
|
emoji
|
@@ -1,10 +1,22 @@
|
|
1
1
|
require 'determinator/retrieve/routemaster'
|
2
|
+
require 'active_support/cache'
|
2
3
|
|
3
|
-
Determinator.
|
4
|
-
|
5
|
-
|
6
|
-
retrieval_cache: ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
|
7
|
-
)
|
8
|
-
# The following would allow tracking of errors in NewRelic
|
9
|
-
# errors: -> error { NewRelic::Agent.notice_error(error) }
|
4
|
+
retrieval = Determinator::Retrieve::Routemaster.new(discovery_url: 'https://flo.dev/')
|
5
|
+
feature_cache = Determinator::Cache::FetchWrapper.new(
|
6
|
+
ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
|
10
7
|
)
|
8
|
+
Determinator.configure(retrieval: retrieval, feature_cache: feature_cache)
|
9
|
+
|
10
|
+
Determinator.on_error do |error|
|
11
|
+
# NewRelic::Agent.notice_error(error)
|
12
|
+
end
|
13
|
+
|
14
|
+
Determinator.on_missing_feature do |feature_name|
|
15
|
+
# STATSD.increment 'determinator.missing_feature', tags: ["feature:#{name}"]
|
16
|
+
end
|
17
|
+
|
18
|
+
Determinator.on_determination do |id, guid, feature, determination|
|
19
|
+
if feature.experiment? && determination != false
|
20
|
+
puts "TODO: Track that user #{id}/#{guid} saw the #{determination} variant of '#{feature.name}' for analysis"
|
21
|
+
end
|
22
|
+
end
|
data/lib/determinator.rb
CHANGED
@@ -4,40 +4,91 @@ require 'determinator/feature'
|
|
4
4
|
require 'determinator/target_group'
|
5
5
|
require 'determinator/retrieve/routemaster'
|
6
6
|
require 'determinator/retrieve/null_retriever'
|
7
|
+
require 'determinator/cache/fetch_wrapper'
|
7
8
|
|
8
9
|
module Determinator
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
@
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
class << self
|
11
|
+
# @param :retrieval [Determinator::Retrieve::Routemaster] A retrieval instance for Features
|
12
|
+
# @param :errors [#call, nil] a proc, accepting an error, which will be called with any errors which occur while determinating
|
13
|
+
# @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
|
14
|
+
# @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
|
15
|
+
def configure(retrieval:, errors: nil, missing_feature: nil, feature_cache: nil)
|
16
|
+
self.on_error_logger(&errors) if errors
|
17
|
+
self.on_missing_feature(&missing_feature) if missing_feature
|
18
|
+
@feature_cache = feature_cache if feature_cache.respond_to?(:call)
|
19
|
+
@instance = Control.new(retrieval: retrieval)
|
20
|
+
end
|
17
21
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
# Returns the currently configured Determinator::Control instance
|
23
|
+
#
|
24
|
+
# @return [Determinator::Control] The currently active instance of determinator.
|
25
|
+
# @raises [RuntimeError] If no Determinator instance is set up (with `.configure`)
|
26
|
+
def instance
|
27
|
+
raise "No singleton Determinator instance defined" unless @instance
|
28
|
+
@instance
|
29
|
+
end
|
24
30
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
instance.retrieval.retrieve(name)
|
30
|
-
end
|
31
|
+
# Defines how errors that shouldn't break your application should be logged
|
32
|
+
def on_error(&block)
|
33
|
+
@error_logger = block
|
34
|
+
end
|
31
35
|
|
32
|
-
|
33
|
-
|
36
|
+
# Defines how to record the moment when a feature which doesn't exist is requested.
|
37
|
+
# If this happens a lot it indicates poor set up, so can be useful for tracking.
|
38
|
+
def on_missing_feature(&block)
|
39
|
+
@missing_feature_logger = block
|
40
|
+
end
|
34
41
|
|
35
|
-
|
36
|
-
|
42
|
+
# Defines code that should execute when a determination is completed. This is particularly
|
43
|
+
# helpful for preparing or sending events to record that an actor has seen a particular experiment variant.
|
44
|
+
#
|
45
|
+
# Please note that this block will be executed _synchronously_ before delivering the determination to the callsite.
|
46
|
+
#
|
47
|
+
# @yield [id, guid, feature, determination] Will be called when a determination was requested for the
|
48
|
+
# specified `feature`, for the actor with `id` and `guid`, and received the determination `determination`.
|
49
|
+
# @yieldparam id [String, nil] The ID that was used to request the determination
|
50
|
+
# @yieldparam guid [String, nil] The GUID that was used to request the determination
|
51
|
+
# @yieldparam feature [Determinator::Feature] The feature that was requested
|
52
|
+
# @yieldparam determination [String,Boolean] The result of the determination
|
53
|
+
def on_determination(&block)
|
54
|
+
@determination_callback = block
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the feature with the given name as Determinator uses it. This is useful for
|
58
|
+
# debugging issues with the retrieval mechanism which delivers features to Determinator.
|
59
|
+
# @returns [Determinator::Feature,nil] The feature details Determinator would use for a determination right now.
|
60
|
+
def feature_details(name)
|
61
|
+
with_retrieval_cache(name) { instance.retrieval.retrieve(name) }
|
62
|
+
end
|
63
|
+
|
64
|
+
# Allows Determinator to track that an error has happened with determination
|
65
|
+
# @api private
|
66
|
+
def notice_error(error)
|
67
|
+
return unless @error_logger
|
68
|
+
|
69
|
+
error = RuntimeError.new(error) unless error.is_a?(StandardError)
|
70
|
+
@error_logger.call(error)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Allows Determinator to track that a feature was requested but was missing
|
74
|
+
# @api private
|
75
|
+
def notice_missing_feature(name)
|
76
|
+
return unless @missing_feature_logger
|
77
|
+
|
78
|
+
@missing_feature_logger.call(name)
|
79
|
+
end
|
80
|
+
|
81
|
+
def notice_determination(id, guid, feature, determination)
|
82
|
+
return unless @determination_callback
|
83
|
+
@determination_callback.call(id, guid, feature, determination)
|
84
|
+
end
|
37
85
|
|
38
|
-
|
39
|
-
|
86
|
+
# Allows access to the chosen caching mechanism for any retrieval plugin.
|
87
|
+
# @api private
|
88
|
+
def with_retrieval_cache(name)
|
89
|
+
return yield unless @feature_cache.respond_to?(:call)
|
40
90
|
|
41
|
-
|
91
|
+
@feature_cache.call(name) { yield }
|
92
|
+
end
|
42
93
|
end
|
43
94
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Determinator
|
2
|
+
module Cache
|
3
|
+
class FetchWrapper
|
4
|
+
# @param cache [#fetch] An instance of a cache class which implements #fetch like ActiveSupport::Cache does
|
5
|
+
def initialize(cache)
|
6
|
+
@cache = cache
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(feature_name)
|
10
|
+
@cache.fetch(key(feature_name)) { yield }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def key(feature_name)
|
16
|
+
"determinator:feature_cache:#{feature_name}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/determinator/control.rb
CHANGED
@@ -27,9 +27,10 @@ module Determinator
|
|
27
27
|
# @param :id [#to_s] The id of the actor being determinated for
|
28
28
|
# @param :guid [#to_s] The Anonymous id of the actor being determinated for
|
29
29
|
# @param :properties [Hash<Symbol,String>] The properties of this actor which will be used for including this actor or not
|
30
|
+
# @raise [ArgumentError] When the arguments given to this method aren't ever going to produce a useful response
|
30
31
|
# @return [true,false] Whether the feature is on (true) or off (false) for this actor
|
31
32
|
def feature_flag_on?(name, id: nil, guid: nil, properties: {})
|
32
|
-
|
33
|
+
determinate_and_notice(name, id: id, guid: guid, properties: properties) do |feature|
|
33
34
|
feature.feature_flag?
|
34
35
|
end
|
35
36
|
end
|
@@ -40,9 +41,10 @@ module Determinator
|
|
40
41
|
# @param :id [#to_s] The id of the actor being determinated for
|
41
42
|
# @param :guid [#to_s] The Anonymous id of the actor being determinated for
|
42
43
|
# @param :properties [Hash<Symbol,String>] The properties of this actor which will be used for including this actor or not
|
44
|
+
# @raise [ArgumentError] When the arguments given to this method aren't ever going to produce a useful response
|
43
45
|
# @return [false,String] Returns false, if the actor is not in this experiment, or otherwise the variant name.
|
44
46
|
def which_variant(name, id: nil, guid: nil, properties: {})
|
45
|
-
|
47
|
+
determinate_and_notice(name, id: id, guid: guid, properties: properties) do |feature|
|
46
48
|
feature.experiment?
|
47
49
|
end
|
48
50
|
end
|
@@ -55,26 +57,33 @@ module Determinator
|
|
55
57
|
|
56
58
|
Indicators = Struct.new(:rollout, :variant)
|
57
59
|
|
58
|
-
def
|
59
|
-
feature = retrieval.retrieve(name)
|
60
|
+
def determinate_and_notice(name, id:, guid:, properties:)
|
61
|
+
feature = Determinator.with_retrieval_cache(name) { retrieval.retrieve(name) }
|
62
|
+
|
60
63
|
if feature.nil?
|
61
|
-
Determinator.
|
64
|
+
Determinator.notice_missing_feature(name)
|
62
65
|
return false
|
63
66
|
end
|
64
67
|
|
68
|
+
determinate(feature, id: id, guid: guid, properties: properties).tap do |determination|
|
69
|
+
Determinator.notice_determination(id, guid, feature, determination)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def determinate(feature, id:, guid:, properties:)
|
65
74
|
# Calling method can place constraints on the feature, eg. experiment only
|
66
75
|
return false if block_given? && !yield(feature)
|
67
76
|
|
68
|
-
#
|
69
|
-
return feature.override_value_for(id) if feature.overridden_for?(id)
|
70
|
-
|
77
|
+
# Inactive features are always, always off
|
71
78
|
return false unless feature.active?
|
72
79
|
|
80
|
+
return feature.override_value_for(id) if feature.overridden_for?(id)
|
81
|
+
|
73
82
|
target_group = choose_target_group(feature, properties)
|
74
83
|
# Given constraints have excluded this actor from this experiment
|
75
84
|
return false unless target_group
|
76
85
|
|
77
|
-
indicators = indicators_for(feature, id, guid)
|
86
|
+
indicators = indicators_for(feature, actor_identifier(feature, id, guid))
|
78
87
|
# This actor isn't described in enough detail to form indicators
|
79
88
|
return false unless indicators
|
80
89
|
|
@@ -86,30 +95,53 @@ module Determinator
|
|
86
95
|
return true unless feature.experiment?
|
87
96
|
|
88
97
|
variant_for(feature, indicators.variant)
|
98
|
+
|
99
|
+
rescue ArgumentError
|
100
|
+
raise
|
101
|
+
|
102
|
+
rescue => e
|
103
|
+
Determinator.notice_error(e)
|
104
|
+
false
|
89
105
|
end
|
90
106
|
|
91
107
|
def choose_target_group(feature, properties)
|
92
|
-
# Keys must be strings
|
93
|
-
normalised_properties = properties.each_with_object({}) do |(
|
94
|
-
|
108
|
+
# Keys and values must be strings
|
109
|
+
normalised_properties = properties.each_with_object({}) do |(name, values), hash|
|
110
|
+
hash[name.to_s] = [*values].map(&:to_s)
|
95
111
|
end
|
96
112
|
|
97
113
|
feature.target_groups.select { |tg|
|
114
|
+
next false unless tg.rollout.between?(1, 65_536)
|
115
|
+
|
98
116
|
tg.constraints.reduce(true) do |fit, (scope, *required)|
|
99
|
-
present = [*normalised_properties[scope
|
117
|
+
present = [*normalised_properties[scope]]
|
100
118
|
fit && (required.flatten & present.flatten).any?
|
101
119
|
end
|
102
120
|
# Must choose target group deterministically, if more than one match
|
103
121
|
}.sort_by { |tg| tg.rollout }.last
|
104
122
|
end
|
105
123
|
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
124
|
+
def actor_identifier(feature, id, guid)
|
125
|
+
case feature.bucket_type
|
126
|
+
when :id
|
127
|
+
id
|
128
|
+
when :guid
|
129
|
+
return guid if guid.to_s != ''
|
130
|
+
|
131
|
+
raise ArgumentError, 'A GUID must always be given for GUID bucketed features'
|
132
|
+
when :fallback
|
133
|
+
identifier = (id || guid).to_s
|
134
|
+
return identifier if identifier != ''
|
135
|
+
|
136
|
+
raise ArgumentError, 'An ID or GUID must always be given for Fallback bucketed features'
|
137
|
+
when :single
|
138
|
+
'all'
|
139
|
+
else
|
140
|
+
Determinator.notice_error "Cannot process the '#{feature.bucket_type}' bucket type found in #{feature.name}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def indicators_for(feature, actor_identifier)
|
113
145
|
# No identified means not enough info was given by the caller
|
114
146
|
# to determine an outcome for this feature
|
115
147
|
return unless actor_identifier
|
@@ -132,12 +164,12 @@ module Determinator
|
|
132
164
|
variant_weight_total = feature.variants.values.reduce(:+)
|
133
165
|
scale_factor = 65_535 / variant_weight_total.to_f
|
134
166
|
|
167
|
+
sorted_variants = feature.variants.keys.sort
|
135
168
|
# Find the variant the indicator sits within
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
return
|
140
|
-
previous_upper_bound = new_upper_bound
|
169
|
+
upper_bound = 0
|
170
|
+
sorted_variants.each do |variant_name|
|
171
|
+
upper_bound = upper_bound + scale_factor * feature.variants[variant_name]
|
172
|
+
return variant_name if indicator <= upper_bound
|
141
173
|
end
|
142
174
|
|
143
175
|
raise ArgumentError, "A variant should have been found by this point, there is a bug in the code."
|
data/lib/determinator/feature.rb
CHANGED
@@ -5,21 +5,20 @@ module Determinator
|
|
5
5
|
class Feature
|
6
6
|
attr_reader :name, :identifier, :bucket_type, :variants, :target_groups, :active, :winning_variant
|
7
7
|
|
8
|
-
BUCKET_TYPES = %i(id guid fallback)
|
9
|
-
|
10
8
|
def initialize(name:, identifier:, bucket_type:, target_groups:, variants: {}, overrides: {}, active: false, winning_variant: nil)
|
11
9
|
@name = name.to_s
|
12
|
-
@identifier = identifier.to_s
|
10
|
+
@identifier = (identifier || name).to_s
|
13
11
|
@variants = variants
|
14
|
-
@target_groups = target_groups
|
15
|
-
@winning_variant = winning_variant
|
12
|
+
@target_groups = parse_target_groups(target_groups)
|
13
|
+
@winning_variant = parse_outcome(winning_variant, allow_exclusion: false)
|
16
14
|
@active = active
|
17
|
-
|
18
15
|
@bucket_type = bucket_type.to_sym
|
19
|
-
raise ArgumentError, "Unknown bucket type: #{bucket_type}" unless BUCKET_TYPES.include?(@bucket_type)
|
20
16
|
|
21
17
|
# To prevent confusion between actor id data types
|
22
|
-
@overrides =
|
18
|
+
@overrides = overrides.each_with_object({}) do |(identifier, outcome), hash|
|
19
|
+
parsed = parse_outcome(outcome, allow_exclusion: true)
|
20
|
+
hash[identifier.to_s] = parsed unless parsed.nil?
|
21
|
+
end
|
23
22
|
end
|
24
23
|
|
25
24
|
def active?
|
@@ -47,8 +46,40 @@ module Determinator
|
|
47
46
|
overrides[id.to_s]
|
48
47
|
end
|
49
48
|
|
49
|
+
# Validates the given outcome for this feature.
|
50
|
+
def parse_outcome(outcome, allow_exclusion:)
|
51
|
+
valid_outcomes = experiment? ? variants.keys : [true]
|
52
|
+
valid_outcomes << false if allow_exclusion
|
53
|
+
valid_outcomes.include?(outcome) ? outcome : nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def ==(other)
|
57
|
+
Marshal.dump(self) == Marshal.dump(other)
|
58
|
+
end
|
59
|
+
|
50
60
|
private
|
51
61
|
|
52
62
|
attr_reader :overrides
|
63
|
+
|
64
|
+
def parse_target_groups(target_groups)
|
65
|
+
target_groups.map(&method(:parse_target_group)).compact
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_target_group(target_group)
|
69
|
+
return target_group if target_group.is_a? TargetGroup
|
70
|
+
|
71
|
+
constraints = target_group['constraints'].to_h
|
72
|
+
|
73
|
+
TargetGroup.new(
|
74
|
+
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
|
78
|
+
)
|
79
|
+
|
80
|
+
# Invalid target groups are ignored
|
81
|
+
rescue
|
82
|
+
nil
|
83
|
+
end
|
53
84
|
end
|
54
85
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Determinator
|
4
|
+
module Retrieve
|
5
|
+
# A class which loads features from files within the initialized folder
|
6
|
+
class File
|
7
|
+
# @param :root [String,Pathname] The path to be used as the root to look in
|
8
|
+
# @param :serializer [#load] A serializer which will take the string of the read file and return a Feature object.
|
9
|
+
def initialize(root:, serializer: Determinator::Serializers::JSON )
|
10
|
+
@root = Pathname.new(root)
|
11
|
+
@serializer = serializer
|
12
|
+
end
|
13
|
+
|
14
|
+
def retrieve(feature_id)
|
15
|
+
feature = @root.join(feature_id)
|
16
|
+
return unless feature.exist?
|
17
|
+
@serializer.load(feature.read)
|
18
|
+
rescue => e
|
19
|
+
Determinator.notice_error(e)
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'uri'
|
2
2
|
require 'routemaster/drain/caching'
|
3
3
|
require 'routemaster/responses/hateoas_response'
|
4
|
-
require 'determinator/
|
4
|
+
require 'determinator/serializers/json'
|
5
5
|
|
6
6
|
module Determinator
|
7
7
|
module Retrieve
|
@@ -18,21 +18,18 @@ module Determinator
|
|
18
18
|
CALLBACK_PATH = (URI.parse(ENV['ROUTEMASTER_CALLBACK_URL']).path rescue '/events').freeze
|
19
19
|
|
20
20
|
# @param :discovery_url [String] The bootstrap URL of the instance of Florence which defines Features.
|
21
|
-
def initialize(discovery_url
|
21
|
+
def initialize(discovery_url:)
|
22
22
|
client = ::Routemaster::APIClient.new(
|
23
23
|
response_class: ::Routemaster::Responses::HateoasResponse
|
24
24
|
)
|
25
|
-
@retrieval_cache = retrieval_cache
|
26
25
|
@actor_service = client.discover(discovery_url)
|
27
|
-
|
28
26
|
end
|
29
27
|
|
30
28
|
# Retrieves and processes the feature that goes by the given name on this retrieval mechanism.
|
31
29
|
# @return [Determinator::Feature,nil] The details of the specified feature
|
32
30
|
def retrieve(name)
|
33
|
-
|
34
|
-
|
35
|
-
end
|
31
|
+
obj = @actor_service.feature.show(name)
|
32
|
+
Determinator::Serializers::JSON.load(obj.body.to_hash)
|
36
33
|
rescue ::Routemaster::Errors::ResourceNotFound
|
37
34
|
# Don't be noisy
|
38
35
|
nil
|
@@ -51,68 +48,6 @@ module Determinator
|
|
51
48
|
def routemaster_app
|
52
49
|
@routemaster_app ||= ::Routemaster::Drain::Caching.new
|
53
50
|
end
|
54
|
-
|
55
|
-
def self.lookup_cache_key(feature_name)
|
56
|
-
"determinator_cache:#{feature_name}"
|
57
|
-
end
|
58
|
-
|
59
|
-
private
|
60
|
-
|
61
|
-
def cached_feature_lookup(feature_name)
|
62
|
-
build_feature_from_api_response(
|
63
|
-
@retrieval_cache.fetch(self.class.lookup_cache_key(feature_name)){ yield }
|
64
|
-
)
|
65
|
-
end
|
66
|
-
|
67
|
-
def build_feature_from_api_response(obj)
|
68
|
-
Feature.new(
|
69
|
-
name: obj.body.name,
|
70
|
-
identifier: obj.body.identifier,
|
71
|
-
bucket_type: obj.body.bucket_type,
|
72
|
-
active: obj.body.active,
|
73
|
-
target_groups: target_groups_attribute(obj.body.target_groups),
|
74
|
-
variants: obj.body.variants.to_h,
|
75
|
-
overrides: overrides_attribute(obj.body.overrides),
|
76
|
-
winning_variant: obj.body.winning_variant,
|
77
|
-
)
|
78
|
-
end
|
79
|
-
|
80
|
-
def target_groups_attribute(target_groups)
|
81
|
-
target_groups.map do |tg|
|
82
|
-
TargetGroup.new(
|
83
|
-
rollout: tg.rollout,
|
84
|
-
constraints: constraints_attribute(tg.constraints)
|
85
|
-
)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def constraints_attribute(constraints)
|
90
|
-
# TODO when FLO-3 closed: no need for legacy guard
|
91
|
-
return legacy_constraints_attribute(constraints) unless constraints.respond_to?(:each_pair)
|
92
|
-
constraints
|
93
|
-
end
|
94
|
-
|
95
|
-
def legacy_constraints_attribute(constraints)
|
96
|
-
constraints.each_with_object({}) do |constraint, h|
|
97
|
-
if h[constraint.scope]
|
98
|
-
h[constraint.scope] = [*h[constraint.scope], constraint.identifier]
|
99
|
-
else
|
100
|
-
h[constraint.scope] = constraint.identifier
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def overrides_attribute(overrides)
|
106
|
-
# TODO when FLO-3 closed: no need for legacy guard
|
107
|
-
return legacy_overrides_attribute(overrides) unless overrides.respond_to?(:each_pair)
|
108
|
-
overrides
|
109
|
-
end
|
110
|
-
|
111
|
-
def legacy_overrides_attribute(overrides)
|
112
|
-
overrides.each_with_object({}) do |override, h|
|
113
|
-
h[override[:user_id]] = override[:variant]
|
114
|
-
end
|
115
|
-
end
|
116
51
|
end
|
117
52
|
end
|
118
53
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Determinator
|
4
|
+
module Serializers
|
5
|
+
module JSON
|
6
|
+
class << self
|
7
|
+
def dump(feature)
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
def load(string_or_hash)
|
12
|
+
obj = string_or_hash.is_a?(Hash) ? string_or_hash : ::JSON.parse(string_or_hash)
|
13
|
+
|
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,
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/determinator/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: determinator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.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: 2018-02-
|
11
|
+
date: 2018-02-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: routemaster-drain
|
@@ -130,6 +130,7 @@ extensions: []
|
|
130
130
|
extra_rdoc_files: []
|
131
131
|
files:
|
132
132
|
- ".gitignore"
|
133
|
+
- ".gitmodules"
|
133
134
|
- ".rspec"
|
134
135
|
- ".travis.yml"
|
135
136
|
- CHANGELOG.md
|
@@ -178,11 +179,13 @@ files:
|
|
178
179
|
- examples/determinator-rails/public/robots.txt
|
179
180
|
- lib/determinator.rb
|
180
181
|
- lib/determinator/actor_control.rb
|
182
|
+
- lib/determinator/cache/fetch_wrapper.rb
|
181
183
|
- lib/determinator/control.rb
|
182
184
|
- lib/determinator/feature.rb
|
183
|
-
- lib/determinator/retrieve/
|
185
|
+
- lib/determinator/retrieve/file.rb
|
184
186
|
- lib/determinator/retrieve/null_retriever.rb
|
185
187
|
- lib/determinator/retrieve/routemaster.rb
|
188
|
+
- lib/determinator/serializers/json.rb
|
186
189
|
- lib/determinator/target_group.rb
|
187
190
|
- lib/determinator/version.rb
|
188
191
|
- lib/rspec/determinator.rb
|