launchdarkly-server-sdk 6.4.0 → 7.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +28 -31
- 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 +39 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +213 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +8 -121
- data/lib/ldclient-rb/impl/model/segment.rb +126 -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
|