Skip to content

Commit

Permalink
feat: Add evaluation details to finally hook stage (#335)
Browse files Browse the repository at this point in the history
Signed-off-by: André Silva <[email protected]>
  • Loading branch information
askpt authored Jan 6, 2025
1 parent 8527b03 commit 2ef9955
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 46 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ public class MyHook : Hook
// code to run if there's an error during before hooks or during flag evaluation
}

public ValueTask FinallyAsync<T>(HookContext<T> context, IReadOnlyDictionary<string, object> hints = null)
public ValueTask FinallyAsync<T>(HookContext<T> context, FlagEvaluationDetails<T> evaluationDetails, IReadOnlyDictionary<string, object> hints = null)
{
// code to run after all other stages, regardless of success/failure
}
Expand Down
21 changes: 15 additions & 6 deletions src/OpenFeature/Hook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public abstract class Hook
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
/// <returns>Modified EvaluationContext that is used for the flag evaluation</returns>
public virtual ValueTask<EvaluationContext> BeforeAsync<T>(HookContext<T> context,
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
IReadOnlyDictionary<string, object>? hints = null,
CancellationToken cancellationToken = default)
{
return new ValueTask<EvaluationContext>(EvaluationContext.Empty);
}
Expand All @@ -44,8 +45,10 @@ public virtual ValueTask<EvaluationContext> BeforeAsync<T>(HookContext<T> contex
/// <param name="hints">Caller provided data</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
public virtual ValueTask AfterAsync<T>(HookContext<T> context, FlagEvaluationDetails<T> details,
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
public virtual ValueTask AfterAsync<T>(HookContext<T> context,
FlagEvaluationDetails<T> details,
IReadOnlyDictionary<string, object>? hints = null,
CancellationToken cancellationToken = default)
{
return new ValueTask();
}
Expand All @@ -58,8 +61,10 @@ public virtual ValueTask AfterAsync<T>(HookContext<T> context, FlagEvaluationDet
/// <param name="hints">Caller provided data</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
public virtual ValueTask ErrorAsync<T>(HookContext<T> context, Exception error,
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
public virtual ValueTask ErrorAsync<T>(HookContext<T> context,
Exception error,
IReadOnlyDictionary<string, object>? hints = null,
CancellationToken cancellationToken = default)
{
return new ValueTask();
}
Expand All @@ -68,10 +73,14 @@ public virtual ValueTask ErrorAsync<T>(HookContext<T> context, Exception error,
/// Called unconditionally after flag evaluation.
/// </summary>
/// <param name="context">Provides context of innovation</param>
/// <param name="evaluationDetails">Flag evaluation information</param>
/// <param name="hints">Caller provided data</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
public virtual ValueTask FinallyAsync<T>(HookContext<T> context, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
public virtual ValueTask FinallyAsync<T>(HookContext<T> context,
FlagEvaluationDetails<T> evaluationDetails,
IReadOnlyDictionary<string, object>? hints = null,
CancellationToken cancellationToken = default)
{
return new ValueTask();
}
Expand Down
12 changes: 7 additions & 5 deletions src/OpenFeature/OpenFeatureClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ private async Task<FlagEvaluationDetails<T>> EvaluateFlagAsync<T>(
evaluationContextBuilder.Build()
);

FlagEvaluationDetails<T> evaluation;
FlagEvaluationDetails<T>? evaluation = null;
try
{
var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -297,7 +297,9 @@ await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, exception, opti
}
finally
{
await this.TriggerFinallyHooksAsync(allHooksReversed, hookContext, options, cancellationToken).ConfigureAwait(false);
evaluation ??= new FlagEvaluationDetails<T>(flagKey, defaultValue, ErrorType.General, Reason.Error, string.Empty,
"Evaluation failed to return a result.");
await this.TriggerFinallyHooksAsync(allHooksReversed, evaluation, hookContext, options, cancellationToken).ConfigureAwait(false);
}

return evaluation;
Expand Down Expand Up @@ -351,14 +353,14 @@ private async Task TriggerErrorHooksAsync<T>(IReadOnlyList<Hook> hooks, HookCont
}
}

private async Task TriggerFinallyHooksAsync<T>(IReadOnlyList<Hook> hooks, HookContext<T> context,
FlagEvaluationOptions? options, CancellationToken cancellationToken = default)
private async Task TriggerFinallyHooksAsync<T>(IReadOnlyList<Hook> hooks, FlagEvaluationDetails<T> evaluation,
HookContext<T> context, FlagEvaluationOptions? options, CancellationToken cancellationToken = default)
{
foreach (var hook in hooks)
{
try
{
await hook.FinallyAsync(context, options?.HookHints, cancellationToken).ConfigureAwait(false);
await hook.FinallyAsync(context, evaluation, options?.HookHints, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
Expand Down
20 changes: 20 additions & 0 deletions test/OpenFeature.Tests/OpenFeatureClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -656,5 +656,25 @@ public async Task TheClient_MergesTheEvaluationContextInTheCorrectOrder(string k

Assert.Equal(expectedResult, actualEvaluationContext.GetValue(key).AsString);
}

[Fact]
[Specification("4.3.8", "'evaluation details' passed to the 'finally' stage matches the evaluation details returned to the application author")]
public async Task FinallyHook_IncludesEvaluationDetails()
{
// Arrange
var provider = new TestProvider();
var providerHook = Substitute.For<Hook>();
provider.AddHook(providerHook);
await Api.Instance.SetProviderAsync(provider);
var client = Api.Instance.GetClient();

const string flagName = "flagName";

// Act
var evaluationDetails = await client.GetBooleanDetailsAsync(flagName, true);

// Assert
await providerHook.Received(1).FinallyAsync(Arg.Any<HookContext<bool>>(), evaluationDetails);
}
}
}
Loading

0 comments on commit 2ef9955

Please sign in to comment.