launchdarkly-server-sdk 5.8.2 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +28 -122
  3. data/.ldrelease/circleci/linux/execute.sh +18 -0
  4. data/.ldrelease/circleci/mac/execute.sh +18 -0
  5. data/.ldrelease/circleci/template/build.sh +29 -0
  6. data/.ldrelease/circleci/template/publish.sh +23 -0
  7. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  8. data/.ldrelease/circleci/template/test.sh +10 -0
  9. data/.ldrelease/circleci/template/update-version.sh +8 -0
  10. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  11. data/.ldrelease/config.yml +7 -3
  12. data/CHANGELOG.md +9 -0
  13. data/CONTRIBUTING.md +1 -1
  14. data/Gemfile.lock +69 -42
  15. data/README.md +2 -2
  16. data/azure-pipelines.yml +1 -1
  17. data/launchdarkly-server-sdk.gemspec +16 -16
  18. data/lib/ldclient-rb.rb +0 -1
  19. data/lib/ldclient-rb/config.rb +15 -3
  20. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  21. data/lib/ldclient-rb/events.rb +1 -4
  22. data/lib/ldclient-rb/file_data_source.rb +1 -1
  23. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  24. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  25. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  26. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  27. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  28. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  29. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -9
  30. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  31. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  32. data/lib/ldclient-rb/ldclient.rb +14 -9
  33. data/lib/ldclient-rb/polling.rb +1 -4
  34. data/lib/ldclient-rb/requestor.rb +25 -15
  35. data/lib/ldclient-rb/stream.rb +9 -6
  36. data/lib/ldclient-rb/util.rb +12 -8
  37. data/lib/ldclient-rb/version.rb +1 -1
  38. data/spec/evaluation_detail_spec.rb +135 -0
  39. data/spec/event_sender_spec.rb +20 -2
  40. data/spec/http_util.rb +11 -1
  41. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  42. data/spec/impl/evaluator_clause_spec.rb +55 -0
  43. data/spec/impl/evaluator_operators_spec.rb +141 -0
  44. data/spec/impl/evaluator_rule_spec.rb +96 -0
  45. data/spec/impl/evaluator_segment_spec.rb +125 -0
  46. data/spec/impl/evaluator_spec.rb +305 -0
  47. data/spec/impl/evaluator_spec_base.rb +75 -0
  48. data/spec/impl/model/serialization_spec.rb +41 -0
  49. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  50. data/spec/ldclient_end_to_end_spec.rb +34 -0
  51. data/spec/ldclient_spec.rb +10 -8
  52. data/spec/polling_spec.rb +2 -2
  53. data/spec/redis_feature_store_spec.rb +2 -2
  54. data/spec/requestor_spec.rb +11 -11
  55. metadata +89 -46
  56. data/lib/ldclient-rb/evaluation.rb +0 -462
  57. data/spec/evaluation_spec.rb +0 -789
@@ -1,462 +0,0 @@
1
- require "date"
2
- require "semantic"
3
-
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
- #
15
- # The result of the flag evaluation. This will be either one of the flag's variations, or the
16
- # default value that was passed to {LDClient#variation_detail}. It is the same as the return
17
- # value of {LDClient#variation}.
18
- #
19
- # @return [Object]
20
- #
21
- attr_reader :value
22
-
23
- #
24
- # The index of the returned value within the flag's list of variations. The first variation is
25
- # 0, the second is 1, etc. This is `nil` if the default value was returned.
26
- #
27
- # @return [int|nil]
28
- #
29
- attr_reader :variation_index
30
-
31
- #
32
- # An object describing the main factor that influenced the flag evaluation value.
33
- #
34
- # This object is currently represented as a Hash, which may have the following keys:
35
- #
36
- # `:kind`: The general category of reason. Possible values:
37
- #
38
- # * `'OFF'`: the flag was off and therefore returned its configured off value
39
- # * `'FALLTHROUGH'`: the flag was on but the user did not match any targets or rules
40
- # * `'TARGET_MATCH'`: the user key was specifically targeted for this flag
41
- # * `'RULE_MATCH'`: the user matched one of the flag's rules
42
- # * `'PREREQUISITE_FAILED`': the flag was considered off because it had at least one
43
- # prerequisite flag that either was off or did not return the desired variation
44
- # * `'ERROR'`: the flag could not be evaluated, so the default value was returned
45
- #
46
- # `:ruleIndex`: If the kind was `RULE_MATCH`, this is the positional index of the
47
- # matched rule (0 for the first rule).
48
- #
49
- # `:ruleId`: If the kind was `RULE_MATCH`, this is the rule's unique identifier.
50
- #
51
- # `:prerequisiteKey`: If the kind was `PREREQUISITE_FAILED`, this is the flag key of
52
- # the prerequisite flag that failed.
53
- #
54
- # `:errorKind`: If the kind was `ERROR`, this indicates the type of error:
55
- #
56
- # * `'CLIENT_NOT_READY'`: the caller tried to evaluate a flag before the client had
57
- # successfully initialized
58
- # * `'FLAG_NOT_FOUND'`: the caller provided a flag key that did not match any known flag
59
- # * `'MALFORMED_FLAG'`: there was an internal inconsistency in the flag data, e.g. a
60
- # rule specified a nonexistent variation
61
- # * `'USER_NOT_SPECIFIED'`: the user object or user key was not provied
62
- # * `'EXCEPTION'`: an unexpected exception stopped flag evaluation
63
- #
64
- # @return [Hash]
65
- #
66
- attr_reader :reason
67
-
68
- #
69
- # Tests whether the flag evaluation returned a default value. This is the same as checking
70
- # whether {#variation_index} is nil.
71
- #
72
- # @return [Boolean]
73
- #
74
- def default_value?
75
- variation_index.nil?
76
- end
77
-
78
- def ==(other)
79
- @value == other.value && @variation_index == other.variation_index && @reason == other.reason
80
- end
81
- end
82
-
83
- # @private
84
- module Evaluation
85
- BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
86
-
87
- NUMERIC_VERSION_COMPONENTS_REGEX = Regexp.new("^[0-9.]*")
88
-
89
- DATE_OPERAND = lambda do |v|
90
- if v.is_a? String
91
- begin
92
- DateTime.rfc3339(v).strftime("%Q").to_i
93
- rescue => e
94
- nil
95
- end
96
- elsif v.is_a? Numeric
97
- v
98
- else
99
- nil
100
- end
101
- end
102
-
103
- SEMVER_OPERAND = lambda do |v|
104
- semver = nil
105
- if v.is_a? String
106
- for _ in 0..2 do
107
- begin
108
- semver = Semantic::Version.new(v)
109
- break # Some versions of jruby cannot properly handle a return here and return from the method that calls this lambda
110
- rescue ArgumentError
111
- v = addZeroVersionComponent(v)
112
- end
113
- end
114
- end
115
- semver
116
- end
117
-
118
- def self.addZeroVersionComponent(v)
119
- NUMERIC_VERSION_COMPONENTS_REGEX.match(v) { |m|
120
- m[0] + ".0" + v[m[0].length..-1]
121
- }
122
- end
123
-
124
- def self.comparator(converter)
125
- lambda do |a, b|
126
- av = converter.call(a)
127
- bv = converter.call(b)
128
- if !av.nil? && !bv.nil?
129
- yield av <=> bv
130
- else
131
- return false
132
- end
133
- end
134
- end
135
-
136
- OPERATORS = {
137
- in:
138
- lambda do |a, b|
139
- a == b
140
- end,
141
- endsWith:
142
- lambda do |a, b|
143
- (a.is_a? String) && (b.is_a? String) && (a.end_with? b)
144
- end,
145
- startsWith:
146
- lambda do |a, b|
147
- (a.is_a? String) && (b.is_a? String) && (a.start_with? b)
148
- end,
149
- matches:
150
- lambda do |a, b|
151
- if (b.is_a? String) && (b.is_a? String)
152
- begin
153
- re = Regexp.new b
154
- !re.match(a).nil?
155
- rescue
156
- false
157
- end
158
- else
159
- false
160
- end
161
- end,
162
- contains:
163
- lambda do |a, b|
164
- (a.is_a? String) && (b.is_a? String) && (a.include? b)
165
- end,
166
- lessThan:
167
- lambda do |a, b|
168
- (a.is_a? Numeric) && (b.is_a? Numeric) && (a < b)
169
- end,
170
- lessThanOrEqual:
171
- lambda do |a, b|
172
- (a.is_a? Numeric) && (b.is_a? Numeric) && (a <= b)
173
- end,
174
- greaterThan:
175
- lambda do |a, b|
176
- (a.is_a? Numeric) && (b.is_a? Numeric) && (a > b)
177
- end,
178
- greaterThanOrEqual:
179
- lambda do |a, b|
180
- (a.is_a? Numeric) && (b.is_a? Numeric) && (a >= b)
181
- end,
182
- before:
183
- comparator(DATE_OPERAND) { |n| n < 0 },
184
- after:
185
- comparator(DATE_OPERAND) { |n| n > 0 },
186
- semVerEqual:
187
- comparator(SEMVER_OPERAND) { |n| n == 0 },
188
- semVerLessThan:
189
- comparator(SEMVER_OPERAND) { |n| n < 0 },
190
- semVerGreaterThan:
191
- comparator(SEMVER_OPERAND) { |n| n > 0 },
192
- segmentMatch:
193
- lambda do |a, b|
194
- false # we should never reach this - instead we special-case this operator in clause_match_user
195
- end
196
- }
197
-
198
- # Used internally to hold an evaluation result and the events that were generated from prerequisites.
199
- EvalResult = Struct.new(:detail, :events)
200
-
201
- USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION = [ :key, :secondary ]
202
- # Currently we are not stringifying the rest of the built-in attributes prior to evaluation, only for events.
203
- # This is because it could affect evaluation results for existing users (ch35206).
204
-
205
- def error_result(errorKind, value = nil)
206
- EvaluationDetail.new(value, nil, { kind: 'ERROR', errorKind: errorKind })
207
- end
208
-
209
- # Evaluates a feature flag and returns an EvalResult. The result.value will be nil if the flag returns
210
- # the default value. Error conditions produce a result with an error reason, not an exception.
211
- def evaluate(flag, user, store, logger, event_factory)
212
- if user.nil? || user[:key].nil?
213
- return EvalResult.new(error_result('USER_NOT_SPECIFIED'), [])
214
- end
215
-
216
- sanitized_user = Util.stringify_attrs(user, USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION)
217
-
218
- events = []
219
- detail = eval_internal(flag, sanitized_user, store, events, logger, event_factory)
220
- return EvalResult.new(detail, events)
221
- end
222
-
223
- def eval_internal(flag, user, store, events, logger, event_factory)
224
- if !flag[:on]
225
- return get_off_value(flag, { kind: 'OFF' }, logger)
226
- end
227
-
228
- prereq_failure_reason = check_prerequisites(flag, user, store, events, logger, event_factory)
229
- if !prereq_failure_reason.nil?
230
- return get_off_value(flag, prereq_failure_reason, logger)
231
- end
232
-
233
- # Check user target matches
234
- (flag[:targets] || []).each do |target|
235
- (target[:values] || []).each do |value|
236
- if value == user[:key]
237
- return get_variation(flag, target[:variation], { kind: 'TARGET_MATCH' }, logger)
238
- end
239
- end
240
- end
241
-
242
- # Check custom rules
243
- rules = flag[:rules] || []
244
- rules.each_index do |i|
245
- rule = rules[i]
246
- if rule_match_user(rule, user, store)
247
- return get_value_for_variation_or_rollout(flag, rule, user,
248
- { kind: 'RULE_MATCH', ruleIndex: i, ruleId: rule[:id] }, logger)
249
- end
250
- end
251
-
252
- # Check the fallthrough rule
253
- if !flag[:fallthrough].nil?
254
- return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user,
255
- { kind: 'FALLTHROUGH' }, logger)
256
- end
257
-
258
- return EvaluationDetail.new(nil, nil, { kind: 'FALLTHROUGH' })
259
- end
260
-
261
- def check_prerequisites(flag, user, store, events, logger, event_factory)
262
- (flag[:prerequisites] || []).each do |prerequisite|
263
- prereq_ok = true
264
- prereq_key = prerequisite[:key]
265
- prereq_flag = store.get(FEATURES, prereq_key)
266
-
267
- if prereq_flag.nil?
268
- logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
269
- prereq_ok = false
270
- else
271
- begin
272
- prereq_res = eval_internal(prereq_flag, user, store, events, logger, event_factory)
273
- # Note that if the prerequisite flag is off, we don't consider it a match no matter what its
274
- # off variation was. But we still need to evaluate it in order to generate an event.
275
- if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
276
- prereq_ok = false
277
- end
278
- event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag)
279
- events.push(event)
280
- rescue => exn
281
- Util.log_exception(logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
282
- prereq_ok = false
283
- end
284
- end
285
- if !prereq_ok
286
- return { kind: 'PREREQUISITE_FAILED', prerequisiteKey: prereq_key }
287
- end
288
- end
289
- nil
290
- end
291
-
292
- def rule_match_user(rule, user, store)
293
- return false if !rule[:clauses]
294
-
295
- (rule[:clauses] || []).each do |clause|
296
- return false if !clause_match_user(clause, user, store)
297
- end
298
-
299
- return true
300
- end
301
-
302
- def clause_match_user(clause, user, store)
303
- # In the case of a segment match operator, we check if the user is in any of the segments,
304
- # and possibly negate
305
- if clause[:op].to_sym == :segmentMatch
306
- (clause[:values] || []).each do |v|
307
- segment = store.get(SEGMENTS, v)
308
- return maybe_negate(clause, true) if !segment.nil? && segment_match_user(segment, user)
309
- end
310
- return maybe_negate(clause, false)
311
- end
312
- clause_match_user_no_segments(clause, user)
313
- end
314
-
315
- def clause_match_user_no_segments(clause, user)
316
- val = user_value(user, clause[:attribute])
317
- return false if val.nil?
318
-
319
- op = OPERATORS[clause[:op].to_sym]
320
- if op.nil?
321
- return false
322
- end
323
-
324
- if val.is_a? Enumerable
325
- val.each do |v|
326
- return maybe_negate(clause, true) if match_any(op, v, clause[:values])
327
- end
328
- return maybe_negate(clause, false)
329
- end
330
-
331
- maybe_negate(clause, match_any(op, val, clause[:values]))
332
- end
333
-
334
- def variation_index_for_user(flag, rule, user)
335
- variation = rule[:variation]
336
- return variation if !variation.nil? # fixed variation
337
- rollout = rule[:rollout]
338
- return nil if rollout.nil?
339
- variations = rollout[:variations]
340
- if !variations.nil? && variations.length > 0 # percentage rollout
341
- rollout = rule[:rollout]
342
- bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
343
- bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
344
- sum = 0;
345
- variations.each do |variate|
346
- sum += variate[:weight].to_f / 100000.0
347
- if bucket < sum
348
- return variate[:variation]
349
- end
350
- end
351
- # The user's bucket value was greater than or equal to the end of the last bucket. This could happen due
352
- # to a rounding error, or due to the fact that we are scaling to 100000 rather than 99999, or the flag
353
- # data could contain buckets that don't actually add up to 100000. Rather than returning an error in
354
- # this case (or changing the scaling, which would potentially change the results for *all* users), we
355
- # will simply put the user in the last bucket.
356
- variations[-1][:variation]
357
- else # the rule isn't well-formed
358
- nil
359
- end
360
- end
361
-
362
- def segment_match_user(segment, user)
363
- return false unless user[:key]
364
-
365
- return true if segment[:included].include?(user[:key])
366
- return false if segment[:excluded].include?(user[:key])
367
-
368
- (segment[:rules] || []).each do |r|
369
- return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
370
- end
371
-
372
- return false
373
- end
374
-
375
- def segment_rule_match_user(rule, user, segment_key, salt)
376
- (rule[:clauses] || []).each do |c|
377
- return false unless clause_match_user_no_segments(c, user)
378
- end
379
-
380
- # If the weight is absent, this rule matches
381
- return true if !rule[:weight]
382
-
383
- # All of the clauses are met. See if the user buckets in
384
- bucket = bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt)
385
- weight = rule[:weight].to_f / 100000.0
386
- return bucket < weight
387
- end
388
-
389
- def bucket_user(user, key, bucket_by, salt)
390
- return nil unless user[:key]
391
-
392
- id_hash = bucketable_string_value(user_value(user, bucket_by))
393
- if id_hash.nil?
394
- return 0.0
395
- end
396
-
397
- if user[:secondary]
398
- id_hash += "." + user[:secondary]
399
- end
400
-
401
- hash_key = "%s.%s.%s" % [key, salt, id_hash]
402
-
403
- hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
404
- hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
405
- end
406
-
407
- def bucketable_string_value(value)
408
- return value if value.is_a? String
409
- return value.to_s if value.is_a? Integer
410
- nil
411
- end
412
-
413
- def user_value(user, attribute)
414
- attribute = attribute.to_sym
415
-
416
- if BUILTINS.include? attribute
417
- user[attribute]
418
- elsif !user[:custom].nil?
419
- user[:custom][attribute]
420
- else
421
- nil
422
- end
423
- end
424
-
425
- def maybe_negate(clause, b)
426
- clause[:negate] ? !b : b
427
- end
428
-
429
- def match_any(op, value, values)
430
- values.each do |v|
431
- return true if op.call(value, v)
432
- end
433
- return false
434
- end
435
-
436
- private
437
-
438
- def get_variation(flag, index, reason, logger)
439
- if index < 0 || index >= flag[:variations].length
440
- logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
441
- return error_result('MALFORMED_FLAG')
442
- end
443
- EvaluationDetail.new(flag[:variations][index], index, reason)
444
- end
445
-
446
- def get_off_value(flag, reason, logger)
447
- if flag[:offVariation].nil? # off variation unspecified - return default value
448
- return EvaluationDetail.new(nil, nil, reason)
449
- end
450
- get_variation(flag, flag[:offVariation], reason, logger)
451
- end
452
-
453
- def get_value_for_variation_or_rollout(flag, vr, user, reason, logger)
454
- index = variation_index_for_user(flag, vr, user)
455
- if index.nil?
456
- logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
457
- return error_result('MALFORMED_FLAG')
458
- end
459
- return get_variation(flag, index, reason, logger)
460
- end
461
- end
462
- end