determinator 0.12.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|