22require "semantic"
33
44module 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
354400end
0 commit comments