From e7d27715b3172a0752a0a9a75af3d0d12f70aae9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:40:29 +0000 Subject: [PATCH] feat: Populate OpenFeature flag metadata from the evaluation reason Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- README.md | 18 +++++ .../EvaluationDetailExtensions.cs | 51 ++++++++++++- .../EvaluationDetailExtensionsTest.cs | 72 +++++++++++++++++++ 3 files changed, 139 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 47909b1..3011175 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider/EvaluationDetailExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider/EvaluationDetailExtensions.cs index 97eca6b..7fb1d0a 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider/EvaluationDetailExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider/EvaluationDetailExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using LaunchDarkly.Sdk; using OpenFeature.Constant; using OpenFeature.Model; @@ -11,6 +12,13 @@ namespace LaunchDarkly.OpenFeature.ServerProvider /// 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"; + /// /// Convert an into a string identifier. /// This string identifier is the same as we would use in a JSON representation. @@ -39,6 +47,43 @@ private static string ToIdentifier(this EvaluationReasonKind value) } } + /// + /// Convert the LaunchDarkly specific parts of an evaluation into OpenFeature flag metadata. + /// + /// The reason for the evaluation result + /// The index of the returned variation, if there was one + /// The flag metadata for the evaluation + private static ImmutableMetadata ToFlagMetadata(this EvaluationReason reason, int? variationIndex) + { + var metadata = new Dictionary(); + 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); + } + /// /// Convert an to a . /// @@ -52,7 +97,8 @@ public static ResolutionDetails ToResolutionDetails(this EvaluationDetail< if (detail.Reason.Kind != EvaluationReasonKind.Error) { return new ResolutionDetails(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; @@ -82,7 +128,8 @@ public static ResolutionDetails ToResolutionDetails(this EvaluationDetail< } return new ResolutionDetails(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)); } /// diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvaluationDetailExtensionsTest.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvaluationDetailExtensionsTest.cs index 77c03ad..970b907 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvaluationDetailExtensionsTest.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvaluationDetailExtensionsTest.cs @@ -75,6 +75,78 @@ public void ItCanHandleErrorTypes() .ToResolutionDetails("test-flag").ErrorType); } + [Fact] + public void ItIncludesTheVariationIndexInFlagMetadata() + { + var resolutionDetail = new EvaluationDetail(true, 10, EvaluationReason.FallthroughReason) + .ToResolutionDetails("test-flag"); + Assert.Equal(10, resolutionDetail.FlagMetadata.GetInt("variationIndex")); + } + + [Fact] + public void ItOmitsTheVariationIndexFromFlagMetadataWhenThereIsNoVariation() + { + var resolutionDetail = new EvaluationDetail(true, null, + EvaluationReason.ErrorReason(EvaluationErrorKind.FlagNotFound)) + .ToResolutionDetails("test-flag"); + Assert.Null(resolutionDetail.FlagMetadata.GetInt("variationIndex")); + } + + [Fact] + public void ItIncludesInExperimentInFlagMetadataForExperimentEvaluations() + { + var resolutionDetail = new EvaluationDetail(true, 10, + EvaluationReason.FallthroughReason.WithInExperiment(true)) + .ToResolutionDetails("test-flag"); + Assert.True(resolutionDetail.FlagMetadata.GetBool("inExperiment")); + } + + [Fact] + public void ItOmitsInExperimentFromFlagMetadataForNonExperimentEvaluations() + { + var resolutionDetail = new EvaluationDetail(true, 10, EvaluationReason.FallthroughReason) + .ToResolutionDetails("test-flag"); + Assert.Null(resolutionDetail.FlagMetadata.GetBool("inExperiment")); + } + + [Fact] + public void ItIncludesTheRuleInFlagMetadataForRuleMatches() + { + var resolutionDetail = new EvaluationDetail(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(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(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.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() {