launchdarkly-server-sdk 6.0.0 → 6.2.1
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 +14 -0
- data/README.md +0 -1
- data/lib/ldclient-rb/evaluation_detail.rb +38 -7
- data/lib/ldclient-rb/events.rb +5 -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 +28 -0
- data/lib/ldclient-rb/ldclient.rb +29 -7
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/events_spec.rb +10 -0
- 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 +57 -15
- 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: 0c3e33076372820b2ecde009b3effb174d633fee79cb700380e1b887f6f1876c
|
4
|
+
data.tar.gz: 30b62850e576db2710058de74bf28c1f95ef72e79479326df6214577c343d100
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b93a893777b34b7eb0774d150cbef556166852afb9bdebed78293b62ddc0c7f8639a4a789dc74655d461120b8720123ff687d851d873d2074ced86611b635cad
|
7
|
+
data.tar.gz: 90aa095737b094516659bd63b6987a6a523e5f20a83eeef3793ae9c667b5d950c7f494e7a7e44d655a5f7e6b2a0865e1cbeca0d35c8ec16ee8b084a7756027da
|
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,19 @@
|
|
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.0] - 2021-06-17
|
6
|
+
### Added:
|
7
|
+
- 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.
|
8
|
+
|
9
|
+
## [6.1.1] - 2021-05-27
|
10
|
+
### Fixed:
|
11
|
+
- 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.
|
12
|
+
|
13
|
+
## [6.1.0] - 2021-02-04
|
14
|
+
### Added:
|
15
|
+
- Added the `alias` method. This can be used to associate two user objects for analytics purposes by generating an alias event.
|
16
|
+
|
17
|
+
|
5
18
|
## [6.0.0] - 2021-01-26
|
6
19
|
### Added:
|
7
20
|
- 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.
|
@@ -10,6 +23,7 @@ All notable changes to the LaunchDarkly Ruby SDK will be documented in this file
|
|
10
23
|
- Switched to the `http` gem instead of `socketry` (with a custom http client) for streaming, and instead of `Net::HTTP` for polling / events.
|
11
24
|
- Dropped support for Ruby < version 2.5
|
12
25
|
- Dropped support for JRuby < version 9.2
|
26
|
+
- Switched the default polling domain from `app.launchdarkly.com` to `sdk.launchdarkly.com`.
|
13
27
|
|
14
28
|
## [5.8.2] - 2021-01-19
|
15
29
|
### Fixed:
|
data/README.md
CHANGED
@@ -55,4 +55,3 @@ About LaunchDarkly
|
|
55
55
|
* [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides
|
56
56
|
* [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation
|
57
57
|
* [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates
|
58
|
-
* [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies
|
@@ -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
@@ -439,10 +439,11 @@ module LaunchDarkly
|
|
439
439
|
out[:variation] = event[:variation] if event.has_key?(:variation)
|
440
440
|
out[:version] = event[:version] if event.has_key?(:version)
|
441
441
|
out[:prereqOf] = event[:prereqOf] if event.has_key?(:prereqOf)
|
442
|
+
out[:contextKind] = event[:contextKind] if event.has_key?(:contextKind)
|
442
443
|
if @inline_users || is_debug
|
443
444
|
out[:user] = process_user(event)
|
444
445
|
else
|
445
|
-
out[:userKey] = event[:user]
|
446
|
+
out[:userKey] = event[:user][:key]
|
446
447
|
end
|
447
448
|
out[:reason] = event[:reason] if !event[:reason].nil?
|
448
449
|
out
|
@@ -450,7 +451,7 @@ module LaunchDarkly
|
|
450
451
|
{
|
451
452
|
kind: "identify",
|
452
453
|
creationDate: event[:creationDate],
|
453
|
-
key: event[:user]
|
454
|
+
key: event[:user][:key].to_s,
|
454
455
|
user: process_user(event)
|
455
456
|
}
|
456
457
|
when "custom"
|
@@ -463,9 +464,10 @@ module LaunchDarkly
|
|
463
464
|
if @inline_users
|
464
465
|
out[:user] = process_user(event)
|
465
466
|
else
|
466
|
-
out[:userKey] = event[:user]
|
467
|
+
out[:userKey] = event[:user][:key]
|
467
468
|
end
|
468
469
|
out[:metricValue] = event[:metricValue] if event.has_key?(:metricValue)
|
470
|
+
out[:contextKind] = event[:contextKind] if event.has_key?(:contextKind)
|
469
471
|
out
|
470
472
|
when "index"
|
471
473
|
{
|
@@ -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)
|
@@ -28,6 +28,7 @@ module LaunchDarkly
|
|
28
28
|
e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
|
29
29
|
e[:prereqOf] = prereq_of_flag[:key] if !prereq_of_flag.nil?
|
30
30
|
e[:reason] = detail.reason if add_experiment_data || @with_reasons
|
31
|
+
e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
|
31
32
|
e
|
32
33
|
end
|
33
34
|
|
@@ -43,6 +44,7 @@ module LaunchDarkly
|
|
43
44
|
e[:trackEvents] = true if flag[:trackEvents]
|
44
45
|
e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
|
45
46
|
e[:reason] = reason if @with_reasons
|
47
|
+
e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
|
46
48
|
e
|
47
49
|
end
|
48
50
|
|
@@ -55,6 +57,7 @@ module LaunchDarkly
|
|
55
57
|
default: default_value
|
56
58
|
}
|
57
59
|
e[:reason] = reason if @with_reasons
|
60
|
+
e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
|
58
61
|
e
|
59
62
|
end
|
60
63
|
|
@@ -66,6 +69,16 @@ module LaunchDarkly
|
|
66
69
|
}
|
67
70
|
end
|
68
71
|
|
72
|
+
def new_alias_event(current_context, previous_context)
|
73
|
+
{
|
74
|
+
kind: 'alias',
|
75
|
+
key: current_context[:key],
|
76
|
+
contextKind: context_to_context_kind(current_context),
|
77
|
+
previousKey: previous_context[:key],
|
78
|
+
previousContextKind: context_to_context_kind(previous_context)
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
69
82
|
def new_custom_event(event_name, user, data, metric_value)
|
70
83
|
e = {
|
71
84
|
kind: 'custom',
|
@@ -74,13 +87,27 @@ module LaunchDarkly
|
|
74
87
|
}
|
75
88
|
e[:data] = data if !data.nil?
|
76
89
|
e[:metricValue] = metric_value if !metric_value.nil?
|
90
|
+
e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
|
77
91
|
e
|
78
92
|
end
|
79
93
|
|
80
94
|
private
|
81
95
|
|
96
|
+
def context_to_context_kind(user)
|
97
|
+
if !user.nil? && user[:anonymous]
|
98
|
+
return "anonymousUser"
|
99
|
+
else
|
100
|
+
return "user"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
82
104
|
def is_experiment(flag, reason)
|
83
105
|
return false if !reason
|
106
|
+
|
107
|
+
if reason.in_experiment
|
108
|
+
return true
|
109
|
+
end
|
110
|
+
|
84
111
|
case reason[:kind]
|
85
112
|
when 'RULE_MATCH'
|
86
113
|
index = reason[:ruleIndex]
|
@@ -93,6 +120,7 @@ module LaunchDarkly
|
|
93
120
|
end
|
94
121
|
false
|
95
122
|
end
|
123
|
+
|
96
124
|
end
|
97
125
|
end
|
98
126
|
end
|
data/lib/ldclient-rb/ldclient.rb
CHANGED
@@ -282,6 +282,23 @@ module LaunchDarkly
|
|
282
282
|
@event_processor.add_event(@event_factory_default.new_custom_event(event_name, user, data, metric_value))
|
283
283
|
end
|
284
284
|
|
285
|
+
#
|
286
|
+
# Associates a new and old user object for analytics purposes via an alias event.
|
287
|
+
#
|
288
|
+
# @param current_context [Hash] The current version of a user.
|
289
|
+
# @param previous_context [Hash] The previous version of a user.
|
290
|
+
# @return [void]
|
291
|
+
#
|
292
|
+
def alias(current_context, previous_context)
|
293
|
+
if !current_context || current_context[:key].nil? || !previous_context || previous_context[:key].nil?
|
294
|
+
@config.logger.warn("Alias called with nil user or nil user key!")
|
295
|
+
return
|
296
|
+
end
|
297
|
+
sanitize_user(current_context)
|
298
|
+
sanitize_user(previous_context)
|
299
|
+
@event_processor.add_event(@event_factory_default.new_alias_event(current_context, previous_context))
|
300
|
+
end
|
301
|
+
|
285
302
|
#
|
286
303
|
# Returns all feature flag values for the given user.
|
287
304
|
#
|
@@ -384,6 +401,18 @@ module LaunchDarkly
|
|
384
401
|
return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
|
385
402
|
end
|
386
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
|
+
|
387
416
|
if !initialized?
|
388
417
|
if @store.initialized?
|
389
418
|
@config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
|
@@ -404,13 +433,6 @@ module LaunchDarkly
|
|
404
433
|
return detail
|
405
434
|
end
|
406
435
|
|
407
|
-
unless user
|
408
|
-
@config.logger.error { "[LDClient] Must specify user" }
|
409
|
-
detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
|
410
|
-
@event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
|
411
|
-
return detail
|
412
|
-
end
|
413
|
-
|
414
436
|
begin
|
415
437
|
res = @evaluator.evaluate(feature, user, event_factory)
|
416
438
|
if !res.events.nil?
|
data/lib/ldclient-rb/version.rb
CHANGED
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 }
|
@@ -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
@@ -25,6 +25,12 @@ describe LaunchDarkly::LDClient do
|
|
25
25
|
}
|
26
26
|
}
|
27
27
|
end
|
28
|
+
let(:user_anonymous) do
|
29
|
+
{
|
30
|
+
key: "anonymous@test.com",
|
31
|
+
anonymous: true
|
32
|
+
}
|
33
|
+
end
|
28
34
|
let(:numeric_key_user) do
|
29
35
|
{
|
30
36
|
key: 33,
|
@@ -139,36 +145,38 @@ describe LaunchDarkly::LDClient do
|
|
139
145
|
client.variation("key", user, "default")
|
140
146
|
end
|
141
147
|
|
142
|
-
it "
|
148
|
+
it "does not send an event if user is nil" do
|
143
149
|
config.feature_store.init({ LaunchDarkly::FEATURES => {} })
|
144
150
|
config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
|
145
|
-
expect(event_processor).
|
146
|
-
|
147
|
-
key: "key",
|
148
|
-
version: 100,
|
149
|
-
user: nil,
|
150
|
-
value: "default",
|
151
|
-
default: "default",
|
152
|
-
trackEvents: true,
|
153
|
-
debugEventsUntilDate: 1000
|
154
|
-
))
|
151
|
+
expect(event_processor).not_to receive(:add_event)
|
152
|
+
expect(logger).to receive(:error)
|
155
153
|
client.variation("key", nil, "default")
|
156
154
|
end
|
157
155
|
|
158
|
-
it "queues a feature event for an existing feature when user
|
156
|
+
it "queues a feature event for an existing feature when user is anonymous" do
|
159
157
|
config.feature_store.init({ LaunchDarkly::FEATURES => {} })
|
160
158
|
config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
|
161
|
-
bad_user = { name: "Bob" }
|
162
159
|
expect(event_processor).to receive(:add_event).with(hash_including(
|
163
160
|
kind: "feature",
|
164
161
|
key: "key",
|
165
162
|
version: 100,
|
166
|
-
|
167
|
-
|
163
|
+
contextKind: "anonymousUser",
|
164
|
+
user: user_anonymous,
|
165
|
+
variation: 0,
|
166
|
+
value: "value",
|
168
167
|
default: "default",
|
169
168
|
trackEvents: true,
|
170
169
|
debugEventsUntilDate: 1000
|
171
170
|
))
|
171
|
+
client.variation("key", user_anonymous, "default")
|
172
|
+
end
|
173
|
+
|
174
|
+
it "does not queue a feature event for an existing feature when user key is nil" do
|
175
|
+
config.feature_store.init({ LaunchDarkly::FEATURES => {} })
|
176
|
+
config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
|
177
|
+
bad_user = { name: "Bob" }
|
178
|
+
expect(event_processor).not_to receive(:add_event)
|
179
|
+
expect(logger).to receive(:warn)
|
172
180
|
client.variation("key", bad_user, "default")
|
173
181
|
end
|
174
182
|
|
@@ -289,6 +297,14 @@ describe LaunchDarkly::LDClient do
|
|
289
297
|
))
|
290
298
|
client.variation_detail("key", user, "default")
|
291
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
|
292
308
|
end
|
293
309
|
|
294
310
|
describe '#all_flags' do
|
@@ -455,6 +471,12 @@ describe LaunchDarkly::LDClient do
|
|
455
471
|
client.track("custom_event_name", user, nil, 1.5)
|
456
472
|
end
|
457
473
|
|
474
|
+
it "includes contextKind with anonymous user" do
|
475
|
+
expect(event_processor).to receive(:add_event).with(hash_including(
|
476
|
+
kind: "custom", key: "custom_event_name", user: user_anonymous, metricValue: 2.2, contextKind: "anonymousUser"))
|
477
|
+
client.track("custom_event_name", user_anonymous, nil, 2.2)
|
478
|
+
end
|
479
|
+
|
458
480
|
it "sanitizes the user in the event" do
|
459
481
|
expect(event_processor).to receive(:add_event).with(hash_including(user: sanitized_numeric_key_user))
|
460
482
|
client.track("custom_event_name", numeric_key_user, nil)
|
@@ -473,6 +495,26 @@ describe LaunchDarkly::LDClient do
|
|
473
495
|
end
|
474
496
|
end
|
475
497
|
|
498
|
+
describe '#alias' do
|
499
|
+
it "queues up an alias event" do
|
500
|
+
expect(event_processor).to receive(:add_event).with(hash_including(
|
501
|
+
kind: "alias", key: user[:key], contextKind: "user", previousKey: user_anonymous[:key], previousContextKind: "anonymousUser"))
|
502
|
+
client.alias(user, user_anonymous)
|
503
|
+
end
|
504
|
+
|
505
|
+
it "does not send an event, and logs a warning, if user is nil" do
|
506
|
+
expect(event_processor).not_to receive(:add_event)
|
507
|
+
expect(logger).to receive(:warn)
|
508
|
+
client.alias(nil, nil)
|
509
|
+
end
|
510
|
+
|
511
|
+
it "does not send an event, and logs a warning, if user key is nil" do
|
512
|
+
expect(event_processor).not_to receive(:add_event)
|
513
|
+
expect(logger).to receive(:warn)
|
514
|
+
client.alias(user_without_key, user_without_key)
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
476
518
|
describe '#identify' do
|
477
519
|
it "queues up an identify event" do
|
478
520
|
expect(event_processor).to receive(:add_event).with(hash_including(kind: "identify", key: user[:key], user: user))
|
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.1
|
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-15 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.0.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.6
|