From 63ea2812f7e4c7151efda0e1c49e1c7f5376d365 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:30:24 +0000 Subject: [PATCH 1/2] feat: Populate OpenFeature flag metadata from the evaluation reason Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- README.md | 14 ++++ .../EvaluationDetailConverter.java | 26 +++++++ .../EvaluationDetailConverterTest.java | 75 +++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/README.md b/README.md index a115fea..dc10c94 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,20 @@ API can be used to wait until the provider is ready, or it has encountered a per It the provider has been shutdown, because the OpenFeature API has been shutdown, or because the provider was no longer in use by the OpenFeature API, then the underlying LaunchDarkly SDK will be closed. This is an important consideration if you are using the `getLdClient` method of the provider to access the underlying SDK instance. +### Flag Metadata + +Evaluation details include flag metadata containing the parts of the LaunchDarkly evaluation reason that do not fit +into the OpenFeature reason and error code. Each entry is only present when it applies to the evaluation. + +| Key | Type | Description | +|-----|------|-------------| +| `variationIndex` | integer | The index of the variation that was returned. Absent when the SDK returned the default value. | +| `inExperiment` | boolean | Only present, and always `true`, when the evaluation was part of an experiment. | +| `ruleIndex` | integer | The index of the targeting rule that matched. Only present for a `RULE_MATCH` reason. | +| `ruleId` | string | The identifier of the targeting rule that matched. Only present for a `RULE_MATCH` reason. | +| `prerequisiteKey` | string | The key of the prerequisite flag that failed. Only present for a `PREREQUISITE_FAILED` reason. | +| `bigSegmentsStatus` | string | The status of the Big Segments store, when the evaluation used Big Segments. | + ### Examples #### A single user context diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java index 6fc7d47..4584387 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java @@ -5,6 +5,7 @@ import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDValue; import dev.openfeature.sdk.ErrorCode; +import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.Reason; import dev.openfeature.sdk.Value; @@ -63,6 +64,31 @@ private static ProviderEvaluation getProviderEvaluation(T value, Evaluati if (!isDefault) { builder.variant(String.valueOf(variationIndex)); } + builder.flagMetadata(FlagMetadata(reason, isDefault, variationIndex)); + + return builder.build(); + } + + private static ImmutableMetadata FlagMetadata(EvaluationReason reason, boolean isDefault, int variationIndex) { + var builder = ImmutableMetadata.builder(); + if (!isDefault) { + builder.addInteger("variationIndex", variationIndex); + } + if (reason.isInExperiment()) { + builder.addBoolean("inExperiment", true); + } + if (reason.getKind() == EvaluationReason.Kind.RULE_MATCH) { + builder.addInteger("ruleIndex", reason.getRuleIndex()); + if (reason.getRuleId() != null) { + builder.addString("ruleId", reason.getRuleId()); + } + } + if (reason.getPrerequisiteKey() != null) { + builder.addString("prerequisiteKey", reason.getPrerequisiteKey()); + } + if (reason.getBigSegmentsStatus() != null) { + builder.addString("bigSegmentsStatus", reason.getBigSegmentsStatus().name()); + } return builder.build(); } diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java index 8cb0c3b..e719d2b 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java @@ -155,4 +155,79 @@ public void itCanHandleAnObjectResult() { assertEquals(17.0, nestedList.get(0).asDouble(), EPSILON); } + + @Test + public void itIncludesTheVariationIndexInFlagMetadata() { + EvaluationDetail detail = EvaluationDetail.fromValue( + true, 3, EvaluationReason.fallthrough()); + + ImmutableMetadata metadata = evaluationDetailConverter.toEvaluationDetails(detail).getFlagMetadata(); + + assertEquals(3, metadata.getInteger("variationIndex")); + assertNull(metadata.getBoolean("inExperiment")); + } + + @Test + public void itOmitsTheVariationIndexForDefaultValues() { + EvaluationDetail detail = EvaluationDetail.fromValue( + true, EvaluationDetail.NO_VARIATION, + EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)); + + ImmutableMetadata metadata = evaluationDetailConverter.toEvaluationDetails(detail).getFlagMetadata(); + + assertNull(metadata.getInteger("variationIndex")); + } + + @Test + public void itIncludesExperimentationInformationInFlagMetadata() { + EvaluationDetail detail = EvaluationDetail.fromValue( + true, 1, EvaluationReason.fallthrough(true)); + + ImmutableMetadata metadata = evaluationDetailConverter.toEvaluationDetails(detail).getFlagMetadata(); + + assertEquals(true, metadata.getBoolean("inExperiment")); + } + + @Test + public void itIncludesRuleInformationInFlagMetadata() { + EvaluationDetail detail = EvaluationDetail.fromValue( + true, 1, EvaluationReason.ruleMatch(4, "the-rule-id")); + + ImmutableMetadata metadata = evaluationDetailConverter.toEvaluationDetails(detail).getFlagMetadata(); + + assertEquals(4, metadata.getInteger("ruleIndex")); + assertEquals("the-rule-id", metadata.getString("ruleId")); + } + + @Test + public void itIncludesThePrerequisiteKeyInFlagMetadata() { + EvaluationDetail detail = EvaluationDetail.fromValue( + true, 0, EvaluationReason.prerequisiteFailed("the-prerequisite-key")); + + ImmutableMetadata metadata = evaluationDetailConverter.toEvaluationDetails(detail).getFlagMetadata(); + + assertEquals("the-prerequisite-key", metadata.getString("prerequisiteKey")); + } + + @Test + public void itIncludesTheBigSegmentsStatusInFlagMetadata() { + EvaluationDetail detail = EvaluationDetail.fromValue( + true, 0, EvaluationReason.fallthrough() + .withBigSegmentsStatus(EvaluationReason.BigSegmentsStatus.STALE)); + + ImmutableMetadata metadata = evaluationDetailConverter.toEvaluationDetails(detail).getFlagMetadata(); + + assertEquals("STALE", metadata.getString("bigSegmentsStatus")); + } + + @Test + public void itIncludesFlagMetadataForObjectResults() { + EvaluationDetail detail = EvaluationDetail.fromValue( + new ObjectBuilder().put("aKey", "aValue").build(), 2, EvaluationReason.fallthrough(true)); + + ImmutableMetadata metadata = evaluationDetailConverter.toEvaluationDetailsLdValue(detail).getFlagMetadata(); + + assertEquals(2, metadata.getInteger("variationIndex")); + assertEquals(true, metadata.getBoolean("inExperiment")); + } } From c93fd2c7aff8cec0d94c0b1cc874715be4fa9faa 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:35:45 +0000 Subject: [PATCH 2/2] refactor: Use constants for flag metadata keys Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../EvaluationDetailConverter.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java index 4584387..876e36a 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java @@ -14,6 +14,13 @@ * Converts an EvaluationDetail into an OpenFeature ResolutionDetails. */ class EvaluationDetailConverter { + private static final String VARIATION_INDEX_KEY = "variationIndex"; + private static final String IN_EXPERIMENT_KEY = "inExperiment"; + private static final String RULE_INDEX_KEY = "ruleIndex"; + private static final String RULE_ID_KEY = "ruleId"; + private static final String PREREQUISITE_KEY_KEY = "prerequisiteKey"; + private static final String BIG_SEGMENTS_STATUS_KEY = "bigSegmentsStatus"; + LDLogger logger; LDValueConverter ldValueConverter; @@ -72,22 +79,22 @@ private static ProviderEvaluation getProviderEvaluation(T value, Evaluati private static ImmutableMetadata FlagMetadata(EvaluationReason reason, boolean isDefault, int variationIndex) { var builder = ImmutableMetadata.builder(); if (!isDefault) { - builder.addInteger("variationIndex", variationIndex); + builder.addInteger(VARIATION_INDEX_KEY, variationIndex); } if (reason.isInExperiment()) { - builder.addBoolean("inExperiment", true); + builder.addBoolean(IN_EXPERIMENT_KEY, true); } if (reason.getKind() == EvaluationReason.Kind.RULE_MATCH) { - builder.addInteger("ruleIndex", reason.getRuleIndex()); + builder.addInteger(RULE_INDEX_KEY, reason.getRuleIndex()); if (reason.getRuleId() != null) { - builder.addString("ruleId", reason.getRuleId()); + builder.addString(RULE_ID_KEY, reason.getRuleId()); } } if (reason.getPrerequisiteKey() != null) { - builder.addString("prerequisiteKey", reason.getPrerequisiteKey()); + builder.addString(PREREQUISITE_KEY_KEY, reason.getPrerequisiteKey()); } if (reason.getBigSegmentsStatus() != null) { - builder.addString("bigSegmentsStatus", reason.getBigSegmentsStatus().name()); + builder.addString(BIG_SEGMENTS_STATUS_KEY, reason.getBigSegmentsStatus().name()); } return builder.build();