launchdarkly-server-sdk 6.4.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ldclient-rb/config.rb +102 -56
- data/lib/ldclient-rb/context.rb +487 -0
- data/lib/ldclient-rb/evaluation_detail.rb +20 -20
- data/lib/ldclient-rb/events.rb +77 -132
- data/lib/ldclient-rb/flags_state.rb +4 -4
- data/lib/ldclient-rb/impl/big_segments.rb +17 -17
- data/lib/ldclient-rb/impl/context.rb +96 -0
- data/lib/ldclient-rb/impl/context_filter.rb +145 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
- data/lib/ldclient-rb/impl/evaluator.rb +379 -131
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +31 -34
- data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
- data/lib/ldclient-rb/impl/event_sender.rb +6 -6
- data/lib/ldclient-rb/impl/event_summarizer.rb +12 -7
- data/lib/ldclient-rb/impl/event_types.rb +18 -30
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +29 -29
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +92 -12
- data/lib/ldclient-rb/impl/model/clause.rb +45 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +232 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +8 -121
- data/lib/ldclient-rb/impl/model/segment.rb +132 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +52 -12
- data/lib/ldclient-rb/impl/repeating_task.rb +1 -1
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
- data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +2 -2
- data/lib/ldclient-rb/in_memory_store.rb +2 -2
- data/lib/ldclient-rb/integrations/consul.rb +1 -1
- data/lib/ldclient-rb/integrations/dynamodb.rb +1 -1
- data/lib/ldclient-rb/integrations/file_data.rb +3 -3
- data/lib/ldclient-rb/integrations/redis.rb +4 -4
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +218 -62
- data/lib/ldclient-rb/integrations/test_data.rb +16 -12
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +9 -9
- data/lib/ldclient-rb/interfaces.rb +14 -14
- data/lib/ldclient-rb/ldclient.rb +94 -144
- data/lib/ldclient-rb/memoized_value.rb +1 -1
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
- data/lib/ldclient-rb/polling.rb +2 -2
- data/lib/ldclient-rb/reference.rb +274 -0
- data/lib/ldclient-rb/requestor.rb +5 -5
- data/lib/ldclient-rb/stream.rb +7 -8
- data/lib/ldclient-rb/util.rb +4 -19
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +2 -3
- metadata +34 -17
- data/lib/ldclient-rb/file_data_source.rb +0 -23
- data/lib/ldclient-rb/newrelic.rb +0 -17
- data/lib/ldclient-rb/redis_store.rb +0 -88
- data/lib/ldclient-rb/user_filter.rb +0 -52
data/lib/ldclient-rb/events.rb
CHANGED
@@ -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
|
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
|
-
|
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(
|
47
|
+
def record_identify_event(context)
|
47
48
|
end
|
48
49
|
|
49
50
|
def record_custom_event(
|
50
|
-
|
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
|
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
|
-
@
|
127
|
-
post_to_inbox(
|
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
|
-
@
|
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
|
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
|
-
|
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,
|
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(
|
169
|
-
post_to_inbox(LaunchDarkly::Impl::IdentifyEvent.new(timestamp,
|
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(
|
173
|
-
post_to_inbox(LaunchDarkly::Impl::CustomEvent.new(timestamp,
|
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
|
-
@
|
196
|
-
@diagnostic_event_task.shutdown
|
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
|
-
@
|
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
|
-
@
|
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
|
276
|
-
@
|
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
|
-
|
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
|
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
|
331
|
-
# an identify event for that
|
332
|
-
if !(
|
333
|
-
|
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)
|
317
|
+
outbox.add_event(debug_event) unless debug_event.nil?
|
340
318
|
end
|
341
319
|
|
342
|
-
#
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
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
|
-
|
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, @
|
395
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
@
|
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
|
-
|
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
|
496
|
-
out[:variation] = event.variation
|
497
|
-
out[:version] = event.version
|
498
|
-
out[:prereqOf] = event.prereq_of
|
499
|
-
|
500
|
-
|
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.
|
509
|
-
|
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
|
519
|
-
|
520
|
-
out[:metricValue] = event.metric_value
|
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
|
-
|
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
|
-
|
548
|
-
value: original.value
|
511
|
+
context: @context_filter.filter(original.context),
|
512
|
+
value: original.value,
|
549
513
|
}
|
550
|
-
out[:default] = original.default
|
551
|
-
out[:variation] = original.variation
|
552
|
-
out[:version] = original.version
|
553
|
-
out[:prereqOf] = original.prereq_of
|
554
|
-
|
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
|
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
|
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
|
-
|
37
|
+
unless omit_details
|
38
38
|
meta[:version] = flag_state[:version]
|
39
39
|
end
|
40
40
|
|
41
|
-
meta[:variation] = flag_state[:variation]
|
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
|
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
|
-
|
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
|
36
|
-
@store.stop
|
35
|
+
@poll_worker.stop unless @poll_worker.nil?
|
36
|
+
@store.stop unless @store.nil?
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
40
|
-
return nil
|
41
|
-
membership = @cache[
|
42
|
-
|
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.
|
44
|
+
membership = @store.get_membership(BigSegmentStoreManager.hash_for_context_key(context_key))
|
45
45
|
membership = EMPTY_MEMBERSHIP if membership.nil?
|
46
|
-
@cache[
|
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
|
53
|
-
|
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
|
-
|
65
|
+
unless @store.nil?
|
66
66
|
begin
|
67
67
|
metadata = @store.get_metadata
|
68
|
-
new_status = Interfaces::BigSegmentStoreStatus.new(true, !metadata ||
|
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
|
79
|
+
def stale?(timestamp)
|
80
80
|
!timestamp || ((Impl::Util.current_time_millis - timestamp) >= @stale_after_millis)
|
81
81
|
end
|
82
82
|
|
83
|
-
def self.
|
84
|
-
Digest::SHA256.base64digest(
|
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
|