Skip to content

Commit 4adda57

Browse files
authored
prepare 5.2.0 release (#107)
1 parent 181fc39 commit 4adda57

File tree

8 files changed

+610
-245
lines changed

8 files changed

+610
-245
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
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.2.0] - 2018-08-29
6+
### Added:
7+
- 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.
8+
9+
### Fixed:
10+
- Evaluating a prerequisite feature flag did not produce an analytics event if the prerequisite flag was off.
11+
12+
513
## [5.1.0] - 2018-08-27
614
### Added:
715
- 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.

lib/ldclient-rb/evaluation.rb

Lines changed: 123 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,37 @@
22
require "semantic"
33

44
module LaunchDarkly
5+
# An object returned by `LDClient.variation_detail`, combining the result of a flag evaluation with
6+
# an explanation of how it was calculated.
7+
class EvaluationDetail
8+
def initialize(value, variation_index, reason)
9+
@value = value
10+
@variation_index = variation_index
11+
@reason = reason
12+
end
13+
14+
# @return [Object] The result of the flag evaluation. This will be either one of the flag's
15+
# variations or the default value that was passed to the `variation` method.
16+
attr_reader :value
17+
18+
# @return [int|nil] The index of the returned value within the flag's list of variations, e.g.
19+
# 0 for the first variation - or `nil` if the default value was returned.
20+
attr_reader :variation_index
21+
22+
# @return [Hash] An object describing the main factor that influenced the flag evaluation value.
23+
attr_reader :reason
24+
25+
# @return [boolean] True if the flag evaluated to the default value rather than to one of its
26+
# variations.
27+
def default_value?
28+
variation_index.nil?
29+
end
30+
31+
def ==(other)
32+
@value == other.value && @variation_index == other.variation_index && @reason == other.reason
33+
end
34+
end
35+
536
module Evaluation
637
BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
738

@@ -107,113 +138,103 @@ def self.comparator(converter)
107138
end
108139
}
109140

110-
class EvaluationError < StandardError
141+
# Used internally to hold an evaluation result and the events that were generated from prerequisites.
142+
EvalResult = Struct.new(:detail, :events)
143+
144+
def error_result(errorKind, value = nil)
145+
EvaluationDetail.new(value, nil, { kind: 'ERROR', errorKind: errorKind })
111146
end
112147

113-
# Evaluates a feature flag, returning a hash containing the evaluation result and any events
114-
# generated during prerequisite evaluation. Raises EvaluationError if the flag is not well-formed
115-
# Will return nil, but not raise an exception, indicating that the rules (including fallthrough) did not match
116-
# In that case, the caller should return the default value.
148+
# Evaluates a feature flag and returns an EvalResult. The result.value will be nil if the flag returns
149+
# the default value. Error conditions produce a result with an error reason, not an exception.
117150
def evaluate(flag, user, store, logger)
118-
if flag.nil?
119-
raise EvaluationError, "Flag does not exist"
120-
end
121-
122151
if user.nil? || user[:key].nil?
123-
raise EvaluationError, "Invalid user"
152+
return EvalResult.new(error_result('USER_NOT_SPECIFIED'), [])
124153
end
125154

126155
events = []
156+
detail = eval_internal(flag, user, store, events, logger)
157+
return EvalResult.new(detail, events)
158+
end
159+
160+
def eval_internal(flag, user, store, events, logger)
161+
if !flag[:on]
162+
return get_off_value(flag, { kind: 'OFF' }, logger)
163+
end
127164

128-
if flag[:on]
129-
res = eval_internal(flag, user, store, events, logger)
130-
if !res.nil?
131-
res[:events] = events
132-
return res
165+
prereq_failure_reason = check_prerequisites(flag, user, store, events, logger)
166+
if !prereq_failure_reason.nil?
167+
return get_off_value(flag, prereq_failure_reason, logger)
168+
end
169+
170+
# Check user target matches
171+
(flag[:targets] || []).each do |target|
172+
(target[:values] || []).each do |value|
173+
if value == user[:key]
174+
return get_variation(flag, target[:variation], { kind: 'TARGET_MATCH' }, logger)
175+
end
176+
end
177+
end
178+
179+
# Check custom rules
180+
rules = flag[:rules] || []
181+
rules.each_index do |i|
182+
rule = rules[i]
183+
if rule_match_user(rule, user, store)
184+
return get_value_for_variation_or_rollout(flag, rule, user,
185+
{ kind: 'RULE_MATCH', ruleIndex: i, ruleId: rule[:id] }, logger)
133186
end
134187
end
135188

136-
offVariation = flag[:offVariation]
137-
if !offVariation.nil? && offVariation < flag[:variations].length
138-
value = flag[:variations][offVariation]
139-
return { variation: offVariation, value: value, events: events }
189+
# Check the fallthrough rule
190+
if !flag[:fallthrough].nil?
191+
return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user,
192+
{ kind: 'FALLTHROUGH' }, logger)
140193
end
141194

142-
{ variation: nil, value: nil, events: events }
195+
return EvaluationDetail.new(nil, nil, { kind: 'FALLTHROUGH' })
143196
end
144197

145-
def eval_internal(flag, user, store, events, logger)
146-
failed_prereq = false
147-
# Evaluate prerequisites, if any
198+
def check_prerequisites(flag, user, store, events, logger)
148199
(flag[:prerequisites] || []).each do |prerequisite|
149-
prereq_flag = store.get(FEATURES, prerequisite[:key])
200+
prereq_ok = true
201+
prereq_key = prerequisite[:key]
202+
prereq_flag = store.get(FEATURES, prereq_key)
150203

151-
if prereq_flag.nil? || !prereq_flag[:on]
152-
failed_prereq = true
204+
if prereq_flag.nil?
205+
logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
206+
prereq_ok = false
153207
else
154208
begin
155209
prereq_res = eval_internal(prereq_flag, user, store, events, logger)
210+
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
211+
# off variation was. But we still need to evaluate it in order to generate an event.
212+
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
213+
prereq_ok = false
214+
end
156215
event = {
157216
kind: "feature",
158-
key: prereq_flag[:key],
159-
variation: prereq_res.nil? ? nil : prereq_res[:variation],
160-
value: prereq_res.nil? ? nil : prereq_res[:value],
217+
key: prereq_key,
218+
variation: prereq_res.variation_index,
219+
value: prereq_res.value,
161220
version: prereq_flag[:version],
162221
prereqOf: flag[:key],
163222
trackEvents: prereq_flag[:trackEvents],
164223
debugEventsUntilDate: prereq_flag[:debugEventsUntilDate]
165224
}
166225
events.push(event)
167-
if prereq_res.nil? || prereq_res[:variation] != prerequisite[:variation]
168-
failed_prereq = true
169-
end
170226
rescue => exn
171-
logger.error { "[LDClient] Error evaluating prerequisite: #{exn.inspect}" }
172-
failed_prereq = true
227+
Util.log_exception(logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"{flag[:key]}\"", exn)
228+
prereq_ok = false
173229
end
174230
end
175-
end
176-
177-
if failed_prereq
178-
return nil
179-
end
180-
# The prerequisites were satisfied.
181-
# Now walk through the evaluation steps and get the correct
182-
# variation index
183-
eval_rules(flag, user, store)
184-
end
185-
186-
def eval_rules(flag, user, store)
187-
# Check user target matches
188-
(flag[:targets] || []).each do |target|
189-
(target[:values] || []).each do |value|
190-
if value == user[:key]
191-
return { variation: target[:variation], value: get_variation(flag, target[:variation]) }
192-
end
231+
if !prereq_ok
232+
return { kind: 'PREREQUISITE_FAILED', prerequisiteKey: prereq_key }
193233
end
194234
end
195-
196-
# Check custom rules
197-
(flag[:rules] || []).each do |rule|
198-
return variation_for_user(rule, user, flag) if rule_match_user(rule, user, store)
199-
end
200-
201-
# Check the fallthrough rule
202-
if !flag[:fallthrough].nil?
203-
return variation_for_user(flag[:fallthrough], user, flag)
204-
end
205-
206-
# Not even the fallthrough matched-- return the off variation or default
207235
nil
208236
end
209237

210-
def get_variation(flag, index)
211-
if index >= flag[:variations].length
212-
raise EvaluationError, "Invalid variation index"
213-
end
214-
flag[:variations][index]
215-
end
216-
217238
def rule_match_user(rule, user, store)
218239
return false if !rule[:clauses]
219240

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

244265
op = OPERATORS[clause[:op].to_sym]
245-
246266
if op.nil?
247-
raise EvaluationError, "Unsupported operator #{clause[:op]} in evaluation"
267+
return false
248268
end
249269

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

260-
def variation_for_user(rule, user, flag)
280+
def variation_index_for_user(flag, rule, user)
261281
if !rule[:variation].nil? # fixed variation
262-
return { variation: rule[:variation], value: get_variation(flag, rule[:variation]) }
282+
return rule[:variation]
263283
elsif !rule[:rollout].nil? # percentage rollout
264284
rollout = rule[:rollout]
265285
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
@@ -268,12 +288,12 @@ def variation_for_user(rule, user, flag)
268288
rollout[:variations].each do |variate|
269289
sum += variate[:weight].to_f / 100000.0
270290
if bucket < sum
271-
return { variation: variate[:variation], value: get_variation(flag, variate[:variation]) }
291+
return variate[:variation]
272292
end
273293
end
274294
nil
275295
else # the rule isn't well-formed
276-
raise EvaluationError, "Rule does not define a variation or rollout"
296+
nil
277297
end
278298
end
279299

@@ -350,5 +370,31 @@ def match_any(op, value, values)
350370
end
351371
return false
352372
end
373+
374+
private
375+
376+
def get_variation(flag, index, reason, logger)
377+
if index < 0 || index >= flag[:variations].length
378+
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
379+
return error_result('MALFORMED_FLAG')
380+
end
381+
EvaluationDetail.new(flag[:variations][index], index, reason)
382+
end
383+
384+
def get_off_value(flag, reason, logger)
385+
if flag[:offVariation].nil? # off variation unspecified - return default value
386+
return EvaluationDetail.new(nil, nil, reason)
387+
end
388+
get_variation(flag, flag[:offVariation], reason, logger)
389+
end
390+
391+
def get_value_for_variation_or_rollout(flag, vr, user, reason, logger)
392+
index = variation_index_for_user(flag, vr, user)
393+
if index.nil?
394+
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
395+
return error_result('MALFORMED_FLAG')
396+
end
397+
return get_variation(flag, index, reason, logger)
398+
end
353399
end
354400
end

lib/ldclient-rb/events.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ def make_output_event(event)
363363
else
364364
out[:userKey] = event[:user].nil? ? nil : event[:user][:key]
365365
end
366+
out[:reason] = event[:reason] if !event[:reason].nil?
366367
out
367368
when "identify"
368369
{

lib/ldclient-rb/flags_state.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ def initialize(valid)
1515
end
1616

1717
# Used internally to build the state map.
18-
def add_flag(flag, value, variation)
18+
def add_flag(flag, value, variation, reason = nil)
1919
key = flag[:key]
2020
@flag_values[key] = value
2121
meta = { version: flag[:version], trackEvents: flag[:trackEvents] }
2222
meta[:variation] = variation if !variation.nil?
2323
meta[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
24+
meta[:reason] = reason if !reason.nil?
2425
@flag_metadata[key] = meta
2526
end
2627

0 commit comments

Comments
 (0)