|
7 | 7 | require "ldclient-rb/impl/flag_tracker"
|
8 | 8 | require "ldclient-rb/impl/store_client_wrapper"
|
9 | 9 | require "ldclient-rb/impl/migrations/tracker"
|
| 10 | +require "concurrent" |
10 | 11 | require "concurrent/atomics"
|
11 | 12 | require "digest/sha1"
|
12 | 13 | require "forwardable"
|
@@ -54,6 +55,7 @@ def initialize(sdk_key, config = Config.default, wait_for_sec = 5)
|
54 | 55 | end
|
55 | 56 |
|
56 | 57 | @sdk_key = sdk_key
|
| 58 | + @hooks = Concurrent::Array.new(config.hooks) |
57 | 59 |
|
58 | 60 | @shared_executor = Concurrent::SingleThreadExecutor.new
|
59 | 61 |
|
@@ -131,6 +133,23 @@ def initialize(sdk_key, config = Config.default, wait_for_sec = 5)
|
131 | 133 | end
|
132 | 134 | end
|
133 | 135 |
|
| 136 | + # |
| 137 | + # Add a hook to the client. In order to register a hook before the client starts, please use the `hooks` property of |
| 138 | + # {#LDConfig}. |
| 139 | + # |
| 140 | + # Hooks provide entrypoints which allow for observation of SDK functions. |
| 141 | + # |
| 142 | + # @param hook [Interfaces::Hooks::Hook] |
| 143 | + # |
| 144 | + def add_hook(hook) |
| 145 | + unless hook.is_a?(Interfaces::Hooks::Hook) |
| 146 | + @config.logger.error { "[LDClient] Attempted to add a hook that does not include the LaunchDarkly::Intefaces::Hooks::Hook mixin. Ignoring." } |
| 147 | + return |
| 148 | + end |
| 149 | + |
| 150 | + @hooks.push(hook) |
| 151 | + end |
| 152 | + |
134 | 153 | #
|
135 | 154 | # Tells the client that all pending analytics events should be delivered as soon as possible.
|
136 | 155 | #
|
@@ -226,10 +245,116 @@ def variation(key, context, default)
|
226 | 245 | # @return [EvaluationDetail] an object describing the result
|
227 | 246 | #
|
228 | 247 | def variation_detail(key, context, default)
|
229 |
| - detail, _, _ = evaluate_internal(key, context, default, true) |
| 248 | + detail, _, _ = evaluate_with_hooks(key, context, default, :variation_detail) do |
| 249 | + evaluate_internal(key, context, default, true) |
| 250 | + end |
| 251 | + |
230 | 252 | detail
|
231 | 253 | end
|
232 | 254 |
|
| 255 | + # |
| 256 | + # evaluate_with_hook will run the provided block, wrapping it with evaluation hook support. |
| 257 | + # |
| 258 | + # Example: |
| 259 | + # |
| 260 | + # ```ruby |
| 261 | + # evaluate_with_hooks(key, context, default, method) do |
| 262 | + # puts 'This is being wrapped with evaluation hooks' |
| 263 | + # end |
| 264 | + # ``` |
| 265 | + # |
| 266 | + # @param key [String] |
| 267 | + # @param context [String] |
| 268 | + # @param default [String] |
| 269 | + # @param method [Symbol] |
| 270 | + # @param &block [#call] Implicit passed block |
| 271 | + # |
| 272 | + private def evaluate_with_hooks(key, context, default, method) |
| 273 | + return yield if @hooks.empty? |
| 274 | + |
| 275 | + hooks, evaluation_series_context = prepare_hooks(key, context, default, method) |
| 276 | + hook_data = execute_before_evaluation(hooks, evaluation_series_context) |
| 277 | + evaluation_detail, flag, error = yield |
| 278 | + execute_after_evaluation(hooks, evaluation_series_context, hook_data, evaluation_detail) |
| 279 | + |
| 280 | + [evaluation_detail, flag, error] |
| 281 | + end |
| 282 | + |
| 283 | + # |
| 284 | + # Execute the :before_evaluation stage of the evaluation series. |
| 285 | + # |
| 286 | + # This method will return the results of each hook, indexed into an array in the same order as the hooks. If a hook |
| 287 | + # raised an uncaught exception, the value will be nil. |
| 288 | + # |
| 289 | + # @param hooks [Array<Interfaces::Hooks::Hook>] |
| 290 | + # @param evaluation_series_context [EvaluationSeriesContext] |
| 291 | + # |
| 292 | + # @return [Array<any>] |
| 293 | + # |
| 294 | + private def execute_before_evaluation(hooks, evaluation_series_context) |
| 295 | + hooks.map do |hook| |
| 296 | + try_execute_stage(:before_evaluation, hook.metadata.name) do |
| 297 | + hook.before_evaluation(evaluation_series_context, {}) |
| 298 | + end |
| 299 | + end |
| 300 | + end |
| 301 | + |
| 302 | + # |
| 303 | + # Execute the :after_evaluation stage of the evaluation series. |
| 304 | + # |
| 305 | + # This method will return the results of each hook, indexed into an array in the same order as the hooks. If a hook |
| 306 | + # raised an uncaught exception, the value will be nil. |
| 307 | + # |
| 308 | + # @param hooks [Array<Interfaces::Hooks::Hook>] |
| 309 | + # @param evaluation_series_context [EvaluationSeriesContext] |
| 310 | + # @param hook_data [Array<any>] |
| 311 | + # @param evaluation_detail [EvaluationDetail] |
| 312 | + # |
| 313 | + # @return [Array<any>] |
| 314 | + # |
| 315 | + private def execute_after_evaluation(hooks, evaluation_series_context, hook_data, evaluation_detail) |
| 316 | + hooks.zip(hook_data).reverse.map do |(hook, data)| |
| 317 | + try_execute_stage(:after_evaluation, hook.metadata.name) do |
| 318 | + hook.after_evaluation(evaluation_series_context, data, evaluation_detail) |
| 319 | + end |
| 320 | + end |
| 321 | + end |
| 322 | + |
| 323 | + # |
| 324 | + # Try to execute the provided block. If execution raises an exception, catch and log it, then move on with |
| 325 | + # execution. |
| 326 | + # |
| 327 | + # @return [any] |
| 328 | + # |
| 329 | + private def try_execute_stage(method, hook_name) |
| 330 | + begin |
| 331 | + yield |
| 332 | + rescue => e |
| 333 | + @config.logger.error { "[LDClient] An error occurred in #{method} of the hook #{hook_name}: #{e}" } |
| 334 | + nil |
| 335 | + end |
| 336 | + end |
| 337 | + |
| 338 | + # |
| 339 | + # Return a copy of the existing hooks and a few instance of the EvaluationSeriesContext used for the evaluation series. |
| 340 | + # |
| 341 | + # @param key [String] |
| 342 | + # @param context [LDContext] |
| 343 | + # @param default [any] |
| 344 | + # @param method [Symbol] |
| 345 | + # @return [Array[Array<Interfaces::Hooks::Hook>, Interfaces::Hooks::EvaluationSeriesContext]] |
| 346 | + # |
| 347 | + private def prepare_hooks(key, context, default, method) |
| 348 | + # Copy the hooks to use a consistent set during the evaluation series. |
| 349 | + # |
| 350 | + # Hooks can be added and we want to ensure all correct stages for a given hook execute. For example, we do not |
| 351 | + # want to trigger the after_evaluation method without also triggering the before_evaluation method. |
| 352 | + hooks = @hooks.dup |
| 353 | + evaluation_series_context = Interfaces::Hooks::EvaluationSeriesContext.new(key, context, default, method) |
| 354 | + |
| 355 | + [hooks, evaluation_series_context] |
| 356 | + end |
| 357 | + |
233 | 358 | #
|
234 | 359 | # This method returns the migration stage of the migration feature flag for the given evaluation context.
|
235 | 360 | #
|
@@ -508,7 +633,9 @@ def create_default_data_source(sdk_key, config, diagnostic_accumulator)
|
508 | 633 | # @return [Array<EvaluationDetail, [LaunchDarkly::Impl::Model::FeatureFlag, nil], [String, nil]>]
|
509 | 634 | #
|
510 | 635 | def variation_with_flag(key, context, default)
|
511 |
| - evaluate_internal(key, context, default, false) |
| 636 | + evaluate_with_hooks(key, context, default, :variation_detail) do |
| 637 | + evaluate_internal(key, context, default, false) |
| 638 | + end |
512 | 639 | end
|
513 | 640 |
|
514 | 641 | #
|
|
0 commit comments