launchdarkly-server-sdk 5.5.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +134 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.gitignore +15 -0
  6. data/.hound.yml +2 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +600 -0
  9. data/.simplecov +4 -0
  10. data/.yardopts +9 -0
  11. data/CHANGELOG.md +261 -0
  12. data/CODEOWNERS +1 -0
  13. data/CONTRIBUTING.md +37 -0
  14. data/Gemfile +3 -0
  15. data/Gemfile.lock +102 -0
  16. data/LICENSE.txt +13 -0
  17. data/README.md +56 -0
  18. data/Rakefile +5 -0
  19. data/azure-pipelines.yml +51 -0
  20. data/ext/mkrf_conf.rb +11 -0
  21. data/launchdarkly-server-sdk.gemspec +40 -0
  22. data/lib/ldclient-rb.rb +29 -0
  23. data/lib/ldclient-rb/cache_store.rb +45 -0
  24. data/lib/ldclient-rb/config.rb +411 -0
  25. data/lib/ldclient-rb/evaluation.rb +455 -0
  26. data/lib/ldclient-rb/event_summarizer.rb +55 -0
  27. data/lib/ldclient-rb/events.rb +468 -0
  28. data/lib/ldclient-rb/expiring_cache.rb +77 -0
  29. data/lib/ldclient-rb/file_data_source.rb +312 -0
  30. data/lib/ldclient-rb/flags_state.rb +76 -0
  31. data/lib/ldclient-rb/impl.rb +13 -0
  32. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
  33. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
  34. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
  35. data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
  36. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  37. data/lib/ldclient-rb/in_memory_store.rb +100 -0
  38. data/lib/ldclient-rb/integrations.rb +55 -0
  39. data/lib/ldclient-rb/integrations/consul.rb +38 -0
  40. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
  41. data/lib/ldclient-rb/integrations/redis.rb +55 -0
  42. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
  43. data/lib/ldclient-rb/interfaces.rb +153 -0
  44. data/lib/ldclient-rb/ldclient.rb +424 -0
  45. data/lib/ldclient-rb/memoized_value.rb +32 -0
  46. data/lib/ldclient-rb/newrelic.rb +17 -0
  47. data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
  48. data/lib/ldclient-rb/polling.rb +78 -0
  49. data/lib/ldclient-rb/redis_store.rb +87 -0
  50. data/lib/ldclient-rb/requestor.rb +101 -0
  51. data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
  52. data/lib/ldclient-rb/stream.rb +141 -0
  53. data/lib/ldclient-rb/user_filter.rb +51 -0
  54. data/lib/ldclient-rb/util.rb +50 -0
  55. data/lib/ldclient-rb/version.rb +3 -0
  56. data/scripts/gendocs.sh +11 -0
  57. data/scripts/release.sh +27 -0
  58. data/spec/config_spec.rb +63 -0
  59. data/spec/evaluation_spec.rb +739 -0
  60. data/spec/event_summarizer_spec.rb +63 -0
  61. data/spec/events_spec.rb +642 -0
  62. data/spec/expiring_cache_spec.rb +76 -0
  63. data/spec/feature_store_spec_base.rb +213 -0
  64. data/spec/file_data_source_spec.rb +255 -0
  65. data/spec/fixtures/feature.json +37 -0
  66. data/spec/fixtures/feature1.json +36 -0
  67. data/spec/fixtures/user.json +9 -0
  68. data/spec/flags_state_spec.rb +81 -0
  69. data/spec/http_util.rb +109 -0
  70. data/spec/in_memory_feature_store_spec.rb +12 -0
  71. data/spec/integrations/consul_feature_store_spec.rb +42 -0
  72. data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
  73. data/spec/integrations/store_wrapper_spec.rb +276 -0
  74. data/spec/ldclient_spec.rb +471 -0
  75. data/spec/newrelic_spec.rb +5 -0
  76. data/spec/polling_spec.rb +120 -0
  77. data/spec/redis_feature_store_spec.rb +95 -0
  78. data/spec/requestor_spec.rb +214 -0
  79. data/spec/segment_store_spec_base.rb +95 -0
  80. data/spec/simple_lru_cache_spec.rb +24 -0
  81. data/spec/spec_helper.rb +9 -0
  82. data/spec/store_spec.rb +10 -0
  83. data/spec/stream_spec.rb +60 -0
  84. data/spec/user_filter_spec.rb +91 -0
  85. data/spec/util_spec.rb +17 -0
  86. data/spec/version_spec.rb +7 -0
  87. metadata +375 -0
@@ -0,0 +1,455 @@
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) && (a.end_with? b)
144
+ end,
145
+ startsWith:
146
+ lambda do |a, b|
147
+ (a.is_a? String) && (a.start_with? b)
148
+ end,
149
+ matches:
150
+ lambda do |a, b|
151
+ (b.is_a? String) && !(Regexp.new b).match(a).nil?
152
+ end,
153
+ contains:
154
+ lambda do |a, b|
155
+ (a.is_a? String) && (a.include? b)
156
+ end,
157
+ lessThan:
158
+ lambda do |a, b|
159
+ (a.is_a? Numeric) && (a < b)
160
+ end,
161
+ lessThanOrEqual:
162
+ lambda do |a, b|
163
+ (a.is_a? Numeric) && (a <= b)
164
+ end,
165
+ greaterThan:
166
+ lambda do |a, b|
167
+ (a.is_a? Numeric) && (a > b)
168
+ end,
169
+ greaterThanOrEqual:
170
+ lambda do |a, b|
171
+ (a.is_a? Numeric) && (a >= b)
172
+ end,
173
+ before:
174
+ comparator(DATE_OPERAND) { |n| n < 0 },
175
+ after:
176
+ comparator(DATE_OPERAND) { |n| n > 0 },
177
+ semVerEqual:
178
+ comparator(SEMVER_OPERAND) { |n| n == 0 },
179
+ semVerLessThan:
180
+ comparator(SEMVER_OPERAND) { |n| n < 0 },
181
+ semVerGreaterThan:
182
+ comparator(SEMVER_OPERAND) { |n| n > 0 },
183
+ segmentMatch:
184
+ lambda do |a, b|
185
+ false # we should never reach this - instead we special-case this operator in clause_match_user
186
+ end
187
+ }
188
+
189
+ # Used internally to hold an evaluation result and the events that were generated from prerequisites.
190
+ EvalResult = Struct.new(:detail, :events)
191
+
192
+ USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION = [ :key, :secondary ]
193
+ # Currently we are not stringifying the rest of the built-in attributes prior to evaluation, only for events.
194
+ # This is because it could affect evaluation results for existing users (ch35206).
195
+
196
+ def error_result(errorKind, value = nil)
197
+ EvaluationDetail.new(value, nil, { kind: 'ERROR', errorKind: errorKind })
198
+ end
199
+
200
+ # Evaluates a feature flag and returns an EvalResult. The result.value will be nil if the flag returns
201
+ # the default value. Error conditions produce a result with an error reason, not an exception.
202
+ def evaluate(flag, user, store, logger)
203
+ if user.nil? || user[:key].nil?
204
+ return EvalResult.new(error_result('USER_NOT_SPECIFIED'), [])
205
+ end
206
+
207
+ sanitized_user = Util.stringify_attrs(user, USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION)
208
+
209
+ events = []
210
+ detail = eval_internal(flag, sanitized_user, store, events, logger)
211
+ return EvalResult.new(detail, events)
212
+ end
213
+
214
+ def eval_internal(flag, user, store, events, logger)
215
+ if !flag[:on]
216
+ return get_off_value(flag, { kind: 'OFF' }, logger)
217
+ end
218
+
219
+ prereq_failure_reason = check_prerequisites(flag, user, store, events, logger)
220
+ if !prereq_failure_reason.nil?
221
+ return get_off_value(flag, prereq_failure_reason, logger)
222
+ end
223
+
224
+ # Check user target matches
225
+ (flag[:targets] || []).each do |target|
226
+ (target[:values] || []).each do |value|
227
+ if value == user[:key]
228
+ return get_variation(flag, target[:variation], { kind: 'TARGET_MATCH' }, logger)
229
+ end
230
+ end
231
+ end
232
+
233
+ # Check custom rules
234
+ rules = flag[:rules] || []
235
+ rules.each_index do |i|
236
+ rule = rules[i]
237
+ if rule_match_user(rule, user, store)
238
+ return get_value_for_variation_or_rollout(flag, rule, user,
239
+ { kind: 'RULE_MATCH', ruleIndex: i, ruleId: rule[:id] }, logger)
240
+ end
241
+ end
242
+
243
+ # Check the fallthrough rule
244
+ if !flag[:fallthrough].nil?
245
+ return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user,
246
+ { kind: 'FALLTHROUGH' }, logger)
247
+ end
248
+
249
+ return EvaluationDetail.new(nil, nil, { kind: 'FALLTHROUGH' })
250
+ end
251
+
252
+ def check_prerequisites(flag, user, store, events, logger)
253
+ (flag[:prerequisites] || []).each do |prerequisite|
254
+ prereq_ok = true
255
+ prereq_key = prerequisite[:key]
256
+ prereq_flag = store.get(FEATURES, prereq_key)
257
+
258
+ if prereq_flag.nil?
259
+ logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
260
+ prereq_ok = false
261
+ else
262
+ begin
263
+ prereq_res = eval_internal(prereq_flag, user, store, events, logger)
264
+ # Note that if the prerequisite flag is off, we don't consider it a match no matter what its
265
+ # off variation was. But we still need to evaluate it in order to generate an event.
266
+ if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
267
+ prereq_ok = false
268
+ end
269
+ event = {
270
+ kind: "feature",
271
+ key: prereq_key,
272
+ user: user,
273
+ variation: prereq_res.variation_index,
274
+ value: prereq_res.value,
275
+ version: prereq_flag[:version],
276
+ prereqOf: flag[:key],
277
+ trackEvents: prereq_flag[:trackEvents],
278
+ debugEventsUntilDate: prereq_flag[:debugEventsUntilDate]
279
+ }
280
+ events.push(event)
281
+ rescue => exn
282
+ Util.log_exception(logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
283
+ prereq_ok = false
284
+ end
285
+ end
286
+ if !prereq_ok
287
+ return { kind: 'PREREQUISITE_FAILED', prerequisiteKey: prereq_key }
288
+ end
289
+ end
290
+ nil
291
+ end
292
+
293
+ def rule_match_user(rule, user, store)
294
+ return false if !rule[:clauses]
295
+
296
+ (rule[:clauses] || []).each do |clause|
297
+ return false if !clause_match_user(clause, user, store)
298
+ end
299
+
300
+ return true
301
+ end
302
+
303
+ def clause_match_user(clause, user, store)
304
+ # In the case of a segment match operator, we check if the user is in any of the segments,
305
+ # and possibly negate
306
+ if clause[:op].to_sym == :segmentMatch
307
+ (clause[:values] || []).each do |v|
308
+ segment = store.get(SEGMENTS, v)
309
+ return maybe_negate(clause, true) if !segment.nil? && segment_match_user(segment, user)
310
+ end
311
+ return maybe_negate(clause, false)
312
+ end
313
+ clause_match_user_no_segments(clause, user)
314
+ end
315
+
316
+ def clause_match_user_no_segments(clause, user)
317
+ val = user_value(user, clause[:attribute])
318
+ return false if val.nil?
319
+
320
+ op = OPERATORS[clause[:op].to_sym]
321
+ if op.nil?
322
+ return false
323
+ end
324
+
325
+ if val.is_a? Enumerable
326
+ val.each do |v|
327
+ return maybe_negate(clause, true) if match_any(op, v, clause[:values])
328
+ end
329
+ return maybe_negate(clause, false)
330
+ end
331
+
332
+ maybe_negate(clause, match_any(op, val, clause[:values]))
333
+ end
334
+
335
+ def variation_index_for_user(flag, rule, user)
336
+ if !rule[:variation].nil? # fixed variation
337
+ return rule[:variation]
338
+ elsif !rule[:rollout].nil? # percentage rollout
339
+ rollout = rule[:rollout]
340
+ bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
341
+ bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
342
+ sum = 0;
343
+ rollout[:variations].each do |variate|
344
+ sum += variate[:weight].to_f / 100000.0
345
+ if bucket < sum
346
+ return variate[:variation]
347
+ end
348
+ end
349
+ nil
350
+ else # the rule isn't well-formed
351
+ nil
352
+ end
353
+ end
354
+
355
+ def segment_match_user(segment, user)
356
+ return false unless user[:key]
357
+
358
+ return true if segment[:included].include?(user[:key])
359
+ return false if segment[:excluded].include?(user[:key])
360
+
361
+ (segment[:rules] || []).each do |r|
362
+ return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
363
+ end
364
+
365
+ return false
366
+ end
367
+
368
+ def segment_rule_match_user(rule, user, segment_key, salt)
369
+ (rule[:clauses] || []).each do |c|
370
+ return false unless clause_match_user_no_segments(c, user)
371
+ end
372
+
373
+ # If the weight is absent, this rule matches
374
+ return true if !rule[:weight]
375
+
376
+ # All of the clauses are met. See if the user buckets in
377
+ bucket = bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt)
378
+ weight = rule[:weight].to_f / 100000.0
379
+ return bucket < weight
380
+ end
381
+
382
+ def bucket_user(user, key, bucket_by, salt)
383
+ return nil unless user[:key]
384
+
385
+ id_hash = bucketable_string_value(user_value(user, bucket_by))
386
+ if id_hash.nil?
387
+ return 0.0
388
+ end
389
+
390
+ if user[:secondary]
391
+ id_hash += "." + user[:secondary]
392
+ end
393
+
394
+ hash_key = "%s.%s.%s" % [key, salt, id_hash]
395
+
396
+ hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
397
+ hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
398
+ end
399
+
400
+ def bucketable_string_value(value)
401
+ return value if value.is_a? String
402
+ return value.to_s if value.is_a? Integer
403
+ nil
404
+ end
405
+
406
+ def user_value(user, attribute)
407
+ attribute = attribute.to_sym
408
+
409
+ if BUILTINS.include? attribute
410
+ user[attribute]
411
+ elsif !user[:custom].nil?
412
+ user[:custom][attribute]
413
+ else
414
+ nil
415
+ end
416
+ end
417
+
418
+ def maybe_negate(clause, b)
419
+ clause[:negate] ? !b : b
420
+ end
421
+
422
+ def match_any(op, value, values)
423
+ values.each do |v|
424
+ return true if op.call(value, v)
425
+ end
426
+ return false
427
+ end
428
+
429
+ private
430
+
431
+ def get_variation(flag, index, reason, logger)
432
+ if index < 0 || index >= flag[:variations].length
433
+ logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
434
+ return error_result('MALFORMED_FLAG')
435
+ end
436
+ EvaluationDetail.new(flag[:variations][index], index, reason)
437
+ end
438
+
439
+ def get_off_value(flag, reason, logger)
440
+ if flag[:offVariation].nil? # off variation unspecified - return default value
441
+ return EvaluationDetail.new(nil, nil, reason)
442
+ end
443
+ get_variation(flag, flag[:offVariation], reason, logger)
444
+ end
445
+
446
+ def get_value_for_variation_or_rollout(flag, vr, user, reason, logger)
447
+ index = variation_index_for_user(flag, vr, user)
448
+ if index.nil?
449
+ logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
450
+ return error_result('MALFORMED_FLAG')
451
+ end
452
+ return get_variation(flag, index, reason, logger)
453
+ end
454
+ end
455
+ end