launchdarkly-server-sdk 5.8.2 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
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