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.
- checksums.yaml +5 -5
- data/.circleci/config.yml +28 -122
- data/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- data/.github/ISSUE_TEMPLATE/config.yml +5 -0
- data/.gitignore +2 -1
- data/.ldrelease/build-docs.sh +18 -0
- data/.ldrelease/circleci/linux/execute.sh +18 -0
- data/.ldrelease/circleci/mac/execute.sh +18 -0
- data/.ldrelease/circleci/template/build.sh +29 -0
- data/.ldrelease/circleci/template/publish.sh +23 -0
- data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
- data/.ldrelease/circleci/template/test.sh +10 -0
- data/.ldrelease/circleci/template/update-version.sh +8 -0
- data/.ldrelease/circleci/windows/execute.ps1 +19 -0
- data/.ldrelease/config.yml +14 -2
- data/CHANGELOG.md +29 -0
- data/CONTRIBUTING.md +1 -1
- data/README.md +4 -3
- data/azure-pipelines.yml +1 -1
- data/docs/Makefile +26 -0
- data/docs/index.md +9 -0
- data/launchdarkly-server-sdk.gemspec +20 -13
- data/lib/ldclient-rb.rb +0 -1
- data/lib/ldclient-rb/config.rb +15 -3
- data/lib/ldclient-rb/evaluation_detail.rb +293 -0
- data/lib/ldclient-rb/events.rb +6 -7
- data/lib/ldclient-rb/file_data_source.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +225 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
- data/lib/ldclient-rb/impl/event_factory.rb +22 -0
- data/lib/ldclient-rb/impl/event_sender.rb +56 -40
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -7
- data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
- data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
- data/lib/ldclient-rb/ldclient.rb +38 -17
- data/lib/ldclient-rb/polling.rb +1 -4
- data/lib/ldclient-rb/requestor.rb +25 -23
- data/lib/ldclient-rb/stream.rb +10 -30
- data/lib/ldclient-rb/util.rb +12 -8
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/evaluation_detail_spec.rb +135 -0
- data/spec/event_sender_spec.rb +20 -2
- data/spec/events_spec.rb +10 -0
- data/spec/http_util.rb +11 -1
- data/spec/impl/evaluator_bucketing_spec.rb +111 -0
- data/spec/impl/evaluator_clause_spec.rb +55 -0
- data/spec/impl/evaluator_operators_spec.rb +141 -0
- data/spec/impl/evaluator_rule_spec.rb +96 -0
- data/spec/impl/evaluator_segment_spec.rb +125 -0
- data/spec/impl/evaluator_spec.rb +305 -0
- data/spec/impl/evaluator_spec_base.rb +75 -0
- data/spec/impl/model/serialization_spec.rb +41 -0
- data/spec/launchdarkly-server-sdk_spec.rb +1 -1
- data/spec/ldclient_end_to_end_spec.rb +34 -0
- data/spec/ldclient_spec.rb +64 -12
- data/spec/polling_spec.rb +2 -2
- data/spec/redis_feature_store_spec.rb +2 -2
- data/spec/requestor_spec.rb +11 -45
- data/spec/spec_helper.rb +0 -3
- data/spec/stream_spec.rb +1 -16
- metadata +111 -61
- data/.yardopts +0 -9
- data/Gemfile.lock +0 -100
- data/lib/ldclient-rb/evaluation.rb +0 -462
- data/scripts/gendocs.sh +0 -11
- data/scripts/release.sh +0 -27
- 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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
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 :
|
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 =
|
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
|
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 =
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
data/lib/ldclient-rb/ldclient.rb
CHANGED
@@ -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, @
|
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 ?
|
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,
|
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(
|
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(
|
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(
|
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,
|
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(
|
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
|