Skip to content

feat: Add plugin support to Client SDK#229

Merged
abelonogov-ld merged 12 commits into
mainfrom
andrey/clientsdk-plugins-and-hooks
Mar 3, 2026
Merged

feat: Add plugin support to Client SDK#229
abelonogov-ld merged 12 commits into
mainfrom
andrey/clientsdk-plugins-and-hooks

Conversation

@abelonogov-ld

@abelonogov-ld abelonogov-ld commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds plugin architecture and hook-based flag evaluation lifecycle support to the LaunchDarkly Client SDK for .NET, ported from the Server SDK implementation.

Plugin System

  • Introduced a Plugin base class (Plugins/Plugin.cs) that provides a GetHooks() method, allowing plugins to register hooks with the SDK
  • Added PluginConfiguration and PluginConfigurationBuilder to configure plugins via ConfigurationBuilder.Plugins()
  • Updated LdClient to initialize plugins during startup, collect their hooks, and dispose of them properly during shutdown
  • Refactored LdClient cleanup logic to ensure proper disposal order of resources

Hook Framework

  • Hook base class with virtual BeforeEvaluation, AfterEvaluation, BeforeIdentify, and AfterIdentify stage methods, plus IDisposable support
  • EvaluationSeriesContext - captures flag key, context, default value, method, and environment ID for evaluation hooks
  • IdentifySeriesContext / IdentifySeriesResult - context and result types for identify hooks
  • Method constants mapping to SDK variation methods (e.g. LdClient.BoolVariation)
  • SeriesDataBuilder - fluent API for building immutable series data passed between hook stages

Hook Execution Engine

  • Executor / NoopExecutor implementing IHookExecutor - manages hook lifecycle with proper forward/reverse ordering
  • BeforeEvaluation / AfterEvaluation stage executors with error isolation - exceptions in one hook do not prevent other hooks from executing
  • Integrated hook execution into LdClient.EvaluateInternal(), wrapping flag evaluations with BeforeEvaluation and AfterEvaluation calls
  • Added GetMethodName<T>() to resolve the correct Method constant based on variation type and detail mode
  • Fixed prerequisite evaluation to use EvaluateInternal directly, avoiding recursive hook invocations
image

Test Plan

  • EvaluationSeriesTest - hooks execute in LIFO order (forward before, reverse after)
  • EvaluationSeriesTest - exceptions in hook stages don't prevent other hooks from running
  • EvaluationSeriesTest - stage failures log expected error messages with flag key and hook name
  • EvaluationSeriesTest - disposing executor disposes all registered hooks
  • PluginConfigurationBuilderTest - plugin configuration builder tests pass
  • LdClientPluginTests - end-to-end plugin/hook integration tests pass
  • All 7 evaluation series tests pass (dotnet test --filter EvaluationSeriesTest)

Note

Medium Risk
Touches core LdClient evaluation flow by wrapping all Variation* calls with hook execution and adds plugin registration at startup, so regressions could affect flag evaluation behavior/performance despite safeguards like noop execution when unconfigured.

Overview
Adds client-side plugin support by introducing PluginConfigurationBuilder/PluginConfiguration and wiring a new Plugins(...) option through Components, ConfigurationBuilder, and Configuration.

Integrates plugin lifecycle into LdClient initialization: builds plugin config, creates EnvironmentMetadata, collects plugin-provided Hooks, instantiates a hook Executor (or NoopExecutor when none), then registers plugins; hook execution is disposed with the client and logs under a new LogNames.HooksSubLog.

Introduces a hook API and execution pipeline for evaluations (Hook, EvaluationSeriesContext, stage executors) and wraps VariationInternal so hooks run before/after evaluations; prerequisite evaluations are refactored to call EvaluateInternal directly to avoid triggering hooks. Adds unit tests covering hook ordering/error isolation/disposal and plugin builder/registration behavior.

Written by Cursor Bugbot for commit e5d9dc0. This will update automatically on new commits. Configure here.

Example of API

var ldConfig = Configuration.Builder(mobileKey, LaunchDarkly.Sdk.Client.ConfigurationBuilder.AutoEnvAttributes.Enabled)
        .Plugins(new PluginConfigurationBuilder()
        	.Add(ObservabilityPlugin.Builder(new ObservabilityOptions(
        		isEnabled: true,
        		serviceName: "maui-sample-app",
				otlpEndpoint: "https://otel.observability.ld-stg.launchdarkly.com:4318/",
				backendUrl: "https://pub.observability.ld-stg.launchdarkly.com/"
        	)).Build())
        	.Add(SessionReplayPlugin.Builder(new SessionReplayOptions(
        		isEnabled: true,
        		privacy: new SessionReplayOptions.PrivacyOptions(
        			maskTextInputs: true,
        			maskWebViews: false,
        			maskLabels: false
        		)
        	)).Build())
        ).Build();
        
        

@abelonogov-ld
abelonogov-ld requested a review from a team as a code owner February 25, 2026 19:38
@abelonogov-ld abelonogov-ld changed the title feat: Plugin support for CliendSDK feat: Add plugin support to Client SDK Feb 25, 2026
Comment thread pkgs/sdk/client/src/LdClient.cs Outdated
@tanderson-ld
tanderson-ld self-requested a review February 26, 2026 17:10
@abelonogov-ld abelonogov-ld changed the title feat: Add plugin support to Client SDK WIP feat: Add plugin support to Client SDK Feb 26, 2026
Comment thread pkgs/sdk/client/src/LdClient.cs
Comment thread pkgs/sdk/client/src/LdClient.cs
@abelonogov-ld abelonogov-ld changed the title WIP feat: Add plugin support to Client SDK feat: Add plugin support to Client SDK Feb 26, 2026
Comment thread pkgs/sdk/client/src/Plugins/Plugin.cs
Comment thread pkgs/sdk/client/src/LdClient.cs Outdated
Comment thread pkgs/sdk/client/src/Plugins/Plugin.cs
Comment thread pkgs/sdk/client/src/LdClient.cs Outdated
Comment thread pkgs/sdk/client/src/LdClient.cs Outdated
Comment thread pkgs/sdk/client/src/LdClient.cs Outdated
Comment thread pkgs/sdk/client/src/Hooks/Hook.cs Outdated
Comment thread pkgs/sdk/client/src/LdClient.cs

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes 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.

Comment thread pkgs/sdk/client/src/Internal/Hooks/Executor/Executor.cs
@abelonogov-ld
abelonogov-ld merged commit ce5cdd9 into main Mar 3, 2026
14 checks passed
@abelonogov-ld
abelonogov-ld deleted the andrey/clientsdk-plugins-and-hooks branch March 3, 2026 19:26
abelonogov-ld pushed a commit that referenced this pull request Mar 3, 2026
🤖 I have created a release *beep* *boop*
---


##
[5.6.0](LaunchDarkly.ClientSdk-v5.5.4...LaunchDarkly.ClientSdk-v5.6.0)
(2026-03-03)


### Features

* Add plugin support to Client SDK
([#229](#229))
([ce5cdd9](ce5cdd9))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) is
generating a summary for commit
2c47172. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
evgenygunko pushed a commit to evgenygunko/CopyWordsDA that referenced this pull request Mar 4, 2026
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [LaunchDarkly.ClientSdk](https://github.com/launchdarkly/dotnet-core) | nuget | minor | `5.5.4` -> `5.6.0` |

---

### Release Notes

<details>
<summary>launchdarkly/dotnet-core (LaunchDarkly.ClientSdk)</summary>

### [`v5.6.0`](https://github.com/launchdarkly/dotnet-core/releases/tag/LaunchDarkly.ClientSdk-v5.6.0): LaunchDarkly.ClientSdk: v5.6.0

[Compare Source](launchdarkly/dotnet-core@LaunchDarkly.ClientSdk-v5.5.4...LaunchDarkly.ClientSdk-v5.6.0)

##### Features

-   Add plugin support to Client SDK ([#&#8203;229](launchdarkly/dotnet-core#229)) ([ce5cdd9](launchdarkly/dotnet-core@ce5cdd9))

***

This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or PR is renamed to start with "rebase!".

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
abelonogov-ld added a commit that referenced this pull request Mar 5, 2026
* main:
  chore(main): release LaunchDarkly.ServerSdk.Ai 0.9.3 (#233)
  fix: Make defaultValue optional with a disabled default (#232)
  chore(main): release LaunchDarkly.ClientSdk 5.6.0 (#231)
  feat: Add plugin support to Client SDK (#229)

# Conflicts:
#	pkgs/sdk/client/src/Internal/Hooks/Executor/Executor.cs
jsonbailey pushed a commit that referenced this pull request Jun 5, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.10.0](LaunchDarkly.ServerSdk.Ai-v0.9.4...LaunchDarkly.ServerSdk.Ai-v0.10.0)
(2026-06-05)


### ⚠ BREAKING CHANGES

* Move Role from LaunchDarkly.Sdk.Server.Ai.DataModel into
LdAiConfigTypes
* Rename LdAiConfig.ModelConfiguration to LdAiConfigTypes.ModelConfig
and LdAiConfig.ModelProvider to LdAiConfigTypes.ProviderConfig
* Move shared types nested in LdAiConfig into static LdAiConfigTypes —
Message, ModelConfiguration, and ModelProvider
* Enforce at-most-once tracking — each metric type (duration, tokens,
feedback, success/error, time-to-first-token) records once per tracker;
duplicates are dropped with a warning
* Add ResumptionToken on ILdAiConfigTracker and
ILdAiClient.CreateTracker(resumptionToken, context) for cross-process
tracker reconstruction with the original runId
* Add per-execution runId on all AI track event payloads for billing
isolation
* Change CompletionConfig to return LdAiCompletionConfig instead of
ILdAiConfigTracker — obtain a tracker via config.CreateTracker()
* Default LdAiCompletionConfigDefault.Enabled to true per AISDK spec
(was false on the LdAiConfig builder in 0.9.x)
* Remove ILdAiConfigTracker.Config property — read config fields from
the LdAiCompletionConfig the caller already holds
* Make LdAiConfigTracker SDK-constructed only — the public constructor
is removed; obtain trackers via config.CreateTracker() or
ILdAiClient.CreateTracker(resumptionToken, context)
* Convert LdAiCompletionConfig and LdAiCompletionConfigDefault from
records to classes — equality is reference-based instead of structural
* Remove LaunchDarkly.Sdk.Server.Ai.DataModel namespace — delete unused
AiConfig, Meta, Model, Provider, and Message JSON DTO classes
* Split unified LdAiConfig into LdAiCompletionConfig (SDK output) and
LdAiCompletionConfigDefault (user input) — Builder, New(), and Disabled
move to the Default type; introduce abstract LdAiConfig and
LdAiConfigDefault base types for future agent/judge modes

### Features

* Add MetricSummary property on ILdAiConfigTracker summarizing recorded
metrics
([44ff485](44ff485))
* Add per-execution runId on all AI track event payloads for billing
isolation
([44ff485](44ff485))
* Add ResumptionToken on ILdAiConfigTracker and
ILdAiClient.CreateTracker(resumptionToken, context) for cross-process
tracker reconstruction with the original runId
([44ff485](44ff485))
* Change CompletionConfig to return LdAiCompletionConfig instead of
ILdAiConfigTracker — obtain a tracker via config.CreateTracker()
([92f799f](92f799f))
* Convert LdAiCompletionConfig and LdAiCompletionConfigDefault from
records to classes — equality is reference-based instead of structural
([92f799f](92f799f))
* Default LdAiCompletionConfigDefault.Enabled to true per AISDK spec
(was false on the LdAiConfig builder in 0.9.x)
([92f799f](92f799f))
* Enforce at-most-once tracking — each metric type (duration, tokens,
feedback, success/error, time-to-first-token) records once per tracker;
duplicates are dropped with a warning
([44ff485](44ff485))
* Make LdAiConfigTracker SDK-constructed only — the public constructor
is removed; obtain trackers via config.CreateTracker() or
ILdAiClient.CreateTracker(resumptionToken, context)
([92f799f](92f799f))
* Mode-mismatch detection — log a warning and return the caller's
default when the flag's _ldMeta.mode does not match the requested mode
(per
sdk-specs[#229](#229))
([92f799f](92f799f))
* Move Role from LaunchDarkly.Sdk.Server.Ai.DataModel into
LdAiConfigTypes
([ac7fd06](ac7fd06))
* Move shared types nested in LdAiConfig into static LdAiConfigTypes —
Message, ModelConfiguration, and ModelProvider
([ac7fd06](ac7fd06))
* Non-object variation handling — log an error and return the caller's
default when the variation result is not an object
([92f799f](92f799f))
* Per-message interpolation fallback — a malformed Mustache template
keeps raw content for that message rather than discarding the entire
config
([92f799f](92f799f))
* Remove ILdAiConfigTracker.Config property — read config fields from
the LdAiCompletionConfig the caller already holds
([92f799f](92f799f))
* Remove LaunchDarkly.Sdk.Server.Ai.DataModel namespace — delete unused
AiConfig, Meta, Model, Provider, and Message JSON DTO classes
([92f799f](92f799f))
* Rename LdAiConfig.ModelConfiguration to LdAiConfigTypes.ModelConfig
and LdAiConfig.ModelProvider to LdAiConfigTypes.ProviderConfig
([ac7fd06](ac7fd06))
* Split unified LdAiConfig into LdAiCompletionConfig (SDK output) and
LdAiCompletionConfigDefault (user input) — Builder, New(), and Disabled
move to the Default type; introduce abstract LdAiConfig and
LdAiConfigDefault base types for future agent/judge modes
([92f799f](92f799f))
* Tolerant LdValue parsing — missing or wrong-typed fields degrade to
typed defaults instead of discarding the whole config
([92f799f](92f799f))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> The tagged release ships extensive breaking public API and
telemetry/billing semantics; this PR is low-risk mechanically but
merging it publishes high consumer upgrade risk.
> 
> **Overview**
> **Release Please PR** that publishes **LaunchDarkly.ServerSdk.Ai
0.10.0** by bumping `0.9.4` → `0.10.0` in
`.release-please-manifest.json`, `LaunchDarkly.ServerSdk.Ai.csproj`,
`SdkInfo.Version`, and `PROVENANCE.md`, and adding the **0.10.0**
section to `CHANGELOG.md`.
> 
> The changelog documents a **breaking** AI SDK realignment with the
AISDK spec: split `LdAiConfig` into **`LdAiCompletionConfig`**
(evaluation output) vs **`LdAiCompletionConfigDefault`** (caller input);
**`CompletionConfig` returns config, not a tracker**
(`config.CreateTracker()` / `ILdAiClient.CreateTracker(resumptionToken,
context)`); tracker construction is SDK-only with **`runId`**,
**resumption tokens**, **at-most-once metrics**, and
**`MetricSummary`**; shared types move to **`LdAiConfigTypes`** and
**`DataModel` is removed**; completion defaults default **`Enabled` to
true**; config/records become **classes** (reference equality).
Non-breaking behavior called out includes tolerant **`LdValue`
parsing**, mode-mismatch and non-object variation fallbacks, and
per-message Mustache fallback.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
82deb10. 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: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants