launchdarkly-server-sdk 5.7.3 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +28 -122
  3. data/.gitignore +1 -1
  4. data/.ldrelease/build-docs.sh +18 -0
  5. data/.ldrelease/circleci/linux/execute.sh +18 -0
  6. data/.ldrelease/circleci/mac/execute.sh +18 -0
  7. data/.ldrelease/circleci/template/build.sh +29 -0
  8. data/.ldrelease/circleci/template/publish.sh +23 -0
  9. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  10. data/.ldrelease/circleci/template/test.sh +10 -0
  11. data/.ldrelease/circleci/template/update-version.sh +8 -0
  12. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  13. data/.ldrelease/config.yml +14 -2
  14. data/CHANGELOG.md +36 -0
  15. data/CONTRIBUTING.md +1 -1
  16. data/Gemfile.lock +92 -76
  17. data/README.md +5 -3
  18. data/azure-pipelines.yml +1 -1
  19. data/docs/Makefile +26 -0
  20. data/docs/index.md +9 -0
  21. data/launchdarkly-server-sdk.gemspec +20 -13
  22. data/lib/ldclient-rb.rb +0 -1
  23. data/lib/ldclient-rb/config.rb +15 -3
  24. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  25. data/lib/ldclient-rb/events.rb +1 -4
  26. data/lib/ldclient-rb/file_data_source.rb +1 -1
  27. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  28. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  29. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  30. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  31. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  32. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  33. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +8 -7
  34. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  35. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  36. data/lib/ldclient-rb/integrations/redis.rb +3 -0
  37. data/lib/ldclient-rb/ldclient.rb +16 -11
  38. data/lib/ldclient-rb/polling.rb +1 -4
  39. data/lib/ldclient-rb/redis_store.rb +1 -0
  40. data/lib/ldclient-rb/requestor.rb +25 -23
  41. data/lib/ldclient-rb/stream.rb +10 -30
  42. data/lib/ldclient-rb/user_filter.rb +3 -2
  43. data/lib/ldclient-rb/util.rb +12 -8
  44. data/lib/ldclient-rb/version.rb +1 -1
  45. data/spec/evaluation_detail_spec.rb +135 -0
  46. data/spec/event_sender_spec.rb +20 -2
  47. data/spec/events_spec.rb +11 -0
  48. data/spec/http_util.rb +11 -1
  49. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  50. data/spec/impl/evaluator_clause_spec.rb +55 -0
  51. data/spec/impl/evaluator_operators_spec.rb +141 -0
  52. data/spec/impl/evaluator_rule_spec.rb +96 -0
  53. data/spec/impl/evaluator_segment_spec.rb +125 -0
  54. data/spec/impl/evaluator_spec.rb +305 -0
  55. data/spec/impl/evaluator_spec_base.rb +75 -0
  56. data/spec/impl/model/serialization_spec.rb +41 -0
  57. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  58. data/spec/ldclient_end_to_end_spec.rb +34 -0
  59. data/spec/ldclient_spec.rb +10 -8
  60. data/spec/polling_spec.rb +2 -2
  61. data/spec/redis_feature_store_spec.rb +32 -3
  62. data/spec/requestor_spec.rb +11 -45
  63. data/spec/spec_helper.rb +0 -3
  64. data/spec/stream_spec.rb +1 -16
  65. metadata +110 -60
  66. data/.yardopts +0 -9
  67. data/lib/ldclient-rb/evaluation.rb +0 -462
  68. data/scripts/gendocs.sh +0 -11
  69. data/scripts/release.sh +0 -27
  70. data/spec/evaluation_spec.rb +0 -789
data/.yardopts DELETED
@@ -1,9 +0,0 @@
1
- --no-private
2
- --markup markdown
3
- --embed-mixins
4
- lib/*.rb
5
- lib/**/*.rb
6
- lib/**/**/*.rb
7
- lib/**/**/**/*.rb
8
- -
9
- README.md
@@ -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