launchdarkly-server-sdk 8.3.0 → 8.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8386f877f9c75e57e88df29c2983ccb3e70c47f038a374a54cd07a904480b4e
4
- data.tar.gz: 1edbe1a519399f47e8da823c0cb5f034ab657d47612647b0846237e280976d70
3
+ metadata.gz: 6067b2e1ab38ffb622a683a63206d485bd13744f361d99f89de467b5f1bc0c9e
4
+ data.tar.gz: 28999c77f6007effa021c4d88a99d3b6ad7ddd951d0e93449ee25276b311290b
5
5
  SHA512:
6
- metadata.gz: 6af0847efe755ef23429d0060a759fc44be91f50af5d5509550dbba3d3c953dd052c0773836f7a2786b5e993c5ca31a7c59a8661a925e76954b28e07a63c1bdf
7
- data.tar.gz: 6f58802cf33e04bad0c77d40acf1f3442909b8dbc54647b9fc32540d3fc8d538f56b6651a2ad24c99509813e5a9b89a554aac6e204459b891015338f593a32b1
6
+ metadata.gz: 9f4e045335066941a471cb09a183867318256c093449992b966724b7b5a0f12e8caae2b73ec4beda03c8732130e0891827209ddc22f42bb7de50267135ee3ed6
7
+ data.tar.gz: e949b6e9d82371ef4a9d6e0d412b6979dc0e1963336fde18c15b6bffea1b023d0ab885cc12fc8a73522cbc59a1174b5ca8c60bbe84fada1aaf6671acbaf8e9b2
@@ -43,6 +43,7 @@ module LaunchDarkly
43
43
  # @option opts [BigSegmentsConfig] :big_segments See {#big_segments}.
44
44
  # @option opts [Hash] :application See {#application}
45
45
  # @option opts [String] :payload_filter_key See {#payload_filter_key}
46
+ # @option hooks [Array<Interfaces::Hooks::Hook]
46
47
  #
47
48
  def initialize(opts = {})
48
49
  @base_uri = (opts[:base_uri] || Config.default_base_uri).chomp("/")
@@ -75,6 +76,7 @@ module LaunchDarkly
75
76
  @big_segments = opts[:big_segments] || BigSegmentsConfig.new(store: nil)
76
77
  @application = LaunchDarkly::Impl::Util.validate_application_info(opts[:application] || {}, @logger)
77
78
  @payload_filter_key = opts[:payload_filter_key]
79
+ @hooks = (opts[:hooks] || []).keep_if { |hook| hook.is_a? Interfaces::Hooks::Hook }
78
80
  @data_source_update_sink = nil
79
81
  end
80
82
 
@@ -372,6 +374,17 @@ module LaunchDarkly
372
374
  #
373
375
  attr_reader :socket_factory
374
376
 
377
+ #
378
+ # Initial set of hooks for the client.
379
+ #
380
+ # Hooks provide entrypoints which allow for observation of SDK functions.
381
+ #
382
+ # LaunchDarkly provides integration packages, and most applications will not
383
+ # need to implement their own hooks. Refer to the `launchdarkly-server-sdk-otel` gem
384
+ # for instrumentation.
385
+ #
386
+ attr_reader :hooks
387
+
375
388
  #
376
389
  # The default LaunchDarkly client configuration. This configuration sets
377
390
  # reasonable defaults for most users.
@@ -458,7 +471,7 @@ module LaunchDarkly
458
471
  # @return [Logger] the Rails logger if in Rails, or a default Logger at WARN level otherwise
459
472
  #
460
473
  def self.default_logger
461
- if defined?(Rails) && Rails.respond_to?(:logger)
474
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
462
475
  Rails.logger
463
476
  else
464
477
  log = ::Logger.new($stdout)
@@ -0,0 +1,34 @@
1
+ module LaunchDarkly
2
+ module Impl
3
+ #
4
+ # Simple helper class for returning formatted data.
5
+ #
6
+ # The variation methods make use of the new hook support. Those methods all need to return an evaluation detail, and
7
+ # some other unstructured bit of data.
8
+ #
9
+ class EvaluationWithHookResult
10
+ #
11
+ # Return the evaluation detail that was generated as part of the evaluation.
12
+ #
13
+ # @return [LaunchDarkly::EvaluationDetail]
14
+ #
15
+ attr_reader :evaluation_detail
16
+
17
+ #
18
+ # All purpose container for additional return values from the wrapping method
19
+ #
20
+ # @return [any]
21
+ #
22
+ attr_reader :results
23
+
24
+ #
25
+ # @param evaluation_detail [LaunchDarkly::EvaluationDetail]
26
+ # @param results [any]
27
+ #
28
+ def initialize(evaluation_detail, results = nil)
29
+ @evaluation_detail = evaluation_detail
30
+ @results = results
31
+ end
32
+ end
33
+ end
34
+ end
@@ -37,7 +37,7 @@ module LaunchDarkly
37
37
  @off_variation = data[:offVariation]
38
38
  check_variation_range(self, errors, @off_variation, "off variation")
39
39
  @prerequisites = (data[:prerequisites] || []).map do |prereq_data|
40
- Prerequisite.new(prereq_data, self, errors)
40
+ Prerequisite.new(prereq_data, self)
41
41
  end
42
42
  @targets = (data[:targets] || []).map do |target_data|
43
43
  Target.new(target_data, self, errors)
@@ -118,13 +118,12 @@ module LaunchDarkly
118
118
  end
119
119
 
120
120
  class Prerequisite
121
- def initialize(data, flag, errors_out = nil)
121
+ def initialize(data, flag)
122
122
  @data = data
123
123
  @key = data[:key]
124
124
  @variation = data[:variation]
125
125
  @failure_result = EvaluatorHelpers.evaluation_detail_for_off_variation(flag,
126
126
  EvaluationReason::prerequisite_failed(@key))
127
- check_variation_range(flag, errors_out, @variation, "prerequisite")
128
127
  end
129
128
 
130
129
  # @return [Hash]
@@ -885,5 +885,90 @@ module LaunchDarkly
885
885
  end
886
886
  end
887
887
  end
888
+
889
+ module Hooks
890
+ #
891
+ # Mixin for extending SDK functionality via hooks.
892
+ #
893
+ # All provided hook implementations **MUST** include this mixin. Hooks without this mixin will be ignored.
894
+ #
895
+ # This mixin includes default implementations for all hook handlers. This allows LaunchDarkly to expand the list
896
+ # of hook handlers without breaking customer integrations.
897
+ #
898
+ module Hook
899
+ #
900
+ # Get metadata about the hook implementation.
901
+ #
902
+ # @return [Metadata]
903
+ #
904
+ def metadata
905
+ Metadata.new('UNDEFINED')
906
+ end
907
+
908
+ #
909
+ # The before method is called during the execution of a variation method before the flag value has been
910
+ # determined. The method is executed synchronously.
911
+ #
912
+ # @param evaluation_series_context [EvaluationSeriesContext] Contains information about the evaluation being
913
+ # performed. This is not mutable.
914
+ # @param data [Hash] A record associated with each stage of hook invocations. Each stage is called with the data
915
+ # of the previous stage for a series. The input record should not be modified.
916
+ # @return [Hash] Data to use when executing the next state of the hook in the evaluation series.
917
+ #
918
+ def before_evaluation(evaluation_series_context, data)
919
+ data
920
+ end
921
+
922
+ #
923
+ # The after method is called during the execution of the variation method after the flag value has been
924
+ # determined. The method is executed synchronously.
925
+ #
926
+ # @param evaluation_series_context [EvaluationSeriesContext] Contains read-only information about the evaluation
927
+ # being performed.
928
+ # @param data [Hash] A record associated with each stage of hook invocations. Each stage is called with the data
929
+ # of the previous stage for a series.
930
+ # @param detail [LaunchDarkly::EvaluationDetail] The result of the evaluation. This value should not be
931
+ # modified.
932
+ # @return [Hash] Data to use when executing the next state of the hook in the evaluation series.
933
+ #
934
+ def after_evaluation(evaluation_series_context, data, detail)
935
+ data
936
+ end
937
+ end
938
+
939
+ #
940
+ # Metadata data class used for annotating hook implementations.
941
+ #
942
+ class Metadata
943
+ attr_reader :name
944
+
945
+ def initialize(name)
946
+ @name = name
947
+ end
948
+ end
949
+
950
+ #
951
+ # Contextual information that will be provided to handlers during evaluation series.
952
+ #
953
+ class EvaluationSeriesContext
954
+ attr_reader :key
955
+ attr_reader :context
956
+ attr_reader :default_value
957
+ attr_reader :method
958
+
959
+ #
960
+ # @param key [String]
961
+ # @param context [LaunchDarkly::LDContext]
962
+ # @param default_value [any]
963
+ # @param method [Symbol]
964
+ #
965
+ def initialize(key, context, default_value, method)
966
+ @key = key
967
+ @context = context
968
+ @default_value = default_value
969
+ @method = method
970
+ end
971
+ end
972
+ end
888
973
  end
889
974
  end
@@ -4,9 +4,11 @@ require "ldclient-rb/impl/data_source"
4
4
  require "ldclient-rb/impl/data_store"
5
5
  require "ldclient-rb/impl/diagnostic_events"
6
6
  require "ldclient-rb/impl/evaluator"
7
+ require "ldclient-rb/impl/evaluation_with_hook_result"
7
8
  require "ldclient-rb/impl/flag_tracker"
8
9
  require "ldclient-rb/impl/store_client_wrapper"
9
10
  require "ldclient-rb/impl/migrations/tracker"
11
+ require "concurrent"
10
12
  require "concurrent/atomics"
11
13
  require "digest/sha1"
12
14
  require "forwardable"
@@ -54,6 +56,7 @@ module LaunchDarkly
54
56
  end
55
57
 
56
58
  @sdk_key = sdk_key
59
+ @hooks = Concurrent::Array.new(config.hooks)
57
60
 
58
61
  @shared_executor = Concurrent::SingleThreadExecutor.new
59
62
 
@@ -131,6 +134,23 @@ module LaunchDarkly
131
134
  end
132
135
  end
133
136
 
137
+ #
138
+ # Add a hook to the client. In order to register a hook before the client starts, please use the `hooks` property of
139
+ # {#LDConfig}.
140
+ #
141
+ # Hooks provide entrypoints which allow for observation of SDK functions.
142
+ #
143
+ # @param hook [Interfaces::Hooks::Hook]
144
+ #
145
+ def add_hook(hook)
146
+ unless hook.is_a?(Interfaces::Hooks::Hook)
147
+ @config.logger.error { "[LDClient] Attempted to add a hook that does not include the LaunchDarkly::Intefaces::Hooks::Hook mixin. Ignoring." }
148
+ return
149
+ end
150
+
151
+ @hooks.push(hook)
152
+ end
153
+
134
154
  #
135
155
  # Tells the client that all pending analytics events should be delivered as soon as possible.
136
156
  #
@@ -198,8 +218,13 @@ module LaunchDarkly
198
218
  # @return the variation for the provided context, or the default value if there's an error
199
219
  #
200
220
  def variation(key, context, default)
201
- detail, _, _, = variation_with_flag(key, context, default)
202
- detail.value
221
+ context = Impl::Context::make_context(context)
222
+ result = evaluate_with_hooks(key, context, default, :variation) do
223
+ detail, _, _ = variation_with_flag(key, context, default)
224
+ LaunchDarkly::Impl::EvaluationWithHookResult.new(detail)
225
+ end
226
+
227
+ result.evaluation_detail.value
203
228
  end
204
229
 
205
230
  #
@@ -226,8 +251,118 @@ module LaunchDarkly
226
251
  # @return [EvaluationDetail] an object describing the result
227
252
  #
228
253
  def variation_detail(key, context, default)
229
- detail, _, _ = evaluate_internal(key, context, default, true)
230
- detail
254
+ context = Impl::Context::make_context(context)
255
+ result = evaluate_with_hooks(key, context, default, :variation_detail) do
256
+ detail, _, _ = evaluate_internal(key, context, default, true)
257
+ LaunchDarkly::Impl::EvaluationWithHookResult.new(detail)
258
+ end
259
+
260
+ result.evaluation_detail
261
+ end
262
+
263
+ #
264
+ # evaluate_with_hook will run the provided block, wrapping it with evaluation hook support.
265
+ #
266
+ # Example:
267
+ #
268
+ # ```ruby
269
+ # evaluate_with_hooks(key, context, default, method) do
270
+ # puts 'This is being wrapped with evaluation hooks'
271
+ # end
272
+ # ```
273
+ #
274
+ # @param key [String]
275
+ # @param context [LDContext]
276
+ # @param default [any]
277
+ # @param method [Symbol]
278
+ # @param &block [#call] Implicit passed block
279
+ #
280
+ # @return [LaunchDarkly::Impl::EvaluationWithHookResult]
281
+ #
282
+ private def evaluate_with_hooks(key, context, default, method)
283
+ return yield if @hooks.empty?
284
+
285
+ hooks, evaluation_series_context = prepare_hooks(key, context, default, method)
286
+ hook_data = execute_before_evaluation(hooks, evaluation_series_context)
287
+ evaluation_result = yield
288
+ execute_after_evaluation(hooks, evaluation_series_context, hook_data, evaluation_result.evaluation_detail)
289
+
290
+ evaluation_result
291
+ end
292
+
293
+ #
294
+ # Execute the :before_evaluation stage of the evaluation series.
295
+ #
296
+ # This method will return the results of each hook, indexed into an array in the same order as the hooks. If a hook
297
+ # raised an uncaught exception, the value will be nil.
298
+ #
299
+ # @param hooks [Array<Interfaces::Hooks::Hook>]
300
+ # @param evaluation_series_context [EvaluationSeriesContext]
301
+ #
302
+ # @return [Array<any>]
303
+ #
304
+ private def execute_before_evaluation(hooks, evaluation_series_context)
305
+ hooks.map do |hook|
306
+ try_execute_stage(:before_evaluation, hook.metadata.name) do
307
+ hook.before_evaluation(evaluation_series_context, {})
308
+ end
309
+ end
310
+ end
311
+
312
+ #
313
+ # Execute the :after_evaluation stage of the evaluation series.
314
+ #
315
+ # This method will return the results of each hook, indexed into an array in the same order as the hooks. If a hook
316
+ # raised an uncaught exception, the value will be nil.
317
+ #
318
+ # @param hooks [Array<Interfaces::Hooks::Hook>]
319
+ # @param evaluation_series_context [EvaluationSeriesContext]
320
+ # @param hook_data [Array<any>]
321
+ # @param evaluation_detail [EvaluationDetail]
322
+ #
323
+ # @return [Array<any>]
324
+ #
325
+ private def execute_after_evaluation(hooks, evaluation_series_context, hook_data, evaluation_detail)
326
+ hooks.zip(hook_data).reverse.map do |(hook, data)|
327
+ try_execute_stage(:after_evaluation, hook.metadata.name) do
328
+ hook.after_evaluation(evaluation_series_context, data, evaluation_detail)
329
+ end
330
+ end
331
+ end
332
+
333
+ #
334
+ # Try to execute the provided block. If execution raises an exception, catch and log it, then move on with
335
+ # execution.
336
+ #
337
+ # @return [any]
338
+ #
339
+ private def try_execute_stage(method, hook_name)
340
+ begin
341
+ yield
342
+ rescue => e
343
+ @config.logger.error { "[LDClient] An error occurred in #{method} of the hook #{hook_name}: #{e}" }
344
+ nil
345
+ end
346
+ end
347
+
348
+ #
349
+ # Return a copy of the existing hooks and a few instance of the EvaluationSeriesContext used for the evaluation series.
350
+ #
351
+ # @param key [String]
352
+ # @param context [LDContext]
353
+ # @param default [any]
354
+ # @param method [Symbol]
355
+ # @return [Array[Array<Interfaces::Hooks::Hook>, Interfaces::Hooks::EvaluationSeriesContext]]
356
+ #
357
+ private def prepare_hooks(key, context, default, method)
358
+ # Copy the hooks to use a consistent set during the evaluation series.
359
+ #
360
+ # Hooks can be added and we want to ensure all correct stages for a given hook execute. For example, we do not
361
+ # want to trigger the after_evaluation method without also triggering the before_evaluation method.
362
+ hooks = @hooks.dup
363
+ evaluation_series_context = Interfaces::Hooks::EvaluationSeriesContext.new(key, context, default, method)
364
+
365
+ [hooks, evaluation_series_context]
231
366
  end
232
367
 
233
368
  #
@@ -249,20 +384,24 @@ module LaunchDarkly
249
384
  end
250
385
 
251
386
  context = Impl::Context::make_context(context)
252
- detail, flag, _ = variation_with_flag(key, context, default_stage.to_s)
387
+ result = evaluate_with_hooks(key, context, default_stage, :migration_variation) do
388
+ detail, flag, _ = variation_with_flag(key, context, default_stage.to_s)
253
389
 
254
- stage = detail.value
255
- stage = stage.to_sym if stage.respond_to? :to_sym
390
+ stage = detail.value
391
+ stage = stage.to_sym if stage.respond_to? :to_sym
256
392
 
257
- if Migrations::VALID_STAGES.include?(stage)
393
+ if Migrations::VALID_STAGES.include?(stage)
394
+ tracker = Impl::Migrations::OpTracker.new(@config.logger, key, flag, context, detail, default_stage)
395
+ next LaunchDarkly::Impl::EvaluationWithHookResult.new(detail, {stage: stage, tracker: tracker})
396
+ end
397
+
398
+ detail = LaunchDarkly::Impl::Evaluator.error_result(LaunchDarkly::EvaluationReason::ERROR_WRONG_TYPE, default_stage.to_s)
258
399
  tracker = Impl::Migrations::OpTracker.new(@config.logger, key, flag, context, detail, default_stage)
259
- return stage, tracker
260
- end
261
400
 
262
- detail = LaunchDarkly::Impl::Evaluator.error_result(LaunchDarkly::EvaluationReason::ERROR_WRONG_TYPE, default_stage.to_s)
263
- tracker = Impl::Migrations::OpTracker.new(@config.logger, key, flag, context, detail, default_stage)
401
+ LaunchDarkly::Impl::EvaluationWithHookResult.new(detail, {stage: default_stage, tracker: tracker})
402
+ end
264
403
 
265
- [default_stage, tracker]
404
+ [result.results[:stage], result.results[:tracker]]
266
405
  end
267
406
 
268
407
  #
@@ -502,7 +641,7 @@ module LaunchDarkly
502
641
 
503
642
  #
504
643
  # @param key [String]
505
- # @param context [Hash, LDContext]
644
+ # @param context [LDContext]
506
645
  # @param default [Object]
507
646
  #
508
647
  # @return [Array<EvaluationDetail, [LaunchDarkly::Impl::Model::FeatureFlag, nil], [String, nil]>]
@@ -513,7 +652,7 @@ module LaunchDarkly
513
652
 
514
653
  #
515
654
  # @param key [String]
516
- # @param context [Hash, LDContext]
655
+ # @param context [LDContext]
517
656
  # @param default [Object]
518
657
  # @param with_reasons [Boolean]
519
658
  #
@@ -530,7 +669,6 @@ module LaunchDarkly
530
669
  return detail, nil, "no context provided"
531
670
  end
532
671
 
533
- context = Impl::Context::make_context(context)
534
672
  unless context.valid?
535
673
  @config.logger.error { "[LDClient] Context was invalid for evaluation of flag '#{key}' (#{context.error}); returning default value" }
536
674
  detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "8.3.0" # x-release-please-version
2
+ VERSION = "8.4.0" # x-release-please-version
3
3
  end
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: 8.3.0
4
+ version: 8.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: 2024-03-14 00:00:00.000000000 Z
11
+ date: 2024-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb
@@ -295,6 +295,7 @@ files:
295
295
  - lib/ldclient-rb/impl/data_store.rb
296
296
  - lib/ldclient-rb/impl/dependency_tracker.rb
297
297
  - lib/ldclient-rb/impl/diagnostic_events.rb
298
+ - lib/ldclient-rb/impl/evaluation_with_hook_result.rb
298
299
  - lib/ldclient-rb/impl/evaluator.rb
299
300
  - lib/ldclient-rb/impl/evaluator_bucketing.rb
300
301
  - lib/ldclient-rb/impl/evaluator_helpers.rb