Module: LaunchDarkly::Evaluation
- Included in:
- LDClient
- Defined in:
- lib/ldclient-rb/evaluation.rb
Defined Under Namespace
Classes: EvalResult
Constant Summary collapse
- BUILTINS =
[:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
- NUMERIC_VERSION_COMPONENTS_REGEX =
Regexp.new("^[0-9.]*")
- DATE_OPERAND =
lambda do |v| if v.is_a? String begin DateTime.rfc3339(v).strftime("%Q").to_i rescue => e nil end elsif v.is_a? Numeric v else nil end end
- SEMVER_OPERAND =
lambda do |v| semver = nil if v.is_a? String for _ in 0..2 do begin semver = Semantic::Version.new(v) break # Some versions of jruby cannot properly handle a return here and return from the method that calls this lambda rescue ArgumentError v = addZeroVersionComponent(v) end end end semver end
- OPERATORS =
{ in: lambda do |a, b| a == b end, endsWith: lambda do |a, b| (a.is_a? String) && (a.end_with? b) end, startsWith: lambda do |a, b| (a.is_a? String) && (a.start_with? b) end, matches: lambda do |a, b| (b.is_a? String) && !(Regexp.new b).match(a).nil? end, contains: lambda do |a, b| (a.is_a? String) && (a.include? b) end, lessThan: lambda do |a, b| (a.is_a? Numeric) && (a < b) end, lessThanOrEqual: lambda do |a, b| (a.is_a? Numeric) && (a <= b) end, greaterThan: lambda do |a, b| (a.is_a? Numeric) && (a > b) end, greaterThanOrEqual: lambda do |a, b| (a.is_a? Numeric) && (a >= b) end, before: comparator(DATE_OPERAND) { |n| n < 0 }, after: comparator(DATE_OPERAND) { |n| n > 0 }, semVerEqual: comparator(SEMVER_OPERAND) { |n| n == 0 }, semVerLessThan: comparator(SEMVER_OPERAND) { |n| n < 0 }, semVerGreaterThan: comparator(SEMVER_OPERAND) { |n| n > 0 }, segmentMatch: lambda do |a, b| false # we should never reach this - instead we special-case this operator in clause_match_user end }
Class Method Summary collapse
Instance Method Summary collapse
- #bucket_user(user, key, bucket_by, salt) ⇒ Object
- #bucketable_string_value(value) ⇒ Object
- #check_prerequisites(flag, user, store, events, logger) ⇒ Object
- #clause_match_user(clause, user, store) ⇒ Object
- #clause_match_user_no_segments(clause, user) ⇒ Object
- #error_result(errorKind, value = nil) ⇒ Object
- #eval_internal(flag, user, store, events, logger) ⇒ Object
-
#evaluate(flag, user, store, logger) ⇒ Object
Evaluates a feature flag and returns an EvalResult.
- #match_any(op, value, values) ⇒ Object
- #maybe_negate(clause, b) ⇒ Object
- #rule_match_user(rule, user, store) ⇒ Object
- #segment_match_user(segment, user) ⇒ Object
- #segment_rule_match_user(rule, user, segment_key, salt) ⇒ Object
- #user_value(user, attribute) ⇒ Object
- #variation_index_for_user(flag, rule, user) ⇒ Object
Class Method Details
.addZeroVersionComponent(v) ⇒ Object
70 71 72 73 74 |
# File 'lib/ldclient-rb/evaluation.rb', line 70 def self.addZeroVersionComponent(v) NUMERIC_VERSION_COMPONENTS_REGEX.match(v) { |m| m[0] + ".0" + v[m[0].length..-1] } end |
.comparator(converter) ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/ldclient-rb/evaluation.rb', line 76 def self.comparator(converter) lambda do |a, b| av = converter.call(a) bv = converter.call(b) if !av.nil? && !bv.nil? yield av <=> bv else return false end end end |
Instance Method Details
#bucket_user(user, key, bucket_by, salt) ⇒ Object
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/ldclient-rb/evaluation.rb', line 327 def bucket_user(user, key, bucket_by, salt) return nil unless user[:key] id_hash = bucketable_string_value(user_value(user, bucket_by)) if id_hash.nil? return 0.0 end if user[:secondary] id_hash += "." + user[:secondary] end hash_key = "%s.%s.%s" % [key, salt, id_hash] hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14] hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF) end |
#bucketable_string_value(value) ⇒ Object
345 346 347 348 349 |
# File 'lib/ldclient-rb/evaluation.rb', line 345 def bucketable_string_value(value) return value if value.is_a? String return value.to_s if value.is_a? Integer nil end |
#check_prerequisites(flag, user, store, events, logger) ⇒ Object
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/ldclient-rb/evaluation.rb', line 198 def check_prerequisites(flag, user, store, events, logger) (flag[:prerequisites] || []).each do |prerequisite| prereq_ok = true prereq_key = prerequisite[:key] prereq_flag = store.get(FEATURES, prereq_key) 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_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) rescue => exn Util.log_exception(logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"{flag[:key]}\"", exn) prereq_ok = false end end if !prereq_ok return { kind: 'PREREQUISITE_FAILED', prerequisiteKey: prereq_key } end end nil end |
#clause_match_user(clause, user, store) ⇒ Object
248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/ldclient-rb/evaluation.rb', line 248 def clause_match_user(clause, user, store) # In the case of a segment match operator, we check if the user is in any of the segments, # and possibly negate if clause[:op].to_sym == :segmentMatch (clause[:values] || []).each do |v| segment = store.get(SEGMENTS, v) return maybe_negate(clause, true) if !segment.nil? && segment_match_user(segment, user) end return maybe_negate(clause, false) end clause_match_user_no_segments(clause, user) end |
#clause_match_user_no_segments(clause, user) ⇒ Object
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/ldclient-rb/evaluation.rb', line 261 def clause_match_user_no_segments(clause, user) val = user_value(user, clause[:attribute]) return false if val.nil? op = OPERATORS[clause[:op].to_sym] if op.nil? return false end if val.is_a? Enumerable val.each do |v| return maybe_negate(clause, true) if match_any(op, v, clause[:values]) end return maybe_negate(clause, false) end maybe_negate(clause, match_any(op, val, clause[:values])) end |
#error_result(errorKind, value = nil) ⇒ Object
144 145 146 |
# File 'lib/ldclient-rb/evaluation.rb', line 144 def error_result(errorKind, value = nil) EvaluationDetail.new(value, nil, { kind: 'ERROR', errorKind: errorKind }) end |
#eval_internal(flag, user, store, events, logger) ⇒ Object
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/ldclient-rb/evaluation.rb', line 160 def eval_internal(flag, user, store, events, logger) if !flag[:on] return get_off_value(flag, { kind: 'OFF' }, logger) end 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 # Check the fallthrough rule if !flag[:fallthrough].nil? return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, { kind: 'FALLTHROUGH' }, logger) end return EvaluationDetail.new(nil, nil, { kind: 'FALLTHROUGH' }) end |
#evaluate(flag, user, store, logger) ⇒ Object
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.
150 151 152 153 154 155 156 157 158 |
# File 'lib/ldclient-rb/evaluation.rb', line 150 def evaluate(flag, user, store, logger) if user.nil? || user[:key].nil? return EvalResult.new(error_result('USER_NOT_SPECIFIED'), []) end events = [] detail = eval_internal(flag, user, store, events, logger) return EvalResult.new(detail, events) end |
#match_any(op, value, values) ⇒ Object
367 368 369 370 371 372 |
# File 'lib/ldclient-rb/evaluation.rb', line 367 def match_any(op, value, values) values.each do |v| return true if op.call(value, v) end return false end |
#maybe_negate(clause, b) ⇒ Object
363 364 365 |
# File 'lib/ldclient-rb/evaluation.rb', line 363 def maybe_negate(clause, b) clause[:negate] ? !b : b end |
#rule_match_user(rule, user, store) ⇒ Object
238 239 240 241 242 243 244 245 246 |
# File 'lib/ldclient-rb/evaluation.rb', line 238 def rule_match_user(rule, user, store) return false if !rule[:clauses] (rule[:clauses] || []).each do |clause| return false if !clause_match_user(clause, user, store) end return true end |
#segment_match_user(segment, user) ⇒ Object
300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'lib/ldclient-rb/evaluation.rb', line 300 def segment_match_user(segment, user) return false unless user[:key] return true if segment[:included].include?(user[:key]) return false if segment[:excluded].include?(user[:key]) (segment[:rules] || []).each do |r| return true if segment_rule_match_user(r, user, segment[:key], segment[:salt]) end return false end |
#segment_rule_match_user(rule, user, segment_key, salt) ⇒ Object
313 314 315 316 317 318 319 320 321 322 323 324 325 |
# File 'lib/ldclient-rb/evaluation.rb', line 313 def segment_rule_match_user(rule, user, segment_key, salt) (rule[:clauses] || []).each do |c| return false unless clause_match_user_no_segments(c, user) end # If the weight is absent, this rule matches return true if !rule[:weight] # All of the clauses are met. See if the user buckets in bucket = bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt) weight = rule[:weight].to_f / 100000.0 return bucket < weight end |
#user_value(user, attribute) ⇒ Object
351 352 353 354 355 356 357 358 359 360 361 |
# File 'lib/ldclient-rb/evaluation.rb', line 351 def user_value(user, attribute) attribute = attribute.to_sym if BUILTINS.include? attribute user[attribute] elsif !user[:custom].nil? user[:custom][attribute] else nil end end |
#variation_index_for_user(flag, rule, user) ⇒ Object
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 |
# File 'lib/ldclient-rb/evaluation.rb', line 280 def variation_index_for_user(flag, rule, user) if !rule[:variation].nil? # fixed variation return rule[:variation] elsif !rule[:rollout].nil? # percentage rollout rollout = rule[:rollout] bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy] bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt]) sum = 0; rollout[:variations].each do |variate| sum += variate[:weight].to_f / 100000.0 if bucket < sum return variate[:variation] end end nil else # the rule isn't well-formed nil end end |