unleash 3.2.5 → 4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1535ef477a380d94767952c3d4a660ab029bf78100f7d30246ff211517172f2
4
- data.tar.gz: b357d2bda18a9b1378988d4467b78cbe02129b519578272da7fd0259cada97d0
3
+ metadata.gz: 4ce69a2f4588a0d5e459e76aa8d78894497736f484ae20f5d987d50b34aced50
4
+ data.tar.gz: 24222c2c023197dc4f283ee4e09316f233245b2e82ca4c9ad934fe5519de75d5
5
5
  SHA512:
6
- metadata.gz: e1fe0ae8b8ff86fdbe3c0d9a68b0d559112a9d568bcb8762f946d191b33ee8bdcd25bad63983d6b507269534309690fc2f858c5989b287004e5df2a3b8755e36
7
- data.tar.gz: 8e05aeb05427eaa6e0391703aa524902c3094be21c176a374093ec5263b2a2fcc4b9bce4ab2fb9627d18ecf822dfbd1f8cfecaaba8a9517ae96f9efd687ab1a8
6
+ metadata.gz: 6e056f24c5f840b211f04bc3c605ccf0846d1ff8717cc824d1e50307e636f28c50f91a8fe85f648de9f16377d5441d5feea459a15ad1eae00c35a6238ebb0355
7
+ data.tar.gz: 51b49947d4aea7d1901bf0083ef366604b5d1cd7859cab1cfaceacc420fb056c4188dd4512c1a7feddc5f9711e0c5ad2660e91c1e681cd57e47307f29948790c
@@ -0,0 +1,71 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+
10
+ runs-on: ${{ matrix.os }}-latest
11
+
12
+ strategy:
13
+ matrix:
14
+ os:
15
+ - ubuntu
16
+ - macos
17
+ ruby-version:
18
+ - jruby
19
+ - 3.0
20
+ - 2.7
21
+ - 2.6
22
+ - 2.5
23
+
24
+ steps:
25
+ - uses: actions/checkout@v2
26
+ - name: Set up Ruby ${{ matrix.ruby-version }}
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ bundler-cache: true
30
+ ruby-version: ${{ matrix.ruby-version }}
31
+ - name: Install dependencies
32
+ run: bundle install
33
+ - name: Download test cases
34
+ run: git clone --depth 5 --branch v4.0.0 https://github.com/Unleash/client-specification.git client-specification
35
+ - name: rubocop
36
+ uses: reviewdog/action-rubocop@v2
37
+ with:
38
+ github_token: ${{ secrets.GITHUB_TOKEN }}
39
+ rubocop_version: gemfile
40
+ rubocop_extensions: rubocop-rspec:gemfile
41
+ reporter: github-pr-review # Default is github-pr-check
42
+ - name: Run tests
43
+ run: bundle exec rake
44
+ env:
45
+ COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46
+ - name: Coveralls Parallel
47
+ uses: coverallsapp/github-action@master
48
+ with:
49
+ github-token: ${{ secrets.GITHUB_TOKEN }}
50
+ flag-name: run-${{ matrix.test_number }}
51
+ parallel: true
52
+ - name: Notify Slack of pipeline completion
53
+ uses: 8398a7/action-slack@v3
54
+ if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}
55
+ with:
56
+ status: ${{ job.status }}
57
+ text: Built on ${{ matrix.os }} - Ruby ${{ matrix.ruby-version }}
58
+ fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
59
+ env:
60
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
61
+
62
+ finish:
63
+ needs: test
64
+ runs-on: ubuntu-latest
65
+ steps:
66
+ - name: Coveralls Finished
67
+ uses: coverallsapp/github-action@master
68
+ with:
69
+ github-token: ${{ secrets.GITHUB_TOKEN }}
70
+ parallel-finished: true
71
+
data/.gitignore CHANGED
@@ -9,6 +9,9 @@
9
9
  /tmp/
10
10
  /vendor
11
11
 
12
+ # IntelliJ
13
+ .idea/
14
+
12
15
  # rspec failure tracking
13
16
  .rspec_status
14
17
 
data/.rubocop.yml CHANGED
@@ -14,17 +14,19 @@ Layout/LineLength:
14
14
  Metrics/MethodLength:
15
15
  Max: 20
16
16
  Metrics/BlockLength:
17
- Max: 100
17
+ Max: 110
18
18
  Exclude:
19
+ - 'spec/unleash/configuration_spec.rb'
19
20
  - 'spec/unleash/client_spec.rb'
21
+ - 'spec/unleash/context_spec.rb'
20
22
  - 'spec/unleash/feature_toggle_spec.rb'
21
23
 
22
24
  Metrics/AbcSize:
23
- Max: 25
25
+ Max: 28
24
26
  Metrics/CyclomaticComplexity:
25
27
  Max: 9
26
28
  Metrics/PerceivedComplexity:
27
- Max: 9
29
+ Max: 10
28
30
 
29
31
  Style/Documentation:
30
32
  Enabled: false
@@ -51,6 +53,12 @@ Style/HashTransformKeys:
51
53
  Enabled: true
52
54
  Style/HashTransformValues:
53
55
  Enabled: true
56
+ Style/EmptyElse:
57
+ Exclude:
58
+ - 'lib/unleash/strategy/flexible_rollout.rb'
59
+
60
+ Style/DoubleNegation:
61
+ Enabled: false
54
62
 
55
63
  Style/IfInsideElse:
56
64
  Exclude:
@@ -60,6 +68,61 @@ Style/Next:
60
68
  Exclude:
61
69
  - 'lib/unleash/scheduled_executor.rb'
62
70
 
71
+
72
+ Style/AccessorGrouping:
73
+ Enabled: true
74
+ Style/BisectedAttrAccessor:
75
+ Enabled: true
76
+ Style/CaseLikeIf:
77
+ Enabled: true
78
+ #Style/ClassEqualityComparison:
79
+ # Enabled: true
80
+ Style/CombinableLoops:
81
+ Enabled: true
82
+ Style/ExplicitBlockArgument:
83
+ Enabled: true
84
+ Style/ExponentialNotation:
85
+ Enabled: true
86
+ #Style/GlobalStdStream:
87
+ # Enabled: true
88
+ Style/HashAsLastArrayItem:
89
+ Enabled: true
90
+ Style/HashLikeCase:
91
+ Enabled: true
92
+ Style/KeywordParametersOrder:
93
+ Enabled: true
94
+ #Style/OptionalBooleanParameter:
95
+ # Enabled: false
96
+ Style/RedundantAssignment:
97
+ Enabled: true
98
+ Style/RedundantFetchBlock:
99
+ Enabled: true
100
+ Style/RedundantFileExtensionInRequire:
101
+ Enabled: true
102
+ Style/RedundantRegexpCharacterClass:
103
+ Enabled: true
104
+ Style/RedundantRegexpEscape:
105
+ Enabled: true
106
+ Style/RedundantSelfAssignment:
107
+ Enabled: true
108
+ Style/SingleArgumentDig:
109
+ Enabled: true
110
+ Style/SlicingWithRange:
111
+ Enabled: true
112
+ Style/SoleNestedConditional:
113
+ Enabled: true
114
+ Style/StringConcatenation:
115
+ Enabled: false
116
+ Style/TrailingCommaInHashLiteral:
117
+ Enabled: true
118
+ # EnforcedStyleForMultiline: consistent_comma
119
+
120
+ Layout/BeginEndAlignment:
121
+ Enabled: true
122
+ Layout/EmptyLinesAroundAttributeAccessor:
123
+ Enabled: true
124
+ Layout/SpaceAroundMethodCallOperator:
125
+ Enabled: true
63
126
  Layout/MultilineMethodCallIndentation:
64
127
  EnforcedStyle: indented
65
128
 
@@ -68,3 +131,50 @@ Layout/SpaceBeforeBlockBraces:
68
131
  Exclude:
69
132
  - 'unleash-client.gemspec'
70
133
  - 'spec/**/*.rb'
134
+
135
+ Lint/BinaryOperatorWithIdenticalOperands:
136
+ Enabled: true
137
+ Lint/ConstantDefinitionInBlock:
138
+ Enabled: false
139
+ Lint/DeprecatedOpenSSLConstant:
140
+ Enabled: true
141
+ Lint/DuplicateElsifCondition:
142
+ Enabled: true
143
+ Lint/DuplicateRequire:
144
+ Enabled: true
145
+ Lint/DuplicateRescueException:
146
+ Enabled: true
147
+ Lint/EmptyConditionalBody:
148
+ Enabled: true
149
+ Lint/EmptyFile:
150
+ Enabled: true
151
+ Lint/FloatComparison:
152
+ Enabled: true
153
+ Lint/HashCompareByIdentity:
154
+ Enabled: true
155
+ Lint/IdentityComparison:
156
+ Enabled: true
157
+ Lint/MissingSuper:
158
+ Enabled: false
159
+ Lint/MixedRegexpCaptureTypes:
160
+ Enabled: true
161
+ Lint/OutOfRangeRegexpRef:
162
+ Enabled: true
163
+ Lint/RaiseException:
164
+ Enabled: true
165
+ Lint/RedundantSafeNavigation:
166
+ Enabled: true
167
+ Lint/SelfAssignment:
168
+ Enabled: true
169
+ Lint/StructNewOverride:
170
+ Enabled: true
171
+ Lint/TopLevelReturnWithArgument:
172
+ Enabled: true
173
+ Lint/TrailingCommaInAttributeDeclaration:
174
+ Enabled: true
175
+ Lint/UnreachableLoop:
176
+ Enabled: true
177
+ Lint/UselessMethodDefinition:
178
+ Enabled: true
179
+ Lint/UselessTimes:
180
+ Enabled: true
data/README.md CHANGED
@@ -21,7 +21,7 @@ Leverage the [Unleash Server](https://github.com/Unleash/unleash) for powerful f
21
21
  Add this line to your application's Gemfile:
22
22
 
23
23
  ```ruby
24
- gem 'unleash', '~> 3.2.5'
24
+ gem 'unleash', '~> 4.0.0'
25
25
  ```
26
26
 
27
27
  And then execute:
@@ -34,22 +34,29 @@ Or install it yourself as:
34
34
 
35
35
  ## Configure
36
36
 
37
- It is **required** to configure the `url` of the unleash server and `app_name` with the name of the runninng application. Please substitute the sample `'http://unleash.herokuapp.com/api'` for the url of your own instance.
37
+ It is **required** to configure:
38
+ - `url` of the unleash server
39
+ - `app_name` with the name of the runninng application.
40
+ - `custom_http_headers` with `{'Authorization': '<API token>'}` when using Unleash v4.0.0 and later.
38
41
 
39
- It is **highly recommended** to configure the `instance_id` parameter as well.
42
+ Please substitute the example `'http://unleash.herokuapp.com/api'` for the url of your own instance.
43
+
44
+ It is **highly recommended** to configure:
45
+ - `instance_id` parameter with a unique identifier for the running instance.
40
46
 
41
47
 
42
48
  ```ruby
43
49
  Unleash.configure do |config|
44
- config.url = 'http://unleash.herokuapp.com/api'
45
- config.app_name = 'my_ruby_app'
50
+ config.app_name = 'my_ruby_app'
51
+ config.url = 'http://unleash.herokuapp.com/api'
52
+ config.custom_http_headers = {'Authorization': '<API token>'}
46
53
  end
47
54
  ```
48
55
 
49
56
  or instantiate the client with the valid configuration:
50
57
 
51
58
  ```ruby
52
- UNLEASH = Unleash::Client.new(url: 'http://unleash.herokuapp.com/api', app_name: 'my_ruby_app')
59
+ UNLEASH = Unleash::Client.new(url: 'http://unleash.herokuapp.com/api', app_name: 'my_ruby_app', custom_http_headers: {'Authorization': '<API token>'})
53
60
  ```
54
61
 
55
62
  #### List of Arguments
@@ -60,16 +67,17 @@ Argument | Description | Required? | Type | Default Value|
60
67
  `app_name` | Name of your program. | Y | String | N/A |
61
68
  `instance_id` | Identifier for the running instance of program. Important so you can trace back to where metrics are being collected from. **Highly recommended be be set.** | N | String | random UUID |
62
69
  `environment` | Environment the program is running on. Could be for example `prod` or `dev`. Not yet in use. | N | String | `default` |
70
+ `project_name` | Name of the project to retrieve features from. If not set, all feature flags will be retrieved. | N | String | nil |
63
71
  `refresh_interval` | How often the unleash client should check with the server for configuration changes. | N | Integer | 15 |
64
- `metrics_interval` | How often the unleash client should send metrics to server. | N | Integer | 10 |
72
+ `metrics_interval` | How often the unleash client should send metrics to server. | N | Integer | 30 |
65
73
  `disable_client` | Disables all communication with the Unleash server, effectively taking it *offline*. If set, `is_enabled?` will always answer with the `default_value` and configuration validation is skipped. Defeats the entire purpose of using unleash, but can be useful in when running tests. | N | Boolean | `false` |
66
74
  `disable_metrics` | Disables sending metrics to Unleash server. | N | Boolean | `false` |
67
- `custom_http_headers` | Custom headers to send to Unleash. | N | Hash | {} |
75
+ `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 | {} |
68
76
  `timeout` | How long to wait for the connection to be established or wait in reading state (open_timeout/read_timeout) | N | Integer | 30 |
69
77
  `retry_limit` | How many consecutive failures in connecting to the Unleash server are allowed before giving up. Use `Float::INFINITY` if you would like it to never give up. | N | Numeric | 5 |
70
78
  `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` |
71
79
  `logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` |
72
- `log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::ERROR` |
80
+ `log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::WARN` |
73
81
 
74
82
  For in a more in depth look, please see `lib/unleash/configuration.rb`.
75
83
 
@@ -80,7 +88,7 @@ For in a more in depth look, please see `lib/unleash/configuration.rb`.
80
88
  require 'unleash'
81
89
  require 'unleash/context'
82
90
 
83
- @unleash = Unleash::Client.new(url: 'http://unleash.herokuapp.com/api', app_name: 'my_ruby_app')
91
+ @unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: 'http://unleash.herokuapp.com/api', custom_http_headers: {'Authorization': '<API token>'})
84
92
 
85
93
  feature_name = "AwesomeFeature"
86
94
  unleash_context = Unleash::Context.new
@@ -101,8 +109,8 @@ Put in `config/initializers/unleash.rb`:
101
109
 
102
110
  ```ruby
103
111
  Unleash.configure do |config|
104
- config.url = 'http://unleash.herokuapp.com/api'
105
112
  config.app_name = Rails.application.class.parent.to_s
113
+ config.url = 'http://unleash.herokuapp.com/api'
106
114
  # config.instance_id = "#{Socket.gethostname}"
107
115
  config.logger = Rails.logger
108
116
  config.environment = Rails.env
@@ -122,9 +130,10 @@ on_worker_boot do
122
130
  # ...
123
131
 
124
132
  Unleash.configure do |config|
125
- config.url = 'http://unleash.herokuapp.com/api'
126
- config.app_name = Rails.application.class.parent.to_s
133
+ config.app_name = Rails.application.class.parent.to_s
127
134
  config.environment = Rails.env
135
+ config.url = 'http://unleash.herokuapp.com/api'
136
+ config.custom_http_headers = {'Authorization': '<API token>'}
128
137
  end
129
138
  Rails.configuration.unleash = Unleash::Client.new
130
139
  end
@@ -132,6 +141,28 @@ end
132
141
 
133
142
  Instead of the configuration in `config/initializers/unleash.rb`.
134
143
 
144
+ #### Add Initializer if using [Phusion Passenger](https://github.com/phusion/passenger)
145
+
146
+ The unleash client needs to be configured and instantiated inside the `PhusionPassenger.on_event(:starting_worker_process)` code block due to [smart spawning](https://www.phusionpassenger.com/library/indepth/ruby/spawn_methods/#smart-spawning-caveats):
147
+
148
+ The initializer in `config/initializers/unleash.rb` should look like:
149
+
150
+ ```ruby
151
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
152
+ if forked
153
+ Unleash.configure do |config|
154
+ config.app_name = Rails.application.class.parent.to_s
155
+ # config.instance_id = "#{Socket.gethostname}"
156
+ config.logger = Rails.logger
157
+ config.environment = Rails.env
158
+ config.url = 'http://unleash.herokuapp.com/api'
159
+ config.custom_http_headers = {'Authorization': '<API token>'}
160
+ end
161
+
162
+ UNLEASH = Unleash::Client.new
163
+ end
164
+ end
165
+ ```
135
166
 
136
167
  #### Set Unleash::Context
137
168
 
@@ -180,6 +211,40 @@ if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context, true
180
211
  end
181
212
  ```
182
213
 
214
+ Another possibility is to send a block, [Lambda](https://ruby-doc.org/core-3.0.1/Kernel.html#method-i-lambda) or [Proc](https://ruby-doc.org/core-3.0.1/Proc.html#method-i-yield)
215
+ to evaluate the default value:
216
+
217
+ ```ruby
218
+ net_check_proc = proc do |feature_name, context|
219
+ context.remote_address.starts_with?("10.0.0.")
220
+ end
221
+
222
+ if UNLEASH.is_enabled?("AwesomeFeature", @unleash_context, &net_check_proc)
223
+ puts "AwesomeFeature is enabled by default if you are in the 10.0.0.* network."
224
+ end
225
+ ```
226
+
227
+ or
228
+
229
+ ```ruby
230
+ awesomeness = 10
231
+ @unleash_context.properties[:coolness] = 10
232
+
233
+ if UNLEASH.is_enabled?("AwesomeFeature", @unleash_context) { |feat, ctx| awesomeness >= 6 && ctx.properties[:coolness] >= 8 }
234
+ puts "AwesomeFeature is enabled by default if both the user has a high enought coolness and the application has a high enough awesomeness"
235
+ end
236
+ ```
237
+
238
+ Note:
239
+ - The block/lambda/proc can use feature name and context as an arguments.
240
+ - The client will evaluate the fallback function once per call of `is_enabled()`.
241
+ Please keep this in mind when creating your fallback function!
242
+ - The returned value of the block should be a boolean.
243
+ However the client will coerce the result to boolean via `!!`.
244
+ - If both a `default_value` and `fallback_function` are supplied,
245
+ the client will define the default value by `OR`ing the default value and the output of the fallback function.
246
+
247
+
183
248
  Alternatively by using `if_enabled` you can send a code block to be executed as a parameter:
184
249
 
185
250
  ```ruby
@@ -188,6 +253,8 @@ UNLEASH.if_enabled "AwesomeFeature", @unleash_context, true do
188
253
  end
189
254
  ```
190
255
 
256
+ Note: `if_enabled` only supports `default_value`, but not `fallback_function`.
257
+
191
258
  ##### Variations
192
259
 
193
260
  If no variant is found in the server, use the fallback variant.
@@ -211,6 +278,7 @@ Method Name | Description | Return Type |
211
278
  `shutdown` | Save metrics to disk, flush metrics to server, and then kill ToggleFetcher and MetricsReporter threads. A safe shutdown. Not really useful in long running applications, like web applications. | nil |
212
279
  `shutdown!` | Kill ToggleFetcher and MetricsReporter threads immediately. | nil |
213
280
 
281
+ For the full method signatures, please see [client.rb](lib/unleash/client.rb)
214
282
 
215
283
  ## Local test client
216
284
 
@@ -228,6 +296,7 @@ This client comes with the all the required strategies out of the box:
228
296
 
229
297
  * ApplicationHostnameStrategy
230
298
  * DefaultStrategy
299
+ * FlexibleRolloutStrategy
231
300
  * GradualRolloutRandomStrategy
232
301
  * GradualRolloutSessionIdStrategy
233
302
  * GradualRolloutUserIdStrategy
data/bin/unleash-client CHANGED
@@ -12,11 +12,13 @@ options = {
12
12
  url: 'http://localhost:4242',
13
13
  demo: false,
14
14
  disable_metrics: true,
15
+ custom_http_headers: {},
15
16
  sleep: 0.1
16
17
  }
17
18
 
18
19
  OptionParser.new do |opts|
19
- opts.banner = "Usage: #{__FILE__} [options] feature [key1=val1] [key2=val2]"
20
+ opts.banner = "Usage: #{__FILE__} [options] feature [contextKey1=val1] [contextKey2=val2] \n\n" \
21
+ "Where contextKey1 could be user_id, session_id, remote_address or any field in the Context class (or any property within it).\n"
20
22
 
21
23
  opts.on("-V", "--variant", "Fetch variant for feature") do |v|
22
24
  options[:variant] = v
@@ -46,6 +48,13 @@ OptionParser.new do |opts|
46
48
  options[:sleep] = s
47
49
  end
48
50
 
51
+ opts.on("-H", "--http-headers='Authorization: *:developement.secretstring'",
52
+ "Adds http headers to all requests on the unleash server. Use multiple times for multiple headers.") do |h|
53
+ http_header_as_hash = [h].to_h{ |l| l.split(": ") }.transform_keys(&:to_sym)
54
+
55
+ options[:custom_http_headers].merge!(http_header_as_hash)
56
+ end
57
+
49
58
  opts.on("-h", "--help", "Prints this help") do
50
59
  puts opts
51
60
  exit
@@ -70,10 +79,11 @@ log_level = \
70
79
  url: options[:url],
71
80
  app_name: 'unleash-client-ruby-cli',
72
81
  disable_metrics: options[:metrics],
82
+ custom_http_headers: options[:custom_http_headers],
73
83
  log_level: log_level
74
84
  )
75
85
 
76
- context_params = ARGV.map{ |e| e.split("=") }.map{ |k, v| [k.to_sym, v] }.to_h
86
+ context_params = ARGV.to_h{ |l| l.split("=") }.transform_keys(&:to_sym)
77
87
  context_properties = context_params.reject{ |k, _v| [:user_id, :session_id, :remote_address].include? k }
78
88
  context_params.select!{ |k, _v| [:user_id, :session_id, :remote_address].include? k }
79
89
  context_params.merge!(properties: context_properties) unless context_properties.nil?
@@ -97,12 +107,12 @@ if options[:demo]
97
107
  end
98
108
  elsif options[:variant]
99
109
  variant = @unleash.get_variant(feature_name, unleash_context)
100
- puts " For feature \'#{feature_name}\' got variant \'#{variant}\'"
110
+ puts " For feature '#{feature_name}' got variant '#{variant}'"
101
111
  else
102
112
  if @unleash.is_enabled?(feature_name, unleash_context)
103
- puts " \'#{feature_name}\' is enabled according to unleash"
113
+ puts " '#{feature_name}' is enabled according to unleash"
104
114
  else
105
- puts " \'#{feature_name}\' is disabled according to unleash"
115
+ puts " '#{feature_name}' is disabled according to unleash"
106
116
  end
107
117
  end
108
118
 
data/examples/simple.rb CHANGED
@@ -7,6 +7,7 @@ puts ">> START simple.rb"
7
7
 
8
8
  # Unleash.configure do |config|
9
9
  # config.url = 'http://unleash.herokuapp.com/api'
10
+ # config.custom_http_headers = { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' }
10
11
  # config.app_name = 'simple-test'
11
12
  # config.refresh_interval = 2
12
13
  # config.metrics_interval = 2
@@ -17,13 +18,13 @@ puts ">> START simple.rb"
17
18
  # or:
18
19
 
19
20
  @unleash = Unleash::Client.new(
20
- url: 'https://app.unleash-hosted.com/demo/api',
21
+ url: 'http://unleash.herokuapp.com/api',
22
+ custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
21
23
  app_name: 'simple-test',
22
24
  instance_id: 'local-test-cli',
23
25
  refresh_interval: 2,
24
26
  metrics_interval: 2,
25
- retry_limit: 2,
26
- custom_http_headers: {'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0'},
27
+ retry_limit: 2
27
28
  )
28
29
 
29
30
  # feature_name = "AwesomeFeature"
@@ -28,9 +28,15 @@ module Unleash
28
28
  start_metrics unless Unleash.configuration.disable_metrics
29
29
  end
30
30
 
31
- def is_enabled?(feature, context = nil, default_value = false)
31
+ def is_enabled?(feature, context = nil, default_value_param = false, &fallback_blk)
32
32
  Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}"
33
33
 
34
+ default_value = if block_given?
35
+ default_value_param || !!fallback_blk.call(feature, context)
36
+ else
37
+ default_value_param
38
+ end
39
+
34
40
  if Unleash.configuration.disable_client
35
41
  Unleash.logger.warn "unleash_client is disabled! Always returning #{default_value} for feature #{feature}!"
36
42
  return default_value
@@ -56,19 +62,19 @@ module Unleash
56
62
  yield(blk) if is_enabled?(feature, context, default_value)
57
63
  end
58
64
 
59
- def get_variant(feature, context = nil, fallback_variant = nil)
65
+ def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant)
60
66
  Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"
61
67
 
62
68
  if Unleash.configuration.disable_client
63
69
  Unleash.logger.debug "unleash_client is disabled! Always returning #{fallback_variant} for feature #{feature}!"
64
- return fallback_variant || Unleash::FeatureToggle.disabled_variant
70
+ return fallback_variant
65
71
  end
66
72
 
67
73
  toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
68
74
 
69
75
  if toggle_as_hash.nil?
70
76
  Unleash.logger.debug "Unleash::Client.get_variant feature: #{feature} not found"
71
- return fallback_variant || Unleash::FeatureToggle.disabled_variant
77
+ return fallback_variant
72
78
  end
73
79
 
74
80
  toggle = Unleash::FeatureToggle.new(toggle_as_hash)
@@ -76,7 +82,7 @@ module Unleash
76
82
 
77
83
  if variant.nil?
78
84
  Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found"
79
- return fallback_variant || Unleash::FeatureToggle.disabled_variant
85
+ return fallback_variant
80
86
  end
81
87
 
82
88
  # TODO: Add to README: name, payload, enabled (bool)
@@ -88,7 +94,7 @@ module Unleash
88
94
  def shutdown
89
95
  unless Unleash.configuration.disable_client
90
96
  Unleash.toggle_fetcher.save!
91
- Unleash.reporter.send unless Unleash.configuration.disable_metrics
97
+ Unleash.reporter.post unless Unleash.configuration.disable_metrics
92
98
  shutdown!
93
99
  end
94
100
  end
@@ -135,7 +141,7 @@ module Unleash
135
141
  Unleash.configuration.retry_limit
136
142
  )
137
143
  self.metrics_scheduled_executor.run do
138
- Unleash.reporter.send
144
+ Unleash.reporter.post
139
145
  end
140
146
  end
141
147
 
@@ -144,12 +150,16 @@ module Unleash
144
150
 
145
151
  # Send the request, if possible
146
152
  begin
147
- response = Unleash::Util::Http.post(Unleash.configuration.client_register_url, info.to_json)
153
+ response = Unleash::Util::Http.post(Unleash.configuration.client_register_uri, info.to_json)
148
154
  rescue StandardError => e
149
155
  Unleash.logger.error "unable to register client with unleash server due to exception #{e.class}:'#{e}'."
150
156
  Unleash.logger.error "stacktrace: #{e.backtrace}"
151
157
  end
152
158
  Unleash.logger.debug "client registered: #{response}"
153
159
  end
160
+
161
+ def disabled_variant
162
+ @disabled_variant ||= Unleash::FeatureToggle.disabled_variant
163
+ end
154
164
  end
155
165
  end
@@ -8,6 +8,7 @@ module Unleash
8
8
  :app_name,
9
9
  :environment,
10
10
  :instance_id,
11
+ :project_name,
11
12
  :custom_http_headers,
12
13
  :disable_client,
13
14
  :disable_metrics,
@@ -41,7 +42,7 @@ module Unleash
41
42
  end
42
43
 
43
44
  def refresh_backup_file!
44
- self.backup_file = Dir.tmpdir + "/unleash-#{app_name}-repo.json" if self.backup_file.nil?
45
+ self.backup_file = File.join(Dir.tmpdir, "unleash-#{app_name}-repo.json")
45
46
  end
46
47
 
47
48
  def http_headers
@@ -51,16 +52,22 @@ module Unleash
51
52
  }.merge(custom_http_headers.dup)
52
53
  end
53
54
 
54
- def fetch_toggles_url
55
- self.url + '/client/features'
55
+ def fetch_toggles_uri
56
+ uri = URI("#{self.url_stripped_of_slash}/client/features")
57
+ uri.query = "project=#{self.project_name}" unless self.project_name.nil?
58
+ uri
56
59
  end
57
60
 
58
- def client_metrics_url
59
- self.url + '/client/metrics'
61
+ def client_metrics_uri
62
+ URI("#{self.url_stripped_of_slash}/client/metrics")
60
63
  end
61
64
 
62
- def client_register_url
63
- self.url + '/client/register'
65
+ def client_register_uri
66
+ URI("#{self.url_stripped_of_slash}/client/register")
67
+ end
68
+
69
+ def url_stripped_of_slash
70
+ self.url.delete_suffix '/'
64
71
  end
65
72
 
66
73
  private
@@ -76,6 +83,7 @@ module Unleash
76
83
  self.environment = 'default'
77
84
  self.url = nil
78
85
  self.instance_id = SecureRandom.uuid
86
+ self.project_name = nil
79
87
  self.disable_client = false
80
88
  self.disable_metrics = false
81
89
  self.refresh_interval = 10
@@ -28,7 +28,7 @@ module Unleash
28
28
  if ATTRS.include? normalized_name
29
29
  self.send(normalized_name)
30
30
  else
31
- self.properties.fetch(normalized_name)
31
+ self.properties.fetch(normalized_name, nil) || self.properties.fetch(name.to_sym)
32
32
  end
33
33
  end
34
34
 
@@ -32,23 +32,30 @@ module Unleash
32
32
  result
33
33
  end
34
34
 
35
- def get_variant(context, fallback_variant = disabled_variant)
35
+ def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant)
36
36
  raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant'
37
37
 
38
38
  context = ensure_valid_context(context)
39
39
 
40
- return disabled_variant unless self.enabled && am_enabled?(context, true)
41
- return disabled_variant if sum_variant_defs_weights <= 0
40
+ return Unleash::FeatureToggle.disabled_variant unless self.enabled && am_enabled?(context, true)
41
+ return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
42
42
 
43
- variant = variant_from_override_match(context)
44
- variant = variant_from_weights(context) if variant.nil?
43
+ variant = variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
45
44
 
46
45
  Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics
47
46
  variant
48
47
  end
49
48
 
49
+ def self.disabled_variant
50
+ Unleash::Variant.new(name: 'disabled', enabled: false)
51
+ end
52
+
50
53
  private
51
54
 
55
+ def resolve_stickiness
56
+ self.variant_definitions&.map(&:stickiness)&.compact&.first || "default"
57
+ end
58
+
52
59
  # only check if it is enabled, do not do metrics
53
60
  def am_enabled?(context, default_result)
54
61
  result =
@@ -77,15 +84,12 @@ module Unleash
77
84
  strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
78
85
  end
79
86
 
80
- def disabled_variant
81
- Unleash::Variant.new(name: 'disabled', enabled: false)
82
- end
83
-
84
87
  def sum_variant_defs_weights
85
88
  self.variant_definitions.map(&:weight).reduce(0, :+)
86
89
  end
87
90
 
88
- def variant_salt(context)
91
+ def variant_salt(context, stickiness = "default")
92
+ return context.get_by_name(stickiness) unless stickiness == "default"
89
93
  return context.user_id unless context.user_id.to_s.empty?
90
94
  return context.session_id unless context.session_id.to_s.empty?
91
95
  return context.remote_address unless context.remote_address.to_s.empty?
@@ -100,8 +104,8 @@ module Unleash
100
104
  Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload)
101
105
  end
102
106
 
103
- def variant_from_weights(context)
104
- variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context), self.name, sum_variant_defs_weights)
107
+ def variant_from_weights(context, stickiness)
108
+ variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context, stickiness), self.name, sum_variant_defs_weights)
105
109
  prev_weights = 0
106
110
 
107
111
  variant_definition = self.variant_definitions
@@ -110,7 +114,7 @@ module Unleash
110
114
  prev_weights += v.weight
111
115
  res
112
116
  end
113
- return disabled_variant if variant_definition.nil?
117
+ return self.disabled_variant if variant_definition.nil?
114
118
 
115
119
  Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
116
120
  end
@@ -150,6 +154,7 @@ module Unleash
150
154
  v.fetch('name', ''),
151
155
  v.fetch('weight', 0),
152
156
  v.fetch('payload', nil),
157
+ v.fetch('stickiness', nil),
153
158
  v.fetch('overrides', [])
154
159
  )
155
160
  end || []
@@ -6,6 +6,8 @@ require 'time'
6
6
 
7
7
  module Unleash
8
8
  class MetricsReporter
9
+ LONGEST_WITHOUT_A_REPORT = 600
10
+
9
11
  attr_accessor :last_time
10
12
 
11
13
  def initialize
@@ -33,16 +35,28 @@ module Unleash
33
35
  report
34
36
  end
35
37
 
36
- def send
37
- Unleash.logger.debug "send() Report"
38
+ def post
39
+ Unleash.logger.debug "post() Report"
40
+
41
+ if bucket_empty? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes...
42
+ Unleash.logger.debug "Report not posted to server, as it would have been empty. (and has been empty for up to 10 min)"
43
+
44
+ return
45
+ end
38
46
 
39
- response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_url, self.generate_report.to_json)
47
+ response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, self.generate_report.to_json)
40
48
 
41
49
  if ['200', '202'].include? response.code
42
- Unleash.logger.debug "Report sent to unleash server sucessfully. Server responded with http code #{response.code}"
50
+ Unleash.logger.debug "Report sent to unleash server successfully. Server responded with http code #{response.code}"
43
51
  else
44
52
  Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}."
45
53
  end
46
54
  end
55
+
56
+ private
57
+
58
+ def bucket_empty?
59
+ Unleash.toggle_metrics.features.empty?
60
+ end
47
61
  end
48
62
  end
@@ -4,6 +4,7 @@ module Unleash
4
4
  module Strategy
5
5
  class ApplicationHostname < Base
6
6
  attr_accessor :hostname
7
+
7
8
  PARAM = 'hostnames'.freeze
8
9
 
9
10
  def initialize
@@ -38,16 +38,16 @@ module Unleash
38
38
 
39
39
  def resolve_stickiness(stickiness, context)
40
40
  case stickiness
41
- when 'userId'
42
- context.user_id
43
- when 'sessionId'
44
- context.session_id
45
41
  when 'random'
46
42
  random
47
43
  when 'default'
48
44
  context.user_id || context.session_id || random
49
45
  else
50
- nil
46
+ begin
47
+ context.get_by_name(stickiness)
48
+ rescue KeyError
49
+ nil
50
+ end
51
51
  end
52
52
  end
53
53
  end
@@ -36,7 +36,7 @@ module Unleash
36
36
  # rename to refresh_from_server! ??
37
37
  def fetch
38
38
  Unleash.logger.debug "fetch()"
39
- response = Unleash::Util::Http.get(Unleash.configuration.fetch_toggles_url, etag)
39
+ response = Unleash::Util::Http.get(Unleash.configuration.fetch_toggles_uri, etag)
40
40
 
41
41
  if response.code == '304'
42
42
  Unleash.logger.debug "No changes according to the unleash server, nothing to do."
@@ -106,10 +106,11 @@ module Unleash
106
106
 
107
107
  def read!
108
108
  Unleash.logger.debug "read!()"
109
- return nil unless File.exist?(Unleash.configuration.backup_file)
109
+ backup_file = Unleash.configuration.backup_file
110
+ return nil unless File.exist?(backup_file)
110
111
 
111
112
  begin
112
- file = File.new(Unleash.configuration.backup_file, "r")
113
+ file = File.new(backup_file, "r")
113
114
  file_content = file.read
114
115
 
115
116
  backup_as_hash = JSON.parse(file_content)
@@ -4,8 +4,7 @@ require 'uri'
4
4
  module Unleash
5
5
  module Util
6
6
  module Http
7
- def self.get(url, etag = nil)
8
- uri = URI(url)
7
+ def self.get(uri, etag = nil)
9
8
  http = http_connection(uri)
10
9
 
11
10
  request = Net::HTTP::Get.new(uri.request_uri, http_headers(etag))
@@ -13,8 +12,7 @@ module Unleash
13
12
  http.request(request)
14
13
  end
15
14
 
16
- def self.post(url, body)
17
- uri = URI(url)
15
+ def self.post(uri, body)
18
16
  http = http_connection(uri)
19
17
 
20
18
  request = Net::HTTP::Post.new(uri.request_uri, http_headers)
@@ -2,13 +2,13 @@ require 'unleash/variant_override'
2
2
 
3
3
  module Unleash
4
4
  class VariantDefinition
5
- attr_accessor :name, :weight, :payload, :overrides
5
+ attr_accessor :name, :weight, :payload, :overrides, :stickiness
6
6
 
7
- def initialize(name, weight = 0, payload = nil, overrides = [])
7
+ def initialize(name, weight = 0, payload = nil, stickiness = nil, overrides = [])
8
8
  self.name = name
9
9
  self.weight = weight
10
10
  self.payload = payload
11
- # self.overrides = overrides
11
+ self.stickiness = stickiness
12
12
  self.overrides = (overrides || [])
13
13
  .select{ |v| v.is_a?(Hash) && v.has_key?('contextName') }
14
14
  .map{ |v| VariantOverride.new(v.fetch('contextName', ''), v.fetch('values', [])) } || []
@@ -19,7 +19,8 @@ module Unleash
19
19
  end
20
20
 
21
21
  def to_s
22
- "<VariantDefinition: name=#{self.name},weight=#{self.weight},payload=#{self.payload},overrides=#{self.overrides}>"
22
+ "<VariantDefinition: name=#{self.name},weight=#{self.weight},payload=#{self.payload},stickiness=#{self.stickiness}" \
23
+ ",overrides=#{self.overrides}>"
23
24
  end
24
25
  end
25
26
  end
@@ -1,3 +1,3 @@
1
1
  module Unleash
2
- VERSION = "3.2.5".freeze
2
+ VERSION = "4.0.0".freeze
3
3
  end
data/lib/unleash.rb CHANGED
@@ -24,11 +24,6 @@ module Unleash
24
24
  attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :logger
25
25
  end
26
26
 
27
- def self.initialize
28
- self.toggles = []
29
- self.toggle_metrics = {}
30
- end
31
-
32
27
  # Support for configuration via yield:
33
28
  def self.configure
34
29
  self.configuration ||= Unleash::Configuration.new
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "rspec-json_expectations", "~> 2.2"
32
32
  spec.add_development_dependency "webmock", "~> 3.8"
33
33
 
34
- spec.add_development_dependency "coveralls", "~> 0.8"
35
34
  spec.add_development_dependency "rubocop", "~> 0.80"
35
+ spec.add_development_dependency "simplecov", "~> 0.21.2"
36
+ spec.add_development_dependency "simplecov-lcov", "~> 0.8.0"
36
37
  end
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: 3.2.5
4
+ version: 4.0.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: 2021-10-01 00:00:00.000000000 Z
11
+ date: 2021-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: murmurhash3
@@ -95,33 +95,47 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.8'
97
97
  - !ruby/object:Gem::Dependency
98
- name: coveralls
98
+ name: rubocop
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0.8'
103
+ version: '0.80'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0.8'
110
+ version: '0.80'
111
111
  - !ruby/object:Gem::Dependency
112
- name: rubocop
112
+ name: simplecov
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '0.80'
117
+ version: 0.21.2
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '0.80'
124
+ version: 0.21.2
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov-lcov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.8.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.8.0
125
139
  description: |-
126
140
  This is the ruby client for Unleash, a powerful feature toggle system
127
141
  that gives you a great overview over all feature toggles across all your applications and services.
@@ -132,10 +146,10 @@ executables:
132
146
  extensions: []
133
147
  extra_rdoc_files: []
134
148
  files:
149
+ - ".github/workflows/pull_request.yml"
135
150
  - ".gitignore"
136
151
  - ".rspec"
137
152
  - ".rubocop.yml"
138
- - ".travis.yml"
139
153
  - Gemfile
140
154
  - LICENSE
141
155
  - README.md
data/.travis.yml DELETED
@@ -1,15 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - jruby
5
- - 3.0
6
- - 2.7
7
- - 2.6
8
- - 2.5
9
- before_install:
10
- - gem install bundler -v 2.1.4
11
- - git clone --depth 5 --branch v3.3.0 https://github.com/Unleash/client-specification.git client-specification
12
-
13
- notifications:
14
- slack:
15
- secure: x593zOjdl2yVB8uP54v8CmuCOat8GFHnK99NPvPHKvif5U7PGe0YOgYh4DC1+Jc9vfjn1ke+0++m+Gif4quowpeOaA/t45xpB494lyziXsBulYml245jRp9yzoUmIIt7KxHhv4rlo3Q1ztMJgh6a5yDCornKHW2bKTkLsvqVTwxBRatLOrt6K9O8FivO/NaqgcoXl7Rw0fOx/bsZtx2IAFueTCH19NoqW1mk9KFEZ96YqJSvuqmfDC0AO7siq03WKlB++nPlKe1QcrlPalCrcsSzrYNhYJ3akBTt/ZbE1v6YJv2L+zUqRnAPTY2H+qp8WejFQtdhIjfeJ/SWox0iWv/Wy/mTFfj+EhFO9Aq+xhMjJ1OOLtNAPoYJyatEVgJkILb6M26igTFcuI60xBbGNmh5ZYeyRdn5/xFb7G2zyJ2Swc3PvN1uLzMHfTF0R7WzGq4CRNGIOjrHTGncyB3IGAONOdJdM3iT9XKY6cdlRK0VkQjEsEMe0eNv2fxxLVSGna4sdJoTND6LhJ6qCfuS9DEDXwoRdLxAXxefycCh9VNp7gloMJx8IbHYxOW0BFZqc3hxNU9X2SwOj6j72DZMrdYDg2aPAW69HG0iMontQ37Di87JEW2F2Cpgb49+4twByrQNIx+st+DGNce1vpc0DN+KuJVdIcmha654lT7Ffe8=