launchdarkly-server-sdk 5.8.0 → 6.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +28 -122
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  4. data/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. data/.gitignore +2 -1
  6. data/.ldrelease/build-docs.sh +18 -0
  7. data/.ldrelease/circleci/linux/execute.sh +18 -0
  8. data/.ldrelease/circleci/mac/execute.sh +18 -0
  9. data/.ldrelease/circleci/template/build.sh +29 -0
  10. data/.ldrelease/circleci/template/publish.sh +23 -0
  11. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  12. data/.ldrelease/circleci/template/test.sh +10 -0
  13. data/.ldrelease/circleci/template/update-version.sh +8 -0
  14. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  15. data/.ldrelease/config.yml +14 -2
  16. data/CHANGELOG.md +29 -0
  17. data/CONTRIBUTING.md +1 -1
  18. data/README.md +4 -3
  19. data/azure-pipelines.yml +1 -1
  20. data/docs/Makefile +26 -0
  21. data/docs/index.md +9 -0
  22. data/launchdarkly-server-sdk.gemspec +20 -13
  23. data/lib/ldclient-rb.rb +0 -1
  24. data/lib/ldclient-rb/config.rb +15 -3
  25. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  26. data/lib/ldclient-rb/events.rb +6 -7
  27. data/lib/ldclient-rb/file_data_source.rb +1 -1
  28. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  29. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  30. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  31. data/lib/ldclient-rb/impl/event_factory.rb +22 -0
  32. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  33. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  34. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  35. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -7
  36. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  37. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  38. data/lib/ldclient-rb/ldclient.rb +38 -17
  39. data/lib/ldclient-rb/polling.rb +1 -4
  40. data/lib/ldclient-rb/requestor.rb +25 -23
  41. data/lib/ldclient-rb/stream.rb +10 -30
  42. data/lib/ldclient-rb/util.rb +12 -8
  43. data/lib/ldclient-rb/version.rb +1 -1
  44. data/spec/evaluation_detail_spec.rb +135 -0
  45. data/spec/event_sender_spec.rb +20 -2
  46. data/spec/events_spec.rb +10 -0
  47. data/spec/http_util.rb +11 -1
  48. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  49. data/spec/impl/evaluator_clause_spec.rb +55 -0
  50. data/spec/impl/evaluator_operators_spec.rb +141 -0
  51. data/spec/impl/evaluator_rule_spec.rb +96 -0
  52. data/spec/impl/evaluator_segment_spec.rb +125 -0
  53. data/spec/impl/evaluator_spec.rb +305 -0
  54. data/spec/impl/evaluator_spec_base.rb +75 -0
  55. data/spec/impl/model/serialization_spec.rb +41 -0
  56. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  57. data/spec/ldclient_end_to_end_spec.rb +34 -0
  58. data/spec/ldclient_spec.rb +64 -12
  59. data/spec/polling_spec.rb +2 -2
  60. data/spec/redis_feature_store_spec.rb +2 -2
  61. data/spec/requestor_spec.rb +11 -45
  62. data/spec/spec_helper.rb +0 -3
  63. data/spec/stream_spec.rb +1 -16
  64. metadata +111 -61
  65. data/.yardopts +0 -9
  66. data/Gemfile.lock +0 -100
  67. data/lib/ldclient-rb/evaluation.rb +0 -462
  68. data/scripts/gendocs.sh +0 -11
  69. data/scripts/release.sh +0 -27
  70. data/spec/evaluation_spec.rb +0 -789
@@ -28,6 +28,7 @@ module LaunchDarkly
28
28
  e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
29
29
  e[:prereqOf] = prereq_of_flag[:key] if !prereq_of_flag.nil?
30
30
  e[:reason] = detail.reason if add_experiment_data || @with_reasons
31
+ e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
31
32
  e
32
33
  end
33
34
 
@@ -43,6 +44,7 @@ module LaunchDarkly
43
44
  e[:trackEvents] = true if flag[:trackEvents]
44
45
  e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
45
46
  e[:reason] = reason if @with_reasons
47
+ e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
46
48
  e
47
49
  end
48
50
 
@@ -55,6 +57,7 @@ module LaunchDarkly
55
57
  default: default_value
56
58
  }
57
59
  e[:reason] = reason if @with_reasons
60
+ e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
58
61
  e
59
62
  end
60
63
 
@@ -66,6 +69,16 @@ module LaunchDarkly
66
69
  }
67
70
  end
68
71
 
72
+ def new_alias_event(current_context, previous_context)
73
+ {
74
+ kind: 'alias',
75
+ key: current_context[:key],
76
+ contextKind: context_to_context_kind(current_context),
77
+ previousKey: previous_context[:key],
78
+ previousContextKind: context_to_context_kind(previous_context)
79
+ }
80
+ end
81
+
69
82
  def new_custom_event(event_name, user, data, metric_value)
70
83
  e = {
71
84
  kind: 'custom',
@@ -74,11 +87,20 @@ module LaunchDarkly
74
87
  }
75
88
  e[:data] = data if !data.nil?
76
89
  e[:metricValue] = metric_value if !metric_value.nil?
90
+ e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
77
91
  e
78
92
  end
79
93
 
80
94
  private
81
95
 
96
+ def context_to_context_kind(user)
97
+ if !user.nil? && user[:anonymous]
98
+ return "anonymousUser"
99
+ else
100
+ return "user"
101
+ end
102
+ end
103
+
82
104
  def is_experiment(flag, reason)
83
105
  return false if !reason
84
106
  case reason[:kind]
@@ -1,4 +1,7 @@
1
+ require "ldclient-rb/impl/unbounded_pool"
2
+
1
3
  require "securerandom"
4
+ require "http"
2
5
 
3
6
  module LaunchDarkly
4
7
  module Impl
@@ -9,62 +12,75 @@ module LaunchDarkly
9
12
  DEFAULT_RETRY_INTERVAL = 1
10
13
 
11
14
  def initialize(sdk_key, config, http_client = nil, retry_interval = DEFAULT_RETRY_INTERVAL)
12
- @client = http_client ? http_client : LaunchDarkly::Util.new_http_client(config.events_uri, config)
13
15
  @sdk_key = sdk_key
14
16
  @config = config
15
17
  @events_uri = config.events_uri + "/bulk"
16
18
  @diagnostic_uri = config.events_uri + "/diagnostic"
17
19
  @logger = config.logger
18
20
  @retry_interval = retry_interval
21
+ @http_client_pool = UnboundedPool.new(
22
+ lambda { LaunchDarkly::Util.new_http_client(@config.events_uri, @config) },
23
+ lambda { |client| client.close })
24
+ end
25
+
26
+ def stop
27
+ @http_client_pool.dispose_all()
19
28
  end
20
29
 
21
30
  def send_event_data(event_data, description, is_diagnostic)
22
31
  uri = is_diagnostic ? @diagnostic_uri : @events_uri
23
32
  payload_id = is_diagnostic ? nil : SecureRandom.uuid
24
- res = nil
25
- (0..1).each do |attempt|
26
- if attempt > 0
27
- @logger.warn { "[LDClient] Will retry posting events after #{@retry_interval} second" }
28
- sleep(@retry_interval)
29
- end
30
- begin
31
- @client.start if !@client.started?
32
- @logger.debug { "[LDClient] sending #{description}: #{event_data}" }
33
- req = Net::HTTP::Post.new(uri)
34
- req.content_type = "application/json"
35
- req.body = event_data
36
- Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| req[k] = v }
37
- if !is_diagnostic
38
- req["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
39
- req["X-LaunchDarkly-Payload-ID"] = payload_id
33
+ begin
34
+ http_client = @http_client_pool.acquire()
35
+ response = nil
36
+ (0..1).each do |attempt|
37
+ if attempt > 0
38
+ @logger.warn { "[LDClient] Will retry posting events after #{@retry_interval} second" }
39
+ sleep(@retry_interval)
40
40
  end
41
- req["Connection"] = "keep-alive"
42
- res = @client.request(req)
43
- rescue StandardError => exn
44
- @logger.warn { "[LDClient] Error sending events: #{exn.inspect}." }
45
- next
46
- end
47
- status = res.code.to_i
48
- if status >= 200 && status < 300
49
- res_time = nil
50
- if !res["date"].nil?
51
- begin
52
- res_time = Time.httpdate(res["date"])
53
- rescue ArgumentError
41
+ begin
42
+ @logger.debug { "[LDClient] sending #{description}: #{event_data}" }
43
+ headers = {}
44
+ headers["content-type"] = "application/json"
45
+ Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
46
+ if !is_diagnostic
47
+ headers["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
48
+ headers["X-LaunchDarkly-Payload-ID"] = payload_id
54
49
  end
50
+ response = http_client.request("POST", uri, {
51
+ headers: headers,
52
+ body: event_data
53
+ })
54
+ rescue StandardError => exn
55
+ @logger.warn { "[LDClient] Error sending events: #{exn.inspect}." }
56
+ next
57
+ end
58
+ status = response.status.code
59
+ # must fully read body for persistent connections
60
+ body = response.to_s
61
+ if status >= 200 && status < 300
62
+ res_time = nil
63
+ if !response.headers["date"].nil?
64
+ begin
65
+ res_time = Time.httpdate(response.headers["date"])
66
+ rescue ArgumentError
67
+ end
68
+ end
69
+ return EventSenderResult.new(true, false, res_time)
70
+ end
71
+ must_shutdown = !LaunchDarkly::Util.http_error_recoverable?(status)
72
+ can_retry = !must_shutdown && attempt == 0
73
+ message = LaunchDarkly::Util.http_error_message(status, "event delivery", can_retry ? "will retry" : "some events were dropped")
74
+ @logger.error { "[LDClient] #{message}" }
75
+ if must_shutdown
76
+ return EventSenderResult.new(false, true, nil)
55
77
  end
56
- return EventSenderResult.new(true, false, res_time)
57
- end
58
- must_shutdown = !LaunchDarkly::Util.http_error_recoverable?(status)
59
- can_retry = !must_shutdown && attempt == 0
60
- message = LaunchDarkly::Util.http_error_message(status, "event delivery", can_retry ? "will retry" : "some events were dropped")
61
- @logger.error { "[LDClient] #{message}" }
62
- if must_shutdown
63
- return EventSenderResult.new(false, true, nil)
64
78
  end
79
+ # used up our retries
80
+ return EventSenderResult.new(false, false, nil)
81
+ ensure
82
+ @http_client_pool.release(http_client)
65
83
  end
66
- # used up our retries
67
- return EventSenderResult.new(false, false, nil)
68
84
  end
69
85
  end
70
86
  end
@@ -39,7 +39,7 @@ module LaunchDarkly
39
39
  # Insert or update every provided item
40
40
  all_data.each do |kind, items|
41
41
  items.values.each do |item|
42
- value = item.to_json
42
+ value = Model.serialize(kind, item)
43
43
  key = item_key(kind, item[:key])
44
44
  ops.push({ 'KV' => { 'Verb' => 'set', 'Key' => key, 'Value' => value } })
45
45
  unused_old_keys.delete(key)
@@ -62,7 +62,7 @@ module LaunchDarkly
62
62
 
63
63
  def get_internal(kind, key)
64
64
  value = Diplomat::Kv.get(item_key(kind, key), {}, :return) # :return means "don't throw an error if not found"
65
- (value.nil? || value == "") ? nil : JSON.parse(value, symbolize_names: true)
65
+ (value.nil? || value == "") ? nil : Model.deserialize(kind, value)
66
66
  end
67
67
 
68
68
  def get_all_internal(kind)
@@ -71,7 +71,7 @@ module LaunchDarkly
71
71
  (results == "" ? [] : results).each do |result|
72
72
  value = result[:value]
73
73
  if !value.nil?
74
- item = JSON.parse(value, symbolize_names: true)
74
+ item = Model.deserialize(kind, value)
75
75
  items_out[item[:key].to_sym] = item
76
76
  end
77
77
  end
@@ -80,7 +80,7 @@ module LaunchDarkly
80
80
 
81
81
  def upsert_internal(kind, new_item)
82
82
  key = item_key(kind, new_item[:key])
83
- json = new_item.to_json
83
+ json = Model.serialize(kind, new_item)
84
84
 
85
85
  # We will potentially keep retrying indefinitely until someone's write succeeds
86
86
  while true
@@ -88,7 +88,7 @@ module LaunchDarkly
88
88
  if old_value.nil? || old_value == ""
89
89
  mod_index = 0
90
90
  else
91
- old_item = JSON.parse(old_value[0]["Value"], symbolize_names: true)
91
+ old_item = Model.deserialize(kind, old_value[0]["Value"])
92
92
  # Check whether the item is stale. If so, don't do the update (and return the existing item to
93
93
  # FeatureStoreWrapper so it can be cached)
94
94
  if old_item[:version] >= new_item[:version]
@@ -77,7 +77,7 @@ module LaunchDarkly
77
77
 
78
78
  def get_internal(kind, key)
79
79
  resp = get_item_by_keys(namespace_for_kind(kind), key)
80
- unmarshal_item(resp.item)
80
+ unmarshal_item(kind, resp.item)
81
81
  end
82
82
 
83
83
  def get_all_internal(kind)
@@ -86,7 +86,7 @@ module LaunchDarkly
86
86
  while true
87
87
  resp = @client.query(req)
88
88
  resp.items.each do |item|
89
- item_out = unmarshal_item(item)
89
+ item_out = unmarshal_item(kind, item)
90
90
  items_out[item_out[:key].to_sym] = item_out
91
91
  end
92
92
  break if resp.last_evaluated_key.nil? || resp.last_evaluated_key.length == 0
@@ -196,15 +196,15 @@ module LaunchDarkly
196
196
  def marshal_item(kind, item)
197
197
  make_keys_hash(namespace_for_kind(kind), item[:key]).merge({
198
198
  VERSION_ATTRIBUTE => item[:version],
199
- ITEM_JSON_ATTRIBUTE => item.to_json
199
+ ITEM_JSON_ATTRIBUTE => Model.serialize(kind, item)
200
200
  })
201
201
  end
202
202
 
203
- def unmarshal_item(item)
203
+ def unmarshal_item(kind, item)
204
204
  return nil if item.nil? || item.length == 0
205
205
  json_attr = item[ITEM_JSON_ATTRIBUTE]
206
206
  raise RuntimeError.new("DynamoDB map did not contain expected item string") if json_attr.nil?
207
- JSON.parse(json_attr, symbolize_names: true)
207
+ Model.deserialize(kind, json_attr)
208
208
  end
209
209
  end
210
210
 
@@ -55,7 +55,7 @@ module LaunchDarkly
55
55
  multi.del(items_key(kind))
56
56
  count = count + items.count
57
57
  items.each do |key, item|
58
- multi.hset(items_key(kind), key, item.to_json)
58
+ multi.hset(items_key(kind), key, Model.serialize(kind,item))
59
59
  end
60
60
  end
61
61
  multi.set(inited_key, inited_key)
@@ -75,8 +75,7 @@ module LaunchDarkly
75
75
  with_connection do |redis|
76
76
  hashfs = redis.hgetall(items_key(kind))
77
77
  hashfs.each do |k, json_item|
78
- f = JSON.parse(json_item, symbolize_names: true)
79
- fs[k.to_sym] = f
78
+ fs[k.to_sym] = Model.deserialize(kind, json_item)
80
79
  end
81
80
  end
82
81
  fs
@@ -95,7 +94,7 @@ module LaunchDarkly
95
94
  before_update_transaction(base_key, key)
96
95
  if old_item.nil? || old_item[:version] < new_item[:version]
97
96
  result = redis.multi do |multi|
98
- multi.hset(base_key, key, new_item.to_json)
97
+ multi.hset(base_key, key, Model.serialize(kind, new_item))
99
98
  end
100
99
  if result.nil?
101
100
  @logger.debug { "RedisFeatureStore: concurrent modification detected, retrying" }
@@ -115,7 +114,7 @@ module LaunchDarkly
115
114
  end
116
115
 
117
116
  def initialized_internal?
118
- with_connection { |redis| redis.exists(inited_key) }
117
+ with_connection { |redis| redis.exists?(inited_key) }
119
118
  end
120
119
 
121
120
  def stop
@@ -148,8 +147,7 @@ module LaunchDarkly
148
147
  end
149
148
 
150
149
  def get_redis(redis, kind, key)
151
- json_item = redis.hget(items_key(kind), key)
152
- json_item.nil? ? nil : JSON.parse(json_item, symbolize_names: true)
150
+ Model.deserialize(kind, redis.hget(items_key(kind), key))
153
151
  end
154
152
  end
155
153
  end
@@ -0,0 +1,62 @@
1
+
2
+ module LaunchDarkly
3
+ module Impl
4
+ module Model
5
+ # Abstraction of deserializing a feature flag or segment that was read from a data store or
6
+ # received from LaunchDarkly.
7
+ def self.deserialize(kind, json)
8
+ return nil if json.nil?
9
+ item = JSON.parse(json, symbolize_names: true)
10
+ postprocess_item_after_deserializing!(kind, item)
11
+ item
12
+ end
13
+
14
+ # Abstraction of serializing a feature flag or segment that will be written to a data store.
15
+ # Currently we just call to_json.
16
+ def self.serialize(kind, item)
17
+ item.to_json
18
+ end
19
+
20
+ # Translates a { flags: ..., segments: ... } object received from LaunchDarkly to the data store format.
21
+ def self.make_all_store_data(received_data)
22
+ flags = received_data[:flags]
23
+ postprocess_items_after_deserializing!(FEATURES, flags)
24
+ segments = received_data[:segments]
25
+ postprocess_items_after_deserializing!(SEGMENTS, segments)
26
+ { FEATURES => flags, SEGMENTS => segments }
27
+ end
28
+
29
+ # Called after we have deserialized a model item from JSON (because we received it from LaunchDarkly,
30
+ # or read it from a persistent data store). This allows us to precompute some derived attributes that
31
+ # will never change during the lifetime of that item.
32
+ def self.postprocess_item_after_deserializing!(kind, item)
33
+ return if !item
34
+ # Currently we are special-casing this for FEATURES; eventually it will be handled by delegating
35
+ # to the "kind" object or the item class.
36
+ if kind.eql? FEATURES
37
+ # For feature flags, we precompute all possible parameterized EvaluationReason instances.
38
+ prereqs = item[:prerequisites]
39
+ if !prereqs.nil?
40
+ prereqs.each do |prereq|
41
+ prereq[:_reason] = EvaluationReason::prerequisite_failed(prereq[:key])
42
+ end
43
+ end
44
+ rules = item[:rules]
45
+ if !rules.nil?
46
+ rules.each_index do |i|
47
+ rule = rules[i]
48
+ rule[:_reason] = EvaluationReason::rule_match(i, rule[:id])
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def self.postprocess_items_after_deserializing!(kind, items_map)
55
+ return items_map if !items_map
56
+ items_map.each do |key, item|
57
+ postprocess_item_after_deserializing!(kind, item)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,34 @@
1
+ module LaunchDarkly
2
+ module Impl
3
+ # A simple thread safe generic unbounded resource pool abstraction
4
+ class UnboundedPool
5
+ def initialize(instance_creator, instance_destructor)
6
+ @pool = Array.new
7
+ @lock = Mutex.new
8
+ @instance_creator = instance_creator
9
+ @instance_destructor = instance_destructor
10
+ end
11
+
12
+ def acquire
13
+ @lock.synchronize {
14
+ if @pool.length == 0
15
+ @instance_creator.call()
16
+ else
17
+ @pool.pop()
18
+ end
19
+ }
20
+ end
21
+
22
+ def release(instance)
23
+ @lock.synchronize { @pool.push(instance) }
24
+ end
25
+
26
+ def dispose_all
27
+ @lock.synchronize {
28
+ @pool.map { |instance| @instance_destructor.call(instance) } if !@instance_destructor.nil?
29
+ @pool.clear()
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,4 +1,5 @@
1
1
  require "ldclient-rb/impl/diagnostic_events"
2
+ require "ldclient-rb/impl/evaluator"
2
3
  require "ldclient-rb/impl/event_factory"
3
4
  require "ldclient-rb/impl/store_client_wrapper"
4
5
  require "concurrent/atomics"
@@ -14,7 +15,6 @@ module LaunchDarkly
14
15
  # should create a single client instance for the lifetime of the application.
15
16
  #
16
17
  class LDClient
17
- include Evaluation
18
18
  include Impl
19
19
  #
20
20
  # Creates a new client instance that connects to LaunchDarkly. A custom
@@ -57,6 +57,10 @@ module LaunchDarkly
57
57
  updated_config.instance_variable_set(:@feature_store, @store)
58
58
  @config = updated_config
59
59
 
60
+ get_flag = lambda { |key| @store.get(FEATURES, key) }
61
+ get_segment = lambda { |key| @store.get(SEGMENTS, key) }
62
+ @evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, @config.logger)
63
+
60
64
  if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out?
61
65
  diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key))
62
66
  else
@@ -278,6 +282,23 @@ module LaunchDarkly
278
282
  @event_processor.add_event(@event_factory_default.new_custom_event(event_name, user, data, metric_value))
279
283
  end
280
284
 
285
+ #
286
+ # Associates a new and old user object for analytics purposes via an alias event.
287
+ #
288
+ # @param current_context [Hash] The current version of a user.
289
+ # @param previous_context [Hash] The previous version of a user.
290
+ # @return [void]
291
+ #
292
+ def alias(current_context, previous_context)
293
+ if !current_context || current_context[:key].nil? || !previous_context || previous_context[:key].nil?
294
+ @config.logger.warn("Alias called with nil user or nil user key!")
295
+ return
296
+ end
297
+ sanitize_user(current_context)
298
+ sanitize_user(previous_context)
299
+ @event_processor.add_event(@event_factory_default.new_alias_event(current_context, previous_context))
300
+ end
301
+
281
302
  #
282
303
  # Returns all feature flag values for the given user.
283
304
  #
@@ -333,12 +354,13 @@ module LaunchDarkly
333
354
  next
334
355
  end
335
356
  begin
336
- result = evaluate(f, user, @store, @config.logger, @event_factory_default)
357
+ result = @evaluator.evaluate(f, user, @event_factory_default)
337
358
  state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil,
338
359
  details_only_if_tracked)
339
360
  rescue => exn
340
361
  Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
341
- state.add_flag(f, nil, nil, with_reasons ? { kind: 'ERROR', errorKind: 'EXCEPTION' } : nil, details_only_if_tracked)
362
+ state.add_flag(f, nil, nil, with_reasons ? EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION) : nil,
363
+ details_only_if_tracked)
342
364
  end
343
365
  end
344
366
 
@@ -363,12 +385,12 @@ module LaunchDarkly
363
385
  return NullUpdateProcessor.new
364
386
  end
365
387
  raise ArgumentError, "sdk_key must not be nil" if sdk_key.nil? # see LDClient constructor comment on sdk_key
366
- requestor = Requestor.new(sdk_key, config)
367
388
  if config.stream?
368
- StreamProcessor.new(sdk_key, config, requestor, diagnostic_accumulator)
389
+ StreamProcessor.new(sdk_key, config, diagnostic_accumulator)
369
390
  else
370
391
  config.logger.info { "Disabling streaming API" }
371
392
  config.logger.warn { "You should only disable the streaming API if instructed to do so by LaunchDarkly support" }
393
+ requestor = Requestor.new(sdk_key, config)
372
394
  PollingProcessor.new(config, requestor)
373
395
  end
374
396
  end
@@ -376,7 +398,13 @@ module LaunchDarkly
376
398
  # @return [EvaluationDetail]
377
399
  def evaluate_internal(key, user, default, event_factory)
378
400
  if @config.offline?
379
- return error_result('CLIENT_NOT_READY', default)
401
+ return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
402
+ end
403
+
404
+ unless user
405
+ @config.logger.error { "[LDClient] Must specify user" }
406
+ detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
407
+ return detail
380
408
  end
381
409
 
382
410
  if !initialized?
@@ -384,7 +412,7 @@ module LaunchDarkly
384
412
  @config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
385
413
  else
386
414
  @config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
387
- detail = error_result('CLIENT_NOT_READY', default)
415
+ detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
388
416
  @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
389
417
  return detail
390
418
  end
@@ -394,20 +422,13 @@ module LaunchDarkly
394
422
 
395
423
  if feature.nil?
396
424
  @config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
397
- detail = error_result('FLAG_NOT_FOUND', default)
425
+ detail = Evaluator.error_result(EvaluationReason::ERROR_FLAG_NOT_FOUND, default)
398
426
  @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
399
427
  return detail
400
428
  end
401
429
 
402
- unless user
403
- @config.logger.error { "[LDClient] Must specify user" }
404
- detail = error_result('USER_NOT_SPECIFIED', default)
405
- @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
406
- return detail
407
- end
408
-
409
430
  begin
410
- res = evaluate(feature, user, @store, @config.logger, event_factory)
431
+ res = @evaluator.evaluate(feature, user, event_factory)
411
432
  if !res.events.nil?
412
433
  res.events.each do |event|
413
434
  @event_processor.add_event(event)
@@ -421,7 +442,7 @@ module LaunchDarkly
421
442
  return detail
422
443
  rescue => exn
423
444
  Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
424
- detail = error_result('EXCEPTION', default)
445
+ detail = Evaluator.error_result(EvaluationReason::ERROR_EXCEPTION, default)
425
446
  @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
426
447
  return detail
427
448
  end