launchdarkly-server-sdk 6.1.0 → 6.2.2
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 +1 -1
- data/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- data/.github/ISSUE_TEMPLATE/config.yml +5 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +17 -0
- data/azure-pipelines.yml +1 -1
- data/lib/ldclient-rb/evaluation_detail.rb +38 -7
- data/lib/ldclient-rb/events.rb +3 -3
- 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 +12 -7
- data/lib/ldclient-rb/requestor.rb +1 -1
- data/lib/ldclient-rb/version.rb +1 -1
- 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 +14 -22
- data/spec/requestor_spec.rb +13 -0
- metadata +5 -3
- data/Gemfile.lock +0 -116
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eef5d4b08484f02739bfab4d151f5212624cba09157e756e0126603ccacf0a31
|
4
|
+
data.tar.gz: c0510cc29bbb40dd18dc3428803d0b6a33d1c9df868a4ac43caa801c9e3329f0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 338a0e3189764f8c5cfe1034ac6602050430a70468da33688953385322c8c94df60919ad04707e7504d6653a51b6928ea0d0fb121518fbdb5fd7ade9e418fa6e
|
7
|
+
data.tar.gz: 94d7650238107eadc7cb2846b826816fe01fe6ee3f421a1a396821681ae8e2b3854c6675bede18cf7a7abf391164123b838dca615bb8404910e07ce350071d02
|
data/.circleci/config.yml
CHANGED
@@ -8,7 +8,7 @@ assignees: ''
|
|
8
8
|
---
|
9
9
|
|
10
10
|
**Is this a support request?**
|
11
|
-
This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the SDK code. If you're not sure whether the problem you are having is specifically related to the SDK, or to the LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to investigate the problem and will consult the SDK team if necessary. You can submit a support request by going [here](https://support.launchdarkly.com/)
|
11
|
+
This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the SDK code. If you're not sure whether the problem you are having is specifically related to the SDK, or to the LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to investigate the problem and will consult the SDK team if necessary. You can submit a support request by going [here](https://support.launchdarkly.com/hc/en-us/requests/new) or by emailing support@launchdarkly.com.
|
12
12
|
|
13
13
|
Note that issues filed on this issue tracker are publicly accessible. Do not provide any private account information on your issues. If your problem is specific to your account, you should submit a support request as described above.
|
14
14
|
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,23 @@
|
|
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.1] - 2021-07-15
|
6
|
+
### Changed:
|
7
|
+
- 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))
|
8
|
+
|
9
|
+
## [6.2.0] - 2021-06-17
|
10
|
+
### Added:
|
11
|
+
- 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.
|
12
|
+
|
13
|
+
## [6.1.1] - 2021-05-27
|
14
|
+
### Fixed:
|
15
|
+
- 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.
|
16
|
+
|
17
|
+
## [6.1.0] - 2021-02-04
|
18
|
+
### Added:
|
19
|
+
- Added the `alias` method. This can be used to associate two user objects for analytics purposes by generating an alias event.
|
20
|
+
|
21
|
+
|
5
22
|
## [6.0.0] - 2021-01-26
|
6
23
|
### Added:
|
7
24
|
- Added a `socket_factory` configuration option which can be used for socket creation by the HTTP client if provided. The value of `socket_factory` must be an object providing an `open(uri, timeout)` method and returning a connected socket.
|
data/azure-pipelines.yml
CHANGED
@@ -45,7 +45,7 @@ jobs:
|
|
45
45
|
workingDirectory: $(System.DefaultWorkingDirectory)
|
46
46
|
script: |
|
47
47
|
ruby -v
|
48
|
-
gem install bundler
|
48
|
+
gem install bundler
|
49
49
|
bundle install
|
50
50
|
mkdir rspec
|
51
51
|
bundle exec rspec --format progress --format RspecJunitFormatter -o ./rspec/rspec.xml spec
|
@@ -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)
|
data/lib/ldclient-rb/events.rb
CHANGED
@@ -443,7 +443,7 @@ module LaunchDarkly
|
|
443
443
|
if @inline_users || is_debug
|
444
444
|
out[:user] = process_user(event)
|
445
445
|
else
|
446
|
-
out[:userKey] = event[:user]
|
446
|
+
out[:userKey] = event[:user][:key]
|
447
447
|
end
|
448
448
|
out[:reason] = event[:reason] if !event[:reason].nil?
|
449
449
|
out
|
@@ -451,7 +451,7 @@ module LaunchDarkly
|
|
451
451
|
{
|
452
452
|
kind: "identify",
|
453
453
|
creationDate: event[:creationDate],
|
454
|
-
key: event[:user]
|
454
|
+
key: event[:user][:key].to_s,
|
455
455
|
user: process_user(event)
|
456
456
|
}
|
457
457
|
when "custom"
|
@@ -464,7 +464,7 @@ module LaunchDarkly
|
|
464
464
|
if @inline_users
|
465
465
|
out[:user] = process_user(event)
|
466
466
|
else
|
467
|
-
out[:userKey] = event[:user]
|
467
|
+
out[:userKey] = event[:user][:key]
|
468
468
|
end
|
469
469
|
out[:metricValue] = event[:metricValue] if event.has_key?(:metricValue)
|
470
470
|
out[:contextKind] = event[:contextKind] if event.has_key?(:contextKind)
|
@@ -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
@@ -401,6 +401,18 @@ module LaunchDarkly
|
|
401
401
|
return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
|
402
402
|
end
|
403
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
|
408
|
+
end
|
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
|
+
|
404
416
|
if !initialized?
|
405
417
|
if @store.initialized?
|
406
418
|
@config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
|
@@ -421,13 +433,6 @@ module LaunchDarkly
|
|
421
433
|
return detail
|
422
434
|
end
|
423
435
|
|
424
|
-
unless user
|
425
|
-
@config.logger.error { "[LDClient] Must specify user" }
|
426
|
-
detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
|
427
|
-
@event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
|
428
|
-
return detail
|
429
|
-
end
|
430
|
-
|
431
436
|
begin
|
432
437
|
res = @evaluator.evaluate(feature, user, event_factory)
|
433
438
|
if !res.events.nil?
|
@@ -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
@@ -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
@@ -145,19 +145,11 @@ describe LaunchDarkly::LDClient do
|
|
145
145
|
client.variation("key", user, "default")
|
146
146
|
end
|
147
147
|
|
148
|
-
it "
|
148
|
+
it "does not send an event if user is nil" do
|
149
149
|
config.feature_store.init({ LaunchDarkly::FEATURES => {} })
|
150
150
|
config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
|
151
|
-
expect(event_processor).
|
152
|
-
|
153
|
-
key: "key",
|
154
|
-
version: 100,
|
155
|
-
user: nil,
|
156
|
-
value: "default",
|
157
|
-
default: "default",
|
158
|
-
trackEvents: true,
|
159
|
-
debugEventsUntilDate: 1000
|
160
|
-
))
|
151
|
+
expect(event_processor).not_to receive(:add_event)
|
152
|
+
expect(logger).to receive(:error)
|
161
153
|
client.variation("key", nil, "default")
|
162
154
|
end
|
163
155
|
|
@@ -179,20 +171,12 @@ describe LaunchDarkly::LDClient do
|
|
179
171
|
client.variation("key", user_anonymous, "default")
|
180
172
|
end
|
181
173
|
|
182
|
-
it "
|
174
|
+
it "does not queue a feature event for an existing feature when user key is nil" do
|
183
175
|
config.feature_store.init({ LaunchDarkly::FEATURES => {} })
|
184
176
|
config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
|
185
177
|
bad_user = { name: "Bob" }
|
186
|
-
expect(event_processor).
|
187
|
-
|
188
|
-
key: "key",
|
189
|
-
version: 100,
|
190
|
-
user: bad_user,
|
191
|
-
value: "default",
|
192
|
-
default: "default",
|
193
|
-
trackEvents: true,
|
194
|
-
debugEventsUntilDate: 1000
|
195
|
-
))
|
178
|
+
expect(event_processor).not_to receive(:add_event)
|
179
|
+
expect(logger).to receive(:warn)
|
196
180
|
client.variation("key", bad_user, "default")
|
197
181
|
end
|
198
182
|
|
@@ -313,6 +297,14 @@ describe LaunchDarkly::LDClient do
|
|
313
297
|
))
|
314
298
|
client.variation_detail("key", user, "default")
|
315
299
|
end
|
300
|
+
|
301
|
+
it "does not send an event if user is nil" do
|
302
|
+
config.feature_store.init({ LaunchDarkly::FEATURES => {} })
|
303
|
+
config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
|
304
|
+
expect(event_processor).not_to receive(:add_event)
|
305
|
+
expect(logger).to receive(:error)
|
306
|
+
client.variation_detail("key", nil, "default")
|
307
|
+
end
|
316
308
|
end
|
317
309
|
|
318
310
|
describe '#all_flags' do
|
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.2
|
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-07-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-dynamodb
|
@@ -243,6 +243,7 @@ extra_rdoc_files: []
|
|
243
243
|
files:
|
244
244
|
- ".circleci/config.yml"
|
245
245
|
- ".github/ISSUE_TEMPLATE/bug_report.md"
|
246
|
+
- ".github/ISSUE_TEMPLATE/config.yml"
|
246
247
|
- ".github/ISSUE_TEMPLATE/feature_request.md"
|
247
248
|
- ".github/pull_request_template.md"
|
248
249
|
- ".gitignore"
|
@@ -264,7 +265,6 @@ files:
|
|
264
265
|
- CODEOWNERS
|
265
266
|
- CONTRIBUTING.md
|
266
267
|
- Gemfile
|
267
|
-
- Gemfile.lock
|
268
268
|
- LICENSE.txt
|
269
269
|
- README.md
|
270
270
|
- azure-pipelines.yml
|
@@ -336,6 +336,7 @@ files:
|
|
336
336
|
- spec/impl/evaluator_segment_spec.rb
|
337
337
|
- spec/impl/evaluator_spec.rb
|
338
338
|
- spec/impl/evaluator_spec_base.rb
|
339
|
+
- spec/impl/event_factory_spec.rb
|
339
340
|
- spec/impl/model/serialization_spec.rb
|
340
341
|
- spec/in_memory_feature_store_spec.rb
|
341
342
|
- spec/integrations/consul_feature_store_spec.rb
|
@@ -402,6 +403,7 @@ test_files:
|
|
402
403
|
- spec/impl/evaluator_segment_spec.rb
|
403
404
|
- spec/impl/evaluator_spec.rb
|
404
405
|
- spec/impl/evaluator_spec_base.rb
|
406
|
+
- spec/impl/event_factory_spec.rb
|
405
407
|
- spec/impl/model/serialization_spec.rb
|
406
408
|
- spec/in_memory_feature_store_spec.rb
|
407
409
|
- spec/integrations/consul_feature_store_spec.rb
|
data/Gemfile.lock
DELETED
@@ -1,116 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
launchdarkly-server-sdk (6.1.0)
|
5
|
-
concurrent-ruby (~> 1.1)
|
6
|
-
http (~> 4.4.1)
|
7
|
-
json (~> 2.3.1)
|
8
|
-
ld-eventsource (~> 2.0)
|
9
|
-
semantic (~> 1.6)
|
10
|
-
|
11
|
-
GEM
|
12
|
-
remote: https://rubygems.org/
|
13
|
-
specs:
|
14
|
-
addressable (2.7.0)
|
15
|
-
public_suffix (>= 2.0.2, < 5.0)
|
16
|
-
ansi (1.5.0)
|
17
|
-
ast (2.4.2)
|
18
|
-
aws-eventstream (1.1.0)
|
19
|
-
aws-partitions (1.418.0)
|
20
|
-
aws-sdk-core (3.111.2)
|
21
|
-
aws-eventstream (~> 1, >= 1.0.2)
|
22
|
-
aws-partitions (~> 1, >= 1.239.0)
|
23
|
-
aws-sigv4 (~> 1.1)
|
24
|
-
jmespath (~> 1.0)
|
25
|
-
aws-sdk-dynamodb (1.58.0)
|
26
|
-
aws-sdk-core (~> 3, >= 3.109.0)
|
27
|
-
aws-sigv4 (~> 1.1)
|
28
|
-
aws-sigv4 (1.2.2)
|
29
|
-
aws-eventstream (~> 1, >= 1.0.2)
|
30
|
-
concurrent-ruby (1.1.8)
|
31
|
-
connection_pool (2.2.3)
|
32
|
-
deep_merge (1.2.1)
|
33
|
-
diff-lcs (1.4.4)
|
34
|
-
diplomat (2.4.2)
|
35
|
-
deep_merge (~> 1.0, >= 1.0.1)
|
36
|
-
faraday (>= 0.9, < 1.1.0)
|
37
|
-
domain_name (0.5.20190701)
|
38
|
-
unf (>= 0.0.5, < 1.0.0)
|
39
|
-
faraday (1.0.1)
|
40
|
-
multipart-post (>= 1.2, < 3)
|
41
|
-
ffi (1.14.2)
|
42
|
-
ffi-compiler (1.0.1)
|
43
|
-
ffi (>= 1.0.0)
|
44
|
-
rake
|
45
|
-
http (4.4.1)
|
46
|
-
addressable (~> 2.3)
|
47
|
-
http-cookie (~> 1.0)
|
48
|
-
http-form_data (~> 2.2)
|
49
|
-
http-parser (~> 1.2.0)
|
50
|
-
http-cookie (1.0.3)
|
51
|
-
domain_name (~> 0.5)
|
52
|
-
http-form_data (2.3.0)
|
53
|
-
http-parser (1.2.3)
|
54
|
-
ffi-compiler (>= 1.0, < 2.0)
|
55
|
-
jmespath (1.4.0)
|
56
|
-
json (2.3.1)
|
57
|
-
ld-eventsource (2.0.0)
|
58
|
-
concurrent-ruby (~> 1.0)
|
59
|
-
http (~> 4.4.1)
|
60
|
-
listen (3.4.1)
|
61
|
-
rb-fsevent (~> 0.10, >= 0.10.3)
|
62
|
-
rb-inotify (~> 0.9, >= 0.9.10)
|
63
|
-
multipart-post (2.1.1)
|
64
|
-
oga (2.15)
|
65
|
-
ast
|
66
|
-
ruby-ll (~> 2.1)
|
67
|
-
public_suffix (4.0.6)
|
68
|
-
rake (13.0.3)
|
69
|
-
rb-fsevent (0.10.4)
|
70
|
-
rb-inotify (0.10.1)
|
71
|
-
ffi (~> 1.0)
|
72
|
-
redis (4.2.5)
|
73
|
-
rspec (3.10.0)
|
74
|
-
rspec-core (~> 3.10.0)
|
75
|
-
rspec-expectations (~> 3.10.0)
|
76
|
-
rspec-mocks (~> 3.10.0)
|
77
|
-
rspec-core (3.10.1)
|
78
|
-
rspec-support (~> 3.10.0)
|
79
|
-
rspec-expectations (3.10.1)
|
80
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
81
|
-
rspec-support (~> 3.10.0)
|
82
|
-
rspec-mocks (3.10.1)
|
83
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
84
|
-
rspec-support (~> 3.10.0)
|
85
|
-
rspec-support (3.10.1)
|
86
|
-
rspec_junit_formatter (0.4.1)
|
87
|
-
rspec-core (>= 2, < 4, != 2.12.0)
|
88
|
-
ruby-ll (2.1.2)
|
89
|
-
ansi
|
90
|
-
ast
|
91
|
-
semantic (1.6.1)
|
92
|
-
timecop (0.9.2)
|
93
|
-
unf (0.1.4)
|
94
|
-
unf_ext
|
95
|
-
unf_ext (0.0.7.7)
|
96
|
-
webrick (1.7.0)
|
97
|
-
|
98
|
-
PLATFORMS
|
99
|
-
ruby
|
100
|
-
|
101
|
-
DEPENDENCIES
|
102
|
-
aws-sdk-dynamodb (~> 1.57)
|
103
|
-
bundler (~> 2.1)
|
104
|
-
connection_pool (~> 2.2.3)
|
105
|
-
diplomat (~> 2.4.2)
|
106
|
-
launchdarkly-server-sdk!
|
107
|
-
listen (~> 3.3)
|
108
|
-
oga (~> 2.2)
|
109
|
-
redis (~> 4.2)
|
110
|
-
rspec (~> 3.10)
|
111
|
-
rspec_junit_formatter (~> 0.4)
|
112
|
-
timecop (~> 0.9)
|
113
|
-
webrick (~> 1.7)
|
114
|
-
|
115
|
-
BUNDLED WITH
|
116
|
-
2.2.8
|