2
2
require "semantic"
3
3
4
4
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
+
5
36
module Evaluation
6
37
BUILTINS = [ :key , :ip , :country , :email , :firstName , :lastName , :avatar , :name , :anonymous ]
7
38
@@ -107,113 +138,103 @@ def self.comparator(converter)
107
138
end
108
139
}
109
140
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 } )
111
146
end
112
147
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.
117
150
def evaluate ( flag , user , store , logger )
118
- if flag . nil?
119
- raise EvaluationError , "Flag does not exist"
120
- end
121
-
122
151
if user . nil? || user [ :key ] . nil?
123
- raise EvaluationError , "Invalid user"
152
+ return EvalResult . new ( error_result ( 'USER_NOT_SPECIFIED' ) , [ ] )
124
153
end
125
154
126
155
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
127
164
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 )
133
186
end
134
187
end
135
188
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 )
140
193
end
141
194
142
- { variation : nil , value : nil , events : events }
195
+ return EvaluationDetail . new ( nil , nil , { kind : 'FALLTHROUGH' } )
143
196
end
144
197
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 )
148
199
( 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 )
150
203
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
153
207
else
154
208
begin
155
209
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
156
215
event = {
157
216
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 ,
161
220
version : prereq_flag [ :version ] ,
162
221
prereqOf : flag [ :key ] ,
163
222
trackEvents : prereq_flag [ :trackEvents ] ,
164
223
debugEventsUntilDate : prereq_flag [ :debugEventsUntilDate ]
165
224
}
166
225
events . push ( event )
167
- if prereq_res . nil? || prereq_res [ :variation ] != prerequisite [ :variation ]
168
- failed_prereq = true
169
- end
170
226
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
173
229
end
174
230
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 }
193
233
end
194
234
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
207
235
nil
208
236
end
209
237
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
-
217
238
def rule_match_user ( rule , user , store )
218
239
return false if !rule [ :clauses ]
219
240
@@ -242,9 +263,8 @@ def clause_match_user_no_segments(clause, user)
242
263
return false if val . nil?
243
264
244
265
op = OPERATORS [ clause [ :op ] . to_sym ]
245
-
246
266
if op . nil?
247
- raise EvaluationError , "Unsupported operator #{ clause [ :op ] } in evaluation"
267
+ return false
248
268
end
249
269
250
270
if val . is_a? Enumerable
@@ -257,9 +277,9 @@ def clause_match_user_no_segments(clause, user)
257
277
maybe_negate ( clause , match_any ( op , val , clause [ :values ] ) )
258
278
end
259
279
260
- def variation_for_user ( rule , user , flag )
280
+ def variation_index_for_user ( flag , rule , user )
261
281
if !rule [ :variation ] . nil? # fixed variation
262
- return { variation : rule [ :variation ] , value : get_variation ( flag , rule [ :variation ] ) }
282
+ return rule [ :variation ]
263
283
elsif !rule [ :rollout ] . nil? # percentage rollout
264
284
rollout = rule [ :rollout ]
265
285
bucket_by = rollout [ :bucketBy ] . nil? ? "key" : rollout [ :bucketBy ]
@@ -268,12 +288,12 @@ def variation_for_user(rule, user, flag)
268
288
rollout [ :variations ] . each do |variate |
269
289
sum += variate [ :weight ] . to_f / 100000.0
270
290
if bucket < sum
271
- return { variation : variate [ :variation ] , value : get_variation ( flag , variate [ :variation ] ) }
291
+ return variate [ :variation ]
272
292
end
273
293
end
274
294
nil
275
295
else # the rule isn't well-formed
276
- raise EvaluationError , "Rule does not define a variation or rollout"
296
+ nil
277
297
end
278
298
end
279
299
@@ -350,5 +370,31 @@ def match_any(op, value, values)
350
370
end
351
371
return false
352
372
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
353
399
end
354
400
end
0 commit comments