launchdarkly-server-sdk 8.3.1 → 8.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2582d6e0bd754b6fbb46d591713cb9b6d3784ae991a498dfa0f810aa079c491e
4
- data.tar.gz: 68c0369beb8e2bd6f3a865129591a392511a82f85b22fda5b8a8c3c7161f4fa1
3
+ metadata.gz: 6067b2e1ab38ffb622a683a63206d485bd13744f361d99f89de467b5f1bc0c9e
4
+ data.tar.gz: 28999c77f6007effa021c4d88a99d3b6ad7ddd951d0e93449ee25276b311290b
5
5
  SHA512:
6
- metadata.gz: 0516d7e11b48b9d582878270ff533138b73f9fdfb8d89a9dc6470c0aeff8d71a9139d5e7b77a4b5eb0e7c6083c33446ecdf9d57b394cfb650ac53426fee2cd06
7
- data.tar.gz: 71f2eb165af4d1ef812f1360f326b2dfefe6be266c4d476737943661a3c2534f7f275bb02245e4718c653aaa2df3e07675af3a3e3ca1f451cfb045d2bac506c8
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.
@@ -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
@@ -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.1" # 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.1
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-28 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