Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ LaunchDarkly SDK for Ruby
Quick setup
-----------

1. Install the Ruby SDK with `gem`
0. Install the Ruby SDK with `gem`

gem install ldclient-rb

1. Require the LaunchDarkly client:

require 'ldclient-rb'


2. Create a new LDClient with your API key:

client = LaunchDarkly::LDClient.new("your_api_key")
Expand Down
93 changes: 90 additions & 3 deletions lib/ldclient-rb/config.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,102 @@
require 'logger'

module LaunchDarkly

#
# This class exposes advanced configuration options for the LaunchDarkly client library. Most users
# will not need to use a custom configuration-- the default configuration sets sane defaults for most use cases.
#
#
class Config
def initialize(base_uri)
@base_uri = base_uri
#
# Constructor for creating custom LaunchDarkly configurations.
#
# @param opts [Hash] the configuration options
# @option opts [Logger] :logger A logger to use for messages from the LaunchDarkly client. Defaults to the Rails logger in a Rails environment, or stdout otherwise.
# @option opts [String] :base_uri ("https://app.launchdarkly.com") The base URL for the LaunchDarkly server. Most users should use the default value.
# @option opts [Integer] :capacity (10000) The capacity of the events buffer. The client buffers up to this many events in memory before flushing. If the capacity is exceeded before the buffer is flushed, events will be discarded.
# @option opts [Integer] :flush_interval (30) The number of seconds between flushes of the event buffer.
# @option opts [Object] :store A cache store for the Faraday HTTP caching library. Defaults to the Rails cache in a Rails environment, or a thread-safe in-memory store otherwise.
#
# @return [type] [description]
def initialize(opts = {})
@base_uri = opts[:base_uri] || Config.default_base_uri
@capacity = opts[:capacity] || Config.default_capacity
@logger = opts[:logger] || Config.default_logger
@store = opts[:store] || Config.default_store
@flush_interval = opts[:flush_interval] || Config.default_flush_interval
end

#
# The base URL for the LaunchDarkly server.
#
# @return [String] The configured base URL for the LaunchDarkly server.
def base_uri
@base_uri
end

#
# The number of seconds between flushes of the event buffer. Decreasing the flush interval means
# that the event buffer is less likely to reach capacity.
#
# @return [Integer] The configured number of seconds between flushes of the event buffer.
def flush_interval
@flush_interval
end

#
# The configured logger for the LaunchDarkly client. The client library uses the log to
# print warning and error messages.
#
# @return [Logger] The configured logger
def logger
@logger
end

#
# The capacity of the events buffer. The client buffers up to this many events in memory before flushing. If the capacity is exceeded before the buffer is flushed, events will be discarded.
# Increasing the capacity means that events are less likely to be discarded, at the cost of consuming more memory.
#
# @return [Integer] The configured capacity of the event buffer
def capacity
@capacity
end

#
# The store for the Faraday HTTP caching library. Stores should respond to 'read' and 'write' requests.
#
# @return [Object] The configured store for the Faraday HTTP caching library.
def store
@store
end

#
# The default LaunchDarkly client configuration. This configuration sets reasonable defaults for most users.
#
# @return [Config] The default LaunchDarkly configuration.
def self.default
Config.new('https://app.launchdarkly.com')
Config.new()
end

def self.default_capacity
10000
end

def self.default_base_uri
"https://app.launchdarkly.com"
end

def self.default_store
defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : ThreadSafeMemoryStore.new
end

def self.default_flush_interval
30
end

def self.default_logger
defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : ::Logger.new($stdout)
end

end
end
175 changes: 148 additions & 27 deletions lib/ldclient-rb/ldclient.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,130 @@
require 'faraday/http_cache'
require 'json'
require 'digest/sha1'
require 'thread'
require 'logger'

module LaunchDarkly
#
# A client for the LaunchDarkly API. Client instances are thread-safe. Users
# should create a single client instance for the lifetime of the application.
#
#
class LDClient

LONG_SCALE = Float(0xFFFFFFFFFFFFFFF)

#
# Creates a new client instance that connects to LaunchDarkly. A custom
# configuration parameter can also supplied to specify advanced options,
# but for most use cases, the default configuration is appropriate.
#
#
# @param api_key [String] the API key for your LaunchDarkly account
# @param config [Config] an optional client configuration object
#
# @return [LDClient] The LaunchDarkly client instance
def initialize(api_key, config = Config.default)
store = ThreadSafeMemoryStore.new
@queue = Queue.new
@api_key = api_key
@config = config
@client = Faraday.new do |builder|
builder.use :http_cache, store: store
builder.use :http_cache, store: @config.store

builder.adapter Faraday.default_adapter
end

Thread.new do
while true do
events = []
num_events = @queue.length()
num_events.times do
events << @queue.pop()
end

if !events.empty?()
res =
@client.post (@config.base_uri + "/api/events/bulk") do |req|
req.headers['Authorization'] = 'api_key ' + @api_key
req.headers['User-Agent'] = 'RubyClient/' + LaunchDarkly::VERSION
req.headers['Content-Type'] = 'application/json'
req.body = events.to_json
end
if res.status != 200
@config.logger.error("Unexpected status code while processing events: " + res.status)
end
end

sleep(@config.flush_interval)
end
end

end

#
# Calculates the value of a feature flag for a given user. At a minimum, the user hash
# should contain a +:key+ .
#
# @example Basic user hash
# {:key => "user@example.com"}
#
# For authenticated users, the +:key+ should be the unique identifier for your user. For anonymous users,
# the +:key+ should be a session identifier or cookie. In either case, the only requirement is that the key
# is unique to a user.
#
# You can also pass IP addresses and country codes in the user hash.
#
# @example More complete user hash
# {:key => "user@example.com", :ip => "127.0.0.1", :country => "US"}
#
# Countries should be sent as ISO 3166-1 alpha-2 codes.
#
# The user hash can contain arbitrary custom attributes stored in a +:custom+ sub-hash:
#
# @example A user hash with custom attributes
# {:key => "user@example.com", :custom => {:customer_rank => 1000, :groups => ["google", "microsoft"]}}
#
# Attribute values in the custom hash can be integers, booleans, strings, or lists of integers, booleans, or strings.
#
# @param key [String] the unique feature key for the feature flag, as shown on the LaunchDarkly dashboard
# @param user [Hash] a hash containing parameters for the end user requesting the flag
# @param default=false [Boolean] the default value of the flag
#
# @return [Boolean] whether or not the flag should be enabled, or the default value if the flag is disabled on the LaunchDarkly control panel
def get_flag?(key, user, default=false)
begin
value = get_flag_int(key, user, default)
add_event({:kind => 'feature', :key => key, :user => user, :value => value})
return value
rescue StandardError => error
@config.logger.error("Unhandled exception in get_flag: " + error.message)
default
end
end

def add_event(event)
if @queue.length() < @config.capacity
event[:creationDate] = (Time.now.to_f * 1000).to_i
@queue.push(event)
else
@config.logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events.")
end
end

#
# Tracks that a user performed an event
#
# @param event_name [String] The name of the event
# @param user [Hash] The user that performed the event. This should be the same user hash used in calls to {#get_flag?}
# @param data [Hash] A hash containing any additional data associated with the event
#
# @return [void]
def send_event(event_name, user, data)
add_event({:kind => 'custom', :key => event_name, :user => user, :data => data })
end

def get_flag_int(key, user, default)

unless user
@config.logger.error("Must specify user")
return default
end

Expand All @@ -30,51 +134,68 @@ def get_flag?(key, user, default=false)
req.headers['User-Agent'] = 'RubyClient/' + LaunchDarkly::VERSION
end

feature = JSON.parse(res.body)
if res.status == 401
@config.logger.error("Invalid API key")
return default
end

if res.status == 404
@config.logger.error("Unknown feature key: " + key)
return default
end

if res.status != 200
@config.logger.error("Unexpected status code " + res.status)
return default
end


feature = JSON.parse(res.body, :symbolize_names => true)

val = evaluate(feature, user)

val == nil ? default : val
end

def param_for_user(feature, user)
if user.has_key? 'key'
id_hash = user['key']
if user.has_key? :key
id_hash = user[:key]
else
return nil
end

if user.has_key? 'secondary'
id_hash += '.' + user['secondary']
if user.has_key? :secondary
id_hash += '.' + user[:secondary]
end

hash_key = "%s.%s.%s" % [feature['key'], feature['salt'], id_hash]
hash_key = "%s.%s.%s" % [feature[:key], feature[:salt], id_hash]

hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
return hash_val.to_i(16) / LONG_SCALE
return hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
end

def match_target?(target, user)
attrib = target['attribute']
attrib = target[:attribute].to_sym

if attrib == 'key' or attrib == 'ip' or attrib == 'country'
if attrib == :key or attrib == :ip or attrib == :country
if user[attrib]
u_value = user[attrib]
return target['values'].include? u_value
return target[:values].include? u_value
else
return false
end
else # custom attribute
unless user.has_key? 'custom'
unless user.has_key? :custom
return false
end
unless user['custom'].include? attrib
unless user[:custom].include? attrib
return false
end
u_value = user['custom'][attrib]
u_value = user[:custom][attrib]
if u_value.is_a? String or u_value.is_a? Numeric
return target['values'].include? u_value
return target[:values].include? u_value
elsif u_value.is_a? Array
return ! ((target['values'] & u_value).empty?)
return ! ((target[:values] & u_value).empty?)
end

return false
Expand All @@ -83,7 +204,7 @@ def match_target?(target, user)
end

def match_variation?(variation, user)
variation['targets'].each do |target|
variation[:targets].each do |target|
if match_target?(target, user)
return true
end
Expand All @@ -92,7 +213,7 @@ def match_variation?(variation, user)
end

def evaluate(feature, user)
unless feature['on']
unless feature[:on]
return nil
end

Expand All @@ -102,26 +223,26 @@ def evaluate(feature, user)
return nil
end

feature['variations'].each do |variation|
feature[:variations].each do |variation|
if match_variation?(variation, user)
return variation['value']
return variation[:value]
end
end

total = 0.0
feature['variations'].each do |variation|
total += variation['weight'].to_f / 100.0
feature[:variations].each do |variation|
total += variation[:weight].to_f / 100.0

if param < total
return variation['value']
return variation[:value]
end
end

return nil

end

private :param_for_user, :match_target?, :match_variation?, :evaluate
private :add_event, :get_flag_int, :param_for_user, :match_target?, :match_variation?, :evaluate


end
Expand Down
Loading