launchdarkly-server-sdk 5.5.12 → 5.6.0

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
  SHA1:
3
- metadata.gz: 6711ad265d73300d06cb1a287c76ae1981e9c1e7
4
- data.tar.gz: 27c72e98745eb679e91a0691766991277490995f
3
+ metadata.gz: 502749fe2b55f4a3116ea223872835e76f7f698b
4
+ data.tar.gz: 9ab5eae90b672dc39db4c3487bd67aa32878b467
5
5
  SHA512:
6
- metadata.gz: 5f0a6c09dd0cff741849392b32d75ac469a08116d918ebaeff2020693d7971bdf5082fee42fe310298fdfb20051da3547e98964fec88c952a3351d1642b5a4c8
7
- data.tar.gz: 4f4474d63efe13be2db68977b51b1f7d65217415f740823e439568980c28713e0bbdcaab83a7e23c32d0889eed32fcd6cf0778d8dd1e15e0d573994081219157
6
+ metadata.gz: a4f9f6642ab989aab6069924fc8b9f543123672cf475be58b72e1410a2563f246ce40d0cfef34dad0c77c37e28d695d05368c719df889eb5766d3618b19848b3
7
+ data.tar.gz: 8c3af4780de0251380f194c8e4c4c3ce359f2a1612763e45a2a8b2eadab3d0dd20f83d5f0d60f8efb45759f5f8a4af7ccc6143085d2999065e4ca8791cf50291
@@ -0,0 +1,17 @@
1
+ repo:
2
+ public: ruby-server-sdk
3
+ private: ruby-server-sdk-private
4
+
5
+ publications:
6
+ - url: https://rubygems.org/gems/launchdarkly-server-sdk
7
+ description: RubyGems
8
+ - url: https://www.rubydoc.info/gems/launchdarkly-server-sdk
9
+ description: documentation
10
+
11
+ template:
12
+ name: ruby
13
+ env:
14
+ LD_SKIP_DATABASE_TESTS: 1 # Don't run Redis/Consul/DynamoDB tests in release; they are run in CI
15
+
16
+ sdk:
17
+ displayName: "Ruby"
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
+ ## [5.6.0] - 2019-08-20
6
+ ### Added:
7
+ - Added support for upcoming LaunchDarkly experimentation features. See `LDClient.track()`.
8
+
5
9
  ## [5.5.12] - 2019-08-05
6
10
  ### Fixed:
7
11
  - Under conditions where analytics events are being generated at an extremely high rate (for instance, if an application is evaluating a flag repeatedly in a tight loop on many threads), it was possible for the internal event processing logic to fall behind on processing the events, causing them to use more and more memory. The logic has been changed to drop events if necessary so that besides the existing limit on the number of events waiting to be sent to LaunchDarkly (`config.capacity`), the same limit also applies on the number of events that are waiting to be processed by the worker thread that decides whether or not to send them to LaunchDarkly. If that limit is exceeded, this warning message will be logged once: "Events are being produced faster than they can be processed; some events will be dropped". Under normal conditions this should never happen; this change is meant to avoid a concurrency bottleneck in applications that are already so busy that thread starvation is likely.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- launchdarkly-server-sdk (5.5.7)
4
+ launchdarkly-server-sdk (5.6.0)
5
5
  concurrent-ruby (~> 1.0)
6
6
  json (>= 1.8, < 3)
7
7
  ld-eventsource (= 1.0.1)
@@ -199,7 +199,7 @@ module LaunchDarkly
199
199
 
200
200
  # Evaluates a feature flag and returns an EvalResult. The result.value will be nil if the flag returns
201
201
  # the default value. Error conditions produce a result with an error reason, not an exception.
202
- def evaluate(flag, user, store, logger)
202
+ def evaluate(flag, user, store, logger, event_factory)
203
203
  if user.nil? || user[:key].nil?
204
204
  return EvalResult.new(error_result('USER_NOT_SPECIFIED'), [])
205
205
  end
@@ -207,16 +207,16 @@ module LaunchDarkly
207
207
  sanitized_user = Util.stringify_attrs(user, USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION)
208
208
 
209
209
  events = []
210
- detail = eval_internal(flag, sanitized_user, store, events, logger)
210
+ detail = eval_internal(flag, sanitized_user, store, events, logger, event_factory)
211
211
  return EvalResult.new(detail, events)
212
212
  end
213
213
 
214
- def eval_internal(flag, user, store, events, logger)
214
+ def eval_internal(flag, user, store, events, logger, event_factory)
215
215
  if !flag[:on]
216
216
  return get_off_value(flag, { kind: 'OFF' }, logger)
217
217
  end
218
218
 
219
- prereq_failure_reason = check_prerequisites(flag, user, store, events, logger)
219
+ prereq_failure_reason = check_prerequisites(flag, user, store, events, logger, event_factory)
220
220
  if !prereq_failure_reason.nil?
221
221
  return get_off_value(flag, prereq_failure_reason, logger)
222
222
  end
@@ -249,7 +249,7 @@ module LaunchDarkly
249
249
  return EvaluationDetail.new(nil, nil, { kind: 'FALLTHROUGH' })
250
250
  end
251
251
 
252
- def check_prerequisites(flag, user, store, events, logger)
252
+ def check_prerequisites(flag, user, store, events, logger, event_factory)
253
253
  (flag[:prerequisites] || []).each do |prerequisite|
254
254
  prereq_ok = true
255
255
  prereq_key = prerequisite[:key]
@@ -260,23 +260,13 @@ module LaunchDarkly
260
260
  prereq_ok = false
261
261
  else
262
262
  begin
263
- prereq_res = eval_internal(prereq_flag, user, store, events, logger)
263
+ prereq_res = eval_internal(prereq_flag, user, store, events, logger, event_factory)
264
264
  # Note that if the prerequisite flag is off, we don't consider it a match no matter what its
265
265
  # off variation was. But we still need to evaluate it in order to generate an event.
266
266
  if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
267
267
  prereq_ok = false
268
268
  end
269
- event = {
270
- kind: "feature",
271
- key: prereq_key,
272
- user: user,
273
- variation: prereq_res.variation_index,
274
- value: prereq_res.value,
275
- version: prereq_flag[:version],
276
- prereqOf: flag[:key],
277
- trackEvents: prereq_flag[:trackEvents],
278
- debugEventsUntilDate: prereq_flag[:debugEventsUntilDate]
279
- }
269
+ event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag)
280
270
  events.push(event)
281
271
  rescue => exn
282
272
  Util.log_exception(logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
@@ -456,6 +456,7 @@ module LaunchDarkly
456
456
  else
457
457
  out[:userKey] = event[:user].nil? ? nil : event[:user][:key]
458
458
  end
459
+ out[:metricValue] = event[:metricValue] if event.has_key?(:metricValue)
459
460
  out
460
461
  when "index"
461
462
  {
@@ -0,0 +1,98 @@
1
+
2
+ module LaunchDarkly
3
+ module Impl
4
+ # Event constructors are centralized here to avoid mistakes and repetitive logic.
5
+ # The LDClient owns two instances of EventFactory: one that always embeds evaluation reasons
6
+ # in the events (for when variation_detail is called) and one that doesn't.
7
+ #
8
+ # Note that these methods do not set the "creationDate" property, because in the Ruby client,
9
+ # that is done by EventProcessor.add_event().
10
+ class EventFactory
11
+ def initialize(with_reasons)
12
+ @with_reasons = with_reasons
13
+ end
14
+
15
+ def new_eval_event(flag, user, detail, default_value, prereq_of_flag = nil)
16
+ add_experiment_data = is_experiment(flag, detail.reason)
17
+ e = {
18
+ kind: 'feature',
19
+ key: flag[:key],
20
+ user: user,
21
+ variation: detail.variation_index,
22
+ value: detail.value,
23
+ default: default_value,
24
+ version: flag[:version]
25
+ }
26
+ # the following properties are handled separately so we don't waste bandwidth on unused keys
27
+ e[:trackEvents] = true if add_experiment_data || flag[:trackEvents]
28
+ e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
29
+ e[:prereqOf] = prereq_of_flag[:key] if !prereq_of_flag.nil?
30
+ e[:reason] = detail.reason if add_experiment_data || @with_reasons
31
+ e
32
+ end
33
+
34
+ def new_default_event(flag, user, default_value, reason)
35
+ e = {
36
+ kind: 'feature',
37
+ key: flag[:key],
38
+ user: user,
39
+ value: default_value,
40
+ default: default_value,
41
+ version: flag[:version]
42
+ }
43
+ e[:trackEvents] = true if flag[:trackEvents]
44
+ e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
45
+ e[:reason] = reason if @with_reasons
46
+ e
47
+ end
48
+
49
+ def new_unknown_flag_event(key, user, default_value, reason)
50
+ e = {
51
+ kind: 'feature',
52
+ key: key,
53
+ user: user,
54
+ value: default_value,
55
+ default: default_value
56
+ }
57
+ e[:reason] = reason if @with_reasons
58
+ e
59
+ end
60
+
61
+ def new_identify_event(user)
62
+ {
63
+ kind: 'identify',
64
+ key: user[:key],
65
+ user: user
66
+ }
67
+ end
68
+
69
+ def new_custom_event(event_name, user, data, metric_value)
70
+ e = {
71
+ kind: 'custom',
72
+ key: event_name,
73
+ user: user
74
+ }
75
+ e[:data] = data if !data.nil?
76
+ e[:metricValue] = metric_value if !metric_value.nil?
77
+ e
78
+ end
79
+
80
+ private
81
+
82
+ def is_experiment(flag, reason)
83
+ return false if !reason
84
+ case reason[:kind]
85
+ when 'RULE_MATCH'
86
+ index = reason[:ruleIndex]
87
+ if !index.nil?
88
+ rules = flag[:rules] || []
89
+ return index >= 0 && index < rules.length && rules[index][:trackEvents]
90
+ end
91
+ when 'FALLTHROUGH'
92
+ return !!flag[:trackEventsFallthrough]
93
+ end
94
+ false
95
+ end
96
+ end
97
+ end
98
+ end
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/impl/event_factory"
1
2
  require "ldclient-rb/impl/store_client_wrapper"
2
3
  require "concurrent/atomics"
3
4
  require "digest/sha1"
@@ -13,6 +14,7 @@ module LaunchDarkly
13
14
  #
14
15
  class LDClient
15
16
  include Evaluation
17
+ include Impl
16
18
  #
17
19
  # Creates a new client instance that connects to LaunchDarkly. A custom
18
20
  # configuration parameter can also supplied to specify advanced options,
@@ -32,6 +34,9 @@ module LaunchDarkly
32
34
  def initialize(sdk_key, config = Config.default, wait_for_sec = 5)
33
35
  @sdk_key = sdk_key
34
36
 
37
+ @event_factory_default = EventFactory.new(false)
38
+ @event_factory_with_reasons = EventFactory.new(true)
39
+
35
40
  # We need to wrap the feature store object with a FeatureStoreClientWrapper in order to add
36
41
  # some necessary logic around updates. Unfortunately, we have code elsewhere that accesses
37
42
  # the feature store through the Config object, so we need to make a new Config that uses
@@ -165,7 +170,7 @@ module LaunchDarkly
165
170
  # @return the variation to show the user, or the default value if there's an an error
166
171
  #
167
172
  def variation(key, user, default)
168
- evaluate_internal(key, user, default, false).value
173
+ evaluate_internal(key, user, default, @event_factory_default).value
169
174
  end
170
175
 
171
176
  #
@@ -192,7 +197,7 @@ module LaunchDarkly
192
197
  # @return [EvaluationDetail] an object describing the result
193
198
  #
194
199
  def variation_detail(key, user, default)
195
- evaluate_internal(key, user, default, true)
200
+ evaluate_internal(key, user, default, @event_factory_with_reasons)
196
201
  end
197
202
 
198
203
  #
@@ -215,7 +220,8 @@ module LaunchDarkly
215
220
  @config.logger.warn("Identify called with nil user or nil user key!")
216
221
  return
217
222
  end
218
- @event_processor.add_event(kind: "identify", key: user[:key], user: user)
223
+ sanitize_user(user)
224
+ @event_processor.add_event(@event_factory_default.new_identify_event(user))
219
225
  end
220
226
 
221
227
  #
@@ -225,18 +231,28 @@ module LaunchDarkly
225
231
  # Note that event delivery is asynchronous, so the event may not actually be sent
226
232
  # until later; see {#flush}.
227
233
  #
234
+ # As of this version’s release date, the LaunchDarkly service does not support the `metricValue`
235
+ # parameter. As a result, specifying `metricValue` will not yet produce any different behavior
236
+ # from omitting it. Refer to the [SDK reference guide](https://docs.launchdarkly.com/docs/ruby-sdk-reference#section-track)
237
+ # for the latest status.
238
+ #
228
239
  # @param event_name [String] The name of the event
229
240
  # @param user [Hash] The user to register; this can have all the same user properties
230
241
  # described in {#variation}
231
- # @param data [Hash] A hash containing any additional data associated with the event
242
+ # @param data [Hash] An optional hash containing any additional data associated with the event
243
+ # @param metric_value [Number] A numeric value used by the LaunchDarkly experimentation
244
+ # feature in numeric custom metrics. Can be omitted if this event is used by only
245
+ # non-numeric metrics. This field will also be returned as part of the custom event
246
+ # for Data Export.
232
247
  # @return [void]
233
248
  #
234
- def track(event_name, user, data)
249
+ def track(event_name, user, data = nil, metric_value = nil)
235
250
  if !user || user[:key].nil?
236
251
  @config.logger.warn("Track called with nil user or nil user key!")
237
252
  return
238
253
  end
239
- @event_processor.add_event(kind: "custom", key: event_name, user: user, data: data)
254
+ sanitize_user(user)
255
+ @event_processor.add_event(@event_factory_default.new_custom_event(event_name, user, data, metric_value))
240
256
  end
241
257
 
242
258
  #
@@ -294,7 +310,7 @@ module LaunchDarkly
294
310
  next
295
311
  end
296
312
  begin
297
- result = evaluate(f, user, @store, @config.logger)
313
+ result = evaluate(f, user, @store, @config.logger, @event_factory_default)
298
314
  state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil,
299
315
  details_only_if_tracked)
300
316
  rescue => exn
@@ -334,7 +350,7 @@ module LaunchDarkly
334
350
  end
335
351
 
336
352
  # @return [EvaluationDetail]
337
- def evaluate_internal(key, user, default, include_reasons_in_events)
353
+ def evaluate_internal(key, user, default, event_factory)
338
354
  if @config.offline?
339
355
  return error_result('CLIENT_NOT_READY', default)
340
356
  end
@@ -344,8 +360,9 @@ module LaunchDarkly
344
360
  @config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
345
361
  else
346
362
  @config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
347
- @event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)
348
- return error_result('CLIENT_NOT_READY', default)
363
+ detail = error_result('CLIENT_NOT_READY', default)
364
+ @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
365
+ return detail
349
366
  end
350
367
  end
351
368
 
@@ -354,20 +371,19 @@ module LaunchDarkly
354
371
  if feature.nil?
355
372
  @config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
356
373
  detail = error_result('FLAG_NOT_FOUND', default)
357
- @event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user,
358
- reason: include_reasons_in_events ? detail.reason : nil)
374
+ @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
359
375
  return detail
360
376
  end
361
377
 
362
378
  unless user
363
379
  @config.logger.error { "[LDClient] Must specify user" }
364
380
  detail = error_result('USER_NOT_SPECIFIED', default)
365
- @event_processor.add_event(make_feature_event(feature, nil, detail, default, include_reasons_in_events))
381
+ @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
366
382
  return detail
367
383
  end
368
384
 
369
385
  begin
370
- res = evaluate(feature, user, @store, @config.logger) # note, evaluate will do its own sanitization
386
+ res = evaluate(feature, user, @store, @config.logger, event_factory)
371
387
  if !res.events.nil?
372
388
  res.events.each do |event|
373
389
  @event_processor.add_event(event)
@@ -377,29 +393,20 @@ module LaunchDarkly
377
393
  if detail.default_value?
378
394
  detail = EvaluationDetail.new(default, nil, detail.reason)
379
395
  end
380
- @event_processor.add_event(make_feature_event(feature, user, detail, default, include_reasons_in_events))
396
+ @event_processor.add_event(event_factory.new_eval_event(feature, user, detail, default))
381
397
  return detail
382
398
  rescue => exn
383
399
  Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
384
400
  detail = error_result('EXCEPTION', default)
385
- @event_processor.add_event(make_feature_event(feature, user, detail, default, include_reasons_in_events))
401
+ @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
386
402
  return detail
387
403
  end
388
404
  end
389
405
 
390
- def make_feature_event(flag, user, detail, default, with_reasons)
391
- {
392
- kind: "feature",
393
- key: flag[:key],
394
- user: user,
395
- variation: detail.variation_index,
396
- value: detail.value,
397
- default: default,
398
- version: flag[:version],
399
- trackEvents: flag[:trackEvents],
400
- debugEventsUntilDate: flag[:debugEventsUntilDate],
401
- reason: with_reasons ? detail.reason : nil
402
- }
406
+ def sanitize_user(user)
407
+ if user[:key]
408
+ user[:key] = user[:key].to_s
409
+ end
403
410
  end
404
411
  end
405
412
 
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "5.5.12"
2
+ VERSION = "5.6.0"
3
3
  end
@@ -7,6 +7,8 @@ describe LaunchDarkly::Evaluation do
7
7
 
8
8
  let(:features) { LaunchDarkly::InMemoryFeatureStore.new }
9
9
 
10
+ let(:factory) { LaunchDarkly::Impl::EventFactory.new(false) }
11
+
10
12
  let(:user) {
11
13
  {
12
14
  key: "userkey",
@@ -36,7 +38,7 @@ describe LaunchDarkly::Evaluation do
36
38
  }
37
39
  user = { key: 'x' }
38
40
  detail = LaunchDarkly::EvaluationDetail.new('b', 1, { kind: 'OFF' })
39
- result = evaluate(flag, user, features, logger)
41
+ result = evaluate(flag, user, features, logger, factory)
40
42
  expect(result.detail).to eq(detail)
41
43
  expect(result.events).to eq([])
42
44
  end
@@ -50,7 +52,7 @@ describe LaunchDarkly::Evaluation do
50
52
  }
51
53
  user = { key: 'x' }
52
54
  detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'OFF' })
53
- result = evaluate(flag, user, features, logger)
55
+ result = evaluate(flag, user, features, logger, factory)
54
56
  expect(result.detail).to eq(detail)
55
57
  expect(result.events).to eq([])
56
58
  end
@@ -66,7 +68,7 @@ describe LaunchDarkly::Evaluation do
66
68
  user = { key: 'x' }
67
69
  detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
68
70
  { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
69
- result = evaluate(flag, user, features, logger)
71
+ result = evaluate(flag, user, features, logger, factory)
70
72
  expect(result.detail).to eq(detail)
71
73
  expect(result.events).to eq([])
72
74
  end
@@ -82,7 +84,7 @@ describe LaunchDarkly::Evaluation do
82
84
  user = { key: 'x' }
83
85
  detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
84
86
  { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
85
- result = evaluate(flag, user, features, logger)
87
+ result = evaluate(flag, user, features, logger, factory)
86
88
  expect(result.detail).to eq(detail)
87
89
  expect(result.events).to eq([])
88
90
  end
@@ -99,7 +101,7 @@ describe LaunchDarkly::Evaluation do
99
101
  user = { key: 'x' }
100
102
  detail = LaunchDarkly::EvaluationDetail.new('b', 1,
101
103
  { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'badfeature' })
102
- result = evaluate(flag, user, features, logger)
104
+ result = evaluate(flag, user, features, logger, factory)
103
105
  expect(result.detail).to eq(detail)
104
106
  expect(result.events).to eq([])
105
107
  end
@@ -127,10 +129,9 @@ describe LaunchDarkly::Evaluation do
127
129
  detail = LaunchDarkly::EvaluationDetail.new('b', 1,
128
130
  { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' })
129
131
  events_should_be = [{
130
- kind: 'feature', key: 'feature1', user: user, variation: nil, value: nil, version: 2, prereqOf: 'feature0',
131
- trackEvents: nil, debugEventsUntilDate: nil
132
+ kind: 'feature', key: 'feature1', user: user, value: nil, default: nil, variation: nil, version: 2, prereqOf: 'feature0'
132
133
  }]
133
- result = evaluate(flag, user, features, logger)
134
+ result = evaluate(flag, user, features, logger, factory)
134
135
  expect(result.detail).to eq(detail)
135
136
  expect(result.events).to eq(events_should_be)
136
137
  end
@@ -159,10 +160,9 @@ describe LaunchDarkly::Evaluation do
159
160
  detail = LaunchDarkly::EvaluationDetail.new('b', 1,
160
161
  { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' })
161
162
  events_should_be = [{
162
- kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', version: 2, prereqOf: 'feature0',
163
- trackEvents: nil, debugEventsUntilDate: nil
163
+ kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
164
164
  }]
165
- result = evaluate(flag, user, features, logger)
165
+ result = evaluate(flag, user, features, logger, factory)
166
166
  expect(result.detail).to eq(detail)
167
167
  expect(result.events).to eq(events_should_be)
168
168
  end
@@ -189,10 +189,9 @@ describe LaunchDarkly::Evaluation do
189
189
  detail = LaunchDarkly::EvaluationDetail.new('b', 1,
190
190
  { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' })
191
191
  events_should_be = [{
192
- kind: 'feature', key: 'feature1', user: user, variation: 0, value: 'd', version: 2, prereqOf: 'feature0',
193
- trackEvents: nil, debugEventsUntilDate: nil
192
+ kind: 'feature', key: 'feature1', user: user, variation: 0, value: 'd', default: nil, version: 2, prereqOf: 'feature0'
194
193
  }]
195
- result = evaluate(flag, user, features, logger)
194
+ result = evaluate(flag, user, features, logger, factory)
196
195
  expect(result.detail).to eq(detail)
197
196
  expect(result.events).to eq(events_should_be)
198
197
  end
@@ -218,10 +217,9 @@ describe LaunchDarkly::Evaluation do
218
217
  user = { key: 'x' }
219
218
  detail = LaunchDarkly::EvaluationDetail.new('a', 0, { kind: 'FALLTHROUGH' })
220
219
  events_should_be = [{
221
- kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', version: 2, prereqOf: 'feature0',
222
- trackEvents: nil, debugEventsUntilDate: nil
220
+ kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
223
221
  }]
224
- result = evaluate(flag, user, features, logger)
222
+ result = evaluate(flag, user, features, logger, factory)
225
223
  expect(result.detail).to eq(detail)
226
224
  expect(result.events).to eq(events_should_be)
227
225
  end
@@ -236,7 +234,7 @@ describe LaunchDarkly::Evaluation do
236
234
  }
237
235
  user = { key: 'userkey' }
238
236
  detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
239
- result = evaluate(flag, user, features, logger)
237
+ result = evaluate(flag, user, features, logger, factory)
240
238
  expect(result.detail).to eq(detail)
241
239
  expect(result.events).to eq([])
242
240
  end
@@ -251,7 +249,7 @@ describe LaunchDarkly::Evaluation do
251
249
  }
252
250
  user = { key: 'userkey' }
253
251
  detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
254
- result = evaluate(flag, user, features, logger)
252
+ result = evaluate(flag, user, features, logger, factory)
255
253
  expect(result.detail).to eq(detail)
256
254
  expect(result.events).to eq([])
257
255
  end
@@ -266,7 +264,7 @@ describe LaunchDarkly::Evaluation do
266
264
  }
267
265
  user = { key: 'userkey' }
268
266
  detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
269
- result = evaluate(flag, user, features, logger)
267
+ result = evaluate(flag, user, features, logger, factory)
270
268
  expect(result.detail).to eq(detail)
271
269
  expect(result.events).to eq([])
272
270
  end
@@ -281,7 +279,7 @@ describe LaunchDarkly::Evaluation do
281
279
  }
282
280
  user = { key: 'userkey' }
283
281
  detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
284
- result = evaluate(flag, user, features, logger)
282
+ result = evaluate(flag, user, features, logger, factory)
285
283
  expect(result.detail).to eq(detail)
286
284
  expect(result.events).to eq([])
287
285
  end
@@ -299,7 +297,7 @@ describe LaunchDarkly::Evaluation do
299
297
  }
300
298
  user = { key: 'userkey' }
301
299
  detail = LaunchDarkly::EvaluationDetail.new('c', 2, { kind: 'TARGET_MATCH' })
302
- result = evaluate(flag, user, features, logger)
300
+ result = evaluate(flag, user, features, logger, factory)
303
301
  expect(result.detail).to eq(detail)
304
302
  expect(result.events).to eq([])
305
303
  end
@@ -310,7 +308,7 @@ describe LaunchDarkly::Evaluation do
310
308
  user = { key: 'userkey' }
311
309
  detail = LaunchDarkly::EvaluationDetail.new(true, 1,
312
310
  { kind: 'RULE_MATCH', ruleIndex: 0, ruleId: 'ruleid' })
313
- result = evaluate(flag, user, features, logger)
311
+ result = evaluate(flag, user, features, logger, factory)
314
312
  expect(result.detail).to eq(detail)
315
313
  expect(result.events).to eq([])
316
314
  end
@@ -321,7 +319,7 @@ describe LaunchDarkly::Evaluation do
321
319
  user = { key: 'userkey' }
322
320
  detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
323
321
  { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
324
- result = evaluate(flag, user, features, logger)
322
+ result = evaluate(flag, user, features, logger, factory)
325
323
  expect(result.detail).to eq(detail)
326
324
  expect(result.events).to eq([])
327
325
  end
@@ -332,7 +330,7 @@ describe LaunchDarkly::Evaluation do
332
330
  user = { key: 'userkey' }
333
331
  detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
334
332
  { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
335
- result = evaluate(flag, user, features, logger)
333
+ result = evaluate(flag, user, features, logger, factory)
336
334
  expect(result.detail).to eq(detail)
337
335
  expect(result.events).to eq([])
338
336
  end
@@ -343,7 +341,7 @@ describe LaunchDarkly::Evaluation do
343
341
  user = { key: 'userkey' }
344
342
  detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
345
343
  { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
346
- result = evaluate(flag, user, features, logger)
344
+ result = evaluate(flag, user, features, logger, factory)
347
345
  expect(result.detail).to eq(detail)
348
346
  expect(result.events).to eq([])
349
347
  end
@@ -355,7 +353,7 @@ describe LaunchDarkly::Evaluation do
355
353
  user = { key: 'userkey' }
356
354
  detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
357
355
  { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
358
- result = evaluate(flag, user, features, logger)
356
+ result = evaluate(flag, user, features, logger, factory)
359
357
  expect(result.detail).to eq(detail)
360
358
  expect(result.events).to eq([])
361
359
  end
@@ -364,7 +362,7 @@ describe LaunchDarkly::Evaluation do
364
362
  clause = { attribute: 'key', op: 'in', values: ['999'] }
365
363
  flag = boolean_flag_with_clauses([clause])
366
364
  user = { key: 999 }
367
- result = evaluate(flag, user, features, logger)
365
+ result = evaluate(flag, user, features, logger, factory)
368
366
  expect(result.detail.value).to eq(true)
369
367
  end
370
368
 
@@ -375,7 +373,7 @@ describe LaunchDarkly::Evaluation do
375
373
  rollout: { salt: '', variations: [ { weight: 100000, variation: 1 } ] } }
376
374
  flag = boolean_flag_with_rules([rule])
377
375
  user = { key: "userkey", secondary: 999 }
378
- result = evaluate(flag, user, features, logger)
376
+ result = evaluate(flag, user, features, logger, factory)
379
377
  expect(result.detail.reason).to eq({ kind: 'RULE_MATCH', ruleIndex: 0, ruleId: 'ruleid'})
380
378
  end
381
379
  end
@@ -385,28 +383,28 @@ describe LaunchDarkly::Evaluation do
385
383
  user = { key: 'x', name: 'Bob' }
386
384
  clause = { attribute: 'name', op: 'in', values: ['Bob'] }
387
385
  flag = boolean_flag_with_clauses([clause])
388
- expect(evaluate(flag, user, features, logger).detail.value).to be true
386
+ expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
389
387
  end
390
388
 
391
389
  it "can match custom attribute" do
392
390
  user = { key: 'x', name: 'Bob', custom: { legs: 4 } }
393
391
  clause = { attribute: 'legs', op: 'in', values: [4] }
394
392
  flag = boolean_flag_with_clauses([clause])
395
- expect(evaluate(flag, user, features, logger).detail.value).to be true
393
+ expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
396
394
  end
397
395
 
398
396
  it "returns false for missing attribute" do
399
397
  user = { key: 'x', name: 'Bob' }
400
398
  clause = { attribute: 'legs', op: 'in', values: [4] }
401
399
  flag = boolean_flag_with_clauses([clause])
402
- expect(evaluate(flag, user, features, logger).detail.value).to be false
400
+ expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
403
401
  end
404
402
 
405
403
  it "returns false for unknown operator" do
406
404
  user = { key: 'x', name: 'Bob' }
407
405
  clause = { attribute: 'name', op: 'unknown', values: [4] }
408
406
  flag = boolean_flag_with_clauses([clause])
409
- expect(evaluate(flag, user, features, logger).detail.value).to be false
407
+ expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
410
408
  end
411
409
 
412
410
  it "does not stop evaluating rules after clause with unknown operator" do
@@ -416,14 +414,14 @@ describe LaunchDarkly::Evaluation do
416
414
  clause1 = { attribute: 'name', op: 'in', values: ['Bob'] }
417
415
  rule1 = { clauses: [ clause1 ], variation: 1 }
418
416
  flag = boolean_flag_with_rules([rule0, rule1])
419
- expect(evaluate(flag, user, features, logger).detail.value).to be true
417
+ expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
420
418
  end
421
419
 
422
420
  it "can be negated" do
423
421
  user = { key: 'x', name: 'Bob' }
424
422
  clause = { attribute: 'name', op: 'in', values: ['Bob'], negate: true }
425
423
  flag = boolean_flag_with_clauses([clause])
426
- expect(evaluate(flag, user, features, logger).detail.value).to be false
424
+ expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
427
425
  end
428
426
 
429
427
  it "retrieves segment from segment store for segmentMatch operator" do
@@ -438,14 +436,14 @@ describe LaunchDarkly::Evaluation do
438
436
  user = { key: 'userkey' }
439
437
  clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
440
438
  flag = boolean_flag_with_clauses([clause])
441
- expect(evaluate(flag, user, features, logger).detail.value).to be true
439
+ expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
442
440
  end
443
441
 
444
442
  it "falls through with no errors if referenced segment is not found" do
445
443
  user = { key: 'userkey' }
446
444
  clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
447
445
  flag = boolean_flag_with_clauses([clause])
448
- expect(evaluate(flag, user, features, logger).detail.value).to be false
446
+ expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
449
447
  end
450
448
 
451
449
  it "can be negated" do
@@ -454,7 +452,7 @@ describe LaunchDarkly::Evaluation do
454
452
  flag = boolean_flag_with_clauses([clause])
455
453
  expect {
456
454
  clause[:negate] = true
457
- }.to change {evaluate(flag, user, features, logger).detail.value}.from(true).to(false)
455
+ }.to change {evaluate(flag, user, features, logger, factory).detail.value}.from(true).to(false)
458
456
  end
459
457
  end
460
458
 
@@ -557,7 +555,7 @@ describe LaunchDarkly::Evaluation do
557
555
  user = { key: 'x', custom: { foo: value1 } }
558
556
  clause = { attribute: 'foo', op: op, values: [value2] }
559
557
  flag = boolean_flag_with_clauses([clause])
560
- expect(evaluate(flag, user, features, logger).detail.value).to be shouldBe
558
+ expect(evaluate(flag, user, features, logger, factory).detail.value).to be shouldBe
561
559
  end
562
560
  end
563
561
  end
@@ -648,7 +646,7 @@ describe LaunchDarkly::Evaluation do
648
646
  features.upsert(LaunchDarkly::SEGMENTS, segment)
649
647
  clause = make_segment_match_clause(segment)
650
648
  flag = boolean_flag_with_clauses([clause])
651
- evaluate(flag, user, features, logger).detail.value
649
+ evaluate(flag, user, features, logger, factory).detail.value
652
650
  end
653
651
 
654
652
  it 'explicitly includes user' do
data/spec/events_spec.rb CHANGED
@@ -342,7 +342,7 @@ describe LaunchDarkly::EventProcessor do
342
342
 
343
343
  it "queues custom event with user" do
344
344
  @ep = subject.new("sdk_key", default_config, hc)
345
- e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } }
345
+ e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" }, metricValue: 1.5 }
346
346
  @ep.add_event(e)
347
347
 
348
348
  output = flush_and_get_events
@@ -565,6 +565,7 @@ describe LaunchDarkly::EventProcessor do
565
565
  else
566
566
  out[:user] = inline_user
567
567
  end
568
+ out[:metricValue] = e[:metricValue] if e.has_key?(:metricValue)
568
569
  out
569
570
  end
570
571
 
@@ -25,6 +25,22 @@ describe LaunchDarkly::LDClient do
25
25
  }
26
26
  }
27
27
  end
28
+ let(:numeric_key_user) do
29
+ {
30
+ key: 33,
31
+ custom: {
32
+ groups: [ "microsoft", "google" ]
33
+ }
34
+ }
35
+ end
36
+ let(:sanitized_numeric_key_user) do
37
+ {
38
+ key: "33",
39
+ custom: {
40
+ groups: [ "microsoft", "google" ]
41
+ }
42
+ }
43
+ end
28
44
  let(:user_without_key) do
29
45
  { name: "Keyless Joe" }
30
46
  end
@@ -91,7 +107,6 @@ describe LaunchDarkly::LDClient do
91
107
  key: "key",
92
108
  version: 100,
93
109
  user: nil,
94
- variation: nil,
95
110
  value: "default",
96
111
  default: "default",
97
112
  trackEvents: true,
@@ -109,7 +124,6 @@ describe LaunchDarkly::LDClient do
109
124
  key: "key",
110
125
  version: 100,
111
126
  user: bad_user,
112
- variation: nil,
113
127
  value: "default",
114
128
  default: "default",
115
129
  trackEvents: true,
@@ -117,6 +131,61 @@ describe LaunchDarkly::LDClient do
117
131
  ))
118
132
  client.variation("key", bad_user, "default")
119
133
  end
134
+
135
+ it "sets trackEvents and reason if trackEvents is set for matched rule" do
136
+ flag = {
137
+ key: 'flag',
138
+ on: true,
139
+ variations: [ 'value' ],
140
+ version: 100,
141
+ rules: [
142
+ clauses: [
143
+ { attribute: 'key', op: 'in', values: [ user[:key] ] }
144
+ ],
145
+ variation: 0,
146
+ id: 'id',
147
+ trackEvents: true
148
+ ]
149
+ }
150
+ config.feature_store.init({ LaunchDarkly::FEATURES => {} })
151
+ config.feature_store.upsert(LaunchDarkly::FEATURES, flag)
152
+ expect(event_processor).to receive(:add_event).with(hash_including(
153
+ kind: 'feature',
154
+ key: 'flag',
155
+ version: 100,
156
+ user: user,
157
+ value: 'value',
158
+ default: 'default',
159
+ trackEvents: true,
160
+ reason: { kind: 'RULE_MATCH', ruleIndex: 0, ruleId: 'id' }
161
+ ))
162
+ client.variation('flag', user, 'default')
163
+ end
164
+
165
+ it "sets trackEvents and reason if trackEventsFallthrough is set and we fell through" do
166
+ flag = {
167
+ key: 'flag',
168
+ on: true,
169
+ variations: [ 'value' ],
170
+ fallthrough: { variation: 0 },
171
+ version: 100,
172
+ rules: [],
173
+ trackEventsFallthrough: true
174
+ }
175
+ config.feature_store.init({ LaunchDarkly::FEATURES => {} })
176
+ config.feature_store.upsert(LaunchDarkly::FEATURES, flag)
177
+ expect(event_processor).to receive(:add_event).with(hash_including(
178
+ kind: 'feature',
179
+ key: 'flag',
180
+ version: 100,
181
+ user: user,
182
+ value: 'value',
183
+ default: 'default',
184
+ trackEvents: true,
185
+ reason: { kind: 'FALLTHROUGH' }
186
+ ))
187
+ client.variation('flag', user, 'default')
188
+ end
120
189
  end
121
190
 
122
191
  describe '#variation_detail' do
@@ -338,6 +407,17 @@ describe LaunchDarkly::LDClient do
338
407
  client.track("custom_event_name", user, 42)
339
408
  end
340
409
 
410
+ it "can include a metric value" do
411
+ expect(event_processor).to receive(:add_event).with(hash_including(
412
+ kind: "custom", key: "custom_event_name", user: user, metricValue: 1.5))
413
+ client.track("custom_event_name", user, nil, 1.5)
414
+ end
415
+
416
+ it "sanitizes the user in the event" do
417
+ expect(event_processor).to receive(:add_event).with(hash_including(user: sanitized_numeric_key_user))
418
+ client.track("custom_event_name", numeric_key_user, nil)
419
+ end
420
+
341
421
  it "does not send an event, and logs a warning, if user is nil" do
342
422
  expect(event_processor).not_to receive(:add_event)
343
423
  expect(logger).to receive(:warn)
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: 5.5.12
4
+ version: 5.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-05 00:00:00.000000000 Z
11
+ date: 2019-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb
@@ -239,6 +239,7 @@ files:
239
239
  - ".github/ISSUE_TEMPLATE/feature_request.md"
240
240
  - ".gitignore"
241
241
  - ".hound.yml"
242
+ - ".ldrelease/config.yml"
242
243
  - ".rspec"
243
244
  - ".rubocop.yml"
244
245
  - ".simplecov"
@@ -265,6 +266,7 @@ files:
265
266
  - lib/ldclient-rb/file_data_source.rb
266
267
  - lib/ldclient-rb/flags_state.rb
267
268
  - lib/ldclient-rb/impl.rb
269
+ - lib/ldclient-rb/impl/event_factory.rb
268
270
  - lib/ldclient-rb/impl/integrations/consul_impl.rb
269
271
  - lib/ldclient-rb/impl/integrations/dynamodb_impl.rb
270
272
  - lib/ldclient-rb/impl/integrations/redis_impl.rb
@@ -342,7 +344,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
342
344
  version: '0'
343
345
  requirements: []
344
346
  rubyforge_project:
345
- rubygems_version: 2.5.2
347
+ rubygems_version: 2.5.2.3
346
348
  signing_key:
347
349
  specification_version: 4
348
350
  summary: LaunchDarkly SDK for Ruby