feat: add registerPlugin so a plugin can be added after the client starts - #522
Merged
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 were held in a constant array, which registration after start cannot extend, so they now live behind a lock and are replaced rather than mutated in place. Each series reads a snapshot once, so the hooks a series ends with are the hooks it began with: 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. Hooks go live only once register returns, matching the Android and .NET ordering, so 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, and removes the duplicate construction in start and collectHooks. Co-authored-by: Cursor <cursoragent@cursor.com>
| guard !newHooks.isEmpty else { return } | ||
|
|
||
| hooksLock.lock() | ||
| defer { hooksLock.unlock() } |
Contributor
There was a problem hiding this comment.
defer is not always without cost. Do you need defer here?
Contributor
Author
There was a problem hiding this comment.
LDClient is the class and defer would be negligible compare to lock/unlock itself
Contributor
Author
There was a problem hiding this comment.
defer is considered good pattern for locking
| /// be given an "after" stage for a series whose "before" stage it was never in. | ||
| var hooks: [Hook] { | ||
| hooksLock.lock() | ||
| defer { hooksLock.unlock() } |
Contributor
There was a problem hiding this comment.
defer is not always without cost. Do you need defer here?
Contributor
Author
There was a problem hiding this comment.
LDClient is the class and defer would be negligible compare to lock/unlock itself
| public func registerPlugin(_ plugin: Plugin) { | ||
| let pluginHooks = plugin.getHooks(metadata: environmentMetadata) | ||
| plugin.register(client: self, metadata: environmentMetadata) | ||
| addHooks(pluginHooks) |
Contributor
There was a problem hiding this comment.
I think addHooks should be before register.
While hooks could be added during register, if the SDK supports runtime hook registration, this could result in a plugin missing hook invocations. This is because other plugins could call track, identify, or variation methods during registration. Getting a list of hooks before
registration ensures that the plugin cannot miss any operations.
tanderson-ld
approved these changes
Aug 25, 2026
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(_:), 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.private varbehind anNSLockand 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 anafterstage for a series whosebeforestage it was never in. The three read sites (evaluateWithHooks,executeBeforeIdentifyHooks,executeAfterTrackHooks) all take that snapshot up front.registerreturns. This matches the Android and .NET ordering, and differs from configuration-time registration where a plugin's hooks are active beforeregisteris called. So a plugin's own hooks will not observe evaluations or identify calls itsregistermakes; everything afterwards does run them.EnvironmentMetadatais 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 bothstartandcollectHooks.Registration applies to the one client it is called on, so a multi-environment setup means calling it per environment.
Note that the iOS
Pluginprotocol has noonPluginsReady, so unlike Android this path is justgetHooksthenregisterthen activate.Test plan
Five cases added to
LDClientPluginsSpec, all passing on an iPhone 16e simulator alongside the two pre-existing plugin tests (7 total):testRegisterPluginPassesClientAndEnvironmentMetadatatestRegisterPluginActivatesBundledHookstestRegisterPluginDoesNotRunTheRegisteringPluginsOwnHookstestRegisterPluginHooksRunAfterConfiguredHookstestRegisterPluginAppliesOnlyToTheClientItIsCalledOnNote
Overview
Adds
LDClient.registerPlugin(_:)so integrations can attach plugins afterstart, aligned with other LaunchDarkly mobile SDKs. Plugins still useLDConfig.pluginsat 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
letarray tostoredHooksguarded byNSLock, withaddHooksappending plugin hooks at registration. Evaluation, identify, and track paths each snapshothooksonce per series so mid-flightregisterPlugincannot pair anafterstage with abeforethe new hook never ran.environmentMetadatais stored on the client and passed toregisterandgetHooks, so late plugins get the same environment description as config-time plugins and duplicate metadata construction instart/collectHooksis removed.registerPlugincallsgetHooksand activates hooks beforePlugin.register, matching config-time ordering (including work done insideregister). Registration applies only to the client instance you call it on (multi-environment setups need per-client calls).Tests in
LDClientPluginsSpeccover metadata handoff, hook activation, ordering after config hooks, hooks duringregister, and per-environment isolation.Reviewed by Cursor Bugbot for commit 1c5db77. Bugbot is set up for automated code reviews on this repo. Configure here.