unleash 3.2.2 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/.github/workflows/add-to-project.yml +14 -0
 - data/.github/workflows/pull_request.yml +79 -0
 - data/.gitignore +5 -1
 - data/.rubocop.yml +114 -6
 - data/README.md +263 -27
 - data/bin/unleash-client +15 -5
 - data/examples/bootstrap.rb +51 -0
 - data/examples/default-toggles.json +42 -0
 - data/examples/simple.rb +5 -4
 - data/lib/unleash/bootstrap/configuration.rb +25 -0
 - data/lib/unleash/bootstrap/handler.rb +22 -0
 - data/lib/unleash/bootstrap/provider/base.rb +14 -0
 - data/lib/unleash/bootstrap/provider/from_file.rb +14 -0
 - data/lib/unleash/bootstrap/provider/from_url.rb +19 -0
 - data/lib/unleash/client.rb +27 -17
 - data/lib/unleash/configuration.rb +41 -19
 - data/lib/unleash/constraint.rb +88 -10
 - data/lib/unleash/context.rb +3 -2
 - data/lib/unleash/feature_toggle.rb +26 -19
 - data/lib/unleash/metrics_reporter.rb +18 -4
 - data/lib/unleash/scheduled_executor.rb +5 -2
 - data/lib/unleash/strategy/application_hostname.rb +1 -0
 - data/lib/unleash/strategy/flexible_rollout.rb +5 -5
 - data/lib/unleash/strategy/remote_address.rb +17 -1
 - data/lib/unleash/toggle_fetcher.rb +51 -42
 - data/lib/unleash/util/http.rb +7 -6
 - data/lib/unleash/variant_definition.rb +5 -4
 - data/lib/unleash/version.rb +1 -1
 - data/lib/unleash.rb +0 -5
 - data/unleash-client.gemspec +3 -2
 - metadata +35 -13
 - data/.travis.yml +0 -15
 
| 
         @@ -0,0 +1,19 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'unleash/bootstrap/provider/base'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Unleash
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Bootstrap
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Provider
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class FromUrl < Base
         
     | 
| 
      
 7 
     | 
    
         
            +
                    # @param url [String]
         
     | 
| 
      
 8 
     | 
    
         
            +
                    # @param headers [Hash, nil] HTTP headers to use. If not set, the unleash client SDK ones will be used.
         
     | 
| 
      
 9 
     | 
    
         
            +
                    def self.read(url, headers = nil)
         
     | 
| 
      
 10 
     | 
    
         
            +
                      response = Unleash::Util::Http.get(URI.parse(url), nil, headers)
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                      return nil if response.code != '200'
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                      response.body
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/unleash/client.rb
    CHANGED
    
    | 
         @@ -18,8 +18,9 @@ module Unleash 
     | 
|
| 
       18 
18 
     | 
    
         
             
                  Unleash.logger = Unleash.configuration.logger.clone
         
     | 
| 
       19 
19 
     | 
    
         
             
                  Unleash.logger.level = Unleash.configuration.log_level
         
     | 
| 
       20 
20 
     | 
    
         | 
| 
      
 21 
     | 
    
         
            +
                  Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
         
     | 
| 
       21 
22 
     | 
    
         
             
                  if Unleash.configuration.disable_client
         
     | 
| 
       22 
     | 
    
         
            -
                    Unleash.logger.warn "Unleash::Client is disabled! Will only return default results!"
         
     | 
| 
      
 23 
     | 
    
         
            +
                    Unleash.logger.warn "Unleash::Client is disabled! Will only return default (or bootstrapped if available) results!"
         
     | 
| 
       23 
24 
     | 
    
         
             
                    return
         
     | 
| 
       24 
25 
     | 
    
         
             
                  end
         
     | 
| 
       25 
26 
     | 
    
         | 
| 
         @@ -28,13 +29,14 @@ module Unleash 
     | 
|
| 
       28 
29 
     | 
    
         
             
                  start_metrics unless Unleash.configuration.disable_metrics
         
     | 
| 
       29 
30 
     | 
    
         
             
                end
         
     | 
| 
       30 
31 
     | 
    
         | 
| 
       31 
     | 
    
         
            -
                def is_enabled?(feature, context = nil,  
     | 
| 
      
 32 
     | 
    
         
            +
                def is_enabled?(feature, context = nil, default_value_param = false, &fallback_blk)
         
     | 
| 
       32 
33 
     | 
    
         
             
                  Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}"
         
     | 
| 
       33 
34 
     | 
    
         | 
| 
       34 
     | 
    
         
            -
                  if  
     | 
| 
       35 
     | 
    
         
            -
             
     | 
| 
       36 
     | 
    
         
            -
             
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
      
 35 
     | 
    
         
            +
                  default_value = if block_given?
         
     | 
| 
      
 36 
     | 
    
         
            +
                                    default_value_param || !!fallback_blk.call(feature, context)
         
     | 
| 
      
 37 
     | 
    
         
            +
                                  else
         
     | 
| 
      
 38 
     | 
    
         
            +
                                    default_value_param
         
     | 
| 
      
 39 
     | 
    
         
            +
                                  end
         
     | 
| 
       38 
40 
     | 
    
         | 
| 
       39 
41 
     | 
    
         
             
                  toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
         
     | 
| 
       40 
42 
     | 
    
         | 
| 
         @@ -45,7 +47,7 @@ module Unleash 
     | 
|
| 
       45 
47 
     | 
    
         | 
| 
       46 
48 
     | 
    
         
             
                  toggle = Unleash::FeatureToggle.new(toggle_as_hash)
         
     | 
| 
       47 
49 
     | 
    
         | 
| 
       48 
     | 
    
         
            -
                  toggle.is_enabled?(context 
     | 
| 
      
 50 
     | 
    
         
            +
                  toggle.is_enabled?(context)
         
     | 
| 
       49 
51 
     | 
    
         
             
                end
         
     | 
| 
       50 
52 
     | 
    
         | 
| 
       51 
53 
     | 
    
         
             
                # enabled? is a more ruby idiomatic method name than is_enabled?
         
     | 
| 
         @@ -56,19 +58,19 @@ module Unleash 
     | 
|
| 
       56 
58 
     | 
    
         
             
                  yield(blk) if is_enabled?(feature, context, default_value)
         
     | 
| 
       57 
59 
     | 
    
         
             
                end
         
     | 
| 
       58 
60 
     | 
    
         | 
| 
       59 
     | 
    
         
            -
                def get_variant(feature, context =  
     | 
| 
      
 61 
     | 
    
         
            +
                def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant)
         
     | 
| 
       60 
62 
     | 
    
         
             
                  Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"
         
     | 
| 
       61 
63 
     | 
    
         | 
| 
       62 
64 
     | 
    
         
             
                  if Unleash.configuration.disable_client
         
     | 
| 
       63 
     | 
    
         
            -
                    Unleash.logger.debug "unleash_client is disabled! Always returning #{ 
     | 
| 
       64 
     | 
    
         
            -
                    return fallback_variant 
     | 
| 
      
 65 
     | 
    
         
            +
                    Unleash.logger.debug "unleash_client is disabled! Always returning #{fallback_variant} for feature #{feature}!"
         
     | 
| 
      
 66 
     | 
    
         
            +
                    return fallback_variant
         
     | 
| 
       65 
67 
     | 
    
         
             
                  end
         
     | 
| 
       66 
68 
     | 
    
         | 
| 
       67 
69 
     | 
    
         
             
                  toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
         
     | 
| 
       68 
70 
     | 
    
         | 
| 
       69 
71 
     | 
    
         
             
                  if toggle_as_hash.nil?
         
     | 
| 
       70 
72 
     | 
    
         
             
                    Unleash.logger.debug "Unleash::Client.get_variant feature: #{feature} not found"
         
     | 
| 
       71 
     | 
    
         
            -
                    return fallback_variant 
     | 
| 
      
 73 
     | 
    
         
            +
                    return fallback_variant
         
     | 
| 
       72 
74 
     | 
    
         
             
                  end
         
     | 
| 
       73 
75 
     | 
    
         | 
| 
       74 
76 
     | 
    
         
             
                  toggle = Unleash::FeatureToggle.new(toggle_as_hash)
         
     | 
| 
         @@ -76,7 +78,7 @@ module Unleash 
     | 
|
| 
       76 
78 
     | 
    
         | 
| 
       77 
79 
     | 
    
         
             
                  if variant.nil?
         
     | 
| 
       78 
80 
     | 
    
         
             
                    Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found"
         
     | 
| 
       79 
     | 
    
         
            -
                    return fallback_variant 
     | 
| 
      
 81 
     | 
    
         
            +
                    return fallback_variant
         
     | 
| 
       80 
82 
     | 
    
         
             
                  end
         
     | 
| 
       81 
83 
     | 
    
         | 
| 
       82 
84 
     | 
    
         
             
                  # TODO: Add to README: name, payload, enabled (bool)
         
     | 
| 
         @@ -88,7 +90,7 @@ module Unleash 
     | 
|
| 
       88 
90 
     | 
    
         
             
                def shutdown
         
     | 
| 
       89 
91 
     | 
    
         
             
                  unless Unleash.configuration.disable_client
         
     | 
| 
       90 
92 
     | 
    
         
             
                    Unleash.toggle_fetcher.save!
         
     | 
| 
       91 
     | 
    
         
            -
                    Unleash.reporter. 
     | 
| 
      
 93 
     | 
    
         
            +
                    Unleash.reporter.post unless Unleash.configuration.disable_metrics
         
     | 
| 
       92 
94 
     | 
    
         
             
                    shutdown!
         
     | 
| 
       93 
95 
     | 
    
         
             
                  end
         
     | 
| 
       94 
96 
     | 
    
         
             
                end
         
     | 
| 
         @@ -115,11 +117,11 @@ module Unleash 
     | 
|
| 
       115 
117 
     | 
    
         
             
                end
         
     | 
| 
       116 
118 
     | 
    
         | 
| 
       117 
119 
     | 
    
         
             
                def start_toggle_fetcher
         
     | 
| 
       118 
     | 
    
         
            -
                  Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
         
     | 
| 
       119 
120 
     | 
    
         
             
                  self.fetcher_scheduled_executor = Unleash::ScheduledExecutor.new(
         
     | 
| 
       120 
121 
     | 
    
         
             
                    'ToggleFetcher',
         
     | 
| 
       121 
122 
     | 
    
         
             
                    Unleash.configuration.refresh_interval,
         
     | 
| 
       122 
     | 
    
         
            -
                    Unleash.configuration.retry_limit
         
     | 
| 
      
 123 
     | 
    
         
            +
                    Unleash.configuration.retry_limit,
         
     | 
| 
      
 124 
     | 
    
         
            +
                    first_fetch_is_eager
         
     | 
| 
       123 
125 
     | 
    
         
             
                  )
         
     | 
| 
       124 
126 
     | 
    
         
             
                  self.fetcher_scheduled_executor.run do
         
     | 
| 
       125 
127 
     | 
    
         
             
                    Unleash.toggle_fetcher.fetch
         
     | 
| 
         @@ -135,7 +137,7 @@ module Unleash 
     | 
|
| 
       135 
137 
     | 
    
         
             
                    Unleash.configuration.retry_limit
         
     | 
| 
       136 
138 
     | 
    
         
             
                  )
         
     | 
| 
       137 
139 
     | 
    
         
             
                  self.metrics_scheduled_executor.run do
         
     | 
| 
       138 
     | 
    
         
            -
                    Unleash.reporter. 
     | 
| 
      
 140 
     | 
    
         
            +
                    Unleash.reporter.post
         
     | 
| 
       139 
141 
     | 
    
         
             
                  end
         
     | 
| 
       140 
142 
     | 
    
         
             
                end
         
     | 
| 
       141 
143 
     | 
    
         | 
| 
         @@ -144,12 +146,20 @@ module Unleash 
     | 
|
| 
       144 
146 
     | 
    
         | 
| 
       145 
147 
     | 
    
         
             
                  # Send the request, if possible
         
     | 
| 
       146 
148 
     | 
    
         
             
                  begin
         
     | 
| 
       147 
     | 
    
         
            -
                    response = Unleash::Util::Http.post(Unleash.configuration. 
     | 
| 
      
 149 
     | 
    
         
            +
                    response = Unleash::Util::Http.post(Unleash.configuration.client_register_uri, info.to_json)
         
     | 
| 
       148 
150 
     | 
    
         
             
                  rescue StandardError => e
         
     | 
| 
       149 
151 
     | 
    
         
             
                    Unleash.logger.error "unable to register client with unleash server due to exception #{e.class}:'#{e}'."
         
     | 
| 
       150 
152 
     | 
    
         
             
                    Unleash.logger.error "stacktrace: #{e.backtrace}"
         
     | 
| 
       151 
153 
     | 
    
         
             
                  end
         
     | 
| 
       152 
154 
     | 
    
         
             
                  Unleash.logger.debug "client registered: #{response}"
         
     | 
| 
       153 
155 
     | 
    
         
             
                end
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
                def disabled_variant
         
     | 
| 
      
 158 
     | 
    
         
            +
                  @disabled_variant ||= Unleash::FeatureToggle.disabled_variant
         
     | 
| 
      
 159 
     | 
    
         
            +
                end
         
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
                def first_fetch_is_eager
         
     | 
| 
      
 162 
     | 
    
         
            +
                  Unleash.configuration.use_bootstrap?
         
     | 
| 
      
 163 
     | 
    
         
            +
                end
         
     | 
| 
       154 
164 
     | 
    
         
             
              end
         
     | 
| 
       155 
165 
     | 
    
         
             
            end
         
     | 
| 
         @@ -1,5 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            require 'securerandom'
         
     | 
| 
       2 
2 
     | 
    
         
             
            require 'tmpdir'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'unleash/bootstrap/configuration'
         
     | 
| 
       3 
4 
     | 
    
         | 
| 
       4 
5 
     | 
    
         
             
            module Unleash
         
     | 
| 
       5 
6 
     | 
    
         
             
              class Configuration
         
     | 
| 
         @@ -8,6 +9,7 @@ module Unleash 
     | 
|
| 
       8 
9 
     | 
    
         
             
                  :app_name,
         
     | 
| 
       9 
10 
     | 
    
         
             
                  :environment,
         
     | 
| 
       10 
11 
     | 
    
         
             
                  :instance_id,
         
     | 
| 
      
 12 
     | 
    
         
            +
                  :project_name,
         
     | 
| 
       11 
13 
     | 
    
         
             
                  :custom_http_headers,
         
     | 
| 
       12 
14 
     | 
    
         
             
                  :disable_client,
         
     | 
| 
       13 
15 
     | 
    
         
             
                  :disable_metrics,
         
     | 
| 
         @@ -17,10 +19,11 @@ module Unleash 
     | 
|
| 
       17 
19 
     | 
    
         
             
                  :metrics_interval,
         
     | 
| 
       18 
20 
     | 
    
         
             
                  :backup_file,
         
     | 
| 
       19 
21 
     | 
    
         
             
                  :logger,
         
     | 
| 
       20 
     | 
    
         
            -
                  :log_level
         
     | 
| 
      
 22 
     | 
    
         
            +
                  :log_level,
         
     | 
| 
      
 23 
     | 
    
         
            +
                  :bootstrap_config
         
     | 
| 
       21 
24 
     | 
    
         | 
| 
       22 
25 
     | 
    
         
             
                def initialize(opts = {})
         
     | 
| 
       23 
     | 
    
         
            -
                   
     | 
| 
      
 26 
     | 
    
         
            +
                  validate_custom_http_headers!(opts[:custom_http_headers]) if opts.has_key?(:custom_http_headers)
         
     | 
| 
       24 
27 
     | 
    
         
             
                  set_defaults
         
     | 
| 
       25 
28 
     | 
    
         | 
| 
       26 
29 
     | 
    
         
             
                  initialize_default_logger if opts[:logger].nil?
         
     | 
| 
         @@ -37,53 +40,60 @@ module Unleash 
     | 
|
| 
       37 
40 
     | 
    
         
             
                  return if self.disable_client
         
     | 
| 
       38 
41 
     | 
    
         | 
| 
       39 
42 
     | 
    
         
             
                  raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? || self.url.nil?
         
     | 
| 
       40 
     | 
    
         
            -
             
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  validate_custom_http_headers!(self.custom_http_headers)
         
     | 
| 
       41 
45 
     | 
    
         
             
                end
         
     | 
| 
       42 
46 
     | 
    
         | 
| 
       43 
47 
     | 
    
         
             
                def refresh_backup_file!
         
     | 
| 
       44 
     | 
    
         
            -
                  self.backup_file = Dir.tmpdir  
     | 
| 
      
 48 
     | 
    
         
            +
                  self.backup_file = File.join(Dir.tmpdir, "unleash-#{app_name}-repo.json")
         
     | 
| 
       45 
49 
     | 
    
         
             
                end
         
     | 
| 
       46 
50 
     | 
    
         | 
| 
       47 
51 
     | 
    
         
             
                def http_headers
         
     | 
| 
       48 
52 
     | 
    
         
             
                  {
         
     | 
| 
       49 
53 
     | 
    
         
             
                    'UNLEASH-INSTANCEID' => self.instance_id,
         
     | 
| 
       50 
54 
     | 
    
         
             
                    'UNLEASH-APPNAME' => self.app_name
         
     | 
| 
       51 
     | 
    
         
            -
                  }.merge( 
     | 
| 
      
 55 
     | 
    
         
            +
                  }.merge!(generate_custom_http_headers)
         
     | 
| 
       52 
56 
     | 
    
         
             
                end
         
     | 
| 
       53 
57 
     | 
    
         | 
| 
       54 
     | 
    
         
            -
                def  
     | 
| 
       55 
     | 
    
         
            -
                  self. 
     | 
| 
      
 58 
     | 
    
         
            +
                def fetch_toggles_uri
         
     | 
| 
      
 59 
     | 
    
         
            +
                  uri = URI("#{self.url_stripped_of_slash}/client/features")
         
     | 
| 
      
 60 
     | 
    
         
            +
                  uri.query = "project=#{self.project_name}" unless self.project_name.nil?
         
     | 
| 
      
 61 
     | 
    
         
            +
                  uri
         
     | 
| 
       56 
62 
     | 
    
         
             
                end
         
     | 
| 
       57 
63 
     | 
    
         | 
| 
       58 
     | 
    
         
            -
                def  
     | 
| 
       59 
     | 
    
         
            -
                  self. 
     | 
| 
      
 64 
     | 
    
         
            +
                def client_metrics_uri
         
     | 
| 
      
 65 
     | 
    
         
            +
                  URI("#{self.url_stripped_of_slash}/client/metrics")
         
     | 
| 
       60 
66 
     | 
    
         
             
                end
         
     | 
| 
       61 
67 
     | 
    
         | 
| 
       62 
     | 
    
         
            -
                def  
     | 
| 
       63 
     | 
    
         
            -
                  self. 
     | 
| 
      
 68 
     | 
    
         
            +
                def client_register_uri
         
     | 
| 
      
 69 
     | 
    
         
            +
                  URI("#{self.url_stripped_of_slash}/client/register")
         
     | 
| 
       64 
70 
     | 
    
         
             
                end
         
     | 
| 
       65 
71 
     | 
    
         | 
| 
       66 
     | 
    
         
            -
                 
     | 
| 
      
 72 
     | 
    
         
            +
                def url_stripped_of_slash
         
     | 
| 
      
 73 
     | 
    
         
            +
                  self.url.delete_suffix '/'
         
     | 
| 
      
 74 
     | 
    
         
            +
                end
         
     | 
| 
       67 
75 
     | 
    
         | 
| 
       68 
     | 
    
         
            -
                def  
     | 
| 
       69 
     | 
    
         
            -
                   
     | 
| 
       70 
     | 
    
         
            -
                    raise ArgumentError, "custom_http_headers must be a hash."
         
     | 
| 
       71 
     | 
    
         
            -
                  end
         
     | 
| 
      
 76 
     | 
    
         
            +
                def use_bootstrap?
         
     | 
| 
      
 77 
     | 
    
         
            +
                  self.bootstrap_config&.valid?
         
     | 
| 
       72 
78 
     | 
    
         
             
                end
         
     | 
| 
       73 
79 
     | 
    
         | 
| 
      
 80 
     | 
    
         
            +
                private
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
       74 
82 
     | 
    
         
             
                def set_defaults
         
     | 
| 
       75 
83 
     | 
    
         
             
                  self.app_name         = nil
         
     | 
| 
       76 
84 
     | 
    
         
             
                  self.environment      = 'default'
         
     | 
| 
       77 
85 
     | 
    
         
             
                  self.url              = nil
         
     | 
| 
       78 
86 
     | 
    
         
             
                  self.instance_id      = SecureRandom.uuid
         
     | 
| 
      
 87 
     | 
    
         
            +
                  self.project_name     = nil
         
     | 
| 
       79 
88 
     | 
    
         
             
                  self.disable_client   = false
         
     | 
| 
       80 
89 
     | 
    
         
             
                  self.disable_metrics  = false
         
     | 
| 
       81 
     | 
    
         
            -
                  self.refresh_interval =  
     | 
| 
       82 
     | 
    
         
            -
                  self.metrics_interval =  
     | 
| 
      
 90 
     | 
    
         
            +
                  self.refresh_interval = 10
         
     | 
| 
      
 91 
     | 
    
         
            +
                  self.metrics_interval = 60
         
     | 
| 
       83 
92 
     | 
    
         
             
                  self.timeout          = 30
         
     | 
| 
       84 
     | 
    
         
            -
                  self.retry_limit      =  
     | 
| 
      
 93 
     | 
    
         
            +
                  self.retry_limit      = Float::INFINITY
         
     | 
| 
       85 
94 
     | 
    
         
             
                  self.backup_file      = nil
         
     | 
| 
       86 
95 
     | 
    
         
             
                  self.log_level        = Logger::WARN
         
     | 
| 
      
 96 
     | 
    
         
            +
                  self.bootstrap_config = nil
         
     | 
| 
       87 
97 
     | 
    
         | 
| 
       88 
98 
     | 
    
         
             
                  self.custom_http_headers = {}
         
     | 
| 
       89 
99 
     | 
    
         
             
                end
         
     | 
| 
         @@ -103,6 +113,18 @@ module Unleash 
     | 
|
| 
       103 
113 
     | 
    
         
             
                  self
         
     | 
| 
       104 
114 
     | 
    
         
             
                end
         
     | 
| 
       105 
115 
     | 
    
         | 
| 
      
 116 
     | 
    
         
            +
                def validate_custom_http_headers!(custom_http_headers)
         
     | 
| 
      
 117 
     | 
    
         
            +
                  return if custom_http_headers.is_a?(Hash) || custom_http_headers.respond_to?(:call)
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                  raise ArgumentError, "custom_http_headers must be a Hash or a Proc."
         
     | 
| 
      
 120 
     | 
    
         
            +
                end
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
                def generate_custom_http_headers
         
     | 
| 
      
 123 
     | 
    
         
            +
                  return self.custom_http_headers.call if self.custom_http_headers.respond_to?(:call)
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
                  self.custom_http_headers
         
     | 
| 
      
 126 
     | 
    
         
            +
                end
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
       106 
128 
     | 
    
         
             
                def set_option(opt, val)
         
     | 
| 
       107 
129 
     | 
    
         
             
                  __send__("#{opt}=", val)
         
     | 
| 
       108 
130 
     | 
    
         
             
                rescue NoMethodError
         
     | 
    
        data/lib/unleash/constraint.rb
    CHANGED
    
    | 
         @@ -1,26 +1,104 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'date'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
       1 
3 
     | 
    
         
             
            module Unleash
         
     | 
| 
       2 
4 
     | 
    
         
             
              class Constraint
         
     | 
| 
       3 
     | 
    
         
            -
                attr_accessor :context_name, :operator, : 
     | 
| 
      
 5 
     | 
    
         
            +
                attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                OPERATORS = {
         
     | 
| 
      
 8 
     | 
    
         
            +
                  IN: ->(context_v, constraint_v){ constraint_v.include? context_v },
         
     | 
| 
      
 9 
     | 
    
         
            +
                  NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v },
         
     | 
| 
      
 10 
     | 
    
         
            +
                  STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
         
     | 
| 
      
 11 
     | 
    
         
            +
                  STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
         
     | 
| 
      
 12 
     | 
    
         
            +
                  STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
         
     | 
| 
      
 13 
     | 
    
         
            +
                  NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } },
         
     | 
| 
      
 14 
     | 
    
         
            +
                  NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } },
         
     | 
| 
      
 15 
     | 
    
         
            +
                  NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } },
         
     | 
| 
      
 16 
     | 
    
         
            +
                  NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } },
         
     | 
| 
      
 17 
     | 
    
         
            +
                  NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } },
         
     | 
| 
      
 18 
     | 
    
         
            +
                  DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } },
         
     | 
| 
      
 19 
     | 
    
         
            +
                  DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
         
     | 
| 
      
 20 
     | 
    
         
            +
                  SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
         
     | 
| 
      
 21 
     | 
    
         
            +
                  SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } },
         
     | 
| 
      
 22 
     | 
    
         
            +
                  SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } }
         
     | 
| 
      
 23 
     | 
    
         
            +
                }.freeze
         
     | 
| 
       4 
24 
     | 
    
         | 
| 
       5 
     | 
    
         
            -
                 
     | 
| 
      
 25 
     | 
    
         
            +
                LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
         
     | 
| 
       6 
26 
     | 
    
         | 
| 
       7 
     | 
    
         
            -
                def initialize(context_name, operator,  
     | 
| 
      
 27 
     | 
    
         
            +
                def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
         
     | 
| 
       8 
28 
     | 
    
         
             
                  raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
         
     | 
| 
       9 
     | 
    
         
            -
                  raise ArgumentError, "operator does not hold a valid value:" +  
     | 
| 
       10 
     | 
    
         
            -
             
     | 
| 
      
 29 
     | 
    
         
            +
                  raise ArgumentError, "operator does not hold a valid value:" + OPERATORS.keys unless OPERATORS.include? operator.to_sym
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                  self.validate_constraint_value_type(operator.to_sym, value)
         
     | 
| 
       11 
32 
     | 
    
         | 
| 
       12 
33 
     | 
    
         
             
                  self.context_name = context_name
         
     | 
| 
       13 
     | 
    
         
            -
                  self.operator = operator
         
     | 
| 
       14 
     | 
    
         
            -
                  self. 
     | 
| 
      
 34 
     | 
    
         
            +
                  self.operator = operator.to_sym
         
     | 
| 
      
 35 
     | 
    
         
            +
                  self.value = value
         
     | 
| 
      
 36 
     | 
    
         
            +
                  self.inverted = !!inverted
         
     | 
| 
      
 37 
     | 
    
         
            +
                  self.case_insensitive = !!case_insensitive
         
     | 
| 
       15 
38 
     | 
    
         
             
                end
         
     | 
| 
       16 
39 
     | 
    
         | 
| 
       17 
40 
     | 
    
         
             
                def matches_context?(context)
         
     | 
| 
       18 
     | 
    
         
            -
                  Unleash.logger.debug "Unleash::Constraint matches_context?  
     | 
| 
      
 41 
     | 
    
         
            +
                  Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" \
         
     | 
| 
       19 
42 
     | 
    
         
             
                    " #{context.get_by_name(self.context_name)} "
         
     | 
| 
      
 43 
     | 
    
         
            +
                  match = matches_constraint?(context)
         
     | 
| 
      
 44 
     | 
    
         
            +
                  self.inverted ? !match : match
         
     | 
| 
      
 45 
     | 
    
         
            +
                rescue KeyError
         
     | 
| 
      
 46 
     | 
    
         
            +
                  Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
         
     | 
| 
      
 47 
     | 
    
         
            +
                  found on the context"
         
     | 
| 
      
 48 
     | 
    
         
            +
                  false
         
     | 
| 
      
 49 
     | 
    
         
            +
                end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                def self.on_valid_date(val1, val2)
         
     | 
| 
      
 52 
     | 
    
         
            +
                  val1 = DateTime.parse(val1)
         
     | 
| 
      
 53 
     | 
    
         
            +
                  val2 = DateTime.parse(val2)
         
     | 
| 
      
 54 
     | 
    
         
            +
                  yield(val1, val2)
         
     | 
| 
      
 55 
     | 
    
         
            +
                rescue ArgumentError
         
     | 
| 
      
 56 
     | 
    
         
            +
                  Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
         
     | 
| 
      
 57 
     | 
    
         
            +
                  or constraint_value (#{val2}) into a date. Returning false!"
         
     | 
| 
      
 58 
     | 
    
         
            +
                  false
         
     | 
| 
      
 59 
     | 
    
         
            +
                end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                def self.on_valid_float(val1, val2)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  val1 = Float(val1)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  val2 = Float(val2)
         
     | 
| 
      
 64 
     | 
    
         
            +
                  yield(val1, val2)
         
     | 
| 
      
 65 
     | 
    
         
            +
                rescue ArgumentError
         
     | 
| 
      
 66 
     | 
    
         
            +
                  Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
         
     | 
| 
      
 67 
     | 
    
         
            +
                  or constraint_value (#{val2}) into a number. Returning false!"
         
     | 
| 
      
 68 
     | 
    
         
            +
                  false
         
     | 
| 
      
 69 
     | 
    
         
            +
                end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                def self.on_valid_version(val1, val2)
         
     | 
| 
      
 72 
     | 
    
         
            +
                  val1 = Gem::Version.new(val1)
         
     | 
| 
      
 73 
     | 
    
         
            +
                  val2 = Gem::Version.new(val2)
         
     | 
| 
      
 74 
     | 
    
         
            +
                  yield(val1, val2)
         
     | 
| 
      
 75 
     | 
    
         
            +
                rescue ArgumentError
         
     | 
| 
      
 76 
     | 
    
         
            +
                  Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
         
     | 
| 
      
 77 
     | 
    
         
            +
                  or constraint_value (#{val2}) into a version. Return false!"
         
     | 
| 
      
 78 
     | 
    
         
            +
                  false
         
     | 
| 
      
 79 
     | 
    
         
            +
                end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                # This should be a private method but for some reason this fails on Ruby 2.5
         
     | 
| 
      
 82 
     | 
    
         
            +
                def validate_constraint_value_type(operator, value)
         
     | 
| 
      
 83 
     | 
    
         
            +
                  raise ArgumentError, "context_name is not an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  raise ArgumentError, "context_name is not a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
         
     | 
| 
      
 85 
     | 
    
         
            +
                end
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                private
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                def matches_constraint?(context)
         
     | 
| 
      
 90 
     | 
    
         
            +
                  unless OPERATORS.include?(self.operator)
         
     | 
| 
      
 91 
     | 
    
         
            +
                    Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
         
     | 
| 
      
 92 
     | 
    
         
            +
                    false
         
     | 
| 
      
 93 
     | 
    
         
            +
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                  v = self.value.dup
         
     | 
| 
      
 96 
     | 
    
         
            +
                  context_value = context.get_by_name(self.context_name)
         
     | 
| 
       20 
97 
     | 
    
         | 
| 
       21 
     | 
    
         
            -
                   
     | 
| 
      
 98 
     | 
    
         
            +
                  v.map!(&:upcase) if self.case_insensitive
         
     | 
| 
      
 99 
     | 
    
         
            +
                  context_value.upcase! if self.case_insensitive
         
     | 
| 
       22 
100 
     | 
    
         | 
| 
       23 
     | 
    
         
            -
                  operator  
     | 
| 
      
 101 
     | 
    
         
            +
                  OPERATORS[self.operator].call(context_value, v)
         
     | 
| 
       24 
102 
     | 
    
         
             
                end
         
     | 
| 
       25 
103 
     | 
    
         
             
              end
         
     | 
| 
       26 
104 
     | 
    
         
             
            end
         
     | 
    
        data/lib/unleash/context.rb
    CHANGED
    
    | 
         @@ -1,6 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module Unleash
         
     | 
| 
       2 
2 
     | 
    
         
             
              class Context
         
     | 
| 
       3 
     | 
    
         
            -
                ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address].freeze
         
     | 
| 
      
 3 
     | 
    
         
            +
                ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address, :current_time].freeze
         
     | 
| 
       4 
4 
     | 
    
         | 
| 
       5 
5 
     | 
    
         
             
                attr_accessor(*[ATTRS, :properties].flatten)
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
         @@ -12,6 +12,7 @@ module Unleash 
     | 
|
| 
       12 
12 
     | 
    
         
             
                  self.user_id     = value_for('userId', params)
         
     | 
| 
       13 
13 
     | 
    
         
             
                  self.session_id  = value_for('sessionId', params)
         
     | 
| 
       14 
14 
     | 
    
         
             
                  self.remote_address = value_for('remoteAddress', params)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
         
     | 
| 
       15 
16 
     | 
    
         | 
| 
       16 
17 
     | 
    
         
             
                  properties = value_for('properties', params)
         
     | 
| 
       17 
18 
     | 
    
         
             
                  self.properties = properties.is_a?(Hash) ? properties.transform_keys(&:to_sym) : {}
         
     | 
| 
         @@ -28,7 +29,7 @@ module Unleash 
     | 
|
| 
       28 
29 
     | 
    
         
             
                  if ATTRS.include? normalized_name
         
     | 
| 
       29 
30 
     | 
    
         
             
                    self.send(normalized_name)
         
     | 
| 
       30 
31 
     | 
    
         
             
                  else
         
     | 
| 
       31 
     | 
    
         
            -
                    self.properties.fetch(normalized_name)
         
     | 
| 
      
 32 
     | 
    
         
            +
                    self.properties.fetch(normalized_name, nil) || self.properties.fetch(name.to_sym)
         
     | 
| 
       32 
33 
     | 
    
         
             
                  end
         
     | 
| 
       33 
34 
     | 
    
         
             
                end
         
     | 
| 
       34 
35 
     | 
    
         | 
| 
         @@ -23,8 +23,8 @@ module Unleash 
     | 
|
| 
       23 
23 
     | 
    
         
             
                  "<FeatureToggle: name=#{name},enabled=#{enabled},strategies=#{strategies},variant_definitions=#{variant_definitions}>"
         
     | 
| 
       24 
24 
     | 
    
         
             
                end
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
       26 
     | 
    
         
            -
                def is_enabled?(context 
     | 
| 
       27 
     | 
    
         
            -
                  result = am_enabled?(context 
     | 
| 
      
 26 
     | 
    
         
            +
                def is_enabled?(context)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  result = am_enabled?(context)
         
     | 
| 
       28 
28 
     | 
    
         | 
| 
       29 
29 
     | 
    
         
             
                  choice = result ? :yes : :no
         
     | 
| 
       30 
30 
     | 
    
         
             
                  Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
         
     | 
| 
         @@ -32,25 +32,32 @@ module Unleash 
     | 
|
| 
       32 
32 
     | 
    
         
             
                  result
         
     | 
| 
       33 
33 
     | 
    
         
             
                end
         
     | 
| 
       34 
34 
     | 
    
         | 
| 
       35 
     | 
    
         
            -
                def get_variant(context, fallback_variant = disabled_variant)
         
     | 
| 
      
 35 
     | 
    
         
            +
                def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant)
         
     | 
| 
       36 
36 
     | 
    
         
             
                  raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant'
         
     | 
| 
       37 
37 
     | 
    
         | 
| 
       38 
38 
     | 
    
         
             
                  context = ensure_valid_context(context)
         
     | 
| 
       39 
39 
     | 
    
         | 
| 
       40 
     | 
    
         
            -
                  return disabled_variant unless self.enabled && am_enabled?(context 
     | 
| 
       41 
     | 
    
         
            -
                  return disabled_variant if sum_variant_defs_weights <= 0
         
     | 
| 
      
 40 
     | 
    
         
            +
                  return Unleash::FeatureToggle.disabled_variant unless self.enabled && am_enabled?(context)
         
     | 
| 
      
 41 
     | 
    
         
            +
                  return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
         
     | 
| 
       42 
42 
     | 
    
         | 
| 
       43 
     | 
    
         
            -
                  variant = variant_from_override_match(context)
         
     | 
| 
       44 
     | 
    
         
            -
                  variant = variant_from_weights(context) if variant.nil?
         
     | 
| 
      
 43 
     | 
    
         
            +
                  variant = variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
         
     | 
| 
       45 
44 
     | 
    
         | 
| 
       46 
45 
     | 
    
         
             
                  Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics
         
     | 
| 
       47 
46 
     | 
    
         
             
                  variant
         
     | 
| 
       48 
47 
     | 
    
         
             
                end
         
     | 
| 
       49 
48 
     | 
    
         | 
| 
      
 49 
     | 
    
         
            +
                def self.disabled_variant
         
     | 
| 
      
 50 
     | 
    
         
            +
                  Unleash::Variant.new(name: 'disabled', enabled: false)
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
       50 
53 
     | 
    
         
             
                private
         
     | 
| 
       51 
54 
     | 
    
         | 
| 
      
 55 
     | 
    
         
            +
                def resolve_stickiness
         
     | 
| 
      
 56 
     | 
    
         
            +
                  self.variant_definitions&.map(&:stickiness)&.compact&.first || "default"
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
       52 
59 
     | 
    
         
             
                # only check if it is enabled, do not do metrics
         
     | 
| 
       53 
     | 
    
         
            -
                def am_enabled?(context 
     | 
| 
      
 60 
     | 
    
         
            +
                def am_enabled?(context)
         
     | 
| 
       54 
61 
     | 
    
         
             
                  result =
         
     | 
| 
       55 
62 
     | 
    
         
             
                    if self.enabled
         
     | 
| 
       56 
63 
     | 
    
         
             
                      self.strategies.empty? ||
         
     | 
| 
         @@ -58,10 +65,10 @@ module Unleash 
     | 
|
| 
       58 
65 
     | 
    
         
             
                          strategy_enabled?(s, context) && strategy_constraint_matches?(s, context)
         
     | 
| 
       59 
66 
     | 
    
         
             
                        end
         
     | 
| 
       60 
67 
     | 
    
         
             
                    else
         
     | 
| 
       61 
     | 
    
         
            -
                       
     | 
| 
      
 68 
     | 
    
         
            +
                      false
         
     | 
| 
       62 
69 
     | 
    
         
             
                    end
         
     | 
| 
       63 
70 
     | 
    
         | 
| 
       64 
     | 
    
         
            -
                  Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled}  
     | 
| 
      
 71 
     | 
    
         
            +
                  Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \
         
     | 
| 
       65 
72 
     | 
    
         
             
                    "and Strategies combined with contraints returned #{result})"
         
     | 
| 
       66 
73 
     | 
    
         | 
| 
       67 
74 
     | 
    
         
             
                  result
         
     | 
| 
         @@ -77,15 +84,12 @@ module Unleash 
     | 
|
| 
       77 
84 
     | 
    
         
             
                  strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
         
     | 
| 
       78 
85 
     | 
    
         
             
                end
         
     | 
| 
       79 
86 
     | 
    
         | 
| 
       80 
     | 
    
         
            -
                def disabled_variant
         
     | 
| 
       81 
     | 
    
         
            -
                  Unleash::Variant.new(name: 'disabled', enabled: false)
         
     | 
| 
       82 
     | 
    
         
            -
                end
         
     | 
| 
       83 
     | 
    
         
            -
             
     | 
| 
       84 
87 
     | 
    
         
             
                def sum_variant_defs_weights
         
     | 
| 
       85 
88 
     | 
    
         
             
                  self.variant_definitions.map(&:weight).reduce(0, :+)
         
     | 
| 
       86 
89 
     | 
    
         
             
                end
         
     | 
| 
       87 
90 
     | 
    
         | 
| 
       88 
     | 
    
         
            -
                def variant_salt(context)
         
     | 
| 
      
 91 
     | 
    
         
            +
                def variant_salt(context, stickiness = "default")
         
     | 
| 
      
 92 
     | 
    
         
            +
                  return context.get_by_name(stickiness) unless stickiness == "default"
         
     | 
| 
       89 
93 
     | 
    
         
             
                  return context.user_id unless context.user_id.to_s.empty?
         
     | 
| 
       90 
94 
     | 
    
         
             
                  return context.session_id unless context.session_id.to_s.empty?
         
     | 
| 
       91 
95 
     | 
    
         
             
                  return context.remote_address unless context.remote_address.to_s.empty?
         
     | 
| 
         @@ -100,8 +104,8 @@ module Unleash 
     | 
|
| 
       100 
104 
     | 
    
         
             
                  Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload)
         
     | 
| 
       101 
105 
     | 
    
         
             
                end
         
     | 
| 
       102 
106 
     | 
    
         | 
| 
       103 
     | 
    
         
            -
                def variant_from_weights(context)
         
     | 
| 
       104 
     | 
    
         
            -
                  variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context), self.name, sum_variant_defs_weights)
         
     | 
| 
      
 107 
     | 
    
         
            +
                def variant_from_weights(context, stickiness)
         
     | 
| 
      
 108 
     | 
    
         
            +
                  variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context, stickiness), self.name, sum_variant_defs_weights)
         
     | 
| 
       105 
109 
     | 
    
         
             
                  prev_weights = 0
         
     | 
| 
       106 
110 
     | 
    
         | 
| 
       107 
111 
     | 
    
         
             
                  variant_definition = self.variant_definitions
         
     | 
| 
         @@ -110,7 +114,7 @@ module Unleash 
     | 
|
| 
       110 
114 
     | 
    
         
             
                      prev_weights += v.weight
         
     | 
| 
       111 
115 
     | 
    
         
             
                      res
         
     | 
| 
       112 
116 
     | 
    
         
             
                    end
         
     | 
| 
       113 
     | 
    
         
            -
                  return disabled_variant if variant_definition.nil?
         
     | 
| 
      
 117 
     | 
    
         
            +
                  return self.disabled_variant if variant_definition.nil?
         
     | 
| 
       114 
118 
     | 
    
         | 
| 
       115 
119 
     | 
    
         
             
                  Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
         
     | 
| 
       116 
120 
     | 
    
         
             
                end
         
     | 
| 
         @@ -135,7 +139,9 @@ module Unleash 
     | 
|
| 
       135 
139 
     | 
    
         
             
                          Constraint.new(
         
     | 
| 
       136 
140 
     | 
    
         
             
                            c.fetch('contextName'),
         
     | 
| 
       137 
141 
     | 
    
         
             
                            c.fetch('operator'),
         
     | 
| 
       138 
     | 
    
         
            -
                            c.fetch('values')
         
     | 
| 
      
 142 
     | 
    
         
            +
                            c.fetch('values', nil) || c.fetch('value', nil),
         
     | 
| 
      
 143 
     | 
    
         
            +
                            inverted: c.fetch('inverted', false),
         
     | 
| 
      
 144 
     | 
    
         
            +
                            case_insensitive: c.fetch('caseInsensitive', false)
         
     | 
| 
       139 
145 
     | 
    
         
             
                          )
         
     | 
| 
       140 
146 
     | 
    
         
             
                        end
         
     | 
| 
       141 
147 
     | 
    
         
             
                      )
         
     | 
| 
         @@ -150,6 +156,7 @@ module Unleash 
     | 
|
| 
       150 
156 
     | 
    
         
             
                        v.fetch('name', ''),
         
     | 
| 
       151 
157 
     | 
    
         
             
                        v.fetch('weight', 0),
         
     | 
| 
       152 
158 
     | 
    
         
             
                        v.fetch('payload', nil),
         
     | 
| 
      
 159 
     | 
    
         
            +
                        v.fetch('stickiness', nil),
         
     | 
| 
       153 
160 
     | 
    
         
             
                        v.fetch('overrides', [])
         
     | 
| 
       154 
161 
     | 
    
         
             
                      )
         
     | 
| 
       155 
162 
     | 
    
         
             
                    end || []
         
     | 
| 
         @@ -6,6 +6,8 @@ require 'time' 
     | 
|
| 
       6 
6 
     | 
    
         | 
| 
       7 
7 
     | 
    
         
             
            module Unleash
         
     | 
| 
       8 
8 
     | 
    
         
             
              class MetricsReporter
         
     | 
| 
      
 9 
     | 
    
         
            +
                LONGEST_WITHOUT_A_REPORT = 600
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
       9 
11 
     | 
    
         
             
                attr_accessor :last_time
         
     | 
| 
       10 
12 
     | 
    
         | 
| 
       11 
13 
     | 
    
         
             
                def initialize
         
     | 
| 
         @@ -33,16 +35,28 @@ module Unleash 
     | 
|
| 
       33 
35 
     | 
    
         
             
                  report
         
     | 
| 
       34 
36 
     | 
    
         
             
                end
         
     | 
| 
       35 
37 
     | 
    
         | 
| 
       36 
     | 
    
         
            -
                def  
     | 
| 
       37 
     | 
    
         
            -
                  Unleash.logger.debug " 
     | 
| 
      
 38 
     | 
    
         
            +
                def post
         
     | 
| 
      
 39 
     | 
    
         
            +
                  Unleash.logger.debug "post() Report"
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  if bucket_empty? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes...
         
     | 
| 
      
 42 
     | 
    
         
            +
                    Unleash.logger.debug "Report not posted to server, as it would have been empty. (and has been empty for up to 10 min)"
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                    return
         
     | 
| 
      
 45 
     | 
    
         
            +
                  end
         
     | 
| 
       38 
46 
     | 
    
         | 
| 
       39 
     | 
    
         
            -
                  response = Unleash::Util::Http.post(Unleash.configuration. 
     | 
| 
      
 47 
     | 
    
         
            +
                  response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, self.generate_report.to_json)
         
     | 
| 
       40 
48 
     | 
    
         | 
| 
       41 
49 
     | 
    
         
             
                  if ['200', '202'].include? response.code
         
     | 
| 
       42 
     | 
    
         
            -
                    Unleash.logger.debug "Report sent to unleash server  
     | 
| 
      
 50 
     | 
    
         
            +
                    Unleash.logger.debug "Report sent to unleash server successfully. Server responded with http code #{response.code}"
         
     | 
| 
       43 
51 
     | 
    
         
             
                  else
         
     | 
| 
       44 
52 
     | 
    
         
             
                    Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}."
         
     | 
| 
       45 
53 
     | 
    
         
             
                  end
         
     | 
| 
       46 
54 
     | 
    
         
             
                end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                private
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                def bucket_empty?
         
     | 
| 
      
 59 
     | 
    
         
            +
                  Unleash.toggle_metrics.features.empty?
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
       47 
61 
     | 
    
         
             
              end
         
     | 
| 
       48 
62 
     | 
    
         
             
            end
         
     | 
| 
         @@ -1,19 +1,22 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module Unleash
         
     | 
| 
       2 
2 
     | 
    
         
             
              class ScheduledExecutor
         
     | 
| 
       3 
     | 
    
         
            -
                attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread
         
     | 
| 
      
 3 
     | 
    
         
            +
                attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread, :immediate_execution
         
     | 
| 
       4 
4 
     | 
    
         | 
| 
       5 
     | 
    
         
            -
                def initialize(name, interval, max_exceptions = 5)
         
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(name, interval, max_exceptions = 5, immediate_execution = false)
         
     | 
| 
       6 
6 
     | 
    
         
             
                  self.name = name || ''
         
     | 
| 
       7 
7 
     | 
    
         
             
                  self.interval = interval
         
     | 
| 
       8 
8 
     | 
    
         
             
                  self.max_exceptions = max_exceptions
         
     | 
| 
       9 
9 
     | 
    
         
             
                  self.retry_count = 0
         
     | 
| 
       10 
10 
     | 
    
         
             
                  self.thread = nil
         
     | 
| 
      
 11 
     | 
    
         
            +
                  self.immediate_execution = immediate_execution
         
     | 
| 
       11 
12 
     | 
    
         
             
                end
         
     | 
| 
       12 
13 
     | 
    
         | 
| 
       13 
14 
     | 
    
         
             
                def run(&blk)
         
     | 
| 
       14 
15 
     | 
    
         
             
                  self.thread = Thread.new do
         
     | 
| 
       15 
16 
     | 
    
         
             
                    Thread.current[:name] = self.name
         
     | 
| 
       16 
17 
     | 
    
         | 
| 
      
 18 
     | 
    
         
            +
                    run_blk{ blk.call } if self.immediate_execution
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
       17 
20 
     | 
    
         
             
                    Unleash.logger.debug "thread #{name} loop starting"
         
     | 
| 
       18 
21 
     | 
    
         
             
                    loop do
         
     | 
| 
       19 
22 
     | 
    
         
             
                      Unleash.logger.debug "thread #{name} sleeping for #{interval} seconds"
         
     | 
| 
         @@ -38,16 +38,16 @@ module Unleash 
     | 
|
| 
       38 
38 
     | 
    
         | 
| 
       39 
39 
     | 
    
         
             
                  def resolve_stickiness(stickiness, context)
         
     | 
| 
       40 
40 
     | 
    
         
             
                    case stickiness
         
     | 
| 
       41 
     | 
    
         
            -
                    when 'userId'
         
     | 
| 
       42 
     | 
    
         
            -
                      context.user_id
         
     | 
| 
       43 
     | 
    
         
            -
                    when 'sessionId'
         
     | 
| 
       44 
     | 
    
         
            -
                      context.session_id
         
     | 
| 
       45 
41 
     | 
    
         
             
                    when 'random'
         
     | 
| 
       46 
42 
     | 
    
         
             
                      random
         
     | 
| 
       47 
43 
     | 
    
         
             
                    when 'default'
         
     | 
| 
       48 
44 
     | 
    
         
             
                      context.user_id || context.session_id || random
         
     | 
| 
       49 
45 
     | 
    
         
             
                    else
         
     | 
| 
       50 
     | 
    
         
            -
                       
     | 
| 
      
 46 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 47 
     | 
    
         
            +
                        context.get_by_name(stickiness)
         
     | 
| 
      
 48 
     | 
    
         
            +
                      rescue KeyError
         
     | 
| 
      
 49 
     | 
    
         
            +
                        nil
         
     | 
| 
      
 50 
     | 
    
         
            +
                      end
         
     | 
| 
       51 
51 
     | 
    
         
             
                    end
         
     | 
| 
       52 
52 
     | 
    
         
             
                  end
         
     | 
| 
       53 
53 
     | 
    
         
             
                end
         
     | 
| 
         @@ -13,7 +13,23 @@ module Unleash 
     | 
|
| 
       13 
13 
     | 
    
         
             
                    return false unless params.fetch(PARAM, nil).is_a? String
         
     | 
| 
       14 
14 
     | 
    
         
             
                    return false unless context.class.name == 'Unleash::Context'
         
     | 
| 
       15 
15 
     | 
    
         | 
| 
       16 
     | 
    
         
            -
                     
     | 
| 
      
 16 
     | 
    
         
            +
                    remote_address = ipaddr_or_nil_from_str(context.remote_address)
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    params[PARAM]
         
     | 
| 
      
 19 
     | 
    
         
            +
                      .split(',')
         
     | 
| 
      
 20 
     | 
    
         
            +
                      .map(&:strip)
         
     | 
| 
      
 21 
     | 
    
         
            +
                      .map{ |ipblock| ipaddr_or_nil_from_str(ipblock) }
         
     | 
| 
      
 22 
     | 
    
         
            +
                      .compact
         
     | 
| 
      
 23 
     | 
    
         
            +
                      .map{ |ipb| ipb.include? remote_address }
         
     | 
| 
      
 24 
     | 
    
         
            +
                      .any?
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                  private
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  def ipaddr_or_nil_from_str(ip)
         
     | 
| 
      
 30 
     | 
    
         
            +
                    IPAddr.new(ip)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  rescue StandardError
         
     | 
| 
      
 32 
     | 
    
         
            +
                    nil
         
     | 
| 
       17 
33 
     | 
    
         
             
                  end
         
     | 
| 
       18 
34 
     | 
    
         
             
                end
         
     | 
| 
       19 
35 
     | 
    
         
             
              end
         
     |