launchdarkly-server-sdk 6.4.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +102 -56
  3. data/lib/ldclient-rb/context.rb +487 -0
  4. data/lib/ldclient-rb/evaluation_detail.rb +20 -20
  5. data/lib/ldclient-rb/events.rb +77 -132
  6. data/lib/ldclient-rb/flags_state.rb +4 -4
  7. data/lib/ldclient-rb/impl/big_segments.rb +17 -17
  8. data/lib/ldclient-rb/impl/context.rb +96 -0
  9. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  10. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  11. data/lib/ldclient-rb/impl/evaluator.rb +379 -131
  12. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  13. data/lib/ldclient-rb/impl/evaluator_helpers.rb +28 -31
  14. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  15. data/lib/ldclient-rb/impl/event_sender.rb +6 -6
  16. data/lib/ldclient-rb/impl/event_summarizer.rb +12 -7
  17. data/lib/ldclient-rb/impl/event_types.rb +18 -30
  18. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
  19. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +29 -29
  20. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  21. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +92 -12
  22. data/lib/ldclient-rb/impl/model/clause.rb +39 -0
  23. data/lib/ldclient-rb/impl/model/feature_flag.rb +213 -0
  24. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +8 -121
  25. data/lib/ldclient-rb/impl/model/segment.rb +126 -0
  26. data/lib/ldclient-rb/impl/model/serialization.rb +52 -12
  27. data/lib/ldclient-rb/impl/repeating_task.rb +1 -1
  28. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  29. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  30. data/lib/ldclient-rb/impl/util.rb +2 -2
  31. data/lib/ldclient-rb/in_memory_store.rb +2 -2
  32. data/lib/ldclient-rb/integrations/consul.rb +1 -1
  33. data/lib/ldclient-rb/integrations/dynamodb.rb +1 -1
  34. data/lib/ldclient-rb/integrations/file_data.rb +3 -3
  35. data/lib/ldclient-rb/integrations/redis.rb +4 -4
  36. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +218 -62
  37. data/lib/ldclient-rb/integrations/test_data.rb +16 -12
  38. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +9 -9
  39. data/lib/ldclient-rb/interfaces.rb +14 -14
  40. data/lib/ldclient-rb/ldclient.rb +94 -144
  41. data/lib/ldclient-rb/memoized_value.rb +1 -1
  42. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  43. data/lib/ldclient-rb/polling.rb +2 -2
  44. data/lib/ldclient-rb/reference.rb +274 -0
  45. data/lib/ldclient-rb/requestor.rb +5 -5
  46. data/lib/ldclient-rb/stream.rb +7 -8
  47. data/lib/ldclient-rb/util.rb +4 -19
  48. data/lib/ldclient-rb/version.rb +1 -1
  49. data/lib/ldclient-rb.rb +2 -3
  50. metadata +34 -17
  51. data/lib/ldclient-rb/file_data_source.rb +0 -23
  52. data/lib/ldclient-rb/newrelic.rb +0 -17
  53. data/lib/ldclient-rb/redis_store.rb +0 -88
  54. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/impl/context_filter"
1
2
  require "ldclient-rb/impl/diagnostic_events"
2
3
  require "ldclient-rb/impl/event_sender"
3
4
  require "ldclient-rb/impl/event_summarizer"
@@ -20,7 +21,7 @@ require "time"
20
21
  # On a separate worker thread, EventDispatcher consumes events from the inbox. These are considered
21
22
  # "input events" because they may or may not actually be sent to LaunchDarkly; most flag evaluation
22
23
  # events are not sent, but are counted and the counters become part of a single summary event.
23
- # EventDispatcher updates those counters, creates "index" events for any users that have not been seen
24
+ # EventDispatcher updates those counters, creates "index" events for any contexts that have not been seen
24
25
  # recently, and places any events that will be sent to LaunchDarkly into the "outbox" queue.
25
26
  #
26
27
  # When it is time to flush events to LaunchDarkly, the contents of the outbox are handed off to
@@ -30,7 +31,7 @@ require "time"
30
31
  module LaunchDarkly
31
32
  module EventProcessorMethods
32
33
  def record_eval_event(
33
- user,
34
+ context,
34
35
  key,
35
36
  version = nil,
36
37
  variation = nil,
@@ -43,20 +44,17 @@ module LaunchDarkly
43
44
  )
44
45
  end
45
46
 
46
- def record_identify_event(user)
47
+ def record_identify_event(context)
47
48
  end
48
49
 
49
50
  def record_custom_event(
50
- user,
51
+ context,
51
52
  key,
52
53
  data = nil,
53
54
  metric_value = nil
54
55
  )
55
56
  end
56
57
 
57
- def record_alias_event(user, previous_user)
58
- end
59
-
60
58
  def flush
61
59
  end
62
60
 
@@ -65,11 +63,7 @@ module LaunchDarkly
65
63
  end
66
64
 
67
65
  MAX_FLUSH_WORKERS = 5
68
- USER_ATTRS_TO_STRINGIFY_FOR_EVENTS = [ :key, :secondary, :ip, :country, :email, :firstName, :lastName,
69
- :avatar, :name ]
70
-
71
66
  private_constant :MAX_FLUSH_WORKERS
72
- private_constant :USER_ATTRS_TO_STRINGIFY_FOR_EVENTS
73
67
 
74
68
  # @private
75
69
  class NullEventProcessor
@@ -81,7 +75,7 @@ module LaunchDarkly
81
75
  end
82
76
 
83
77
  # @private
84
- class FlushUsersMessage
78
+ class FlushContextsMessage
85
79
  end
86
80
 
87
81
  # @private
@@ -93,7 +87,7 @@ module LaunchDarkly
93
87
  def initialize
94
88
  @reply = Concurrent::Semaphore.new(0)
95
89
  end
96
-
90
+
97
91
  def completed
98
92
  @reply.release
99
93
  end
@@ -123,10 +117,10 @@ module LaunchDarkly
123
117
  post_to_inbox(FlushMessage.new)
124
118
  end
125
119
  @flush_task.execute
126
- @users_flush_task = Concurrent::TimerTask.new(execution_interval: config.user_keys_flush_interval) do
127
- post_to_inbox(FlushUsersMessage.new)
120
+ @contexts_flush_task = Concurrent::TimerTask.new(execution_interval: config.context_keys_flush_interval) do
121
+ post_to_inbox(FlushContextsMessage.new)
128
122
  end
129
- @users_flush_task.execute
123
+ @contexts_flush_task.execute
130
124
  if !diagnostic_accumulator.nil?
131
125
  interval = test_properties && test_properties.has_key?(:diagnostic_recording_interval) ?
132
126
  test_properties[:diagnostic_recording_interval] :
@@ -142,7 +136,7 @@ module LaunchDarkly
142
136
  @inbox_full = Concurrent::AtomicBoolean.new(false)
143
137
 
144
138
  event_sender = (test_properties || {})[:event_sender] ||
145
- Impl::EventSender.new(sdk_key, config, client ? client : Util.new_http_client(config.events_uri, config))
139
+ Impl::EventSender.new(sdk_key, config, client || Util.new_http_client(config.events_uri, config))
146
140
 
147
141
  @timestamp_fn = (test_properties || {})[:timestamp_fn] || proc { Impl::Util.current_time_millis }
148
142
 
@@ -150,7 +144,7 @@ module LaunchDarkly
150
144
  end
151
145
 
152
146
  def record_eval_event(
153
- user,
147
+ context,
154
148
  key,
155
149
  version = nil,
156
150
  variation = nil,
@@ -161,26 +155,16 @@ module LaunchDarkly
161
155
  debug_until = nil,
162
156
  prereq_of = nil
163
157
  )
164
- post_to_inbox(LaunchDarkly::Impl::EvalEvent.new(timestamp, user, key, version, variation, value, reason,
158
+ post_to_inbox(LaunchDarkly::Impl::EvalEvent.new(timestamp, context, key, version, variation, value, reason,
165
159
  default, track_events, debug_until, prereq_of))
166
160
  end
167
161
 
168
- def record_identify_event(user)
169
- post_to_inbox(LaunchDarkly::Impl::IdentifyEvent.new(timestamp, user))
162
+ def record_identify_event(context)
163
+ post_to_inbox(LaunchDarkly::Impl::IdentifyEvent.new(timestamp, context))
170
164
  end
171
165
 
172
- def record_custom_event(user, key, data = nil, metric_value = nil)
173
- post_to_inbox(LaunchDarkly::Impl::CustomEvent.new(timestamp, user, key, data, metric_value))
174
- end
175
-
176
- def record_alias_event(user, previous_user)
177
- post_to_inbox(LaunchDarkly::Impl::AliasEvent.new(
178
- timestamp,
179
- user.nil? ? nil : user[:key],
180
- user_to_context_kind(user),
181
- previous_user.nil? ? nil : previous_user[:key],
182
- user_to_context_kind(previous_user)
183
- ))
166
+ def record_custom_event(context, key, data = nil, metric_value = nil)
167
+ post_to_inbox(LaunchDarkly::Impl::CustomEvent.new(timestamp, context, key, data, metric_value))
184
168
  end
185
169
 
186
170
  def flush
@@ -192,8 +176,8 @@ module LaunchDarkly
192
176
  # final shutdown, which includes a final flush, is done synchronously
193
177
  if @stopped.make_true
194
178
  @flush_task.shutdown
195
- @users_flush_task.shutdown
196
- @diagnostic_event_task.shutdown if !@diagnostic_event_task.nil?
179
+ @contexts_flush_task.shutdown
180
+ @diagnostic_event_task.shutdown unless @diagnostic_event_task.nil?
197
181
  # Note that here we are not calling post_to_inbox, because we *do* want to wait if the inbox
198
182
  # is full; an orderly shutdown can't happen unless these messages are received.
199
183
  @inbox << FlushMessage.new
@@ -227,10 +211,6 @@ module LaunchDarkly
227
211
  end
228
212
  end
229
213
  end
230
-
231
- private def user_to_context_kind(user)
232
- (user.nil? || !user[:anonymous]) ? 'user' : 'anonymousUser'
233
- end
234
214
  end
235
215
 
236
216
  # @private
@@ -241,13 +221,13 @@ module LaunchDarkly
241
221
  @diagnostic_accumulator = config.diagnostic_opt_out? ? nil : diagnostic_accumulator
242
222
  @event_sender = event_sender
243
223
 
244
- @user_keys = SimpleLRUCacheSet.new(config.user_keys_capacity)
224
+ @context_keys = SimpleLRUCacheSet.new(config.context_keys_capacity)
245
225
  @formatter = EventOutputFormatter.new(config)
246
226
  @disabled = Concurrent::AtomicBoolean.new(false)
247
227
  @last_known_past_time = Concurrent::AtomicReference.new(0)
248
- @deduplicated_users = 0
228
+ @deduplicated_contexts = 0
249
229
  @events_in_last_batch = 0
250
-
230
+
251
231
  outbox = EventBuffer.new(config.capacity, config.logger)
252
232
  flush_workers = NonBlockingThreadPool.new(MAX_FLUSH_WORKERS)
253
233
 
@@ -272,8 +252,8 @@ module LaunchDarkly
272
252
  case message
273
253
  when FlushMessage
274
254
  trigger_flush(outbox, flush_workers)
275
- when FlushUsersMessage
276
- @user_keys.clear
255
+ when FlushContextsMessage
256
+ @context_keys.clear
277
257
  when DiagnosticEventMessage
278
258
  send_and_reset_diagnostics(outbox, diagnostic_event_workers)
279
259
  when TestSyncMessage
@@ -295,7 +275,7 @@ module LaunchDarkly
295
275
  def do_shutdown(flush_workers, diagnostic_event_workers)
296
276
  flush_workers.shutdown
297
277
  flush_workers.wait_for_termination
298
- if !diagnostic_event_workers.nil?
278
+ unless diagnostic_event_workers.nil?
299
279
  diagnostic_event_workers.shutdown
300
280
  diagnostic_event_workers.wait_for_termination
301
281
  end
@@ -305,7 +285,7 @@ module LaunchDarkly
305
285
  def synchronize_for_testing(flush_workers, diagnostic_event_workers)
306
286
  # Used only by unit tests. Wait until all active flush workers have finished.
307
287
  flush_workers.wait_all
308
- diagnostic_event_workers.wait_all if !diagnostic_event_workers.nil?
288
+ diagnostic_event_workers.wait_all unless diagnostic_event_workers.nil?
309
289
  end
310
290
 
311
291
  def dispatch_event(event, outbox)
@@ -327,27 +307,26 @@ module LaunchDarkly
327
307
  will_add_full_event = true
328
308
  end
329
309
 
330
- # For each user we haven't seen before, we add an index event - unless this is already
331
- # an identify event for that user.
332
- if !(will_add_full_event && @config.inline_users_in_events)
333
- if !event.user.nil? && !notice_user(event.user) && !event.is_a?(LaunchDarkly::Impl::IdentifyEvent)
334
- outbox.add_event(LaunchDarkly::Impl::IndexEvent.new(event.timestamp, event.user))
335
- end
310
+ # For each context we haven't seen before, we add an index event - unless this is already
311
+ # an identify event for that context.
312
+ if !event.context.nil? && !notice_context(event.context) && !event.is_a?(LaunchDarkly::Impl::IdentifyEvent)
313
+ outbox.add_event(LaunchDarkly::Impl::IndexEvent.new(event.timestamp, event.context))
336
314
  end
337
315
 
338
316
  outbox.add_event(event) if will_add_full_event
339
- outbox.add_event(debug_event) if !debug_event.nil?
317
+ outbox.add_event(debug_event) unless debug_event.nil?
340
318
  end
341
319
 
342
- # Add to the set of users we've noticed, and return true if the user was already known to us.
343
- def notice_user(user)
344
- if user.nil? || !user.has_key?(:key)
345
- true
346
- else
347
- known = @user_keys.add(user[:key].to_s)
348
- @deduplicated_users += 1 if known
349
- known
350
- end
320
+ #
321
+ # Add to the set of contexts we've noticed, and return true if the context
322
+ # was already known to us.
323
+ # @param context [LaunchDarkly::LDContext]
324
+ # @return [Boolean]
325
+ #
326
+ def notice_context(context)
327
+ known = @context_keys.add(context.fully_qualified_key)
328
+ @deduplicated_contexts += 1 if known
329
+ known
351
330
  end
352
331
 
353
332
  def should_debug_event(event)
@@ -365,7 +344,7 @@ module LaunchDarkly
365
344
  return
366
345
  end
367
346
 
368
- payload = outbox.get_payload
347
+ payload = outbox.get_payload
369
348
  if !payload.events.empty? || !payload.summary.counters.empty?
370
349
  count = payload.events.length + (payload.summary.counters.empty? ? 0 : 1)
371
350
  @events_in_last_batch = count
@@ -375,7 +354,7 @@ module LaunchDarkly
375
354
  events_out = @formatter.make_output_events(payload.events, payload.summary)
376
355
  result = @event_sender.send_event_data(events_out.to_json, "#{events_out.length} events", false)
377
356
  @disabled.value = true if result.must_shutdown
378
- if !result.time_from_server.nil?
357
+ unless result.time_from_server.nil?
379
358
  @last_known_past_time.value = (result.time_from_server.to_f * 1000).to_i
380
359
  end
381
360
  rescue => e
@@ -391,8 +370,8 @@ module LaunchDarkly
391
370
  def send_and_reset_diagnostics(outbox, diagnostic_event_workers)
392
371
  return if @diagnostic_accumulator.nil?
393
372
  dropped_count = outbox.get_and_clear_dropped_count
394
- event = @diagnostic_accumulator.create_periodic_event_and_reset(dropped_count, @deduplicated_users, @events_in_last_batch)
395
- @deduplicated_users = 0
373
+ event = @diagnostic_accumulator.create_periodic_event_and_reset(dropped_count, @deduplicated_contexts, @events_in_last_batch)
374
+ @deduplicated_contexts = 0
396
375
  @events_in_last_batch = 0
397
376
  send_diagnostic_event(event, diagnostic_event_workers)
398
377
  end
@@ -430,7 +409,7 @@ module LaunchDarkly
430
409
  @capacity_exceeded = false
431
410
  else
432
411
  @dropped_events += 1
433
- if !@capacity_exceeded
412
+ unless @capacity_exceeded
434
413
  @capacity_exceeded = true
435
414
  @logger.warn { "[LDClient] Exceeded event queue capacity. Increase capacity to avoid dropping events." }
436
415
  end
@@ -442,7 +421,7 @@ module LaunchDarkly
442
421
  end
443
422
 
444
423
  def get_payload
445
- return FlushPayload.new(@events, @summarizer.snapshot)
424
+ FlushPayload.new(@events, @summarizer.snapshot)
446
425
  end
447
426
 
448
427
  def get_and_clear_dropped_count
@@ -462,21 +441,18 @@ module LaunchDarkly
462
441
  FEATURE_KIND = 'feature'
463
442
  IDENTIFY_KIND = 'identify'
464
443
  CUSTOM_KIND = 'custom'
465
- ALIAS_KIND = 'alias'
466
444
  INDEX_KIND = 'index'
467
445
  DEBUG_KIND = 'debug'
468
446
  SUMMARY_KIND = 'summary'
469
- ANONYMOUS_USER_CONTEXT_KIND = 'anonymousUser'
470
447
 
471
448
  def initialize(config)
472
- @inline_users = config.inline_users_in_events
473
- @user_filter = UserFilter.new(config)
449
+ @context_filter = LaunchDarkly::Impl::ContextFilter.new(config.all_attributes_private, config.private_attributes)
474
450
  end
475
451
 
476
452
  # Transforms events into the format used for event sending.
477
453
  def make_output_events(events, summary)
478
454
  events_out = events.map { |e| make_output_event(e) }
479
- if !summary.counters.empty?
455
+ unless summary.counters.empty?
480
456
  events_out.push(make_summary_event(summary))
481
457
  end
482
458
  events_out
@@ -484,75 +460,62 @@ module LaunchDarkly
484
460
 
485
461
  private def make_output_event(event)
486
462
  case event
487
-
463
+
488
464
  when LaunchDarkly::Impl::EvalEvent
489
465
  out = {
490
466
  kind: FEATURE_KIND,
491
467
  creationDate: event.timestamp,
492
468
  key: event.key,
493
- value: event.value
469
+ value: event.value,
494
470
  }
495
- out[:default] = event.default if !event.default.nil?
496
- out[:variation] = event.variation if !event.variation.nil?
497
- out[:version] = event.version if !event.version.nil?
498
- out[:prereqOf] = event.prereq_of if !event.prereq_of.nil?
499
- set_opt_context_kind(out, event.user)
500
- set_user_or_user_key(out, event.user)
501
- out[:reason] = event.reason if !event.reason.nil?
471
+ out[:default] = event.default unless event.default.nil?
472
+ out[:variation] = event.variation unless event.variation.nil?
473
+ out[:version] = event.version unless event.version.nil?
474
+ out[:prereqOf] = event.prereq_of unless event.prereq_of.nil?
475
+ out[:contextKeys] = event.context.keys
476
+ out[:reason] = event.reason unless event.reason.nil?
502
477
  out
503
478
 
504
479
  when LaunchDarkly::Impl::IdentifyEvent
505
480
  {
506
481
  kind: IDENTIFY_KIND,
507
482
  creationDate: event.timestamp,
508
- key: event.user[:key].to_s,
509
- user: process_user(event.user)
483
+ key: event.context.fully_qualified_key,
484
+ context: @context_filter.filter(event.context),
510
485
  }
511
-
486
+
512
487
  when LaunchDarkly::Impl::CustomEvent
513
488
  out = {
514
489
  kind: CUSTOM_KIND,
515
490
  creationDate: event.timestamp,
516
- key: event.key
491
+ key: event.key,
517
492
  }
518
- out[:data] = event.data if !event.data.nil?
519
- set_user_or_user_key(out, event.user)
520
- out[:metricValue] = event.metric_value if !event.metric_value.nil?
521
- set_opt_context_kind(out, event.user)
493
+ out[:data] = event.data unless event.data.nil?
494
+ out[:contextKeys] = event.context.keys
495
+ out[:metricValue] = event.metric_value unless event.metric_value.nil?
522
496
  out
523
497
 
524
- when LaunchDarkly::Impl::AliasEvent
525
- {
526
- kind: ALIAS_KIND,
527
- creationDate: event.timestamp,
528
- key: event.key,
529
- contextKind: event.context_kind,
530
- previousKey: event.previous_key,
531
- previousContextKind: event.previous_context_kind
532
- }
533
-
534
498
  when LaunchDarkly::Impl::IndexEvent
535
499
  {
536
500
  kind: INDEX_KIND,
537
501
  creationDate: event.timestamp,
538
- user: process_user(event.user)
502
+ context: @context_filter.filter(event.context),
539
503
  }
540
-
504
+
541
505
  when LaunchDarkly::Impl::DebugEvent
542
506
  original = event.eval_event
543
507
  out = {
544
508
  kind: DEBUG_KIND,
545
509
  creationDate: original.timestamp,
546
510
  key: original.key,
547
- user: process_user(original.user),
548
- value: original.value
511
+ context: @context_filter.filter(original.context),
512
+ value: original.value,
549
513
  }
550
- out[:default] = original.default if !original.default.nil?
551
- out[:variation] = original.variation if !original.variation.nil?
552
- out[:version] = original.version if !original.version.nil?
553
- out[:prereqOf] = original.prereq_of if !original.prereq_of.nil?
554
- set_opt_context_kind(out, original.user)
555
- out[:reason] = original.reason if !original.reason.nil?
514
+ out[:default] = original.default unless original.default.nil?
515
+ out[:variation] = original.variation unless original.variation.nil?
516
+ out[:version] = original.version unless original.version.nil?
517
+ out[:prereqOf] = original.prereq_of unless original.prereq_of.nil?
518
+ out[:reason] = original.reason unless original.reason.nil?
556
519
  out
557
520
 
558
521
  else
@@ -569,9 +532,9 @@ module LaunchDarkly
569
532
  variations.each do |variation, counter|
570
533
  c = {
571
534
  value: counter.value,
572
- count: counter.count
535
+ count: counter.count,
573
536
  }
574
- c[:variation] = variation if !variation.nil?
537
+ c[:variation] = variation unless variation.nil?
575
538
  if version.nil?
576
539
  c[:unknown] = true
577
540
  else
@@ -580,32 +543,14 @@ module LaunchDarkly
580
543
  counters.push(c)
581
544
  end
582
545
  end
583
- flags[flagKey] = { default: flagInfo.default, counters: counters }
546
+ flags[flagKey] = { default: flagInfo.default, counters: counters, contextKinds: flagInfo.context_kinds.to_a }
584
547
  end
585
548
  {
586
549
  kind: SUMMARY_KIND,
587
550
  startDate: summary[:start_date],
588
551
  endDate: summary[:end_date],
589
- features: flags
552
+ features: flags,
590
553
  }
591
554
  end
592
-
593
- private def set_opt_context_kind(out, user)
594
- out[:contextKind] = ANONYMOUS_USER_CONTEXT_KIND if !user.nil? && user[:anonymous]
595
- end
596
-
597
- private def set_user_or_user_key(out, user)
598
- if @inline_users
599
- out[:user] = process_user(user)
600
- else
601
- key = user[:key]
602
- out[:userKey] = key.is_a?(String) ? key : key.to_s
603
- end
604
- end
605
-
606
- private def process_user(user)
607
- filtered = @user_filter.transform_user_props(user)
608
- Util.stringify_attrs(filtered, USER_ATTRS_TO_STRINGIFY_FOR_EVENTS)
609
- end
610
555
  end
611
556
  end
@@ -2,7 +2,7 @@ require 'json'
2
2
 
3
3
  module LaunchDarkly
4
4
  #
5
- # A snapshot of the state of all feature flags with regard to a specific user, generated by
5
+ # A snapshot of the state of all feature flags with regard to a specific context, generated by
6
6
  # calling the {LDClient#all_flags_state}. Serializing this object to JSON using
7
7
  # `JSON.generate` (or the `to_json` method) will produce the appropriate data structure for
8
8
  # bootstrapping the LaunchDarkly JavaScript client.
@@ -34,11 +34,11 @@ module LaunchDarkly
34
34
  meta[:reason] = reason
35
35
  end
36
36
 
37
- if !omit_details
37
+ unless omit_details
38
38
  meta[:version] = flag_state[:version]
39
39
  end
40
40
 
41
- meta[:variation] = flag_state[:variation] if !flag_state[:variation].nil?
41
+ meta[:variation] = flag_state[:variation] unless flag_state[:variation].nil?
42
42
  meta[:trackEvents] = true if flag_state[:trackEvents]
43
43
  meta[:trackReason] = true if flag_state[:trackReason]
44
44
  meta[:debugEventsUntilDate] = flag_state[:debugEventsUntilDate] if flag_state[:debugEventsUntilDate]
@@ -46,7 +46,7 @@ module LaunchDarkly
46
46
  end
47
47
 
48
48
  # Returns true if this object contains a valid snapshot of feature flag state, or false if the
49
- # state could not be computed (for instance, because the client was offline or there was no user).
49
+ # state could not be computed (for instance, because the client was offline or there was no context).
50
50
  def valid?
51
51
  @valid
52
52
  end
@@ -22,7 +22,7 @@ module LaunchDarkly
22
22
  @logger = logger
23
23
  @last_status = nil
24
24
 
25
- if !@store.nil?
25
+ unless @store.nil?
26
26
  @cache = ExpiringCache.new(big_segments_config.user_cache_size, big_segments_config.user_cache_time)
27
27
  @poll_worker = RepeatingTask.new(big_segments_config.status_poll_interval, 0, -> { poll_store_and_update_status }, logger)
28
28
  @poll_worker.start
@@ -32,25 +32,25 @@ module LaunchDarkly
32
32
  attr_reader :status_provider
33
33
 
34
34
  def stop
35
- @poll_worker.stop if !@poll_worker.nil?
36
- @store.stop if !@store.nil?
35
+ @poll_worker.stop unless @poll_worker.nil?
36
+ @store.stop unless @store.nil?
37
37
  end
38
38
 
39
- def get_user_membership(user_key)
40
- return nil if !@store
41
- membership = @cache[user_key]
42
- if !membership
39
+ def get_context_membership(context_key)
40
+ return nil unless @store
41
+ membership = @cache[context_key]
42
+ unless membership
43
43
  begin
44
- membership = @store.get_membership(BigSegmentStoreManager.hash_for_user_key(user_key))
44
+ membership = @store.get_membership(BigSegmentStoreManager.hash_for_context_key(context_key))
45
45
  membership = EMPTY_MEMBERSHIP if membership.nil?
46
- @cache[user_key] = membership
46
+ @cache[context_key] = membership
47
47
  rescue => e
48
48
  LaunchDarkly::Util.log_exception(@logger, "Big Segment store membership query returned error", e)
49
49
  return BigSegmentMembershipResult.new(nil, BigSegmentsStatus::STORE_ERROR)
50
50
  end
51
51
  end
52
- poll_store_and_update_status if !@last_status
53
- if !@last_status.available
52
+ poll_store_and_update_status unless @last_status
53
+ unless @last_status.available
54
54
  return BigSegmentMembershipResult.new(membership, BigSegmentsStatus::STORE_ERROR)
55
55
  end
56
56
  BigSegmentMembershipResult.new(membership, @last_status.stale ? BigSegmentsStatus::STALE : BigSegmentsStatus::HEALTHY)
@@ -62,26 +62,26 @@ module LaunchDarkly
62
62
 
63
63
  def poll_store_and_update_status
64
64
  new_status = Interfaces::BigSegmentStoreStatus.new(false, false) # default to "unavailable" if we don't get a new status below
65
- if !@store.nil?
65
+ unless @store.nil?
66
66
  begin
67
67
  metadata = @store.get_metadata
68
- new_status = Interfaces::BigSegmentStoreStatus.new(true, !metadata || is_stale(metadata.last_up_to_date))
68
+ new_status = Interfaces::BigSegmentStoreStatus.new(true, !metadata || stale?(metadata.last_up_to_date))
69
69
  rescue => e
70
70
  LaunchDarkly::Util.log_exception(@logger, "Big Segment store status query returned error", e)
71
71
  end
72
72
  end
73
73
  @last_status = new_status
74
74
  @status_provider.update_status(new_status)
75
-
75
+
76
76
  new_status
77
77
  end
78
78
 
79
- def is_stale(timestamp)
79
+ def stale?(timestamp)
80
80
  !timestamp || ((Impl::Util.current_time_millis - timestamp) >= @stale_after_millis)
81
81
  end
82
82
 
83
- def self.hash_for_user_key(user_key)
84
- Digest::SHA256.base64digest(user_key)
83
+ def self.hash_for_context_key(context_key)
84
+ Digest::SHA256.base64digest(context_key)
85
85
  end
86
86
  end
87
87
 
@@ -0,0 +1,96 @@
1
+ require "erb"
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+ module Context
6
+ ERR_KIND_NON_STRING = 'context kind must be a string'
7
+ ERR_KIND_CANNOT_BE_KIND = '"kind" is not a valid context kind'
8
+ ERR_KIND_CANNOT_BE_MULTI = '"multi" is not a valid context kind'
9
+ ERR_KIND_INVALID_CHARS = 'context kind contains disallowed characters'
10
+
11
+ ERR_KEY_NON_STRING = 'context key must be a string'
12
+ ERR_KEY_EMPTY = 'context key must not be empty'
13
+
14
+ ERR_NAME_NON_STRING = 'context name must be a string'
15
+
16
+ ERR_ANONYMOUS_NON_BOOLEAN = 'context anonymous must be a boolean'
17
+
18
+ #
19
+ # We allow consumers of this SDK to provide us with either a Hash or an
20
+ # instance of an LDContext. This is convenient for them but not as much
21
+ # for us. To make the conversion slightly more convenient for us, we have
22
+ # created this method.
23
+ #
24
+ # @param context [Hash, LDContext]
25
+ # @return [LDContext]
26
+ #
27
+ def self.make_context(context)
28
+ return context if context.is_a?(LDContext)
29
+
30
+ LDContext.create(context)
31
+ end
32
+
33
+ #
34
+ # Returns an error message if the kind is invalid; nil otherwise.
35
+ #
36
+ # @param kind [any]
37
+ # @return [String, nil]
38
+ #
39
+ def self.validate_kind(kind)
40
+ return ERR_KIND_NON_STRING unless kind.is_a?(String)
41
+ return ERR_KIND_CANNOT_BE_KIND if kind == "kind"
42
+ return ERR_KIND_CANNOT_BE_MULTI if kind == "multi"
43
+ return ERR_KIND_INVALID_CHARS unless kind.match?(/^[\w.-]+$/)
44
+ end
45
+
46
+ #
47
+ # Returns an error message if the key is invalid; nil otherwise.
48
+ #
49
+ # @param key [any]
50
+ # @return [String, nil]
51
+ #
52
+ def self.validate_key(key)
53
+ return ERR_KEY_NON_STRING unless key.is_a?(String)
54
+ return ERR_KEY_EMPTY if key == ""
55
+ end
56
+
57
+ #
58
+ # Returns an error message if the name is invalid; nil otherwise.
59
+ #
60
+ # @param name [any]
61
+ # @return [String, nil]
62
+ #
63
+ def self.validate_name(name)
64
+ return ERR_NAME_NON_STRING unless name.nil? || name.is_a?(String)
65
+ end
66
+
67
+ #
68
+ # Returns an error message if anonymous is invalid; nil otherwise.
69
+ #
70
+ # @param anonymous [any]
71
+ # @param allow_nil [Boolean]
72
+ # @return [String, nil]
73
+ #
74
+ def self.validate_anonymous(anonymous, allow_nil)
75
+ return nil if anonymous.nil? && allow_nil
76
+ return nil if [true, false].include? anonymous
77
+
78
+ ERR_ANONYMOUS_NON_BOOLEAN
79
+ end
80
+
81
+ #
82
+ # @param kind [String]
83
+ # @param key [String]
84
+ # @return [String]
85
+ #
86
+ def self.canonicalize_key_for_kind(kind, key)
87
+ # When building a FullyQualifiedKey, ':' and '%' are percent-escaped;
88
+ # we do not use a full URL-encoding function because implementations of
89
+ # this are inconsistent across platforms.
90
+ encoded = key.gsub("%", "%25").gsub(":", "%3A")
91
+
92
+ "#{kind}:#{encoded}"
93
+ end
94
+ end
95
+ end
96
+ end