unleash 3.2.5 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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=