launchdarkly-server-sdk 6.1.1 → 6.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +4 -1
- data/.ldrelease/circleci/template/build.sh +2 -12
- data/.ldrelease/circleci/template/gems-setup.sh +16 -0
- data/.ldrelease/circleci/template/prepare.sh +17 -0
- data/.ldrelease/circleci/template/publish.sh +5 -9
- data/.ldrelease/circleci/template/test.sh +2 -2
- data/CHANGELOG.md +20 -0
- data/launchdarkly-server-sdk.gemspec +4 -6
- data/lib/ldclient-rb/evaluation_detail.rb +38 -7
- data/lib/ldclient-rb/impl/diagnostic_events.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +8 -2
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +22 -9
- data/lib/ldclient-rb/impl/event_factory.rb +6 -0
- data/lib/ldclient-rb/ldclient.rb +6 -0
- data/lib/ldclient-rb/requestor.rb +1 -1
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/diagnostic_events_spec.rb +9 -7
- data/spec/impl/evaluator_bucketing_spec.rb +131 -26
- data/spec/impl/evaluator_rule_spec.rb +32 -0
- data/spec/impl/evaluator_spec.rb +44 -0
- data/spec/impl/event_factory_spec.rb +108 -0
- data/spec/ldclient_spec.rb +3 -11
- data/spec/requestor_spec.rb +13 -0
- metadata +26 -17
- data/.ldrelease/circleci/template/set-gem-home.sh +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 326e10ac69bf7d73c5727c555a2981b7d271d1651d9521b976005d93646a188b
|
4
|
+
data.tar.gz: 608205f6ac545e410aaa9e507180b8ad097ebe1ed23c6dc011b2c522209d9e3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1b648a1d796ad1b65feffd9ee83fcd3dcb63c3146d513e44173d5cb6da32fd29b993d8b7343957094b801b69da5c0873b458fb568e6d174e4c402e158d129c4
|
7
|
+
data.tar.gz: 433d7761a4386538c25a8db6a0520b7d1e8c7184dcc9967e9770bbf5639e3f0a74476f2a0ef20f50bf53a5c6da4df62aacfd444f5fad5cfb1d64bc73a113374f
|
data/.circleci/config.yml
CHANGED
@@ -20,10 +20,13 @@ jobs:
|
|
20
20
|
LD_RELEASE_DOCS_TITLE: ""
|
21
21
|
LD_RELEASE_PROJECT: "ruby-server-sdk"
|
22
22
|
LD_RELEASE_PROJECT_TEMPLATE: "ruby"
|
23
|
-
LD_RELEASE_VERSION: "6.
|
23
|
+
LD_RELEASE_VERSION: "6.2.4"
|
24
24
|
LD_SKIP_DATABASE_TESTS: "1"
|
25
25
|
steps:
|
26
26
|
- checkout
|
27
|
+
- run:
|
28
|
+
name: "Releaser: prepare"
|
29
|
+
command: .ldrelease/circleci/mac/execute.sh prepare .ldrelease/circleci/template/prepare.sh
|
27
30
|
- run:
|
28
31
|
name: "Releaser: build"
|
29
32
|
command: .ldrelease/circleci/mac/execute.sh build .ldrelease/circleci/template/build.sh
|
@@ -7,20 +7,10 @@ set -ue
|
|
7
7
|
echo "Using gem $(gem --version)"
|
8
8
|
|
9
9
|
#shellcheck source=/dev/null
|
10
|
-
source "$(dirname "$0")/
|
11
|
-
|
12
|
-
# If the gemspec specifies a certain version of bundler, we need to make sure we install that version.
|
13
|
-
echo "Installing bundler"
|
14
|
-
GEMSPEC_BUNDLER_VERSION=$(sed -n -e "s/.*['\"]bundler['\"], *['\"]\([^'\"]*\)['\"]/\1/p" ./*.gemspec | tr -d ' ')
|
15
|
-
if [ -n "${GEMSPEC_BUNDLER_VERSION}" ]; then
|
16
|
-
GEMSPEC_OPTIONS="-v ${GEMSPEC_BUNDLER_VERSION}"
|
17
|
-
else
|
18
|
-
GEMSPEC_OPTIONS=""
|
19
|
-
fi
|
20
|
-
gem install bundler ${GEMSPEC_OPTIONS} || { echo "installing bundler failed" >&2; exit 1; }
|
10
|
+
source "$(dirname "$0")/gems-setup.sh"
|
21
11
|
|
22
12
|
echo; echo "Installing dependencies"
|
23
|
-
|
13
|
+
${BUNDLER_COMMAND} install
|
24
14
|
|
25
15
|
# Build Ruby Gem - this assumes there is a single .gemspec file in the main project directory
|
26
16
|
# Note that the gemspec must be able to get the project version either from $LD_RELEASE_VERSION,
|
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
# helper script to set GEM_HOME, PATH, and BUNDLER_COMMAND for Ruby - must be sourced, not executed
|
4
|
+
|
5
|
+
mkdir -p "${LD_RELEASE_TEMP_DIR}/gems"
|
6
|
+
export GEM_HOME="${LD_RELEASE_TEMP_DIR}/gems"
|
7
|
+
export PATH="${GEM_HOME}/bin:${PATH}"
|
8
|
+
|
9
|
+
# also, determine whether we'll need to run a specific version of Bundler
|
10
|
+
|
11
|
+
GEMSPEC_BUNDLER_VERSION=$(sed -n -e "s/.*['\"]bundler['\"], *['\"]\([^'\"]*\)['\"]/\1/p" ./*.gemspec | tr -d ' ')
|
12
|
+
if [ -n "${GEMSPEC_BUNDLER_VERSION}" ]; then
|
13
|
+
BUNDLER_COMMAND="bundler _${GEMSPEC_BUNDLER_VERSION}_"
|
14
|
+
else
|
15
|
+
BUNDLER_COMMAND="bundler"
|
16
|
+
fi
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -ue
|
4
|
+
|
5
|
+
echo "Using gem $(gem --version)"
|
6
|
+
|
7
|
+
#shellcheck source=/dev/null
|
8
|
+
source "$(dirname "$0")/gems-setup.sh"
|
9
|
+
|
10
|
+
# If the gemspec specifies a certain version of bundler, we need to make sure we install that version.
|
11
|
+
echo "Installing bundler"
|
12
|
+
if [ -n "${GEMSPEC_BUNDLER_VERSION:-}" ]; then
|
13
|
+
GEMSPEC_OPTIONS="-v ${GEMSPEC_BUNDLER_VERSION}"
|
14
|
+
else
|
15
|
+
GEMSPEC_OPTIONS=""
|
16
|
+
fi
|
17
|
+
gem install bundler ${GEMSPEC_OPTIONS} || { echo "installing bundler failed" >&2; exit 1; }
|
@@ -4,17 +4,13 @@ set -ue
|
|
4
4
|
|
5
5
|
# Standard publish.sh for Ruby-based projects - we can assume build.sh has already been run
|
6
6
|
|
7
|
-
|
7
|
+
#shellcheck source=/dev/null
|
8
|
+
source "$(dirname "$0")/gems-setup.sh"
|
8
9
|
|
9
10
|
# If we're running in CircleCI, the RubyGems credentials will be in an environment
|
10
|
-
# variable and
|
11
|
-
if [ -n "${LD_RELEASE_RUBYGEMS_API_KEY}" ]; then
|
12
|
-
|
13
|
-
cat >~/.gem/credentials <<EOF
|
14
|
-
---
|
15
|
-
:rubygems_api_key: $LD_RELEASE_RUBYGEMS_API_KEY
|
16
|
-
EOF
|
17
|
-
chmod 0600 ~/.gem/credentials
|
11
|
+
# variable and should be copied to the variable the gem command expects
|
12
|
+
if [ -n "${LD_RELEASE_RUBYGEMS_API_KEY:-}" ]; then
|
13
|
+
export GEM_HOST_API_KEY="${LD_RELEASE_RUBYGEMS_API_KEY}"
|
18
14
|
fi
|
19
15
|
|
20
16
|
# Since all Releaser builds are clean builds, we can assume that the only .gem file here
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,26 @@
|
|
2
2
|
|
3
3
|
All notable changes to the LaunchDarkly Ruby SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
|
4
4
|
|
5
|
+
## [6.2.3] - 2021-08-06
|
6
|
+
### Fixed:
|
7
|
+
- Diagnostic events did not properly set the `usingProxy` attribute when a proxy was configured with the `HTTPS_PROXY` environment variable. ([#182](https://github.com/launchdarkly/ruby-server-sdk/issues/182))
|
8
|
+
|
9
|
+
## [6.2.2] - 2021-07-23
|
10
|
+
### Fixed:
|
11
|
+
- Enabling debug logging in polling mode could cause polling to fail with a `NameError`. (Thanks, [mmurphy-notarize](https://github.com/launchdarkly/ruby-server-sdk/pull/180)!)
|
12
|
+
|
13
|
+
## [6.2.1] - 2021-07-15
|
14
|
+
### Changed:
|
15
|
+
- If `variation` or `variation_detail` is called with a user object that has no `key` (an invalid condition that will always result in the default value being returned), the SDK now logs a `warn`-level message to alert you to this incorrect usage. This makes the Ruby SDK's logging behavior consistent with the other server-side LaunchDarkly SDKs. ([#177](https://github.com/launchdarkly/ruby-server-sdk/issues/177))
|
16
|
+
|
17
|
+
## [6.2.0] - 2021-06-17
|
18
|
+
### Added:
|
19
|
+
- The SDK now supports the ability to control the proportion of traffic allocation to an experiment. This works in conjunction with a new platform feature now available to early access customers.
|
20
|
+
|
21
|
+
## [6.1.1] - 2021-05-27
|
22
|
+
### Fixed:
|
23
|
+
- Calling `variation` with a nil user parameter is invalid, causing the SDK to log an error and return a fallback value, but the SDK was still sending an analytics event for this. An event without a user is meaningless and can't be processed by LaunchDarkly. This is now fixed so the SDK will not send one.
|
24
|
+
|
5
25
|
## [6.1.0] - 2021-02-04
|
6
26
|
### Added:
|
7
27
|
- Added the `alias` method. This can be used to associate two user objects for analytics purposes by generating an alias event.
|
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.required_ruby_version = ">= 2.5.0"
|
23
23
|
|
24
24
|
spec.add_development_dependency "aws-sdk-dynamodb", "~> 1.57"
|
25
|
-
spec.add_development_dependency "bundler", "
|
25
|
+
spec.add_development_dependency "bundler", "2.2.10"
|
26
26
|
spec.add_development_dependency "rspec", "~> 3.10"
|
27
27
|
spec.add_development_dependency "diplomat", "~> 2.4.2"
|
28
28
|
spec.add_development_dependency "redis", "~> 4.2"
|
@@ -36,10 +36,8 @@ Gem::Specification.new do |spec|
|
|
36
36
|
|
37
37
|
spec.add_runtime_dependency "semantic", "~> 1.6"
|
38
38
|
spec.add_runtime_dependency "concurrent-ruby", "~> 1.1"
|
39
|
-
spec.add_runtime_dependency "ld-eventsource", "
|
39
|
+
spec.add_runtime_dependency "ld-eventsource", "2.0.1"
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
spec.add_runtime_dependency "json", "~> 2.3.1"
|
44
|
-
spec.add_runtime_dependency "http", "~> 4.4.1"
|
41
|
+
spec.add_runtime_dependency "json", "~> 2.3"
|
42
|
+
spec.add_runtime_dependency "http", ">= 4.4.0", "< 6.0.0"
|
45
43
|
end
|
@@ -120,6 +120,9 @@ module LaunchDarkly
|
|
120
120
|
# or deleted. If {#kind} is not {#RULE_MATCH}, this will be `nil`.
|
121
121
|
attr_reader :rule_id
|
122
122
|
|
123
|
+
# A boolean or nil value representing if the rule or fallthrough has an experiment rollout.
|
124
|
+
attr_reader :in_experiment
|
125
|
+
|
123
126
|
# The key of the prerequisite flag that did not return the desired variation. If {#kind} is not
|
124
127
|
# {#PREREQUISITE_FAILED}, this will be `nil`.
|
125
128
|
attr_reader :prerequisite_key
|
@@ -136,8 +139,12 @@ module LaunchDarkly
|
|
136
139
|
|
137
140
|
# Returns an instance whose {#kind} is {#FALLTHROUGH}.
|
138
141
|
# @return [EvaluationReason]
|
139
|
-
def self.fallthrough
|
140
|
-
|
142
|
+
def self.fallthrough(in_experiment=false)
|
143
|
+
if in_experiment
|
144
|
+
@@fallthrough_with_experiment
|
145
|
+
else
|
146
|
+
@@fallthrough
|
147
|
+
end
|
141
148
|
end
|
142
149
|
|
143
150
|
# Returns an instance whose {#kind} is {#TARGET_MATCH}.
|
@@ -153,10 +160,16 @@ module LaunchDarkly
|
|
153
160
|
# @param rule_id [String] unique string identifier for the matched rule
|
154
161
|
# @return [EvaluationReason]
|
155
162
|
# @raise [ArgumentError] if `rule_index` is not a number or `rule_id` is not a string
|
156
|
-
def self.rule_match(rule_index, rule_id)
|
163
|
+
def self.rule_match(rule_index, rule_id, in_experiment=false)
|
157
164
|
raise ArgumentError.new("rule_index must be a number") if !(rule_index.is_a? Numeric)
|
158
165
|
raise ArgumentError.new("rule_id must be a string") if !rule_id.nil? && !(rule_id.is_a? String) # in test data, ID could be nil
|
159
|
-
|
166
|
+
|
167
|
+
if in_experiment
|
168
|
+
er = new(:RULE_MATCH, rule_index, rule_id, nil, nil, true)
|
169
|
+
else
|
170
|
+
er = new(:RULE_MATCH, rule_index, rule_id, nil, nil)
|
171
|
+
end
|
172
|
+
er
|
160
173
|
end
|
161
174
|
|
162
175
|
# Returns an instance whose {#kind} is {#PREREQUISITE_FAILED}.
|
@@ -204,11 +217,17 @@ module LaunchDarkly
|
|
204
217
|
def inspect
|
205
218
|
case @kind
|
206
219
|
when :RULE_MATCH
|
207
|
-
|
220
|
+
if @in_experiment
|
221
|
+
"RULE_MATCH(#{@rule_index},#{@rule_id},#{@in_experiment})"
|
222
|
+
else
|
223
|
+
"RULE_MATCH(#{@rule_index},#{@rule_id})"
|
224
|
+
end
|
208
225
|
when :PREREQUISITE_FAILED
|
209
226
|
"PREREQUISITE_FAILED(#{@prerequisite_key})"
|
210
227
|
when :ERROR
|
211
228
|
"ERROR(#{@error_kind})"
|
229
|
+
when :FALLTHROUGH
|
230
|
+
@in_experiment ? "FALLTHROUGH(#{@in_experiment})" : @kind.to_s
|
212
231
|
else
|
213
232
|
@kind.to_s
|
214
233
|
end
|
@@ -225,11 +244,21 @@ module LaunchDarkly
|
|
225
244
|
# as_json and then modify the result.
|
226
245
|
case @kind
|
227
246
|
when :RULE_MATCH
|
228
|
-
|
247
|
+
if @in_experiment
|
248
|
+
{ kind: @kind, ruleIndex: @rule_index, ruleId: @rule_id, inExperiment: @in_experiment }
|
249
|
+
else
|
250
|
+
{ kind: @kind, ruleIndex: @rule_index, ruleId: @rule_id }
|
251
|
+
end
|
229
252
|
when :PREREQUISITE_FAILED
|
230
253
|
{ kind: @kind, prerequisiteKey: @prerequisite_key }
|
231
254
|
when :ERROR
|
232
255
|
{ kind: @kind, errorKind: @error_kind }
|
256
|
+
when :FALLTHROUGH
|
257
|
+
if @in_experiment
|
258
|
+
{ kind: @kind, inExperiment: @in_experiment }
|
259
|
+
else
|
260
|
+
{ kind: @kind }
|
261
|
+
end
|
233
262
|
else
|
234
263
|
{ kind: @kind }
|
235
264
|
end
|
@@ -263,7 +292,7 @@ module LaunchDarkly
|
|
263
292
|
|
264
293
|
private
|
265
294
|
|
266
|
-
def initialize(kind, rule_index, rule_id, prerequisite_key, error_kind)
|
295
|
+
def initialize(kind, rule_index, rule_id, prerequisite_key, error_kind, in_experiment=nil)
|
267
296
|
@kind = kind.to_sym
|
268
297
|
@rule_index = rule_index
|
269
298
|
@rule_id = rule_id
|
@@ -271,6 +300,7 @@ module LaunchDarkly
|
|
271
300
|
@prerequisite_key = prerequisite_key
|
272
301
|
@prerequisite_key.freeze if !prerequisite_key.nil?
|
273
302
|
@error_kind = error_kind
|
303
|
+
@in_experiment = in_experiment
|
274
304
|
end
|
275
305
|
|
276
306
|
private_class_method :new
|
@@ -279,6 +309,7 @@ module LaunchDarkly
|
|
279
309
|
new(:ERROR, nil, nil, nil, error_kind)
|
280
310
|
end
|
281
311
|
|
312
|
+
@@fallthrough_with_experiment = new(:FALLTHROUGH, nil, nil, nil, nil, true)
|
282
313
|
@@fallthrough = new(:FALLTHROUGH, nil, nil, nil, nil)
|
283
314
|
@@off = new(:OFF, nil, nil, nil, nil)
|
284
315
|
@@target_match = new(:TARGET_MATCH, nil, nil, nil, nil)
|
@@ -79,7 +79,7 @@ module LaunchDarkly
|
|
79
79
|
streamingDisabled: !config.stream?,
|
80
80
|
userKeysCapacity: config.user_keys_capacity,
|
81
81
|
userKeysFlushIntervalMillis: self.seconds_to_millis(config.user_keys_flush_interval),
|
82
|
-
usingProxy: ENV.has_key?('http_proxy') || ENV.has_key?('https_proxy') || ENV.has_key?('HTTP_PROXY'),
|
82
|
+
usingProxy: ENV.has_key?('http_proxy') || ENV.has_key?('https_proxy') || ENV.has_key?('HTTP_PROXY') || ENV.has_key?('HTTPS_PROXY'),
|
83
83
|
usingRelayDaemon: config.use_ldd?,
|
84
84
|
}
|
85
85
|
ret
|
@@ -190,7 +190,7 @@ module LaunchDarkly
|
|
190
190
|
return true if !rule[:weight]
|
191
191
|
|
192
192
|
# All of the clauses are met. See if the user buckets in
|
193
|
-
bucket = EvaluatorBucketing.bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt)
|
193
|
+
bucket = EvaluatorBucketing.bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt, nil)
|
194
194
|
weight = rule[:weight].to_f / 100000.0
|
195
195
|
return bucket < weight
|
196
196
|
end
|
@@ -213,7 +213,13 @@ module LaunchDarkly
|
|
213
213
|
end
|
214
214
|
|
215
215
|
def get_value_for_variation_or_rollout(flag, vr, user, reason)
|
216
|
-
index = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
|
216
|
+
index, in_experiment = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
|
217
|
+
#if in experiment is true, set reason to a different reason instance/singleton with in_experiment set
|
218
|
+
if in_experiment && reason.kind == :FALLTHROUGH
|
219
|
+
reason = EvaluationReason::fallthrough(in_experiment)
|
220
|
+
elsif in_experiment && reason.kind == :RULE_MATCH
|
221
|
+
reason = EvaluationReason::rule_match(reason.rule_index, reason.rule_id, in_experiment)
|
222
|
+
end
|
217
223
|
if index.nil?
|
218
224
|
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
|
219
225
|
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
@@ -10,20 +10,26 @@ module LaunchDarkly
|
|
10
10
|
# @param user [Object] the user properties
|
11
11
|
# @return [Number] the variation index, or nil if there is an error
|
12
12
|
def self.variation_index_for_user(flag, rule, user)
|
13
|
+
|
13
14
|
variation = rule[:variation]
|
14
|
-
return variation if !variation.nil? # fixed variation
|
15
|
+
return variation, false if !variation.nil? # fixed variation
|
15
16
|
rollout = rule[:rollout]
|
16
|
-
return nil if rollout.nil?
|
17
|
+
return nil, false if rollout.nil?
|
17
18
|
variations = rollout[:variations]
|
18
19
|
if !variations.nil? && variations.length > 0 # percentage rollout
|
19
|
-
rollout = rule[:rollout]
|
20
20
|
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
|
21
|
-
|
21
|
+
|
22
|
+
seed = rollout[:seed]
|
23
|
+
bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt], seed) # may not be present
|
22
24
|
sum = 0;
|
23
25
|
variations.each do |variate|
|
26
|
+
if rollout[:kind] == "experiment" && !variate[:untracked]
|
27
|
+
in_experiment = true
|
28
|
+
end
|
29
|
+
|
24
30
|
sum += variate[:weight].to_f / 100000.0
|
25
31
|
if bucket < sum
|
26
|
-
return variate[:variation]
|
32
|
+
return variate[:variation], !!in_experiment
|
27
33
|
end
|
28
34
|
end
|
29
35
|
# The user's bucket value was greater than or equal to the end of the last bucket. This could happen due
|
@@ -31,9 +37,12 @@ module LaunchDarkly
|
|
31
37
|
# data could contain buckets that don't actually add up to 100000. Rather than returning an error in
|
32
38
|
# this case (or changing the scaling, which would potentially change the results for *all* users), we
|
33
39
|
# will simply put the user in the last bucket.
|
34
|
-
variations[-1]
|
40
|
+
last_variation = variations[-1]
|
41
|
+
in_experiment = rollout[:kind] == "experiment" && !last_variation[:untracked]
|
42
|
+
|
43
|
+
[last_variation[:variation], in_experiment]
|
35
44
|
else # the rule isn't well-formed
|
36
|
-
nil
|
45
|
+
[nil, false]
|
37
46
|
end
|
38
47
|
end
|
39
48
|
|
@@ -44,7 +53,7 @@ module LaunchDarkly
|
|
44
53
|
# @param bucket_by [String|Symbol] the name of the user attribute to be used for bucketing
|
45
54
|
# @param salt [String] the feature flag's or segment's salt value
|
46
55
|
# @return [Number] the bucket value, from 0 inclusive to 1 exclusive
|
47
|
-
def self.bucket_user(user, key, bucket_by, salt)
|
56
|
+
def self.bucket_user(user, key, bucket_by, salt, seed)
|
48
57
|
return nil unless user[:key]
|
49
58
|
|
50
59
|
id_hash = bucketable_string_value(EvaluatorOperators.user_value(user, bucket_by))
|
@@ -56,7 +65,11 @@ module LaunchDarkly
|
|
56
65
|
id_hash += "." + user[:secondary].to_s
|
57
66
|
end
|
58
67
|
|
59
|
-
|
68
|
+
if seed
|
69
|
+
hash_key = "%d.%s" % [seed, id_hash]
|
70
|
+
else
|
71
|
+
hash_key = "%s.%s.%s" % [key, salt, id_hash]
|
72
|
+
end
|
60
73
|
|
61
74
|
hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
|
62
75
|
hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
|
@@ -103,6 +103,11 @@ module LaunchDarkly
|
|
103
103
|
|
104
104
|
def is_experiment(flag, reason)
|
105
105
|
return false if !reason
|
106
|
+
|
107
|
+
if reason.in_experiment
|
108
|
+
return true
|
109
|
+
end
|
110
|
+
|
106
111
|
case reason[:kind]
|
107
112
|
when 'RULE_MATCH'
|
108
113
|
index = reason[:ruleIndex]
|
@@ -115,6 +120,7 @@ module LaunchDarkly
|
|
115
120
|
end
|
116
121
|
false
|
117
122
|
end
|
123
|
+
|
118
124
|
end
|
119
125
|
end
|
120
126
|
end
|
data/lib/ldclient-rb/ldclient.rb
CHANGED
@@ -407,6 +407,12 @@ module LaunchDarkly
|
|
407
407
|
return detail
|
408
408
|
end
|
409
409
|
|
410
|
+
if user[:key].nil?
|
411
|
+
@config.logger.warn { "[LDClient] Variation called with nil user key; returning default value" }
|
412
|
+
detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
|
413
|
+
return detail
|
414
|
+
end
|
415
|
+
|
410
416
|
if !initialized?
|
411
417
|
if @store.initialized?
|
412
418
|
@config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
|
@@ -60,9 +60,9 @@ module LaunchDarkly
|
|
60
60
|
headers: headers
|
61
61
|
})
|
62
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
63
|
# must fully read body for persistent connections
|
65
64
|
body = response.to_s
|
65
|
+
@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{response.headers.to_h}\n\tbody: #{body}" }
|
66
66
|
if status == 304 && !cached.nil?
|
67
67
|
body = cached.body
|
68
68
|
else
|
data/lib/ldclient-rb/version.rb
CHANGED
@@ -79,13 +79,15 @@ module LaunchDarkly
|
|
79
79
|
end
|
80
80
|
end
|
81
81
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
82
|
+
['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY'].each do |name|
|
83
|
+
it "detects proxy #{name}" do
|
84
|
+
begin
|
85
|
+
ENV[name] = 'http://my-proxy'
|
86
|
+
event = default_acc.create_init_event(Config.new)
|
87
|
+
expect(event[:configuration][:usingProxy]).to be true
|
88
|
+
ensure
|
89
|
+
ENV[name] = nil
|
90
|
+
end
|
89
91
|
end
|
90
92
|
end
|
91
93
|
|
@@ -4,17 +4,58 @@ describe LaunchDarkly::Impl::EvaluatorBucketing do
|
|
4
4
|
subject { LaunchDarkly::Impl::EvaluatorBucketing }
|
5
5
|
|
6
6
|
describe "bucket_user" do
|
7
|
+
describe "seed exists" do
|
8
|
+
let(:seed) { 61 }
|
9
|
+
it "returns the expected bucket values for seed" do
|
10
|
+
user = { key: "userKeyA" }
|
11
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", seed)
|
12
|
+
expect(bucket).to be_within(0.0000001).of(0.09801207);
|
13
|
+
|
14
|
+
user = { key: "userKeyB" }
|
15
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", seed)
|
16
|
+
expect(bucket).to be_within(0.0000001).of(0.14483777);
|
17
|
+
|
18
|
+
user = { key: "userKeyC" }
|
19
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", seed)
|
20
|
+
expect(bucket).to be_within(0.0000001).of(0.9242641);
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns the same bucket regardless of hashKey and salt" do
|
24
|
+
user = { key: "userKeyA" }
|
25
|
+
bucket1 = subject.bucket_user(user, "hashKey", "key", "saltyA", seed)
|
26
|
+
bucket2 = subject.bucket_user(user, "hashKey1", "key", "saltyB", seed)
|
27
|
+
bucket3 = subject.bucket_user(user, "hashKey2", "key", "saltyC", seed)
|
28
|
+
expect(bucket1).to eq(bucket2)
|
29
|
+
expect(bucket2).to eq(bucket3)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "returns a different bucket if the seed is not the same" do
|
33
|
+
user = { key: "userKeyA" }
|
34
|
+
bucket1 = subject.bucket_user(user, "hashKey", "key", "saltyA", seed)
|
35
|
+
bucket2 = subject.bucket_user(user, "hashKey1", "key", "saltyB", seed+1)
|
36
|
+
expect(bucket1).to_not eq(bucket2)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns a different bucket if the user is not the same" do
|
40
|
+
user1 = { key: "userKeyA" }
|
41
|
+
user2 = { key: "userKeyB" }
|
42
|
+
bucket1 = subject.bucket_user(user1, "hashKey", "key", "saltyA", seed)
|
43
|
+
bucket2 = subject.bucket_user(user2, "hashKey1", "key", "saltyB", seed)
|
44
|
+
expect(bucket1).to_not eq(bucket2)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
7
48
|
it "gets expected bucket values for specific keys" do
|
8
49
|
user = { key: "userKeyA" }
|
9
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
50
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", nil)
|
10
51
|
expect(bucket).to be_within(0.0000001).of(0.42157587);
|
11
52
|
|
12
53
|
user = { key: "userKeyB" }
|
13
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
54
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", nil)
|
14
55
|
expect(bucket).to be_within(0.0000001).of(0.6708485);
|
15
56
|
|
16
57
|
user = { key: "userKeyC" }
|
17
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
58
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", nil)
|
18
59
|
expect(bucket).to be_within(0.0000001).of(0.10343106);
|
19
60
|
end
|
20
61
|
|
@@ -26,8 +67,8 @@ describe LaunchDarkly::Impl::EvaluatorBucketing do
|
|
26
67
|
intAttr: 33333
|
27
68
|
}
|
28
69
|
}
|
29
|
-
stringResult = subject.bucket_user(user, "hashKey", "stringAttr", "saltyA")
|
30
|
-
intResult = subject.bucket_user(user, "hashKey", "intAttr", "saltyA")
|
70
|
+
stringResult = subject.bucket_user(user, "hashKey", "stringAttr", "saltyA", nil)
|
71
|
+
intResult = subject.bucket_user(user, "hashKey", "intAttr", "saltyA", nil)
|
31
72
|
|
32
73
|
expect(intResult).to be_within(0.0000001).of(0.54771423)
|
33
74
|
expect(intResult).to eq(stringResult)
|
@@ -40,7 +81,7 @@ describe LaunchDarkly::Impl::EvaluatorBucketing do
|
|
40
81
|
floatAttr: 33.5
|
41
82
|
}
|
42
83
|
}
|
43
|
-
result = subject.bucket_user(user, "hashKey", "floatAttr", "saltyA")
|
84
|
+
result = subject.bucket_user(user, "hashKey", "floatAttr", "saltyA", nil)
|
44
85
|
expect(result).to eq(0.0)
|
45
86
|
end
|
46
87
|
|
@@ -52,60 +93,124 @@ describe LaunchDarkly::Impl::EvaluatorBucketing do
|
|
52
93
|
boolAttr: true
|
53
94
|
}
|
54
95
|
}
|
55
|
-
result = subject.bucket_user(user, "hashKey", "boolAttr", "saltyA")
|
96
|
+
result = subject.bucket_user(user, "hashKey", "boolAttr", "saltyA", nil)
|
56
97
|
expect(result).to eq(0.0)
|
57
98
|
end
|
58
99
|
end
|
59
100
|
|
60
101
|
describe "variation_index_for_user" do
|
61
|
-
|
62
|
-
|
102
|
+
context "rollout is not an experiment" do
|
103
|
+
it "matches bucket" do
|
104
|
+
user = { key: "userkey" }
|
105
|
+
flag_key = "flagkey"
|
106
|
+
salt = "salt"
|
107
|
+
|
108
|
+
# First verify that with our test inputs, the bucket value will be greater than zero and less than 100000,
|
109
|
+
# so we can construct a rollout whose second bucket just barely contains that value
|
110
|
+
bucket_value = (subject.bucket_user(user, flag_key, "key", salt, nil) * 100000).truncate()
|
111
|
+
expect(bucket_value).to be > 0
|
112
|
+
expect(bucket_value).to be < 100000
|
113
|
+
|
114
|
+
bad_variation_a = 0
|
115
|
+
matched_variation = 1
|
116
|
+
bad_variation_b = 2
|
117
|
+
rule = {
|
118
|
+
rollout: {
|
119
|
+
variations: [
|
120
|
+
{ variation: bad_variation_a, weight: bucket_value }, # end of bucket range is not inclusive, so it will *not* match the target value
|
121
|
+
{ variation: matched_variation, weight: 1 }, # size of this bucket is 1, so it only matches that specific value
|
122
|
+
{ variation: bad_variation_b, weight: 100000 - (bucket_value + 1) }
|
123
|
+
]
|
124
|
+
}
|
125
|
+
}
|
126
|
+
flag = { key: flag_key, salt: salt }
|
127
|
+
|
128
|
+
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user)
|
129
|
+
expect(result_variation).to be matched_variation
|
130
|
+
expect(inExperiment).to be(false)
|
131
|
+
end
|
132
|
+
|
133
|
+
it "uses last bucket if bucket value is equal to total weight" do
|
134
|
+
user = { key: "userkey" }
|
135
|
+
flag_key = "flagkey"
|
136
|
+
salt = "salt"
|
137
|
+
|
138
|
+
bucket_value = (subject.bucket_user(user, flag_key, "key", salt, nil) * 100000).truncate()
|
139
|
+
|
140
|
+
# We'll construct a list of variations that stops right at the target bucket value
|
141
|
+
rule = {
|
142
|
+
rollout: {
|
143
|
+
variations: [
|
144
|
+
{ variation: 0, weight: bucket_value }
|
145
|
+
]
|
146
|
+
}
|
147
|
+
}
|
148
|
+
flag = { key: flag_key, salt: salt }
|
149
|
+
|
150
|
+
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user)
|
151
|
+
expect(result_variation).to be 0
|
152
|
+
expect(inExperiment).to be(false)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
context "rollout is an experiment" do
|
158
|
+
it "returns whether user is in the experiment or not" do
|
159
|
+
user1 = { key: "userKeyA" }
|
160
|
+
user2 = { key: "userKeyB" }
|
161
|
+
user3 = { key: "userKeyC" }
|
63
162
|
flag_key = "flagkey"
|
64
163
|
salt = "salt"
|
164
|
+
seed = 61
|
65
165
|
|
66
|
-
|
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
|
166
|
+
|
75
167
|
rule = {
|
76
168
|
rollout: {
|
169
|
+
seed: seed,
|
170
|
+
kind: 'experiment',
|
77
171
|
variations: [
|
78
|
-
{ variation:
|
79
|
-
{ variation:
|
80
|
-
{ variation:
|
172
|
+
{ variation: 0, weight: 10000, untracked: false },
|
173
|
+
{ variation: 2, weight: 20000, untracked: false },
|
174
|
+
{ variation: 0, weight: 70000 , untracked: true }
|
81
175
|
]
|
82
176
|
}
|
83
177
|
}
|
84
178
|
flag = { key: flag_key, salt: salt }
|
85
179
|
|
86
|
-
result_variation = subject.variation_index_for_user(flag, rule,
|
87
|
-
expect(result_variation).to be
|
180
|
+
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user1)
|
181
|
+
expect(result_variation).to be(0)
|
182
|
+
expect(inExperiment).to be(true)
|
183
|
+
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user2)
|
184
|
+
expect(result_variation).to be(2)
|
185
|
+
expect(inExperiment).to be(true)
|
186
|
+
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user3)
|
187
|
+
expect(result_variation).to be(0)
|
188
|
+
expect(inExperiment).to be(false)
|
88
189
|
end
|
89
190
|
|
90
191
|
it "uses last bucket if bucket value is equal to total weight" do
|
91
192
|
user = { key: "userkey" }
|
92
193
|
flag_key = "flagkey"
|
93
194
|
salt = "salt"
|
195
|
+
seed = 61
|
94
196
|
|
95
|
-
bucket_value = (subject.bucket_user(user, flag_key, "key", salt) * 100000).truncate()
|
197
|
+
bucket_value = (subject.bucket_user(user, flag_key, "key", salt, seed) * 100000).truncate()
|
96
198
|
|
97
199
|
# We'll construct a list of variations that stops right at the target bucket value
|
98
200
|
rule = {
|
99
201
|
rollout: {
|
202
|
+
seed: seed,
|
203
|
+
kind: 'experiment',
|
100
204
|
variations: [
|
101
|
-
{ variation: 0, weight: bucket_value }
|
205
|
+
{ variation: 0, weight: bucket_value, untracked: false }
|
102
206
|
]
|
103
207
|
}
|
104
208
|
}
|
105
209
|
flag = { key: flag_key, salt: salt }
|
106
210
|
|
107
|
-
result_variation = subject.variation_index_for_user(flag, rule, user)
|
211
|
+
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user)
|
108
212
|
expect(result_variation).to be 0
|
213
|
+
expect(inExperiment).to be(true)
|
109
214
|
end
|
110
215
|
end
|
111
216
|
end
|
@@ -91,6 +91,38 @@ module LaunchDarkly
|
|
91
91
|
result = basic_evaluator.evaluate(flag, user, factory)
|
92
92
|
expect(result.detail.reason).to eq(EvaluationReason::rule_match(0, 'ruleid'))
|
93
93
|
end
|
94
|
+
|
95
|
+
describe "experiment rollout behavior" do
|
96
|
+
it "sets the in_experiment value if rollout kind is experiment " do
|
97
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
|
98
|
+
rollout: { kind: 'experiment', variations: [ { weight: 100000, variation: 1, untracked: false } ] } }
|
99
|
+
flag = boolean_flag_with_rules([rule])
|
100
|
+
user = { key: "userkey", secondary: 999 }
|
101
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
102
|
+
expect(result.detail.reason.to_json).to include('"inExperiment":true')
|
103
|
+
expect(result.detail.reason.in_experiment).to eq(true)
|
104
|
+
end
|
105
|
+
|
106
|
+
it "does not set the in_experiment value if rollout kind is not experiment " do
|
107
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
|
108
|
+
rollout: { kind: 'rollout', variations: [ { weight: 100000, variation: 1, untracked: false } ] } }
|
109
|
+
flag = boolean_flag_with_rules([rule])
|
110
|
+
user = { key: "userkey", secondary: 999 }
|
111
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
112
|
+
expect(result.detail.reason.to_json).to_not include('"inExperiment":true')
|
113
|
+
expect(result.detail.reason.in_experiment).to eq(nil)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "does not set the in_experiment value if rollout kind is experiment and untracked is true" do
|
117
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
|
118
|
+
rollout: { kind: 'experiment', variations: [ { weight: 100000, variation: 1, untracked: true } ] } }
|
119
|
+
flag = boolean_flag_with_rules([rule])
|
120
|
+
user = { key: "userkey", secondary: 999 }
|
121
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
122
|
+
expect(result.detail.reason.to_json).to_not include('"inExperiment":true')
|
123
|
+
expect(result.detail.reason.in_experiment).to eq(nil)
|
124
|
+
end
|
125
|
+
end
|
94
126
|
end
|
95
127
|
end
|
96
128
|
end
|
data/spec/impl/evaluator_spec.rb
CHANGED
@@ -299,6 +299,50 @@ module LaunchDarkly
|
|
299
299
|
expect(result.detail).to eq(detail)
|
300
300
|
expect(result.events).to eq(nil)
|
301
301
|
end
|
302
|
+
|
303
|
+
describe "experiment rollout behavior" do
|
304
|
+
it "sets the in_experiment value if rollout kind is experiment and untracked false" do
|
305
|
+
flag = {
|
306
|
+
key: 'feature',
|
307
|
+
on: true,
|
308
|
+
fallthrough: { rollout: { kind: 'experiment', variations: [ { weight: 100000, variation: 1, untracked: false } ] } },
|
309
|
+
offVariation: 1,
|
310
|
+
variations: ['a', 'b', 'c']
|
311
|
+
}
|
312
|
+
user = { key: 'userkey' }
|
313
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
314
|
+
expect(result.detail.reason.to_json).to include('"inExperiment":true')
|
315
|
+
expect(result.detail.reason.in_experiment).to eq(true)
|
316
|
+
end
|
317
|
+
|
318
|
+
it "does not set the in_experiment value if rollout kind is not experiment" do
|
319
|
+
flag = {
|
320
|
+
key: 'feature',
|
321
|
+
on: true,
|
322
|
+
fallthrough: { rollout: { kind: 'rollout', variations: [ { weight: 100000, variation: 1, untracked: false } ] } },
|
323
|
+
offVariation: 1,
|
324
|
+
variations: ['a', 'b', 'c']
|
325
|
+
}
|
326
|
+
user = { key: 'userkey' }
|
327
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
328
|
+
expect(result.detail.reason.to_json).to_not include('"inExperiment":true')
|
329
|
+
expect(result.detail.reason.in_experiment).to eq(nil)
|
330
|
+
end
|
331
|
+
|
332
|
+
it "does not set the in_experiment value if rollout kind is experiment and untracked is true" do
|
333
|
+
flag = {
|
334
|
+
key: 'feature',
|
335
|
+
on: true,
|
336
|
+
fallthrough: { rollout: { kind: 'experiment', variations: [ { weight: 100000, variation: 1, untracked: true } ] } },
|
337
|
+
offVariation: 1,
|
338
|
+
variations: ['a', 'b', 'c']
|
339
|
+
}
|
340
|
+
user = { key: 'userkey' }
|
341
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
342
|
+
expect(result.detail.reason.to_json).to_not include('"inExperiment":true')
|
343
|
+
expect(result.detail.reason.in_experiment).to eq(nil)
|
344
|
+
end
|
345
|
+
end
|
302
346
|
end
|
303
347
|
end
|
304
348
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe LaunchDarkly::Impl::EventFactory do
|
4
|
+
subject { LaunchDarkly::Impl::EventFactory }
|
5
|
+
|
6
|
+
describe "#new_eval_event" do
|
7
|
+
let(:event_factory_without_reason) { subject.new(false) }
|
8
|
+
let(:user) { { 'key': 'userA' } }
|
9
|
+
let(:rule_with_experiment_rollout) {
|
10
|
+
{ id: 'ruleid',
|
11
|
+
clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
|
12
|
+
trackEvents: false,
|
13
|
+
rollout: { kind: 'experiment', salt: '', variations: [ { weight: 100000, variation: 0, untracked: false } ] }
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
let(:rule_with_rollout) {
|
18
|
+
{ id: 'ruleid',
|
19
|
+
trackEvents: false,
|
20
|
+
clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
|
21
|
+
rollout: { salt: '', variations: [ { weight: 100000, variation: 0, untracked: false } ] }
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
let(:fallthrough_with_rollout) {
|
26
|
+
{ rollout: { kind: 'rollout', salt: '', variations: [ { weight: 100000, variation: 0, untracked: false } ], trackEventsFallthrough: false } }
|
27
|
+
}
|
28
|
+
|
29
|
+
let(:rule_reason) { LaunchDarkly::EvaluationReason::rule_match(0, 'ruleid') }
|
30
|
+
let(:rule_reason_with_experiment) { LaunchDarkly::EvaluationReason::rule_match(0, 'ruleid', true) }
|
31
|
+
let(:fallthrough_reason) { LaunchDarkly::EvaluationReason::fallthrough }
|
32
|
+
let(:fallthrough_reason_with_experiment) { LaunchDarkly::EvaluationReason::fallthrough(true) }
|
33
|
+
|
34
|
+
context "in_experiment is true" do
|
35
|
+
it "sets the reason and trackevents: true for rules" do
|
36
|
+
flag = createFlag('rule', rule_with_experiment_rollout)
|
37
|
+
detail = LaunchDarkly::EvaluationDetail.new(true, 0, rule_reason_with_experiment)
|
38
|
+
r = subject.new(false).new_eval_event(flag, user, detail, nil, nil)
|
39
|
+
expect(r[:trackEvents]).to eql(true)
|
40
|
+
expect(r[:reason].to_s).to eql("RULE_MATCH(0,ruleid,true)")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "sets the reason and trackevents: true for the fallthrough" do
|
44
|
+
fallthrough_with_rollout[:kind] = 'experiment'
|
45
|
+
flag = createFlag('fallthrough', fallthrough_with_rollout)
|
46
|
+
detail = LaunchDarkly::EvaluationDetail.new(true, 0, fallthrough_reason_with_experiment)
|
47
|
+
r = subject.new(false).new_eval_event(flag, user, detail, nil, nil)
|
48
|
+
expect(r[:trackEvents]).to eql(true)
|
49
|
+
expect(r[:reason].to_s).to eql("FALLTHROUGH(true)")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "in_experiment is false" do
|
54
|
+
it "sets the reason & trackEvents: true if rule has trackEvents set to true" do
|
55
|
+
rule_with_rollout[:trackEvents] = true
|
56
|
+
flag = createFlag('rule', rule_with_rollout)
|
57
|
+
detail = LaunchDarkly::EvaluationDetail.new(true, 0, rule_reason)
|
58
|
+
r = subject.new(false).new_eval_event(flag, user, detail, nil, nil)
|
59
|
+
expect(r[:trackEvents]).to eql(true)
|
60
|
+
expect(r[:reason].to_s).to eql("RULE_MATCH(0,ruleid)")
|
61
|
+
end
|
62
|
+
|
63
|
+
it "sets the reason & trackEvents: true if fallthrough has trackEventsFallthrough set to true" do
|
64
|
+
flag = createFlag('fallthrough', fallthrough_with_rollout)
|
65
|
+
flag[:trackEventsFallthrough] = true
|
66
|
+
detail = LaunchDarkly::EvaluationDetail.new(true, 0, fallthrough_reason)
|
67
|
+
r = subject.new(false).new_eval_event(flag, user, detail, nil, nil)
|
68
|
+
expect(r[:trackEvents]).to eql(true)
|
69
|
+
expect(r[:reason].to_s).to eql("FALLTHROUGH")
|
70
|
+
end
|
71
|
+
|
72
|
+
it "doesn't set the reason & trackEvents if rule has trackEvents set to false" do
|
73
|
+
flag = createFlag('rule', rule_with_rollout)
|
74
|
+
detail = LaunchDarkly::EvaluationDetail.new(true, 0, rule_reason)
|
75
|
+
r = subject.new(false).new_eval_event(flag, user, detail, nil, nil)
|
76
|
+
expect(r[:trackEvents]).to be_nil
|
77
|
+
expect(r[:reason]).to be_nil
|
78
|
+
end
|
79
|
+
|
80
|
+
it "doesn't set the reason & trackEvents if fallthrough has trackEventsFallthrough set to false" do
|
81
|
+
flag = createFlag('fallthrough', fallthrough_with_rollout)
|
82
|
+
detail = LaunchDarkly::EvaluationDetail.new(true, 0, fallthrough_reason)
|
83
|
+
r = subject.new(false).new_eval_event(flag, user, detail, nil, nil)
|
84
|
+
expect(r[:trackEvents]).to be_nil
|
85
|
+
expect(r[:reason]).to be_nil
|
86
|
+
end
|
87
|
+
|
88
|
+
it "sets trackEvents true and doesn't set the reason if flag[:trackEvents] = true" do
|
89
|
+
flag = createFlag('fallthrough', fallthrough_with_rollout)
|
90
|
+
flag[:trackEvents] = true
|
91
|
+
detail = LaunchDarkly::EvaluationDetail.new(true, 0, fallthrough_reason)
|
92
|
+
r = subject.new(false).new_eval_event(flag, user, detail, nil, nil)
|
93
|
+
expect(r[:trackEvents]).to eql(true)
|
94
|
+
expect(r[:reason]).to be_nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def createFlag(kind, rule)
|
100
|
+
if kind == 'rule'
|
101
|
+
{ key: 'feature', on: true, rules: [rule], fallthrough: { variation: 0 }, variations: [ false, true ] }
|
102
|
+
elsif kind == 'fallthrough'
|
103
|
+
{ key: 'feature', on: true, fallthrough: rule, variations: [ false, true ] }
|
104
|
+
else
|
105
|
+
{ key: 'feature', on: true, fallthrough: { variation: 0 }, variations: [ false, true ] }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/spec/ldclient_spec.rb
CHANGED
@@ -171,20 +171,12 @@ describe LaunchDarkly::LDClient do
|
|
171
171
|
client.variation("key", user_anonymous, "default")
|
172
172
|
end
|
173
173
|
|
174
|
-
it "
|
174
|
+
it "does not queue a feature event for an existing feature when user key is nil" do
|
175
175
|
config.feature_store.init({ LaunchDarkly::FEATURES => {} })
|
176
176
|
config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
|
177
177
|
bad_user = { name: "Bob" }
|
178
|
-
expect(event_processor).
|
179
|
-
|
180
|
-
key: "key",
|
181
|
-
version: 100,
|
182
|
-
user: bad_user,
|
183
|
-
value: "default",
|
184
|
-
default: "default",
|
185
|
-
trackEvents: true,
|
186
|
-
debugEventsUntilDate: 1000
|
187
|
-
))
|
178
|
+
expect(event_processor).not_to receive(:add_event)
|
179
|
+
expect(logger).to receive(:warn)
|
188
180
|
client.variation("key", bad_user, "default")
|
189
181
|
end
|
190
182
|
|
data/spec/requestor_spec.rb
CHANGED
@@ -40,6 +40,19 @@ describe LaunchDarkly::Requestor do
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
it "logs debug output" do
|
44
|
+
logger = ::Logger.new($stdout)
|
45
|
+
logger.level = ::Logger::DEBUG
|
46
|
+
with_server do |server|
|
47
|
+
with_requestor(server.base_uri.to_s, { logger: logger }) do |requestor|
|
48
|
+
server.setup_ok_response("/", { flags: { x: { key: "y" } } }.to_json)
|
49
|
+
expect do
|
50
|
+
requestor.request_all_data()
|
51
|
+
end.to output(/\[LDClient\] Got response from uri\:/).to_stdout_from_any_process
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
43
56
|
it "sends etag from previous response" do
|
44
57
|
etag = "xyz"
|
45
58
|
with_server do |server|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: launchdarkly-server-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 6.
|
4
|
+
version: 6.2.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LaunchDarkly
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-08-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-dynamodb
|
@@ -28,16 +28,16 @@ dependencies:
|
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - '='
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 2.2.10
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - '='
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 2.2.10
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -196,44 +196,50 @@ dependencies:
|
|
196
196
|
name: ld-eventsource
|
197
197
|
requirement: !ruby/object:Gem::Requirement
|
198
198
|
requirements:
|
199
|
-
- -
|
199
|
+
- - '='
|
200
200
|
- !ruby/object:Gem::Version
|
201
|
-
version:
|
201
|
+
version: 2.0.1
|
202
202
|
type: :runtime
|
203
203
|
prerelease: false
|
204
204
|
version_requirements: !ruby/object:Gem::Requirement
|
205
205
|
requirements:
|
206
|
-
- -
|
206
|
+
- - '='
|
207
207
|
- !ruby/object:Gem::Version
|
208
|
-
version:
|
208
|
+
version: 2.0.1
|
209
209
|
- !ruby/object:Gem::Dependency
|
210
210
|
name: json
|
211
211
|
requirement: !ruby/object:Gem::Requirement
|
212
212
|
requirements:
|
213
213
|
- - "~>"
|
214
214
|
- !ruby/object:Gem::Version
|
215
|
-
version: 2.3
|
215
|
+
version: '2.3'
|
216
216
|
type: :runtime
|
217
217
|
prerelease: false
|
218
218
|
version_requirements: !ruby/object:Gem::Requirement
|
219
219
|
requirements:
|
220
220
|
- - "~>"
|
221
221
|
- !ruby/object:Gem::Version
|
222
|
-
version: 2.3
|
222
|
+
version: '2.3'
|
223
223
|
- !ruby/object:Gem::Dependency
|
224
224
|
name: http
|
225
225
|
requirement: !ruby/object:Gem::Requirement
|
226
226
|
requirements:
|
227
|
-
- - "
|
227
|
+
- - ">="
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: 4.4.0
|
230
|
+
- - "<"
|
228
231
|
- !ruby/object:Gem::Version
|
229
|
-
version:
|
232
|
+
version: 6.0.0
|
230
233
|
type: :runtime
|
231
234
|
prerelease: false
|
232
235
|
version_requirements: !ruby/object:Gem::Requirement
|
233
236
|
requirements:
|
234
|
-
- - "
|
237
|
+
- - ">="
|
238
|
+
- !ruby/object:Gem::Version
|
239
|
+
version: 4.4.0
|
240
|
+
- - "<"
|
235
241
|
- !ruby/object:Gem::Version
|
236
|
-
version:
|
242
|
+
version: 6.0.0
|
237
243
|
description: Official LaunchDarkly SDK for Ruby
|
238
244
|
email:
|
239
245
|
- team@launchdarkly.com
|
@@ -252,8 +258,9 @@ files:
|
|
252
258
|
- ".ldrelease/circleci/linux/execute.sh"
|
253
259
|
- ".ldrelease/circleci/mac/execute.sh"
|
254
260
|
- ".ldrelease/circleci/template/build.sh"
|
261
|
+
- ".ldrelease/circleci/template/gems-setup.sh"
|
262
|
+
- ".ldrelease/circleci/template/prepare.sh"
|
255
263
|
- ".ldrelease/circleci/template/publish.sh"
|
256
|
-
- ".ldrelease/circleci/template/set-gem-home.sh"
|
257
264
|
- ".ldrelease/circleci/template/test.sh"
|
258
265
|
- ".ldrelease/circleci/template/update-version.sh"
|
259
266
|
- ".ldrelease/circleci/windows/execute.ps1"
|
@@ -336,6 +343,7 @@ files:
|
|
336
343
|
- spec/impl/evaluator_segment_spec.rb
|
337
344
|
- spec/impl/evaluator_spec.rb
|
338
345
|
- spec/impl/evaluator_spec_base.rb
|
346
|
+
- spec/impl/event_factory_spec.rb
|
339
347
|
- spec/impl/model/serialization_spec.rb
|
340
348
|
- spec/in_memory_feature_store_spec.rb
|
341
349
|
- spec/integrations/consul_feature_store_spec.rb
|
@@ -402,6 +410,7 @@ test_files:
|
|
402
410
|
- spec/impl/evaluator_segment_spec.rb
|
403
411
|
- spec/impl/evaluator_spec.rb
|
404
412
|
- spec/impl/evaluator_spec_base.rb
|
413
|
+
- spec/impl/event_factory_spec.rb
|
405
414
|
- spec/impl/model/serialization_spec.rb
|
406
415
|
- spec/in_memory_feature_store_spec.rb
|
407
416
|
- spec/integrations/consul_feature_store_spec.rb
|