launchdarkly-server-sdk 6.4.0 → 7.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +102 -56
  3. data/lib/ldclient-rb/context.rb +487 -0
  4. data/lib/ldclient-rb/evaluation_detail.rb +20 -20
  5. data/lib/ldclient-rb/events.rb +77 -132
  6. data/lib/ldclient-rb/flags_state.rb +4 -4
  7. data/lib/ldclient-rb/impl/big_segments.rb +17 -17
  8. data/lib/ldclient-rb/impl/context.rb +96 -0
  9. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  10. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  11. data/lib/ldclient-rb/impl/evaluator.rb +379 -131
  12. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  13. data/lib/ldclient-rb/impl/evaluator_helpers.rb +28 -31
  14. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  15. data/lib/ldclient-rb/impl/event_sender.rb +6 -6
  16. data/lib/ldclient-rb/impl/event_summarizer.rb +12 -7
  17. data/lib/ldclient-rb/impl/event_types.rb +18 -30
  18. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
  19. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +29 -29
  20. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  21. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +92 -12
  22. data/lib/ldclient-rb/impl/model/clause.rb +39 -0
  23. data/lib/ldclient-rb/impl/model/feature_flag.rb +213 -0
  24. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +8 -121
  25. data/lib/ldclient-rb/impl/model/segment.rb +126 -0
  26. data/lib/ldclient-rb/impl/model/serialization.rb +52 -12
  27. data/lib/ldclient-rb/impl/repeating_task.rb +1 -1
  28. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  29. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  30. data/lib/ldclient-rb/impl/util.rb +2 -2
  31. data/lib/ldclient-rb/in_memory_store.rb +2 -2
  32. data/lib/ldclient-rb/integrations/consul.rb +1 -1
  33. data/lib/ldclient-rb/integrations/dynamodb.rb +1 -1
  34. data/lib/ldclient-rb/integrations/file_data.rb +3 -3
  35. data/lib/ldclient-rb/integrations/redis.rb +4 -4
  36. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +218 -62
  37. data/lib/ldclient-rb/integrations/test_data.rb +16 -12
  38. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +9 -9
  39. data/lib/ldclient-rb/interfaces.rb +14 -14
  40. data/lib/ldclient-rb/ldclient.rb +94 -144
  41. data/lib/ldclient-rb/memoized_value.rb +1 -1
  42. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  43. data/lib/ldclient-rb/polling.rb +2 -2
  44. data/lib/ldclient-rb/reference.rb +274 -0
  45. data/lib/ldclient-rb/requestor.rb +5 -5
  46. data/lib/ldclient-rb/stream.rb +7 -8
  47. data/lib/ldclient-rb/util.rb +4 -19
  48. data/lib/ldclient-rb/version.rb +1 -1
  49. data/lib/ldclient-rb.rb +2 -3
  50. metadata +34 -17
  51. data/lib/ldclient-rb/file_data_source.rb +0 -23
  52. data/lib/ldclient-rb/newrelic.rb +0 -17
  53. data/lib/ldclient-rb/redis_store.rb +0 -88
  54. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -2,6 +2,8 @@ require "ldclient-rb/evaluation_detail"
2
2
  require "ldclient-rb/impl/evaluator_bucketing"
3
3
  require "ldclient-rb/impl/evaluator_helpers"
4
4
  require "ldclient-rb/impl/evaluator_operators"
5
+ require "ldclient-rb/impl/model/feature_flag"
6
+ require "ldclient-rb/impl/model/segment"
5
7
 
6
8
  module LaunchDarkly
7
9
  module Impl
@@ -12,6 +14,81 @@ module LaunchDarkly
12
14
  :detail # the EvaluationDetail representing the evaluation result
13
15
  )
14
16
 
17
+ class EvaluationException < StandardError
18
+ def initialize(msg, error_kind = EvaluationReason::ERROR_MALFORMED_FLAG)
19
+ super(msg)
20
+ @error_kind = error_kind
21
+ end
22
+
23
+ # @return [Symbol]
24
+ attr_reader :error_kind
25
+ end
26
+
27
+ class InvalidReferenceException < EvaluationException
28
+ end
29
+
30
+ class EvaluatorState
31
+ # @param original_flag [LaunchDarkly::Impl::Model::FeatureFlag]
32
+ def initialize(original_flag)
33
+ @prereq_stack = EvaluatorStack.new(original_flag.key)
34
+ @segment_stack = EvaluatorStack.new(nil)
35
+ end
36
+
37
+ attr_reader :prereq_stack
38
+ attr_reader :segment_stack
39
+ end
40
+
41
+ #
42
+ # A helper class for managing cycle detection.
43
+ #
44
+ # Each time a method sees a new flag or segment, they can push that
45
+ # object's key onto the stack. Once processing for that object has
46
+ # finished, you can call pop to remove it.
47
+ #
48
+ # Because the most common use case would be a flag or segment without ANY
49
+ # prerequisites, this stack has a small optimization in place-- the stack
50
+ # is not created until absolutely necessary.
51
+ #
52
+ class EvaluatorStack
53
+ # @param original [String, nil]
54
+ def initialize(original)
55
+ @original = original
56
+ # @type [Array<String>, nil]
57
+ @stack = nil
58
+ end
59
+
60
+ # @param key [String]
61
+ def push(key)
62
+ # No need to store the key if we already have a record in our instance
63
+ # variable.
64
+ return if @original == key
65
+
66
+ # The common use case is that flags/segments won't have prereqs, so we
67
+ # don't allocate the stack memory until we absolutely must.
68
+ if @stack.nil?
69
+ @stack = []
70
+ end
71
+
72
+ @stack.push(key)
73
+ end
74
+
75
+ def pop
76
+ return if @stack.nil? || @stack.empty?
77
+ @stack.pop
78
+ end
79
+
80
+ #
81
+ # @param key [String]
82
+ # @return [Boolean]
83
+ #
84
+ def include?(key)
85
+ return true if key == @original
86
+ return false if @stack.nil?
87
+
88
+ @stack.include? key
89
+ end
90
+ end
91
+
15
92
  # Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
16
93
  # if it needs to retrieve flags or segments that are referenced by a flag, it does so through a simple function that
17
94
  # is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite
@@ -22,7 +99,7 @@ module LaunchDarkly
22
99
  # @param get_flag [Function] called if the Evaluator needs to query a different flag from the one that it is
23
100
  # currently evaluating (i.e. a prerequisite flag); takes a single parameter, the flag key, and returns the
24
101
  # flag data - or nil if the flag is unknown or deleted
25
- # @param get_segment [Function] similar to `get_flag`, but is used to query a user segment.
102
+ # @param get_segment [Function] similar to `get_flag`, but is used to query a context segment.
26
103
  # @param logger [Logger] the client's logger
27
104
  def initialize(get_flag, get_segment, get_big_segments_membership, logger)
28
105
  @get_flag = get_flag
@@ -30,15 +107,15 @@ module LaunchDarkly
30
107
  @get_big_segments_membership = get_big_segments_membership
31
108
  @logger = logger
32
109
  end
33
-
110
+
34
111
  # Used internally to hold an evaluation result and additional state that may be accumulated during an
35
112
  # evaluation. It's simpler and a bit more efficient to represent these as mutable properties rather than
36
113
  # trying to use a pure functional approach, and since we're not exposing this object to any application code
37
114
  # or retaining it anywhere, we don't have to be quite as strict about immutability.
38
115
  #
39
116
  # The big_segments_status and big_segments_membership properties are not used by the caller; they are used
40
- # during an evaluation to cache the result of any Big Segments query that we've done for this user, because
41
- # we don't want to do multiple queries for the same user if multiple Big Segments are referenced in the same
117
+ # during an evaluation to cache the result of any Big Segments query that we've done for this context, because
118
+ # we don't want to do multiple queries for the same context if multiple Big Segments are referenced in the same
42
119
  # evaluation.
43
120
  EvalResult = Struct.new(
44
121
  :detail, # the EvaluationDetail representing the evaluation result
@@ -56,223 +133,394 @@ module LaunchDarkly
56
133
  # any events that were generated for prerequisite flags; its `value` will be `nil` if the flag returns the
57
134
  # default value. Error conditions produce a result with a nil value and an error reason, not an exception.
58
135
  #
59
- # @param flag [Object] the flag
60
- # @param user [Object] the user properties
61
- # @return [EvalResult] the evaluation result
62
- def evaluate(flag, user)
136
+ # @param flag [LaunchDarkly::Impl::Model::FeatureFlag] the flag
137
+ # @param context [LaunchDarkly::LDContext] the evaluation context
138
+ # @return [EvalResult] the evaluation result
139
+ def evaluate(flag, context)
140
+ state = EvaluatorState.new(flag)
141
+
63
142
  result = EvalResult.new
64
- if user.nil? || user[:key].nil?
65
- result.detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED)
143
+ begin
144
+ detail = eval_internal(flag, context, result, state)
145
+ rescue EvaluationException => exn
146
+ LaunchDarkly::Util.log_exception(@logger, "Unexpected error when evaluating flag #{flag.key}", exn)
147
+ result.detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(exn.error_kind))
148
+ return result
149
+ rescue => exn
150
+ LaunchDarkly::Util.log_exception(@logger, "Unexpected error when evaluating flag #{flag.key}", exn)
151
+ result.detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION))
66
152
  return result
67
153
  end
68
-
69
- detail = eval_internal(flag, user, result)
70
- if !result.big_segments_status.nil?
154
+
155
+ unless result.big_segments_status.nil?
71
156
  # If big_segments_status is non-nil at the end of the evaluation, it means a query was done at
72
157
  # some point and we will want to include the status in the evaluation reason.
73
158
  detail = EvaluationDetail.new(detail.value, detail.variation_index,
74
159
  detail.reason.with_big_segments_status(result.big_segments_status))
75
160
  end
76
161
  result.detail = detail
77
- return result
162
+ result
78
163
  end
79
164
 
165
+ # @param segment [LaunchDarkly::Impl::Model::Segment]
80
166
  def self.make_big_segment_ref(segment) # method is visible for testing
81
167
  # The format of Big Segment references is independent of what store implementation is being
82
168
  # used; the store implementation receives only this string and does not know the details of
83
169
  # the data model. The Relay Proxy will use the same format when writing to the store.
84
- "#{segment[:key]}.g#{segment[:generation]}"
170
+ "#{segment.key}.g#{segment.generation}"
85
171
  end
86
172
 
87
- private
88
-
89
- def eval_internal(flag, user, state)
90
- if !flag[:on]
91
- return EvaluatorHelpers.off_result(flag)
173
+ # @param flag [LaunchDarkly::Impl::Model::FeatureFlag] the flag
174
+ # @param context [LaunchDarkly::LDContext] the evaluation context
175
+ # @param eval_result [EvalResult]
176
+ # @param state [EvaluatorState]
177
+ # @raise [EvaluationException]
178
+ private def eval_internal(flag, context, eval_result, state)
179
+ unless flag.on
180
+ return flag.off_result
92
181
  end
93
182
 
94
- prereq_failure_result = check_prerequisites(flag, user, state)
95
- return prereq_failure_result if !prereq_failure_result.nil?
183
+ prereq_failure_result = check_prerequisites(flag, context, eval_result, state)
184
+ return prereq_failure_result unless prereq_failure_result.nil?
185
+
186
+ # Check context target matches
187
+ target_result = check_targets(context, flag)
188
+ return target_result unless target_result.nil?
96
189
 
97
- # Check user target matches
98
- (flag[:targets] || []).each do |target|
99
- (target[:values] || []).each do |value|
100
- if value == user[:key]
101
- return EvaluatorHelpers.target_match_result(target, flag)
102
- end
103
- end
104
- end
105
-
106
190
  # Check custom rules
107
- rules = flag[:rules] || []
108
- rules.each_index do |i|
109
- rule = rules[i]
110
- if rule_match_user(rule, user, state)
111
- reason = rule[:_reason] # try to use cached reason for this rule
112
- reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
113
- return get_value_for_variation_or_rollout(flag, rule, user, reason,
114
- EvaluatorHelpers.rule_precomputed_results(rule))
191
+ flag.rules.each do |rule|
192
+ if rule_match_context(rule, context, eval_result, state)
193
+ return get_value_for_variation_or_rollout(flag, rule.variation_or_rollout, context, rule.match_results)
115
194
  end
116
195
  end
117
196
 
118
197
  # Check the fallthrough rule
119
- if !flag[:fallthrough].nil?
120
- return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, EvaluationReason::fallthrough,
121
- EvaluatorHelpers.fallthrough_precomputed_results(flag))
198
+ unless flag.fallthrough.nil?
199
+ return get_value_for_variation_or_rollout(flag, flag.fallthrough, context, flag.fallthrough_results)
122
200
  end
123
201
 
124
- return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
202
+ EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
125
203
  end
126
204
 
127
- def check_prerequisites(flag, user, state)
128
- (flag[:prerequisites] || []).each do |prerequisite|
129
- prereq_ok = true
130
- prereq_key = prerequisite[:key]
131
- prereq_flag = @get_flag.call(prereq_key)
205
+ # @param flag [LaunchDarkly::Impl::Model::FeatureFlag] the flag
206
+ # @param context [LaunchDarkly::LDContext] the evaluation context
207
+ # @param eval_result [EvalResult]
208
+ # @param state [EvaluatorState]
209
+ # @raise [EvaluationException] if a flag prereq cycle is detected
210
+ private def check_prerequisites(flag, context, eval_result, state)
211
+ return if flag.prerequisites.empty?
132
212
 
133
- if prereq_flag.nil?
134
- @logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
135
- prereq_ok = false
136
- else
137
- begin
138
- prereq_res = eval_internal(prereq_flag, user, state)
213
+ state.prereq_stack.push(flag.key)
214
+
215
+ begin
216
+ flag.prerequisites.each do |prerequisite|
217
+ prereq_ok = true
218
+ prereq_key = prerequisite.key
219
+
220
+ if state.prereq_stack.include?(prereq_key)
221
+ raise LaunchDarkly::Impl::EvaluationException.new(
222
+ "prerequisite relationship to \"#{prereq_key}\" caused a circular reference; this is probably a temporary condition due to an incomplete update"
223
+ )
224
+ end
225
+
226
+ prereq_flag = @get_flag.call(prereq_key)
227
+
228
+ if prereq_flag.nil?
229
+ @logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag.key}\"" }
230
+ prereq_ok = false
231
+ else
232
+ prereq_res = eval_internal(prereq_flag, context, eval_result, state)
139
233
  # Note that if the prerequisite flag is off, we don't consider it a match no matter what its
140
234
  # off variation was. But we still need to evaluate it in order to generate an event.
141
- if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
235
+ if !prereq_flag.on || prereq_res.variation_index != prerequisite.variation
142
236
  prereq_ok = false
143
237
  end
144
238
  prereq_eval = PrerequisiteEvalRecord.new(prereq_flag, flag, prereq_res)
145
- state.prereq_evals = [] if state.prereq_evals.nil?
146
- state.prereq_evals.push(prereq_eval)
147
- rescue => exn
148
- Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
149
- prereq_ok = false
239
+ eval_result.prereq_evals = [] if eval_result.prereq_evals.nil?
240
+ eval_result.prereq_evals.push(prereq_eval)
241
+ end
242
+
243
+ unless prereq_ok
244
+ return prerequisite.failure_result
150
245
  end
151
246
  end
152
- if !prereq_ok
153
- return EvaluatorHelpers.prerequisite_failed_result(prerequisite, flag)
154
- end
247
+ ensure
248
+ state.prereq_stack.pop
155
249
  end
250
+
156
251
  nil
157
252
  end
158
253
 
159
- def rule_match_user(rule, user, state)
160
- return false if !rule[:clauses]
161
-
162
- (rule[:clauses] || []).each do |clause|
163
- return false if !clause_match_user(clause, user, state)
254
+ # @param rule [LaunchDarkly::Impl::Model::FlagRule]
255
+ # @param context [LaunchDarkly::LDContext]
256
+ # @param eval_result [EvalResult]
257
+ # @param state [EvaluatorState]
258
+ # @raise [InvalidReferenceException]
259
+ private def rule_match_context(rule, context, eval_result, state)
260
+ rule.clauses.each do |clause|
261
+ return false unless clause_match_context(clause, context, eval_result, state)
164
262
  end
165
263
 
166
- return true
264
+ true
167
265
  end
168
266
 
169
- def clause_match_user(clause, user, state)
170
- # In the case of a segment match operator, we check if the user is in any of the segments,
267
+ # @param clause [LaunchDarkly::Impl::Model::Clause]
268
+ # @param context [LaunchDarkly::LDContext]
269
+ # @param eval_result [EvalResult]
270
+ # @param state [EvaluatorState]
271
+ # @raise [InvalidReferenceException]
272
+ private def clause_match_context(clause, context, eval_result, state)
273
+ # In the case of a segment match operator, we check if the context is in any of the segments,
171
274
  # and possibly negate
172
- if clause[:op].to_sym == :segmentMatch
173
- result = (clause[:values] || []).any? { |v|
275
+ if clause.op == :segmentMatch
276
+ result = clause.values.any? { |v|
277
+ if state.segment_stack.include?(v)
278
+ raise LaunchDarkly::Impl::EvaluationException.new(
279
+ "segment rule referencing segment \"#{v}\" caused a circular reference; this is probably a temporary condition due to an incomplete update"
280
+ )
281
+ end
282
+
174
283
  segment = @get_segment.call(v)
175
- !segment.nil? && segment_match_user(segment, user, state)
284
+ !segment.nil? && segment_match_context(segment, context, eval_result, state)
176
285
  }
177
- clause[:negate] ? !result : result
286
+ clause.negate ? !result : result
178
287
  else
179
- clause_match_user_no_segments(clause, user)
288
+ clause_match_context_no_segments(clause, context)
180
289
  end
181
290
  end
182
291
 
183
- def clause_match_user_no_segments(clause, user)
184
- user_val = EvaluatorOperators.user_value(user, clause[:attribute])
185
- return false if user_val.nil?
292
+ # @param clause [LaunchDarkly::Impl::Model::Clause]
293
+ # @param context_value [any]
294
+ # @return [Boolean]
295
+ private def match_any_clause_value(clause, context_value)
296
+ op = clause.op
297
+ clause.values.any? { |cv| EvaluatorOperators.apply(op, context_value, cv) }
298
+ end
299
+
300
+ # @param clause [LaunchDarkly::Impl::Model::Clause]
301
+ # @param context [LaunchDarkly::LDContext]
302
+ # @return [Boolean]
303
+ private def clause_match_by_kind(clause, context)
304
+ # If attribute is "kind", then we treat operator and values as a match
305
+ # expression against a list of all individual kinds in the context.
306
+ # That is, for a multi-kind context with kinds of "org" and "user", it
307
+ # is a match if either of those strings is a match with Operator and
308
+ # Values.
186
309
 
187
- op = clause[:op].to_sym
188
- clause_vals = clause[:values]
189
- result = if user_val.is_a? Enumerable
190
- user_val.any? { |uv| clause_vals.any? { |cv| EvaluatorOperators.apply(op, uv, cv) } }
310
+ (0...context.individual_context_count).each do |i|
311
+ c = context.individual_context(i)
312
+ if !c.nil? && match_any_clause_value(clause, c.kind)
313
+ return true
314
+ end
315
+ end
316
+
317
+ false
318
+ end
319
+
320
+ # @param clause [LaunchDarkly::Impl::Model::Clause]
321
+ # @param context [LaunchDarkly::LDContext]
322
+ # @return [Boolean]
323
+ # @raise [InvalidReferenceException] Raised if the clause.attribute is an invalid reference
324
+ private def clause_match_context_no_segments(clause, context)
325
+ raise InvalidReferenceException.new(clause.attribute.error) unless clause.attribute.error.nil?
326
+
327
+ if clause.attribute.depth == 1 && clause.attribute.component(0) == :kind
328
+ result = clause_match_by_kind(clause, context)
329
+ return clause.negate ? !result : result
330
+ end
331
+
332
+ matched_context = context.individual_context(clause.context_kind || LaunchDarkly::LDContext::KIND_DEFAULT)
333
+ return false if matched_context.nil?
334
+
335
+ context_val = matched_context.get_value_for_reference(clause.attribute)
336
+ return false if context_val.nil?
337
+
338
+ result = if context_val.is_a? Enumerable
339
+ context_val.any? { |uv| match_any_clause_value(clause, uv) }
191
340
  else
192
- clause_vals.any? { |cv| EvaluatorOperators.apply(op, user_val, cv) }
341
+ match_any_clause_value(clause, context_val)
193
342
  end
194
- clause[:negate] ? !result : result
343
+ clause.negate ? !result : result
195
344
  end
196
345
 
197
- def segment_match_user(segment, user, state)
198
- return false unless user[:key]
199
- segment[:unbounded] ? big_segment_match_user(segment, user, state) : simple_segment_match_user(segment, user, true)
346
+ # @param segment [LaunchDarkly::Impl::Model::Segment]
347
+ # @param context [LaunchDarkly::LDContext]
348
+ # @param eval_result [EvalResult]
349
+ # @param state [EvaluatorState]
350
+ # @return [Boolean]
351
+ private def segment_match_context(segment, context, eval_result, state)
352
+ return big_segment_match_context(segment, context, eval_result, state) if segment.unbounded
353
+
354
+ simple_segment_match_context(segment, context, true, eval_result, state)
200
355
  end
201
356
 
202
- def big_segment_match_user(segment, user, state)
203
- if !segment[:generation]
357
+ # @param segment [LaunchDarkly::Impl::Model::Segment]
358
+ # @param context [LaunchDarkly::LDContext]
359
+ # @param eval_result [EvalResult]
360
+ # @param state [EvaluatorState]
361
+ # @return [Boolean]
362
+ private def big_segment_match_context(segment, context, eval_result, state)
363
+ unless segment.generation
204
364
  # Big segment queries can only be done if the generation is known. If it's unset,
205
365
  # that probably means the data store was populated by an older SDK that doesn't know
206
366
  # about the generation property and therefore dropped it from the JSON data. We'll treat
207
367
  # that as a "not configured" condition.
208
- state.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
368
+ eval_result.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
209
369
  return false
210
370
  end
211
- if !state.big_segments_status
212
- result = @get_big_segments_membership.nil? ? nil : @get_big_segments_membership.call(user[:key])
371
+
372
+ matched_context = context.individual_context(segment.unbounded_context_kind)
373
+ return false if matched_context.nil?
374
+
375
+ membership = eval_result.big_segments_membership.nil? ? nil : eval_result.big_segments_membership[matched_context.key]
376
+
377
+ if membership.nil?
378
+ # Note that this query is just by key; the context kind doesn't matter because any given
379
+ # Big Segment can only reference one context kind. So if segment A for the "user" kind
380
+ # includes a "user" context with key X, and segment B for the "org" kind includes an "org"
381
+ # context with the same key X, it is fine to say that the membership for key X is
382
+ # segment A and segment B-- there is no ambiguity.
383
+ result = @get_big_segments_membership.nil? ? nil : @get_big_segments_membership.call(matched_context.key)
213
384
  if result
214
- state.big_segments_membership = result.membership
215
- state.big_segments_status = result.status
385
+ eval_result.big_segments_status = result.status
386
+
387
+ membership = result.membership
388
+ eval_result.big_segments_membership = {} if eval_result.big_segments_membership.nil?
389
+ eval_result.big_segments_membership[matched_context.key] = membership
216
390
  else
217
- state.big_segments_membership = nil
218
- state.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
391
+ eval_result.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
219
392
  end
220
393
  end
221
- segment_ref = Evaluator.make_big_segment_ref(segment)
222
- membership = state.big_segments_membership
223
- included = membership.nil? ? nil : membership[segment_ref]
224
- return included if !included.nil?
225
- simple_segment_match_user(segment, user, false)
394
+
395
+ membership_result = nil
396
+ unless membership.nil?
397
+ segment_ref = Evaluator.make_big_segment_ref(segment)
398
+ membership_result = membership.nil? ? nil : membership[segment_ref]
399
+ end
400
+
401
+ return membership_result unless membership_result.nil?
402
+ simple_segment_match_context(segment, context, false, eval_result, state)
226
403
  end
227
404
 
228
- def simple_segment_match_user(segment, user, use_includes_and_excludes)
405
+ # @param segment [LaunchDarkly::Impl::Model::Segment]
406
+ # @param context [LaunchDarkly::LDContext]
407
+ # @param use_includes_and_excludes [Boolean]
408
+ # @param state [EvaluatorState]
409
+ # @return [Boolean]
410
+ private def simple_segment_match_context(segment, context, use_includes_and_excludes, eval_result, state)
229
411
  if use_includes_and_excludes
230
- return true if segment[:included].include?(user[:key])
231
- return false if segment[:excluded].include?(user[:key])
412
+ if EvaluatorHelpers.context_key_in_target_list(context, nil, segment.included)
413
+ return true
414
+ end
415
+
416
+ segment.included_contexts.each do |target|
417
+ if EvaluatorHelpers.context_key_in_target_list(context, target.context_kind, target.values)
418
+ return true
419
+ end
420
+ end
421
+
422
+ if EvaluatorHelpers.context_key_in_target_list(context, nil, segment.excluded)
423
+ return false
424
+ end
425
+
426
+ segment.excluded_contexts.each do |target|
427
+ if EvaluatorHelpers.context_key_in_target_list(context, target.context_kind, target.values)
428
+ return false
429
+ end
430
+ end
232
431
  end
233
432
 
234
- (segment[:rules] || []).each do |r|
235
- return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
433
+ rules = segment.rules
434
+ state.segment_stack.push(segment.key) unless rules.empty?
435
+
436
+ begin
437
+ rules.each do |r|
438
+ return true if segment_rule_match_context(r, context, segment.key, segment.salt, eval_result, state)
439
+ end
440
+ ensure
441
+ state.segment_stack.pop
236
442
  end
237
443
 
238
- return false
444
+ false
239
445
  end
240
446
 
241
- def segment_rule_match_user(rule, user, segment_key, salt)
242
- (rule[:clauses] || []).each do |c|
243
- return false unless clause_match_user_no_segments(c, user)
447
+ # @param rule [LaunchDarkly::Impl::Model::SegmentRule]
448
+ # @param context [LaunchDarkly::LDContext]
449
+ # @param segment_key [String]
450
+ # @param salt [String]
451
+ # @return [Boolean]
452
+ # @raise [InvalidReferenceException]
453
+ private def segment_rule_match_context(rule, context, segment_key, salt, eval_result, state)
454
+ rule.clauses.each do |c|
455
+ return false unless clause_match_context(c, context, eval_result, state)
244
456
  end
245
457
 
246
458
  # If the weight is absent, this rule matches
247
- return true if !rule[:weight]
248
-
459
+ return true unless rule.weight
460
+
249
461
  # All of the clauses are met. See if the user buckets in
250
- bucket = EvaluatorBucketing.bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt, nil)
251
- weight = rule[:weight].to_f / 100000.0
252
- return bucket < weight
462
+ begin
463
+ bucket = EvaluatorBucketing.bucket_context(context, rule.rollout_context_kind, segment_key, rule.bucket_by || "key", salt, nil)
464
+ rescue InvalidReferenceException
465
+ return false
466
+ end
467
+
468
+ weight = rule.weight.to_f / 100000.0
469
+ bucket.nil? || bucket < weight
253
470
  end
254
471
 
255
- private
256
-
257
- def get_value_for_variation_or_rollout(flag, vr, user, reason, precomputed_results)
258
- index, in_experiment = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
472
+ private def get_value_for_variation_or_rollout(flag, vr, context, precomputed_results)
473
+ index, in_experiment = EvaluatorBucketing.variation_index_for_context(flag, vr, context)
474
+
259
475
  if index.nil?
260
- @logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
476
+ @logger.error("[LDClient] Data inconsistency in feature flag \"#{flag.key}\": variation/rollout object with no variation or rollout")
261
477
  return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
262
478
  end
263
- if precomputed_results
264
- return precomputed_results.for_variation(index, in_experiment)
265
- else
266
- #if in experiment is true, set reason to a different reason instance/singleton with in_experiment set
267
- if in_experiment
268
- if reason.kind == :FALLTHROUGH
269
- reason = EvaluationReason::fallthrough(in_experiment)
270
- elsif reason.kind == :RULE_MATCH
271
- reason = EvaluationReason::rule_match(reason.rule_index, reason.rule_id, in_experiment)
479
+ precomputed_results.for_variation(index, in_experiment)
480
+ end
481
+
482
+ # @param [LaunchDarkly::LDContext] context
483
+ # @param [LaunchDarkly::Impl::Model::FeatureFlag] flag
484
+ # @return [LaunchDarkly::EvaluationDetail, nil]
485
+ private def check_targets(context, flag)
486
+ targets = flag.targets
487
+ context_targets = flag.context_targets
488
+
489
+ if context_targets.empty?
490
+ unless targets.empty?
491
+ user_context = context.individual_context(LDContext::KIND_DEFAULT)
492
+ return nil if user_context.nil?
493
+
494
+ targets.each do |target|
495
+ if target.values.include?(user_context.key) # rubocop:disable Performance/InefficientHashSearch
496
+ return target.match_result
497
+ end
498
+ end
499
+ end
500
+
501
+ return nil
502
+ end
503
+
504
+ context_targets.each do |target|
505
+ if target.kind == LDContext::KIND_DEFAULT
506
+ user_context = context.individual_context(LDContext::KIND_DEFAULT)
507
+ next if user_context.nil?
508
+
509
+ user_key = user_context.key
510
+ targets.each do |user_target|
511
+ if user_target.variation == target.variation
512
+ if user_target.values.include?(user_key) # rubocop:disable Performance/InefficientHashSearch
513
+ return target.match_result
514
+ end
515
+ break
516
+ end
272
517
  end
518
+ elsif EvaluatorHelpers.context_key_in_target_list(context, target.kind, target.values)
519
+ return target.match_result
273
520
  end
274
- return EvaluatorHelpers.evaluation_detail_for_variation(flag, index, reason)
275
521
  end
522
+
523
+ nil
276
524
  end
277
525
  end
278
526
  end