launchdarkly-server-sdk 5.8.2 → 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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +28 -122
  3. data/.ldrelease/circleci/linux/execute.sh +18 -0
  4. data/.ldrelease/circleci/mac/execute.sh +18 -0
  5. data/.ldrelease/circleci/template/build.sh +29 -0
  6. data/.ldrelease/circleci/template/publish.sh +23 -0
  7. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  8. data/.ldrelease/circleci/template/test.sh +10 -0
  9. data/.ldrelease/circleci/template/update-version.sh +8 -0
  10. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  11. data/.ldrelease/config.yml +7 -3
  12. data/CHANGELOG.md +9 -0
  13. data/CONTRIBUTING.md +1 -1
  14. data/Gemfile.lock +69 -42
  15. data/README.md +2 -2
  16. data/azure-pipelines.yml +1 -1
  17. data/launchdarkly-server-sdk.gemspec +16 -16
  18. data/lib/ldclient-rb.rb +0 -1
  19. data/lib/ldclient-rb/config.rb +15 -3
  20. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  21. data/lib/ldclient-rb/events.rb +1 -4
  22. data/lib/ldclient-rb/file_data_source.rb +1 -1
  23. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  24. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  25. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  26. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  27. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  28. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  29. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -9
  30. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  31. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  32. data/lib/ldclient-rb/ldclient.rb +14 -9
  33. data/lib/ldclient-rb/polling.rb +1 -4
  34. data/lib/ldclient-rb/requestor.rb +25 -15
  35. data/lib/ldclient-rb/stream.rb +9 -6
  36. data/lib/ldclient-rb/util.rb +12 -8
  37. data/lib/ldclient-rb/version.rb +1 -1
  38. data/spec/evaluation_detail_spec.rb +135 -0
  39. data/spec/event_sender_spec.rb +20 -2
  40. data/spec/http_util.rb +11 -1
  41. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  42. data/spec/impl/evaluator_clause_spec.rb +55 -0
  43. data/spec/impl/evaluator_operators_spec.rb +141 -0
  44. data/spec/impl/evaluator_rule_spec.rb +96 -0
  45. data/spec/impl/evaluator_segment_spec.rb +125 -0
  46. data/spec/impl/evaluator_spec.rb +305 -0
  47. data/spec/impl/evaluator_spec_base.rb +75 -0
  48. data/spec/impl/model/serialization_spec.rb +41 -0
  49. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  50. data/spec/ldclient_end_to_end_spec.rb +34 -0
  51. data/spec/ldclient_spec.rb +10 -8
  52. data/spec/polling_spec.rb +2 -2
  53. data/spec/redis_feature_store_spec.rb +2 -2
  54. data/spec/requestor_spec.rb +11 -11
  55. metadata +89 -46
  56. data/lib/ldclient-rb/evaluation.rb +0 -462
  57. data/spec/evaluation_spec.rb +0 -789
data/README.md CHANGED
@@ -17,7 +17,7 @@ LaunchDarkly overview
17
17
  Supported Ruby versions
18
18
  -----------------------
19
19
 
20
- This version of the LaunchDarkly SDK has a minimum Ruby version of 2.2.6, or 9.1.6 for JRuby.
20
+ This version of the LaunchDarkly SDK has a minimum Ruby version of 2.5.0, or 9.2.0 for JRuby.
21
21
 
22
22
  Getting started
23
23
  -----------
@@ -55,4 +55,4 @@ About LaunchDarkly
55
55
  * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides
56
56
  * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation
57
57
  * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates
58
- * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies
58
+ * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies
@@ -45,7 +45,7 @@ jobs:
45
45
  workingDirectory: $(System.DefaultWorkingDirectory)
46
46
  script: |
47
47
  ruby -v
48
- gem install bundler -v 1.17.3
48
+ gem install bundler
49
49
  bundle install
50
50
  mkdir rspec
51
51
  bundle exec rspec --format progress --format RspecJunitFormatter -o ./rspec/rspec.xml spec
@@ -19,27 +19,27 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
20
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
21
  spec.require_paths = ["lib"]
22
+ spec.required_ruby_version = ">= 2.5.0"
22
23
 
23
- spec.add_development_dependency "aws-sdk-dynamodb", "~> 1.18"
24
- spec.add_development_dependency "bundler", "~> 1.17"
25
- spec.add_development_dependency "rspec", "~> 3.2"
26
- spec.add_development_dependency "diplomat", ">= 2.0.2"
27
- spec.add_development_dependency "redis", "~> 3.3.5"
28
- spec.add_development_dependency "connection_pool", ">= 2.1.2"
29
- spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
30
- spec.add_development_dependency "timecop", "~> 0.9.1"
31
- spec.add_development_dependency "listen", "~> 3.0" # see file_data_source.rb
32
- # these are transitive dependencies of listen and consul respectively
33
- # we constrain them here to make sure the ruby 2.2, 2.3, and 2.4 CI
34
- # cases all pass
35
- spec.add_development_dependency "ffi", "<= 1.12" # >1.12 doesnt support ruby 2.2
36
- spec.add_development_dependency "faraday", "~> 0.17" # >=0.18 doesnt support ruby 2.2
24
+ spec.add_development_dependency "aws-sdk-dynamodb", "~> 1.57"
25
+ spec.add_development_dependency "bundler", "~> 2.1"
26
+ spec.add_development_dependency "rspec", "~> 3.10"
27
+ spec.add_development_dependency "diplomat", "~> 2.4.2"
28
+ spec.add_development_dependency "redis", "~> 4.2"
29
+ spec.add_development_dependency "connection_pool", "~> 2.2.3"
30
+ spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
31
+ spec.add_development_dependency "timecop", "~> 0.9"
32
+ spec.add_development_dependency "listen", "~> 3.3" # see file_data_source.rb
33
+ spec.add_development_dependency "webrick", "~> 1.7"
34
+ # required by dynamodb
35
+ spec.add_development_dependency "oga", "~> 2.2"
37
36
 
38
37
  spec.add_runtime_dependency "semantic", "~> 1.6"
39
- spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
40
- spec.add_runtime_dependency "ld-eventsource", "1.0.3"
38
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.1"
39
+ spec.add_runtime_dependency "ld-eventsource", "~> 2.0"
41
40
 
42
41
  # lock json to 2.3.x as ruby libraries often remove
43
42
  # support for older ruby versions in minor releases
44
43
  spec.add_runtime_dependency "json", "~> 2.3.1"
44
+ spec.add_runtime_dependency "http", "~> 4.4.1"
45
45
  end
@@ -8,7 +8,6 @@ end
8
8
  require "ldclient-rb/version"
9
9
  require "ldclient-rb/interfaces"
10
10
  require "ldclient-rb/util"
11
- require "ldclient-rb/evaluation"
12
11
  require "ldclient-rb/flags_state"
13
12
  require "ldclient-rb/ldclient"
14
13
  require "ldclient-rb/cache_store"
@@ -15,7 +15,7 @@ module LaunchDarkly
15
15
  #
16
16
  # @param opts [Hash] the configuration options
17
17
  # @option opts [Logger] :logger See {#logger}.
18
- # @option opts [String] :base_uri ("https://app.launchdarkly.com") See {#base_uri}.
18
+ # @option opts [String] :base_uri ("https://sdk.launchdarkly.com") See {#base_uri}.
19
19
  # @option opts [String] :stream_uri ("https://stream.launchdarkly.com") See {#stream_uri}.
20
20
  # @option opts [String] :events_uri ("https://events.launchdarkly.com") See {#events_uri}.
21
21
  # @option opts [Integer] :capacity (10000) See {#capacity}.
@@ -41,6 +41,7 @@ module LaunchDarkly
41
41
  # @option opts [Float] :diagnostic_recording_interval (900) See {#diagnostic_recording_interval}.
42
42
  # @option opts [String] :wrapper_name See {#wrapper_name}.
43
43
  # @option opts [String] :wrapper_version See {#wrapper_version}.
44
+ # @option opts [#open] :socket_factory See {#socket_factory}.
44
45
  #
45
46
  def initialize(opts = {})
46
47
  @base_uri = (opts[:base_uri] || Config.default_base_uri).chomp("/")
@@ -71,6 +72,7 @@ module LaunchDarkly
71
72
  opts[:diagnostic_recording_interval] : Config.default_diagnostic_recording_interval
72
73
  @wrapper_name = opts[:wrapper_name]
73
74
  @wrapper_version = opts[:wrapper_version]
75
+ @socket_factory = opts[:socket_factory]
74
76
  end
75
77
 
76
78
  #
@@ -305,6 +307,16 @@ module LaunchDarkly
305
307
  #
306
308
  attr_reader :wrapper_version
307
309
 
310
+ #
311
+ # The factory used to construct sockets for HTTP operations. The factory must
312
+ # provide the method `open(uri, timeout)`. The `open` method must return a
313
+ # connected stream that implements the `IO` class, such as a `TCPSocket`.
314
+ #
315
+ # Defaults to nil.
316
+ # @return [#open]
317
+ #
318
+ attr_reader :socket_factory
319
+
308
320
  #
309
321
  # The default LaunchDarkly client configuration. This configuration sets
310
322
  # reasonable defaults for most users.
@@ -324,10 +336,10 @@ module LaunchDarkly
324
336
 
325
337
  #
326
338
  # The default value for {#base_uri}.
327
- # @return [String] "https://app.launchdarkly.com"
339
+ # @return [String] "https://sdk.launchdarkly.com"
328
340
  #
329
341
  def self.default_base_uri
330
- "https://app.launchdarkly.com"
342
+ "https://sdk.launchdarkly.com"
331
343
  end
332
344
 
333
345
  #
@@ -0,0 +1,293 @@
1
+
2
+ module LaunchDarkly
3
+ # An object returned by {LDClient#variation_detail}, combining the result of a flag evaluation with
4
+ # an explanation of how it was calculated.
5
+ class EvaluationDetail
6
+ # Creates a new instance.
7
+ #
8
+ # @param value the result value of the flag evaluation; may be of any type
9
+ # @param variation_index [int|nil] the index of the value within the flag's list of variations, or
10
+ # `nil` if the application default value was returned
11
+ # @param reason [EvaluationReason] an object describing the main factor that influenced the result
12
+ # @raise [ArgumentError] if `variation_index` or `reason` is not of the correct type
13
+ def initialize(value, variation_index, reason)
14
+ raise ArgumentError.new("variation_index must be a number") if !variation_index.nil? && !(variation_index.is_a? Numeric)
15
+ raise ArgumentError.new("reason must be an EvaluationReason") if !(reason.is_a? EvaluationReason)
16
+ @value = value
17
+ @variation_index = variation_index
18
+ @reason = reason
19
+ end
20
+
21
+ #
22
+ # The result of the flag evaluation. This will be either one of the flag's variations, or the
23
+ # default value that was passed to {LDClient#variation_detail}. It is the same as the return
24
+ # value of {LDClient#variation}.
25
+ #
26
+ # @return [Object]
27
+ #
28
+ attr_reader :value
29
+
30
+ #
31
+ # The index of the returned value within the flag's list of variations. The first variation is
32
+ # 0, the second is 1, etc. This is `nil` if the default value was returned.
33
+ #
34
+ # @return [int|nil]
35
+ #
36
+ attr_reader :variation_index
37
+
38
+ #
39
+ # An object describing the main factor that influenced the flag evaluation value.
40
+ #
41
+ # @return [EvaluationReason]
42
+ #
43
+ attr_reader :reason
44
+
45
+ #
46
+ # Tests whether the flag evaluation returned a default value. This is the same as checking
47
+ # whether {#variation_index} is nil.
48
+ #
49
+ # @return [Boolean]
50
+ #
51
+ def default_value?
52
+ variation_index.nil?
53
+ end
54
+
55
+ def ==(other)
56
+ @value == other.value && @variation_index == other.variation_index && @reason == other.reason
57
+ end
58
+ end
59
+
60
+ # Describes the reason that a flag evaluation produced a particular value. This is returned by
61
+ # methods such as {LDClient#variation_detail} as the `reason` property of an {EvaluationDetail}.
62
+ #
63
+ # The `kind` property is always defined, but other properties will have non-nil values only for
64
+ # certain values of `kind`. All properties are immutable.
65
+ #
66
+ # There is a standard JSON representation of evaluation reasons when they appear in analytics events.
67
+ # Use `as_json` or `to_json` to convert to this representation.
68
+ #
69
+ # Use factory methods such as {EvaluationReason#off} to obtain instances of this class.
70
+ class EvaluationReason
71
+ # Value for {#kind} indicating that the flag was off and therefore returned its configured off value.
72
+ OFF = :OFF
73
+
74
+ # Value for {#kind} indicating that the flag was on but the user did not match any targets or rules.
75
+ FALLTHROUGH = :FALLTHROUGH
76
+
77
+ # Value for {#kind} indicating that the user key was specifically targeted for this flag.
78
+ TARGET_MATCH = :TARGET_MATCH
79
+
80
+ # Value for {#kind} indicating that the user matched one of the flag's rules.
81
+ RULE_MATCH = :RULE_MATCH
82
+
83
+ # Value for {#kind} indicating that the flag was considered off because it had at least one
84
+ # prerequisite flag that either was off or did not return the desired variation.
85
+ PREREQUISITE_FAILED = :PREREQUISITE_FAILED
86
+
87
+ # Value for {#kind} indicating that the flag could not be evaluated, e.g. because it does not exist
88
+ # or due to an unexpected error. In this case the result value will be the application default value
89
+ # that the caller passed to the client. Check {#error_kind} for more details on the problem.
90
+ ERROR = :ERROR
91
+
92
+ # Value for {#error_kind} indicating that the caller tried to evaluate a flag before the client had
93
+ # successfully initialized.
94
+ ERROR_CLIENT_NOT_READY = :CLIENT_NOT_READY
95
+
96
+ # Value for {#error_kind} indicating that the caller provided a flag key that did not match any known flag.
97
+ ERROR_FLAG_NOT_FOUND = :FLAG_NOT_FOUND
98
+
99
+ # Value for {#error_kind} indicating that there was an internal inconsistency in the flag data, e.g.
100
+ # a rule specified a nonexistent variation. An error message will always be logged in this case.
101
+ ERROR_MALFORMED_FLAG = :MALFORMED_FLAG
102
+
103
+ # Value for {#error_kind} indicating that the caller passed `nil` for the user parameter, or the
104
+ # user lacked a key.
105
+ ERROR_USER_NOT_SPECIFIED = :USER_NOT_SPECIFIED
106
+
107
+ # Value for {#error_kind} indicating that an unexpected exception stopped flag evaluation. An error
108
+ # message will always be logged in this case.
109
+ ERROR_EXCEPTION = :EXCEPTION
110
+
111
+ # Indicates the general category of the reason. Will always be one of the class constants such
112
+ # as {#OFF}.
113
+ attr_reader :kind
114
+
115
+ # The index of the rule that was matched (0 for the first rule in the feature flag). If
116
+ # {#kind} is not {#RULE_MATCH}, this will be `nil`.
117
+ attr_reader :rule_index
118
+
119
+ # A unique string identifier for the matched rule, which will not change if other rules are added
120
+ # or deleted. If {#kind} is not {#RULE_MATCH}, this will be `nil`.
121
+ attr_reader :rule_id
122
+
123
+ # The key of the prerequisite flag that did not return the desired variation. If {#kind} is not
124
+ # {#PREREQUISITE_FAILED}, this will be `nil`.
125
+ attr_reader :prerequisite_key
126
+
127
+ # A value indicating the general category of error. This should be one of the class constants such
128
+ # as {#ERROR_FLAG_NOT_FOUND}. If {#kind} is not {#ERROR}, it will be `nil`.
129
+ attr_reader :error_kind
130
+
131
+ # Returns an instance whose {#kind} is {#OFF}.
132
+ # @return [EvaluationReason]
133
+ def self.off
134
+ @@off
135
+ end
136
+
137
+ # Returns an instance whose {#kind} is {#FALLTHROUGH}.
138
+ # @return [EvaluationReason]
139
+ def self.fallthrough
140
+ @@fallthrough
141
+ end
142
+
143
+ # Returns an instance whose {#kind} is {#TARGET_MATCH}.
144
+ # @return [EvaluationReason]
145
+ def self.target_match
146
+ @@target_match
147
+ end
148
+
149
+ # Returns an instance whose {#kind} is {#RULE_MATCH}.
150
+ #
151
+ # @param rule_index [Number] the index of the rule that was matched (0 for the first rule in
152
+ # the feature flag)
153
+ # @param rule_id [String] unique string identifier for the matched rule
154
+ # @return [EvaluationReason]
155
+ # @raise [ArgumentError] if `rule_index` is not a number or `rule_id` is not a string
156
+ def self.rule_match(rule_index, rule_id)
157
+ raise ArgumentError.new("rule_index must be a number") if !(rule_index.is_a? Numeric)
158
+ raise ArgumentError.new("rule_id must be a string") if !rule_id.nil? && !(rule_id.is_a? String) # in test data, ID could be nil
159
+ new(:RULE_MATCH, rule_index, rule_id, nil, nil)
160
+ end
161
+
162
+ # Returns an instance whose {#kind} is {#PREREQUISITE_FAILED}.
163
+ #
164
+ # @param prerequisite_key [String] key of the prerequisite flag that did not return the desired variation
165
+ # @return [EvaluationReason]
166
+ # @raise [ArgumentError] if `prerequisite_key` is nil or not a string
167
+ def self.prerequisite_failed(prerequisite_key)
168
+ raise ArgumentError.new("prerequisite_key must be a string") if !(prerequisite_key.is_a? String)
169
+ new(:PREREQUISITE_FAILED, nil, nil, prerequisite_key, nil)
170
+ end
171
+
172
+ # Returns an instance whose {#kind} is {#ERROR}.
173
+ #
174
+ # @param error_kind [Symbol] value indicating the general category of error
175
+ # @return [EvaluationReason]
176
+ # @raise [ArgumentError] if `error_kind` is not a symbol
177
+ def self.error(error_kind)
178
+ raise ArgumentError.new("error_kind must be a symbol") if !(error_kind.is_a? Symbol)
179
+ e = @@error_instances[error_kind]
180
+ e.nil? ? make_error(error_kind) : e
181
+ end
182
+
183
+ def ==(other)
184
+ if other.is_a? EvaluationReason
185
+ @kind == other.kind && @rule_index == other.rule_index && @rule_id == other.rule_id &&
186
+ @prerequisite_key == other.prerequisite_key && @error_kind == other.error_kind
187
+ elsif other.is_a? Hash
188
+ @kind.to_s == other[:kind] && @rule_index == other[:ruleIndex] && @rule_id == other[:ruleId] &&
189
+ @prerequisite_key == other[:prerequisiteKey] &&
190
+ (other[:errorKind] == @error_kind.nil? ? nil : @error_kind.to_s)
191
+ end
192
+ end
193
+
194
+ # Equivalent to {#inspect}.
195
+ # @return [String]
196
+ def to_s
197
+ inspect
198
+ end
199
+
200
+ # Returns a concise string representation of the reason. Examples: `"FALLTHROUGH"`,
201
+ # `"ERROR(FLAG_NOT_FOUND)"`. The exact syntax is not guaranteed to remain the same; this is meant
202
+ # for debugging.
203
+ # @return [String]
204
+ def inspect
205
+ case @kind
206
+ when :RULE_MATCH
207
+ "RULE_MATCH(#{@rule_index},#{@rule_id})"
208
+ when :PREREQUISITE_FAILED
209
+ "PREREQUISITE_FAILED(#{@prerequisite_key})"
210
+ when :ERROR
211
+ "ERROR(#{@error_kind})"
212
+ else
213
+ @kind.to_s
214
+ end
215
+ end
216
+
217
+ # Returns a hash that can be used as a JSON representation of the reason, in the format used
218
+ # in LaunchDarkly analytics events.
219
+ # @return [Hash]
220
+ def as_json(*) # parameter is unused, but may be passed if we're using the json gem
221
+ # Note that this implementation is somewhat inefficient; it allocates a new hash every time.
222
+ # However, in normal usage the SDK only serializes reasons if 1. full event tracking is
223
+ # enabled for a flag and the application called variation_detail, or 2. experimentation is
224
+ # enabled for an evaluation. We can't reuse these hashes because an application could call
225
+ # as_json and then modify the result.
226
+ case @kind
227
+ when :RULE_MATCH
228
+ { kind: @kind, ruleIndex: @rule_index, ruleId: @rule_id }
229
+ when :PREREQUISITE_FAILED
230
+ { kind: @kind, prerequisiteKey: @prerequisite_key }
231
+ when :ERROR
232
+ { kind: @kind, errorKind: @error_kind }
233
+ else
234
+ { kind: @kind }
235
+ end
236
+ end
237
+
238
+ # Same as {#as_json}, but converts the JSON structure into a string.
239
+ # @return [String]
240
+ def to_json(*a)
241
+ as_json.to_json(a)
242
+ end
243
+
244
+ # Allows this object to be treated as a hash corresponding to its JSON representation. For
245
+ # instance, if `reason.kind` is {#RULE_MATCH}, then `reason[:kind]` will be `"RULE_MATCH"` and
246
+ # `reason[:ruleIndex]` will be equal to `reason.rule_index`.
247
+ def [](key)
248
+ case key
249
+ when :kind
250
+ @kind.to_s
251
+ when :ruleIndex
252
+ @rule_index
253
+ when :ruleId
254
+ @rule_id
255
+ when :prerequisiteKey
256
+ @prerequisite_key
257
+ when :errorKind
258
+ @error_kind.nil? ? nil : @error_kind.to_s
259
+ else
260
+ nil
261
+ end
262
+ end
263
+
264
+ private
265
+
266
+ def initialize(kind, rule_index, rule_id, prerequisite_key, error_kind)
267
+ @kind = kind.to_sym
268
+ @rule_index = rule_index
269
+ @rule_id = rule_id
270
+ @rule_id.freeze if !rule_id.nil?
271
+ @prerequisite_key = prerequisite_key
272
+ @prerequisite_key.freeze if !prerequisite_key.nil?
273
+ @error_kind = error_kind
274
+ end
275
+
276
+ private_class_method :new
277
+
278
+ def self.make_error(error_kind)
279
+ new(:ERROR, nil, nil, nil, error_kind)
280
+ end
281
+
282
+ @@fallthrough = new(:FALLTHROUGH, nil, nil, nil, nil)
283
+ @@off = new(:OFF, nil, nil, nil, nil)
284
+ @@target_match = new(:TARGET_MATCH, nil, nil, nil, nil)
285
+ @@error_instances = {
286
+ ERROR_CLIENT_NOT_READY => make_error(ERROR_CLIENT_NOT_READY),
287
+ ERROR_FLAG_NOT_FOUND => make_error(ERROR_FLAG_NOT_FOUND),
288
+ ERROR_MALFORMED_FLAG => make_error(ERROR_MALFORMED_FLAG),
289
+ ERROR_USER_NOT_SPECIFIED => make_error(ERROR_USER_NOT_SPECIFIED),
290
+ ERROR_EXCEPTION => make_error(ERROR_EXCEPTION)
291
+ }
292
+ end
293
+ end
@@ -238,10 +238,7 @@ module LaunchDarkly
238
238
  diagnostic_event_workers.shutdown
239
239
  diagnostic_event_workers.wait_for_termination
240
240
  end
241
- begin
242
- @client.finish
243
- rescue
244
- end
241
+ @event_sender.stop if @event_sender.respond_to?(:stop)
245
242
  end
246
243
 
247
244
  def synchronize_for_testing(flush_workers, diagnostic_event_workers)
@@ -51,7 +51,7 @@ module LaunchDarkly
51
51
  # output as the starting point for your file. In Linux you would do this:
52
52
  #
53
53
  # ```
54
- # curl -H "Authorization: YOUR_SDK_KEY" https://app.launchdarkly.com/sdk/latest-all
54
+ # curl -H "Authorization: YOUR_SDK_KEY" https://sdk.launchdarkly.com/sdk/latest-all
55
55
  # ```
56
56
  #
57
57
  # The output will look something like this (but with many more properties):
@@ -0,0 +1,225 @@
1
+ require "ldclient-rb/evaluation_detail"
2
+ require "ldclient-rb/impl/evaluator_bucketing"
3
+ require "ldclient-rb/impl/evaluator_operators"
4
+
5
+ module LaunchDarkly
6
+ module Impl
7
+ # Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
8
+ # if it needs to retrieve flags or segments that are referenced by a flag, it does so through a simple function that
9
+ # is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite
10
+ # flags, but does not send them.
11
+ class Evaluator
12
+ # A single Evaluator is instantiated for each client instance.
13
+ #
14
+ # @param get_flag [Function] called if the Evaluator needs to query a different flag from the one that it is
15
+ # currently evaluating (i.e. a prerequisite flag); takes a single parameter, the flag key, and returns the
16
+ # flag data - or nil if the flag is unknown or deleted
17
+ # @param get_segment [Function] similar to `get_flag`, but is used to query a user segment.
18
+ # @param logger [Logger] the client's logger
19
+ def initialize(get_flag, get_segment, logger)
20
+ @get_flag = get_flag
21
+ @get_segment = get_segment
22
+ @logger = logger
23
+ 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)
29
+
30
+ # Helper function used internally to construct an EvaluationDetail for an error result.
31
+ def self.error_result(errorKind, value = nil)
32
+ EvaluationDetail.new(value, nil, EvaluationReason.error(errorKind))
33
+ end
34
+
35
+ # The client's entry point for evaluating a flag. The returned `EvalResult` contains the evaluation result and
36
+ # any events that were generated for prerequisite flags; its `value` will be `nil` if the flag returns the
37
+ # default value. Error conditions produce a result with a nil value and an error reason, not an exception.
38
+ #
39
+ # @param flag [Object] the flag
40
+ # @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
+ # @return [EvalResult] the evaluation result
44
+ def evaluate(flag, user, event_factory)
45
+ if user.nil? || user[:key].nil?
46
+ return EvalResult.new(Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED), [])
47
+ 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
55
+ end
56
+
57
+ detail = eval_internal(flag, user, events, event_factory)
58
+ return EvalResult.new(detail, events.nil? || events.empty? ? nil : events)
59
+ end
60
+
61
+ private
62
+
63
+ def eval_internal(flag, user, events, event_factory)
64
+ if !flag[:on]
65
+ return get_off_value(flag, EvaluationReason::off)
66
+ end
67
+
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
72
+
73
+ # Check user target matches
74
+ (flag[:targets] || []).each do |target|
75
+ (target[:values] || []).each do |value|
76
+ if value == user[:key]
77
+ return get_variation(flag, target[:variation], EvaluationReason::target_match)
78
+ end
79
+ end
80
+ end
81
+
82
+ # Check custom rules
83
+ rules = flag[:rules] || []
84
+ rules.each_index do |i|
85
+ rule = rules[i]
86
+ if rule_match_user(rule, user)
87
+ reason = rule[:_reason] # try to use cached reason for this rule
88
+ reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
89
+ return get_value_for_variation_or_rollout(flag, rule, user, reason)
90
+ end
91
+ end
92
+
93
+ # Check the fallthrough rule
94
+ if !flag[:fallthrough].nil?
95
+ return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, EvaluationReason::fallthrough)
96
+ end
97
+
98
+ return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
99
+ end
100
+
101
+ def check_prerequisites(flag, user, events, event_factory)
102
+ (flag[:prerequisites] || []).each do |prerequisite|
103
+ prereq_ok = true
104
+ prereq_key = prerequisite[:key]
105
+ prereq_flag = @get_flag.call(prereq_key)
106
+
107
+ if prereq_flag.nil?
108
+ @logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
109
+ prereq_ok = false
110
+ else
111
+ begin
112
+ prereq_res = eval_internal(prereq_flag, user, events, event_factory)
113
+ # Note that if the prerequisite flag is off, we don't consider it a match no matter what its
114
+ # off variation was. But we still need to evaluate it in order to generate an event.
115
+ if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
116
+ prereq_ok = false
117
+ end
118
+ event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag)
119
+ events.push(event)
120
+ rescue => exn
121
+ Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
122
+ prereq_ok = false
123
+ end
124
+ end
125
+ if !prereq_ok
126
+ reason = prerequisite[:_reason] # try to use cached reason
127
+ return reason.nil? ? EvaluationReason::prerequisite_failed(prereq_key) : reason
128
+ end
129
+ end
130
+ nil
131
+ end
132
+
133
+ def rule_match_user(rule, user)
134
+ return false if !rule[:clauses]
135
+
136
+ (rule[:clauses] || []).each do |clause|
137
+ return false if !clause_match_user(clause, user)
138
+ end
139
+
140
+ return true
141
+ end
142
+
143
+ def clause_match_user(clause, user)
144
+ # In the case of a segment match operator, we check if the user is in any of the segments,
145
+ # and possibly negate
146
+ if clause[:op].to_sym == :segmentMatch
147
+ result = (clause[:values] || []).any? { |v|
148
+ segment = @get_segment.call(v)
149
+ !segment.nil? && segment_match_user(segment, user)
150
+ }
151
+ clause[:negate] ? !result : result
152
+ else
153
+ clause_match_user_no_segments(clause, user)
154
+ end
155
+ end
156
+
157
+ def clause_match_user_no_segments(clause, user)
158
+ user_val = EvaluatorOperators.user_value(user, clause[:attribute])
159
+ return false if user_val.nil?
160
+
161
+ op = clause[:op].to_sym
162
+ clause_vals = clause[:values]
163
+ result = if user_val.is_a? Enumerable
164
+ user_val.any? { |uv| clause_vals.any? { |cv| EvaluatorOperators.apply(op, uv, cv) } }
165
+ else
166
+ clause_vals.any? { |cv| EvaluatorOperators.apply(op, user_val, cv) }
167
+ end
168
+ clause[:negate] ? !result : result
169
+ end
170
+
171
+ def segment_match_user(segment, user)
172
+ return false unless user[:key]
173
+
174
+ return true if segment[:included].include?(user[:key])
175
+ return false if segment[:excluded].include?(user[:key])
176
+
177
+ (segment[:rules] || []).each do |r|
178
+ return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
179
+ end
180
+
181
+ return false
182
+ end
183
+
184
+ def segment_rule_match_user(rule, user, segment_key, salt)
185
+ (rule[:clauses] || []).each do |c|
186
+ return false unless clause_match_user_no_segments(c, user)
187
+ end
188
+
189
+ # If the weight is absent, this rule matches
190
+ return true if !rule[:weight]
191
+
192
+ # 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)
194
+ weight = rule[:weight].to_f / 100000.0
195
+ return bucket < weight
196
+ end
197
+
198
+ 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)
217
+ if index.nil?
218
+ @logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
219
+ return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
220
+ end
221
+ return get_variation(flag, index, reason)
222
+ end
223
+ end
224
+ end
225
+ end