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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd5b1ccdc654b9ca033547f366d12fe0dde0d25245459de79f9cf1e3ed0e9b79
4
- data.tar.gz: ee6461f8365f0df76055b8c1e5e71b58934df15b24bba22c8b61eb99619b21dc
3
+ metadata.gz: 0c3e33076372820b2ecde009b3effb174d633fee79cb700380e1b887f6f1876c
4
+ data.tar.gz: 30b62850e576db2710058de74bf28c1f95ef72e79479326df6214577c343d100
5
5
  SHA512:
6
- metadata.gz: d6723297fc581f47325f0147f915efa543f1daf27d01af6d63a13546a5d18e800743060478e9f40c62083ceae5bcd8acfaa8c84d83fc870b893069ae51a9f519
7
- data.tar.gz: 89b4885e33f1ebf61fd88b4a65c4da060087901aaf9772100875888b0983f420099b23971c6b8ba8d71697da410daf0b43e88c946192cb8899682aff9b77a64d
6
+ metadata.gz: b93a893777b34b7eb0774d150cbef556166852afb9bdebed78293b62ddc0c7f8639a4a789dc74655d461120b8720123ff687d851d873d2074ced86611b635cad
7
+ data.tar.gz: 90aa095737b094516659bd63b6987a6a523e5f20a83eeef3793ae9c667b5d950c7f494e7a7e44d655a5f7e6b2a0865e1cbeca0d35c8ec16ee8b084a7756027da
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.0.0"
23
+ LD_RELEASE_VERSION: "6.2.1"
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,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
- @@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)
@@ -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].nil? ? nil : event[:user][:key]
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].nil? ? nil : event[:user][:key].to_s,
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].nil? ? nil : event[:user][:key]
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
- 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)
@@ -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
@@ -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?
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "6.0.0"
2
+ VERSION = "6.2.1"
3
3
  end
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
- 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
@@ -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 "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
143
149
  config.feature_store.init({ LaunchDarkly::FEATURES => {} })
144
150
  config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
145
- expect(event_processor).to receive(:add_event).with(hash_including(
146
- kind: "feature",
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 key is nil" do
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
- user: bad_user,
167
- value: "default",
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.0.0
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-01-26 00:00:00.000000000 Z
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