diff --git a/src/main/java/cloud/eppo/BaseEppoClient.java b/src/main/java/cloud/eppo/BaseEppoClient.java index 92bd89a..08533a6 100644 --- a/src/main/java/cloud/eppo/BaseEppoClient.java +++ b/src/main/java/cloud/eppo/BaseEppoClient.java @@ -182,37 +182,83 @@ protected CompletableFuture loadConfigurationAsync() { return future; } - protected EppoValue getTypedAssignment( + /** + * Top-level assignment details method that evaluates, logs if applicable, and returns the + * user-facing AssignmentDetails result class. If any error in the evaluation, the result value + * will be set to the default value. + */ + protected AssignmentDetails getTypedAssignmentWithDetails( String flagKey, String subjectKey, Attributes subjectAttributes, - EppoValue defaultValue, + T defaultValue, VariationType expectedType) { + EvaluationDetails details = + evaluateAndMaybeLog(flagKey, subjectKey, subjectAttributes, expectedType); + + T resultValue = + details.evaluationSuccessful() + ? details.getVariationValue().unwrap(expectedType) + : defaultValue; + return new AssignmentDetails<>(resultValue, null, details); + } + + /** + * Core evaluation method that handles validation, evaluation, and logging. This consolidates the + * shared logic between all assignment methods. Returns evaluation details with variationValue set + * to the result. + */ + protected EvaluationDetails evaluateAndMaybeLog( + String flagKey, String subjectKey, Attributes subjectAttributes, VariationType expectedType) { + throwIfEmptyOrNull(flagKey, "flagKey must not be empty"); throwIfEmptyOrNull(subjectKey, "subjectKey must not be empty"); Configuration config = getConfiguration(); + // Check if flag exists FlagConfig flag = config.getFlag(flagKey); if (flag == null) { log.warn("no configuration found for key: {}", flagKey); - return defaultValue; + return EvaluationDetails.buildDefault( + config.getEnvironmentName(), + config.getConfigFetchedAt(), + config.getConfigPublishedAt(), + FlagEvaluationCode.FLAG_UNRECOGNIZED_OR_DISABLED, + "Unrecognized or disabled flag: " + flagKey, + null); } + // Check if flag is enabled if (!flag.isEnabled()) { log.info( "no assigned variation because the experiment or feature flag is disabled: {}", flagKey); - return defaultValue; + return EvaluationDetails.buildDefault( + config.getEnvironmentName(), + config.getConfigFetchedAt(), + config.getConfigPublishedAt(), + FlagEvaluationCode.FLAG_UNRECOGNIZED_OR_DISABLED, + "Unrecognized or disabled flag: " + flagKey, + null); } + // Check if flag type matches expected type if (flag.getVariationType() != expectedType) { log.warn( "no assigned variation because the flag type doesn't match the requested type: {} has type {}, requested {}", flagKey, flag.getVariationType(), expectedType); - return defaultValue; + return EvaluationDetails.buildDefault( + config.getEnvironmentName(), + config.getConfigFetchedAt(), + config.getConfigPublishedAt(), + FlagEvaluationCode.TYPE_MISMATCH, + String.format( + "Flag \"%s\" has type %s, requested %s", + flagKey, flag.getVariationType(), expectedType), + null); } // Evaluate flag with details @@ -226,21 +272,41 @@ protected EppoValue getTypedAssignment( config.getEnvironmentName(), config.getConfigFetchedAt(), config.getConfigPublishedAt()); + EvaluationDetails evaluationDetails = evaluationResult.getEvaluationDetails(); EppoValue assignedValue = evaluationResult.getVariation() != null ? evaluationResult.getVariation().getValue() : null; + // Check if value type matches expected if (assignedValue != null && !valueTypeMatchesExpected(expectedType, assignedValue)) { log.warn( "no assigned variation because the flag type doesn't match the variation type: {} has type {}, variation value is {}", flagKey, flag.getVariationType(), assignedValue); - return defaultValue; + + // Update evaluation details with error code but keep the matched allocation and variation + // info + String variationKey = + evaluationResult.getVariation() != null ? evaluationResult.getVariation().getKey() : null; + String errorDescription = + String.format( + "Variation (%s) is configured for type %s, but is set to incompatible value (%s)", + variationKey, expectedType, assignedValue.doubleValue()); + + return EvaluationDetails.builder(evaluationDetails) + .flagEvaluationCode( + FlagEvaluationCode + .ASSIGNMENT_ERROR) // We use ASSIGNMENT_ERROR for value mismatch as it's a + // misconfiguration of the flag itself + .flagEvaluationDescription(errorDescription) + .variationKey(variationKey) + .variationValue(assignedValue) + .build(); } + // Log assignment if applicable if (assignedValue != null && assignmentLogger != null && evaluationResult.doLog()) { - try { String allocationKey = evaluationResult.getAllocationKey(); String experimentKey = @@ -279,7 +345,8 @@ protected EppoValue getTypedAssignment( log.error("Error logging assignment: {}", e.getMessage(), e); } } - return assignedValue != null ? assignedValue : defaultValue; + + return evaluationDetails; } private boolean valueTypeMatchesExpected(VariationType expectedType, EppoValue value) { @@ -320,17 +387,31 @@ public boolean getBooleanAssignment(String flagKey, String subjectKey, boolean d public boolean getBooleanAssignment( String flagKey, String subjectKey, Attributes subjectAttributes, boolean defaultValue) { + return this.getBooleanAssignmentDetails(flagKey, subjectKey, subjectAttributes, defaultValue) + .getVariation(); + } + + public AssignmentDetails getBooleanAssignmentDetails( + String flagKey, String subjectKey, boolean defaultValue) { + return this.getBooleanAssignmentDetails(flagKey, subjectKey, new Attributes(), defaultValue); + } + + public AssignmentDetails getBooleanAssignmentDetails( + String flagKey, String subjectKey, Attributes subjectAttributes, boolean defaultValue) { try { - EppoValue value = - this.getTypedAssignment( - flagKey, - subjectKey, - subjectAttributes, - EppoValue.valueOf(defaultValue), - VariationType.BOOLEAN); - return value.booleanValue(); + return this.getTypedAssignmentWithDetails( + flagKey, subjectKey, subjectAttributes, defaultValue, VariationType.BOOLEAN); } catch (Exception e) { - return throwIfNotGraceful(e, defaultValue); + return new AssignmentDetails<>( + throwIfNotGraceful(e, defaultValue), + null, + EvaluationDetails.buildDefault( + getConfiguration().getEnvironmentName(), + getConfiguration().getConfigFetchedAt(), + getConfiguration().getConfigPublishedAt(), + FlagEvaluationCode.ASSIGNMENT_ERROR, + e.getMessage(), + EppoValue.valueOf(defaultValue))); } } @@ -340,17 +421,31 @@ public int getIntegerAssignment(String flagKey, String subjectKey, int defaultVa public int getIntegerAssignment( String flagKey, String subjectKey, Attributes subjectAttributes, int defaultValue) { + return this.getIntegerAssignmentDetails(flagKey, subjectKey, subjectAttributes, defaultValue) + .getVariation(); + } + + public AssignmentDetails getIntegerAssignmentDetails( + String flagKey, String subjectKey, int defaultValue) { + return getIntegerAssignmentDetails(flagKey, subjectKey, new Attributes(), defaultValue); + } + + public AssignmentDetails getIntegerAssignmentDetails( + String flagKey, String subjectKey, Attributes subjectAttributes, int defaultValue) { try { - EppoValue value = - this.getTypedAssignment( - flagKey, - subjectKey, - subjectAttributes, - EppoValue.valueOf(defaultValue), - VariationType.INTEGER); - return Double.valueOf(value.doubleValue()).intValue(); + return this.getTypedAssignmentWithDetails( + flagKey, subjectKey, subjectAttributes, defaultValue, VariationType.INTEGER); } catch (Exception e) { - return throwIfNotGraceful(e, defaultValue); + return new AssignmentDetails<>( + throwIfNotGraceful(e, defaultValue), + null, + EvaluationDetails.buildDefault( + getConfiguration().getEnvironmentName(), + getConfiguration().getConfigFetchedAt(), + getConfiguration().getConfigPublishedAt(), + FlagEvaluationCode.ASSIGNMENT_ERROR, + e.getMessage(), + EppoValue.valueOf(defaultValue))); } } @@ -360,17 +455,31 @@ public Double getDoubleAssignment(String flagKey, String subjectKey, double defa public Double getDoubleAssignment( String flagKey, String subjectKey, Attributes subjectAttributes, double defaultValue) { + return this.getDoubleAssignmentDetails(flagKey, subjectKey, subjectAttributes, defaultValue) + .getVariation(); + } + + public AssignmentDetails getDoubleAssignmentDetails( + String flagKey, String subjectKey, double defaultValue) { + return getDoubleAssignmentDetails(flagKey, subjectKey, new Attributes(), defaultValue); + } + + public AssignmentDetails getDoubleAssignmentDetails( + String flagKey, String subjectKey, Attributes subjectAttributes, double defaultValue) { try { - EppoValue value = - this.getTypedAssignment( - flagKey, - subjectKey, - subjectAttributes, - EppoValue.valueOf(defaultValue), - VariationType.NUMERIC); - return value.doubleValue(); + return this.getTypedAssignmentWithDetails( + flagKey, subjectKey, subjectAttributes, defaultValue, VariationType.NUMERIC); } catch (Exception e) { - return throwIfNotGraceful(e, defaultValue); + return new AssignmentDetails<>( + throwIfNotGraceful(e, defaultValue), + null, + EvaluationDetails.buildDefault( + getConfiguration().getEnvironmentName(), + getConfiguration().getConfigFetchedAt(), + getConfiguration().getConfigPublishedAt(), + FlagEvaluationCode.ASSIGNMENT_ERROR, + e.getMessage(), + EppoValue.valueOf(defaultValue))); } } @@ -380,100 +489,103 @@ public String getStringAssignment(String flagKey, String subjectKey, String defa public String getStringAssignment( String flagKey, String subjectKey, Attributes subjectAttributes, String defaultValue) { + return this.getStringAssignmentDetails(flagKey, subjectKey, subjectAttributes, defaultValue) + .getVariation(); + } + + public AssignmentDetails getStringAssignmentDetails( + String flagKey, String subjectKey, String defaultValue) { + return this.getStringAssignmentDetails(flagKey, subjectKey, new Attributes(), defaultValue); + } + + public AssignmentDetails getStringAssignmentDetails( + String flagKey, String subjectKey, Attributes subjectAttributes, String defaultValue) { try { - EppoValue value = - this.getTypedAssignment( - flagKey, - subjectKey, - subjectAttributes, - EppoValue.valueOf(defaultValue), - VariationType.STRING); - return value.stringValue(); + return this.getTypedAssignmentWithDetails( + flagKey, subjectKey, subjectAttributes, defaultValue, VariationType.STRING); } catch (Exception e) { - return throwIfNotGraceful(e, defaultValue); + return new AssignmentDetails<>( + throwIfNotGraceful(e, defaultValue), + null, + EvaluationDetails.buildDefault( + getConfiguration().getEnvironmentName(), + getConfiguration().getConfigFetchedAt(), + getConfiguration().getConfigPublishedAt(), + FlagEvaluationCode.ASSIGNMENT_ERROR, + e.getMessage(), + EppoValue.valueOf(defaultValue))); } } - /** - * Returns the assignment for the provided feature flag key and subject key as a {@link JsonNode}. - * If the flag is not found, does not match the requested type or is disabled, defaultValue is - * returned. - * - * @param flagKey the feature flag key - * @param subjectKey the subject key - * @param defaultValue the default value to return if the flag is not found - * @return the JSON string value of the assignment - */ public JsonNode getJSONAssignment(String flagKey, String subjectKey, JsonNode defaultValue) { return getJSONAssignment(flagKey, subjectKey, new Attributes(), defaultValue); } - /** - * Returns the assignment for the provided feature flag key and subject key as a {@link JsonNode}. - * If the flag is not found, does not match the requested type or is disabled, defaultValue is - * returned. - * - * @param flagKey the feature flag key - * @param subjectKey the subject key - * @param defaultValue the default value to return if the flag is not found - * @return the JSON string value of the assignment - */ public JsonNode getJSONAssignment( String flagKey, String subjectKey, Attributes subjectAttributes, JsonNode defaultValue) { + return this.getJSONAssignmentDetails(flagKey, subjectKey, subjectAttributes, defaultValue) + .getVariation(); + } + + public AssignmentDetails getJSONAssignmentDetails( + String flagKey, String subjectKey, JsonNode defaultValue) { + return this.getJSONAssignmentDetails(flagKey, subjectKey, new Attributes(), defaultValue); + } + + public AssignmentDetails getJSONAssignmentDetails( + String flagKey, String subjectKey, Attributes subjectAttributes, JsonNode defaultValue) { try { - EppoValue value = - this.getTypedAssignment( - flagKey, - subjectKey, - subjectAttributes, - EppoValue.valueOf(defaultValue.toString()), - VariationType.JSON); - return value.unwrap(VariationType.JSON); + return this.getTypedAssignmentWithDetails( + flagKey, subjectKey, subjectAttributes, defaultValue, VariationType.JSON); } catch (Exception e) { - return throwIfNotGraceful(e, defaultValue); + String defaultValueString = defaultValue != null ? defaultValue.toString() : null; + return new AssignmentDetails<>( + throwIfNotGraceful(e, defaultValue), + null, + EvaluationDetails.buildDefault( + getConfiguration().getEnvironmentName(), + getConfiguration().getConfigFetchedAt(), + getConfiguration().getConfigPublishedAt(), + FlagEvaluationCode.ASSIGNMENT_ERROR, + e.getMessage(), + EppoValue.valueOf(defaultValueString))); } } - /** - * Returns the assignment for the provided feature flag key, subject key and subject attributes as - * a JSON string. If the flag is not found, does not match the requested type or is disabled, - * defaultValue is returned. - * - * @param flagKey the feature flag key - * @param subjectKey the subject key - * @param defaultValue the default value to return if the flag is not found - * @return the JSON string value of the assignment - */ + public String getJSONStringAssignment(String flagKey, String subjectKey, String defaultValue) { + return this.getJSONStringAssignment(flagKey, subjectKey, new Attributes(), defaultValue); + } + public String getJSONStringAssignment( String flagKey, String subjectKey, Attributes subjectAttributes, String defaultValue) { + return this.getJSONStringAssignmentDetails(flagKey, subjectKey, subjectAttributes, defaultValue) + .getVariation(); + } + + public AssignmentDetails getJSONStringAssignmentDetails( + String flagKey, String subjectKey, String defaultValue) { + return this.getJSONStringAssignmentDetails(flagKey, subjectKey, new Attributes(), defaultValue); + } + + public AssignmentDetails getJSONStringAssignmentDetails( + String flagKey, String subjectKey, Attributes subjectAttributes, String defaultValue) { try { - EppoValue value = - this.getTypedAssignment( - flagKey, - subjectKey, - subjectAttributes, - EppoValue.valueOf(defaultValue), - VariationType.JSON); - return value.stringValue(); + return this.getTypedAssignmentWithDetails( + flagKey, subjectKey, subjectAttributes, defaultValue, VariationType.JSON); } catch (Exception e) { - return throwIfNotGraceful(e, defaultValue); + return new AssignmentDetails<>( + throwIfNotGraceful(e, defaultValue), + null, + EvaluationDetails.buildDefault( + getConfiguration().getEnvironmentName(), + getConfiguration().getConfigFetchedAt(), + getConfiguration().getConfigPublishedAt(), + FlagEvaluationCode.ASSIGNMENT_ERROR, + e.getMessage(), + EppoValue.valueOf(defaultValue))); } } - /** - * Returns the assignment for the provided feature flag key and subject key as a JSON String. If - * the flag is not found, does not match the requested type or is disabled, defaultValue is - * returned. - * - * @param flagKey the feature flag key - * @param subjectKey the subject key - * @param defaultValue the default value to return if the flag is not found - * @return the JSON string value of the assignment - */ - public String getJSONStringAssignment(String flagKey, String subjectKey, String defaultValue) { - return this.getJSONStringAssignment(flagKey, subjectKey, new Attributes(), defaultValue); - } - public BanditResult getBanditAction( String flagKey, String subjectKey, diff --git a/src/test/java/cloud/eppo/BaseEppoClientTest.java b/src/test/java/cloud/eppo/BaseEppoClientTest.java index 4c80544..c51f17d 100644 --- a/src/test/java/cloud/eppo/BaseEppoClientTest.java +++ b/src/test/java/cloud/eppo/BaseEppoClientTest.java @@ -1,7 +1,6 @@ package cloud.eppo; -import static cloud.eppo.helpers.AssignmentTestCase.parseTestCaseFile; -import static cloud.eppo.helpers.AssignmentTestCase.runTestCase; +import static cloud.eppo.helpers.AssignmentTestCase.*; import static cloud.eppo.helpers.TestUtils.mockHttpError; import static cloud.eppo.helpers.TestUtils.mockHttpResponse; import static cloud.eppo.helpers.TestUtils.setBaseClientHttpClientOverrideField; @@ -188,6 +187,22 @@ public void testObfuscatedAssignments(File testFile) { runTestCase(testCase, eppoClient); } + @ParameterizedTest + @MethodSource("getAssignmentTestData") + public void testUnobfuscatedAssignmentsWithDetails(File testFile) { + initClient(false, false); + AssignmentTestCase testCase = parseTestCaseFile(testFile); + runTestCaseWithDetails(testCase, eppoClient); + } + + @ParameterizedTest + @MethodSource("getAssignmentTestData") + public void testObfuscatedAssignmentsWithDetails(File testFile) { + initClient(false, true); + AssignmentTestCase testCase = parseTestCaseFile(testFile); + runTestCaseWithDetails(testCase, eppoClient); + } + private static Stream getAssignmentTestData() { return AssignmentTestCase.getAssignmentTestData(); } @@ -243,12 +258,8 @@ public void testErrorGracefulModeOn() throws JsonProcessingException { BaseEppoClient spyClient = spy(realClient); doThrow(new RuntimeException("Exception thrown by mock")) .when(spyClient) - .getTypedAssignment( - anyString(), - anyString(), - any(Attributes.class), - any(EppoValue.class), - any(VariationType.class)); + .evaluateAndMaybeLog( + anyString(), anyString(), any(Attributes.class), any(VariationType.class)); assertTrue(spyClient.getBooleanAssignment("experiment1", "subject1", true)); assertFalse(spyClient.getBooleanAssignment("experiment1", "subject1", new Attributes(), false)); @@ -292,12 +303,8 @@ public void testErrorGracefulModeOff() { BaseEppoClient spyClient = spy(realClient); doThrow(new RuntimeException("Exception thrown by mock")) .when(spyClient) - .getTypedAssignment( - anyString(), - anyString(), - any(Attributes.class), - any(EppoValue.class), - any(VariationType.class)); + .evaluateAndMaybeLog( + anyString(), anyString(), any(Attributes.class), any(VariationType.class)); assertThrows( RuntimeException.class, @@ -654,10 +661,29 @@ public void testAssignmentLogErrorNonFatal() { doThrow(new RuntimeException("Mock Assignment Logging Error")) .when(mockAssignmentLogger) .logAssignment(any()); - double assignment = - eppoClient.getDoubleAssignment("numeric_flag", "alice", new Attributes(), 0.0); - - assertEquals(3.1415926, assignment, 0.0000001); + AssignmentDetails assignmentDetails = + eppoClient.getDoubleAssignmentDetails("numeric_flag", "alice", new Attributes(), 0.0); + + assertEquals(3.1415926, assignmentDetails.getVariation(), 0.0000001); + + // Verify evaluation details are populated correctly + EvaluationDetails details = assignmentDetails.getEvaluationDetails(); + assertNotNull(details); + assertEquals(FlagEvaluationCode.MATCH, details.getFlagEvaluationCode()); + assertNotNull(details.getEnvironmentName()); + assertEquals("Test", details.getEnvironmentName()); + + // Verify config timestamps + assertNotNull(details.getConfigPublishedAt()); + assertNotNull(details.getConfigFetchedAt()); + // Published at should be Wed Apr 17 15:40:53 EDT 2024 (from test JSON) + Date expectedPublishedAt = + new Date(1713382853716L); // 2024-04-17T19:40:53.716Z; matches flags-v1.json + assertEquals(expectedPublishedAt, details.getConfigPublishedAt()); + // Fetched at should be after published at (it's set when config is built) + assertTrue( + details.getConfigFetchedAt().after(details.getConfigPublishedAt()), + "Config fetched at should be after config published at"); ArgumentCaptor assignmentLogCaptor = ArgumentCaptor.forClass(Assignment.class); verify(mockAssignmentLogger, times(1)).logAssignment(assignmentLogCaptor.capture()); diff --git a/src/test/java/cloud/eppo/helpers/AssignmentTestCase.java b/src/test/java/cloud/eppo/helpers/AssignmentTestCase.java index dbd5edb..69bebe7 100644 --- a/src/test/java/cloud/eppo/helpers/AssignmentTestCase.java +++ b/src/test/java/cloud/eppo/helpers/AssignmentTestCase.java @@ -4,6 +4,7 @@ import cloud.eppo.BaseEppoClient; import cloud.eppo.api.AllocationDetails; +import cloud.eppo.api.AssignmentDetails; import cloud.eppo.api.Attributes; import cloud.eppo.api.EppoValue; import cloud.eppo.api.EvaluationDetails; @@ -107,45 +108,78 @@ private static void runTestCaseBase( String subjectKey = subjectAssignment.getSubjectKey(); Attributes subjectAttributes = subjectAssignment.getSubjectAttributes(); - // TODO: if validateDetails is true, call the getAssignmentDetails() method - if (validateDetails) { - System.out.println("TODO: call and validate details method"); - } - // Depending on the variation type, call the appropriate assignment method switch (testCase.getVariationType()) { case BOOLEAN: - boolean boolAssignment = - eppoClient.getBooleanAssignment( - flagKey, subjectKey, subjectAttributes, defaultValue.booleanValue()); - assertAssignment(flagKey, subjectAssignment, boolAssignment); + if (validateDetails) { + AssignmentDetails details = + eppoClient.getBooleanAssignmentDetails( + flagKey, subjectKey, subjectAttributes, defaultValue.booleanValue()); + assertAssignment(flagKey, subjectAssignment, details.getVariation()); + assertAssignmentDetails(flagKey, subjectAssignment, details.getEvaluationDetails()); + } else { + boolean boolAssignment = + eppoClient.getBooleanAssignment( + flagKey, subjectKey, subjectAttributes, defaultValue.booleanValue()); + assertAssignment(flagKey, subjectAssignment, boolAssignment); + } break; case INTEGER: - int intAssignment = - eppoClient.getIntegerAssignment( - flagKey, - subjectKey, - subjectAttributes, - Double.valueOf(defaultValue.doubleValue()).intValue()); - assertAssignment(flagKey, subjectAssignment, intAssignment); + int castedDefault = Double.valueOf(defaultValue.doubleValue()).intValue(); + if (validateDetails) { + AssignmentDetails details = + eppoClient.getIntegerAssignmentDetails( + flagKey, subjectKey, subjectAttributes, castedDefault); + assertAssignment(flagKey, subjectAssignment, details.getVariation()); + assertAssignmentDetails(flagKey, subjectAssignment, details.getEvaluationDetails()); + } else { + int intAssignment = + eppoClient.getIntegerAssignment( + flagKey, subjectKey, subjectAttributes, castedDefault); + assertAssignment(flagKey, subjectAssignment, intAssignment); + } break; case NUMERIC: - double doubleAssignment = - eppoClient.getDoubleAssignment( - flagKey, subjectKey, subjectAttributes, defaultValue.doubleValue()); - assertAssignment(flagKey, subjectAssignment, doubleAssignment); + if (validateDetails) { + AssignmentDetails details = + eppoClient.getDoubleAssignmentDetails( + flagKey, subjectKey, subjectAttributes, defaultValue.doubleValue()); + assertAssignment(flagKey, subjectAssignment, details.getVariation()); + assertAssignmentDetails(flagKey, subjectAssignment, details.getEvaluationDetails()); + } else { + double doubleAssignment = + eppoClient.getDoubleAssignment( + flagKey, subjectKey, subjectAttributes, defaultValue.doubleValue()); + assertAssignment(flagKey, subjectAssignment, doubleAssignment); + } break; case STRING: - String stringAssignment = - eppoClient.getStringAssignment( - flagKey, subjectKey, subjectAttributes, defaultValue.stringValue()); - assertAssignment(flagKey, subjectAssignment, stringAssignment); + if (validateDetails) { + AssignmentDetails details = + eppoClient.getStringAssignmentDetails( + flagKey, subjectKey, subjectAttributes, defaultValue.stringValue()); + assertAssignment(flagKey, subjectAssignment, details.getVariation()); + assertAssignmentDetails(flagKey, subjectAssignment, details.getEvaluationDetails()); + } else { + String stringAssignment = + eppoClient.getStringAssignment( + flagKey, subjectKey, subjectAttributes, defaultValue.stringValue()); + assertAssignment(flagKey, subjectAssignment, stringAssignment); + } break; case JSON: - JsonNode jsonAssignment = - eppoClient.getJSONAssignment( - flagKey, subjectKey, subjectAttributes, testCase.getDefaultValue().jsonValue()); - assertAssignment(flagKey, subjectAssignment, jsonAssignment); + if (validateDetails) { + AssignmentDetails details = + eppoClient.getJSONAssignmentDetails( + flagKey, subjectKey, subjectAttributes, testCase.getDefaultValue().jsonValue()); + assertAssignment(flagKey, subjectAssignment, details.getVariation()); + assertAssignmentDetails(flagKey, subjectAssignment, details.getEvaluationDetails()); + } else { + JsonNode jsonAssignment = + eppoClient.getJSONAssignment( + flagKey, subjectKey, subjectAttributes, testCase.getDefaultValue().jsonValue()); + assertAssignment(flagKey, subjectAssignment, jsonAssignment); + } break; default: throw new UnsupportedOperationException(