launchdarkly-server-sdk 6.3.2 → 6.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,8 +15,66 @@ module LaunchDarkly
15
15
  ret["X-LaunchDarkly-Wrapper"] = config.wrapper_name +
16
16
  (config.wrapper_version ? "/" + config.wrapper_version : "")
17
17
  end
18
+
19
+ app_value = application_header_value config.application
20
+ ret["X-LaunchDarkly-Tags"] = app_value unless app_value.nil? || app_value.empty?
21
+
18
22
  ret
19
23
  end
24
+
25
+ #
26
+ # Generate an HTTP Header value containing the application meta information (@see #application).
27
+ #
28
+ # @return [String]
29
+ #
30
+ def self.application_header_value(application)
31
+ parts = []
32
+ unless application[:id].empty?
33
+ parts << "application-id/#{application[:id]}"
34
+ end
35
+
36
+ unless application[:version].empty?
37
+ parts << "application-version/#{application[:version]}"
38
+ end
39
+
40
+ parts.join(" ")
41
+ end
42
+
43
+ #
44
+ # @param value [String]
45
+ # @param name [Symbol]
46
+ # @param logger [Logger]
47
+ # @return [String]
48
+ #
49
+ def self.validate_application_value(value, name, logger)
50
+ value = value.to_s
51
+
52
+ return "" if value.empty?
53
+
54
+ if value.length > 64
55
+ logger.warn { "Value of application[#{name}] was longer than 64 characters and was discarded" }
56
+ return ""
57
+ end
58
+
59
+ if value.match(/[^a-zA-Z0-9._-]/)
60
+ logger.warn { "Value of application[#{name}] contained invalid characters and was discarded" }
61
+ return ""
62
+ end
63
+
64
+ value
65
+ end
66
+
67
+ #
68
+ # @param app [Hash]
69
+ # @param logger [Logger]
70
+ # @return [Hash]
71
+ #
72
+ def self.validate_application_info(app, logger)
73
+ {
74
+ id: validate_application_value(app[:id], :id, logger),
75
+ version: validate_application_value(app[:version], :version, logger),
76
+ }
77
+ end
20
78
  end
21
79
  end
22
80
  end
@@ -1,7 +1,6 @@
1
1
  require "ldclient-rb/impl/big_segments"
2
2
  require "ldclient-rb/impl/diagnostic_events"
3
3
  require "ldclient-rb/impl/evaluator"
4
- require "ldclient-rb/impl/event_factory"
5
4
  require "ldclient-rb/impl/store_client_wrapper"
6
5
  require "concurrent/atomics"
7
6
  require "digest/sha1"
@@ -46,9 +45,6 @@ module LaunchDarkly
46
45
 
47
46
  @sdk_key = sdk_key
48
47
 
49
- @event_factory_default = EventFactory.new(false)
50
- @event_factory_with_reasons = EventFactory.new(true)
51
-
52
48
  # We need to wrap the feature store object with a FeatureStoreClientWrapper in order to add
53
49
  # some necessary logic around updates. Unfortunately, we have code elsewhere that accesses
54
50
  # the feature store through the Config object, so we need to make a new Config that uses
@@ -202,7 +198,7 @@ module LaunchDarkly
202
198
  # @return the variation to show the user, or the default value if there's an an error
203
199
  #
204
200
  def variation(key, user, default)
205
- evaluate_internal(key, user, default, @event_factory_default).value
201
+ evaluate_internal(key, user, default, false).value
206
202
  end
207
203
 
208
204
  #
@@ -229,7 +225,7 @@ module LaunchDarkly
229
225
  # @return [EvaluationDetail] an object describing the result
230
226
  #
231
227
  def variation_detail(key, user, default)
232
- evaluate_internal(key, user, default, @event_factory_with_reasons)
228
+ evaluate_internal(key, user, default, true)
233
229
  end
234
230
 
235
231
  #
@@ -253,7 +249,7 @@ module LaunchDarkly
253
249
  return
254
250
  end
255
251
  sanitize_user(user)
256
- @event_processor.add_event(@event_factory_default.new_identify_event(user))
252
+ @event_processor.record_identify_event(user)
257
253
  end
258
254
 
259
255
  #
@@ -284,7 +280,7 @@ module LaunchDarkly
284
280
  return
285
281
  end
286
282
  sanitize_user(user)
287
- @event_processor.add_event(@event_factory_default.new_custom_event(event_name, user, data, metric_value))
283
+ @event_processor.record_custom_event(user, event_name, data, metric_value)
288
284
  end
289
285
 
290
286
  #
@@ -301,7 +297,7 @@ module LaunchDarkly
301
297
  end
302
298
  sanitize_user(current_context)
303
299
  sanitize_user(previous_context)
304
- @event_processor.add_event(@event_factory_default.new_alias_event(current_context, previous_context))
300
+ @event_processor.record_alias_event(current_context, previous_context)
305
301
  end
306
302
 
307
303
  #
@@ -368,13 +364,13 @@ module LaunchDarkly
368
364
  next
369
365
  end
370
366
  begin
371
- detail = @evaluator.evaluate(f, user, @event_factory_default).detail
367
+ detail = @evaluator.evaluate(f, user).detail
372
368
  rescue => exn
373
369
  detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION))
374
370
  Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
375
371
  end
376
372
 
377
- requires_experiment_data = EventFactory.is_experiment(f, detail.reason)
373
+ requires_experiment_data = is_experiment(f, detail.reason)
378
374
  flag_state = {
379
375
  key: f[:key],
380
376
  value: detail.value,
@@ -430,7 +426,7 @@ module LaunchDarkly
430
426
  end
431
427
 
432
428
  # @return [EvaluationDetail]
433
- def evaluate_internal(key, user, default, event_factory)
429
+ def evaluate_internal(key, user, default, with_reasons)
434
430
  if @config.offline?
435
431
  return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
436
432
  end
@@ -453,7 +449,7 @@ module LaunchDarkly
453
449
  else
454
450
  @config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
455
451
  detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
456
- @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
452
+ record_unknown_flag_eval(key, user, default, detail.reason, with_reasons)
457
453
  return detail
458
454
  end
459
455
  end
@@ -463,32 +459,94 @@ module LaunchDarkly
463
459
  if feature.nil?
464
460
  @config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
465
461
  detail = Evaluator.error_result(EvaluationReason::ERROR_FLAG_NOT_FOUND, default)
466
- @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
462
+ record_unknown_flag_eval(key, user, default, detail.reason, with_reasons)
467
463
  return detail
468
464
  end
469
465
 
470
466
  begin
471
- res = @evaluator.evaluate(feature, user, event_factory)
472
- if !res.events.nil?
473
- res.events.each do |event|
474
- @event_processor.add_event(event)
467
+ res = @evaluator.evaluate(feature, user)
468
+ if !res.prereq_evals.nil?
469
+ res.prereq_evals.each do |prereq_eval|
470
+ record_prereq_flag_eval(prereq_eval.prereq_flag, prereq_eval.prereq_of_flag, user, prereq_eval.detail, with_reasons)
475
471
  end
476
472
  end
477
473
  detail = res.detail
478
474
  if detail.default_value?
479
475
  detail = EvaluationDetail.new(default, nil, detail.reason)
480
476
  end
481
- @event_processor.add_event(event_factory.new_eval_event(feature, user, detail, default))
477
+ record_flag_eval(feature, user, detail, default, with_reasons)
482
478
  return detail
483
479
  rescue => exn
484
480
  Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
485
481
  detail = Evaluator.error_result(EvaluationReason::ERROR_EXCEPTION, default)
486
- @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
482
+ record_flag_eval_error(feature, user, default, detail.reason, with_reasons)
487
483
  return detail
488
484
  end
489
485
  end
490
486
 
491
- def sanitize_user(user)
487
+ private def record_flag_eval(flag, user, detail, default, with_reasons)
488
+ add_experiment_data = is_experiment(flag, detail.reason)
489
+ @event_processor.record_eval_event(
490
+ user,
491
+ flag[:key],
492
+ flag[:version],
493
+ detail.variation_index,
494
+ detail.value,
495
+ (add_experiment_data || with_reasons) ? detail.reason : nil,
496
+ default,
497
+ add_experiment_data || flag[:trackEvents] || false,
498
+ flag[:debugEventsUntilDate],
499
+ nil
500
+ )
501
+ end
502
+
503
+ private def record_prereq_flag_eval(prereq_flag, prereq_of_flag, user, detail, with_reasons)
504
+ add_experiment_data = is_experiment(prereq_flag, detail.reason)
505
+ @event_processor.record_eval_event(
506
+ user,
507
+ prereq_flag[:key],
508
+ prereq_flag[:version],
509
+ detail.variation_index,
510
+ detail.value,
511
+ (add_experiment_data || with_reasons) ? detail.reason : nil,
512
+ nil,
513
+ add_experiment_data || prereq_flag[:trackEvents] || false,
514
+ prereq_flag[:debugEventsUntilDate],
515
+ prereq_of_flag[:key]
516
+ )
517
+ end
518
+
519
+ private def record_flag_eval_error(flag, user, default, reason, with_reasons)
520
+ @event_processor.record_eval_event(user, flag[:key], flag[:version], nil, default, with_reasons ? reason : nil, default,
521
+ flag[:trackEvents], flag[:debugEventsUntilDate], nil)
522
+ end
523
+
524
+ private def record_unknown_flag_eval(flag_key, user, default, reason, with_reasons)
525
+ @event_processor.record_eval_event(user, flag_key, nil, nil, default, with_reasons ? reason : nil, default,
526
+ false, nil, nil)
527
+ end
528
+
529
+ private def is_experiment(flag, reason)
530
+ return false if !reason
531
+
532
+ if reason.in_experiment
533
+ return true
534
+ end
535
+
536
+ case reason[:kind]
537
+ when 'RULE_MATCH'
538
+ index = reason[:ruleIndex]
539
+ if !index.nil?
540
+ rules = flag[:rules] || []
541
+ return index >= 0 && index < rules.length && rules[index][:trackEvents]
542
+ end
543
+ when 'FALLTHROUGH'
544
+ return !!flag[:trackEventsFallthrough]
545
+ end
546
+ false
547
+ end
548
+
549
+ private def sanitize_user(user)
492
550
  if user[:key]
493
551
  user[:key] = user[:key].to_s
494
552
  end
@@ -31,7 +31,7 @@ module LaunchDarkly
31
31
 
32
32
  def request_all_data()
33
33
  all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
34
- Impl::Model.make_all_store_data(all_data)
34
+ Impl::Model.make_all_store_data(all_data, @config.logger)
35
35
  end
36
36
 
37
37
  def stop
@@ -44,7 +44,7 @@ module LaunchDarkly
44
44
  private
45
45
 
46
46
  def request_single_item(kind, path)
47
- Impl::Model.deserialize(kind, make_request(path))
47
+ Impl::Model.deserialize(kind, make_request(path), @config.logger)
48
48
  end
49
49
 
50
50
  def make_request(path)
@@ -86,7 +86,7 @@ module LaunchDarkly
86
86
  @config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
87
87
  if method == PUT
88
88
  message = JSON.parse(message.data, symbolize_names: true)
89
- all_data = Impl::Model.make_all_store_data(message[:data])
89
+ all_data = Impl::Model.make_all_store_data(message[:data], @config.logger)
90
90
  @feature_store.init(all_data)
91
91
  @initialized.make_true
92
92
  @config.logger.info { "[LDClient] Stream initialized" }
@@ -97,7 +97,7 @@ module LaunchDarkly
97
97
  key = key_for_path(kind, data[:path])
98
98
  if key
99
99
  data = data[:data]
100
- Impl::Model.postprocess_item_after_deserializing!(kind, data)
100
+ Impl::DataModelPreprocessing::Preprocessor.new(@config.logger).preprocess_item!(kind, data)
101
101
  @feature_store.upsert(kind, data)
102
102
  break
103
103
  end
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "6.3.2"
2
+ VERSION = "6.4.0"
3
3
  end
data/lib/ldclient-rb.rb CHANGED
@@ -21,7 +21,6 @@ require "ldclient-rb/polling"
21
21
  require "ldclient-rb/user_filter"
22
22
  require "ldclient-rb/simple_lru_cache"
23
23
  require "ldclient-rb/non_blocking_thread_pool"
24
- require "ldclient-rb/event_summarizer"
25
24
  require "ldclient-rb/events"
26
25
  require "ldclient-rb/requestor"
27
26
  require "ldclient-rb/file_data_source"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: launchdarkly-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.3.2
4
+ version: 6.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-18 00:00:00.000000000 Z
11
+ date: 2022-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb
@@ -198,14 +198,14 @@ dependencies:
198
198
  requirements:
199
199
  - - '='
200
200
  - !ruby/object:Gem::Version
201
- version: 2.2.0
201
+ version: 2.2.1
202
202
  type: :runtime
203
203
  prerelease: false
204
204
  version_requirements: !ruby/object:Gem::Requirement
205
205
  requirements:
206
206
  - - '='
207
207
  - !ruby/object:Gem::Version
208
- version: 2.2.0
208
+ version: 2.2.1
209
209
  - !ruby/object:Gem::Dependency
210
210
  name: json
211
211
  requirement: !ruby/object:Gem::Requirement
@@ -254,7 +254,6 @@ files:
254
254
  - lib/ldclient-rb/cache_store.rb
255
255
  - lib/ldclient-rb/config.rb
256
256
  - lib/ldclient-rb/evaluation_detail.rb
257
- - lib/ldclient-rb/event_summarizer.rb
258
257
  - lib/ldclient-rb/events.rb
259
258
  - lib/ldclient-rb/expiring_cache.rb
260
259
  - lib/ldclient-rb/file_data_source.rb
@@ -264,14 +263,17 @@ files:
264
263
  - lib/ldclient-rb/impl/diagnostic_events.rb
265
264
  - lib/ldclient-rb/impl/evaluator.rb
266
265
  - lib/ldclient-rb/impl/evaluator_bucketing.rb
266
+ - lib/ldclient-rb/impl/evaluator_helpers.rb
267
267
  - lib/ldclient-rb/impl/evaluator_operators.rb
268
- - lib/ldclient-rb/impl/event_factory.rb
269
268
  - lib/ldclient-rb/impl/event_sender.rb
269
+ - lib/ldclient-rb/impl/event_summarizer.rb
270
+ - lib/ldclient-rb/impl/event_types.rb
270
271
  - lib/ldclient-rb/impl/integrations/consul_impl.rb
271
272
  - lib/ldclient-rb/impl/integrations/dynamodb_impl.rb
272
273
  - lib/ldclient-rb/impl/integrations/file_data_source.rb
273
274
  - lib/ldclient-rb/impl/integrations/redis_impl.rb
274
275
  - lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb
276
+ - lib/ldclient-rb/impl/model/preprocessed_data.rb
275
277
  - lib/ldclient-rb/impl/model/serialization.rb
276
278
  - lib/ldclient-rb/impl/repeating_task.rb
277
279
  - lib/ldclient-rb/impl/store_client_wrapper.rb
@@ -319,7 +321,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
319
321
  - !ruby/object:Gem::Version
320
322
  version: '0'
321
323
  requirements: []
322
- rubygems_version: 3.3.9
324
+ rubygems_version: 3.3.22
323
325
  signing_key:
324
326
  specification_version: 4
325
327
  summary: LaunchDarkly SDK for Ruby
@@ -1,55 +0,0 @@
1
-
2
- module LaunchDarkly
3
- # @private
4
- EventSummary = Struct.new(:start_date, :end_date, :counters)
5
-
6
- # Manages the state of summarizable information for the EventProcessor, including the
7
- # event counters and user deduplication. Note that the methods of this class are
8
- # deliberately not thread-safe; the EventProcessor is responsible for enforcing
9
- # synchronization across both the summarizer and the event queue.
10
- #
11
- # @private
12
- class EventSummarizer
13
- def initialize
14
- clear
15
- end
16
-
17
- # Adds this event to our counters, if it is a type of event we need to count.
18
- def summarize_event(event)
19
- if event[:kind] == "feature"
20
- counter_key = {
21
- key: event[:key],
22
- version: event[:version],
23
- variation: event[:variation]
24
- }
25
- c = @counters[counter_key]
26
- if c.nil?
27
- @counters[counter_key] = {
28
- value: event[:value],
29
- default: event[:default],
30
- count: 1
31
- }
32
- else
33
- c[:count] = c[:count] + 1
34
- end
35
- time = event[:creationDate]
36
- if !time.nil?
37
- @start_date = time if @start_date == 0 || time < @start_date
38
- @end_date = time if time > @end_date
39
- end
40
- end
41
- end
42
-
43
- # Returns a snapshot of the current summarized event data, and resets this state.
44
- def snapshot
45
- ret = EventSummary.new(@start_date, @end_date, @counters)
46
- ret
47
- end
48
-
49
- def clear
50
- @start_date = 0
51
- @end_date = 0
52
- @counters = {}
53
- end
54
- end
55
- end
@@ -1,123 +0,0 @@
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 = self.class.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[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
32
- e
33
- end
34
-
35
- def new_default_event(flag, user, default_value, reason)
36
- e = {
37
- kind: 'feature',
38
- key: flag[:key],
39
- user: user,
40
- value: default_value,
41
- default: default_value,
42
- version: flag[:version]
43
- }
44
- e[:trackEvents] = true if flag[:trackEvents]
45
- e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
46
- e[:reason] = reason if @with_reasons
47
- e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
48
- e
49
- end
50
-
51
- def new_unknown_flag_event(key, user, default_value, reason)
52
- e = {
53
- kind: 'feature',
54
- key: key,
55
- user: user,
56
- value: default_value,
57
- default: default_value
58
- }
59
- e[:reason] = reason if @with_reasons
60
- e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
61
- e
62
- end
63
-
64
- def new_identify_event(user)
65
- {
66
- kind: 'identify',
67
- key: user[:key],
68
- user: user
69
- }
70
- end
71
-
72
- def new_alias_event(current_context, previous_context)
73
- {
74
- kind: 'alias',
75
- key: current_context[:key],
76
- contextKind: context_to_context_kind(current_context),
77
- previousKey: previous_context[:key],
78
- previousContextKind: context_to_context_kind(previous_context)
79
- }
80
- end
81
-
82
- def new_custom_event(event_name, user, data, metric_value)
83
- e = {
84
- kind: 'custom',
85
- key: event_name,
86
- user: user
87
- }
88
- e[:data] = data if !data.nil?
89
- e[:metricValue] = metric_value if !metric_value.nil?
90
- e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous]
91
- e
92
- end
93
-
94
- def self.is_experiment(flag, reason)
95
- return false if !reason
96
-
97
- if reason.in_experiment
98
- return true
99
- end
100
-
101
- case reason[:kind]
102
- when 'RULE_MATCH'
103
- index = reason[:ruleIndex]
104
- if !index.nil?
105
- rules = flag[:rules] || []
106
- return index >= 0 && index < rules.length && rules[index][:trackEvents]
107
- end
108
- when 'FALLTHROUGH'
109
- return !!flag[:trackEventsFallthrough]
110
- end
111
- false
112
- end
113
-
114
- private def context_to_context_kind(user)
115
- if !user.nil? && user[:anonymous]
116
- return "anonymousUser"
117
- else
118
- return "user"
119
- end
120
- end
121
- end
122
- end
123
- end