Skip to content

Commit a0afa81

Browse files
committed
Merge pull request #1 from launchdarkly/event-processing
Event processing
2 parents b163b03 + d41a43f commit a0afa81

File tree

5 files changed

+268
-32
lines changed

5 files changed

+268
-32
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ LaunchDarkly SDK for Ruby
44
Quick setup
55
-----------
66

7-
1. Install the Ruby SDK with `gem`
7+
0. Install the Ruby SDK with `gem`
88

99
gem install ldclient-rb
1010

11+
1. Require the LaunchDarkly client:
12+
13+
require 'ldclient-rb'
14+
15+
1116
2. Create a new LDClient with your API key:
1217

1318
client = LaunchDarkly::LDClient.new("your_api_key")

lib/ldclient-rb/config.rb

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,102 @@
1+
require 'logger'
2+
13
module LaunchDarkly
4+
5+
#
6+
# This class exposes advanced configuration options for the LaunchDarkly client library. Most users
7+
# will not need to use a custom configuration-- the default configuration sets sane defaults for most use cases.
8+
#
9+
#
210
class Config
3-
def initialize(base_uri)
4-
@base_uri = base_uri
11+
#
12+
# Constructor for creating custom LaunchDarkly configurations.
13+
#
14+
# @param opts [Hash] the configuration options
15+
# @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.
16+
# @option opts [String] :base_uri ("https://app.launchdarkly.com") The base URL for the LaunchDarkly server. Most users should use the default value.
17+
# @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.
18+
# @option opts [Integer] :flush_interval (30) The number of seconds between flushes of the event buffer.
19+
# @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.
20+
#
21+
# @return [type] [description]
22+
def initialize(opts = {})
23+
@base_uri = opts[:base_uri] || Config.default_base_uri
24+
@capacity = opts[:capacity] || Config.default_capacity
25+
@logger = opts[:logger] || Config.default_logger
26+
@store = opts[:store] || Config.default_store
27+
@flush_interval = opts[:flush_interval] || Config.default_flush_interval
528
end
629

30+
#
31+
# The base URL for the LaunchDarkly server.
32+
#
33+
# @return [String] The configured base URL for the LaunchDarkly server.
734
def base_uri
835
@base_uri
936
end
1037

38+
#
39+
# The number of seconds between flushes of the event buffer. Decreasing the flush interval means
40+
# that the event buffer is less likely to reach capacity.
41+
#
42+
# @return [Integer] The configured number of seconds between flushes of the event buffer.
43+
def flush_interval
44+
@flush_interval
45+
end
46+
47+
#
48+
# The configured logger for the LaunchDarkly client. The client library uses the log to
49+
# print warning and error messages.
50+
#
51+
# @return [Logger] The configured logger
52+
def logger
53+
@logger
54+
end
55+
56+
#
57+
# 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.
58+
# Increasing the capacity means that events are less likely to be discarded, at the cost of consuming more memory.
59+
#
60+
# @return [Integer] The configured capacity of the event buffer
61+
def capacity
62+
@capacity
63+
end
64+
65+
#
66+
# The store for the Faraday HTTP caching library. Stores should respond to 'read' and 'write' requests.
67+
#
68+
# @return [Object] The configured store for the Faraday HTTP caching library.
69+
def store
70+
@store
71+
end
72+
73+
#
74+
# The default LaunchDarkly client configuration. This configuration sets reasonable defaults for most users.
75+
#
76+
# @return [Config] The default LaunchDarkly configuration.
1177
def self.default
12-
Config.new('https://app.launchdarkly.com')
78+
Config.new()
1379
end
80+
81+
def self.default_capacity
82+
10000
83+
end
84+
85+
def self.default_base_uri
86+
"https://app.launchdarkly.com"
87+
end
88+
89+
def self.default_store
90+
defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : ThreadSafeMemoryStore.new
91+
end
92+
93+
def self.default_flush_interval
94+
30
95+
end
96+
97+
def self.default_logger
98+
defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : ::Logger.new($stdout)
99+
end
100+
14101
end
15102
end

lib/ldclient-rb/ldclient.rb

Lines changed: 148 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,130 @@
11
require 'faraday/http_cache'
22
require 'json'
33
require 'digest/sha1'
4+
require 'thread'
5+
require 'logger'
46

57
module LaunchDarkly
8+
#
9+
# A client for the LaunchDarkly API. Client instances are thread-safe. Users
10+
# should create a single client instance for the lifetime of the application.
11+
#
12+
#
613
class LDClient
714

8-
LONG_SCALE = Float(0xFFFFFFFFFFFFFFF)
9-
15+
#
16+
# Creates a new client instance that connects to LaunchDarkly. A custom
17+
# configuration parameter can also supplied to specify advanced options,
18+
# but for most use cases, the default configuration is appropriate.
19+
#
20+
#
21+
# @param api_key [String] the API key for your LaunchDarkly account
22+
# @param config [Config] an optional client configuration object
23+
#
24+
# @return [LDClient] The LaunchDarkly client instance
1025
def initialize(api_key, config = Config.default)
11-
store = ThreadSafeMemoryStore.new
26+
@queue = Queue.new
1227
@api_key = api_key
1328
@config = config
1429
@client = Faraday.new do |builder|
15-
builder.use :http_cache, store: store
30+
builder.use :http_cache, store: @config.store
1631

1732
builder.adapter Faraday.default_adapter
1833
end
34+
35+
Thread.new do
36+
while true do
37+
events = []
38+
num_events = @queue.length()
39+
num_events.times do
40+
events << @queue.pop()
41+
end
42+
43+
if !events.empty?()
44+
res =
45+
@client.post (@config.base_uri + "/api/events/bulk") do |req|
46+
req.headers['Authorization'] = 'api_key ' + @api_key
47+
req.headers['User-Agent'] = 'RubyClient/' + LaunchDarkly::VERSION
48+
req.headers['Content-Type'] = 'application/json'
49+
req.body = events.to_json
50+
end
51+
if res.status != 200
52+
@config.logger.error("Unexpected status code while processing events: " + res.status)
53+
end
54+
end
55+
56+
sleep(@config.flush_interval)
57+
end
58+
end
59+
1960
end
2061

62+
#
63+
# Calculates the value of a feature flag for a given user. At a minimum, the user hash
64+
# should contain a +:key+ .
65+
#
66+
# @example Basic user hash
67+
# {:key => "user@example.com"}
68+
#
69+
# For authenticated users, the +:key+ should be the unique identifier for your user. For anonymous users,
70+
# the +:key+ should be a session identifier or cookie. In either case, the only requirement is that the key
71+
# is unique to a user.
72+
#
73+
# You can also pass IP addresses and country codes in the user hash.
74+
#
75+
# @example More complete user hash
76+
# {:key => "user@example.com", :ip => "127.0.0.1", :country => "US"}
77+
#
78+
# Countries should be sent as ISO 3166-1 alpha-2 codes.
79+
#
80+
# The user hash can contain arbitrary custom attributes stored in a +:custom+ sub-hash:
81+
#
82+
# @example A user hash with custom attributes
83+
# {:key => "user@example.com", :custom => {:customer_rank => 1000, :groups => ["google", "microsoft"]}}
84+
#
85+
# Attribute values in the custom hash can be integers, booleans, strings, or lists of integers, booleans, or strings.
86+
#
87+
# @param key [String] the unique feature key for the feature flag, as shown on the LaunchDarkly dashboard
88+
# @param user [Hash] a hash containing parameters for the end user requesting the flag
89+
# @param default=false [Boolean] the default value of the flag
90+
#
91+
# @return [Boolean] whether or not the flag should be enabled, or the default value if the flag is disabled on the LaunchDarkly control panel
2192
def get_flag?(key, user, default=false)
93+
begin
94+
value = get_flag_int(key, user, default)
95+
add_event({:kind => 'feature', :key => key, :user => user, :value => value})
96+
return value
97+
rescue StandardError => error
98+
@config.logger.error("Unhandled exception in get_flag: " + error.message)
99+
default
100+
end
101+
end
102+
103+
def add_event(event)
104+
if @queue.length() < @config.capacity
105+
event[:creationDate] = (Time.now.to_f * 1000).to_i
106+
@queue.push(event)
107+
else
108+
@config.logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events.")
109+
end
110+
end
111+
112+
#
113+
# Tracks that a user performed an event
114+
#
115+
# @param event_name [String] The name of the event
116+
# @param user [Hash] The user that performed the event. This should be the same user hash used in calls to {#get_flag?}
117+
# @param data [Hash] A hash containing any additional data associated with the event
118+
#
119+
# @return [void]
120+
def send_event(event_name, user, data)
121+
add_event({:kind => 'custom', :key => event_name, :user => user, :data => data })
122+
end
123+
124+
def get_flag_int(key, user, default)
22125

23126
unless user
127+
@config.logger.error("Must specify user")
24128
return default
25129
end
26130

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

33-
feature = JSON.parse(res.body)
137+
if res.status == 401
138+
@config.logger.error("Invalid API key")
139+
return default
140+
end
141+
142+
if res.status == 404
143+
@config.logger.error("Unknown feature key: " + key)
144+
return default
145+
end
146+
147+
if res.status != 200
148+
@config.logger.error("Unexpected status code " + res.status)
149+
return default
150+
end
151+
152+
153+
feature = JSON.parse(res.body, :symbolize_names => true)
34154

35155
val = evaluate(feature, user)
36156

37157
val == nil ? default : val
38158
end
39159

40160
def param_for_user(feature, user)
41-
if user.has_key? 'key'
42-
id_hash = user['key']
161+
if user.has_key? :key
162+
id_hash = user[:key]
43163
else
44164
return nil
45165
end
46166

47-
if user.has_key? 'secondary'
48-
id_hash += '.' + user['secondary']
167+
if user.has_key? :secondary
168+
id_hash += '.' + user[:secondary]
49169
end
50170

51-
hash_key = "%s.%s.%s" % [feature['key'], feature['salt'], id_hash]
171+
hash_key = "%s.%s.%s" % [feature[:key], feature[:salt], id_hash]
172+
52173
hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
53-
return hash_val.to_i(16) / LONG_SCALE
174+
return hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
54175
end
55176

56177
def match_target?(target, user)
57-
attrib = target['attribute']
178+
attrib = target[:attribute].to_sym
58179

59-
if attrib == 'key' or attrib == 'ip' or attrib == 'country'
180+
if attrib == :key or attrib == :ip or attrib == :country
60181
if user[attrib]
61182
u_value = user[attrib]
62-
return target['values'].include? u_value
183+
return target[:values].include? u_value
63184
else
64185
return false
65186
end
66187
else # custom attribute
67-
unless user.has_key? 'custom'
188+
unless user.has_key? :custom
68189
return false
69190
end
70-
unless user['custom'].include? attrib
191+
unless user[:custom].include? attrib
71192
return false
72193
end
73-
u_value = user['custom'][attrib]
194+
u_value = user[:custom][attrib]
74195
if u_value.is_a? String or u_value.is_a? Numeric
75-
return target['values'].include? u_value
196+
return target[:values].include? u_value
76197
elsif u_value.is_a? Array
77-
return ! ((target['values'] & u_value).empty?)
198+
return ! ((target[:values] & u_value).empty?)
78199
end
79200

80201
return false
@@ -83,7 +204,7 @@ def match_target?(target, user)
83204
end
84205

85206
def match_variation?(variation, user)
86-
variation['targets'].each do |target|
207+
variation[:targets].each do |target|
87208
if match_target?(target, user)
88209
return true
89210
end
@@ -92,7 +213,7 @@ def match_variation?(variation, user)
92213
end
93214

94215
def evaluate(feature, user)
95-
unless feature['on']
216+
unless feature[:on]
96217
return nil
97218
end
98219

@@ -102,26 +223,26 @@ def evaluate(feature, user)
102223
return nil
103224
end
104225

105-
feature['variations'].each do |variation|
226+
feature[:variations].each do |variation|
106227
if match_variation?(variation, user)
107-
return variation['value']
228+
return variation[:value]
108229
end
109230
end
110231

111232
total = 0.0
112-
feature['variations'].each do |variation|
113-
total += variation['weight'].to_f / 100.0
233+
feature[:variations].each do |variation|
234+
total += variation[:weight].to_f / 100.0
114235

115236
if param < total
116-
return variation['value']
237+
return variation[:value]
117238
end
118239
end
119240

120241
return nil
121242

122243
end
123244

124-
private :param_for_user, :match_target?, :match_variation?, :evaluate
245+
private :add_event, :get_flag_int, :param_for_user, :match_target?, :match_variation?, :evaluate
125246

126247

127248
end

0 commit comments

Comments
 (0)