diff --git a/lib/mixpanel-ruby/flags/local_flags_provider.rb b/lib/mixpanel-ruby/flags/local_flags_provider.rb index 9bf1bb07..f381b739 100644 --- a/lib/mixpanel-ruby/flags/local_flags_provider.rb +++ b/lib/mixpanel-ruby/flags/local_flags_provider.rb @@ -49,13 +49,13 @@ def start_polling_for_definitions! begin fetch_flag_definitions rescue StandardError => e - @error_handler.handle(e) if @error_handler + log_polling_error(e) end end end end rescue StandardError => e - @error_handler.handle(e) if @error_handler + log_polling_error(e) end def stop_polling_for_definitions! @@ -141,6 +141,19 @@ def get_all_variants(context) private + # Surface polling-loop failures unconditionally. The default + # Mixpanel::ErrorHandler is a no-op, so dispatching only via + # @error_handler swallows schema drift (NoMethodError, + # JSON::ParserError, etc.) — the loop runs forever undetected. + # Always warn so the failure is visible without an error_handler + # being configured. Matches the convention in mixpanel-python / + # mixpanel-java / mixpanel-go / mixpanel-node, all of which log + # unconditionally and keep polling. + def log_polling_error(error) + warn "[Mixpanel] Failed to fetch flag definitions: #{error.class}: #{error.message}" + @error_handler.handle(error) if @error_handler + end + def fetch_flag_definitions response = call_flags_endpoint diff --git a/spec/mixpanel-ruby/flags/local_flags_spec.rb b/spec/mixpanel-ruby/flags/local_flags_spec.rb index 69695215..9ad39d3b 100644 --- a/spec/mixpanel-ruby/flags/local_flags_spec.rb +++ b/spec/mixpanel-ruby/flags/local_flags_spec.rb @@ -755,5 +755,39 @@ def user_context_with_properties(properties) polling_provider.stop_polling_for_definitions! end end + + it 'warns to stderr when fetch raises and no error_handler is configured' do + stub_request(:get, endpoint_url_regex).to_return(status: 500, body: 'server down') + + polling_provider = Mixpanel::Flags::LocalFlagsProvider.new( + test_token, + { enable_polling: false }, + mock_tracker, + nil + ) + + expect { polling_provider.start_polling_for_definitions! } + .to output(/\[Mixpanel\] Failed to fetch flag definitions: Mixpanel::ServerError/).to_stderr + end + + it 'surfaces unexpected errors (schema drift) instead of swallowing them silently' do + stub_flag_definitions([create_test_flag]) + + polling_provider = Mixpanel::Flags::LocalFlagsProvider.new( + test_token, + { enable_polling: false }, + mock_tracker, + mock_error_handler + ) + + # Simulate schema drift: server returns a payload the parser doesn't expect. + # Without this fix the error would be dispatched only to @error_handler (whose + # default ErrorHandler#handle is a no-op) — operators would never notice. + allow(polling_provider).to receive(:fetch_flag_definitions).and_raise(NoMethodError, "undefined method `[]' for nil:NilClass") + + expect(mock_error_handler).to receive(:handle).with(an_instance_of(NoMethodError)) + expect { polling_provider.start_polling_for_definitions! } + .to output(/\[Mixpanel\] Failed to fetch flag definitions: NoMethodError/).to_stderr + end end end