feat: add registerPlugin so a plugin can be added after the client starts - #393
Open
abelonogov-ld wants to merge 8 commits into
Open
feat: add registerPlugin so a plugin can be added after the client starts#393abelonogov-ld wants to merge 8 commits into
abelonogov-ld wants to merge 8 commits into
Conversation
…arts Plugins could only be supplied through LDConfig, so an integration that learns about a plugin later — or that wants to instrument a client it did not configure — had no way in. Hooks go live only once register succeeds, matching the .NET ordering, so a plugin whose registration failed never contributes hooks and a plugin's own hooks do not observe its register call. Retaining EnvironmentMetadata on the instance lets a plugin registered later be handed the same environment description as one configured up front. Co-authored-by: Cursor <cursoragent@cursor.com>
6 tasks
…y/android-client-sdk into andrey/register-plugin * 'andrey/register-plugin' of ssh://github.com/launchdarkly/android-client-sdk: fix: make the close/reset timer tests deterministic
| * @deprecated This reports on a batch of plugins registered together, so it has no meaning for | ||
| * {@link LDClient#registerPlugin(Plugin)}, which registers a single plugin and does | ||
| * not call it. Do work that needs the client in {@link #register} instead, which | ||
| * both paths call. |
Contributor
There was a problem hiding this comment.
This deprecation reason should not talk about specific usages of the abstract class.
You can just say "onPluginsReady was necessary in the past, but has since been determined to be problematic and is being removed in the next major version."
abelonogov-ld
added a commit
to launchdarkly/ios-client-sdk
that referenced
this pull request
Aug 25, 2026
…arts (#522) ## Summary Plugins could only be supplied through `LDConfig`, so an integration that learns about a plugin later — or that wants to instrument a client it did not configure — had no way in. This adds `LDClient.registerPlugin(_:)`, matching the method the .NET and Flutter SDKs already expose and the one being added to the Android SDK in launchdarkly/android-client-sdk#393. - **Hooks became mutable, because registration after start cannot extend a constant array.** They now live in a `private var` behind an `NSLock` and are replaced rather than mutated in place. Each series reads a snapshot once into a local, so the hooks a series ends with are the hooks it began with — were the array read again mid-series, a hook registered in between would be handed an `after` stage for a series whose `before` stage it was never in. The three read sites (`evaluateWithHooks`, `executeBeforeIdentifyHooks`, `executeAfterTrackHooks`) all take that snapshot up front. - **Hooks go live only once `register` returns.** This matches the Android and .NET ordering, and differs from configuration-time registration where a plugin's hooks are active before `register` is called. So a plugin's own hooks will not observe evaluations or identify calls its `register` makes; everything afterwards does run them. - **`EnvironmentMetadata` is retained on the instance**, so a plugin registered later is handed the same environment description as one configured up front. This also removes the duplicate construction that previously existed in both `start` and `collectHooks`. Registration applies to the one client it is called on, so a multi-environment setup means calling it per environment. Note that the iOS `Plugin` protocol has no `onPluginsReady`, so unlike Android this path is just `getHooks` then `register` then activate. ## Test plan Five cases added to `LDClientPluginsSpec`, all passing on an iPhone 16e simulator alongside the two pre-existing plugin tests (7 total): - [x] `testRegisterPluginPassesClientAndEnvironmentMetadata` - [x] `testRegisterPluginActivatesBundledHooks` - [x] `testRegisterPluginDoesNotRunTheRegisteringPluginsOwnHooks` - [x] `testRegisterPluginHooksRunAfterConfiguredHooks` - [x] `testRegisterPluginAppliesOnlyToTheClientItIsCalledOn` - [x] SwiftLint introduces no new violations in the touched files <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Overview** > Adds **`LDClient.registerPlugin(_:)`** so integrations can attach plugins after `start`, aligned with other LaunchDarkly mobile SDKs. Plugins still use `LDConfig.plugins` at startup; this path is for late discovery or clients the app did not configure. > > **Hook list is now mutable and thread-safe.** Hooks move from a fixed `let` array to `storedHooks` guarded by `NSLock`, with `addHooks` appending plugin hooks at registration. Evaluation, identify, and track paths each **snapshot** `hooks` once per series so mid-flight `registerPlugin` cannot pair an `after` stage with a `before` the new hook never ran. > > **`environmentMetadata` is stored on the client** and passed to `register` and `getHooks`, so late plugins get the same environment description as config-time plugins and duplicate metadata construction in `start` / `collectHooks` is removed. > > `registerPlugin` calls `getHooks` and activates hooks **before** `Plugin.register`, matching config-time ordering (including work done inside `register`). Registration applies only to the client instance you call it on (multi-environment setups need per-client calls). > > Tests in `LDClientPluginsSpec` cover metadata handoff, hook activation, ordering after config hooks, hooks during `register`, and per-environment isolation. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1c5db77. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b1bb297. Configure here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Plugins could only be supplied through
LDConfig, so an integration that learns about a plugin later — or that wants to instrument a client it did not configure — had no way in. This addsLDClient.registerPlugin(Plugin), mirroring the method the .NET and Flutter SDKs already expose.registersucceeds. This follows the .NET ordering, and it differs from configuration-time registration, where a plugin's hooks are active beforeregisteris called. The consequence is that a plugin whose registration failed never contributes hooks, and a plugin's own hooks do not observe the evaluations or identify calls itsregistermakes.EnvironmentMetadatais now retained on the instance rather than living in a local map duringinit, so a plugin registered later is handed the same environment description as one configured up front. Theinitloop switches from aMap<LDClient, EnvironmentMetadata>to aList<LDClient>accordingly.getHooksleaves the plugin unregistered; a throwingregisteryields a failure result passed toonPluginsReady. Neither propagates to the caller. Reading the plugin's name for the log message tolerates metadata that itself throws, so reporting one failure cannot raise another out of the handler reporting it.Registration applies to the one client it is called on, so a multi-environment setup means calling it per environment.
Test plan
Eight cases added to
LDClientPluginsTest, all passing on an API 36 emulator alongside the three pre-existing plugin tests (11 total):registerPluginPassesClientAndEnvironmentMetadataregisterPluginActivatesBundledHooksregisterPluginDoesNotRunTheRegisteringPluginsOwnHooksregisterPluginReportsSuccessToOnPluginsReadyregisterPluginDoesNotRegisterPluginWhoseGetHooksThrowsregisterPluginToleratesRegisterThrowingregisterPluginRejectsNullPluginregisterPluginAppliesOnlyToTheClientItIsCalledOntestDebugUnitTestgreenNote
Overview
Adds
LDClient.registerPlugin(Plugin)so integrations can attach plugins after the client is created, aligned with other LaunchDarkly mobile SDKs.EnvironmentMetadatais kept on each client instance so late registration gets the same environment description as config-time plugins.Plugin registration is unified in a private
registerPluginspath used by bothLDConfigplugins at init andregisterPlugin. For each plugin:getHooks, thenregister, then all successful hooks are activated in one batch via newHookRunner.addHooks. Hooks are not active during any plugin’sregistercall, and plugins that throw fromgetHooksorregistercontribute no hooks (errors are logged only). Init-time registration still invokes deprecatedonPluginsReadyfor the configured batch;registerPlugindoes not call it and applies only to the client it was invoked on.Plugin.onPluginsReadyis marked@Deprecated. Instrumentation tests cover failure isolation, hook timing, metadata, null rejection, and multi-environment scoping.Reviewed by Cursor Bugbot for commit 0940c6b. Bugbot is set up for automated code reviews on this repo. Configure here.