ldclient-rb 0.8.0 → 2.0.1
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/CHANGELOG.md +25 -0
- data/README.md +3 -3
- data/ext/mkrf_conf.rb +11 -0
- data/ldclient-rb.gemspec +3 -1
- data/lib/ldclient-rb.rb +6 -2
- data/lib/ldclient-rb/{store.rb → cache_store.rb} +0 -0
- data/lib/ldclient-rb/config.rb +27 -29
- data/lib/ldclient-rb/evaluation.rb +265 -0
- data/lib/ldclient-rb/events.rb +75 -0
- data/lib/ldclient-rb/feature_store.rb +60 -0
- data/lib/ldclient-rb/ldclient.rb +92 -303
- data/lib/ldclient-rb/polling.rb +56 -0
- data/lib/ldclient-rb/requestor.rb +56 -0
- data/lib/ldclient-rb/stream.rb +40 -140
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/config_spec.rb +3 -3
- data/spec/fixtures/feature.json +27 -58
- data/spec/ldclient_spec.rb +20 -226
- data/spec/stream_spec.rb +7 -63
- metadata +28 -7
- data/lib/ldclient-rb/settings.rb +0 -40
| @@ -0,0 +1,75 @@ | |
| 1 | 
            +
            require "thread"
         | 
| 2 | 
            +
            require "faraday"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module LaunchDarkly
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              class EventProcessor
         | 
| 7 | 
            +
                def initialize(sdk_key, config)
         | 
| 8 | 
            +
                  @queue = Queue.new
         | 
| 9 | 
            +
                  @sdk_key = sdk_key
         | 
| 10 | 
            +
                  @config = config
         | 
| 11 | 
            +
                  @client = Faraday.new
         | 
| 12 | 
            +
                  @worker = create_worker
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def create_worker
         | 
| 16 | 
            +
                  Thread.new do
         | 
| 17 | 
            +
                    loop do
         | 
| 18 | 
            +
                      begin
         | 
| 19 | 
            +
                        flush
         | 
| 20 | 
            +
                        sleep(@config.flush_interval)
         | 
| 21 | 
            +
                      rescue StandardError => exn
         | 
| 22 | 
            +
                        log_exception(__method__.to_s, exn)
         | 
| 23 | 
            +
                      end
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def post_flushed_events(events)
         | 
| 29 | 
            +
                  res = @client.post (@config.events_uri + "/bulk") do |req|
         | 
| 30 | 
            +
                    req.headers["Authorization"] = @sdk_key
         | 
| 31 | 
            +
                    req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
         | 
| 32 | 
            +
                    req.headers["Content-Type"] = "application/json"
         | 
| 33 | 
            +
                    req.body = events.to_json
         | 
| 34 | 
            +
                    req.options.timeout = @config.read_timeout
         | 
| 35 | 
            +
                    req.options.open_timeout = @config.connect_timeout
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                  if res.status / 100 != 2
         | 
| 38 | 
            +
                    @config.logger.error("[LDClient] Unexpected status code while processing events: #{res.status}")
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def flush
         | 
| 43 | 
            +
                  events = []
         | 
| 44 | 
            +
                  begin
         | 
| 45 | 
            +
                    loop do
         | 
| 46 | 
            +
                      events << @queue.pop(true)
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                  rescue ThreadError
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  if !events.empty?
         | 
| 52 | 
            +
                    post_flushed_events(events)
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def add_event(event)
         | 
| 57 | 
            +
                  return if @offline
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  if @queue.length < @config.capacity
         | 
| 60 | 
            +
                    event[:creationDate] = (Time.now.to_f * 1000).to_i
         | 
| 61 | 
            +
                    @config.logger.debug("[LDClient] Enqueueing event: #{event.to_json}")
         | 
| 62 | 
            +
                    @queue.push(event)
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    if !@worker.alive?
         | 
| 65 | 
            +
                      @worker = create_worker
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
                  else
         | 
| 68 | 
            +
                    @config.logger.warn("[LDClient] Exceeded event queue capacity. Increase capacity to avoid dropping events.")
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
                end  
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                private :create_worker, :post_flushed_events
         | 
| 73 | 
            +
              
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
            end
         | 
| @@ -0,0 +1,60 @@ | |
| 1 | 
            +
            require "concurrent/atomics"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module LaunchDarkly
         | 
| 4 | 
            +
              
         | 
| 5 | 
            +
              class InMemoryFeatureStore
         | 
| 6 | 
            +
                def initialize
         | 
| 7 | 
            +
                  @features = Hash.new
         | 
| 8 | 
            +
                  @lock = Concurrent::ReadWriteLock.new
         | 
| 9 | 
            +
                  @initialized = Concurrent::AtomicBoolean.new(false)
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def get(key)
         | 
| 13 | 
            +
                  @lock.with_read_lock do
         | 
| 14 | 
            +
                    f = @features[key.to_sym]
         | 
| 15 | 
            +
                    (f.nil? || f[:deleted]) ? nil : f
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def all
         | 
| 20 | 
            +
                  @lock.with_read_lock do
         | 
| 21 | 
            +
                    @features.select { |_k, f| not f[:deleted] }
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def delete(key, version)
         | 
| 26 | 
            +
                  @lock.with_write_lock do
         | 
| 27 | 
            +
                    old = @features[key.to_sym]
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    if !old.nil? && old[:version] < version
         | 
| 30 | 
            +
                      old[:deleted] = true
         | 
| 31 | 
            +
                      old[:version] = version
         | 
| 32 | 
            +
                      @features[key.to_sym] = old
         | 
| 33 | 
            +
                    elsif old.nil?
         | 
| 34 | 
            +
                      @features[key.to_sym] = { deleted: true, version: version }
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def init(fs)
         | 
| 40 | 
            +
                  @lock.with_write_lock do
         | 
| 41 | 
            +
                    @features.replace(fs)
         | 
| 42 | 
            +
                    @initialized.make_true
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def upsert(key, feature)
         | 
| 47 | 
            +
                  @lock.with_write_lock do
         | 
| 48 | 
            +
                    old = @features[key.to_sym]
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    if old.nil? || old[:version] < feature[:version]
         | 
| 51 | 
            +
                      @features[key.to_sym] = feature
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def initialized?
         | 
| 57 | 
            +
                  @initialized.value
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
            end
         | 
    
        data/lib/ldclient-rb/ldclient.rb
    CHANGED
    
    | @@ -1,100 +1,77 @@ | |
| 1 | 
            -
            require "faraday/http_cache"
         | 
| 2 | 
            -
            require "json"
         | 
| 3 1 | 
             
            require "digest/sha1"
         | 
| 4 | 
            -
            require "thread"
         | 
| 5 2 | 
             
            require "logger"
         | 
| 6 | 
            -
            require "net/http/persistent"
         | 
| 7 3 | 
             
            require "benchmark"
         | 
| 8 | 
            -
            require " | 
| 4 | 
            +
            require "waitutil"
         | 
| 5 | 
            +
            require "json"
         | 
| 6 | 
            +
            require "openssl"
         | 
| 9 7 |  | 
| 10 8 | 
             
            module LaunchDarkly
         | 
| 11 | 
            -
              BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
         | 
| 12 | 
            -
             | 
| 13 9 | 
             
              #
         | 
| 14 | 
            -
              # A client for  | 
| 10 | 
            +
              # A client for LaunchDarkly. Client instances are thread-safe. Users
         | 
| 15 11 | 
             
              # should create a single client instance for the lifetime of the application.
         | 
| 16 12 | 
             
              #
         | 
| 17 13 | 
             
              #
         | 
| 18 14 | 
             
              class LDClient
         | 
| 19 | 
            -
                include  | 
| 15 | 
            +
                include Evaluation
         | 
| 20 16 | 
             
                #
         | 
| 21 17 | 
             
                # Creates a new client instance that connects to LaunchDarkly. A custom
         | 
| 22 18 | 
             
                # configuration parameter can also supplied to specify advanced options,
         | 
| 23 19 | 
             
                # but for most use cases, the default configuration is appropriate.
         | 
| 24 20 | 
             
                #
         | 
| 25 21 | 
             
                #
         | 
| 26 | 
            -
                # @param  | 
| 22 | 
            +
                # @param sdk_key [String] the SDK key for your LaunchDarkly account
         | 
| 27 23 | 
             
                # @param config [Config] an optional client configuration object
         | 
| 28 24 | 
             
                #
         | 
| 29 25 | 
             
                # @return [LDClient] The LaunchDarkly client instance
         | 
| 30 | 
            -
                def initialize( | 
| 31 | 
            -
                  @ | 
| 32 | 
            -
                  @api_key = api_key
         | 
| 26 | 
            +
                def initialize(sdk_key, config = Config.default, wait_for_sec = 5)
         | 
| 27 | 
            +
                  @sdk_key = sdk_key
         | 
| 33 28 | 
             
                  @config = config
         | 
| 34 | 
            -
                  @ | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
                     | 
| 29 | 
            +
                  @store = config.feature_store
         | 
| 30 | 
            +
                  requestor = Requestor.new(sdk_key, config)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  if !@config.offline?
         | 
| 33 | 
            +
                    if @config.stream?
         | 
| 34 | 
            +
                      @update_processor = StreamProcessor.new(sdk_key, config, requestor)
         | 
| 35 | 
            +
                    else 
         | 
| 36 | 
            +
                      @update_processor = PollingProcessor.new(config, requestor)
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                    @update_processor.start
         | 
| 43 39 | 
             
                  end
         | 
| 44 40 |  | 
| 45 | 
            -
                  @ | 
| 46 | 
            -
                end
         | 
| 41 | 
            +
                  @event_processor = EventProcessor.new(sdk_key, config)
         | 
| 47 42 |  | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
                       | 
| 43 | 
            +
                  if !@config.offline? && wait_for_sec > 0
         | 
| 44 | 
            +
                    begin
         | 
| 45 | 
            +
                      WaitUtil.wait_for_condition("LaunchDarkly client initialization", :timeout_sec => wait_for_sec, :delay_sec => 0.1) do
         | 
| 46 | 
            +
                        @update_processor.initialized?
         | 
| 47 | 
            +
                      end
         | 
| 48 | 
            +
                    rescue WaitUtil::TimeoutError
         | 
| 49 | 
            +
                      @config.logger.error("[LDClient] Timeout encountered waiting for LaunchDarkly client initialization")
         | 
| 53 50 | 
             
                    end
         | 
| 54 | 
            -
                  rescue ThreadError
         | 
| 55 | 
            -
                  end
         | 
| 56 | 
            -
             | 
| 57 | 
            -
                  if !events.empty?
         | 
| 58 | 
            -
                    post_flushed_events(events)
         | 
| 59 51 | 
             
                  end
         | 
| 60 52 | 
             
                end
         | 
| 61 53 |  | 
| 62 | 
            -
                def  | 
| 63 | 
            -
                   | 
| 64 | 
            -
                    next @client.post (@config.events_uri + "/bulk") do |req|
         | 
| 65 | 
            -
                      req.headers["Authorization"] = "api_key " + @api_key
         | 
| 66 | 
            -
                      req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
         | 
| 67 | 
            -
                      req.headers["Content-Type"] = "application/json"
         | 
| 68 | 
            -
                      req.body = events.to_json
         | 
| 69 | 
            -
                      req.options.timeout = @config.read_timeout
         | 
| 70 | 
            -
                      req.options.open_timeout = @config.connect_timeout
         | 
| 71 | 
            -
                    end
         | 
| 72 | 
            -
                  end
         | 
| 73 | 
            -
                  if res.status / 100 != 2
         | 
| 74 | 
            -
                    @config.logger.error("[LDClient] Unexpected status code while processing events: #{res.status}")
         | 
| 75 | 
            -
                  end
         | 
| 54 | 
            +
                def flush
         | 
| 55 | 
            +
                  @event_processor.flush
         | 
| 76 56 | 
             
                end
         | 
| 77 57 |  | 
| 78 | 
            -
                def  | 
| 79 | 
            -
                   | 
| 80 | 
            -
             | 
| 81 | 
            -
             | 
| 82 | 
            -
                        flush
         | 
| 58 | 
            +
                def toggle?(key, user, default = False)
         | 
| 59 | 
            +
                  @config.logger.warn("[LDClient] toggle? is deprecated. Use variation instead")
         | 
| 60 | 
            +
                  variation(key, user, default)
         | 
| 61 | 
            +
                end
         | 
| 83 62 |  | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
            -
                        log_exception(__method__.to_s, exn)
         | 
| 87 | 
            -
                      end
         | 
| 88 | 
            -
                    end
         | 
| 89 | 
            -
                  end
         | 
| 63 | 
            +
                def secure_mode_hash(user)
         | 
| 64 | 
            +
                  OpenSSL::HMAC.hexdigest('sha256', @sdk_key, user[:key].to_s)
         | 
| 90 65 | 
             
                end
         | 
| 91 66 |  | 
| 92 | 
            -
                 | 
| 93 | 
            -
             | 
| 67 | 
            +
                # Returns whether the client has been initialized and is ready to serve feature flag requests
         | 
| 68 | 
            +
                # @return [Boolean] true if the client has been initialized
         | 
| 69 | 
            +
                def initialized?
         | 
| 70 | 
            +
                  @update_processor.initialized?
         | 
| 94 71 | 
             
                end
         | 
| 95 72 |  | 
| 96 73 | 
             
                #
         | 
| 97 | 
            -
                #  | 
| 74 | 
            +
                # Determines the variation of a feature flag to present to a user. At a minimum,
         | 
| 98 75 | 
             
                # the user hash should contain a +:key+ .
         | 
| 99 76 | 
             
                #
         | 
| 100 77 | 
             
                # @example Basic user hash
         | 
| @@ -110,8 +87,6 @@ module LaunchDarkly | |
| 110 87 | 
             
                # @example More complete user hash
         | 
| 111 88 | 
             
                #   {key: "user@example.com", ip: "127.0.0.1", country: "US"}
         | 
| 112 89 | 
             
                #
         | 
| 113 | 
            -
                # Countries should be sent as ISO 3166-1 alpha-2 codes.
         | 
| 114 | 
            -
                #
         | 
| 115 90 | 
             
                # The user hash can contain arbitrary custom attributes stored in a +:custom+ sub-hash:
         | 
| 116 91 | 
             
                #
         | 
| 117 92 | 
             
                # @example A user hash with custom attributes
         | 
| @@ -123,51 +98,53 @@ module LaunchDarkly | |
| 123 98 | 
             
                # @param key [String] the unique feature key for the feature flag, as shown
         | 
| 124 99 | 
             
                #   on the LaunchDarkly dashboard
         | 
| 125 100 | 
             
                # @param user [Hash] a hash containing parameters for the end user requesting the flag
         | 
| 126 | 
            -
                # @param default=false  | 
| 101 | 
            +
                # @param default=false the default value of the flag
         | 
| 127 102 | 
             
                #
         | 
| 128 | 
            -
                # @return  | 
| 129 | 
            -
                #   default value if  | 
| 130 | 
            -
                def  | 
| 131 | 
            -
                  return default if @offline
         | 
| 103 | 
            +
                # @return the variation to show the user, or the
         | 
| 104 | 
            +
                #   default value if there's an an error
         | 
| 105 | 
            +
                def variation(key, user, default)
         | 
| 106 | 
            +
                  return default if @config.offline?
         | 
| 132 107 |  | 
| 133 108 | 
             
                  unless user
         | 
| 134 109 | 
             
                    @config.logger.error("[LDClient] Must specify user")
         | 
| 110 | 
            +
                    @event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)        
         | 
| 135 111 | 
             
                    return default
         | 
| 136 112 | 
             
                  end
         | 
| 137 | 
            -
                  sanitize_user(user)
         | 
| 138 | 
            -
             | 
| 139 | 
            -
                  if @config.stream? && !@stream_processor.started?
         | 
| 140 | 
            -
                    @stream_processor.start
         | 
| 141 | 
            -
                  end
         | 
| 142 113 |  | 
| 143 | 
            -
                  if  | 
| 144 | 
            -
                     | 
| 145 | 
            -
             | 
| 146 | 
            -
                     | 
| 114 | 
            +
                  if !@update_processor.initialized?
         | 
| 115 | 
            +
                    @config.logger.error("[LDClient] Client has not finished initializing. Returning default value")
         | 
| 116 | 
            +
                    @event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)        
         | 
| 117 | 
            +
                    return default
         | 
| 147 118 | 
             
                  end
         | 
| 148 | 
            -
                  value = evaluate(feature, user)
         | 
| 149 | 
            -
                  value = value.nil? ? default : value
         | 
| 150 119 |  | 
| 151 | 
            -
                   | 
| 152 | 
            -
                   | 
| 153 | 
            -
                  return value
         | 
| 154 | 
            -
                rescue StandardError => error
         | 
| 155 | 
            -
                  log_exception(__method__.to_s, error)
         | 
| 156 | 
            -
                  default
         | 
| 157 | 
            -
                end
         | 
| 158 | 
            -
             | 
| 159 | 
            -
                def add_event(event)
         | 
| 160 | 
            -
                  return if @offline
         | 
| 120 | 
            +
                  sanitize_user(user)
         | 
| 121 | 
            +
                  feature = @store.get(key)
         | 
| 161 122 |  | 
| 162 | 
            -
                  if  | 
| 163 | 
            -
                     | 
| 164 | 
            -
                    @ | 
| 123 | 
            +
                  if feature.nil?
         | 
| 124 | 
            +
                    @config.logger.error("[LDClient] Unknown feature flag #{key}. Returning default value")
         | 
| 125 | 
            +
                    @event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)
         | 
| 126 | 
            +
                    return default
         | 
| 127 | 
            +
                  end
         | 
| 165 128 |  | 
| 166 | 
            -
             | 
| 167 | 
            -
             | 
| 129 | 
            +
                  begin
         | 
| 130 | 
            +
                    res = evaluate(feature, user, @store)
         | 
| 131 | 
            +
                    if !res[:events].nil?
         | 
| 132 | 
            +
                      res[:events].each do |event|
         | 
| 133 | 
            +
                        @event_processor.add_event(event)
         | 
| 134 | 
            +
                      end
         | 
| 168 135 | 
             
                    end
         | 
| 169 | 
            -
             | 
| 170 | 
            -
             | 
| 136 | 
            +
                    if !res[:value].nil?
         | 
| 137 | 
            +
                      @event_processor.add_event(kind: "feature", key: key, user: user, value: res[:value], default: default, version: feature[:version])
         | 
| 138 | 
            +
                      return res[:value]
         | 
| 139 | 
            +
                    else
         | 
| 140 | 
            +
                      @config.logger.debug("[LDClient] Result value is null in toggle")
         | 
| 141 | 
            +
                      @event_processor.add_event(kind: "feature", key: key, user: user, value: default, default: default, version: feature[:version])        
         | 
| 142 | 
            +
                      return default            
         | 
| 143 | 
            +
                    end
         | 
| 144 | 
            +
                  rescue => exn
         | 
| 145 | 
            +
                    @config.logger.warn("[LDClient] Error evaluating feature flag: #{exn.inspect}. \nTrace: #{exn.backtrace}")
         | 
| 146 | 
            +
                    @event_processor.add_event(kind: "feature", key: key, user: user, value: default, default: default, version: feature[:version])
         | 
| 147 | 
            +
                    return default
         | 
| 171 148 | 
             
                  end
         | 
| 172 149 | 
             
                end
         | 
| 173 150 |  | 
| @@ -176,21 +153,10 @@ module LaunchDarkly | |
| 176 153 | 
             
                #
         | 
| 177 154 | 
             
                # @param [Hash] The user to register
         | 
| 178 155 | 
             
                #
         | 
| 156 | 
            +
                # @return [void]
         | 
| 179 157 | 
             
                def identify(user)
         | 
| 180 158 | 
             
                  sanitize_user(user)
         | 
| 181 | 
            -
                  add_event(kind: "identify", key: user[:key], user: user)
         | 
| 182 | 
            -
                end
         | 
| 183 | 
            -
             | 
| 184 | 
            -
                def set_offline
         | 
| 185 | 
            -
                  @offline = true
         | 
| 186 | 
            -
                end
         | 
| 187 | 
            -
             | 
| 188 | 
            -
                def set_online
         | 
| 189 | 
            -
                  @offline = false
         | 
| 190 | 
            -
                end
         | 
| 191 | 
            -
             | 
| 192 | 
            -
                def is_offline?
         | 
| 193 | 
            -
                  @offline
         | 
| 159 | 
            +
                  @event_processor.add_event(kind: "identify", key: user[:key], user: user)
         | 
| 194 160 | 
             
                end
         | 
| 195 161 |  | 
| 196 162 | 
             
                #
         | 
| @@ -203,204 +169,30 @@ module LaunchDarkly | |
| 203 169 | 
             
                # @return [void]
         | 
| 204 170 | 
             
                def track(event_name, user, data)
         | 
| 205 171 | 
             
                  sanitize_user(user)
         | 
| 206 | 
            -
                  add_event(kind: "custom", key: event_name, user: user, data: data)
         | 
| 207 | 
            -
                end
         | 
| 208 | 
            -
             | 
| 209 | 
            -
                #
         | 
| 210 | 
            -
                # Returns the key of every feature flag
         | 
| 211 | 
            -
                #
         | 
| 212 | 
            -
                def all_keys
         | 
| 213 | 
            -
                  all_flags.keys
         | 
| 172 | 
            +
                  @event_processor.add_event(kind: "custom", key: event_name, user: user, data: data)
         | 
| 214 173 | 
             
                end
         | 
| 215 174 |  | 
| 216 175 | 
             
                #
         | 
| 217 | 
            -
                # Returns all feature  | 
| 176 | 
            +
                # Returns all feature flag values for the given user
         | 
| 218 177 | 
             
                #
         | 
| 219 | 
            -
                def all_flags
         | 
| 220 | 
            -
                   | 
| 221 | 
            -
             | 
| 222 | 
            -
                  if @config.stream? && !@stream_processor.started?
         | 
| 223 | 
            -
                    @stream_processor.start
         | 
| 224 | 
            -
                  end
         | 
| 225 | 
            -
             | 
| 226 | 
            -
                  if @config.stream? && @stream_processor.initialized?
         | 
| 227 | 
            -
                    @stream_processor.get_all_features
         | 
| 228 | 
            -
                  else
         | 
| 229 | 
            -
                    res = make_request "/api/eval/features"
         | 
| 230 | 
            -
             | 
| 231 | 
            -
                    if res.status / 100 == 2
         | 
| 232 | 
            -
                      JSON.parse(res.body, symbolize_names: true)         
         | 
| 233 | 
            -
                    else
         | 
| 234 | 
            -
                      @config.logger.error("[LDClient] Unexpected status code #{res.status}")
         | 
| 235 | 
            -
                      Hash.new
         | 
| 236 | 
            -
                    end
         | 
| 237 | 
            -
                  end
         | 
| 238 | 
            -
                end
         | 
| 239 | 
            -
             | 
| 240 | 
            -
                def get_user_settings(user)
         | 
| 241 | 
            -
                  Hash[all_flags.map { |key, feature| [key, evaluate(feature, user)]}]
         | 
| 242 | 
            -
                end
         | 
| 243 | 
            -
             | 
| 244 | 
            -
                def get_streamed_flag(key)
         | 
| 245 | 
            -
                  feature = get_flag_stream(key)
         | 
| 246 | 
            -
                  if @config.debug_stream?
         | 
| 247 | 
            -
                    polled = get_flag_int(key)
         | 
| 248 | 
            -
                    diff = HashDiff.diff(feature, polled)
         | 
| 249 | 
            -
                    if not diff.empty?
         | 
| 250 | 
            -
                      @config.logger.error("Streamed flag differs from polled flag " + diff.to_s)
         | 
| 251 | 
            -
                    end
         | 
| 252 | 
            -
                  end
         | 
| 253 | 
            -
                  feature
         | 
| 254 | 
            -
                end
         | 
| 255 | 
            -
             | 
| 256 | 
            -
                def get_flag_stream(key)
         | 
| 257 | 
            -
                  @stream_processor.get_feature(key)
         | 
| 258 | 
            -
                end
         | 
| 259 | 
            -
             | 
| 260 | 
            -
                def get_flag_int(key)
         | 
| 261 | 
            -
                  res = log_timings("Feature request") do
         | 
| 262 | 
            -
                    next make_request "/api/eval/features/" + key
         | 
| 263 | 
            -
                  end
         | 
| 264 | 
            -
             | 
| 265 | 
            -
                  if res.status == 401
         | 
| 266 | 
            -
                    @config.logger.error("[LDClient] Invalid API key")
         | 
| 267 | 
            -
                    return nil
         | 
| 268 | 
            -
                  end
         | 
| 269 | 
            -
             | 
| 270 | 
            -
                  if res.status == 404
         | 
| 271 | 
            -
                    @config.logger.error("[LDClient] Unknown feature key: #{key}")
         | 
| 272 | 
            -
                    return nil
         | 
| 273 | 
            -
                  end
         | 
| 274 | 
            -
             | 
| 275 | 
            -
                  if res.status / 100 != 2
         | 
| 276 | 
            -
                    @config.logger.error("[LDClient] Unexpected status code #{res.status}")
         | 
| 277 | 
            -
                    return nil
         | 
| 278 | 
            -
                  end
         | 
| 279 | 
            -
             | 
| 280 | 
            -
                  JSON.parse(res.body, symbolize_names: true)
         | 
| 281 | 
            -
                end
         | 
| 282 | 
            -
             | 
| 283 | 
            -
                def make_request(path)
         | 
| 284 | 
            -
                  @client.get (@config.base_uri + path) do |req|
         | 
| 285 | 
            -
                    req.headers["Authorization"] = "api_key " + @api_key
         | 
| 286 | 
            -
                    req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
         | 
| 287 | 
            -
                    req.options.timeout = @config.read_timeout
         | 
| 288 | 
            -
                    req.options.open_timeout = @config.connect_timeout
         | 
| 289 | 
            -
                  end
         | 
| 290 | 
            -
                end
         | 
| 291 | 
            -
             | 
| 292 | 
            -
                def param_for_user(feature, user)
         | 
| 293 | 
            -
                  return nil unless user[:key]
         | 
| 294 | 
            -
             | 
| 295 | 
            -
                  id_hash = user[:key]
         | 
| 296 | 
            -
                  if user[:secondary]
         | 
| 297 | 
            -
                    id_hash += "." + user[:secondary]
         | 
| 298 | 
            -
                  end
         | 
| 299 | 
            -
             | 
| 300 | 
            -
                  hash_key = "%s.%s.%s" % [feature[:key], feature[:salt], id_hash]
         | 
| 301 | 
            -
             | 
| 302 | 
            -
                  hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
         | 
| 303 | 
            -
                  hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
         | 
| 304 | 
            -
                end
         | 
| 305 | 
            -
             | 
| 306 | 
            -
                def match_target?(target, user)
         | 
| 307 | 
            -
                  attrib = target[:attribute].to_sym
         | 
| 308 | 
            -
             | 
| 309 | 
            -
                  if BUILTINS.include?(attrib)
         | 
| 310 | 
            -
                    return false unless user[attrib]
         | 
| 311 | 
            -
             | 
| 312 | 
            -
                    u_value = user[attrib]
         | 
| 313 | 
            -
                    return target[:values].include? u_value
         | 
| 314 | 
            -
                  else # custom attribute
         | 
| 315 | 
            -
                    return false unless user[:custom]
         | 
| 316 | 
            -
                    return false unless user[:custom].include? attrib
         | 
| 317 | 
            -
             | 
| 318 | 
            -
                    u_value = user[:custom][attrib]
         | 
| 319 | 
            -
                    if u_value.is_a? Array
         | 
| 320 | 
            -
                      return ! ((target[:values] & u_value).empty?)
         | 
| 321 | 
            -
                    else
         | 
| 322 | 
            -
                      return target[:values].include? u_value
         | 
| 323 | 
            -
                    end
         | 
| 324 | 
            -
             | 
| 325 | 
            -
                    return false
         | 
| 326 | 
            -
                  end
         | 
| 327 | 
            -
                end
         | 
| 328 | 
            -
             | 
| 329 | 
            -
                def match_user?(variation, user)
         | 
| 330 | 
            -
                  if variation[:userTarget]
         | 
| 331 | 
            -
                    return match_target?(variation[:userTarget], user)
         | 
| 332 | 
            -
                  end
         | 
| 333 | 
            -
                  false
         | 
| 334 | 
            -
                end
         | 
| 335 | 
            -
             | 
| 336 | 
            -
                def find_user_match(feature, user)
         | 
| 337 | 
            -
                  feature[:variations].each do |variation|
         | 
| 338 | 
            -
                    return variation[:value] if match_user?(variation, user)
         | 
| 339 | 
            -
                  end
         | 
| 340 | 
            -
                  nil
         | 
| 341 | 
            -
                end
         | 
| 342 | 
            -
             | 
| 343 | 
            -
                def match_variation?(variation, user)
         | 
| 344 | 
            -
                  variation[:targets].each do |target|
         | 
| 345 | 
            -
                    if !!variation[:userTarget] && target[:attribute].to_sym == :key
         | 
| 346 | 
            -
                      next
         | 
| 347 | 
            -
                    end
         | 
| 348 | 
            -
             | 
| 349 | 
            -
                    if match_target?(target, user)
         | 
| 350 | 
            -
                      return true
         | 
| 351 | 
            -
                    end
         | 
| 352 | 
            -
                  end
         | 
| 353 | 
            -
                  false
         | 
| 354 | 
            -
                end
         | 
| 355 | 
            -
             | 
| 356 | 
            -
                def find_target_match(feature, user)
         | 
| 357 | 
            -
                  feature[:variations].each do |variation|
         | 
| 358 | 
            -
                    return variation[:value] if match_variation?(variation, user)
         | 
| 359 | 
            -
                  end
         | 
| 360 | 
            -
                  nil
         | 
| 361 | 
            -
                end
         | 
| 362 | 
            -
             | 
| 363 | 
            -
                def find_weight_match(feature, param)
         | 
| 364 | 
            -
                  total = 0.0
         | 
| 365 | 
            -
                  feature[:variations].each do |variation|
         | 
| 366 | 
            -
                    total += variation[:weight].to_f / 100.0
         | 
| 178 | 
            +
                def all_flags(user)
         | 
| 179 | 
            +
                  sanitize_user(user)
         | 
| 180 | 
            +
                  return Hash.new if @config.offline?
         | 
| 367 181 |  | 
| 368 | 
            -
             | 
| 182 | 
            +
                  unless user
         | 
| 183 | 
            +
                    @config.logger.error("[LDClient] Must specify user in all_flags")
         | 
| 184 | 
            +
                    return Hash.new
         | 
| 369 185 | 
             
                  end
         | 
| 370 186 |  | 
| 371 | 
            -
                   | 
| 372 | 
            -
             | 
| 373 | 
            -
             | 
| 374 | 
            -
                def evaluate(feature, user)
         | 
| 375 | 
            -
                  return nil if feature.nil?
         | 
| 376 | 
            -
                  return nil unless feature[:on]
         | 
| 377 | 
            -
             | 
| 378 | 
            -
                  param = param_for_user(feature, user)
         | 
| 379 | 
            -
                  return nil if param.nil?
         | 
| 380 | 
            -
             | 
| 381 | 
            -
                  value = find_user_match(feature, user)
         | 
| 382 | 
            -
                  return value if !value.nil?
         | 
| 383 | 
            -
             | 
| 384 | 
            -
                  value = find_target_match(feature, user)
         | 
| 385 | 
            -
                  return value if !value.nil?
         | 
| 386 | 
            -
             | 
| 387 | 
            -
                  find_weight_match(feature, param)
         | 
| 388 | 
            -
                end
         | 
| 187 | 
            +
                  begin
         | 
| 188 | 
            +
                    features = @store.all
         | 
| 389 189 |  | 
| 390 | 
            -
             | 
| 391 | 
            -
             | 
| 392 | 
            -
                   | 
| 393 | 
            -
             | 
| 394 | 
            -
             | 
| 395 | 
            -
                    begin
         | 
| 396 | 
            -
                      res = block.call
         | 
| 397 | 
            -
                    rescue StandardError => e
         | 
| 398 | 
            -
                      exn = e
         | 
| 399 | 
            -
                    end
         | 
| 190 | 
            +
                    # TODO rescue if necessary
         | 
| 191 | 
            +
                    Hash[features.map{|k,f| [k, evaluate(f, user, @store)[:value]] }]
         | 
| 192 | 
            +
                  rescue => exn
         | 
| 193 | 
            +
                    @config.logger.warn("[LDClient] Error evaluating all flags: #{exn.inspect}. \nTrace: #{exn.backtrace}")
         | 
| 194 | 
            +
                    return Hash.new
         | 
| 400 195 | 
             
                  end
         | 
| 401 | 
            -
                  @config.logger.debug { "[LDClient] #{label} timing: #{bench}".chomp }
         | 
| 402 | 
            -
                  raise exn if exn
         | 
| 403 | 
            -
                  res
         | 
| 404 196 | 
             
                end
         | 
| 405 197 |  | 
| 406 198 | 
             
                def log_exception(caller, exn)
         | 
| @@ -415,9 +207,6 @@ module LaunchDarkly | |
| 415 207 | 
             
                  end
         | 
| 416 208 | 
             
                end
         | 
| 417 209 |  | 
| 418 | 
            -
                private : | 
| 419 | 
            -
                        :get_flag_stream, :get_flag_int, :make_request, :param_for_user,
         | 
| 420 | 
            -
                        :match_target?, :match_user?, :match_variation?, :evaluate,
         | 
| 421 | 
            -
                        :create_worker, :log_timings, :log_exception, :sanitize_user
         | 
| 210 | 
            +
                private :evaluate, :log_exception, :sanitize_user
         | 
| 422 211 | 
             
              end
         | 
| 423 212 | 
             
            end
         |