Skip to content

prepare 5.2.0 release #107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Aug 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f5092af
Remove @ashanbrown from codeowners
ashanbrown Jul 24, 2018
fd63b2b
log exception stacktraces at debug level
eli-darkly Aug 2, 2018
d4be186
re-add minimal unit test
eli-darkly Aug 2, 2018
d73d66c
log exceptions at error level
eli-darkly Aug 2, 2018
94c8485
Merge pull request #75 from launchdarkly/eb/ch19339/exception-logging
eli-darkly Aug 2, 2018
ca15234
add new version of all_flags that captures more metadata
eli-darkly Aug 17, 2018
ed19523
add tests for FeatureFlagsState
eli-darkly Aug 17, 2018
73f2d89
provide as_json method that returns a hash instead of just a string
eli-darkly Aug 20, 2018
ab896b1
state can be serialized with JSON.generate
eli-darkly Aug 21, 2018
00347c6
add $valid
eli-darkly Aug 21, 2018
bdac27e
add ability to filter for only client-side flags
eli-darkly Aug 21, 2018
748b59b
Merge pull request #76 from launchdarkly/eb/ch22308/all-flags-state
eli-darkly Aug 21, 2018
50b3aa5
Merge pull request #77 from launchdarkly/eb/ch12124/client-side-filter
eli-darkly Aug 21, 2018
cee4c18
implement evaluation with explanations
eli-darkly Aug 23, 2018
d2c2ab8
misc cleanup
eli-darkly Aug 23, 2018
64a00a1
misc cleanup, more error checking
eli-darkly Aug 23, 2018
46b642b
don't keep evaluating prerequisites if one fails
eli-darkly Aug 23, 2018
855c4e2
doc comment
eli-darkly Aug 23, 2018
a0f002f
rename variation to variation_index
eli-darkly Aug 23, 2018
4ec43db
comment
eli-darkly Aug 23, 2018
9622e01
more test coverage, convenience method
eli-darkly Aug 24, 2018
7a453b0
Merge branch 'master' of github.com:launchdarkly/ruby-client
eli-darkly Aug 27, 2018
88d217e
Merge branch 'master' of github.com:launchdarkly/ruby-client
eli-darkly Aug 27, 2018
084d9ea
fix event generation for a prerequisite that is off
eli-darkly Aug 29, 2018
02b5712
fix private
eli-darkly Aug 29, 2018
53e8408
Merge pull request #78 from launchdarkly/eb/ch19976/explanations
eli-darkly Aug 29, 2018
c78db15
Merge pull request #79 from launchdarkly/eb/ch22995/prereq-off
eli-darkly Aug 29, 2018
960bb89
Merge branch 'explanation'
eli-darkly Aug 30, 2018
530ff9e
version 5.2.0
eli-darkly Aug 30, 2018
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

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

## [5.2.0] - 2018-08-29
### Added:
- The new `LDClient` method `variation_detail` allows you to evaluate a feature flag (using the same parameters as you would for `variation`) and receive more information about how the value was calculated. This information is returned in an `EvaluationDetail` object, which contains both the result value and a "reason" object which will tell you, for instance, if the user was individually targeted for the flag or was matched by one of the flag's rules, or if the flag returned the default value due to an error.

### Fixed:
- Evaluating a prerequisite feature flag did not produce an analytics event if the prerequisite flag was off.


## [5.1.0] - 2018-08-27
### Added:
- 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.
Expand Down
200 changes: 123 additions & 77 deletions lib/ldclient-rb/evaluation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@
require "semantic"

module LaunchDarkly
# An object returned by `LDClient.variation_detail`, combining the result of a flag evaluation with
# an explanation of how it was calculated.
class EvaluationDetail
def initialize(value, variation_index, reason)
@value = value
@variation_index = variation_index
@reason = reason
end

# @return [Object] The result of the flag evaluation. This will be either one of the flag's
# variations or the default value that was passed to the `variation` method.
attr_reader :value

# @return [int|nil] The index of the returned value within the flag's list of variations, e.g.
# 0 for the first variation - or `nil` if the default value was returned.
attr_reader :variation_index

# @return [Hash] An object describing the main factor that influenced the flag evaluation value.
attr_reader :reason

# @return [boolean] True if the flag evaluated to the default value rather than to one of its
# variations.
def default_value?
variation_index.nil?
end

def ==(other)
@value == other.value && @variation_index == other.variation_index && @reason == other.reason
end
end

module Evaluation
BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]

Expand Down Expand Up @@ -107,113 +138,103 @@ def self.comparator(converter)
end
}

class EvaluationError < StandardError
# Used internally to hold an evaluation result and the events that were generated from prerequisites.
EvalResult = Struct.new(:detail, :events)

def error_result(errorKind, value = nil)
EvaluationDetail.new(value, nil, { kind: 'ERROR', errorKind: errorKind })
end

# Evaluates a feature flag, returning a hash containing the evaluation result and any events
# generated during prerequisite evaluation. Raises EvaluationError if the flag is not well-formed
# Will return nil, but not raise an exception, indicating that the rules (including fallthrough) did not match
# In that case, the caller should return the default value.
# Evaluates a feature flag and returns an EvalResult. The result.value will be nil if the flag returns
# the default value. Error conditions produce a result with an error reason, not an exception.
def evaluate(flag, user, store, logger)
if flag.nil?
raise EvaluationError, "Flag does not exist"
end

if user.nil? || user[:key].nil?
raise EvaluationError, "Invalid user"
return EvalResult.new(error_result('USER_NOT_SPECIFIED'), [])
end

events = []
detail = eval_internal(flag, user, store, events, logger)
return EvalResult.new(detail, events)
end

def eval_internal(flag, user, store, events, logger)
if !flag[:on]
return get_off_value(flag, { kind: 'OFF' }, logger)
end

if flag[:on]
res = eval_internal(flag, user, store, events, logger)
if !res.nil?
res[:events] = events
return res
prereq_failure_reason = check_prerequisites(flag, user, store, events, logger)
if !prereq_failure_reason.nil?
return get_off_value(flag, prereq_failure_reason, logger)
end

# Check user target matches
(flag[:targets] || []).each do |target|
(target[:values] || []).each do |value|
if value == user[:key]
return get_variation(flag, target[:variation], { kind: 'TARGET_MATCH' }, logger)
end
end
end

# Check custom rules
rules = flag[:rules] || []
rules.each_index do |i|
rule = rules[i]
if rule_match_user(rule, user, store)
return get_value_for_variation_or_rollout(flag, rule, user,
{ kind: 'RULE_MATCH', ruleIndex: i, ruleId: rule[:id] }, logger)
end
end

offVariation = flag[:offVariation]
if !offVariation.nil? && offVariation < flag[:variations].length
value = flag[:variations][offVariation]
return { variation: offVariation, value: value, events: events }
# Check the fallthrough rule
if !flag[:fallthrough].nil?
return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user,
{ kind: 'FALLTHROUGH' }, logger)
end

{ variation: nil, value: nil, events: events }
return EvaluationDetail.new(nil, nil, { kind: 'FALLTHROUGH' })
end

def eval_internal(flag, user, store, events, logger)
failed_prereq = false
# Evaluate prerequisites, if any
def check_prerequisites(flag, user, store, events, logger)
(flag[:prerequisites] || []).each do |prerequisite|
prereq_flag = store.get(FEATURES, prerequisite[:key])
prereq_ok = true
prereq_key = prerequisite[:key]
prereq_flag = store.get(FEATURES, prereq_key)

if prereq_flag.nil? || !prereq_flag[:on]
failed_prereq = true
if prereq_flag.nil?
logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
prereq_ok = false
else
begin
prereq_res = eval_internal(prereq_flag, user, store, events, logger)
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
# off variation was. But we still need to evaluate it in order to generate an event.
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
prereq_ok = false
end
event = {
kind: "feature",
key: prereq_flag[:key],
variation: prereq_res.nil? ? nil : prereq_res[:variation],
value: prereq_res.nil? ? nil : prereq_res[:value],
key: prereq_key,
variation: prereq_res.variation_index,
value: prereq_res.value,
version: prereq_flag[:version],
prereqOf: flag[:key],
trackEvents: prereq_flag[:trackEvents],
debugEventsUntilDate: prereq_flag[:debugEventsUntilDate]
}
events.push(event)
if prereq_res.nil? || prereq_res[:variation] != prerequisite[:variation]
failed_prereq = true
end
rescue => exn
logger.error { "[LDClient] Error evaluating prerequisite: #{exn.inspect}" }
failed_prereq = true
Util.log_exception(logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"{flag[:key]}\"", exn)
prereq_ok = false
end
end
end

if failed_prereq
return nil
end
# The prerequisites were satisfied.
# Now walk through the evaluation steps and get the correct
# variation index
eval_rules(flag, user, store)
end

def eval_rules(flag, user, store)
# Check user target matches
(flag[:targets] || []).each do |target|
(target[:values] || []).each do |value|
if value == user[:key]
return { variation: target[:variation], value: get_variation(flag, target[:variation]) }
end
if !prereq_ok
return { kind: 'PREREQUISITE_FAILED', prerequisiteKey: prereq_key }
end
end

# Check custom rules
(flag[:rules] || []).each do |rule|
return variation_for_user(rule, user, flag) if rule_match_user(rule, user, store)
end

# Check the fallthrough rule
if !flag[:fallthrough].nil?
return variation_for_user(flag[:fallthrough], user, flag)
end

# Not even the fallthrough matched-- return the off variation or default
nil
end

def get_variation(flag, index)
if index >= flag[:variations].length
raise EvaluationError, "Invalid variation index"
end
flag[:variations][index]
end

def rule_match_user(rule, user, store)
return false if !rule[:clauses]

Expand Down Expand Up @@ -242,9 +263,8 @@ def clause_match_user_no_segments(clause, user)
return false if val.nil?

op = OPERATORS[clause[:op].to_sym]

if op.nil?
raise EvaluationError, "Unsupported operator #{clause[:op]} in evaluation"
return false
end

if val.is_a? Enumerable
Expand All @@ -257,9 +277,9 @@ def clause_match_user_no_segments(clause, user)
maybe_negate(clause, match_any(op, val, clause[:values]))
end

def variation_for_user(rule, user, flag)
def variation_index_for_user(flag, rule, user)
if !rule[:variation].nil? # fixed variation
return { variation: rule[:variation], value: get_variation(flag, rule[:variation]) }
return rule[:variation]
elsif !rule[:rollout].nil? # percentage rollout
rollout = rule[:rollout]
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
Expand All @@ -268,12 +288,12 @@ def variation_for_user(rule, user, flag)
rollout[:variations].each do |variate|
sum += variate[:weight].to_f / 100000.0
if bucket < sum
return { variation: variate[:variation], value: get_variation(flag, variate[:variation]) }
return variate[:variation]
end
end
nil
else # the rule isn't well-formed
raise EvaluationError, "Rule does not define a variation or rollout"
nil
end
end

Expand Down Expand Up @@ -350,5 +370,31 @@ def match_any(op, value, values)
end
return false
end

private

def get_variation(flag, index, reason, logger)
if index < 0 || index >= flag[:variations].length
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
return error_result('MALFORMED_FLAG')
end
EvaluationDetail.new(flag[:variations][index], index, reason)
end

def get_off_value(flag, reason, logger)
if flag[:offVariation].nil? # off variation unspecified - return default value
return EvaluationDetail.new(nil, nil, reason)
end
get_variation(flag, flag[:offVariation], reason, logger)
end

def get_value_for_variation_or_rollout(flag, vr, user, reason, logger)
index = variation_index_for_user(flag, vr, user)
if index.nil?
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
return error_result('MALFORMED_FLAG')
end
return get_variation(flag, index, reason, logger)
end
end
end
1 change: 1 addition & 0 deletions lib/ldclient-rb/events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ def make_output_event(event)
else
out[:userKey] = event[:user].nil? ? nil : event[:user][:key]
end
out[:reason] = event[:reason] if !event[:reason].nil?
out
when "identify"
{
Expand Down
3 changes: 2 additions & 1 deletion lib/ldclient-rb/flags_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ def initialize(valid)
end

# Used internally to build the state map.
def add_flag(flag, value, variation)
def add_flag(flag, value, variation, reason = nil)
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]
meta[:reason] = reason if !reason.nil?
@flag_metadata[key] = meta
end

Expand Down
Loading