launchdarkly-server-sdk 5.7.4 → 6.1.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 (70) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +28 -122
  3. data/.gitignore +1 -1
  4. data/.ldrelease/build-docs.sh +18 -0
  5. data/.ldrelease/circleci/linux/execute.sh +18 -0
  6. data/.ldrelease/circleci/mac/execute.sh +18 -0
  7. data/.ldrelease/circleci/template/build.sh +29 -0
  8. data/.ldrelease/circleci/template/publish.sh +23 -0
  9. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  10. data/.ldrelease/circleci/template/test.sh +10 -0
  11. data/.ldrelease/circleci/template/update-version.sh +8 -0
  12. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  13. data/.ldrelease/config.yml +14 -2
  14. data/CHANGELOG.md +28 -0
  15. data/CONTRIBUTING.md +1 -1
  16. data/Gemfile.lock +92 -76
  17. data/README.md +4 -3
  18. data/azure-pipelines.yml +1 -1
  19. data/docs/Makefile +26 -0
  20. data/docs/index.md +9 -0
  21. data/launchdarkly-server-sdk.gemspec +20 -13
  22. data/lib/ldclient-rb.rb +0 -1
  23. data/lib/ldclient-rb/config.rb +15 -3
  24. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  25. data/lib/ldclient-rb/events.rb +3 -4
  26. data/lib/ldclient-rb/file_data_source.rb +1 -1
  27. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  28. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  29. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  30. data/lib/ldclient-rb/impl/event_factory.rb +22 -0
  31. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  32. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  33. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  34. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +8 -7
  35. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  36. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  37. data/lib/ldclient-rb/integrations/redis.rb +3 -0
  38. data/lib/ldclient-rb/ldclient.rb +33 -11
  39. data/lib/ldclient-rb/polling.rb +1 -4
  40. data/lib/ldclient-rb/redis_store.rb +1 -0
  41. data/lib/ldclient-rb/requestor.rb +25 -23
  42. data/lib/ldclient-rb/stream.rb +10 -30
  43. data/lib/ldclient-rb/util.rb +12 -8
  44. data/lib/ldclient-rb/version.rb +1 -1
  45. data/spec/evaluation_detail_spec.rb +135 -0
  46. data/spec/event_sender_spec.rb +20 -2
  47. data/spec/events_spec.rb +10 -0
  48. data/spec/http_util.rb +11 -1
  49. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  50. data/spec/impl/evaluator_clause_spec.rb +55 -0
  51. data/spec/impl/evaluator_operators_spec.rb +141 -0
  52. data/spec/impl/evaluator_rule_spec.rb +96 -0
  53. data/spec/impl/evaluator_segment_spec.rb +125 -0
  54. data/spec/impl/evaluator_spec.rb +305 -0
  55. data/spec/impl/evaluator_spec_base.rb +75 -0
  56. data/spec/impl/model/serialization_spec.rb +41 -0
  57. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  58. data/spec/ldclient_end_to_end_spec.rb +34 -0
  59. data/spec/ldclient_spec.rb +60 -8
  60. data/spec/polling_spec.rb +2 -2
  61. data/spec/redis_feature_store_spec.rb +32 -3
  62. data/spec/requestor_spec.rb +11 -45
  63. data/spec/spec_helper.rb +0 -3
  64. data/spec/stream_spec.rb +1 -16
  65. metadata +110 -60
  66. data/.yardopts +0 -9
  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
 
@@ -33,6 +33,8 @@ module LaunchDarkly
33
33
  @pool = opts[:pool] || ConnectionPool.new(size: max_connections) do
34
34
  ::Redis.new(@redis_opts)
35
35
  end
36
+ # shutdown pool on close unless the client passed a custom pool and specified not to shutdown
37
+ @pool_shutdown_on_close = (!opts[:pool] || opts.fetch(:pool_shutdown_on_close, true))
36
38
  @prefix = opts[:prefix] || LaunchDarkly::Integrations::Redis::default_prefix
37
39
  @logger = opts[:logger] || Config.default_logger
38
40
  @test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
@@ -53,7 +55,7 @@ module LaunchDarkly
53
55
  multi.del(items_key(kind))
54
56
  count = count + items.count
55
57
  items.each do |key, item|
56
- multi.hset(items_key(kind), key, item.to_json)
58
+ multi.hset(items_key(kind), key, Model.serialize(kind,item))
57
59
  end
58
60
  end
59
61
  multi.set(inited_key, inited_key)
@@ -73,8 +75,7 @@ module LaunchDarkly
73
75
  with_connection do |redis|
74
76
  hashfs = redis.hgetall(items_key(kind))
75
77
  hashfs.each do |k, json_item|
76
- f = JSON.parse(json_item, symbolize_names: true)
77
- fs[k.to_sym] = f
78
+ fs[k.to_sym] = Model.deserialize(kind, json_item)
78
79
  end
79
80
  end
80
81
  fs
@@ -93,7 +94,7 @@ module LaunchDarkly
93
94
  before_update_transaction(base_key, key)
94
95
  if old_item.nil? || old_item[:version] < new_item[:version]
95
96
  result = redis.multi do |multi|
96
- multi.hset(base_key, key, new_item.to_json)
97
+ multi.hset(base_key, key, Model.serialize(kind, new_item))
97
98
  end
98
99
  if result.nil?
99
100
  @logger.debug { "RedisFeatureStore: concurrent modification detected, retrying" }
@@ -113,11 +114,12 @@ module LaunchDarkly
113
114
  end
114
115
 
115
116
  def initialized_internal?
116
- with_connection { |redis| redis.exists(inited_key) }
117
+ with_connection { |redis| redis.exists?(inited_key) }
117
118
  end
118
119
 
119
120
  def stop
120
121
  if @stopped.make_true
122
+ return unless @pool_shutdown_on_close
121
123
  @pool.shutdown { |redis| redis.close }
122
124
  end
123
125
  end
@@ -145,8 +147,7 @@ module LaunchDarkly
145
147
  end
146
148
 
147
149
  def get_redis(redis, kind, key)
148
- json_item = redis.hget(items_key(kind), key)
149
- json_item.nil? ? nil : JSON.parse(json_item, symbolize_names: true)
150
+ Model.deserialize(kind, redis.hget(items_key(kind), key))
150
151
  end
151
152
  end
152
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
@@ -45,6 +45,9 @@ module LaunchDarkly
45
45
  # @option opts [Integer] :expiration (15) expiration time for the in-memory cache, in seconds; 0 for no local caching
46
46
  # @option opts [Integer] :capacity (1000) maximum number of items in the cache
47
47
  # @option opts [Object] :pool custom connection pool, if desired
48
+ # @option opts [Boolean] :pool_shutdown_on_close whether calling `close` should shutdown the custom connection pool;
49
+ # this is true by default, and should be set to false only if you are managing the pool yourself and want its
50
+ # lifecycle to be independent of the SDK client
48
51
  # @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
49
52
  #
50
53
  def self.new_feature_store(opts)
@@ -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,7 @@ 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)
380
402
  end
381
403
 
382
404
  if !initialized?
@@ -384,7 +406,7 @@ module LaunchDarkly
384
406
  @config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
385
407
  else
386
408
  @config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
387
- detail = error_result('CLIENT_NOT_READY', default)
409
+ detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
388
410
  @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
389
411
  return detail
390
412
  end
@@ -394,20 +416,20 @@ module LaunchDarkly
394
416
 
395
417
  if feature.nil?
396
418
  @config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
397
- detail = error_result('FLAG_NOT_FOUND', default)
419
+ detail = Evaluator.error_result(EvaluationReason::ERROR_FLAG_NOT_FOUND, default)
398
420
  @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
399
421
  return detail
400
422
  end
401
423
 
402
424
  unless user
403
425
  @config.logger.error { "[LDClient] Must specify user" }
404
- detail = error_result('USER_NOT_SPECIFIED', default)
426
+ detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
405
427
  @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
406
428
  return detail
407
429
  end
408
430
 
409
431
  begin
410
- res = evaluate(feature, user, @store, @config.logger, event_factory)
432
+ res = @evaluator.evaluate(feature, user, event_factory)
411
433
  if !res.events.nil?
412
434
  res.events.each do |event|
413
435
  @event_processor.add_event(event)
@@ -421,7 +443,7 @@ module LaunchDarkly
421
443
  return detail
422
444
  rescue => exn
423
445
  Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
424
- detail = error_result('EXCEPTION', default)
446
+ detail = Evaluator.error_result(EvaluationReason::ERROR_EXCEPTION, default)
425
447
  @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
426
448
  return detail
427
449
  end