Skip to content

Commit a0f0a43

Browse files
authored
prepare 5.1.0 release (#106)
1 parent 6856d28 commit a0f0a43

File tree

11 files changed

+343
-34
lines changed

11 files changed

+343
-34
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
All notable changes to the LaunchDarkly Ruby SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
44

5+
## [5.1.0] - 2018-08-27
6+
### Added:
7+
- The new `LDClient` method `all_flags_state()` should be used instead of `all_flags()` if you are passing flag data to the front end for use with the JavaScript SDK. It preserves some flag metadata that the front end requires in order to send analytics events correctly. Versions 2.5.0 and above of the JavaScript SDK are able to use this metadata, but the output of `all_flags_state()` will still work with older versions.
8+
- The `all_flags_state()` method also allows you to select only client-side-enabled flags to pass to the front end, by using the option `client_side_only: true`.
9+
10+
### Changed:
11+
- Unexpected exceptions are now logged at `ERROR` level, and exception stacktraces at `DEBUG` level. Previously, both were being logged at `WARN` level.
12+
13+
### Deprecated:
14+
- `LDClient.all_flags()`
15+
16+
517
## [5.0.1] - 2018-07-02
618
### Fixed:
719
Fixed a regression in version 5.0.0 that could prevent the client from reconnecting if the stream connection was dropped by the server.

CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
* @ashanbrown
1+

lib/ldclient-rb.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require "ldclient-rb/version"
22
require "ldclient-rb/util"
33
require "ldclient-rb/evaluation"
4+
require "ldclient-rb/flags_state"
45
require "ldclient-rb/ldclient"
56
require "ldclient-rb/cache_store"
67
require "ldclient-rb/expiring_cache"

lib/ldclient-rb/events.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def main_loop(queue, buffer, flush_workers)
142142
message.completed
143143
end
144144
rescue => e
145-
@config.logger.warn { "[LDClient] Unexpected error in event processor: #{e.inspect}. \nTrace: #{e.backtrace}" }
145+
Util.log_exception(@config.logger, "Unexpected error in event processor", e)
146146
end
147147
end
148148
end
@@ -226,7 +226,7 @@ def trigger_flush(buffer, flush_workers)
226226
resp = EventPayloadSendTask.new.run(@sdk_key, @config, @client, payload, @formatter)
227227
handle_response(resp) if !resp.nil?
228228
rescue => e
229-
@config.logger.warn { "[LDClient] Unexpected error in event processor: #{e.inspect}. \nTrace: #{e.backtrace}" }
229+
Util.log_exception(@config.logger, "Unexpected error in event processor", e)
230230
end
231231
end
232232
buffer.clear if success # Reset our internal state, these events now belong to the flush worker

lib/ldclient-rb/flags_state.rb

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
require 'json'
2+
3+
module LaunchDarkly
4+
#
5+
# A snapshot of the state of all feature flags with regard to a specific user, generated by
6+
# calling the client's all_flags_state method. Serializing this object to JSON using
7+
# JSON.generate (or the to_json method) will produce the appropriate data structure for
8+
# bootstrapping the LaunchDarkly JavaScript client.
9+
#
10+
class FeatureFlagsState
11+
def initialize(valid)
12+
@flag_values = {}
13+
@flag_metadata = {}
14+
@valid = valid
15+
end
16+
17+
# Used internally to build the state map.
18+
def add_flag(flag, value, variation)
19+
key = flag[:key]
20+
@flag_values[key] = value
21+
meta = { version: flag[:version], trackEvents: flag[:trackEvents] }
22+
meta[:variation] = variation if !variation.nil?
23+
meta[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
24+
@flag_metadata[key] = meta
25+
end
26+
27+
# Returns true if this object contains a valid snapshot of feature flag state, or false if the
28+
# state could not be computed (for instance, because the client was offline or there was no user).
29+
def valid?
30+
@valid
31+
end
32+
33+
# Returns the value of an individual feature flag at the time the state was recorded.
34+
# Returns nil if the flag returned the default value, or if there was no such flag.
35+
def flag_value(key)
36+
@flag_values[key]
37+
end
38+
39+
# Returns a map of flag keys to flag values. If a flag would have evaluated to the default value,
40+
# its value will be nil.
41+
#
42+
# Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client.
43+
# Instead, use as_json.
44+
def values_map
45+
@flag_values
46+
end
47+
48+
# Returns a hash that can be used as a JSON representation of the entire state map, in the format
49+
# used by the LaunchDarkly JavaScript SDK. Use this method if you are passing data to the front end
50+
# in order to "bootstrap" the JavaScript client.
51+
#
52+
# Do not rely on the exact shape of this data, as it may change in future to support the needs of
53+
# the JavaScript client.
54+
def as_json(*) # parameter is unused, but may be passed if we're using the json gem
55+
ret = @flag_values.clone
56+
ret['$flagsState'] = @flag_metadata
57+
ret['$valid'] = @valid
58+
ret
59+
end
60+
61+
# Same as as_json, but converts the JSON structure into a string.
62+
def to_json(*a)
63+
as_json.to_json(a)
64+
end
65+
end
66+
end

lib/ldclient-rb/ldclient.rb

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def variation(key, user, default)
162162
@event_processor.add_event(make_feature_event(feature, user, res[:variation], value, default))
163163
return value
164164
rescue => exn
165-
@config.logger.warn { "[LDClient] Error evaluating feature flag: #{exn.inspect}. \nTrace: #{exn.backtrace}" }
165+
Util.log_exception(@config.logger, "Error evaluating feature flag", exn)
166166
@event_processor.add_event(make_feature_event(feature, user, nil, default, default))
167167
return default
168168
end
@@ -193,26 +193,61 @@ def track(event_name, user, data)
193193
end
194194

195195
#
196-
# Returns all feature flag values for the given user
196+
# Returns all feature flag values for the given user. This method is deprecated - please use
197+
# {#all_flags_state} instead. Current versions of the client-side SDK will not generate analytics
198+
# events correctly if you pass the result of all_flags.
199+
#
200+
# @param user [Hash] The end user requesting the feature flags
201+
# @return [Hash] a hash of feature flag keys to values
197202
#
198203
def all_flags(user)
199-
sanitize_user(user)
200-
return Hash.new if @config.offline?
204+
all_flags_state(user).values_map
205+
end
201206

202-
unless user
203-
@config.logger.error { "[LDClient] Must specify user in all_flags" }
204-
return Hash.new
207+
#
208+
# Returns a FeatureFlagsState object that encapsulates the state of all feature flags for a given user,
209+
# including the flag values and also metadata that can be used on the front end. This method does not
210+
# send analytics events back to LaunchDarkly.
211+
#
212+
# @param user [Hash] The end user requesting the feature flags
213+
# @param options={} [Hash] Optional parameters to control how the state is generated
214+
# @option options [Boolean] :client_side_only (false) True if only flags marked for use with the
215+
# client-side SDK should be included in the state. By default, all flags are included.
216+
# @return [FeatureFlagsState] a FeatureFlagsState object which can be serialized to JSON
217+
#
218+
def all_flags_state(user, options={})
219+
return FeatureFlagsState.new(false) if @config.offline?
220+
221+
unless user && !user[:key].nil?
222+
@config.logger.error { "[LDClient] User and user key must be specified in all_flags_state" }
223+
return FeatureFlagsState.new(false)
205224
end
206225

226+
sanitize_user(user)
227+
207228
begin
208229
features = @store.all(FEATURES)
209-
210-
# TODO rescue if necessary
211-
Hash[features.map{ |k, f| [k, evaluate(f, user, @store, @config.logger)[:value]] }]
212230
rescue => exn
213-
@config.logger.warn { "[LDClient] Error evaluating all flags: #{exn.inspect}. \nTrace: #{exn.backtrace}" }
214-
return Hash.new
231+
Util.log_exception(@config.logger, "Unable to read flags for all_flags_state", exn)
232+
return FeatureFlagsState.new(false)
215233
end
234+
235+
state = FeatureFlagsState.new(true)
236+
client_only = options[:client_side_only] || false
237+
features.each do |k, f|
238+
if client_only && !f[:clientSide]
239+
next
240+
end
241+
begin
242+
result = evaluate(f, user, @store, @config.logger)
243+
state.add_flag(f, result[:value], result[:variation])
244+
rescue => exn
245+
Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
246+
state.add_flag(f, nil, nil)
247+
end
248+
end
249+
250+
state
216251
end
217252

218253
#
@@ -226,12 +261,6 @@ def close
226261
@store.stop
227262
end
228263

229-
def log_exception(caller, exn)
230-
error_traceback = "#{exn.inspect} #{exn}\n\t#{exn.backtrace.join("\n\t")}"
231-
error = "[LDClient] Unexpected exception in #{caller}: #{error_traceback}"
232-
@config.logger.error { error }
233-
end
234-
235264
def sanitize_user(user)
236265
if user[:key]
237266
user[:key] = user[:key].to_s
@@ -252,7 +281,7 @@ def make_feature_event(flag, user, variation, value, default)
252281
}
253282
end
254283

255-
private :evaluate, :log_exception, :sanitize_user, :make_feature_event
284+
private :evaluate, :sanitize_user, :make_feature_event
256285
end
257286

258287
#

lib/ldclient-rb/util.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11

22
module LaunchDarkly
33
module Util
4+
def self.log_exception(logger, message, exc)
5+
logger.error { "[LDClient] #{message}: #{exc.inspect}" }
6+
logger.debug { "[LDClient] Exception trace: #{exc.backtrace}" }
7+
end
8+
49
def self.http_error_recoverable?(status)
510
if status >= 400 && status < 500
611
status == 400 || status == 408 || status == 429

lib/ldclient-rb/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module LaunchDarkly
2-
VERSION = "5.0.1"
2+
VERSION = "5.1.0"
33
end

spec/flags_state_spec.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
require "spec_helper"
2+
require "json"
3+
4+
describe LaunchDarkly::FeatureFlagsState do
5+
subject { LaunchDarkly::FeatureFlagsState }
6+
7+
it "can get flag value" do
8+
state = subject.new(true)
9+
flag = { key: 'key' }
10+
state.add_flag(flag, 'value', 1)
11+
12+
expect(state.flag_value('key')).to eq 'value'
13+
end
14+
15+
it "returns nil for unknown flag" do
16+
state = subject.new(true)
17+
18+
expect(state.flag_value('key')).to be nil
19+
end
20+
21+
it "can be converted to values map" do
22+
state = subject.new(true)
23+
flag1 = { key: 'key1' }
24+
flag2 = { key: 'key2' }
25+
state.add_flag(flag1, 'value1', 0)
26+
state.add_flag(flag2, 'value2', 1)
27+
28+
expect(state.values_map).to eq({ 'key1' => 'value1', 'key2' => 'value2' })
29+
end
30+
31+
it "can be converted to JSON structure" do
32+
state = subject.new(true)
33+
flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
34+
flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
35+
state.add_flag(flag1, 'value1', 0)
36+
state.add_flag(flag2, 'value2', 1)
37+
38+
result = state.as_json
39+
expect(result).to eq({
40+
'key1' => 'value1',
41+
'key2' => 'value2',
42+
'$flagsState' => {
43+
'key1' => {
44+
:variation => 0,
45+
:version => 100,
46+
:trackEvents => false
47+
},
48+
'key2' => {
49+
:variation => 1,
50+
:version => 200,
51+
:trackEvents => true,
52+
:debugEventsUntilDate => 1000
53+
}
54+
},
55+
'$valid' => true
56+
})
57+
end
58+
59+
it "can be converted to JSON string" do
60+
state = subject.new(true)
61+
flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
62+
flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
63+
state.add_flag(flag1, 'value1', 0)
64+
state.add_flag(flag2, 'value2', 1)
65+
66+
object = state.as_json
67+
str = state.to_json
68+
expect(object.to_json).to eq(str)
69+
end
70+
71+
it "uses our custom serializer with JSON.generate" do
72+
state = subject.new(true)
73+
flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
74+
flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
75+
state.add_flag(flag1, 'value1', 0)
76+
state.add_flag(flag2, 'value2', 1)
77+
78+
stringFromToJson = state.to_json
79+
stringFromGenerate = JSON.generate(state)
80+
expect(stringFromGenerate).to eq(stringFromToJson)
81+
end
82+
end

0 commit comments

Comments
 (0)