Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 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,8 @@ 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
10 changes: 10 additions & 0 deletions 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 @@ -21,5 +22,14 @@ 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>
/// IdentifySeries should run the identify series for each configured hook.
/// </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();
}
}
}
21 changes: 13 additions & 8 deletions pkgs/sdk/client/src/LdClient.cs
Copy link
Contributor

@tanderson-ld tanderson-ld Mar 11, 2026

Choose a reason for hiding this comment

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

The identify series is also supposed to be invoked in the init case I believe. Double check me here with the spec.

Original file line number Diff line number Diff line change
Expand Up @@ -902,11 +902,13 @@ public Task<bool> FlushAndWaitAsync(TimeSpan timeout) =>
/// <inheritdoc/>
public bool Identify(Context context, TimeSpan maxWaitTime)
{
return AsyncUtils.WaitSafely(() => IdentifyAsync(context), maxWaitTime);
return AsyncUtils.WaitSafely(() => IdentifyAsync(context, maxWaitTime), maxWaitTime);
}

/// <inheritdoc/>
public async Task<bool> IdentifyAsync(Context context)
public Task<bool> IdentifyAsync(Context context) => IdentifyAsync(context, TimeSpan.Zero);

internal async Task<bool> IdentifyAsync(Context context, TimeSpan maxWaitTime)
{
Context newContext = _anonymousKeyContextDecorator.DecorateContext(context);
if (_config.AutoEnvAttributes)
Expand Down Expand Up @@ -943,13 +945,16 @@ public async Task<bool> IdentifyAsync(Context context)
false // false means "don't rewrite the flags to persistent storage"
);

EventProcessorIfEnabled().RecordIdentifyEvent(new EventProcessorTypes.IdentifyEvent
return await _hookExecutor.IdentifySeries(newContext, maxWaitTime, async () =>
{
Timestamp = UnixMillisecondTime.Now,
Context = newContext
});
EventProcessorIfEnabled().RecordIdentifyEvent(new EventProcessorTypes.IdentifyEvent
{
Timestamp = UnixMillisecondTime.Now,
Context = newContext
});

return await _connectionManager.SetContext(newContext);
return await _connectionManager.SetContext(newContext);
});
}

private EnvironmentMetadata CreateEnvironmentMetadata()
Expand All @@ -970,7 +975,7 @@ private EnvironmentMetadata CreateEnvironmentMetadata()

return new EnvironmentMetadata(sdkMetadata, _config.MobileKey, CredentialType.MobileKey, applicationMetadata);
}

/// <summary>
/// Permanently shuts down the SDK client.
/// </summary>
Expand Down
Loading
Loading