From 4046c4f7a0a2c47b1450697e2dc65a03f7f9c121 Mon Sep 17 00:00:00 2001 From: Jose Colella Date: Thu, 5 Mar 2026 08:58:40 -0800 Subject: [PATCH 1/2] feat: populate error_code and message in event details payload (spec 5.1.4, 5.1.5) - ProviderStateRegistry now stores event details alongside state, so immediate handlers attached after a provider error can access error_code and message - Extract build_event_details helper in Configuration to include stored details when firing immediate handlers - Add spec tests for Requirements 5.1.4 (error message) and 5.1.5 (error code), including immediate handler scenario Closes #210 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Jose Colella --- lib/open_feature/sdk/configuration.rb | 11 ++-- .../sdk/provider_state_registry.rb | 16 +++++- spec/specification/events_spec.rb | 57 +++++++++++++++++++ 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/lib/open_feature/sdk/configuration.rb b/lib/open_feature/sdk/configuration.rb index 10591459..b1df9d6d 100644 --- a/lib/open_feature/sdk/configuration.rb +++ b/lib/open_feature/sdk/configuration.rb @@ -235,8 +235,7 @@ def run_immediate_handler(event_type, handler, client) provider_state = @provider_state_registry.get_state(provider) if event_type == status_to_event[provider_state] - provider_name = extract_provider_name(provider) - event_details = {provider_name: provider_name} + event_details = build_event_details(provider) begin handler.call(event_details) @@ -253,8 +252,7 @@ def run_immediate_handler(event_type, handler, client) provider_state = @provider_state_registry.get_state(client_provider) if event_type == status_to_event[provider_state] - provider_name = extract_provider_name(client_provider) - event_details = {provider_name: provider_name} + event_details = build_event_details(client_provider) begin handler.call(event_details) @@ -264,6 +262,11 @@ def run_immediate_handler(event_type, handler, client) end end end + + def build_event_details(provider) + stored_details = @provider_state_registry.get_details(provider) + {provider_name: extract_provider_name(provider)}.merge(stored_details) + end end end end diff --git a/lib/open_feature/sdk/provider_state_registry.rb b/lib/open_feature/sdk/provider_state_registry.rb index c4b5940c..b825b2cf 100644 --- a/lib/open_feature/sdk/provider_state_registry.rb +++ b/lib/open_feature/sdk/provider_state_registry.rb @@ -17,7 +17,7 @@ def set_initial_state(provider, state = ProviderState::NOT_READY) return unless provider @mutex.synchronize do - @states[provider.object_id] = state + @states[provider.object_id] = {state: state, details: {}} end end @@ -29,7 +29,7 @@ def update_state_from_event(provider, event_type, event_details = nil) # Only update state if the event should cause a state change if new_state @mutex.synchronize do - @states[provider.object_id] = new_state + @states[provider.object_id] = {state: new_state, details: event_details || {}} end new_state else @@ -42,7 +42,17 @@ def get_state(provider) return ProviderState::NOT_READY unless provider @mutex.synchronize do - @states[provider.object_id] || ProviderState::NOT_READY + entry = @states[provider.object_id] + entry ? entry[:state] : ProviderState::NOT_READY + end + end + + def get_details(provider) + return {} unless provider + + @mutex.synchronize do + entry = @states[provider.object_id] + entry ? entry[:details] : {} end end diff --git a/spec/specification/events_spec.rb b/spec/specification/events_spec.rb index bce81f5e..bdd96c6b 100644 --- a/spec/specification/events_spec.rb +++ b/spec/specification/events_spec.rb @@ -98,6 +98,63 @@ def init(_evaluation_context) end end + context "Requirement 5.1.4" do + specify "PROVIDER_ERROR events SHOULD populate the error message field" do + event_details_received = nil + handler = ->(event_details) { event_details_received = event_details } + + OpenFeature::SDK.add_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR, handler) + + provider = OpenFeature::SDK::Provider::InMemoryProvider.new + allow(provider).to receive(:init).and_raise("Custom init failure") + + OpenFeature::SDK.set_provider(provider) + sleep(0.001) until event_details_received + + expect(event_details_received[:message]).to eq("Custom init failure") + + OpenFeature::SDK.remove_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR, handler) + end + end + + context "Requirement 5.1.5" do + specify "PROVIDER_ERROR events SHOULD populate the error code field" do + event_details_received = nil + handler = ->(event_details) { event_details_received = event_details } + + OpenFeature::SDK.add_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR, handler) + + provider = OpenFeature::SDK::Provider::InMemoryProvider.new + allow(provider).to receive(:init).and_raise("Init failed") + + OpenFeature::SDK.set_provider(provider) + sleep(0.001) until event_details_received + + expect(event_details_received[:error_code]).to eq(OpenFeature::SDK::Provider::ErrorCode::GENERAL) + + OpenFeature::SDK.remove_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR, handler) + end + + specify "error details are available in immediate handlers attached after the error" do + provider = OpenFeature::SDK::Provider::InMemoryProvider.new + allow(provider).to receive(:init).and_raise("Delayed failure") + + OpenFeature::SDK.set_provider(provider) + sleep(0.01) # Wait for async init to complete + + event_details_received = nil + handler = ->(event_details) { event_details_received = event_details } + + OpenFeature::SDK.add_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR, handler) + + expect(event_details_received).not_to be_nil + expect(event_details_received[:error_code]).to eq(OpenFeature::SDK::Provider::ErrorCode::GENERAL) + expect(event_details_received[:message]).to eq("Delayed failure") + + OpenFeature::SDK.remove_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR, handler) + end + end + context "Requirement 5.2.1" do specify "The client MUST provide a function for associating handler functions with provider event types" do client = OpenFeature::SDK.build_client(domain: "test-domain") From c7279d7c4d7d96280110e014164f33bac0738343 Mon Sep 17 00:00:00 2001 From: Jose Colella Date: Thu, 5 Mar 2026 09:08:50 -0800 Subject: [PATCH 2/2] fix: replace sleep with set_provider_and_wait to avoid flaky test Use set_provider_and_wait with rescue instead of sleep(0.01) to deterministically wait for async provider init to complete. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Jose Colella --- spec/specification/events_spec.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/specification/events_spec.rb b/spec/specification/events_spec.rb index bdd96c6b..588f2faa 100644 --- a/spec/specification/events_spec.rb +++ b/spec/specification/events_spec.rb @@ -139,8 +139,11 @@ def init(_evaluation_context) provider = OpenFeature::SDK::Provider::InMemoryProvider.new allow(provider).to receive(:init).and_raise("Delayed failure") - OpenFeature::SDK.set_provider(provider) - sleep(0.01) # Wait for async init to complete + begin + OpenFeature::SDK.set_provider_and_wait(provider) + rescue OpenFeature::SDK::ProviderInitializationError + # Expected — provider init fails, putting provider in ERROR state + end event_details_received = nil handler = ->(event_details) { event_details_received = event_details }