launchdarkly-server-sdk 6.1.1 → 6.4.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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -5
  3. data/lib/ldclient-rb/config.rb +118 -4
  4. data/lib/ldclient-rb/evaluation_detail.rb +104 -14
  5. data/lib/ldclient-rb/events.rb +201 -107
  6. data/lib/ldclient-rb/file_data_source.rb +9 -300
  7. data/lib/ldclient-rb/flags_state.rb +23 -12
  8. data/lib/ldclient-rb/impl/big_segments.rb +117 -0
  9. data/lib/ldclient-rb/impl/diagnostic_events.rb +1 -1
  10. data/lib/ldclient-rb/impl/evaluator.rb +116 -62
  11. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +22 -9
  12. data/lib/ldclient-rb/impl/evaluator_helpers.rb +53 -0
  13. data/lib/ldclient-rb/impl/evaluator_operators.rb +1 -1
  14. data/lib/ldclient-rb/impl/event_summarizer.rb +63 -0
  15. data/lib/ldclient-rb/impl/event_types.rb +90 -0
  16. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +82 -18
  17. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
  18. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +84 -31
  19. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
  20. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +177 -0
  21. data/lib/ldclient-rb/impl/model/serialization.rb +7 -37
  22. data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
  23. data/lib/ldclient-rb/impl/util.rb +62 -1
  24. data/lib/ldclient-rb/integrations/consul.rb +8 -1
  25. data/lib/ldclient-rb/integrations/dynamodb.rb +48 -3
  26. data/lib/ldclient-rb/integrations/file_data.rb +108 -0
  27. data/lib/ldclient-rb/integrations/redis.rb +42 -2
  28. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +438 -0
  29. data/lib/ldclient-rb/integrations/test_data.rb +209 -0
  30. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +5 -0
  31. data/lib/ldclient-rb/integrations.rb +2 -51
  32. data/lib/ldclient-rb/interfaces.rb +152 -2
  33. data/lib/ldclient-rb/ldclient.rb +131 -33
  34. data/lib/ldclient-rb/polling.rb +22 -41
  35. data/lib/ldclient-rb/requestor.rb +3 -3
  36. data/lib/ldclient-rb/stream.rb +4 -3
  37. data/lib/ldclient-rb/util.rb +10 -1
  38. data/lib/ldclient-rb/version.rb +1 -1
  39. data/lib/ldclient-rb.rb +0 -1
  40. metadata +35 -132
  41. data/.circleci/config.yml +0 -40
  42. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -37
  43. data/.github/ISSUE_TEMPLATE/config.yml +0 -5
  44. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  45. data/.github/pull_request_template.md +0 -21
  46. data/.gitignore +0 -16
  47. data/.hound.yml +0 -2
  48. data/.ldrelease/build-docs.sh +0 -18
  49. data/.ldrelease/circleci/linux/execute.sh +0 -18
  50. data/.ldrelease/circleci/mac/execute.sh +0 -18
  51. data/.ldrelease/circleci/template/build.sh +0 -29
  52. data/.ldrelease/circleci/template/publish.sh +0 -23
  53. data/.ldrelease/circleci/template/set-gem-home.sh +0 -7
  54. data/.ldrelease/circleci/template/test.sh +0 -10
  55. data/.ldrelease/circleci/template/update-version.sh +0 -8
  56. data/.ldrelease/circleci/windows/execute.ps1 +0 -19
  57. data/.ldrelease/config.yml +0 -29
  58. data/.rspec +0 -2
  59. data/.rubocop.yml +0 -600
  60. data/.simplecov +0 -4
  61. data/CHANGELOG.md +0 -351
  62. data/CODEOWNERS +0 -1
  63. data/CONTRIBUTING.md +0 -37
  64. data/Gemfile +0 -3
  65. data/azure-pipelines.yml +0 -51
  66. data/docs/Makefile +0 -26
  67. data/docs/index.md +0 -9
  68. data/launchdarkly-server-sdk.gemspec +0 -45
  69. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  70. data/lib/ldclient-rb/impl/event_factory.rb +0 -120
  71. data/spec/config_spec.rb +0 -63
  72. data/spec/diagnostic_events_spec.rb +0 -163
  73. data/spec/evaluation_detail_spec.rb +0 -135
  74. data/spec/event_sender_spec.rb +0 -197
  75. data/spec/event_summarizer_spec.rb +0 -63
  76. data/spec/events_spec.rb +0 -607
  77. data/spec/expiring_cache_spec.rb +0 -76
  78. data/spec/feature_store_spec_base.rb +0 -213
  79. data/spec/file_data_source_spec.rb +0 -283
  80. data/spec/fixtures/feature.json +0 -37
  81. data/spec/fixtures/feature1.json +0 -36
  82. data/spec/fixtures/user.json +0 -9
  83. data/spec/flags_state_spec.rb +0 -81
  84. data/spec/http_util.rb +0 -132
  85. data/spec/impl/evaluator_bucketing_spec.rb +0 -111
  86. data/spec/impl/evaluator_clause_spec.rb +0 -55
  87. data/spec/impl/evaluator_operators_spec.rb +0 -141
  88. data/spec/impl/evaluator_rule_spec.rb +0 -96
  89. data/spec/impl/evaluator_segment_spec.rb +0 -125
  90. data/spec/impl/evaluator_spec.rb +0 -305
  91. data/spec/impl/evaluator_spec_base.rb +0 -75
  92. data/spec/impl/model/serialization_spec.rb +0 -41
  93. data/spec/in_memory_feature_store_spec.rb +0 -12
  94. data/spec/integrations/consul_feature_store_spec.rb +0 -40
  95. data/spec/integrations/dynamodb_feature_store_spec.rb +0 -103
  96. data/spec/integrations/store_wrapper_spec.rb +0 -276
  97. data/spec/launchdarkly-server-sdk_spec.rb +0 -13
  98. data/spec/launchdarkly-server-sdk_spec_autoloadtest.rb +0 -9
  99. data/spec/ldclient_end_to_end_spec.rb +0 -157
  100. data/spec/ldclient_spec.rb +0 -643
  101. data/spec/newrelic_spec.rb +0 -5
  102. data/spec/polling_spec.rb +0 -120
  103. data/spec/redis_feature_store_spec.rb +0 -121
  104. data/spec/requestor_spec.rb +0 -196
  105. data/spec/segment_store_spec_base.rb +0 -95
  106. data/spec/simple_lru_cache_spec.rb +0 -24
  107. data/spec/spec_helper.rb +0 -9
  108. data/spec/store_spec.rb +0 -10
  109. data/spec/stream_spec.rb +0 -45
  110. data/spec/user_filter_spec.rb +0 -91
  111. data/spec/util_spec.rb +0 -17
  112. data/spec/version_spec.rb +0 -7
@@ -1,9 +1,17 @@
1
1
  require "ldclient-rb/evaluation_detail"
2
2
  require "ldclient-rb/impl/evaluator_bucketing"
3
+ require "ldclient-rb/impl/evaluator_helpers"
3
4
  require "ldclient-rb/impl/evaluator_operators"
4
5
 
5
6
  module LaunchDarkly
6
7
  module Impl
8
+ # Used internally to record that we evaluated a prerequisite flag.
9
+ PrerequisiteEvalRecord = Struct.new(
10
+ :prereq_flag, # the prerequisite flag that we evaluated
11
+ :prereq_of_flag, # the flag that it was a prerequisite of
12
+ :detail # the EvaluationDetail representing the evaluation result
13
+ )
14
+
7
15
  # Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
8
16
  # if it needs to retrieve flags or segments that are referenced by a flag, it does so through a simple function that
9
17
  # is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite
@@ -16,16 +24,28 @@ module LaunchDarkly
16
24
  # flag data - or nil if the flag is unknown or deleted
17
25
  # @param get_segment [Function] similar to `get_flag`, but is used to query a user segment.
18
26
  # @param logger [Logger] the client's logger
19
- def initialize(get_flag, get_segment, logger)
27
+ def initialize(get_flag, get_segment, get_big_segments_membership, logger)
20
28
  @get_flag = get_flag
21
29
  @get_segment = get_segment
30
+ @get_big_segments_membership = get_big_segments_membership
22
31
  @logger = logger
23
32
  end
24
-
25
- # Used internally to hold an evaluation result and the events that were generated from prerequisites. The
26
- # `detail` property is an EvaluationDetail. The `events` property can be either an array of feature request
27
- # events or nil.
28
- EvalResult = Struct.new(:detail, :events)
33
+
34
+ # Used internally to hold an evaluation result and additional state that may be accumulated during an
35
+ # evaluation. It's simpler and a bit more efficient to represent these as mutable properties rather than
36
+ # trying to use a pure functional approach, and since we're not exposing this object to any application code
37
+ # or retaining it anywhere, we don't have to be quite as strict about immutability.
38
+ #
39
+ # 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
42
+ # evaluation.
43
+ EvalResult = Struct.new(
44
+ :detail, # the EvaluationDetail representing the evaluation result
45
+ :prereq_evals, # an array of PrerequisiteEvalRecord instances, or nil
46
+ :big_segments_status,
47
+ :big_segments_membership
48
+ )
29
49
 
30
50
  # Helper function used internally to construct an EvaluationDetail for an error result.
31
51
  def self.error_result(errorKind, value = nil)
@@ -38,43 +58,47 @@ module LaunchDarkly
38
58
  #
39
59
  # @param flag [Object] the flag
40
60
  # @param user [Object] the user properties
41
- # @param event_factory [EventFactory] called to construct a feature request event when a prerequisite flag is
42
- # evaluated; the caller is responsible for constructing the feature event for the top-level evaluation
43
61
  # @return [EvalResult] the evaluation result
44
- def evaluate(flag, user, event_factory)
62
+ def evaluate(flag, user)
63
+ result = EvalResult.new
45
64
  if user.nil? || user[:key].nil?
46
- return EvalResult.new(Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED), [])
65
+ result.detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED)
66
+ return result
47
67
  end
48
-
49
- # If the flag doesn't have any prerequisites (which most flags don't) then it cannot generate any feature
50
- # request events for prerequisites and we can skip allocating an array.
51
- if flag[:prerequisites] && !flag[:prerequisites].empty?
52
- events = []
53
- else
54
- events = nil
68
+
69
+ detail = eval_internal(flag, user, result)
70
+ if !result.big_segments_status.nil?
71
+ # If big_segments_status is non-nil at the end of the evaluation, it means a query was done at
72
+ # some point and we will want to include the status in the evaluation reason.
73
+ detail = EvaluationDetail.new(detail.value, detail.variation_index,
74
+ detail.reason.with_big_segments_status(result.big_segments_status))
55
75
  end
76
+ result.detail = detail
77
+ return result
78
+ end
56
79
 
57
- detail = eval_internal(flag, user, events, event_factory)
58
- return EvalResult.new(detail, events.nil? || events.empty? ? nil : events)
80
+ def self.make_big_segment_ref(segment) # method is visible for testing
81
+ # The format of Big Segment references is independent of what store implementation is being
82
+ # used; the store implementation receives only this string and does not know the details of
83
+ # the data model. The Relay Proxy will use the same format when writing to the store.
84
+ "#{segment[:key]}.g#{segment[:generation]}"
59
85
  end
60
86
 
61
87
  private
62
88
 
63
- def eval_internal(flag, user, events, event_factory)
89
+ def eval_internal(flag, user, state)
64
90
  if !flag[:on]
65
- return get_off_value(flag, EvaluationReason::off)
91
+ return EvaluatorHelpers.off_result(flag)
66
92
  end
67
93
 
68
- prereq_failure_reason = check_prerequisites(flag, user, events, event_factory)
69
- if !prereq_failure_reason.nil?
70
- return get_off_value(flag, prereq_failure_reason)
71
- end
94
+ prereq_failure_result = check_prerequisites(flag, user, state)
95
+ return prereq_failure_result if !prereq_failure_result.nil?
72
96
 
73
97
  # Check user target matches
74
98
  (flag[:targets] || []).each do |target|
75
99
  (target[:values] || []).each do |value|
76
100
  if value == user[:key]
77
- return get_variation(flag, target[:variation], EvaluationReason::target_match)
101
+ return EvaluatorHelpers.target_match_result(target, flag)
78
102
  end
79
103
  end
80
104
  end
@@ -83,22 +107,24 @@ module LaunchDarkly
83
107
  rules = flag[:rules] || []
84
108
  rules.each_index do |i|
85
109
  rule = rules[i]
86
- if rule_match_user(rule, user)
110
+ if rule_match_user(rule, user, state)
87
111
  reason = rule[:_reason] # try to use cached reason for this rule
88
112
  reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
89
- return get_value_for_variation_or_rollout(flag, rule, user, reason)
113
+ return get_value_for_variation_or_rollout(flag, rule, user, reason,
114
+ EvaluatorHelpers.rule_precomputed_results(rule))
90
115
  end
91
116
  end
92
117
 
93
118
  # Check the fallthrough rule
94
119
  if !flag[:fallthrough].nil?
95
- return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, EvaluationReason::fallthrough)
120
+ return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, EvaluationReason::fallthrough,
121
+ EvaluatorHelpers.fallthrough_precomputed_results(flag))
96
122
  end
97
123
 
98
124
  return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
99
125
  end
100
126
 
101
- def check_prerequisites(flag, user, events, event_factory)
127
+ def check_prerequisites(flag, user, state)
102
128
  (flag[:prerequisites] || []).each do |prerequisite|
103
129
  prereq_ok = true
104
130
  prereq_key = prerequisite[:key]
@@ -109,44 +135,44 @@ module LaunchDarkly
109
135
  prereq_ok = false
110
136
  else
111
137
  begin
112
- prereq_res = eval_internal(prereq_flag, user, events, event_factory)
138
+ prereq_res = eval_internal(prereq_flag, user, state)
113
139
  # Note that if the prerequisite flag is off, we don't consider it a match no matter what its
114
140
  # off variation was. But we still need to evaluate it in order to generate an event.
115
141
  if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
116
142
  prereq_ok = false
117
143
  end
118
- event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag)
119
- events.push(event)
144
+ 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)
120
147
  rescue => exn
121
148
  Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
122
149
  prereq_ok = false
123
150
  end
124
151
  end
125
152
  if !prereq_ok
126
- reason = prerequisite[:_reason] # try to use cached reason
127
- return reason.nil? ? EvaluationReason::prerequisite_failed(prereq_key) : reason
153
+ return EvaluatorHelpers.prerequisite_failed_result(prerequisite, flag)
128
154
  end
129
155
  end
130
156
  nil
131
157
  end
132
158
 
133
- def rule_match_user(rule, user)
159
+ def rule_match_user(rule, user, state)
134
160
  return false if !rule[:clauses]
135
161
 
136
162
  (rule[:clauses] || []).each do |clause|
137
- return false if !clause_match_user(clause, user)
163
+ return false if !clause_match_user(clause, user, state)
138
164
  end
139
165
 
140
166
  return true
141
167
  end
142
168
 
143
- def clause_match_user(clause, user)
169
+ def clause_match_user(clause, user, state)
144
170
  # In the case of a segment match operator, we check if the user is in any of the segments,
145
171
  # and possibly negate
146
172
  if clause[:op].to_sym == :segmentMatch
147
173
  result = (clause[:values] || []).any? { |v|
148
174
  segment = @get_segment.call(v)
149
- !segment.nil? && segment_match_user(segment, user)
175
+ !segment.nil? && segment_match_user(segment, user, state)
150
176
  }
151
177
  clause[:negate] ? !result : result
152
178
  else
@@ -168,11 +194,42 @@ module LaunchDarkly
168
194
  clause[:negate] ? !result : result
169
195
  end
170
196
 
171
- def segment_match_user(segment, user)
197
+ def segment_match_user(segment, user, state)
172
198
  return false unless user[:key]
199
+ segment[:unbounded] ? big_segment_match_user(segment, user, state) : simple_segment_match_user(segment, user, true)
200
+ end
173
201
 
174
- return true if segment[:included].include?(user[:key])
175
- return false if segment[:excluded].include?(user[:key])
202
+ def big_segment_match_user(segment, user, state)
203
+ if !segment[:generation]
204
+ # Big segment queries can only be done if the generation is known. If it's unset,
205
+ # that probably means the data store was populated by an older SDK that doesn't know
206
+ # about the generation property and therefore dropped it from the JSON data. We'll treat
207
+ # that as a "not configured" condition.
208
+ state.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
209
+ return false
210
+ end
211
+ if !state.big_segments_status
212
+ result = @get_big_segments_membership.nil? ? nil : @get_big_segments_membership.call(user[:key])
213
+ if result
214
+ state.big_segments_membership = result.membership
215
+ state.big_segments_status = result.status
216
+ else
217
+ state.big_segments_membership = nil
218
+ state.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
219
+ end
220
+ 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)
226
+ end
227
+
228
+ def simple_segment_match_user(segment, user, use_includes_and_excludes)
229
+ if use_includes_and_excludes
230
+ return true if segment[:included].include?(user[:key])
231
+ return false if segment[:excluded].include?(user[:key])
232
+ end
176
233
 
177
234
  (segment[:rules] || []).each do |r|
178
235
  return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
@@ -190,35 +247,32 @@ module LaunchDarkly
190
247
  return true if !rule[:weight]
191
248
 
192
249
  # All of the clauses are met. See if the user buckets in
193
- bucket = EvaluatorBucketing.bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt)
250
+ bucket = EvaluatorBucketing.bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt, nil)
194
251
  weight = rule[:weight].to_f / 100000.0
195
252
  return bucket < weight
196
253
  end
197
254
 
198
255
  private
199
-
200
- def get_variation(flag, index, reason)
201
- if index < 0 || index >= flag[:variations].length
202
- @logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
203
- return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
204
- end
205
- EvaluationDetail.new(flag[:variations][index], index, reason)
206
- end
207
-
208
- def get_off_value(flag, reason)
209
- if flag[:offVariation].nil? # off variation unspecified - return default value
210
- return EvaluationDetail.new(nil, nil, reason)
211
- end
212
- get_variation(flag, flag[:offVariation], reason)
213
- end
214
-
215
- def get_value_for_variation_or_rollout(flag, vr, user, reason)
216
- index = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
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)
217
259
  if index.nil?
218
260
  @logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
219
261
  return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
220
262
  end
221
- return get_variation(flag, index, reason)
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)
272
+ end
273
+ end
274
+ return EvaluatorHelpers.evaluation_detail_for_variation(flag, index, reason)
275
+ end
222
276
  end
223
277
  end
224
278
  end
@@ -10,20 +10,26 @@ module LaunchDarkly
10
10
  # @param user [Object] the user properties
11
11
  # @return [Number] the variation index, or nil if there is an error
12
12
  def self.variation_index_for_user(flag, rule, user)
13
+
13
14
  variation = rule[:variation]
14
- return variation if !variation.nil? # fixed variation
15
+ return variation, false if !variation.nil? # fixed variation
15
16
  rollout = rule[:rollout]
16
- return nil if rollout.nil?
17
+ return nil, false if rollout.nil?
17
18
  variations = rollout[:variations]
18
19
  if !variations.nil? && variations.length > 0 # percentage rollout
19
- rollout = rule[:rollout]
20
20
  bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
21
- bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
21
+
22
+ seed = rollout[:seed]
23
+ bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt], seed) # may not be present
22
24
  sum = 0;
23
25
  variations.each do |variate|
26
+ if rollout[:kind] == "experiment" && !variate[:untracked]
27
+ in_experiment = true
28
+ end
29
+
24
30
  sum += variate[:weight].to_f / 100000.0
25
31
  if bucket < sum
26
- return variate[:variation]
32
+ return variate[:variation], !!in_experiment
27
33
  end
28
34
  end
29
35
  # The user's bucket value was greater than or equal to the end of the last bucket. This could happen due
@@ -31,9 +37,12 @@ module LaunchDarkly
31
37
  # data could contain buckets that don't actually add up to 100000. Rather than returning an error in
32
38
  # this case (or changing the scaling, which would potentially change the results for *all* users), we
33
39
  # will simply put the user in the last bucket.
34
- variations[-1][:variation]
40
+ last_variation = variations[-1]
41
+ in_experiment = rollout[:kind] == "experiment" && !last_variation[:untracked]
42
+
43
+ [last_variation[:variation], in_experiment]
35
44
  else # the rule isn't well-formed
36
- nil
45
+ [nil, false]
37
46
  end
38
47
  end
39
48
 
@@ -44,7 +53,7 @@ module LaunchDarkly
44
53
  # @param bucket_by [String|Symbol] the name of the user attribute to be used for bucketing
45
54
  # @param salt [String] the feature flag's or segment's salt value
46
55
  # @return [Number] the bucket value, from 0 inclusive to 1 exclusive
47
- def self.bucket_user(user, key, bucket_by, salt)
56
+ def self.bucket_user(user, key, bucket_by, salt, seed)
48
57
  return nil unless user[:key]
49
58
 
50
59
  id_hash = bucketable_string_value(EvaluatorOperators.user_value(user, bucket_by))
@@ -56,7 +65,11 @@ module LaunchDarkly
56
65
  id_hash += "." + user[:secondary].to_s
57
66
  end
58
67
 
59
- hash_key = "%s.%s.%s" % [key, salt, id_hash]
68
+ if seed
69
+ hash_key = "%d.%s" % [seed, id_hash]
70
+ else
71
+ hash_key = "%s.%s.%s" % [key, salt, id_hash]
72
+ end
60
73
 
61
74
  hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
62
75
  hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
@@ -0,0 +1,53 @@
1
+ require "ldclient-rb/evaluation_detail"
2
+
3
+ # This file contains any pieces of low-level evaluation logic that don't need to be inside the Evaluator
4
+ # class, because they don't depend on any SDK state outside of their input parameters.
5
+
6
+ module LaunchDarkly
7
+ module Impl
8
+ module EvaluatorHelpers
9
+ def self.off_result(flag, logger = nil)
10
+ pre = flag[:_preprocessed]
11
+ pre ? pre.off_result : evaluation_detail_for_off_variation(flag, EvaluationReason::off, logger)
12
+ end
13
+
14
+ def self.target_match_result(target, flag, logger = nil)
15
+ pre = target[:_preprocessed]
16
+ pre ? pre.match_result : evaluation_detail_for_variation(
17
+ flag, target[:variation], EvaluationReason::target_match, logger)
18
+ end
19
+
20
+ def self.prerequisite_failed_result(prereq, flag, logger = nil)
21
+ pre = prereq[:_preprocessed]
22
+ pre ? pre.failed_result : evaluation_detail_for_off_variation(
23
+ flag, EvaluationReason::prerequisite_failed(prereq[:key]), logger
24
+ )
25
+ end
26
+
27
+ def self.fallthrough_precomputed_results(flag)
28
+ pre = flag[:_preprocessed]
29
+ pre ? pre.fallthrough_factory : nil
30
+ end
31
+
32
+ def self.rule_precomputed_results(rule)
33
+ pre = rule[:_preprocessed]
34
+ pre ? pre.all_match_results : nil
35
+ end
36
+
37
+ def self.evaluation_detail_for_off_variation(flag, reason, logger = nil)
38
+ index = flag[:offVariation]
39
+ index.nil? ? EvaluationDetail.new(nil, nil, reason) : evaluation_detail_for_variation(flag, index, reason, logger)
40
+ end
41
+
42
+ def self.evaluation_detail_for_variation(flag, index, reason, logger = nil)
43
+ vars = flag[:variations] || []
44
+ if index < 0 || index >= vars.length
45
+ logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index") unless logger.nil?
46
+ EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
47
+ else
48
+ EvaluationDetail.new(vars[index], index, reason)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -89,7 +89,7 @@ module LaunchDarkly
89
89
 
90
90
  private
91
91
 
92
- BUILTINS = Set[:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
92
+ BUILTINS = Set[:key, :secondary, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
93
93
  NUMERIC_VERSION_COMPONENTS_REGEX = Regexp.new("^[0-9.]*")
94
94
 
95
95
  private_constant :BUILTINS
@@ -0,0 +1,63 @@
1
+ require "ldclient-rb/impl/event_types"
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+ EventSummary = Struct.new(:start_date, :end_date, :counters)
6
+
7
+ EventSummaryFlagInfo = Struct.new(:default, :versions)
8
+
9
+ EventSummaryFlagVariationCounter = Struct.new(:value, :count)
10
+
11
+ # Manages the state of summarizable information for the EventProcessor, including the
12
+ # event counters and user deduplication. Note that the methods of this class are
13
+ # deliberately not thread-safe; the EventProcessor is responsible for enforcing
14
+ # synchronization across both the summarizer and the event queue.
15
+ class EventSummarizer
16
+ class Counter
17
+ end
18
+
19
+ def initialize
20
+ clear
21
+ end
22
+
23
+ # Adds this event to our counters, if it is a type of event we need to count.
24
+ def summarize_event(event)
25
+ return if !event.is_a?(LaunchDarkly::Impl::EvalEvent)
26
+
27
+ counters_for_flag = @counters[event.key]
28
+ if counters_for_flag.nil?
29
+ counters_for_flag = EventSummaryFlagInfo.new(event.default, Hash.new)
30
+ @counters[event.key] = counters_for_flag
31
+ end
32
+ counters_for_flag_version = counters_for_flag.versions[event.version]
33
+ if counters_for_flag_version.nil?
34
+ counters_for_flag_version = Hash.new
35
+ counters_for_flag.versions[event.version] = counters_for_flag_version
36
+ end
37
+ variation_counter = counters_for_flag_version[event.variation]
38
+ if variation_counter.nil?
39
+ counters_for_flag_version[event.variation] = EventSummaryFlagVariationCounter.new(event.value, 1)
40
+ else
41
+ variation_counter.count = variation_counter.count + 1
42
+ end
43
+ time = event.timestamp
44
+ if !time.nil?
45
+ @start_date = time if @start_date == 0 || time < @start_date
46
+ @end_date = time if time > @end_date
47
+ end
48
+ end
49
+
50
+ # Returns a snapshot of the current summarized event data, and resets this state.
51
+ def snapshot
52
+ ret = EventSummary.new(@start_date, @end_date, @counters)
53
+ ret
54
+ end
55
+
56
+ def clear
57
+ @start_date = 0
58
+ @end_date = 0
59
+ @counters = {}
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,90 @@
1
+ module LaunchDarkly
2
+ module Impl
3
+ class Event
4
+ def initialize(timestamp, user)
5
+ @timestamp = timestamp
6
+ @user = user
7
+ end
8
+
9
+ attr_reader :timestamp
10
+ attr_reader :kind
11
+ attr_reader :user
12
+ end
13
+
14
+ class EvalEvent < Event
15
+ def initialize(timestamp, user, key, version = nil, variation = nil, value = nil, reason = nil, default = nil,
16
+ track_events = false, debug_until = nil, prereq_of = nil)
17
+ super(timestamp, user)
18
+ @key = key
19
+ @version = version
20
+ @variation = variation
21
+ @value = value
22
+ @reason = reason
23
+ @default = default
24
+ # avoid setting rarely-used attributes if they have no value - this saves a little space per instance
25
+ @track_events = track_events if track_events
26
+ @debug_until = debug_until if debug_until
27
+ @prereq_of = prereq_of if prereq_of
28
+ end
29
+
30
+ attr_reader :key
31
+ attr_reader :version
32
+ attr_reader :variation
33
+ attr_reader :value
34
+ attr_reader :reason
35
+ attr_reader :default
36
+ attr_reader :track_events
37
+ attr_reader :debug_until
38
+ attr_reader :prereq_of
39
+ end
40
+
41
+ class IdentifyEvent < Event
42
+ def initialize(timestamp, user)
43
+ super(timestamp, user)
44
+ end
45
+ end
46
+
47
+ class CustomEvent < Event
48
+ def initialize(timestamp, user, key, data = nil, metric_value = nil)
49
+ super(timestamp, user)
50
+ @key = key
51
+ @data = data if !data.nil?
52
+ @metric_value = metric_value if !metric_value.nil?
53
+ end
54
+
55
+ attr_reader :key
56
+ attr_reader :data
57
+ attr_reader :metric_value
58
+ end
59
+
60
+ class AliasEvent < Event
61
+ def initialize(timestamp, key, context_kind, previous_key, previous_context_kind)
62
+ super(timestamp, nil)
63
+ @key = key
64
+ @context_kind = context_kind
65
+ @previous_key = previous_key
66
+ @previous_context_kind = previous_context_kind
67
+ end
68
+
69
+ attr_reader :key
70
+ attr_reader :context_kind
71
+ attr_reader :previous_key
72
+ attr_reader :previous_context_kind
73
+ end
74
+
75
+ class IndexEvent < Event
76
+ def initialize(timestamp, user)
77
+ super(timestamp, user)
78
+ end
79
+ end
80
+
81
+ class DebugEvent < Event
82
+ def initialize(eval_event)
83
+ super(eval_event.timestamp, eval_event.user)
84
+ @eval_event = eval_event
85
+ end
86
+
87
+ attr_reader :eval_event
88
+ end
89
+ end
90
+ end