launchdarkly-server-sdk 5.8.0 → 6.1.1
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.
- 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
data/lib/ldclient-rb/polling.rb
CHANGED
@@ -37,10 +37,7 @@ module LaunchDarkly
|
|
37
37
|
def poll
|
38
38
|
all_data = @requestor.request_all_data
|
39
39
|
if all_data
|
40
|
-
@config.feature_store.init(
|
41
|
-
FEATURES => all_data[:flags],
|
42
|
-
SEGMENTS => all_data[:segments]
|
43
|
-
})
|
40
|
+
@config.feature_store.init(all_data)
|
44
41
|
if @initialized.make_true
|
45
42
|
@config.logger.info { "[LDClient] Polling connection initialized" }
|
46
43
|
@ready.set
|
@@ -1,6 +1,9 @@
|
|
1
|
+
require "ldclient-rb/impl/model/serialization"
|
2
|
+
|
1
3
|
require "concurrent/atomics"
|
2
4
|
require "json"
|
3
5
|
require "uri"
|
6
|
+
require "http"
|
4
7
|
|
5
8
|
module LaunchDarkly
|
6
9
|
# @private
|
@@ -22,45 +25,44 @@ module LaunchDarkly
|
|
22
25
|
def initialize(sdk_key, config)
|
23
26
|
@sdk_key = sdk_key
|
24
27
|
@config = config
|
25
|
-
@
|
28
|
+
@http_client = LaunchDarkly::Util.new_http_client(config.base_uri, config)
|
26
29
|
@cache = @config.cache_store
|
27
30
|
end
|
28
31
|
|
29
|
-
def request_flag(key)
|
30
|
-
make_request("/sdk/latest-flags/" + key)
|
31
|
-
end
|
32
|
-
|
33
|
-
def request_segment(key)
|
34
|
-
make_request("/sdk/latest-segments/" + key)
|
35
|
-
end
|
36
|
-
|
37
32
|
def request_all_data()
|
38
|
-
make_request("/sdk/latest-all")
|
33
|
+
all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
|
34
|
+
Impl::Model.make_all_store_data(all_data)
|
39
35
|
end
|
40
36
|
|
41
37
|
def stop
|
42
38
|
begin
|
43
|
-
@
|
39
|
+
@http_client.close
|
44
40
|
rescue
|
45
41
|
end
|
46
42
|
end
|
47
43
|
|
48
44
|
private
|
49
45
|
|
46
|
+
def request_single_item(kind, path)
|
47
|
+
Impl::Model.deserialize(kind, make_request(path))
|
48
|
+
end
|
49
|
+
|
50
50
|
def make_request(path)
|
51
|
-
@client.start if !@client.started?
|
52
51
|
uri = URI(@config.base_uri + path)
|
53
|
-
|
54
|
-
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v|
|
55
|
-
|
52
|
+
headers = {}
|
53
|
+
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
|
54
|
+
headers["Connection"] = "keep-alive"
|
56
55
|
cached = @cache.read(uri)
|
57
56
|
if !cached.nil?
|
58
|
-
|
57
|
+
headers["If-None-Match"] = cached.etag
|
59
58
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
59
|
+
response = @http_client.request("GET", uri, {
|
60
|
+
headers: headers
|
61
|
+
})
|
62
|
+
status = response.status.code
|
63
|
+
@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{response.headers}\n\tbody: #{res.to_s}" }
|
64
|
+
# must fully read body for persistent connections
|
65
|
+
body = response.to_s
|
64
66
|
if status == 304 && !cached.nil?
|
65
67
|
body = cached.body
|
66
68
|
else
|
@@ -68,11 +70,11 @@ module LaunchDarkly
|
|
68
70
|
if status < 200 || status >= 300
|
69
71
|
raise UnexpectedResponseError.new(status)
|
70
72
|
end
|
71
|
-
body = fix_encoding(
|
72
|
-
etag =
|
73
|
+
body = fix_encoding(body, response.headers["content-type"])
|
74
|
+
etag = response.headers["etag"]
|
73
75
|
@cache.write(uri, CacheEntry.new(etag, body)) if !etag.nil?
|
74
76
|
end
|
75
|
-
|
77
|
+
body
|
76
78
|
end
|
77
79
|
|
78
80
|
def fix_encoding(body, content_type)
|
data/lib/ldclient-rb/stream.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "ldclient-rb/impl/model/serialization"
|
2
|
+
|
1
3
|
require "concurrent/atomics"
|
2
4
|
require "json"
|
3
5
|
require "ld-eventsource"
|
@@ -10,10 +12,6 @@ module LaunchDarkly
|
|
10
12
|
# @private
|
11
13
|
DELETE = :delete
|
12
14
|
# @private
|
13
|
-
INDIRECT_PUT = :'indirect/put'
|
14
|
-
# @private
|
15
|
-
INDIRECT_PATCH = :'indirect/patch'
|
16
|
-
# @private
|
17
15
|
READ_TIMEOUT_SECONDS = 300 # 5 minutes; the stream should send a ping every 3 minutes
|
18
16
|
|
19
17
|
# @private
|
@@ -24,11 +22,10 @@ module LaunchDarkly
|
|
24
22
|
|
25
23
|
# @private
|
26
24
|
class StreamProcessor
|
27
|
-
def initialize(sdk_key, config,
|
25
|
+
def initialize(sdk_key, config, diagnostic_accumulator = nil)
|
28
26
|
@sdk_key = sdk_key
|
29
27
|
@config = config
|
30
28
|
@feature_store = config.feature_store
|
31
|
-
@requestor = requestor
|
32
29
|
@initialized = Concurrent::AtomicBoolean.new(false)
|
33
30
|
@started = Concurrent::AtomicBoolean.new(false)
|
34
31
|
@stopped = Concurrent::AtomicBoolean.new(false)
|
@@ -49,7 +46,8 @@ module LaunchDarkly
|
|
49
46
|
opts = {
|
50
47
|
headers: headers,
|
51
48
|
read_timeout: READ_TIMEOUT_SECONDS,
|
52
|
-
logger: @config.logger
|
49
|
+
logger: @config.logger,
|
50
|
+
socket_factory: @config.socket_factory
|
53
51
|
}
|
54
52
|
log_connection_started
|
55
53
|
@es = SSE::Client.new(@config.stream_uri + "/all", **opts) do |conn|
|
@@ -87,10 +85,8 @@ module LaunchDarkly
|
|
87
85
|
@config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
|
88
86
|
if method == PUT
|
89
87
|
message = JSON.parse(message.data, symbolize_names: true)
|
90
|
-
|
91
|
-
|
92
|
-
SEGMENTS => message[:data][:segments]
|
93
|
-
})
|
88
|
+
all_data = Impl::Model.make_all_store_data(message[:data])
|
89
|
+
@feature_store.init(all_data)
|
94
90
|
@initialized.make_true
|
95
91
|
@config.logger.info { "[LDClient] Stream initialized" }
|
96
92
|
@ready.set
|
@@ -99,7 +95,9 @@ module LaunchDarkly
|
|
99
95
|
for kind in [FEATURES, SEGMENTS]
|
100
96
|
key = key_for_path(kind, data[:path])
|
101
97
|
if key
|
102
|
-
|
98
|
+
data = data[:data]
|
99
|
+
Impl::Model.postprocess_item_after_deserializing!(kind, data)
|
100
|
+
@feature_store.upsert(kind, data)
|
103
101
|
break
|
104
102
|
end
|
105
103
|
end
|
@@ -112,24 +110,6 @@ module LaunchDarkly
|
|
112
110
|
break
|
113
111
|
end
|
114
112
|
end
|
115
|
-
elsif method == INDIRECT_PUT
|
116
|
-
all_data = @requestor.request_all_data
|
117
|
-
@feature_store.init({
|
118
|
-
FEATURES => all_data[:flags],
|
119
|
-
SEGMENTS => all_data[:segments]
|
120
|
-
})
|
121
|
-
@initialized.make_true
|
122
|
-
@config.logger.info { "[LDClient] Stream initialized (via indirect message)" }
|
123
|
-
elsif method == INDIRECT_PATCH
|
124
|
-
key = key_for_path(FEATURES, message.data)
|
125
|
-
if key
|
126
|
-
@feature_store.upsert(FEATURES, @requestor.request_flag(key))
|
127
|
-
else
|
128
|
-
key = key_for_path(SEGMENTS, message.data)
|
129
|
-
if key
|
130
|
-
@feature_store.upsert(SEGMENTS, @requestor.request_segment(key))
|
131
|
-
end
|
132
|
-
end
|
133
113
|
else
|
134
114
|
@config.logger.warn { "[LDClient] Unknown message received: #{method}" }
|
135
115
|
end
|
data/lib/ldclient-rb/util.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require "net/http"
|
2
1
|
require "uri"
|
2
|
+
require "http"
|
3
3
|
|
4
4
|
module LaunchDarkly
|
5
5
|
# @private
|
@@ -18,14 +18,18 @@ module LaunchDarkly
|
|
18
18
|
end
|
19
19
|
ret
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def self.new_http_client(uri_s, config)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
23
|
+
http_client_options = {}
|
24
|
+
if config.socket_factory
|
25
|
+
http_client_options["socket_class"] = config.socket_factory
|
26
|
+
end
|
27
|
+
return HTTP::Client.new(http_client_options)
|
28
|
+
.timeout({
|
29
|
+
read: config.read_timeout,
|
30
|
+
connect: config.connect_timeout
|
31
|
+
})
|
32
|
+
.persistent(uri_s)
|
29
33
|
end
|
30
34
|
|
31
35
|
def self.log_exception(logger, message, exc)
|
data/lib/ldclient-rb/version.rb
CHANGED
@@ -0,0 +1,135 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
describe "EvaluationDetail" do
|
5
|
+
subject { EvaluationDetail }
|
6
|
+
|
7
|
+
it "sets properties" do
|
8
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off).value).to eq "x"
|
9
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off).variation_index).to eq 0
|
10
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off).reason).to eq EvaluationReason::off
|
11
|
+
end
|
12
|
+
|
13
|
+
it "checks parameter types" do
|
14
|
+
expect { EvaluationDetail.new(nil, nil, EvaluationReason::off) }.not_to raise_error
|
15
|
+
expect { EvaluationDetail.new(nil, 0, EvaluationReason::off) }.not_to raise_error
|
16
|
+
expect { EvaluationDetail.new(nil, "x", EvaluationReason::off) }.to raise_error(ArgumentError)
|
17
|
+
expect { EvaluationDetail.new(nil, 0, { kind: "OFF" }) }.to raise_error(ArgumentError)
|
18
|
+
expect { EvaluationDetail.new(nil, 0, nil) }.to raise_error(ArgumentError)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "equality test" do
|
22
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).to eq EvaluationDetail.new("x", 0, EvaluationReason::off)
|
23
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("y", 0, EvaluationReason::off)
|
24
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("x", 1, EvaluationReason::off)
|
25
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("x", 0, EvaluationReason::fallthrough)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "EvaluationReason" do
|
30
|
+
subject { EvaluationReason }
|
31
|
+
|
32
|
+
values = [
|
33
|
+
[ EvaluationReason::off, EvaluationReason::OFF, { "kind" => "OFF" }, "OFF", nil ],
|
34
|
+
[ EvaluationReason::fallthrough, EvaluationReason::FALLTHROUGH,
|
35
|
+
{ "kind" => "FALLTHROUGH" }, "FALLTHROUGH", nil ],
|
36
|
+
[ EvaluationReason::target_match, EvaluationReason::TARGET_MATCH,
|
37
|
+
{ "kind" => "TARGET_MATCH" }, "TARGET_MATCH", nil ],
|
38
|
+
[ EvaluationReason::rule_match(1, "x"), EvaluationReason::RULE_MATCH,
|
39
|
+
{ "kind" => "RULE_MATCH", "ruleIndex" => 1, "ruleId" => "x" }, "RULE_MATCH(1,x)",
|
40
|
+
[ EvaluationReason::rule_match(2, "x"), EvaluationReason::rule_match(1, "y") ] ],
|
41
|
+
[ EvaluationReason::prerequisite_failed("x"), EvaluationReason::PREREQUISITE_FAILED,
|
42
|
+
{ "kind" => "PREREQUISITE_FAILED", "prerequisiteKey" => "x" }, "PREREQUISITE_FAILED(x)" ],
|
43
|
+
[ EvaluationReason::error(EvaluationReason::ERROR_FLAG_NOT_FOUND), EvaluationReason::ERROR,
|
44
|
+
{ "kind" => "ERROR", "errorKind" => "FLAG_NOT_FOUND" }, "ERROR(FLAG_NOT_FOUND)" ]
|
45
|
+
]
|
46
|
+
values.each_index do |i|
|
47
|
+
params = values[i]
|
48
|
+
reason = params[0]
|
49
|
+
kind = params[1]
|
50
|
+
json_rep = params[2]
|
51
|
+
brief_str = params[3]
|
52
|
+
unequal_values = params[4]
|
53
|
+
|
54
|
+
describe "reason #{reason.kind}" do
|
55
|
+
it "has correct kind" do
|
56
|
+
expect(reason.kind).to eq kind
|
57
|
+
end
|
58
|
+
|
59
|
+
it "equality to self" do
|
60
|
+
expect(reason).to eq reason
|
61
|
+
end
|
62
|
+
|
63
|
+
it "inequality to others" do
|
64
|
+
values.each_index do |j|
|
65
|
+
if i != j
|
66
|
+
expect(reason).not_to eq values[j][0]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
if !unequal_values.nil?
|
70
|
+
unequal_values.each do |v|
|
71
|
+
expect(reason).not_to eq v
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "JSON representation" do
|
77
|
+
expect(JSON.parse(reason.as_json.to_json)).to eq json_rep
|
78
|
+
expect(JSON.parse(reason.to_json)).to eq json_rep
|
79
|
+
end
|
80
|
+
|
81
|
+
it "brief representation" do
|
82
|
+
expect(reason.inspect).to eq brief_str
|
83
|
+
expect(reason.to_s).to eq brief_str
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it "reuses singleton reasons" do
|
89
|
+
expect(EvaluationReason::off).to be EvaluationReason::off
|
90
|
+
expect(EvaluationReason::fallthrough).to be EvaluationReason::fallthrough
|
91
|
+
expect(EvaluationReason::target_match).to be EvaluationReason::target_match
|
92
|
+
expect(EvaluationReason::rule_match(1, 'x')).not_to be EvaluationReason::rule_match(1, 'x')
|
93
|
+
expect(EvaluationReason::prerequisite_failed('x')).not_to be EvaluationReason::prerequisite_failed('x')
|
94
|
+
errors = [ EvaluationReason::ERROR_CLIENT_NOT_READY, EvaluationReason::ERROR_FLAG_NOT_FOUND,
|
95
|
+
EvaluationReason::ERROR_MALFORMED_FLAG, EvaluationReason::ERROR_USER_NOT_SPECIFIED, EvaluationReason::ERROR_EXCEPTION ]
|
96
|
+
errors.each do |e|
|
97
|
+
expect(EvaluationReason::error(e)).to be EvaluationReason::error(e)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it "supports [] with JSON property names" do
|
102
|
+
expect(EvaluationReason::off[:kind]).to eq "OFF"
|
103
|
+
expect(EvaluationReason::off[:ruleIndex]).to be nil
|
104
|
+
expect(EvaluationReason::off[:ruleId]).to be nil
|
105
|
+
expect(EvaluationReason::off[:prerequisiteKey]).to be nil
|
106
|
+
expect(EvaluationReason::off[:errorKind]).to be nil
|
107
|
+
expect(EvaluationReason::rule_match(1, "x")[:ruleIndex]).to eq 1
|
108
|
+
expect(EvaluationReason::rule_match(1, "x")[:ruleId]).to eq "x"
|
109
|
+
expect(EvaluationReason::prerequisite_failed("x")[:prerequisiteKey]).to eq "x"
|
110
|
+
expect(EvaluationReason::error(EvaluationReason::ERROR_FLAG_NOT_FOUND)[:errorKind]).to eq "FLAG_NOT_FOUND"
|
111
|
+
end
|
112
|
+
|
113
|
+
it "freezes string properties" do
|
114
|
+
rm = EvaluationReason::rule_match(1, "x")
|
115
|
+
expect { rm.rule_id.upcase! }.to raise_error(RuntimeError)
|
116
|
+
pf = EvaluationReason::prerequisite_failed("x")
|
117
|
+
expect { pf.prerequisite_key.upcase! }.to raise_error(RuntimeError)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "checks parameter types" do
|
121
|
+
expect { EvaluationReason::rule_match(nil, "x") }.to raise_error(ArgumentError)
|
122
|
+
expect { EvaluationReason::rule_match(true, "x") }.to raise_error(ArgumentError)
|
123
|
+
expect { EvaluationReason::rule_match(1, nil) }.not_to raise_error # we allow nil rule_id for backward compatibility
|
124
|
+
expect { EvaluationReason::rule_match(1, 9) }.to raise_error(ArgumentError)
|
125
|
+
expect { EvaluationReason::prerequisite_failed(nil) }.to raise_error(ArgumentError)
|
126
|
+
expect { EvaluationReason::prerequisite_failed(9) }.to raise_error(ArgumentError)
|
127
|
+
expect { EvaluationReason::error(nil) }.to raise_error(ArgumentError)
|
128
|
+
expect { EvaluationReason::error(9) }.to raise_error(ArgumentError)
|
129
|
+
end
|
130
|
+
|
131
|
+
it "does not allow direct access to constructor" do
|
132
|
+
expect { EvaluationReason.new(:off, nil, nil, nil, nil) }.to raise_error(NoMethodError)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/spec/event_sender_spec.rb
CHANGED
@@ -39,12 +39,29 @@ module LaunchDarkly
|
|
39
39
|
"authorization" => [ sdk_key ],
|
40
40
|
"content-type" => [ "application/json" ],
|
41
41
|
"user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
|
42
|
-
"x-launchdarkly-event-schema" => [ "3" ]
|
42
|
+
"x-launchdarkly-event-schema" => [ "3" ],
|
43
|
+
"connection" => [ "Keep-Alive" ]
|
43
44
|
})
|
44
45
|
expect(req.header['x-launchdarkly-payload-id']).not_to eq []
|
45
46
|
end
|
46
47
|
end
|
47
|
-
|
48
|
+
|
49
|
+
it "can use a socket factory" do
|
50
|
+
with_server do |server|
|
51
|
+
server.setup_ok_response("/bulk", "")
|
52
|
+
|
53
|
+
config = Config.new(events_uri: "http://events.com/bulk", socket_factory: SocketFactoryFromHash.new({"events.com" => server.port}), logger: $null_log)
|
54
|
+
es = subject.new(sdk_key, config, nil, 0.1)
|
55
|
+
|
56
|
+
result = es.send_event_data(fake_data, "", false)
|
57
|
+
|
58
|
+
expect(result.success).to be true
|
59
|
+
req = server.await_request
|
60
|
+
expect(req.body).to eq fake_data
|
61
|
+
expect(req.host).to eq "events.com"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
48
65
|
it "generates a new payload ID for each payload" do
|
49
66
|
with_sender_and_server do |es, server|
|
50
67
|
server.setup_ok_response("/bulk", "")
|
@@ -78,6 +95,7 @@ module LaunchDarkly
|
|
78
95
|
"authorization" => [ sdk_key ],
|
79
96
|
"content-type" => [ "application/json" ],
|
80
97
|
"user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
|
98
|
+
"connection" => [ "Keep-Alive" ]
|
81
99
|
})
|
82
100
|
expect(req.header['x-launchdarkly-event-schema']).to eq []
|
83
101
|
expect(req.header['x-launchdarkly-payload-id']).to eq []
|
data/spec/events_spec.rb
CHANGED
@@ -408,6 +408,16 @@ describe LaunchDarkly::EventProcessor do
|
|
408
408
|
end
|
409
409
|
end
|
410
410
|
|
411
|
+
it "queues alias event" do
|
412
|
+
with_processor_and_sender(default_config) do |ep, sender|
|
413
|
+
e = { kind: "alias", key: "a", contextKind: "user", previousKey: "b", previousContextKind: "user" }
|
414
|
+
ep.add_event(e)
|
415
|
+
|
416
|
+
output = flush_and_get_events(ep, sender)
|
417
|
+
expect(output).to contain_exactly(e)
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
411
421
|
it "treats nil value for custom the same as an empty hash" do
|
412
422
|
with_processor_and_sender(default_config) do |ep, sender|
|
413
423
|
user_with_nil_custom = { key: "userkey", custom: nil }
|
data/spec/http_util.rb
CHANGED
@@ -3,7 +3,7 @@ require "webrick/httpproxy"
|
|
3
3
|
require "webrick/https"
|
4
4
|
|
5
5
|
class StubHTTPServer
|
6
|
-
attr_reader :requests
|
6
|
+
attr_reader :requests, :port
|
7
7
|
|
8
8
|
@@next_port = 50000
|
9
9
|
|
@@ -120,3 +120,13 @@ def with_server(server = nil)
|
|
120
120
|
server.stop
|
121
121
|
end
|
122
122
|
end
|
123
|
+
|
124
|
+
class SocketFactoryFromHash
|
125
|
+
def initialize(ports = {})
|
126
|
+
@ports = ports
|
127
|
+
end
|
128
|
+
|
129
|
+
def open(uri, timeout)
|
130
|
+
TCPSocket.new 'localhost', @ports[uri]
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe LaunchDarkly::Impl::EvaluatorBucketing do
|
4
|
+
subject { LaunchDarkly::Impl::EvaluatorBucketing }
|
5
|
+
|
6
|
+
describe "bucket_user" do
|
7
|
+
it "gets expected bucket values for specific keys" do
|
8
|
+
user = { key: "userKeyA" }
|
9
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
10
|
+
expect(bucket).to be_within(0.0000001).of(0.42157587);
|
11
|
+
|
12
|
+
user = { key: "userKeyB" }
|
13
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
14
|
+
expect(bucket).to be_within(0.0000001).of(0.6708485);
|
15
|
+
|
16
|
+
user = { key: "userKeyC" }
|
17
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
18
|
+
expect(bucket).to be_within(0.0000001).of(0.10343106);
|
19
|
+
end
|
20
|
+
|
21
|
+
it "can bucket by int value (equivalent to string)" do
|
22
|
+
user = {
|
23
|
+
key: "userkey",
|
24
|
+
custom: {
|
25
|
+
stringAttr: "33333",
|
26
|
+
intAttr: 33333
|
27
|
+
}
|
28
|
+
}
|
29
|
+
stringResult = subject.bucket_user(user, "hashKey", "stringAttr", "saltyA")
|
30
|
+
intResult = subject.bucket_user(user, "hashKey", "intAttr", "saltyA")
|
31
|
+
|
32
|
+
expect(intResult).to be_within(0.0000001).of(0.54771423)
|
33
|
+
expect(intResult).to eq(stringResult)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "cannot bucket by float value" do
|
37
|
+
user = {
|
38
|
+
key: "userkey",
|
39
|
+
custom: {
|
40
|
+
floatAttr: 33.5
|
41
|
+
}
|
42
|
+
}
|
43
|
+
result = subject.bucket_user(user, "hashKey", "floatAttr", "saltyA")
|
44
|
+
expect(result).to eq(0.0)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
it "cannot bucket by bool value" do
|
49
|
+
user = {
|
50
|
+
key: "userkey",
|
51
|
+
custom: {
|
52
|
+
boolAttr: true
|
53
|
+
}
|
54
|
+
}
|
55
|
+
result = subject.bucket_user(user, "hashKey", "boolAttr", "saltyA")
|
56
|
+
expect(result).to eq(0.0)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "variation_index_for_user" do
|
61
|
+
it "matches bucket" do
|
62
|
+
user = { key: "userkey" }
|
63
|
+
flag_key = "flagkey"
|
64
|
+
salt = "salt"
|
65
|
+
|
66
|
+
# First verify that with our test inputs, the bucket value will be greater than zero and less than 100000,
|
67
|
+
# so we can construct a rollout whose second bucket just barely contains that value
|
68
|
+
bucket_value = (subject.bucket_user(user, flag_key, "key", salt) * 100000).truncate()
|
69
|
+
expect(bucket_value).to be > 0
|
70
|
+
expect(bucket_value).to be < 100000
|
71
|
+
|
72
|
+
bad_variation_a = 0
|
73
|
+
matched_variation = 1
|
74
|
+
bad_variation_b = 2
|
75
|
+
rule = {
|
76
|
+
rollout: {
|
77
|
+
variations: [
|
78
|
+
{ variation: bad_variation_a, weight: bucket_value }, # end of bucket range is not inclusive, so it will *not* match the target value
|
79
|
+
{ variation: matched_variation, weight: 1 }, # size of this bucket is 1, so it only matches that specific value
|
80
|
+
{ variation: bad_variation_b, weight: 100000 - (bucket_value + 1) }
|
81
|
+
]
|
82
|
+
}
|
83
|
+
}
|
84
|
+
flag = { key: flag_key, salt: salt }
|
85
|
+
|
86
|
+
result_variation = subject.variation_index_for_user(flag, rule, user)
|
87
|
+
expect(result_variation).to be matched_variation
|
88
|
+
end
|
89
|
+
|
90
|
+
it "uses last bucket if bucket value is equal to total weight" do
|
91
|
+
user = { key: "userkey" }
|
92
|
+
flag_key = "flagkey"
|
93
|
+
salt = "salt"
|
94
|
+
|
95
|
+
bucket_value = (subject.bucket_user(user, flag_key, "key", salt) * 100000).truncate()
|
96
|
+
|
97
|
+
# We'll construct a list of variations that stops right at the target bucket value
|
98
|
+
rule = {
|
99
|
+
rollout: {
|
100
|
+
variations: [
|
101
|
+
{ variation: 0, weight: bucket_value }
|
102
|
+
]
|
103
|
+
}
|
104
|
+
}
|
105
|
+
flag = { key: flag_key, salt: salt }
|
106
|
+
|
107
|
+
result_variation = subject.variation_index_for_user(flag, rule, user)
|
108
|
+
expect(result_variation).to be 0
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|