Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
da37078
Plugin support for CliendSDK
abelonogov-ld Feb 25, 2026
7aee7be
Plugin tests
abelonogov-ld Feb 25, 2026
f4a903b
defensive check
abelonogov-ld Feb 25, 2026
6849630
Refactor LdClient cleanup logic to ensure proper disposal order of re…
abelonogov-ld Feb 25, 2026
efe371c
Merge branch 'main' into andrey/clientsdk-plugins-and-hooks
abelonogov-ld Feb 25, 2026
ebd9c5c
Flag Evaluations hook
abelonogov-ld Feb 26, 2026
5c8ddd4
Refactor plugin registration logic in LdClient to ensure hooks are av…
abelonogov-ld Feb 26, 2026
e5d9dc0
remove unnecessary parameter
abelonogov-ld Feb 26, 2026
55a0950
remove identify
abelonogov-ld Mar 3, 2026
17e0c56
fix comment
abelonogov-ld Mar 3, 2026
88078c3
remove files
abelonogov-ld Mar 3, 2026
b08463a
address feadback
abelonogov-ld Mar 3, 2026
f1f4046
Merge branch 'andrey/clientsdk-plugins-and-hooks' into andrey/clients…
abelonogov-ld Mar 3, 2026
6311201
Merge branch 'main' into andrey/client-identify-hook
abelonogov-ld Mar 5, 2026
451e890
Update IdentifyAsync method to include maxWaitTime parameter
abelonogov-ld Mar 10, 2026
493f598
Identify in Hook interface
abelonogov-ld Mar 10, 2026
61629a6
Fix IdentifyAsync method signature in LdClient documentation and impl…
abelonogov-ld Mar 10, 2026
c4bcf6f
make unneeded parameter internal
abelonogov-ld Mar 10, 2026
283e9d2
Refactor IdentifySeriesContext to use TimeSpan for timeout
abelonogov-ld Mar 10, 2026
fe9d524
IdentifyWithHook
abelonogov-ld Mar 12, 2026
551c275
return false
abelonogov-ld Mar 12, 2026
0430db3
Use large wrapper
abelonogov-ld Mar 13, 2026
167d6c8
noopexecetor
abelonogov-ld Mar 13, 2026
11b9692
let's throw
abelonogov-ld Mar 13, 2026
b881e20
calling start after
abelonogov-ld Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions pkgs/sdk/client/src/Hooks/Hook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,35 @@ public virtual SeriesData BeforeEvaluation(EvaluationSeriesContext context, Seri
public virtual SeriesData AfterEvaluation(EvaluationSeriesContext context, SeriesData data,
EvaluationDetail<LdValue> detail) => data;


/// <summary>
/// BeforeIdentify is executed by the SDK before an identify operation.
///
/// The modified data is not shared with any other hook. It will be passed to subsequent stages in the identify
/// series, including <see cref="AfterIdentify"/>.
///
/// </summary>
/// <param name="context">parameters associated with this identify operation</param>
/// <param name="data">user-configurable data, currently empty</param>
/// <returns>user-configurable data, which will be forwarded to <see cref="AfterIdentify"/></returns>
public virtual SeriesData BeforeIdentify(IdentifySeriesContext context, SeriesData data) =>
data;


/// <summary>
/// AfterIdentify is executed by the SDK after an identify operation.
///
/// The function should return the given <see cref="SeriesData"/> unmodified, for forward compatibility with subsequent
/// stages that may be added.
///
/// </summary>
/// <param name="context">parameters associated with this identify operation</param>
/// <param name="data">user-configurable data from the <see cref="BeforeIdentify"/> stage</param>
/// <param name="result">the result of the identify operation</param>
/// <returns>user-configurable data, which is currently unused but may be forwarded to subsequent stages in future versions of the SDK</returns>
public virtual SeriesData AfterIdentify(IdentifySeriesContext context, SeriesData data,
IdentifySeriesResult result) => data;

/// <summary>
/// Constructs a new Hook with the given name. The name may be used in log messages.
/// </summary>
Expand Down
33 changes: 33 additions & 0 deletions pkgs/sdk/client/src/Hooks/IdentifySeriesContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;

namespace LaunchDarkly.Sdk.Client.Hooks
{
/// <summary>
/// IdentifySeriesContext represents parameters associated with an identify operation. It is
/// made available in <see cref="Hook"/> stage callbacks.
/// </summary>
public sealed class IdentifySeriesContext
{
/// <summary>
/// The Context being identified.
/// </summary>
public Context Context { get; }

/// <summary>
/// The timeout for the identify operation. A value of <see cref="TimeSpan.Zero"/> indicates
/// that no timeout was specified.
/// </summary>
public TimeSpan Timeout { get; }

/// <summary>
/// Constructs a new IdentifySeriesContext.
/// </summary>
/// <param name="context">the context being identified</param>
/// <param name="timeout">the timeout for the identify operation</param>
public IdentifySeriesContext(Context context, TimeSpan timeout)
{
Context = context;
Timeout = timeout;
}
}
}
38 changes: 38 additions & 0 deletions pkgs/sdk/client/src/Hooks/IdentifySeriesResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace LaunchDarkly.Sdk.Client.Hooks
{
/// <summary>
/// IdentifySeriesResult contains the outcome of an identify operation.
/// </summary>
public sealed class IdentifySeriesResult
{
/// <summary>
/// Represents the possible statuses of an identify operation.
/// </summary>
public enum IdentifySeriesStatus
{
/// <summary>
/// The identify operation completed successfully.
/// </summary>
Completed,

/// <summary>
/// The identify operation encountered an error.
/// </summary>
Error
}

/// <summary>
/// The status of the identify operation.
/// </summary>
public IdentifySeriesStatus Status { get; }

/// <summary>
/// Constructs a new IdentifySeriesResult.
/// </summary>
/// <param name="status">the status of the identify operation</param>
public IdentifySeriesResult(IdentifySeriesStatus status)
{
Status = status;
}
}
}
2 changes: 1 addition & 1 deletion pkgs/sdk/client/src/ILdClientExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;
using LaunchDarkly.Sdk.Client.Interfaces;

Expand Down
2 changes: 1 addition & 1 deletion pkgs/sdk/client/src/Interfaces/IFlagTracker.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;

namespace LaunchDarkly.Sdk.Client.Interfaces
{
Expand Down
2 changes: 1 addition & 1 deletion pkgs/sdk/client/src/Interfaces/ILdClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LaunchDarkly.Sdk.Client.Integrations;
Expand Down
31 changes: 31 additions & 0 deletions pkgs/sdk/client/src/Internal/Hooks/Executor/Executor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LaunchDarkly.Logging;
using LaunchDarkly.Sdk.Client.Hooks;
using LaunchDarkly.Sdk.Client.Internal.Hooks.Interfaces;
Expand All @@ -16,12 +17,17 @@ internal sealed class Executor : IHookExecutor
private readonly IStageExecutor<EvaluationSeriesContext> _beforeEvaluation;
private readonly IStageExecutor<EvaluationSeriesContext, EvaluationDetail<LdValue>> _afterEvaluation;

private readonly IStageExecutor<IdentifySeriesContext> _beforeIdentify;
private readonly IStageExecutor<IdentifySeriesContext, IdentifySeriesResult> _afterIdentify;

public Executor(Logger logger, IEnumerable<Hook> hooks)
{
_logger = logger;
_hooks = hooks.ToList();
_beforeEvaluation = new BeforeEvaluation(logger, _hooks, EvaluationStage.Order.Forward);
_afterEvaluation = new AfterEvaluation(logger, _hooks, EvaluationStage.Order.Reverse);
_beforeIdentify = new BeforeIdentify(logger, _hooks, EvaluationStage.Order.Forward);
_afterIdentify = new AfterIdentify(logger, _hooks, EvaluationStage.Order.Reverse);
}

public EvaluationDetail<T> EvaluationSeries<T>(EvaluationSeriesContext context,
Expand All @@ -38,6 +44,31 @@ public EvaluationDetail<T> EvaluationSeries<T>(EvaluationSeriesContext context,
return detail;
}

public async Task<bool> IdentifySeries(Context context, TimeSpan maxWaitTime, Func<Task<bool>> identify)
{
var identifyContext = new IdentifySeriesContext(context, maxWaitTime);
var seriesData = _beforeIdentify.Execute(identifyContext, default);

try
{
var result = await identify();

_afterIdentify.Execute(identifyContext,
new IdentifySeriesResult(IdentifySeriesResult.IdentifySeriesStatus.Completed),
seriesData);

return result;
}
catch (Exception)
{
_afterIdentify.Execute(identifyContext,
new IdentifySeriesResult(IdentifySeriesResult.IdentifySeriesStatus.Error),
seriesData);

throw;
}
}

public void Dispose()
{
foreach (var hook in _hooks)
Expand Down
4 changes: 4 additions & 0 deletions pkgs/sdk/client/src/Internal/Hooks/Executor/NoopExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using LaunchDarkly.Sdk.Client.Hooks;
using LaunchDarkly.Sdk.Client.Internal.Hooks.Interfaces;

Expand All @@ -13,6 +14,9 @@ internal sealed class NoopExecutor : IHookExecutor
public EvaluationDetail<T> EvaluationSeries<T>(EvaluationSeriesContext context,
LdValue.Converter<T> converter, Func<EvaluationDetail<T>> evaluate) => evaluate();

public Task<bool> IdentifySeries(Context context, TimeSpan maxWaitTime, Func<Task<bool>> identify) =>
identify();

public void Dispose()
{
}
Expand Down
15 changes: 14 additions & 1 deletion pkgs/sdk/client/src/Internal/Hooks/Interfaces/IHookExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using LaunchDarkly.Sdk.Client.Hooks;

namespace LaunchDarkly.Sdk.Client.Internal.Hooks.Interfaces
Expand All @@ -12,7 +13,8 @@ namespace LaunchDarkly.Sdk.Client.Internal.Hooks.Interfaces
internal interface IHookExecutor : IDisposable
{
/// <summary>
/// EvaluationSeries should run the evaluation series for each configured hook.
/// Runs the evaluation series for each configured hook, invoking the <paramref name="evaluate"/>
/// delegate to obtain the flag value. Exceptions thrown by the delegate are propagated to the caller.
/// </summary>
/// <param name="context">context for the evaluation series</param>
/// <param name="converter">used to convert the primitive evaluation value into a wrapped <see cref="LdValue"/> suitable for use in hooks</param>
Expand All @@ -21,5 +23,16 @@ internal interface IHookExecutor : IDisposable
/// <returns>the EvaluationDetail returned from the evaluator</returns>
EvaluationDetail<T> EvaluationSeries<T>(EvaluationSeriesContext context,
LdValue.Converter<T> converter, Func<EvaluationDetail<T>> evaluate);

/// <summary>
/// Runs the identify series for each configured hook, invoking the <paramref name="identify"/>
/// delegate to perform the identify operation. Exceptions thrown by the delegate are propagated
/// to the caller.
/// </summary>
/// <param name="context">the evaluation context being identified</param>
/// <param name="maxWaitTime">the timeout for the identify operation</param>
/// <param name="identify">async function that performs the identify operation</param>
/// <returns>the result of the identify operation</returns>
Task<bool> IdentifySeries(Context context, TimeSpan maxWaitTime, Func<Task<bool>> identify);
}
}
89 changes: 89 additions & 0 deletions pkgs/sdk/client/src/Internal/Hooks/Series/IdentifySeries.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using LaunchDarkly.Logging;
using LaunchDarkly.Sdk.Client.Hooks;
using LaunchDarkly.Sdk.Client.Internal.Hooks.Interfaces;

namespace LaunchDarkly.Sdk.Client.Internal.Hooks.Series
{
using SeriesData = ImmutableDictionary<string, object>;

internal class IdentifyStage
{
protected enum Stage
{
BeforeIdentify,
AfterIdentify
}

protected readonly EvaluationStage.Order _order;
private readonly Logger _logger;

protected IdentifyStage(Logger logger, EvaluationStage.Order order)
{
_logger = logger;
_order = order;
}

protected void LogFailure(IdentifySeriesContext context, Hook h, Stage stage, Exception e)
{
_logger.Error("During identify of context \"{0}\", stage \"{1}\" of hook \"{2}\" reported error: {3}",
context.Context.Key, stage.ToString(), h.Metadata.Name, e.Message);
}
}

internal sealed class BeforeIdentify : IdentifyStage, IStageExecutor<IdentifySeriesContext>
{
private readonly IEnumerable<Hook> _hooks;

public BeforeIdentify(Logger logger, IEnumerable<Hook> hooks, EvaluationStage.Order order) : base(logger, order)
{
_hooks = (order == EvaluationStage.Order.Forward) ? hooks : hooks.Reverse();
}

public IEnumerable<SeriesData> Execute(IdentifySeriesContext context, IEnumerable<SeriesData> _)
{
return _hooks.Select(hook =>
{
try
{
return hook.BeforeIdentify(context, SeriesData.Empty);
}
catch (Exception e)
{
LogFailure(context, hook, Stage.BeforeIdentify, e);
return SeriesData.Empty;
}
}).ToList();
}
}

internal sealed class AfterIdentify : IdentifyStage, IStageExecutor<IdentifySeriesContext, IdentifySeriesResult>
{
private readonly IEnumerable<Hook> _hooks;

public AfterIdentify(Logger logger, IEnumerable<Hook> hooks, EvaluationStage.Order order) : base(logger, order)
{
_hooks = (order == EvaluationStage.Order.Forward) ? hooks : hooks.Reverse();
}

public IEnumerable<SeriesData> Execute(IdentifySeriesContext context, IdentifySeriesResult result,
IEnumerable<SeriesData> seriesData)
{
return _hooks.Zip((_order == EvaluationStage.Order.Reverse ? seriesData.Reverse() : seriesData), (hook, data) =>
{
try
{
return hook.AfterIdentify(context, data, result);
}
catch (Exception e)
{
LogFailure(context, hook, Stage.AfterIdentify, e);
return SeriesData.Empty;
}
}).ToList();
}
}
}
Loading
Loading