unleash 4.1.0 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/pull_request.yml +1 -1
- data/.rubocop.yml +4 -6
- data/README.md +106 -17
- data/lib/unleash/configuration.rb +1 -1
- data/lib/unleash/constraint.rb +88 -10
- data/lib/unleash/context.rb +2 -1
- data/lib/unleash/feature_toggle.rb +3 -1
- data/lib/unleash/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93e6312bc93458378b877b6e949274295a1e0037af2cf8206d94b9e2852727eb
|
4
|
+
data.tar.gz: ecb08a7c1a4e9b0b63fad299649632443a99f7b8e4752b486970d7656b862e46
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eef06ea5a86751e6cfe68306a1d7f7e6c49fb0641c4916ddbdda30b4c1bec51a1349487dc156851229c4d496fe67cac96f0e5c875027998d0252bf154f2e02c9
|
7
|
+
data.tar.gz: 6fdf04c5e92b1de875ada2035b9fb495e80cf5138f4cd0171bd6686026a1280857c855ccdaa9bd15a915509f5bac4f9da50d08368f7de663c0a762b8654a3112
|
@@ -33,7 +33,7 @@ jobs:
|
|
33
33
|
- name: Install dependencies
|
34
34
|
run: bundle install
|
35
35
|
- name: Download test cases
|
36
|
-
run: git clone --depth 5 --branch v4.
|
36
|
+
run: git clone --depth 5 --branch v4.1.0 https://github.com/Unleash/client-specification.git client-specification
|
37
37
|
- name: rubocop
|
38
38
|
uses: reviewdog/action-rubocop@v2
|
39
39
|
with:
|
data/.rubocop.yml
CHANGED
@@ -7,8 +7,9 @@ Naming/PredicateName:
|
|
7
7
|
AllowedMethods:
|
8
8
|
- is_enabled?
|
9
9
|
|
10
|
+
|
10
11
|
Metrics/ClassLength:
|
11
|
-
Max:
|
12
|
+
Max: 125
|
12
13
|
Layout/LineLength:
|
13
14
|
Max: 140
|
14
15
|
Metrics/MethodLength:
|
@@ -16,13 +17,10 @@ Metrics/MethodLength:
|
|
16
17
|
Metrics/BlockLength:
|
17
18
|
Max: 110
|
18
19
|
Exclude:
|
19
|
-
- 'spec
|
20
|
-
- 'spec/unleash/client_spec.rb'
|
21
|
-
- 'spec/unleash/context_spec.rb'
|
22
|
-
- 'spec/unleash/feature_toggle_spec.rb'
|
20
|
+
- 'spec/**/*.rb'
|
23
21
|
|
24
22
|
Metrics/AbcSize:
|
25
|
-
Max:
|
23
|
+
Max: 30
|
26
24
|
Metrics/CyclomaticComplexity:
|
27
25
|
Max: 9
|
28
26
|
Metrics/PerceivedComplexity:
|
data/README.md
CHANGED
@@ -76,7 +76,7 @@ Argument | Description | Required? | Type | Default Value|
|
|
76
76
|
`disable_metrics` | Disables sending metrics to Unleash server. | N | Boolean | `false` |
|
77
77
|
`custom_http_headers` | Custom headers to send to Unleash. As of Unleash v4.0.0, the `Authorization` header is required. For example: `{'Authorization': '<API token>'}` | N | Hash | {} |
|
78
78
|
`timeout` | How long to wait for the connection to be established or wait in reading state (open_timeout/read_timeout) | N | Integer | 30 |
|
79
|
-
`retry_limit` | How many consecutive failures in connecting to the Unleash server are allowed before giving up.
|
79
|
+
`retry_limit` | How many consecutive failures in connecting to the Unleash server are allowed before giving up. The default is to retry indefinitely. | N | Float::INFINITY | 5 |
|
80
80
|
`backup_file` | Filename to store the last known state from the Unleash server. Best to not change this from the default. | N | String | `Dir.tmpdir + "/unleash-#{app_name}-repo.json` |
|
81
81
|
`logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` |
|
82
82
|
`log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::WARN` |
|
@@ -124,29 +124,97 @@ Unleash.configure do |config|
|
|
124
124
|
end
|
125
125
|
|
126
126
|
UNLEASH = Unleash::Client.new
|
127
|
+
|
128
|
+
# Or if preferred:
|
129
|
+
# Rails.configuration.unleash = Unleash::Client.new
|
130
|
+
```
|
131
|
+
For `config.instance_id` use a string with a unique identification for the running instance.
|
132
|
+
For example: it could be the hostname, if you only run one App per host.
|
133
|
+
Or the docker container id, if you are running in docker.
|
134
|
+
If it is not set the client will generate an unique UUID for each execution.
|
135
|
+
|
136
|
+
To have it available in the `rails console` command as well, also add to the file above:
|
137
|
+
```ruby
|
138
|
+
Rails.application.console do
|
139
|
+
UNLEASH = Unleash::Client.new
|
140
|
+
# or
|
141
|
+
# Rails.configuration.unleash = Unleash::Client.new
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
#### Add Initializer if using [Puma in clustered mode](https://github.com/puma/puma#clustered-mode)
|
146
|
+
|
147
|
+
That is, multiple workers configured in `puma.rb`:
|
148
|
+
```ruby
|
149
|
+
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
|
127
150
|
```
|
128
|
-
For `config.instance_id` use a string with a unique identification for the running instance. For example: it could be the hostname, if you only run one App per host. Or the docker container id, if you are running in docker. If it is not set the client will generate an unique UUID for each execution.
|
129
151
|
|
152
|
+
##### with `preload_app!`
|
130
153
|
|
131
|
-
|
154
|
+
Then you may keep the client configuration still in `config/initializers/unleash.rb`:
|
155
|
+
```ruby
|
156
|
+
Unleash.configure do |config|
|
157
|
+
config.app_name = Rails.application.class.parent.to_s
|
158
|
+
config.environment = Rails.env
|
159
|
+
config.url = 'http://unleash.herokuapp.com/api'
|
160
|
+
config.custom_http_headers = {'Authorization': '<API token>'}
|
161
|
+
end
|
162
|
+
```
|
132
163
|
|
133
|
-
|
164
|
+
But you must ensure that the unleash client is instantiated only after the process is forked.
|
165
|
+
This is done by creating the client inside the `on_worker_boot` code block in `puma.rb` as below:
|
134
166
|
|
135
167
|
```ruby
|
168
|
+
#...
|
169
|
+
preload_app!
|
170
|
+
#...
|
171
|
+
|
136
172
|
on_worker_boot do
|
137
173
|
# ...
|
138
174
|
|
139
|
-
Unleash.
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
end
|
145
|
-
Rails.configuration.unleash = Unleash::Client.new
|
175
|
+
::UNLEASH = Unleash::Client.new
|
176
|
+
end
|
177
|
+
|
178
|
+
on_worker_shutdown do
|
179
|
+
::UNLEASH.shutdown
|
146
180
|
end
|
147
181
|
```
|
148
182
|
|
149
|
-
|
183
|
+
##### without `preload_app!`
|
184
|
+
|
185
|
+
By not using `preload_app!`:
|
186
|
+
- the `Rails` constant will NOT be available.
|
187
|
+
- but phased restarts will be possible.
|
188
|
+
|
189
|
+
You need to ensure that in `puma.rb`:
|
190
|
+
- loading unleash sdk with `require 'unleash'` explicitly, as it will not be pre-loaded.
|
191
|
+
- all parameters must be explicitly set in the `on_worker_boot` block, as `config/initializers/unleash.rb` is not read.
|
192
|
+
- there are no references to `Rails` constant, as that is not yet available.
|
193
|
+
|
194
|
+
Example for `puma.rb`:
|
195
|
+
```ruby
|
196
|
+
require 'unleash'
|
197
|
+
|
198
|
+
#...
|
199
|
+
# no preload_app!
|
200
|
+
|
201
|
+
on_worker_boot do
|
202
|
+
# ...
|
203
|
+
|
204
|
+
::UNLEASH = Unleash::Client.new(
|
205
|
+
app_name: 'my_rails_app',
|
206
|
+
environment: 'development',
|
207
|
+
url: 'http://unleash.herokuapp.com/api',
|
208
|
+
custom_http_headers: {'Authorization': '<API token>'},
|
209
|
+
)
|
210
|
+
end
|
211
|
+
|
212
|
+
on_worker_shutdown do
|
213
|
+
::UNLEASH.shutdown
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
Note that we also added shutdown hooks in `on_worker_shutdown`, to ensure a clean shutdown.
|
150
218
|
|
151
219
|
#### Add Initializer if using [Phusion Passenger](https://github.com/phusion/passenger)
|
152
220
|
|
@@ -171,6 +239,23 @@ PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
|
171
239
|
end
|
172
240
|
```
|
173
241
|
|
242
|
+
#### Add Initializer hooks when using within [Sidekiq](https://github.com/mperham/sidekiq)
|
243
|
+
|
244
|
+
Note that in this case we require that the code block for `Unleash.configure` is set beforehand.
|
245
|
+
For example in `config/initializers/unleash.rb`.
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
Sidekiq.configure_server do |config|
|
249
|
+
config.on(:startup) do
|
250
|
+
UNLEASH = Unleash::Client.new
|
251
|
+
end
|
252
|
+
|
253
|
+
config.on(:shutdown) do
|
254
|
+
UNLEASH.shutdown
|
255
|
+
end
|
256
|
+
end
|
257
|
+
```
|
258
|
+
|
174
259
|
#### Set Unleash::Context
|
175
260
|
|
176
261
|
Be sure to add the following method and callback in the application controller to have `@unleash_context` set for all requests:
|
@@ -210,7 +295,8 @@ if Rails.configuration.unleash.is_enabled? "AwesomeFeature", @unleash_context
|
|
210
295
|
end
|
211
296
|
```
|
212
297
|
|
213
|
-
If the feature is not found in the server, it will by default return false.
|
298
|
+
If the feature is not found in the server, it will by default return false.
|
299
|
+
However you can override that by setting the default return value to `true`:
|
214
300
|
|
215
301
|
```ruby
|
216
302
|
if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context, true
|
@@ -275,7 +361,8 @@ puts "variant color is: #{variant.payload.fetch('color')}"
|
|
275
361
|
|
276
362
|
## Bootstrapping
|
277
363
|
|
278
|
-
Bootstrap configuration allows the client to be initialized with a predefined set of toggle states.
|
364
|
+
Bootstrap configuration allows the client to be initialized with a predefined set of toggle states.
|
365
|
+
Bootstrapping can be configured by providing a bootstrap configuration when initializing the client.
|
279
366
|
```ruby
|
280
367
|
@unleash = Unleash::Client.new(
|
281
368
|
url: 'http://unleash.herokuapp.com/api',
|
@@ -295,7 +382,8 @@ The `Bootstrap::Configuration` initializer takes a hash with one of the followin
|
|
295
382
|
* `data` - A raw JSON string as returned by the Unleash server.
|
296
383
|
* `block` - A lambda containing custom logic if you need it, an example is provided below.
|
297
384
|
|
298
|
-
You should only specify one type of bootstrapping since only one will be invoked and the others will be ignored.
|
385
|
+
You should only specify one type of bootstrapping since only one will be invoked and the others will be ignored.
|
386
|
+
The order of preference is as follows:
|
299
387
|
|
300
388
|
- Select a data bootstrapper if it exists.
|
301
389
|
- If no data bootstrapper exists, select the block bootstrapper.
|
@@ -324,11 +412,12 @@ custom_boostrapper = lambda {
|
|
324
412
|
custom_http_headers: { 'Authorization': '<API token>' },
|
325
413
|
bootstrap_config: Unleash::Bootstrap::Configuration.new({
|
326
414
|
block: custom_boostrapper
|
327
|
-
}
|
415
|
+
})
|
328
416
|
)
|
329
417
|
```
|
330
418
|
|
331
|
-
This example could be easily achieved with a file bootstrapper, this is just to illustrate the usage of custom bootstrapping.
|
419
|
+
This example could be easily achieved with a file bootstrapper, this is just to illustrate the usage of custom bootstrapping.
|
420
|
+
Be aware that the client initializer will block until bootstrapping is complete.
|
332
421
|
|
333
422
|
#### Client methods
|
334
423
|
|
data/lib/unleash/constraint.rb
CHANGED
@@ -1,26 +1,104 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
1
3
|
module Unleash
|
2
4
|
class Constraint
|
3
|
-
attr_accessor :context_name, :operator, :
|
5
|
+
attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
|
6
|
+
|
7
|
+
OPERATORS = {
|
8
|
+
IN: ->(context_v, constraint_v){ constraint_v.include? context_v },
|
9
|
+
NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v },
|
10
|
+
STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
|
11
|
+
STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
|
12
|
+
STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
|
13
|
+
NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } },
|
14
|
+
NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } },
|
15
|
+
NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } },
|
16
|
+
NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } },
|
17
|
+
NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } },
|
18
|
+
DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } },
|
19
|
+
DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
|
20
|
+
SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
|
21
|
+
SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } },
|
22
|
+
SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } }
|
23
|
+
}.freeze
|
4
24
|
|
5
|
-
|
25
|
+
LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
|
6
26
|
|
7
|
-
def initialize(context_name, operator,
|
27
|
+
def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
|
8
28
|
raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
|
9
|
-
raise ArgumentError, "operator does not hold a valid value:" +
|
10
|
-
|
29
|
+
raise ArgumentError, "operator does not hold a valid value:" + OPERATORS.keys unless OPERATORS.include? operator.to_sym
|
30
|
+
|
31
|
+
self.validate_constraint_value_type(operator.to_sym, value)
|
11
32
|
|
12
33
|
self.context_name = context_name
|
13
|
-
self.operator = operator
|
14
|
-
self.
|
34
|
+
self.operator = operator.to_sym
|
35
|
+
self.value = value
|
36
|
+
self.inverted = !!inverted
|
37
|
+
self.case_insensitive = !!case_insensitive
|
15
38
|
end
|
16
39
|
|
17
40
|
def matches_context?(context)
|
18
|
-
Unleash.logger.debug "Unleash::Constraint matches_context?
|
41
|
+
Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" \
|
19
42
|
" #{context.get_by_name(self.context_name)} "
|
43
|
+
match = matches_constraint?(context)
|
44
|
+
self.inverted ? !match : match
|
45
|
+
rescue KeyError
|
46
|
+
Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
|
47
|
+
found on the context"
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.on_valid_date(val1, val2)
|
52
|
+
val1 = DateTime.parse(val1)
|
53
|
+
val2 = DateTime.parse(val2)
|
54
|
+
yield(val1, val2)
|
55
|
+
rescue ArgumentError
|
56
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
57
|
+
or constraint_value (#{val2}) into a date. Returning false!"
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.on_valid_float(val1, val2)
|
62
|
+
val1 = Float(val1)
|
63
|
+
val2 = Float(val2)
|
64
|
+
yield(val1, val2)
|
65
|
+
rescue ArgumentError
|
66
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
67
|
+
or constraint_value (#{val2}) into a number. Returning false!"
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.on_valid_version(val1, val2)
|
72
|
+
val1 = Gem::Version.new(val1)
|
73
|
+
val2 = Gem::Version.new(val2)
|
74
|
+
yield(val1, val2)
|
75
|
+
rescue ArgumentError
|
76
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
77
|
+
or constraint_value (#{val2}) into a version. Return false!"
|
78
|
+
false
|
79
|
+
end
|
80
|
+
|
81
|
+
# This should be a private method but for some reason this fails on Ruby 2.5
|
82
|
+
def validate_constraint_value_type(operator, value)
|
83
|
+
raise ArgumentError, "context_name is not an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
|
84
|
+
raise ArgumentError, "context_name is not a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def matches_constraint?(context)
|
90
|
+
unless OPERATORS.include?(self.operator)
|
91
|
+
Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
|
92
|
+
false
|
93
|
+
end
|
94
|
+
|
95
|
+
v = self.value.dup
|
96
|
+
context_value = context.get_by_name(self.context_name)
|
20
97
|
|
21
|
-
|
98
|
+
v.map!(&:upcase) if self.case_insensitive
|
99
|
+
context_value.upcase! if self.case_insensitive
|
22
100
|
|
23
|
-
operator
|
101
|
+
OPERATORS[self.operator].call(context_value, v)
|
24
102
|
end
|
25
103
|
end
|
26
104
|
end
|
data/lib/unleash/context.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Unleash
|
2
2
|
class Context
|
3
|
-
ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address].freeze
|
3
|
+
ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address, :current_time].freeze
|
4
4
|
|
5
5
|
attr_accessor(*[ATTRS, :properties].flatten)
|
6
6
|
|
@@ -12,6 +12,7 @@ module Unleash
|
|
12
12
|
self.user_id = value_for('userId', params)
|
13
13
|
self.session_id = value_for('sessionId', params)
|
14
14
|
self.remote_address = value_for('remoteAddress', params)
|
15
|
+
self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
|
15
16
|
|
16
17
|
properties = value_for('properties', params)
|
17
18
|
self.properties = properties.is_a?(Hash) ? properties.transform_keys(&:to_sym) : {}
|
@@ -139,7 +139,9 @@ module Unleash
|
|
139
139
|
Constraint.new(
|
140
140
|
c.fetch('contextName'),
|
141
141
|
c.fetch('operator'),
|
142
|
-
c.fetch('values')
|
142
|
+
c.fetch('values', nil) || c.fetch('value', nil),
|
143
|
+
inverted: c.fetch('inverted', false),
|
144
|
+
case_insensitive: c.fetch('caseInsensitive', false)
|
143
145
|
)
|
144
146
|
end
|
145
147
|
)
|
data/lib/unleash/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unleash
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Renato Arruda
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-03-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: murmurhash3
|