New Upstream Release - ruby-unleash

Ready changes

Summary

Merged new upstream version: 4.4.2 (was: 4.4.1).

Resulting package

Built on 2023-05-22T11:32 (took 4m52s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases ruby-unleash

Lintian Result

Diff

diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 0000000..0d0b1c9
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1 @@
+_extends: .github
diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml
new file mode 100644
index 0000000..ce57699
--- /dev/null
+++ b/.github/workflows/add-to-project.yml
@@ -0,0 +1,14 @@
+name: Add new item to project board
+
+on:
+  issues:
+    types:
+      - opened
+  pull_request_target:
+    types:
+      - opened
+
+jobs:
+  add-to-project:
+      uses: unleash/.github/.github/workflows/add-item-to-project.yml@main
+      secrets: inherit
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
new file mode 100644
index 0000000..1790cac
--- /dev/null
+++ b/.github/workflows/pull_request.yml
@@ -0,0 +1,81 @@
+name: CI
+
+on:
+  push:
+  pull_request:
+
+jobs:
+  lint:
+    name: RuboCop
+    timeout-minutes: 30
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Ruby
+      uses: ruby/setup-ruby@v1
+      with:
+        ruby-version: "2.7"
+        bundler-cache: true
+    - name: Run RuboCop
+      run: bundle exec rubocop
+
+  test:
+    runs-on: ${{ matrix.os }}-latest
+
+    strategy:
+      matrix:
+        os:
+          - ubuntu
+          - macos
+        ruby-version:
+          - jruby-9.4
+          - jruby-9.3
+          - jruby-9.2
+          - 3.2
+          - 3.1
+          - '3.0'
+          - 2.7
+          - 2.6
+          - 2.5
+
+    steps:
+      - uses: actions/checkout@v3
+      - name: Set up Ruby ${{ matrix.ruby-version }}
+        uses: ruby/setup-ruby@v1
+        with:
+          bundler-cache: true
+          ruby-version: ${{ matrix.ruby-version }}
+      - name: Install dependencies
+        run: bundle install
+      - name: Download test cases
+        run: git clone --depth 5 --branch v4.2.2 https://github.com/Unleash/client-specification.git client-specification
+      - name: Run tests
+        run: bundle exec rake
+        env:
+          COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      - name: Coveralls Parallel
+        uses: coverallsapp/github-action@master
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          flag-name: run-${{ matrix.test_number }}
+          parallel: true
+      - name: Notify Slack of pipeline completion
+        uses: 8398a7/action-slack@v3
+        if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}
+        with:
+          status: ${{ job.status }}
+          text: Built on ${{ matrix.os }} - Ruby ${{ matrix.ruby-version }}
+          fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
+        env:
+          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+  finish:
+    needs: test
+    runs-on: ubuntu-latest
+    steps:
+      - name: Coveralls Finished
+        uses: coverallsapp/github-action@master
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          parallel-finished: true
+
diff --git a/.gitignore b/.gitignore
index 15d38a6..da21d9a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
 /.bundle/
 /.yardoc
-/Gemfile.lock
 /_yardoc/
 /coverage/
 /doc/
@@ -9,8 +8,12 @@
 /tmp/
 /vendor
 
+# IntelliJ
+.idea/
+
 # rspec failure tracking
 .rspec_status
+Gemfile.lock
 
 # Clone of the client-specification
 /client-specification/
diff --git a/.rspec b/.rspec
index 8c18f1a..e7fe05c 100644
--- a/.rspec
+++ b/.rspec
@@ -1,2 +1,3 @@
 --format documentation
 --color
+--require 'spec_helper'
diff --git a/.rubocop.yml b/.rubocop.yml
index a1eb64f..7469dd6 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -6,25 +6,26 @@ AllCops:
 Naming/PredicateName:
   AllowedMethods:
     - is_enabled?
+    - is_disabled?
+
 
 Metrics/ClassLength:
-  Max: 120
+  Max: 135
 Layout/LineLength:
   Max: 140
 Metrics/MethodLength:
   Max: 20
 Metrics/BlockLength:
-  Max: 100
+  Max: 110
   Exclude:
-    - 'spec/unleash/client_spec.rb'
-    - 'spec/unleash/feature_toggle_spec.rb'
+    - 'spec/**/*.rb'
 
 Metrics/AbcSize:
-  Max: 25
+  Max: 30
 Metrics/CyclomaticComplexity:
-  Max: 9
+  Max: 10
 Metrics/PerceivedComplexity:
-  Max: 9
+  Max: 10
 
 Style/Documentation:
   Enabled: false
@@ -34,6 +35,9 @@ Style/StringLiterals:
 Style/RedundantSelf:
   Enabled: false
 
+Style/OptionalBooleanParameter:
+  Enabled: false
+
 Style/SymbolArray:
   EnforcedStyle: brackets
 Style/WordArray:
@@ -51,6 +55,12 @@ Style/HashTransformKeys:
   Enabled: true
 Style/HashTransformValues:
   Enabled: true
+Style/EmptyElse:
+  Exclude:
+    - 'lib/unleash/strategy/flexible_rollout.rb'
+
+Style/DoubleNegation:
+  Enabled: false
 
 Style/IfInsideElse:
   Exclude:
@@ -60,6 +70,61 @@ Style/Next:
   Exclude:
     - 'lib/unleash/scheduled_executor.rb'
 
+
+Style/AccessorGrouping:
+  Enabled: true
+Style/BisectedAttrAccessor:
+  Enabled: true
+Style/CaseLikeIf:
+  Enabled: true
+#Style/ClassEqualityComparison:
+#  Enabled: true
+Style/CombinableLoops:
+  Enabled: true
+Style/ExplicitBlockArgument:
+  Enabled: true
+Style/ExponentialNotation:
+  Enabled: true
+#Style/GlobalStdStream:
+#  Enabled: true
+Style/HashAsLastArrayItem:
+  Enabled: true
+Style/HashLikeCase:
+  Enabled: true
+Style/KeywordParametersOrder:
+  Enabled: true
+#Style/OptionalBooleanParameter:
+#  Enabled: false
+Style/RedundantAssignment:
+  Enabled: true
+Style/RedundantFetchBlock:
+  Enabled: true
+Style/RedundantFileExtensionInRequire:
+  Enabled: true
+Style/RedundantRegexpCharacterClass:
+  Enabled: true
+Style/RedundantRegexpEscape:
+  Enabled: true
+Style/RedundantSelfAssignment:
+  Enabled: true
+Style/SingleArgumentDig:
+  Enabled: true
+Style/SlicingWithRange:
+  Enabled: true
+Style/SoleNestedConditional:
+  Enabled: true
+Style/StringConcatenation:
+  Enabled: false
+Style/TrailingCommaInHashLiteral:
+  Enabled: true
+#  EnforcedStyleForMultiline: consistent_comma
+
+Layout/BeginEndAlignment:
+  Enabled: true
+Layout/EmptyLinesAroundAttributeAccessor:
+  Enabled: true
+Layout/SpaceAroundMethodCallOperator:
+  Enabled: true
 Layout/MultilineMethodCallIndentation:
   EnforcedStyle: indented
 
@@ -68,3 +133,50 @@ Layout/SpaceBeforeBlockBraces:
   Exclude:
     - 'unleash-client.gemspec'
     - 'spec/**/*.rb'
+
+Lint/BinaryOperatorWithIdenticalOperands:
+  Enabled: true
+Lint/ConstantDefinitionInBlock:
+  Enabled: false
+Lint/DeprecatedOpenSSLConstant:
+  Enabled: true
+Lint/DuplicateElsifCondition:
+  Enabled: true
+Lint/DuplicateRequire:
+  Enabled: true
+Lint/DuplicateRescueException:
+  Enabled: true
+Lint/EmptyConditionalBody:
+  Enabled: true
+Lint/EmptyFile:
+  Enabled: true
+Lint/FloatComparison:
+  Enabled: true
+Lint/HashCompareByIdentity:
+  Enabled: true
+Lint/IdentityComparison:
+  Enabled: true
+Lint/MissingSuper:
+  Enabled: false
+Lint/MixedRegexpCaptureTypes:
+  Enabled: true
+Lint/OutOfRangeRegexpRef:
+  Enabled: true
+Lint/RaiseException:
+  Enabled: true
+Lint/RedundantSafeNavigation:
+  Enabled: true
+Lint/SelfAssignment:
+  Enabled: true
+Lint/StructNewOverride:
+  Enabled: true
+Lint/TopLevelReturnWithArgument:
+  Enabled: true
+Lint/TrailingCommaInAttributeDeclaration:
+  Enabled: true
+Lint/UnreachableLoop:
+  Enabled: true
+Lint/UselessMethodDefinition:
+  Enabled: true
+Lint/UselessTimes:
+  Enabled: true
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index f3c594b..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-sudo: false
-language: ruby
-rvm:
-- jruby
-- 3.0
-- 2.7
-- 2.6
-- 2.5
-before_install:
-- gem install bundler -v 2.1.4
-- git clone --depth 5 --branch v3.3.0 https://github.com/Unleash/client-specification.git client-specification
-
-notifications:
-  slack:
-    secure: x593zOjdl2yVB8uP54v8CmuCOat8GFHnK99NPvPHKvif5U7PGe0YOgYh4DC1+Jc9vfjn1ke+0++m+Gif4quowpeOaA/t45xpB494lyziXsBulYml245jRp9yzoUmIIt7KxHhv4rlo3Q1ztMJgh6a5yDCornKHW2bKTkLsvqVTwxBRatLOrt6K9O8FivO/NaqgcoXl7Rw0fOx/bsZtx2IAFueTCH19NoqW1mk9KFEZ96YqJSvuqmfDC0AO7siq03WKlB++nPlKe1QcrlPalCrcsSzrYNhYJ3akBTt/ZbE1v6YJv2L+zUqRnAPTY2H+qp8WejFQtdhIjfeJ/SWox0iWv/Wy/mTFfj+EhFO9Aq+xhMjJ1OOLtNAPoYJyatEVgJkILb6M26igTFcuI60xBbGNmh5ZYeyRdn5/xFb7G2zyJ2Swc3PvN1uLzMHfTF0R7WzGq4CRNGIOjrHTGncyB3IGAONOdJdM3iT9XKY6cdlRK0VkQjEsEMe0eNv2fxxLVSGna4sdJoTND6LhJ6qCfuS9DEDXwoRdLxAXxefycCh9VNp7gloMJx8IbHYxOW0BFZqc3hxNU9X2SwOj6j72DZMrdYDg2aPAW69HG0iMontQ37Di87JEW2F2Cpgb49+4twByrQNIx+st+DGNce1vpc0DN+KuJVdIcmha654lT7Ffe8=
diff --git a/README.md b/README.md
index aff2999..2add3ac 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # Unleash::Client
 
-[![Build Status](https://travis-ci.org/Unleash/unleash-client-ruby.svg?branch=master)](https://travis-ci.org/Unleash/unleash-client-ruby)
-[![Coverage Status](https://coveralls.io/repos/github/Unleash/unleash-client-ruby/badge.svg?branch=master)](https://coveralls.io/github/Unleash/unleash-client-ruby?branch=master)
+![Build Status](https://github.com/Unleash/unleash-client-ruby/actions/workflows/pull_request.yml/badge.svg?branch=main)
+[![Coverage Status](https://coveralls.io/repos/github/Unleash/unleash-client-ruby/badge.svg?branch=main)](https://coveralls.io/github/Unleash/unleash-client-ruby?branch=main)
 [![Gem Version](https://badge.fury.io/rb/unleash.svg)](https://badge.fury.io/rb/unleash)
 
 Unleash client so you can roll out your features with confidence.
@@ -10,18 +10,22 @@ Leverage the [Unleash Server](https://github.com/Unleash/unleash) for powerful f
 
 ## Supported Ruby Interpreters
 
+  * MRI 3.2
+  * MRI 3.1
   * MRI 3.0
   * MRI 2.7
   * MRI 2.6
   * MRI 2.5
-  * jruby
+  * jruby 9.4
+  * jruby 9.3
+  * jruby 9.2
 
 ## Installation
 
 Add this line to your application's Gemfile:
 
 ```ruby
-gem 'unleash', '~> 3.2.5'
+gem 'unleash', '~> 4.4.0'
 ```
 
 And then execute:
@@ -34,22 +38,45 @@ Or install it yourself as:
 
 ## Configure
 
-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.
+It is **required** to configure:
+- `url` of the unleash server
+- `app_name` with the name of the runninng application.
+- `custom_http_headers` with `{'Authorization': '<API token>'}` when using Unleash v4.0.0 and later.
 
-It is **highly recommended** to configure the `instance_id` parameter as well.
+Please substitute the example `'https://unleash.herokuapp.com/api'` for the url of your own instance.
+
+It is **highly recommended** to configure:
+- `instance_id` parameter with a unique identifier for the running instance.
 
 
 ```ruby
 Unleash.configure do |config|
-  config.url         = 'http://unleash.herokuapp.com/api'
-  config.app_name    = 'my_ruby_app'
+  config.app_name            = 'my_ruby_app'
+  config.url                 = 'https://unleash.herokuapp.com/api'
+  config.custom_http_headers = {'Authorization': '<API token>'}
 end
 ```
 
 or instantiate the client with the valid configuration:
 
 ```ruby
-UNLEASH = Unleash::Client.new(url: 'http://unleash.herokuapp.com/api', app_name: 'my_ruby_app')
+UNLEASH = Unleash::Client.new(url: 'https://unleash.herokuapp.com/api', app_name: 'my_ruby_app', custom_http_headers: {'Authorization': '<API token>'})
+```
+
+## Dynamic custom HTTP headers
+If you need custom HTTP headers that change during the lifetime of the client, the `custom_http_headers` can be given as a `Proc`.
+
+```ruby
+Unleash.configure do |config|
+  config.app_name            = 'my_ruby_app'
+  config.url                 = 'https://unleash.herokuapp.com/api'
+  config.custom_http_headers =  proc do
+    {
+      'Authorization': '<API token>',
+      'X-Client-Request-Time': Time.now.iso8601
+    }
+  end
+end
 ```
 
 #### List of Arguments
@@ -60,19 +87,26 @@ Argument | Description | Required? |  Type |  Default Value|
 `app_name` | Name of your program. | Y | String | N/A |
 `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 |
 `environment` | Environment the program is running on. Could be for example `prod` or `dev`. Not yet in use. | N | String | `default` |
+`project_name` | Name of the project to retrieve features from. If not set, all feature flags will be retrieved. | N | String | nil |
 `refresh_interval` | How often the unleash client should check with the server for configuration changes. | N | Integer |  15 |
-`metrics_interval` | How often the unleash client should send metrics to server. | N | Integer | 10 |
+`metrics_interval` | How often the unleash client should send metrics to server. | N | Integer | 60 |
 `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` |
 `disable_metrics` | Disables sending metrics to Unleash server. | N | Boolean | `false` |
-`custom_http_headers` | Custom headers to send to Unleash. | N | Hash | {} |
+`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/Proc | {} |
 `timeout` | How long to wait for the connection to be established or wait in reading state (open_timeout/read_timeout) | N | Integer | 30 |
-`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 |
+`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 |
 `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` |
 `logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` |
-`log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::ERROR` |
+`log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::WARN` |
+`bootstrap_config` | Bootstrap config on how to loaded data on start-up. This is useful for loading large states on startup without (or before) hitting the network. | N | Unleash::Bootstrap::Configuration | `nil` |
+`strategies` | Strategies manager that holds all strategies and allows to add custom strategies | N | Unleash::Strategies | `Unleash::Strategies.new` |
 
-For in a more in depth look, please see `lib/unleash/configuration.rb`.
+For a more in-depth look, please see `lib/unleash/configuration.rb`.
 
+Environment Variable | Description
+---------|---------
+`UNLEASH_BOOTSTRAP_FILE` | File to read bootstrap data from
+`UNLEASH_BOOTSTRAP_URL` | URL to read bootstrap data from
 
 ## Usage in a plain Ruby Application
 
@@ -80,7 +114,7 @@ For in a more in depth look, please see `lib/unleash/configuration.rb`.
 require 'unleash'
 require 'unleash/context'
 
-@unleash = Unleash::Client.new(url: 'http://unleash.herokuapp.com/api', app_name: 'my_ruby_app')
+@unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: 'https://unleash.herokuapp.com/api', custom_http_headers: { 'Authorization': '<API token>' })
 
 feature_name = "AwesomeFeature"
 unleash_context = Unleash::Context.new
@@ -91,6 +125,12 @@ if @unleash.is_enabled?(feature_name, unleash_context)
 else
   puts " #{feature_name} is disabled according to unleash"
 end
+
+if @unleash.is_disabled?(feature_name, unleash_context)
+  puts " #{feature_name} is disabled according to unleash"
+else
+  puts " #{feature_name} is enabled according to unleash"
+end
 ```
 
 ## Usage in a Rails Application
@@ -101,37 +141,145 @@ Put in `config/initializers/unleash.rb`:
 
 ```ruby
 Unleash.configure do |config|
-  config.url      = 'http://unleash.herokuapp.com/api'
   config.app_name = Rails.application.class.parent.to_s
+  config.url      = 'https://unleash.herokuapp.com/api'
   # config.instance_id = "#{Socket.gethostname}"
   config.logger   = Rails.logger
   config.environment = Rails.env
 end
 
 UNLEASH = Unleash::Client.new
+
+# Or if preferred:
+# Rails.configuration.unleash = Unleash::Client.new
 ```
-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.
+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.
 
+To have it available in the `rails console` command as well, also add to the file above:
+```ruby
+Rails.application.console do
+  UNLEASH = Unleash::Client.new
+  # or
+  # Rails.configuration.unleash = Unleash::Client.new
+end
+```
+
+#### Add Initializer if using [Puma in clustered mode](https://github.com/puma/puma#clustered-mode)
+
+That is, multiple workers configured in `puma.rb`:
+```ruby
+workers ENV.fetch("WEB_CONCURRENCY") { 2 }
+```
 
-#### Add Initializer if using [Puma](https://github.com/puma/puma)
+##### with `preload_app!`
 
-In `puma.rb` ensure that the unleash client is configured and instantiated as below, inside the `on_worker_boot` code block:
+Then you may keep the client configuration still in `config/initializers/unleash.rb`:
+```ruby
+Unleash.configure do |config|
+  config.app_name    = Rails.application.class.parent.to_s
+  config.environment = Rails.env
+  config.url                 = 'https://unleash.herokuapp.com/api'
+  config.custom_http_headers = {'Authorization': '<API token>'}
+end
+```
+
+But you must ensure that the unleash client is instantiated only after the process is forked.
+This is done by creating the client inside the `on_worker_boot` code block in `puma.rb` as below:
 
 ```ruby
+#...
+preload_app!
+#...
+
 on_worker_boot do
   # ...
 
-  Unleash.configure do |config|
-    config.url      = 'http://unleash.herokuapp.com/api'
-    config.app_name = Rails.application.class.parent.to_s
-    config.environment = Rails.env
+  ::UNLEASH = Unleash::Client.new
+end
+
+on_worker_shutdown do
+  ::UNLEASH.shutdown
+end
+```
+
+##### without `preload_app!`
+
+By not using `preload_app!`:
+- the `Rails` constant will NOT be available.
+- but phased restarts will be possible.
+
+You need to ensure that in `puma.rb`:
+- loading unleash sdk with `require 'unleash'` explicitly, as it will not be pre-loaded.
+- all parameters must be explicitly set in the `on_worker_boot` block, as `config/initializers/unleash.rb` is not read.
+- there are no references to `Rails` constant, as that is not yet available.
+
+Example for `puma.rb`:
+```ruby
+require 'unleash'
+
+#...
+# no preload_app!
+
+on_worker_boot do
+  # ...
+
+  ::UNLEASH = Unleash::Client.new(
+    app_name: 'my_rails_app',
+    environment: 'development',
+    url: 'https://unleash.herokuapp.com/api',
+    custom_http_headers: {'Authorization': '<API token>'},
+  )
+end
+
+on_worker_shutdown do
+  ::UNLEASH.shutdown
+end
+```
+
+Note that we also added shutdown hooks in `on_worker_shutdown`, to ensure a clean shutdown.
+
+#### Add Initializer if using [Phusion Passenger](https://github.com/phusion/passenger)
+
+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):
+
+The initializer in `config/initializers/unleash.rb` should look like:
+
+```ruby
+PhusionPassenger.on_event(:starting_worker_process) do |forked|
+  if forked
+    Unleash.configure do |config|
+      config.app_name    = Rails.application.class.parent.to_s
+      # config.instance_id = "#{Socket.gethostname}"
+      config.logger      = Rails.logger
+      config.environment = Rails.env
+      config.url                 = 'https://unleash.herokuapp.com/api'
+      config.custom_http_headers = {'Authorization': '<API token>'}
+    end
+
+    UNLEASH = Unleash::Client.new
   end
-  Rails.configuration.unleash = Unleash::Client.new
 end
 ```
 
-Instead of the configuration in `config/initializers/unleash.rb`.
+#### Add Initializer hooks when using within [Sidekiq](https://github.com/mperham/sidekiq)
 
+Note that in this case we require that the code block for `Unleash.configure` is set beforehand.
+For example in `config/initializers/unleash.rb`.
+
+```ruby
+Sidekiq.configure_server do |config|
+  config.on(:startup) do
+    UNLEASH = Unleash::Client.new
+  end
+
+  config.on(:shutdown) do
+    UNLEASH.shutdown
+  end
+end
+```
 
 #### Set Unleash::Context
 
@@ -162,6 +310,9 @@ Then wherever in your application that you need a feature toggle, you can use:
 if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context
   puts "AwesomeFeature is enabled"
 end
+if UNLEASH.is_disabled? "AwesomeFeature", @unleash_context
+  puts "AwesomeFeature is disabled"
+end
 ```
 
 or if client is set in `Rails.configuration.unleash`:
@@ -170,9 +321,13 @@ or if client is set in `Rails.configuration.unleash`:
 if Rails.configuration.unleash.is_enabled? "AwesomeFeature", @unleash_context
   puts "AwesomeFeature is enabled"
 end
+if Rails.configuration.unleash.is_disabled? "AwesomeFeature", @unleash_context
+  puts "AwesomeFeature is enabled"
+end
 ```
 
-If the feature is not found in the server, it will by default return false. However you can override that by setting the default return value to `true`:
+If the feature is not found in the server, it will by default return false.
+However you can override that by setting the default return value to `true`:
 
 ```ruby
 if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context, true
@@ -180,6 +335,40 @@ if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context, true
 end
 ```
 
+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)
+to evaluate the default value:
+
+```ruby
+net_check_proc = proc do |feature_name, context|
+  context.remote_address.starts_with?("10.0.0.")
+end
+
+if UNLEASH.is_enabled?("AwesomeFeature", @unleash_context, &net_check_proc)
+  puts "AwesomeFeature is enabled by default if you are in the 10.0.0.* network."
+end
+```
+
+or
+
+```ruby
+awesomeness = 10
+@unleash_context.properties[:coolness] = 10
+
+if UNLEASH.is_enabled?("AwesomeFeature", @unleash_context) { |feat, ctx| awesomeness >= 6 && ctx.properties[:coolness] >= 8 }
+  puts "AwesomeFeature is enabled by default if both the user has a high enought coolness and the application has a high enough awesomeness"
+end
+```
+
+Note:
+- The block/lambda/proc can use feature name and context as an arguments.
+- The client will evaluate the fallback function once per call of `is_enabled()`.
+  Please keep this in mind when creating your fallback function!
+- The returned value of the block should be a boolean.
+  However the client will coerce the result to boolean via `!!`.
+- If both a `default_value` and `fallback_function` are supplied,
+  the client will define the default value by `OR`ing the default value and the output of the fallback function.
+
+
 Alternatively by using `if_enabled` you can send a code block to be executed as a parameter:
 
 ```ruby
@@ -188,6 +377,8 @@ UNLEASH.if_enabled "AwesomeFeature", @unleash_context, true do
 end
 ```
 
+Note: `if_enabled` only supports `default_value`, but not `fallback_function`.
+
 ##### Variations
 
 If no variant is found in the server, use the fallback variant.
@@ -199,6 +390,65 @@ variant = UNLEASH.get_variant "ColorVariants", @unleash_context, fallback_varian
 puts "variant color is: #{variant.payload.fetch('color')}"
 ```
 
+## Bootstrapping
+
+Bootstrap configuration allows the client to be initialized with a predefined set of toggle states.
+Bootstrapping can be configured by providing a bootstrap configuration when initializing the client.
+```ruby
+@unleash = Unleash::Client.new(
+    url: 'https://unleash.herokuapp.com/api',
+    app_name: 'my_ruby_app',
+    custom_http_headers: { 'Authorization': '<API token>' },
+    bootstrap_config: Unleash::Bootstrap::Configuration.new({
+        url: "https://unleash.herokuapp.com/api/client/features",
+        url_headers: {'Authorization': '<API token>'}
+    })
+)
+```
+The `Bootstrap::Configuration` initializer takes a hash with one of the following options specified:
+
+* `file_path` - An absolute or relative path to a file containing a JSON string of the response body from the Unleash server. This can also be set though the `UNLEASH_BOOTSTRAP_FILE` environment variable.
+* `url` - A url pointing to an Unleash server's features endpoint, the code sample above is illustrative. This can also be set though the `UNLEASH_BOOTSTRAP_URL` environment variable.
+* `url_headers` - Headers for the GET http request to the `url` above. Only used if the `url` parameter is also set. If this option isn't set then the bootstrapper will use the same url headers as the Unleash client.
+* `data` - A raw JSON string as returned by the Unleash server.
+* `block` - A lambda containing custom logic if you need it, an example is provided below.
+
+You should only specify one type of bootstrapping since only one will be invoked and the others will be ignored.
+The order of preference is as follows:
+
+- Select a data bootstrapper if it exists.
+- If no data bootstrapper exists, select the block bootstrapper.
+- If no block bootstrapper exists, select the file bootstrapper from either parameters or the specified environment variable.
+- If no file bootstrapper exists, then check for a URL bootstrapper from either the parameters or the specified environment variable.
+
+
+Example usage:
+
+First saving the toggles locally:
+```shell
+curl -H 'Authorization: <API token>' -XGET 'https://unleash.herokuapp.com/api' > ./default-toggles.json
+```
+
+Now using them on start up:
+
+```ruby
+
+custom_boostrapper = lambda {
+  File.read('./default-toggles.json')
+}
+
+@unleash = Unleash::Client.new(
+    app_name: 'my_ruby_app',
+    url: 'https://unleash.herokuapp.com/api',
+    custom_http_headers: { 'Authorization': '<API token>' },
+    bootstrap_config: Unleash::Bootstrap::Configuration.new({
+        block: custom_boostrapper
+    })
+)
+```
+
+This example could be easily achieved with a file bootstrapper, this is just to illustrate the usage of custom bootstrapping.
+Be aware that the client initializer will block until bootstrapping is complete.
 
 #### Client methods
 
@@ -207,10 +457,14 @@ Method Name | Description | Return Type |
 `is_enabled?` | Check if feature toggle is to be enabled or not. | Boolean |
 `enabled?` | Alias to the `is_enabled?` method. But more ruby idiomatic. | Boolean |
 `if_enabled` | Run a code block, if a feature is enabled. | `yield` |
+`is_disabled?` | Check if feature toggle is to be enabled or not. | Boolean |
+`disabled?` | Alias to the `is_disabled?` method. But more ruby idiomatic. | Boolean |
+`if_disabled` | Run a code block, if a feature is disabled. | `yield` |
 `get_variant` | Get variant for a given feature | `Unleash::Variant` |
 `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 |
 `shutdown!` | Kill ToggleFetcher and MetricsReporter threads immediately. | nil |
 
+For the full method signatures, please see [client.rb](lib/unleash/client.rb)
 
 ## Local test client
 
@@ -228,6 +482,7 @@ This client comes with the all the required strategies out of the box:
 
  * ApplicationHostnameStrategy
  * DefaultStrategy
+ * FlexibleRolloutStrategy
  * GradualRolloutRandomStrategy
  * GradualRolloutSessionIdStrategy
  * GradualRolloutUserIdStrategy
@@ -235,11 +490,37 @@ This client comes with the all the required strategies out of the box:
  * UnknownStrategy
  * UserWithIdStrategy
 
+## Custom Strategies
+
+Client allows to add [custom activation strategies](https://docs.getunleash.io/advanced/custom_activation_strategy) using configuration.
+In order for strategy to work correctly it should support two methods `name` and `is_enabled?`
+
+```ruby
+class MyCustomStrategy
+  def name
+    'muCustomStrategy'
+  end
+
+  def is_enabled?(params = {}, context = nil)
+    true
+  end
+end
+
+Unleash.configure do |config|
+  config.strategies.add(MyCustomStrategy.new)
+end
+```
 
 ## Development
 
 After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
 
+This SDK is also built against the Unleash Client Specification tests. To run the Ruby SDK against this test suite, you'll need to have a copy on your machine, you can clone the repository directly using:
+
+`git clone --depth 5 --branch v4.2.2 https://github.com/Unleash/client-specification.git client-specification`
+
+After doing this, `rake spec` will also run the client specification tests.
+
 To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
 
 
diff --git a/bin/unleash-client b/bin/unleash-client
index 613f63b..cda27fe 100755
--- a/bin/unleash-client
+++ b/bin/unleash-client
@@ -12,11 +12,13 @@ options = {
   url: 'http://localhost:4242',
   demo: false,
   disable_metrics: true,
+  custom_http_headers: {},
   sleep: 0.1
 }
 
 OptionParser.new do |opts|
-  opts.banner = "Usage: #{__FILE__} [options] feature [key1=val1] [key2=val2]"
+  opts.banner = "Usage: #{__FILE__} [options] feature [contextKey1=val1] [contextKey2=val2] \n\n" \
+  "Where contextKey1 could be user_id, session_id, remote_address or any field in the Context class (or any property within it).\n"
 
   opts.on("-V", "--variant", "Fetch variant for feature") do |v|
     options[:variant] = v
@@ -46,6 +48,13 @@ OptionParser.new do |opts|
     options[:sleep] = s
   end
 
+  opts.on("-H", "--http-headers='Authorization: *:developement.secretstring'",
+          "Adds http headers to all requests on the unleash server. Use multiple times for multiple headers.") do |h|
+    http_header_as_hash = [h].to_h{ |l| l.split(": ") }.transform_keys(&:to_sym)
+
+    options[:custom_http_headers].merge!(http_header_as_hash)
+  end
+
   opts.on("-h", "--help", "Prints this help") do
     puts opts
     exit
@@ -70,10 +79,11 @@ log_level = \
   url: options[:url],
   app_name: 'unleash-client-ruby-cli',
   disable_metrics: options[:metrics],
+  custom_http_headers: options[:custom_http_headers],
   log_level: log_level
 )
 
-context_params = ARGV.map{ |e| e.split("=") }.map{ |k, v| [k.to_sym, v] }.to_h
+context_params = ARGV.to_h{ |l| l.split("=") }.transform_keys(&:to_sym)
 context_properties = context_params.reject{ |k, _v| [:user_id, :session_id, :remote_address].include? k }
 context_params.select!{ |k, _v| [:user_id, :session_id, :remote_address].include? k }
 context_params.merge!(properties: context_properties) unless context_properties.nil?
@@ -97,12 +107,12 @@ if options[:demo]
   end
 elsif options[:variant]
   variant = @unleash.get_variant(feature_name, unleash_context)
-  puts " For feature \'#{feature_name}\' got variant \'#{variant}\'"
+  puts " For feature '#{feature_name}' got variant '#{variant}'"
 else
   if @unleash.is_enabled?(feature_name, unleash_context)
-    puts " \'#{feature_name}\' is enabled according to unleash"
+    puts " '#{feature_name}' is enabled according to unleash"
   else
-    puts " \'#{feature_name}\' is disabled according to unleash"
+    puts " '#{feature_name}' is disabled according to unleash"
   end
 end
 
diff --git a/debian/changelog b/debian/changelog
index ca0d261..5d4b048 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+ruby-unleash (4.4.2-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Mon, 22 May 2023 11:27:40 -0000
+
 ruby-unleash (3.2.5-2) unstable; urgency=medium
 
   * Team upload.
diff --git a/examples/bootstrap.rb b/examples/bootstrap.rb
new file mode 100755
index 0000000..b72c94d
--- /dev/null
+++ b/examples/bootstrap.rb
@@ -0,0 +1,51 @@
+#!/usr/bin/env ruby
+
+require 'unleash'
+require 'unleash/context'
+require 'unleash/bootstrap/configuration'
+
+puts ">> START bootstrap.rb"
+
+@unleash = Unleash::Client.new(
+  url: 'https://unleash.herokuapp.com/api',
+  custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
+  app_name: 'bootstrap-test',
+  instance_id: 'local-test-cli',
+  refresh_interval: 2,
+  disable_client: true,
+  disable_metrics: true,
+  metrics_interval: 2,
+  retry_limit: 2,
+  bootstrap_config: Unleash::Bootstrap::Configuration.new(file_path: "examples/default-toggles.json")
+)
+
+feature_name = "featureX"
+unleash_context = Unleash::Context.new
+unleash_context.user_id = 123
+
+sleep 1
+3.times do
+  if @unleash.is_enabled?(feature_name, unleash_context)
+    puts "> #{feature_name} is enabled"
+  else
+    puts "> #{feature_name} is not enabled"
+  end
+  sleep 1
+  puts "---"
+  puts ""
+  puts ""
+end
+
+sleep 3
+feature_name = "foobar"
+if @unleash.is_enabled?(feature_name, unleash_context, true)
+  puts "> #{feature_name} is enabled"
+else
+  puts "> #{feature_name} is not enabled"
+end
+
+puts "> shutting down client..."
+
+@unleash.shutdown
+
+puts ">> END bootstrap.rb"
diff --git a/examples/default-toggles.json b/examples/default-toggles.json
new file mode 100644
index 0000000..c3e82b7
--- /dev/null
+++ b/examples/default-toggles.json
@@ -0,0 +1,42 @@
+{
+  "version": 1,
+  "features": [
+    {
+      "name": "featureX",
+      "enabled": true,
+      "strategies": [
+        {
+          "name": "default"
+        }
+      ]
+    },
+    {
+      "name": "featureY",
+      "enabled": false,
+      "strategies": [
+        {
+          "name": "baz",
+          "parameters": {
+            "foo": "bar"
+          }
+        }
+      ]
+    },
+    {
+      "name": "featureZ",
+      "enabled": true,
+      "strategies": [
+        {
+          "name": "default"
+        },
+        {
+          "name": "hola",
+          "parameters": {
+            "name": "val"
+          }
+        }
+      ]
+    }
+  ]
+}
+    
\ No newline at end of file
diff --git a/examples/simple.rb b/examples/simple.rb
index 7dde9d0..4d74d0d 100755
--- a/examples/simple.rb
+++ b/examples/simple.rb
@@ -6,7 +6,8 @@ require 'unleash/context'
 puts ">> START simple.rb"
 
 # Unleash.configure do |config|
-#   config.url = 'http://unleash.herokuapp.com/api'
+#   config.url = 'https://unleash.herokuapp.com/api'
+#   config.custom_http_headers = { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' }
 #   config.app_name = 'simple-test'
 #   config.refresh_interval = 2
 #   config.metrics_interval = 2
@@ -17,13 +18,13 @@ puts ">> START simple.rb"
 # or:
 
 @unleash = Unleash::Client.new(
-  url: 'https://app.unleash-hosted.com/demo/api',
+  url: 'https://unleash.herokuapp.com/api',
+  custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
   app_name: 'simple-test',
   instance_id: 'local-test-cli',
   refresh_interval: 2,
   metrics_interval: 2,
-  retry_limit: 2,
-  custom_http_headers: {'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0'},
+  retry_limit: 2
 )
 
 # feature_name = "AwesomeFeature"
diff --git a/lib/unleash.rb b/lib/unleash.rb
index 8d73883..9a6f1c7 100644
--- a/lib/unleash.rb
+++ b/lib/unleash.rb
@@ -1,40 +1,31 @@
 require 'unleash/version'
 require 'unleash/configuration'
-require 'unleash/strategy/base'
+require 'unleash/strategies'
 require 'unleash/context'
 require 'unleash/client'
 require 'logger'
 
-Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path unless path.end_with? '_spec.rb' }
-
 module Unleash
   TIME_RESOLUTION = 3
 
-  STRATEGIES = Unleash::Strategy.constants
-    .select{ |c| Unleash::Strategy.const_get(c).is_a? Class }
-    .reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) }
-    .map do |c|
-      lowered_c = c.to_s
-      lowered_c[0] = lowered_c[0].downcase
-      [lowered_c.to_sym, Object.const_get("Unleash::Strategy::#{c}").new]
-    end
-    .to_h
-
   class << self
-    attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :logger
+    attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :segment_cache, :logger
   end
 
-  def self.initialize
-    self.toggles = []
-    self.toggle_metrics = {}
-  end
+  self.configuration = Unleash::Configuration.new
+
+  # Deprecated: Use Unleash.configure to add custom strategies
+  STRATEGIES = self.configuration.strategies
 
   # Support for configuration via yield:
   def self.configure
-    self.configuration ||= Unleash::Configuration.new
     yield(configuration)
 
     self.configuration.validate!
     self.configuration.refresh_backup_file!
   end
+
+  def self.strategies
+    self.configuration.strategies
+  end
 end
diff --git a/lib/unleash/activation_strategy.rb b/lib/unleash/activation_strategy.rb
index 16f3078..29feb0c 100644
--- a/lib/unleash/activation_strategy.rb
+++ b/lib/unleash/activation_strategy.rb
@@ -1,9 +1,10 @@
 module Unleash
   class ActivationStrategy
-    attr_accessor :name, :params, :constraints
+    attr_accessor :name, :params, :constraints, :disabled
 
     def initialize(name, params, constraints = [])
       self.name = name
+      self.disabled = false
 
       if params.is_a?(Hash)
         self.params = params
@@ -18,6 +19,7 @@ module Unleash
         self.constraints = constraints
       else
         Unleash.logger.warn "Invalid constraints provided for ActivationStrategy (contraints: #{constraints})"
+        self.disabled = true
         self.constraints = []
       end
     end
diff --git a/lib/unleash/bootstrap/configuration.rb b/lib/unleash/bootstrap/configuration.rb
new file mode 100644
index 0000000..4df8e9d
--- /dev/null
+++ b/lib/unleash/bootstrap/configuration.rb
@@ -0,0 +1,25 @@
+module Unleash
+  module Bootstrap
+    class Configuration
+      attr_accessor :data, :file_path, :url, :url_headers, :block
+
+      def initialize(opts = {})
+        self.file_path = resolve_value_indifferently(opts, 'file_path') || ENV['UNLEASH_BOOTSTRAP_FILE'] || nil
+        self.url = resolve_value_indifferently(opts, 'url') || ENV['UNLEASH_BOOTSTRAP_URL'] || nil
+        self.url_headers = resolve_value_indifferently(opts, 'url_headers')
+        self.data = resolve_value_indifferently(opts, 'data')
+        self.block = resolve_value_indifferently(opts, 'block')
+      end
+
+      def valid?
+        ![self.data, self.file_path, self.url, self.block].all?(&:nil?)
+      end
+
+      private
+
+      def resolve_value_indifferently(opts, key)
+        opts[key] || opts[key.to_sym]
+      end
+    end
+  end
+end
diff --git a/lib/unleash/bootstrap/handler.rb b/lib/unleash/bootstrap/handler.rb
new file mode 100644
index 0000000..319e949
--- /dev/null
+++ b/lib/unleash/bootstrap/handler.rb
@@ -0,0 +1,22 @@
+require 'unleash/bootstrap/provider/from_url'
+require 'unleash/bootstrap/provider/from_file'
+
+module Unleash
+  module Bootstrap
+    class Handler
+      attr_accessor :configuration
+
+      def initialize(configuration)
+        self.configuration = configuration
+      end
+
+      # @return [String] JSON string representing data returned from an Unleash server
+      def retrieve_toggles
+        return configuration.data unless self.configuration.data.nil?
+        return configuration.block.call if self.configuration.block.is_a?(Proc)
+        return Provider::FromFile.read(configuration.file_path) unless self.configuration.file_path.nil?
+        return Provider::FromUrl.read(configuration.url, configuration.url_headers) unless self.configuration.url.nil?
+      end
+    end
+  end
+end
diff --git a/lib/unleash/bootstrap/provider/base.rb b/lib/unleash/bootstrap/provider/base.rb
new file mode 100644
index 0000000..917ee17
--- /dev/null
+++ b/lib/unleash/bootstrap/provider/base.rb
@@ -0,0 +1,14 @@
+module Unleash
+  module Bootstrap
+    module Provider
+      class NotImplemented < RuntimeError
+      end
+
+      class Base
+        def read
+          raise NotImplemented, "Bootstrap is not implemented"
+        end
+      end
+    end
+  end
+end
diff --git a/lib/unleash/bootstrap/provider/from_file.rb b/lib/unleash/bootstrap/provider/from_file.rb
new file mode 100644
index 0000000..87aa434
--- /dev/null
+++ b/lib/unleash/bootstrap/provider/from_file.rb
@@ -0,0 +1,14 @@
+require 'unleash/bootstrap/provider/base'
+
+module Unleash
+  module Bootstrap
+    module Provider
+      class FromFile < Base
+        # @param file_path [String]
+        def self.read(file_path)
+          File.read(file_path)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/unleash/bootstrap/provider/from_url.rb b/lib/unleash/bootstrap/provider/from_url.rb
new file mode 100644
index 0000000..280fe88
--- /dev/null
+++ b/lib/unleash/bootstrap/provider/from_url.rb
@@ -0,0 +1,19 @@
+require 'unleash/bootstrap/provider/base'
+
+module Unleash
+  module Bootstrap
+    module Provider
+      class FromUrl < Base
+        # @param url [String]
+        # @param headers [Hash, nil] HTTP headers to use. If not set, the unleash client SDK ones will be used.
+        def self.read(url, headers = nil)
+          response = Unleash::Util::Http.get(URI.parse(url), nil, headers)
+
+          return nil if response.code != '200'
+
+          response.body
+        end
+      end
+    end
+  end
+end
diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb
index c3f5019..62b75f3 100644
--- a/lib/unleash/client.rb
+++ b/lib/unleash/client.rb
@@ -12,14 +12,15 @@ module Unleash
     attr_accessor :fetcher_scheduled_executor, :metrics_scheduled_executor
 
     def initialize(*opts)
-      Unleash.configuration ||= Unleash::Configuration.new(*opts)
+      Unleash.configuration = Unleash::Configuration.new(*opts) unless opts.empty?
       Unleash.configuration.validate!
 
       Unleash.logger = Unleash.configuration.logger.clone
       Unleash.logger.level = Unleash.configuration.log_level
 
+      Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
       if Unleash.configuration.disable_client
-        Unleash.logger.warn "Unleash::Client is disabled! Will only return default results!"
+        Unleash.logger.warn "Unleash::Client is disabled! Will only return default (or bootstrapped if available) results!"
         return
       end
 
@@ -28,13 +29,14 @@ module Unleash
       start_metrics unless Unleash.configuration.disable_metrics
     end
 
-    def is_enabled?(feature, context = nil, default_value = false)
+    def is_enabled?(feature, context = nil, default_value_param = false, &fallback_blk)
       Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}"
 
-      if Unleash.configuration.disable_client
-        Unleash.logger.warn "unleash_client is disabled! Always returning #{default_value} for feature #{feature}!"
-        return default_value
-      end
+      default_value = if block_given?
+                        default_value_param || !!fallback_blk.call(feature, context)
+                      else
+                        default_value_param
+                      end
 
       toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
 
@@ -43,32 +45,43 @@ module Unleash
         return default_value
       end
 
-      toggle = Unleash::FeatureToggle.new(toggle_as_hash)
+      toggle = Unleash::FeatureToggle.new(toggle_as_hash, Unleash&.segment_cache)
+
+      toggle.is_enabled?(context)
+    end
 
-      toggle.is_enabled?(context, default_value)
+    def is_disabled?(feature, context = nil, default_value_param = true, &fallback_blk)
+      !is_enabled?(feature, context, !default_value_param, &fallback_blk)
     end
 
     # enabled? is a more ruby idiomatic method name than is_enabled?
     alias enabled? is_enabled?
+    # disabled? is a more ruby idiomatic method name than is_disabled?
+    alias disabled? is_disabled?
 
     # execute a code block (passed as a parameter), if is_enabled? is true.
     def if_enabled(feature, context = nil, default_value = false, &blk)
       yield(blk) if is_enabled?(feature, context, default_value)
     end
 
-    def get_variant(feature, context = nil, fallback_variant = nil)
+    # execute a code block (passed as a parameter), if is_disabled? is true.
+    def if_disabled(feature, context = nil, default_value = true, &blk)
+      yield(blk) if is_disabled?(feature, context, default_value)
+    end
+
+    def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant)
       Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"
 
       if Unleash.configuration.disable_client
         Unleash.logger.debug "unleash_client is disabled! Always returning #{fallback_variant} for feature #{feature}!"
-        return fallback_variant || Unleash::FeatureToggle.disabled_variant
+        return fallback_variant
       end
 
       toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
 
       if toggle_as_hash.nil?
         Unleash.logger.debug "Unleash::Client.get_variant feature: #{feature} not found"
-        return fallback_variant || Unleash::FeatureToggle.disabled_variant
+        return fallback_variant
       end
 
       toggle = Unleash::FeatureToggle.new(toggle_as_hash)
@@ -76,7 +89,7 @@ module Unleash
 
       if variant.nil?
         Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found"
-        return fallback_variant || Unleash::FeatureToggle.disabled_variant
+        return fallback_variant
       end
 
       # TODO: Add to README: name, payload, enabled (bool)
@@ -88,7 +101,7 @@ module Unleash
     def shutdown
       unless Unleash.configuration.disable_client
         Unleash.toggle_fetcher.save!
-        Unleash.reporter.send unless Unleash.configuration.disable_metrics
+        Unleash.reporter.post unless Unleash.configuration.disable_metrics
         shutdown!
       end
     end
@@ -108,18 +121,18 @@ module Unleash
         'appName': Unleash.configuration.app_name,
         'instanceId': Unleash.configuration.instance_id,
         'sdkVersion': "unleash-client-ruby:" + Unleash::VERSION,
-        'strategies': Unleash::STRATEGIES.keys,
+        'strategies': Unleash.strategies.keys,
         'started': Time.now.iso8601(Unleash::TIME_RESOLUTION),
         'interval': Unleash.configuration.metrics_interval_in_millis
       }
     end
 
     def start_toggle_fetcher
-      Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
       self.fetcher_scheduled_executor = Unleash::ScheduledExecutor.new(
         'ToggleFetcher',
         Unleash.configuration.refresh_interval,
-        Unleash.configuration.retry_limit
+        Unleash.configuration.retry_limit,
+        first_fetch_is_eager
       )
       self.fetcher_scheduled_executor.run do
         Unleash.toggle_fetcher.fetch
@@ -135,7 +148,7 @@ module Unleash
         Unleash.configuration.retry_limit
       )
       self.metrics_scheduled_executor.run do
-        Unleash.reporter.send
+        Unleash.reporter.post
       end
     end
 
@@ -144,12 +157,20 @@ module Unleash
 
       # Send the request, if possible
       begin
-        response = Unleash::Util::Http.post(Unleash.configuration.client_register_url, info.to_json)
+        response = Unleash::Util::Http.post(Unleash.configuration.client_register_uri, info.to_json)
       rescue StandardError => e
         Unleash.logger.error "unable to register client with unleash server due to exception #{e.class}:'#{e}'."
         Unleash.logger.error "stacktrace: #{e.backtrace}"
       end
       Unleash.logger.debug "client registered: #{response}"
     end
+
+    def disabled_variant
+      @disabled_variant ||= Unleash::FeatureToggle.disabled_variant
+    end
+
+    def first_fetch_is_eager
+      Unleash.configuration.use_bootstrap?
+    end
   end
 end
diff --git a/lib/unleash/configuration.rb b/lib/unleash/configuration.rb
index ec8360c..4f43200 100644
--- a/lib/unleash/configuration.rb
+++ b/lib/unleash/configuration.rb
@@ -1,5 +1,6 @@
 require 'securerandom'
 require 'tmpdir'
+require 'unleash/bootstrap/configuration'
 
 module Unleash
   class Configuration
@@ -8,6 +9,7 @@ module Unleash
       :app_name,
       :environment,
       :instance_id,
+      :project_name,
       :custom_http_headers,
       :disable_client,
       :disable_metrics,
@@ -17,10 +19,12 @@ module Unleash
       :metrics_interval,
       :backup_file,
       :logger,
-      :log_level
+      :log_level,
+      :bootstrap_config,
+      :strategies
 
     def initialize(opts = {})
-      ensure_valid_opts(opts)
+      validate_custom_http_headers!(opts[:custom_http_headers]) if opts.has_key?(:custom_http_headers)
       set_defaults
 
       initialize_default_logger if opts[:logger].nil?
@@ -37,59 +41,68 @@ module Unleash
       return if self.disable_client
 
       raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? || self.url.nil?
-      raise ArgumentError, "custom_http_headers must be a hash." unless self.custom_http_headers.is_a?(Hash)
+
+      validate_custom_http_headers!(self.custom_http_headers)
     end
 
     def refresh_backup_file!
-      self.backup_file = Dir.tmpdir + "/unleash-#{app_name}-repo.json" if self.backup_file.nil?
+      self.backup_file = File.join(Dir.tmpdir, "unleash-#{app_name}-repo.json")
     end
 
     def http_headers
       {
         'UNLEASH-INSTANCEID' => self.instance_id,
-        'UNLEASH-APPNAME' => self.app_name
-      }.merge(custom_http_headers.dup)
+        'UNLEASH-APPNAME' => self.app_name,
+        'Unleash-Client-Spec' => '4.2.2'
+      }.merge!(generate_custom_http_headers)
     end
 
-    def fetch_toggles_url
-      self.url + '/client/features'
+    def fetch_toggles_uri
+      uri = URI("#{self.url_stripped_of_slash}/client/features")
+      uri.query = "project=#{self.project_name}" unless self.project_name.nil?
+      uri
     end
 
-    def client_metrics_url
-      self.url + '/client/metrics'
+    def client_metrics_uri
+      URI("#{self.url_stripped_of_slash}/client/metrics")
     end
 
-    def client_register_url
-      self.url + '/client/register'
+    def client_register_uri
+      URI("#{self.url_stripped_of_slash}/client/register")
     end
 
-    private
+    def url_stripped_of_slash
+      self.url.delete_suffix '/'
+    end
 
-    def ensure_valid_opts(opts)
-      unless opts[:custom_http_headers].is_a?(Hash) || opts[:custom_http_headers].nil?
-        raise ArgumentError, "custom_http_headers must be a hash."
-      end
+    def use_bootstrap?
+      self.bootstrap_config&.valid?
     end
 
+    private
+
     def set_defaults
       self.app_name         = nil
       self.environment      = 'default'
       self.url              = nil
       self.instance_id      = SecureRandom.uuid
+      self.project_name     = nil
       self.disable_client   = false
       self.disable_metrics  = false
       self.refresh_interval = 10
-      self.metrics_interval = 30
+      self.metrics_interval = 60
       self.timeout          = 30
-      self.retry_limit      = 1
+      self.retry_limit      = Float::INFINITY
       self.backup_file      = nil
       self.log_level        = Logger::WARN
+      self.bootstrap_config = nil
+      self.strategies       = Unleash::Strategies.new
 
       self.custom_http_headers = {}
     end
 
     def initialize_default_logger
-      self.logger = Logger.new(STDOUT)
+      self.logger = Logger.new($stdout)
 
       # on default logger, use custom formatter that includes thread_name:
       self.logger.formatter = proc do |severity, datetime, _progname, msg|
@@ -103,6 +116,18 @@ module Unleash
       self
     end
 
+    def validate_custom_http_headers!(custom_http_headers)
+      return if custom_http_headers.is_a?(Hash) || custom_http_headers.respond_to?(:call)
+
+      raise ArgumentError, "custom_http_headers must be a Hash or a Proc."
+    end
+
+    def generate_custom_http_headers
+      return self.custom_http_headers.call if self.custom_http_headers.respond_to?(:call)
+
+      self.custom_http_headers
+    end
+
     def set_option(opt, val)
       __send__("#{opt}=", val)
     rescue NoMethodError
diff --git a/lib/unleash/constraint.rb b/lib/unleash/constraint.rb
index 0347913..51607c1 100644
--- a/lib/unleash/constraint.rb
+++ b/lib/unleash/constraint.rb
@@ -1,26 +1,115 @@
+require 'date'
 module Unleash
   class Constraint
-    attr_accessor :context_name, :operator, :values
+    attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
 
-    VALID_OPERATORS = ['IN', 'NOT_IN'].freeze
+    OPERATORS = {
+      IN: ->(context_v, constraint_v){ constraint_v.include? context_v.to_s },
+      NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v.to_s },
+      STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
+      STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
+      STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
+      NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } },
+      NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } },
+      NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } },
+      NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } },
+      NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } },
+      DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } },
+      DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
+      SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
+      SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } },
+      SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } },
+      FALLBACK_VALIDATOR: ->(_context_v, _constraint_v){ false }
+    }.freeze
 
-    def initialize(context_name, operator, values = [])
+    LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
+
+    def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
       raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
-      raise ArgumentError, "operator does not hold a valid value:" + VALID_OPERATORS unless VALID_OPERATORS.include? operator
-      raise ArgumentError, "values does not hold an Array" unless values.is_a?(Array)
+
+      unless OPERATORS.include? operator.to_sym
+        Unleash.logger.warn "Operator #{operator} is not a supported operator, " \
+          "falling back to FALLBACK_VALIDATOR which skips this constraint."
+        operator = "FALLBACK_VALIDATOR"
+      end
+      self.log_inconsistent_constraint_configuration(operator.to_sym, value)
 
       self.context_name = context_name
-      self.operator = operator
-      self.values = values
+      self.operator = operator.to_sym
+      self.value = value
+      self.inverted = !!inverted
+      self.case_insensitive = !!case_insensitive
     end
 
     def matches_context?(context)
-      Unleash.logger.debug "Unleash::Constraint matches_context? values: #{self.values} context.get_by_name(#{self.context_name})" \
-        " #{context.get_by_name(self.context_name)} "
+      Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})"
+      return false if context.nil?
+
+      match = matches_constraint?(context)
+      self.inverted ? !match : match
+    rescue KeyError
+      Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
+      found on the context"
+      false
+    end
+
+    def self.on_valid_date(val1, val2)
+      val1 = DateTime.parse(val1)
+      val2 = DateTime.parse(val2)
+      yield(val1, val2)
+    rescue ArgumentError
+      Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
+      or constraint_value (#{val2}) into a date. Returning false!"
+      false
+    end
+
+    def self.on_valid_float(val1, val2)
+      val1 = Float(val1)
+      val2 = Float(val2)
+      yield(val1, val2)
+    rescue ArgumentError
+      Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
+      or constraint_value (#{val2}) into a number. Returning false!"
+      false
+    end
+
+    def self.on_valid_version(val1, val2)
+      val1 = Gem::Version.new(val1)
+      val2 = Gem::Version.new(val2)
+      yield(val1, val2)
+    rescue ArgumentError
+      Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
+      or constraint_value (#{val2}) into a version. Return false!"
+      false
+    end
+
+    # This should be a private method but for some reason this fails on Ruby 2.5
+    def log_inconsistent_constraint_configuration(operator, value)
+      Unleash.logger.warn "value is a String, operator is expecting an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
+      Unleash.logger.warn "value is an Array, operator is expecting a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
+    end
+
+    private
+
+    def matches_constraint?(context)
+      Unleash.logger.debug "Unleash::Constraint matches_constraint? value: #{self.value} operator: #{self.operator} " \
+        " context.get_by_name(#{self.context_name})"
+
+      unless OPERATORS.include?(self.operator)
+        Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
+        false
+      end
+
+      # when the operator is NOT_IN and there is no data, return true. In all other cases the operator doesn't match.
+      return self.operator == :NOT_IN unless context.include?(self.context_name)
+
+      v = self.value.dup
+      context_value = context.get_by_name(self.context_name)
 
-      is_included = self.values.include? context.get_by_name(self.context_name)
+      v.map!(&:upcase) if self.case_insensitive
+      context_value.upcase! if self.case_insensitive
 
-      operator == 'IN' ? is_included : !is_included
+      OPERATORS[self.operator].call(context_value, v)
     end
   end
 end
diff --git a/lib/unleash/context.rb b/lib/unleash/context.rb
index aef69b4..79b4cf1 100644
--- a/lib/unleash/context.rb
+++ b/lib/unleash/context.rb
@@ -1,6 +1,6 @@
 module Unleash
   class Context
-    ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address].freeze
+    ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address, :current_time].freeze
 
     attr_accessor(*[ATTRS, :properties].flatten)
 
@@ -9,9 +9,10 @@ module Unleash
 
       self.app_name    = value_for('appName', params, Unleash&.configuration&.app_name)
       self.environment = value_for('environment', params, Unleash&.configuration&.environment || 'default')
-      self.user_id     = value_for('userId', params)
+      self.user_id     = value_for('userId', params)&.to_s
       self.session_id  = value_for('sessionId', params)
       self.remote_address = value_for('remoteAddress', params)
+      self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
 
       properties = value_for('properties', params)
       self.properties = properties.is_a?(Hash) ? properties.transform_keys(&:to_sym) : {}
@@ -28,10 +29,17 @@ module Unleash
       if ATTRS.include? normalized_name
         self.send(normalized_name)
       else
-        self.properties.fetch(normalized_name)
+        self.properties.fetch(normalized_name, nil) || self.properties.fetch(name.to_sym)
       end
     end
 
+    def include?(name)
+      normalized_name = underscore(name)
+      return self.instance_variable_defined? "@#{normalized_name}" if ATTRS.include? normalized_name.to_sym
+
+      self.properties.include?(normalized_name.to_sym) || self.properties.include?(name.to_sym)
+    end
+
     private
 
     # Method to fetch values from hash for two types of keys: string in camelCase and symbol in snake_case
diff --git a/lib/unleash/feature_toggle.rb b/lib/unleash/feature_toggle.rb
index c91ef9f..2818f02 100644
--- a/lib/unleash/feature_toggle.rb
+++ b/lib/unleash/feature_toggle.rb
@@ -9,13 +9,13 @@ module Unleash
   class FeatureToggle
     attr_accessor :name, :enabled, :strategies, :variant_definitions
 
-    def initialize(params = {})
+    def initialize(params = {}, segment_map = {})
       params = {} if params.nil?
 
       self.name       = params.fetch('name', nil)
       self.enabled    = params.fetch('enabled', false)
 
-      self.strategies = initialize_strategies(params)
+      self.strategies = initialize_strategies(params, segment_map)
       self.variant_definitions = initialize_variant_definitions(params)
     end
 
@@ -23,8 +23,8 @@ module Unleash
       "<FeatureToggle: name=#{name},enabled=#{enabled},strategies=#{strategies},variant_definitions=#{variant_definitions}>"
     end
 
-    def is_enabled?(context, default_result)
-      result = am_enabled?(context, default_result)
+    def is_enabled?(context)
+      result = am_enabled?(context)
 
       choice = result ? :yes : :no
       Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
@@ -32,25 +32,38 @@ module Unleash
       result
     end
 
-    def get_variant(context, fallback_variant = disabled_variant)
+    def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant)
       raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant'
 
       context = ensure_valid_context(context)
 
-      return disabled_variant unless self.enabled && am_enabled?(context, true)
-      return disabled_variant if sum_variant_defs_weights <= 0
+      toggle_enabled = am_enabled?(context)
+      variant = resolve_variant(context, toggle_enabled)
 
-      variant = variant_from_override_match(context)
-      variant = variant_from_weights(context) if variant.nil?
-
-      Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics
+      choice = toggle_enabled ? :yes : :no
+      Unleash.toggle_metrics.increment_variant(self.name, choice, variant.name) unless Unleash.configuration.disable_metrics
       variant
     end
 
+    def self.disabled_variant
+      Unleash::Variant.new(name: 'disabled', enabled: false)
+    end
+
     private
 
+    def resolve_variant(context, toggle_enabled)
+      return Unleash::FeatureToggle.disabled_variant unless toggle_enabled
+      return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
+
+      variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
+    end
+
+    def resolve_stickiness
+      self.variant_definitions&.map(&:stickiness)&.compact&.first || "default"
+    end
+
     # only check if it is enabled, do not do metrics
-    def am_enabled?(context, default_result)
+    def am_enabled?(context)
       result =
         if self.enabled
           self.strategies.empty? ||
@@ -58,34 +71,33 @@ module Unleash
               strategy_enabled?(s, context) && strategy_constraint_matches?(s, context)
             end
         else
-          default_result
+          false
         end
 
-      Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} " \
+      Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \
         "and Strategies combined with contraints returned #{result})"
 
       result
     end
 
     def strategy_enabled?(strategy, context)
-      r = Unleash::STRATEGIES.fetch(strategy.name.to_sym, :unknown).is_enabled?(strategy.params, context)
+      r = Unleash.strategies.fetch(strategy.name).is_enabled?(strategy.params, context)
       Unleash.logger.debug "Unleash::FeatureToggle.strategy_enabled? Strategy #{strategy.name} returned #{r} with context: #{context}"
       r
     end
 
     def strategy_constraint_matches?(strategy, context)
-      strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
-    end
+      return false if strategy.disabled
 
-    def disabled_variant
-      Unleash::Variant.new(name: 'disabled', enabled: false)
+      strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
     end
 
     def sum_variant_defs_weights
       self.variant_definitions.map(&:weight).reduce(0, :+)
     end
 
-    def variant_salt(context)
+    def variant_salt(context, stickiness = "default")
+      return context.get_by_name(stickiness) unless stickiness == "default"
       return context.user_id unless context.user_id.to_s.empty?
       return context.session_id unless context.session_id.to_s.empty?
       return context.remote_address unless context.remote_address.to_s.empty?
@@ -100,8 +112,8 @@ module Unleash
       Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload)
     end
 
-    def variant_from_weights(context)
-      variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context), self.name, sum_variant_defs_weights)
+    def variant_from_weights(context, stickiness)
+      variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context, stickiness), self.name, sum_variant_defs_weights)
       prev_weights = 0
 
       variant_definition = self.variant_definitions
@@ -110,7 +122,7 @@ module Unleash
           prev_weights += v.weight
           res
         end
-      return disabled_variant if variant_definition.nil?
+      return self.disabled_variant if variant_definition.nil?
 
       Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
     end
@@ -124,24 +136,35 @@ module Unleash
       context
     end
 
-    def initialize_strategies(params)
+    def initialize_strategies(params, segment_map)
       params.fetch('strategies', [])
-        .select{ |s| s.has_key?('name') && Unleash::STRATEGIES.has_key?(s['name'].to_sym) }
+        .select{ |s| s.has_key?('name') && Unleash.strategies.includes?(s['name']) }
         .map do |s|
           ActivationStrategy.new(
             s['name'],
             s['parameters'],
-            (s['constraints'] || []).map do |c|
-              Constraint.new(
-                c.fetch('contextName'),
-                c.fetch('operator'),
-                c.fetch('values')
-              )
-            end
+            resolve_constraints(s, segment_map)
           )
         end || []
     end
 
+    def resolve_constraints(strategy, segment_map)
+      segment_constraints = (strategy["segments"] || []).map do |segment_id|
+        segment_map[segment_id]&.fetch("constraints")
+      end
+      (strategy.fetch("constraints", []) + segment_constraints).flatten.map do |constraint|
+        return nil if constraint.nil?
+
+        Constraint.new(
+          constraint.fetch('contextName'),
+          constraint.fetch('operator'),
+          constraint.fetch('value', nil) || constraint.fetch('values', nil),
+          inverted: constraint.fetch('inverted', false),
+          case_insensitive: constraint.fetch('caseInsensitive', false)
+        )
+      end
+    end
+
     def initialize_variant_definitions(params)
       (params.fetch('variants', []) || [])
         .select{ |v| v.is_a?(Hash) && v.has_key?('name') }
@@ -150,6 +173,7 @@ module Unleash
             v.fetch('name', ''),
             v.fetch('weight', 0),
             v.fetch('payload', nil),
+            v.fetch('stickiness', nil),
             v.fetch('overrides', [])
           )
         end || []
diff --git a/lib/unleash/metrics.rb b/lib/unleash/metrics.rb
index 5f112d4..4dc8bc5 100644
--- a/lib/unleash/metrics.rb
+++ b/lib/unleash/metrics.rb
@@ -1,33 +1,41 @@
 module Unleash
   class Metrics
-    attr_accessor :features
-
-    # NOTE: no mutexes for features
+    attr_accessor :features, :features_lock
 
     def initialize
       self.features = {}
+      self.features_lock = Mutex.new
     end
 
     def to_s
-      self.features.to_json
+      self.features_lock.synchronize do
+        return self.features.to_json
+      end
     end
 
     def increment(feature, choice)
       raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice
 
-      self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
-      self.features[feature][choice] += 1
+      self.features_lock.synchronize do
+        self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
+        self.features[feature][choice] += 1
+      end
     end
 
-    def increment_variant(feature, variant)
-      self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
-      self.features[feature]['variant'] = {}     unless self.features[feature].include? 'variant'
-      self.features[feature]['variant'][variant] = 0 unless self.features[feature]['variant'].include? variant
-      self.features[feature]['variant'][variant] += 1
+    def increment_variant(feature, choice, variant)
+      self.features_lock.synchronize do
+        self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
+        self.features[feature][choice] += 1
+        self.features[feature]['variant'] = {}     unless self.features[feature].include? 'variant'
+        self.features[feature]['variant'][variant] = 0 unless self.features[feature]['variant'].include? variant
+        self.features[feature]['variant'][variant] += 1
+      end
     end
 
     def reset
-      self.features = {}
+      self.features_lock.synchronize do
+        self.features = {}
+      end
     end
   end
 end
diff --git a/lib/unleash/metrics_reporter.rb b/lib/unleash/metrics_reporter.rb
index 019dca9..fc1e7ca 100755
--- a/lib/unleash/metrics_reporter.rb
+++ b/lib/unleash/metrics_reporter.rb
@@ -6,6 +6,8 @@ require 'time'
 
 module Unleash
   class MetricsReporter
+    LONGEST_WITHOUT_A_REPORT = 600
+
     attr_accessor :last_time
 
     def initialize
@@ -33,16 +35,28 @@ module Unleash
       report
     end
 
-    def send
-      Unleash.logger.debug "send() Report"
+    def post
+      Unleash.logger.debug "post() Report"
+
+      if bucket_empty? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes...
+        Unleash.logger.debug "Report not posted to server, as it would have been empty. (and has been empty for up to 10 min)"
+
+        return
+      end
 
-      response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_url, self.generate_report.to_json)
+      response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, self.generate_report.to_json)
 
       if ['200', '202'].include? response.code
-        Unleash.logger.debug "Report sent to unleash server sucessfully. Server responded with http code #{response.code}"
+        Unleash.logger.debug "Report sent to unleash server successfully. Server responded with http code #{response.code}"
       else
         Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}."
       end
     end
+
+    private
+
+    def bucket_empty?
+      Unleash.toggle_metrics.features.empty?
+    end
   end
 end
diff --git a/lib/unleash/scheduled_executor.rb b/lib/unleash/scheduled_executor.rb
index 7382f89..c129bdc 100755
--- a/lib/unleash/scheduled_executor.rb
+++ b/lib/unleash/scheduled_executor.rb
@@ -1,19 +1,22 @@
 module Unleash
   class ScheduledExecutor
-    attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread
+    attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread, :immediate_execution
 
-    def initialize(name, interval, max_exceptions = 5)
+    def initialize(name, interval, max_exceptions = 5, immediate_execution = false)
       self.name = name || ''
       self.interval = interval
       self.max_exceptions = max_exceptions
       self.retry_count = 0
       self.thread = nil
+      self.immediate_execution = immediate_execution
     end
 
     def run(&blk)
       self.thread = Thread.new do
         Thread.current[:name] = self.name
 
+        run_blk{ blk.call } if self.immediate_execution
+
         Unleash.logger.debug "thread #{name} loop starting"
         loop do
           Unleash.logger.debug "thread #{name} sleeping for #{interval} seconds"
diff --git a/lib/unleash/strategies.rb b/lib/unleash/strategies.rb
new file mode 100644
index 0000000..842af4f
--- /dev/null
+++ b/lib/unleash/strategies.rb
@@ -0,0 +1,80 @@
+require 'unleash/strategy/base'
+Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path }
+
+module Unleash
+  class Strategies
+    def initialize
+      @strategies = {}
+      register_strategies
+    end
+
+    def keys
+      @strategies.keys
+    end
+
+    def includes?(name)
+      @strategies.has_key?(name.to_s)
+    end
+
+    def fetch(name)
+      raise Unleash::Strategy::NotImplemented, "Strategy is not implemented" unless (strategy = @strategies[name.to_s])
+
+      strategy
+    end
+
+    def add(strategy)
+      @strategies[strategy.name] = strategy
+    end
+
+    def []=(key, strategy)
+      warn_deprecated_registration(strategy, 'modifying Unleash::STRATEGIES')
+      @strategies[key.to_s] = strategy
+    end
+
+    def [](key)
+      @strategies[key.to_s]
+    end
+
+    def register_strategies
+      register_base_strategies
+      register_custom_strategies
+    end
+
+    protected
+
+    # Deprecated: Use Unleash.configuration to add custom strategies
+    def register_custom_strategies
+      Unleash::Strategy.constants
+        .select{ |c| Unleash::Strategy.const_get(c).is_a? Class }
+        .reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) } # Reject abstract classes
+        .map{ |c| Object.const_get("Unleash::Strategy::#{c}") }
+        .reject{ |c| DEFAULT_STRATEGIES.include?(c) } # Reject base classes
+        .each do |c|
+        strategy = c.new
+        warn_deprecated_registration(strategy, 'adding custom class into Unleash::Strategy namespace')
+        self.add(strategy)
+      end
+    end
+
+    def register_base_strategies
+      DEFAULT_STRATEGIES.each{ |c| self.add(c.new) }
+    end
+
+    DEFAULT_STRATEGIES = [
+      Unleash::Strategy::ApplicationHostname,
+      Unleash::Strategy::Default,
+      Unleash::Strategy::FlexibleRollout,
+      Unleash::Strategy::GradualRolloutRandom,
+      Unleash::Strategy::GradualRolloutSessionId,
+      Unleash::Strategy::GradualRolloutUserId,
+      Unleash::Strategy::RemoteAddress,
+      Unleash::Strategy::UserWithId
+    ].freeze
+
+    def warn_deprecated_registration(strategy, method)
+      warn "[DEPRECATED] Registering custom Unleash strategy by #{method} is deprecated.
+             Please use Unleash configuration to register custom strategy: " \
+           "`Unleash.configure {|c| c.strategies.add(#{strategy.class.name}.new) }`"
+    end
+  end
+end
diff --git a/lib/unleash/strategy/application_hostname.rb b/lib/unleash/strategy/application_hostname.rb
index f32c8e1..f5fd578 100644
--- a/lib/unleash/strategy/application_hostname.rb
+++ b/lib/unleash/strategy/application_hostname.rb
@@ -4,6 +4,7 @@ module Unleash
   module Strategy
     class ApplicationHostname < Base
       attr_accessor :hostname
+
       PARAM = 'hostnames'.freeze
 
       def initialize
diff --git a/lib/unleash/strategy/flexible_rollout.rb b/lib/unleash/strategy/flexible_rollout.rb
index f25196c..edb8256 100644
--- a/lib/unleash/strategy/flexible_rollout.rb
+++ b/lib/unleash/strategy/flexible_rollout.rb
@@ -10,7 +10,7 @@ module Unleash
       # need: params['percentage']
       def is_enabled?(params = {}, context = nil)
         return false unless params.is_a?(Hash)
-        return false unless context.class.name == 'Unleash::Context'
+        return false unless context.instance_of?(Unleash::Context)
 
         stickiness = params.fetch('stickiness', 'default')
         stickiness_id = resolve_stickiness(stickiness, context)
@@ -38,16 +38,16 @@ module Unleash
 
       def resolve_stickiness(stickiness, context)
         case stickiness
-        when 'userId'
-          context.user_id
-        when 'sessionId'
-          context.session_id
         when 'random'
           random
         when 'default'
           context.user_id || context.session_id || random
         else
-          nil
+          begin
+            context.get_by_name(stickiness)
+          rescue KeyError
+            nil
+          end
         end
       end
     end
diff --git a/lib/unleash/strategy/gradual_rollout_sessionid.rb b/lib/unleash/strategy/gradual_rollout_sessionid.rb
index f581d88..0f2a553 100644
--- a/lib/unleash/strategy/gradual_rollout_sessionid.rb
+++ b/lib/unleash/strategy/gradual_rollout_sessionid.rb
@@ -10,7 +10,7 @@ module Unleash
       # need: params['percentage'], params['groupId'], context.user_id,
       def is_enabled?(params = {}, context = nil)
         return false unless params.is_a?(Hash) && params.has_key?('percentage')
-        return false unless context.class.name == 'Unleash::Context'
+        return false unless context.instance_of?(Unleash::Context)
         return false if context.session_id.nil? || context.session_id.empty?
 
         percentage = Integer(params['percentage'] || 0)
diff --git a/lib/unleash/strategy/gradual_rollout_userid.rb b/lib/unleash/strategy/gradual_rollout_userid.rb
index b0e69d6..1aa3c05 100644
--- a/lib/unleash/strategy/gradual_rollout_userid.rb
+++ b/lib/unleash/strategy/gradual_rollout_userid.rb
@@ -10,7 +10,7 @@ module Unleash
       # need: params['percentage'], params['groupId'], context.user_id,
       def is_enabled?(params = {}, context = nil, _constraints = [])
         return false unless params.is_a?(Hash) && params.has_key?('percentage')
-        return false unless context.class.name == 'Unleash::Context'
+        return false unless context.instance_of?(Unleash::Context)
         return false if context.user_id.nil? || context.user_id.empty?
 
         percentage = Integer(params['percentage'] || 0)
diff --git a/lib/unleash/strategy/remote_address.rb b/lib/unleash/strategy/remote_address.rb
index 154a442..d222311 100644
--- a/lib/unleash/strategy/remote_address.rb
+++ b/lib/unleash/strategy/remote_address.rb
@@ -11,9 +11,25 @@ module Unleash
       def is_enabled?(params = {}, context = nil)
         return false unless params.is_a?(Hash) && params.has_key?(PARAM)
         return false unless params.fetch(PARAM, nil).is_a? String
-        return false unless context.class.name == 'Unleash::Context'
+        return false unless context.instance_of?(Unleash::Context)
 
-        params[PARAM].split(',').map(&:strip).include?(context.remote_address)
+        remote_address = ipaddr_or_nil_from_str(context.remote_address)
+
+        params[PARAM]
+          .split(',')
+          .map(&:strip)
+          .map{ |ipblock| ipaddr_or_nil_from_str(ipblock) }
+          .compact
+          .map{ |ipb| ipb.include? remote_address }
+          .any?
+      end
+
+      private
+
+      def ipaddr_or_nil_from_str(ip)
+        IPAddr.new(ip)
+      rescue StandardError
+        nil
       end
     end
   end
diff --git a/lib/unleash/strategy/user_with_id.rb b/lib/unleash/strategy/user_with_id.rb
index aebde32..c20a75b 100644
--- a/lib/unleash/strategy/user_with_id.rb
+++ b/lib/unleash/strategy/user_with_id.rb
@@ -11,7 +11,7 @@ module Unleash
       def is_enabled?(params = {}, context = nil)
         return false unless params.is_a?(Hash) && params.has_key?(PARAM)
         return false unless params.fetch(PARAM, nil).is_a? String
-        return false unless context.class.name == 'Unleash::Context'
+        return false unless context.instance_of?(Unleash::Context)
 
         params[PARAM].split(",").map(&:strip).include?(context.user_id)
       end
diff --git a/lib/unleash/toggle_fetcher.rb b/lib/unleash/toggle_fetcher.rb
index 176a126..2b33f4d 100755
--- a/lib/unleash/toggle_fetcher.rb
+++ b/lib/unleash/toggle_fetcher.rb
@@ -1,22 +1,29 @@
 require 'unleash/configuration'
+require 'unleash/bootstrap/handler'
 require 'net/http'
 require 'json'
 
 module Unleash
   class ToggleFetcher
-    attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count
+    attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache
 
     def initialize
       self.etag = nil
       self.toggle_cache = nil
+      self.segment_cache = nil
       self.toggle_lock = Mutex.new
       self.toggle_resource = ConditionVariable.new
       self.retry_count = 0
 
-      # start by fetching synchronously, and failing back to reading the backup file.
       begin
-        fetch
+        # if bootstrap configuration is available, initialize. An immediate API read is also triggered
+        if Unleash.configuration.use_bootstrap?
+          bootstrap
+        else
+          fetch
+        end
       rescue StandardError => e
+        # fail back to reading the backup file
         Unleash.logger.warn "ToggleFetcher was unable to fetch from the network, attempting to read from backup file."
         Unleash.logger.debug "Exception Caught: #{e}"
         read!
@@ -36,7 +43,9 @@ module Unleash
     # rename to refresh_from_server!  ??
     def fetch
       Unleash.logger.debug "fetch()"
-      response = Unleash::Util::Http.get(Unleash.configuration.fetch_toggles_url, etag)
+      return if Unleash.configuration.disable_client
+
+      response = Unleash::Util::Http.get(Unleash.configuration.fetch_toggles_uri, etag)
 
       if response.code == '304'
         Unleash.logger.debug "No changes according to the unleash server, nothing to do."
@@ -46,14 +55,7 @@ module Unleash
       end
 
       self.etag = response['ETag']
-      response_hash = JSON.parse(response.body)
-
-      if response_hash['version'] >= 1
-        features = response_hash['features']
-      else
-        raise NotImplemented, "Version of features provided by unleash server" \
-          " is unsupported by this client."
-      end
+      features = get_features(response.body)
 
       # always synchronize with the local cache when fetching:
       synchronize_with_local_cache!(features)
@@ -64,24 +66,20 @@ module Unleash
 
     def save!
       Unleash.logger.debug "Will save toggles to disk now"
-      begin
-        backup_file = Unleash.configuration.backup_file
-        backup_file_tmp = "#{backup_file}.tmp"
 
-        self.toggle_lock.synchronize do
-          file = File.open(backup_file_tmp, "w")
+      backup_file = Unleash.configuration.backup_file
+      backup_file_tmp = "#{backup_file}.tmp"
+
+      self.toggle_lock.synchronize do
+        File.open(backup_file_tmp, "w") do |file|
           file.write(self.toggle_cache.to_json)
-          file.close
-          File.rename(backup_file_tmp, backup_file)
         end
-      rescue StandardError => e
-        # This is not really the end of the world. Swallowing the exception.
-        Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
-        Unleash.logger.error "stacktrace: #{e.backtrace}"
-      ensure
-        file&.close if defined?(file)
-        self.toggle_lock.unlock if self.toggle_lock.locked?
+        File.rename(backup_file_tmp, backup_file)
       end
+    rescue StandardError => e
+      # This is not really the end of the world. Swallowing the exception.
+      Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
+      Unleash.logger.error "stacktrace: #{e.backtrace}"
     end
 
     private
@@ -98,32 +96,54 @@ module Unleash
     end
 
     def update_running_client!
-      if Unleash.toggles != self.toggles
+      if Unleash.toggles != self.toggles["features"] || Unleash.segment_cache != self.toggles["segments"]
         Unleash.logger.info "Updating toggles to main client, there has been a change in the server."
-        Unleash.toggles = self.toggles
+        Unleash.toggles = self.toggles["features"]
+        Unleash.segment_cache = self.toggles["segments"]
       end
     end
 
     def read!
       Unleash.logger.debug "read!()"
-      return nil unless File.exist?(Unleash.configuration.backup_file)
+      backup_file = Unleash.configuration.backup_file
+      return nil unless File.exist?(backup_file)
 
-      begin
-        file = File.new(Unleash.configuration.backup_file, "r")
-        file_content = file.read
-
-        backup_as_hash = JSON.parse(file_content)
-        synchronize_with_local_cache!(backup_as_hash)
-        update_running_client!
-      rescue IOError => e
-        Unleash.logger.error "Unable to read the backup_file: #{e}"
-      rescue JSON::ParserError => e
-        Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
-      rescue StandardError => e
-        Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
-      ensure
-        file&.close
+      backup_as_hash = JSON.parse(File.read(backup_file))
+      synchronize_with_local_cache!(backup_as_hash)
+      update_running_client!
+    rescue IOError => e
+      Unleash.logger.error "Unable to read the backup_file: #{e}"
+    rescue JSON::ParserError => e
+      Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
+    rescue StandardError => e
+      Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
+    end
+
+    def bootstrap
+      bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles
+      synchronize_with_local_cache! get_features bootstrap_payload
+      update_running_client!
+
+      # reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again
+      Unleash.configuration.bootstrap_config = nil
+    end
+
+    def build_segment_map(segments_array)
+      return {} if segments_array.nil?
+
+      segments_array.map{ |segment| [segment["id"], segment] }.to_h
+    end
+
+    # @param response_body [String]
+    def get_features(response_body)
+      response_hash = JSON.parse(response_body)
+
+      if response_hash['version'] >= 1
+        return { "features" => response_hash["features"], "segments" => build_segment_map(response_hash["segments"]) }
       end
+
+      raise NotImplemented, "Version of features provided by unleash server" \
+        " is unsupported by this client."
     end
   end
 end
diff --git a/lib/unleash/util/http.rb b/lib/unleash/util/http.rb
index 343bee9..57c2e34 100644
--- a/lib/unleash/util/http.rb
+++ b/lib/unleash/util/http.rb
@@ -4,17 +4,15 @@ require 'uri'
 module Unleash
   module Util
     module Http
-      def self.get(url, etag = nil)
-        uri = URI(url)
+      def self.get(uri, etag = nil, headers_override = nil)
         http = http_connection(uri)
 
-        request = Net::HTTP::Get.new(uri.request_uri, http_headers(etag))
+        request = Net::HTTP::Get.new(uri.request_uri, http_headers(etag, headers_override))
 
         http.request(request)
       end
 
-      def self.post(url, body)
-        uri = URI(url)
+      def self.post(uri, body)
         http = http_connection(uri)
 
         request = Net::HTTP::Post.new(uri.request_uri, http_headers)
@@ -32,10 +30,13 @@ module Unleash
         http
       end
 
-      def self.http_headers(etag = nil)
+      # @param etag [String, nil]
+      # @param headers_override [Hash, nil]
+      def self.http_headers(etag = nil, headers_override = nil)
         Unleash.logger.debug "ETag: #{etag}" unless etag.nil?
 
         headers = (Unleash.configuration.http_headers || {}).dup
+        headers = headers_override if headers_override.is_a?(Hash)
         headers['Content-Type'] = 'application/json'
         headers['If-None-Match'] = etag unless etag.nil?
 
diff --git a/lib/unleash/variant_definition.rb b/lib/unleash/variant_definition.rb
index a4c9910..d9cecad 100644
--- a/lib/unleash/variant_definition.rb
+++ b/lib/unleash/variant_definition.rb
@@ -2,13 +2,13 @@ require 'unleash/variant_override'
 
 module Unleash
   class VariantDefinition
-    attr_accessor :name, :weight, :payload, :overrides
+    attr_accessor :name, :weight, :payload, :overrides, :stickiness
 
-    def initialize(name, weight = 0, payload = nil, overrides = [])
+    def initialize(name, weight = 0, payload = nil, stickiness = nil, overrides = []) # rubocop:disable Metrics/ParameterLists
       self.name = name
       self.weight = weight
       self.payload = payload
-      # self.overrides = overrides
+      self.stickiness = stickiness
       self.overrides = (overrides || [])
         .select{ |v| v.is_a?(Hash) && v.has_key?('contextName') }
         .map{ |v| VariantOverride.new(v.fetch('contextName', ''), v.fetch('values', [])) } || []
@@ -19,7 +19,8 @@ module Unleash
     end
 
     def to_s
-      "<VariantDefinition: name=#{self.name},weight=#{self.weight},payload=#{self.payload},overrides=#{self.overrides}>"
+      "<VariantDefinition: name=#{self.name},weight=#{self.weight},payload=#{self.payload},stickiness=#{self.stickiness}" \
+          ",overrides=#{self.overrides}>"
     end
   end
 end
diff --git a/lib/unleash/variant_override.rb b/lib/unleash/variant_override.rb
index df1b8f1..bac429a 100644
--- a/lib/unleash/variant_override.rb
+++ b/lib/unleash/variant_override.rb
@@ -14,7 +14,7 @@ module Unleash
     end
 
     def matches_context?(context)
-      raise ArgumentError, 'context must be of class Unleash::Context' unless context.class.name == 'Unleash::Context'
+      raise ArgumentError, 'context must be of class Unleash::Context' unless context.instance_of?(Unleash::Context)
 
       context_value =
         case self.context_name
diff --git a/lib/unleash/version.rb b/lib/unleash/version.rb
index 927216c..0497a7d 100644
--- a/lib/unleash/version.rb
+++ b/lib/unleash/version.rb
@@ -1,3 +1,3 @@
 module Unleash
-  VERSION = "3.2.5".freeze
+  VERSION = "4.4.2".freeze
 end
diff --git a/unleash-client.gemspec b/unleash-client.gemspec
index 4b18539..d577064 100644
--- a/unleash-client.gemspec
+++ b/unleash-client.gemspec
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
   spec.add_development_dependency "rspec-json_expectations", "~> 2.2"
   spec.add_development_dependency "webmock", "~> 3.8"
 
-  spec.add_development_dependency "coveralls", "~> 0.8"
-  spec.add_development_dependency "rubocop", "~> 0.80"
+  spec.add_development_dependency "rubocop", "~> 1.28.2"
+  spec.add_development_dependency "simplecov", "~> 0.21.2"
+  spec.add_development_dependency "simplecov-lcov", "~> 0.8.0"
 end

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/doc/ruby-unleash/examples/default-toggles.json
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/activation_strategy.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/bootstrap/configuration.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/bootstrap/handler.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/bootstrap/provider/base.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/bootstrap/provider/from_file.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/bootstrap/provider/from_url.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/client.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/configuration.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/constraint.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/context.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/feature_toggle.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/metrics.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/metrics_reporter.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/scheduled_executor.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/strategies.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/strategy/application_hostname.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/strategy/base.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/strategy/default.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/strategy/flexible_rollout.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/strategy/gradual_rollout_random.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/strategy/gradual_rollout_sessionid.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/strategy/gradual_rollout_userid.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/strategy/remote_address.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/strategy/user_with_id.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/strategy/util.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/toggle_fetcher.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/util/http.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/variant.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/variant_definition.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/variant_override.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/lib/unleash/version.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/unleash-4.4.2.gemspec
-rwxr-xr-x  root/root   /usr/share/doc/ruby-unleash/examples/bootstrap.rb
-rwxr-xr-x  root/root   /usr/share/rubygems-integration/all/gems/unleash-4.4.2/bin/unleash-client

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/activation_strategy.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/client.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/configuration.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/constraint.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/context.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/feature_toggle.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/metrics.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/metrics_reporter.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/scheduled_executor.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/strategy/application_hostname.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/strategy/base.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/strategy/default.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/strategy/flexible_rollout.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/strategy/gradual_rollout_random.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/strategy/gradual_rollout_sessionid.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/strategy/gradual_rollout_userid.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/strategy/remote_address.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/strategy/user_with_id.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/strategy/util.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/toggle_fetcher.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/util/http.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/variant.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/variant_definition.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/variant_override.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/lib/unleash/version.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/unleash-3.2.5.gemspec
-rwxr-xr-x  root/root   /usr/share/rubygems-integration/all/gems/unleash-3.2.5/bin/unleash-client

No differences were encountered in the control files

More details

Full run details