Skip to content
Draft
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,24 @@ var evaluationContext = EvaluationContext.Builder()
.Build();
```

### Flag Metadata

Evaluations include flag metadata for the LaunchDarkly specific parts of the evaluation result which have no OpenFeature equivalent. Each entry is absent when it does not apply to the evaluation.

| Key | Type | Description |
|---------------------|---------|------------------------------------------------------------------------|
| `variationIndex` | integer | The index of the returned variation. Absent for default values. |
| `inExperiment` | boolean | Present, and `true`, when the evaluation was part of an experiment. |
| `ruleIndex` | integer | The index of the rule that matched. |
| `ruleId` | string | The identifier of the rule that matched. |
| `prerequisiteKey` | string | The key of the prerequisite flag that failed. |
| `bigSegmentsStatus` | string | The status of the Big Segments query made during the evaluation. |

```csharp
var details = await client.GetBooleanDetailsAsync("my-flag", false, evaluationContext);
var inExperiment = details.FlagMetadata.GetBool("inExperiment") ?? false;
```

### Advanced Usage

#### Asynchronous Initialization
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using LaunchDarkly.Sdk;
using OpenFeature.Constant;
using OpenFeature.Model;
Expand All @@ -11,6 +12,13 @@ namespace LaunchDarkly.OpenFeature.ServerProvider
/// </summary>
internal static class EvaluationDetailExtensions
{
private const string VariationIndexKey = "variationIndex";
private const string InExperimentKey = "inExperiment";
private const string RuleIndexKey = "ruleIndex";
private const string RuleIdKey = "ruleId";
private const string PrerequisiteKeyKey = "prerequisiteKey";
private const string BigSegmentsStatusKey = "bigSegmentsStatus";

/// <summary>
/// Convert an <see cref="EvaluationReasonKind"/> into a string identifier.
/// This string identifier is the same as we would use in a JSON representation.
Expand Down Expand Up @@ -39,6 +47,43 @@ private static string ToIdentifier(this EvaluationReasonKind value)
}
}

/// <summary>
/// Convert the LaunchDarkly specific parts of an evaluation into OpenFeature flag metadata.
/// </summary>
/// <param name="reason">The reason for the evaluation result</param>
/// <param name="variationIndex">The index of the returned variation, if there was one</param>
/// <returns>The flag metadata for the evaluation</returns>
private static ImmutableMetadata ToFlagMetadata(this EvaluationReason reason, int? variationIndex)
{
var metadata = new Dictionary<string, object>();
if (variationIndex.HasValue)
{
metadata[VariationIndexKey] = variationIndex.Value;
}
if (reason.InExperiment)
{
metadata[InExperimentKey] = true;
}
if (reason.RuleIndex.HasValue)
{
metadata[RuleIndexKey] = reason.RuleIndex.Value;
}
if (reason.RuleId != null)
{
metadata[RuleIdKey] = reason.RuleId;
}
if (reason.PrerequisiteKey != null)
{
metadata[PrerequisiteKeyKey] = reason.PrerequisiteKey;
}
if (reason.BigSegmentsStatus.HasValue)
{
metadata[BigSegmentsStatusKey] = reason.BigSegmentsStatus.Value.ToString();
}

return new ImmutableMetadata(metadata);
}

/// <summary>
/// Convert an <see cref="EvaluationDetail{T}"/> to a <see cref="ResolutionDetails{T}"/>.
/// </summary>
Expand All @@ -52,7 +97,8 @@ public static ResolutionDetails<T> ToResolutionDetails<T>(this EvaluationDetail<
if (detail.Reason.Kind != EvaluationReasonKind.Error)
{
return new ResolutionDetails<T>(flagKey, detail.Value, ErrorType.None,
reason: detail.Reason.Kind.ToIdentifier(), variant: detail.VariationIndex?.ToString());
reason: detail.Reason.Kind.ToIdentifier(), variant: detail.VariationIndex?.ToString(),
flagMetadata: detail.Reason.ToFlagMetadata(detail.VariationIndex));
}

var errorType = ErrorType.General;
Expand Down Expand Up @@ -82,7 +128,8 @@ public static ResolutionDetails<T> ToResolutionDetails<T>(this EvaluationDetail<
}

return new ResolutionDetails<T>(flagKey, detail.Value, errorType,
reason: detail.Reason.Kind.ToIdentifier(), variant: detail.VariationIndex?.ToString());
reason: detail.Reason.Kind.ToIdentifier(), variant: detail.VariationIndex?.ToString(),
flagMetadata: detail.Reason.ToFlagMetadata(detail.VariationIndex));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,78 @@ public void ItCanHandleErrorTypes()
.ToResolutionDetails("test-flag").ErrorType);
}

[Fact]
public void ItIncludesTheVariationIndexInFlagMetadata()
{
var resolutionDetail = new EvaluationDetail<bool>(true, 10, EvaluationReason.FallthroughReason)
.ToResolutionDetails("test-flag");
Assert.Equal(10, resolutionDetail.FlagMetadata.GetInt("variationIndex"));
}

[Fact]
public void ItOmitsTheVariationIndexFromFlagMetadataWhenThereIsNoVariation()
{
var resolutionDetail = new EvaluationDetail<bool>(true, null,
EvaluationReason.ErrorReason(EvaluationErrorKind.FlagNotFound))
.ToResolutionDetails("test-flag");
Assert.Null(resolutionDetail.FlagMetadata.GetInt("variationIndex"));
}

[Fact]
public void ItIncludesInExperimentInFlagMetadataForExperimentEvaluations()
{
var resolutionDetail = new EvaluationDetail<bool>(true, 10,
EvaluationReason.FallthroughReason.WithInExperiment(true))
.ToResolutionDetails("test-flag");
Assert.True(resolutionDetail.FlagMetadata.GetBool("inExperiment"));
}

[Fact]
public void ItOmitsInExperimentFromFlagMetadataForNonExperimentEvaluations()
{
var resolutionDetail = new EvaluationDetail<bool>(true, 10, EvaluationReason.FallthroughReason)
.ToResolutionDetails("test-flag");
Assert.Null(resolutionDetail.FlagMetadata.GetBool("inExperiment"));
}

[Fact]
public void ItIncludesTheRuleInFlagMetadataForRuleMatches()
{
var resolutionDetail = new EvaluationDetail<bool>(true, 10,
EvaluationReason.RuleMatchReason(2, "the-rule-id"))
.ToResolutionDetails("test-flag");
Assert.Equal(2, resolutionDetail.FlagMetadata.GetInt("ruleIndex"));
Assert.Equal("the-rule-id", resolutionDetail.FlagMetadata.GetString("ruleId"));
}

[Fact]
public void ItIncludesThePrerequisiteKeyInFlagMetadataForFailedPrerequisites()
{
var resolutionDetail = new EvaluationDetail<bool>(true, 10,
EvaluationReason.PrerequisiteFailedReason("the-prerequisite-key"))
.ToResolutionDetails("test-flag");
Assert.Equal("the-prerequisite-key", resolutionDetail.FlagMetadata.GetString("prerequisiteKey"));
}

[Fact]
public void ItIncludesTheBigSegmentsStatusInFlagMetadata()
{
var resolutionDetail = new EvaluationDetail<bool>(true, 10,
EvaluationReason.FallthroughReason.WithBigSegmentsStatus(BigSegmentsStatus.Stale))
.ToResolutionDetails("test-flag");
Assert.Equal("Stale", resolutionDetail.FlagMetadata.GetString("bigSegmentsStatus"));
}

[Fact]
public void ItIncludesFlagMetadataForStructureEvaluations()
{
var detail = new EvaluationDetail<LdValue>(LdValue.Of(true), 10,
EvaluationReason.RuleMatchReason(2, "the-rule-id"));
var resolutionDetail = detail.ToValueDetail(new Value(false)).ToResolutionDetails("test-flag");
Assert.Equal(10, resolutionDetail.FlagMetadata.GetInt("variationIndex"));
Assert.Equal("the-rule-id", resolutionDetail.FlagMetadata.GetString("ruleId"));
}

[Fact]
public void ItCanHandleDefaultValueDetail()
{
Expand Down
Loading