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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 660fcdc21e7d6c74b118c02e180fc0a2a08279d4c936f4d01b45244e2df88732
4
- data.tar.gz: 0fc47ce28bb8e66ef1e0c93b06499990366c749adc32250b368d7b25573c971f
3
+ metadata.gz: eef5d4b08484f02739bfab4d151f5212624cba09157e756e0126603ccacf0a31
4
+ data.tar.gz: c0510cc29bbb40dd18dc3428803d0b6a33d1c9df868a4ac43caa801c9e3329f0
5
5
  SHA512:
6
- metadata.gz: 8c93e00623d1e6f954974a3128bd19c31b669eaa440460d1b14c30f4b9581b5aee2c06cf040f2fcbdd815a38384388d3b606c65834ffc4057ade87a083d1eda3
7
- data.tar.gz: ef7d3c86a418ab1bb07e3b444107fc92d0472e9fe1a2ed9ef87a57527d72a2c63ce6007ad39dd5ff22f242ea420c4524bb292d8a9e3e18d8ebadb4042d82e1d5
6
+ metadata.gz: 338a0e3189764f8c5cfe1034ac6602050430a70468da33688953385322c8c94df60919ad04707e7504d6653a51b6928ea0d0fb121518fbdb5fd7ade9e418fa6e
7
+ data.tar.gz: 94d7650238107eadc7cb2846b826816fe01fe6ee3f421a1a396821681ae8e2b3854c6675bede18cf7a7abf391164123b838dca615bb8404910e07ce350071d02
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.0"
23
+ LD_RELEASE_VERSION: "6.2.2"
24
24
  LD_SKIP_DATABASE_TESTS: "1"
25
25
  steps:
26
26
  - checkout
@@ -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/) and clicking "submit a request", or by emailing 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
 
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Support request
4
+ url: https://support.launchdarkly.com/hc/en-us/requests/new
5
+ about: File your support requests with LaunchDarkly's support team
data/.gitignore CHANGED
@@ -13,3 +13,4 @@
13
13
  mkmf.log
14
14
  *.gem
15
15
  .DS_Store
16
+ Gemfile.lock
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:2.2.7
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
- @@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)
@@ -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].nil? ? nil : event[:user][:key]
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].nil? ? nil : event[:user][:key].to_s,
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].nil? ? nil : event[:user][:key]
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
- 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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "6.1.0"
2
+ VERSION = "6.2.2"
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
@@ -145,19 +145,11 @@ describe LaunchDarkly::LDClient do
145
145
  client.variation("key", user, "default")
146
146
  end
147
147
 
148
- it "queues a feature event for an existing feature when user is nil" do
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).to receive(:add_event).with(hash_including(
152
- kind: "feature",
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 "queues a feature event for an existing feature when user key is nil" do
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).to receive(:add_event).with(hash_including(
187
- kind: "feature",
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
@@ -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.1.0
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-02-04 00:00:00.000000000 Z
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