diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a22014f5..83090cb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,20 +10,13 @@ on: - "**.md" jobs: - build-linux-oldest: + build-linux: uses: ./.github/workflows/build-gem.yml + strategy: + matrix: + version: ["3.2", "jruby-9.4"] with: - version: "3.2" - - build-linux-latest: - uses: ./.github/workflows/build-gem.yml - with: - version: "3.2" - - build-linux-jruby: - uses: ./.github/workflows/build-gem.yml - with: - version: "jruby-9.4" + version: ${{ matrix.version }} build-docs: runs-on: ubuntu-latest diff --git a/lib/ldclient-rb/config.rb b/lib/ldclient-rb/config.rb index bca3db2c..5ec80393 100644 --- a/lib/ldclient-rb/config.rb +++ b/lib/ldclient-rb/config.rb @@ -45,6 +45,7 @@ class Config # @option opts [String] :payload_filter_key See {#payload_filter_key} # @option opts [Boolean] :omit_anonymous_contexts See {#omit_anonymous_contexts} # @option hooks [Array] A list of hooks to be registered with the SDK + # + def get_hooks(environment_metadata) + [] + end + end + end + end +end diff --git a/lib/ldclient-rb/ldclient.rb b/lib/ldclient-rb/ldclient.rb index 379ab71e..2c986933 100644 --- a/lib/ldclient-rb/ldclient.rb +++ b/lib/ldclient-rb/ldclient.rb @@ -89,7 +89,10 @@ def postfork(wait_for_sec = 5) end private def start_up(wait_for_sec) - @hooks = Concurrent::Array.new(@config.hooks) + environment_metadata = get_environment_metadata + plugin_hooks = get_plugin_hooks(environment_metadata) + + @hooks = Concurrent::Array.new(@config.hooks + plugin_hooks) @shared_executor = Concurrent::SingleThreadExecutor.new @@ -156,6 +159,8 @@ def postfork(wait_for_sec = 5) @data_source = data_source_or_factory end + register_plugins(environment_metadata) + ready = @data_source.start return unless wait_for_sec > 0 @@ -172,6 +177,47 @@ def postfork(wait_for_sec = 5) end end + private def get_environment_metadata + sdk_metadata = Interfaces::Plugins::SdkMetadata.new( + name: "ruby-server-sdk", + version: LaunchDarkly::VERSION, + wrapper_name: @config.wrapper_name, + wrapper_version: @config.wrapper_version + ) + + application_metadata = nil + if @config.application && !@config.application.empty? + application_metadata = Interfaces::Plugins::ApplicationMetadata.new( + id: @config.application[:id], + version: @config.application[:version] + ) + end + + Interfaces::Plugins::EnvironmentMetadata.new( + sdk: sdk_metadata, + application: application_metadata, + sdk_key: @sdk_key + ) + end + + private def get_plugin_hooks(environment_metadata) + hooks = [] + @config.plugins.each do |plugin| + hooks.concat(plugin.get_hooks(environment_metadata)) + rescue => e + @config.logger.error { "[LDClient] Error getting hooks from plugin #{plugin.metadata.name}: #{e}" } + end + hooks + end + + private def register_plugins(environment_metadata) + @config.plugins.each do |plugin| + plugin.register(self, environment_metadata) + rescue => e + @config.logger.error { "[LDClient] Error registering plugin #{plugin.metadata.name}: #{e}" } + end + end + # # Add a hook to the client. In order to register a hook before the client starts, please use the `hooks` property of # {#LDConfig}. @@ -375,12 +421,10 @@ def variation_detail(key, context, default) # @return [any] # private def try_execute_stage(method, hook_name) - begin - yield - rescue => e - @config.logger.error { "[LDClient] An error occurred in #{method} of the hook #{hook_name}: #{e}" } - nil - end + yield + rescue => e + @config.logger.error { "[LDClient] An error occurred in #{method} of the hook #{hook_name}: #{e}" } + nil end # diff --git a/spec/ldclient_plugins_spec.rb b/spec/ldclient_plugins_spec.rb new file mode 100644 index 00000000..a05c1cf1 --- /dev/null +++ b/spec/ldclient_plugins_spec.rb @@ -0,0 +1,146 @@ +require "mock_components" +require "spec_helper" + +module LaunchDarkly + describe "LDClient plugins tests" do + context "plugin configuration" do + it "can register a plugin on the config" do + plugin = MockPlugin.new("test-plugin") + config = test_config(plugins: [plugin]) + expect(config.plugins.length).to eq 1 + expect(config.plugins[0]).to eq plugin + end + + it "will drop invalid plugins on config" do + config = test_config(plugins: [true, nil, "example thing"]) + expect(config.plugins.count).to eq 0 + end + + it "can register multiple plugins" do + plugin1 = MockPlugin.new("plugin1") + plugin2 = MockPlugin.new("plugin2") + config = test_config(plugins: [plugin1, plugin2]) + expect(config.plugins.length).to eq 2 + end + end + + context "plugin hook collection" do + it "collects hooks from plugins" do + hook = MockHook.new(->(_, _) { }, ->(_, _, _) { }) + plugin = MockPlugin.new("test-plugin", [hook]) + + with_client(test_config(plugins: [plugin])) do |client| + expect(client.instance_variable_get("@hooks")).to include(hook) + end + end + + it "handles plugin hook errors gracefully" do + plugin = MockPlugin.new("error-plugin") + allow(plugin).to receive(:get_hooks).and_raise("Hook error") + + with_client(test_config(plugins: [plugin])) do |client| + expect(client).to be_initialized + end + end + end + + context "plugin registration" do + it "calls register on plugins during initialization" do + registered = false + register_callback = ->(client, metadata) { registered = true } + plugin = MockPlugin.new("test-plugin", [], register_callback) + + with_client(test_config(plugins: [plugin])) do |client| + expect(registered).to be true + end + end + + it "provides correct environment metadata to plugins" do + received_metadata = nil + register_callback = ->(client, metadata) { received_metadata = metadata } + plugin = MockPlugin.new("test-plugin", [], register_callback) + + with_client(test_config(plugins: [plugin])) do |client| + expect(received_metadata).to be_a(Interfaces::Plugins::EnvironmentMetadata) + expect(received_metadata.sdk.name).to eq("ruby-server-sdk") + expect(received_metadata.sdk.version).to eq(LaunchDarkly::VERSION) + end + end + + it "handles plugin registration errors gracefully" do + register_callback = ->(client, metadata) { raise "Registration error" } + plugin = MockPlugin.new("error-plugin", [], register_callback) + + with_client(test_config(plugins: [plugin])) do |client| + expect(client).to be_initialized + end + end + end + + context "plugin execution order" do + it "registers plugins in the order they were added" do + order = [] + plugin1 = MockPlugin.new("plugin1", [], ->(_, _) { order << "plugin1" }) + plugin2 = MockPlugin.new("plugin2", [], ->(_, _) { order << "plugin2" }) + + with_client(test_config(plugins: [plugin1, plugin2])) do |client| + expect(order).to eq ["plugin1", "plugin2"] + end + end + + it "plugin hooks are added after config hooks" do + config_hook = MockHook.new(->(_, _) { }, ->(_, _, _) { }) + plugin_hook = MockHook.new(->(_, _) { }, ->(_, _, _) { }) + plugin = MockPlugin.new("test-plugin", [plugin_hook]) + + with_client(test_config(hooks: [config_hook], plugins: [plugin])) do |client| + hooks = client.instance_variable_get("@hooks") + config_hook_index = hooks.index(config_hook) + plugin_hook_index = hooks.index(plugin_hook) + expect(config_hook_index).to be < plugin_hook_index + end + end + end + + context "metadata classes" do + it "creates SdkMetadata correctly" do + metadata = Interfaces::Plugins::SdkMetadata.new( + name: "test-sdk", + version: "1.0.0", + wrapper_name: "test-wrapper", + wrapper_version: "2.0.0" + ) + + expect(metadata.name).to eq("test-sdk") + expect(metadata.version).to eq("1.0.0") + expect(metadata.wrapper_name).to eq("test-wrapper") + expect(metadata.wrapper_version).to eq("2.0.0") + end + + it "creates ApplicationMetadata correctly" do + metadata = Interfaces::Plugins::ApplicationMetadata.new( + id: "test-app", + version: "3.0.0" + ) + + expect(metadata.id).to eq("test-app") + expect(metadata.version).to eq("3.0.0") + end + + it "creates EnvironmentMetadata correctly" do + sdk_metadata = Interfaces::Plugins::SdkMetadata.new(name: "test", version: "1.0") + app_metadata = Interfaces::Plugins::ApplicationMetadata.new(id: "app") + + metadata = Interfaces::Plugins::EnvironmentMetadata.new( + sdk: sdk_metadata, + application: app_metadata, + sdk_key: "test-key" + ) + + expect(metadata.sdk).to eq(sdk_metadata) + expect(metadata.application).to eq(app_metadata) + expect(metadata.sdk_key).to eq("test-key") + end + end + end +end diff --git a/spec/mock_components.rb b/spec/mock_components.rb index 4eb0e7a8..1a4470fa 100644 --- a/spec/mock_components.rb +++ b/spec/mock_components.rb @@ -118,4 +118,26 @@ def after_evaluation(evaluation_series_context, data, detail) @after_evaluation.call(evaluation_series_context, data, detail) end end + + class MockPlugin + include Interfaces::Plugins::Plugin + + def initialize(name, hooks = [], register_callback = nil) + @name = name + @hooks = hooks + @register_callback = register_callback + end + + def metadata + Interfaces::Plugins::PluginMetadata.new(@name) + end + + def get_hooks(environment_metadata) + @hooks + end + + def register(client, environment_metadata) + @register_callback.call(client, environment_metadata) if @register_callback + end + end end