launchdarkly-server-sdk 6.1.1 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 631066b2968b323ed2e12aed1565c76cf1f8bb6284e8372121771577b49bda18
4
- data.tar.gz: 3e6b7c3b9769c917996e844c598796912f19c20e796692b877a885886d8fa070
3
+ metadata.gz: 1ee671939e04661c649d31502496cc33cd8844002c9af06864db9f322a81ec56
4
+ data.tar.gz: f5f77f10a6c487e1846f8d86201e9456b04bfb03950c751a328a7d3b9c3b8597
5
5
  SHA512:
6
- metadata.gz: 5a885a5714a9cc157e295dd1a88a353a546847a702a771b4edf49f33c081eb19c4141ee7a0b4a8004620c1a4c20ef0111af0c0900ee3a28845f29e2385ebd13c
7
- data.tar.gz: 6e604eac4e997431d09257bf97ec115f3a130293e4bc83fa08254baaa921eb28a2230e5bc2bd080c855293e773331e5eed006741db6cd9943132a1f2a1dc243d
6
+ metadata.gz: 6dc025a98ed3ab25bfe14baa878ff36beb34875caafee9c1979c724d58af9d08ca2596615cb31819dbb150fdeffd17b597f76e19cf2f09962dc1095ffe686ea7
7
+ data.tar.gz: 5d22b72feb1b46e7537f00b3959c72368d94c6606db640df1a7a9b17e1b01b952f1d34ea260e8d20b661fdee558975a9e034087e0d785aacd297d20ab15e9944
data/.circleci/config.yml CHANGED
@@ -20,7 +20,7 @@ jobs:
20
20
  LD_RELEASE_DOCS_TITLE: ""
21
21
  LD_RELEASE_PROJECT: "ruby-server-sdk"
22
22
  LD_RELEASE_PROJECT_TEMPLATE: "ruby"
23
- LD_RELEASE_VERSION: "6.1.1"
23
+ LD_RELEASE_VERSION: "6.2.0"
24
24
  LD_SKIP_DATABASE_TESTS: "1"
25
25
  steps:
26
26
  - checkout
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
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.1.1] - 2021-05-27
6
+ ### Fixed:
7
+ - 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.
8
+
5
9
  ## [6.1.0] - 2021-02-04
6
10
  ### Added:
7
11
  - Added the `alias` method. This can be used to associate two user objects for analytics purposes by generating an alias event.
@@ -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
- @@fallthrough
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
- new(:RULE_MATCH, rule_index, rule_id, nil, nil)
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
- "RULE_MATCH(#{@rule_index},#{@rule_id})"
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
- { kind: @kind, ruleIndex: @rule_index, ruleId: @rule_id }
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)
@@ -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
- bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
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][:variation]
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
- hash_key = "%s.%s.%s" % [key, salt, id_hash]
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
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "6.1.1"
2
+ VERSION = "6.2.0"
3
3
  end
@@ -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
- it "matches bucket" do
62
- user = { key: "userkey" }
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
- # First verify that with our test inputs, the bucket value will be greater than zero and less than 100000,
67
- # so we can construct a rollout whose second bucket just barely contains that value
68
- bucket_value = (subject.bucket_user(user, flag_key, "key", salt) * 100000).truncate()
69
- expect(bucket_value).to be > 0
70
- expect(bucket_value).to be < 100000
71
-
72
- bad_variation_a = 0
73
- matched_variation = 1
74
- bad_variation_b = 2
166
+
75
167
  rule = {
76
168
  rollout: {
169
+ seed: seed,
170
+ kind: 'experiment',
77
171
  variations: [
78
- { variation: bad_variation_a, weight: bucket_value }, # end of bucket range is not inclusive, so it will *not* match the target value
79
- { variation: matched_variation, weight: 1 }, # size of this bucket is 1, so it only matches that specific value
80
- { variation: bad_variation_b, weight: 100000 - (bucket_value + 1) }
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, user)
87
- expect(result_variation).to be matched_variation
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
@@ -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
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.1.1
4
+ version: 6.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-27 00:00:00.000000000 Z
11
+ date: 2021-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb
@@ -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