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..876e36a 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; @@ -13,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; @@ -63,6 +71,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(VARIATION_INDEX_KEY, variationIndex); + } + if (reason.isInExperiment()) { + builder.addBoolean(IN_EXPERIMENT_KEY, true); + } + if (reason.getKind() == EvaluationReason.Kind.RULE_MATCH) { + builder.addInteger(RULE_INDEX_KEY, reason.getRuleIndex()); + if (reason.getRuleId() != null) { + builder.addString(RULE_ID_KEY, reason.getRuleId()); + } + } + if (reason.getPrerequisiteKey() != null) { + builder.addString(PREREQUISITE_KEY_KEY, reason.getPrerequisiteKey()); + } + if (reason.getBigSegmentsStatus() != null) { + builder.addString(BIG_SEGMENTS_STATUS_KEY, 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")); + } }