diff --git a/openfeature/client.py b/openfeature/client.py index b166f612..450d646e 100644 --- a/openfeature/client.py +++ b/openfeature/client.py @@ -435,11 +435,20 @@ def _establish_hooks_and_provider( evaluation_hooks = flag_evaluation_options.hooks hook_hints = flag_evaluation_options.hook_hints + # Merge transaction context into evaluation context before creating hook_context + # This ensures hooks have access to the complete context including transaction context + merged_eval_context = ( + get_evaluation_context() + .merge(get_transaction_context()) + .merge(self.context) + .merge(evaluation_context) + ) + hook_context = HookContext( flag_key=flag_key, flag_type=flag_type, default_value=default_value, - evaluation_context=evaluation_context, + evaluation_context=merged_eval_context, client_metadata=self.get_metadata(), provider_metadata=provider.get_metadata(), ) @@ -465,7 +474,7 @@ def _assert_provider_status( return ProviderFatalError() return None - def _before_hooks_and_merge_context( + def _run_before_hooks_and_update_context( self, flag_type: FlagType, hook_context: HookContext, @@ -477,19 +486,14 @@ def _before_hooks_and_merge_context( # Any resulting evaluation context from a before hook will overwrite # duplicate fields defined globally, on the client, or in the invocation. # Requirement 3.2.2, 4.3.4: API.context->client.context->invocation.context - invocation_context = before_hooks( + before_hooks_context = before_hooks( flag_type, hook_context, merged_hooks, hook_hints ) - if evaluation_context: - invocation_context = invocation_context.merge(ctx2=evaluation_context) - # Requirement 3.2.2 merge: API.context->transaction.context->client.context->invocation.context - merged_context = ( - get_evaluation_context() - .merge(get_transaction_context()) - .merge(self.context) - .merge(invocation_context) - ) + # The hook_context.evaluation_context already contains the merged context from + # _establish_hooks_and_provider, so we just need to merge with the before hooks result + merged_context = hook_context.evaluation_context.merge(before_hooks_context) + return merged_context @typing.overload @@ -599,7 +603,7 @@ async def evaluate_flag_details_async( ) return flag_evaluation - merged_context = self._before_hooks_and_merge_context( + merged_context = self._run_before_hooks_and_update_context( flag_type, hook_context, merged_hooks, @@ -775,7 +779,7 @@ def evaluate_flag_details( ) return flag_evaluation - merged_context = self._before_hooks_and_merge_context( + merged_context = self._run_before_hooks_and_update_context( flag_type, hook_context, merged_hooks, diff --git a/tests/test_transaction_context_in_hooks.py b/tests/test_transaction_context_in_hooks.py new file mode 100644 index 00000000..61a5b5cf --- /dev/null +++ b/tests/test_transaction_context_in_hooks.py @@ -0,0 +1,51 @@ +from openfeature.api import ( + set_provider, + set_transaction_context, + set_transaction_context_propagator, +) +from openfeature.client import OpenFeatureClient +from openfeature.evaluation_context import EvaluationContext +from openfeature.hook import Hook +from openfeature.provider.no_op_provider import NoOpProvider +from openfeature.transaction_context import ContextVarsTransactionContextPropagator + + +class TransactionContextHook(Hook): + def __init__(self): + self.before_called = False + self.transaction_attr_value = None + + def before(self, hook_context, hints): + self.before_called = True + # Check if the transaction context attribute is in the hook context + if "transaction_attr" in hook_context.evaluation_context.attributes: + self.transaction_attr_value = hook_context.evaluation_context.attributes[ + "transaction_attr" + ] + return None + + +def test_transaction_context_merged_into_hook_context(): + """Test that transaction context is merged into the hook context's evaluation context.""" + set_transaction_context_propagator(ContextVarsTransactionContextPropagator()) + + provider = NoOpProvider() + set_provider(provider) + + client = OpenFeatureClient(domain=None, version=None) + + hook = TransactionContextHook() + client.add_hooks([hook]) + + transaction_context = EvaluationContext( + targeting_key="transaction", + attributes={"transaction_attr": "transaction_value"}, + ) + set_transaction_context(transaction_context) + + client.get_boolean_value(flag_key="test-flag", default_value=False) + + assert hook.before_called, "Hook's before method was not called" + assert hook.transaction_attr_value == "transaction_value", ( + "Transaction context attribute was not found in hook context" + )