Skip to content

Commit

Permalink
Merge pull request #76 from launchdarkly/eb/ch22308/all-flags-state
Browse files Browse the repository at this point in the history
add new version of all_flags that captures more metadata
  • Loading branch information
eli-darkly authored Aug 21, 2018
2 parents 94c8485 + 00347c6 commit 748b59b
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 11 deletions.
1 change: 1 addition & 0 deletions lib/ldclient-rb.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "ldclient-rb/version"
require "ldclient-rb/util"
require "ldclient-rb/evaluation"
require "ldclient-rb/flags_state"
require "ldclient-rb/ldclient"
require "ldclient-rb/cache_store"
require "ldclient-rb/expiring_cache"
Expand Down
66 changes: 66 additions & 0 deletions lib/ldclient-rb/flags_state.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
require 'json'

module LaunchDarkly
#
# A snapshot of the state of all feature flags with regard to a specific user, generated by
# calling the client's all_flags_state method. Serializing this object to JSON using
# JSON.generate (or the to_json method) will produce the appropriate data structure for
# bootstrapping the LaunchDarkly JavaScript client.
#
class FeatureFlagsState
def initialize(valid)
@flag_values = {}
@flag_metadata = {}
@valid = valid
end

# Used internally to build the state map.
def add_flag(flag, value, variation)
key = flag[:key]
@flag_values[key] = value
meta = { version: flag[:version], trackEvents: flag[:trackEvents] }
meta[:variation] = variation if !variation.nil?
meta[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
@flag_metadata[key] = meta
end

# Returns true if this object contains a valid snapshot of feature flag state, or false if the
# state could not be computed (for instance, because the client was offline or there was no user).
def valid?
@valid
end

# Returns the value of an individual feature flag at the time the state was recorded.
# Returns nil if the flag returned the default value, or if there was no such flag.
def flag_value(key)
@flag_values[key]
end

# Returns a map of flag keys to flag values. If a flag would have evaluated to the default value,
# its value will be nil.
#
# Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client.
# Instead, use as_json.
def values_map
@flag_values
end

# Returns a hash that can be used as a JSON representation of the entire state map, in the format
# used by the LaunchDarkly JavaScript SDK. Use this method if you are passing data to the front end
# in order to "bootstrap" the JavaScript client.
#
# Do not rely on the exact shape of this data, as it may change in future to support the needs of
# the JavaScript client.
def as_json(*) # parameter is unused, but may be passed if we're using the json gem
ret = @flag_values.clone
ret['$flagsState'] = @flag_metadata
ret['$valid'] = @valid
ret
end

# Same as as_json, but converts the JSON structure into a string.
def to_json(*a)
as_json.to_json(a)
end
end
end
44 changes: 33 additions & 11 deletions lib/ldclient-rb/ldclient.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,26 +193,48 @@ def track(event_name, user, data)
end

#
# Returns all feature flag values for the given user
# Returns all feature flag values for the given user. This method is deprecated - please use
# all_flags_state instead. Current versions of the client-side SDK will not generate analytics
# events correctly if you pass the result of all_flags.
#
def all_flags(user)
sanitize_user(user)
return Hash.new if @config.offline?
all_flags_state(user).values_map
end

unless user
@config.logger.error { "[LDClient] Must specify user in all_flags" }
return Hash.new
#
# Returns a FeatureFlagsState object that encapsulates the state of all feature flags for a given user,
# including the flag values and also metadata that can be used on the front end. This method does not
# send analytics events back to LaunchDarkly.
#
def all_flags_state(user)
return FeatureFlagsState.new(false) if @config.offline?

unless user && !user[:key].nil?
@config.logger.error { "[LDClient] User and user key must be specified in all_flags_state" }
return FeatureFlagsState.new(false)
end

sanitize_user(user)

begin
features = @store.all(FEATURES)

# TODO rescue if necessary
Hash[features.map{ |k, f| [k, evaluate(f, user, @store, @config.logger)[:value]] }]
rescue => exn
Util.log_exception(@config.logger, "Error evaluating all flags", exn)
return Hash.new
Util.log_exception(@config.logger, "Unable to read flags for all_flags_state", exn)
return FeatureFlagsState.new(false)
end

state = FeatureFlagsState.new(true)
features.each do |k, f|
begin
result = evaluate(f, user, @store, @config.logger)
state.add_flag(f, result[:value], result[:variation])
rescue => exn
Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
state.add_flag(f, nil, nil)
end
end

state
end

#
Expand Down
82 changes: 82 additions & 0 deletions spec/flags_state_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require "spec_helper"
require "json"

describe LaunchDarkly::FeatureFlagsState do
subject { LaunchDarkly::FeatureFlagsState }

it "can get flag value" do
state = subject.new(true)
flag = { key: 'key' }
state.add_flag(flag, 'value', 1)

expect(state.flag_value('key')).to eq 'value'
end

it "returns nil for unknown flag" do
state = subject.new(true)

expect(state.flag_value('key')).to be nil
end

it "can be converted to values map" do
state = subject.new(true)
flag1 = { key: 'key1' }
flag2 = { key: 'key2' }
state.add_flag(flag1, 'value1', 0)
state.add_flag(flag2, 'value2', 1)

expect(state.values_map).to eq({ 'key1' => 'value1', 'key2' => 'value2' })
end

it "can be converted to JSON structure" do
state = subject.new(true)
flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
state.add_flag(flag1, 'value1', 0)
state.add_flag(flag2, 'value2', 1)

result = state.as_json
expect(result).to eq({
'key1' => 'value1',
'key2' => 'value2',
'$flagsState' => {
'key1' => {
:variation => 0,
:version => 100,
:trackEvents => false
},
'key2' => {
:variation => 1,
:version => 200,
:trackEvents => true,
:debugEventsUntilDate => 1000
}
},
'$valid' => true
})
end

it "can be converted to JSON string" do
state = subject.new(true)
flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
state.add_flag(flag1, 'value1', 0)
state.add_flag(flag2, 'value2', 1)

object = state.as_json
str = state.to_json
expect(object.to_json).to eq(str)
end

it "uses our custom serializer with JSON.generate" do
state = subject.new(true)
flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
state.add_flag(flag1, 'value1', 0)
state.add_flag(flag2, 'value2', 1)

stringFromToJson = state.to_json
stringFromGenerate = JSON.generate(state)
expect(stringFromGenerate).to eq(stringFromToJson)
end
end
92 changes: 92 additions & 0 deletions spec/ldclient_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,98 @@ def event_processor
end
end

describe '#all_flags' do
let(:flag1) { { key: "key1", offVariation: 0, variations: [ 'value1' ] } }
let(:flag2) { { key: "key2", offVariation: 0, variations: [ 'value2' ] } }

it "returns flag values" do
config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })

result = client.all_flags({ key: 'userkey' })
expect(result).to eq({ 'key1' => 'value1', 'key2' => 'value2' })
end

it "returns empty map for nil user" do
config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })

result = client.all_flags(nil)
expect(result).to eq({})
end

it "returns empty map for nil user key" do
config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })

result = client.all_flags({})
expect(result).to eq({})
end

it "returns empty map if offline" do
offline_config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })

result = offline_client.all_flags(nil)
expect(result).to eq({})
end
end

describe '#all_flags_state' do
let(:flag1) { { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false } }
let(:flag2) { { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 } }

it "returns flags state" do
config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })

state = client.all_flags_state({ key: 'userkey' })
expect(state.valid?).to be true

values = state.values_map
expect(values).to eq({ 'key1' => 'value1', 'key2' => 'value2' })

result = state.as_json
expect(result).to eq({
'key1' => 'value1',
'key2' => 'value2',
'$flagsState' => {
'key1' => {
:variation => 0,
:version => 100,
:trackEvents => false
},
'key2' => {
:variation => 1,
:version => 200,
:trackEvents => true,
:debugEventsUntilDate => 1000
}
},
'$valid' => true
})
end

it "returns empty state for nil user" do
config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })

state = client.all_flags_state(nil)
expect(state.valid?).to be false
expect(state.values_map).to eq({})
end

it "returns empty state for nil user key" do
config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })

state = client.all_flags_state({})
expect(state.valid?).to be false
expect(state.values_map).to eq({})
end

it "returns empty state if offline" do
offline_config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })

state = offline_client.all_flags_state({ key: 'userkey' })
expect(state.valid?).to be false
expect(state.values_map).to eq({})
end
end

describe '#secure_mode_hash' do
it "will return the expected value for a known message and secret" do
result = client.secure_mode_hash({key: :Message})
Expand Down

0 comments on commit 748b59b

Please sign in to comment.