From 40fe078b9e2f72ac68e893fe0451390e999e04fa Mon Sep 17 00:00:00 2001 From: Eduardo Cunha Date: Mon, 23 Jun 2025 16:53:06 +0100 Subject: [PATCH] fix: carry hook context between before and after hooks The hook context after executing before hooks was not being carried to after hooks, which was previnting sharing data between before and after hooks which is essential in certain scenarios. Signed-off-by: Eduardo Cunha --- lib/open_feature/client.ex | 19 +++++++++++----- test/unit/open_feature/client_test.exs | 31 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/lib/open_feature/client.ex b/lib/open_feature/client.ex index 6b936e3..da8b9eb 100644 --- a/lib/open_feature/client.ex +++ b/lib/open_feature/client.ex @@ -258,7 +258,7 @@ defmodule OpenFeature.Client do } try do - before_hooks(all_hooks, hook_context, opts) + hook_context = before_hooks(all_hooks, hook_context, opts) short_circuit_if_not_ready(client) @@ -341,10 +341,19 @@ defmodule OpenFeature.Client do defp before_hooks(hooks, hook_context, opts) do hook_hints = Keyword.get(opts, :hook_hints, %{}) - Enum.each(hooks, fn - %Hook{before: nil} -> :ok - %Hook{before: before} -> before.(hook_context, hook_hints) - end) + context = + Enum.reduce(hooks, hook_context.context, fn + %Hook{before: nil}, context -> + context + + %Hook{before: before}, context -> + case before.(hook_context, hook_hints) do + data when is_map(data) -> Map.merge(context, data) + _other -> context + end + end) + + Map.put(hook_context, :context, context) end defp after_hooks(hooks, hook_context, evaluation_details, opts) do diff --git a/test/unit/open_feature/client_test.exs b/test/unit/open_feature/client_test.exs index 2c92778..05403e2 100644 --- a/test/unit/open_feature/client_test.exs +++ b/test/unit/open_feature/client_test.exs @@ -255,6 +255,37 @@ defmodule OpenFeature.ClientTest do assert_receive :hook_called end + test "context from before hook is carried to after hook", %{client: client} do + parent = self() + context = %{key: "value"} + key = "key" + default = true + + hook = %Hook{ + before: fn _hook_context, _hints -> + send(parent, :before_hook_called) + %{key: "other data"} + end, + after: fn hook_context, _details, _hints -> + assert_receive :provider_called + assert hook_context.context[:key] == "other data" + send(parent, :after_hook_called) + end + } + + expect(Provider, :resolve_value, 1, fn _provider, _type, _key, _default, _context -> + assert_receive :before_hook_called + send(parent, :provider_called) + {:ok, %ResolutionDetails{value: true}} + end) + + client = Client.add_hooks(client, [hook]) + + Client.get_boolean_value(client, key, default, context: context) + + assert_receive :after_hook_called + end + test "does not resolve value if an error occurs in before hooks", %{client: client} do parent = self() context = %{key: "value"}