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,105 @@ 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 = [ ]
127156
128157 if flag [ :on ]
129- res = eval_internal ( flag , user , store , events , logger )
130- if !res . nil?
131- res [ :events ] = events
132- return res
158+ detail = eval_internal ( flag , user , store , events , logger )
159+ return EvalResult . new ( detail , events )
160+ end
161+
162+ return EvalResult . new ( get_off_value ( flag , { kind : 'OFF' } , logger ) , events )
163+ end
164+
165+
166+ def eval_internal ( flag , user , store , events , logger )
167+ prereq_failure_reason = check_prerequisites ( flag , user , store , events , logger )
168+ if !prereq_failure_reason . nil?
169+ return get_off_value ( flag , prereq_failure_reason , logger )
170+ end
171+
172+ # Check user target matches
173+ ( flag [ :targets ] || [ ] ) . each do |target |
174+ ( target [ :values ] || [ ] ) . each do |value |
175+ if value == user [ :key ]
176+ return get_variation ( flag , target [ :variation ] , { kind : 'TARGET_MATCH' } , logger )
177+ end
178+ end
179+ end
180+
181+ # Check custom rules
182+ rules = flag [ :rules ] || [ ]
183+ rules . each_index do |i |
184+ rule = rules [ i ]
185+ if rule_match_user ( rule , user , store )
186+ return get_value_for_variation_or_rollout ( flag , rule , user ,
187+ { kind : 'RULE_MATCH' , ruleIndex : i , ruleId : rule [ :id ] } , logger )
133188 end
134189 end
135190
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 }
191+ # Check the fallthrough rule
192+ if !flag [ :fallthrough ] . nil?
193+ return get_value_for_variation_or_rollout ( flag , flag [ :fallthrough ] , user ,
194+ { kind : 'FALLTHROUGH' } , logger )
140195 end
141196
142- { variation : nil , value : nil , events : events }
197+ return EvaluationDetail . new ( nil , nil , { kind : 'FALLTHROUGH' } )
143198 end
144199
145- def eval_internal ( flag , user , store , events , logger )
146- failed_prereq = false
147- # Evaluate prerequisites, if any
200+ def check_prerequisites ( flag , user , store , events , logger )
148201 ( flag [ :prerequisites ] || [ ] ) . each do |prerequisite |
149- prereq_flag = store . get ( FEATURES , prerequisite [ :key ] )
202+ prereq_ok = true
203+ prereq_key = prerequisite [ :key ]
204+ prereq_flag = store . get ( FEATURES , prereq_key )
150205
151206 if prereq_flag . nil? || !prereq_flag [ :on ]
152- failed_prereq = true
207+ logger . error { "[LDClient] Could not retrieve prerequisite flag \" #{ prereq_key } \" when evaluating \" #{ flag [ :key ] } \" " }
208+ prereq_ok = false
209+ elsif !prereq_flag [ :on ]
210+ prereq_ok = false
153211 else
154212 begin
155213 prereq_res = eval_internal ( prereq_flag , user , store , events , logger )
156214 event = {
157215 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] ,
216+ key : prereq_key ,
217+ variation : prereq_res . variation_index ,
218+ value : prereq_res . value ,
161219 version : prereq_flag [ :version ] ,
162220 prereqOf : flag [ :key ] ,
163221 trackEvents : prereq_flag [ :trackEvents ] ,
164222 debugEventsUntilDate : prereq_flag [ :debugEventsUntilDate ]
165223 }
166224 events . push ( event )
167- if prereq_res . nil? || prereq_res [ :variation ] != prerequisite [ :variation ]
168- failed_prereq = true
225+ if prereq_res . variation_index != prerequisite [ :variation ]
226+ prereq_ok = false
169227 end
170228 rescue => exn
171- logger . error { "[LDClient] Error evaluating prerequisite: #{ exn . inspect } " }
172- failed_prereq = true
229+ Util . log_exception ( logger , " Error evaluating prerequisite flag \" #{ prereq_key } \" for flag \" {flag[:key]} \" " , exn )
230+ prereq_ok = false
173231 end
174232 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
233+ if !prereq_ok
234+ return { kind : 'PREREQUISITE_FAILED' , prerequisiteKey : prereq_key }
193235 end
194236 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
207237 nil
208238 end
209239
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-
217240 def rule_match_user ( rule , user , store )
218241 return false if !rule [ :clauses ]
219242
@@ -242,9 +265,8 @@ def clause_match_user_no_segments(clause, user)
242265 return false if val . nil?
243266
244267 op = OPERATORS [ clause [ :op ] . to_sym ]
245-
246268 if op . nil?
247- raise EvaluationError , "Unsupported operator #{ clause [ :op ] } in evaluation"
269+ return false
248270 end
249271
250272 if val . is_a? Enumerable
@@ -257,9 +279,9 @@ def clause_match_user_no_segments(clause, user)
257279 maybe_negate ( clause , match_any ( op , val , clause [ :values ] ) )
258280 end
259281
260- def variation_for_user ( rule , user , flag )
282+ def variation_index_for_user ( flag , rule , user )
261283 if !rule [ :variation ] . nil? # fixed variation
262- return { variation : rule [ :variation ] , value : get_variation ( flag , rule [ :variation ] ) }
284+ return rule [ :variation ]
263285 elsif !rule [ :rollout ] . nil? # percentage rollout
264286 rollout = rule [ :rollout ]
265287 bucket_by = rollout [ :bucketBy ] . nil? ? "key" : rollout [ :bucketBy ]
@@ -268,12 +290,12 @@ def variation_for_user(rule, user, flag)
268290 rollout [ :variations ] . each do |variate |
269291 sum += variate [ :weight ] . to_f / 100000.0
270292 if bucket < sum
271- return { variation : variate [ :variation ] , value : get_variation ( flag , variate [ :variation ] ) }
293+ return variate [ :variation ]
272294 end
273295 end
274296 nil
275297 else # the rule isn't well-formed
276- raise EvaluationError , "Rule does not define a variation or rollout"
298+ nil
277299 end
278300 end
279301
@@ -350,5 +372,31 @@ def match_any(op, value, values)
350372 end
351373 return false
352374 end
375+
376+ private
377+
378+ def get_variation ( flag , index , reason , logger )
379+ if index < 0 || index >= flag [ :variations ] . length
380+ logger . error ( "[LDClient] Data inconsistency in feature flag \" #{ flag [ :key ] } \" : invalid variation index" )
381+ return error_result ( 'MALFORMED_FLAG' )
382+ end
383+ EvaluationDetail . new ( flag [ :variations ] [ index ] , index , reason )
384+ end
385+
386+ def get_off_value ( flag , reason , logger )
387+ if flag [ :offVariation ] . nil? # off variation unspecified - return default value
388+ return EvaluationDetail . new ( nil , nil , reason )
389+ end
390+ get_variation ( flag , flag [ :offVariation ] , reason , logger )
391+ end
392+
393+ def get_value_for_variation_or_rollout ( flag , vr , user , reason , logger )
394+ index = variation_index_for_user ( flag , vr , user )
395+ if index . nil?
396+ logger . error ( "[LDClient] Data inconsistency in feature flag \" #{ flag [ :key ] } \" : variation/rollout object with no variation or rollout" )
397+ return error_result ( 'MALFORMED_FLAG' )
398+ end
399+ return get_variation ( flag , index , reason , logger )
400+ end
353401 end
354402end
0 commit comments