Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ private <T> ProviderEvaluation<T> evaluate(String key, T defaultValue, Evaluatio
}

private SelectedVariant<Object> fetchVariant(String key, EvaluationContext ctx) {
SelectedVariant<Object> fallback = new SelectedVariant<>(null);
SelectedVariant<Object> fallback = new SelectedVariant<>(key, null, null, null, null, null);
return flagsProvider.getVariant(key, fallback, convertContext(ctx), true);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void testGetMetadataReturnsCorrectName() {
@SuppressWarnings("unchecked")
@Test
public void testBooleanEvaluationSuccess() {
SelectedVariant<Object> variant = new SelectedVariant<>("on", true, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("bool-flag", "on", true, null, null, null);
when(mockFlagsProvider.getVariant(eq("bool-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand All @@ -56,7 +56,7 @@ public void testBooleanEvaluationSuccess() {
@SuppressWarnings("unchecked")
@Test
public void testBooleanEvaluationTypeMismatch() {
SelectedVariant<Object> variant = new SelectedVariant<>("on", "not-a-boolean", null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("bool-flag", "on", "not-a-boolean", null, null, null);
when(mockFlagsProvider.getVariant(eq("bool-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand Down Expand Up @@ -86,7 +86,7 @@ public void testBooleanEvaluationFlagNotFound() {
@SuppressWarnings("unchecked")
@Test
public void testStringEvaluationSuccess() {
SelectedVariant<Object> variant = new SelectedVariant<>("blue", "blue", null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("color-flag", "blue", "blue", null, null, null);
when(mockFlagsProvider.getVariant(eq("color-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand All @@ -101,7 +101,7 @@ public void testStringEvaluationSuccess() {
@SuppressWarnings("unchecked")
@Test
public void testStringEvaluationTypeMismatch() {
SelectedVariant<Object> variant = new SelectedVariant<>("on", 42, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("string-flag", "on", 42, null, null, null);
when(mockFlagsProvider.getVariant(eq("string-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand Down Expand Up @@ -131,7 +131,7 @@ public void testStringEvaluationFlagNotFound() {
@SuppressWarnings("unchecked")
@Test
public void testIntegerEvaluationSuccess() {
SelectedVariant<Object> variant = new SelectedVariant<>("v1", 42, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("int-flag", "v1", 42, null, null, null);
when(mockFlagsProvider.getVariant(eq("int-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand All @@ -146,7 +146,7 @@ public void testIntegerEvaluationSuccess() {
@SuppressWarnings("unchecked")
@Test
public void testIntegerEvaluationFromLong() {
SelectedVariant<Object> variant = new SelectedVariant<>("v1", 42L, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("int-flag", "v1", 42L, null, null, null);
when(mockFlagsProvider.getVariant(eq("int-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand All @@ -159,7 +159,7 @@ public void testIntegerEvaluationFromLong() {
@SuppressWarnings("unchecked")
@Test
public void testIntegerEvaluationFromDouble() {
SelectedVariant<Object> variant = new SelectedVariant<>("v1", 42.0, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("int-flag", "v1", 42.0, null, null, null);
when(mockFlagsProvider.getVariant(eq("int-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand All @@ -172,7 +172,7 @@ public void testIntegerEvaluationFromDouble() {
@SuppressWarnings("unchecked")
@Test
public void testIntegerEvaluationTypeMismatch() {
SelectedVariant<Object> variant = new SelectedVariant<>("v1", "not-a-number", null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("int-flag", "v1", "not-a-number", null, null, null);
when(mockFlagsProvider.getVariant(eq("int-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand Down Expand Up @@ -200,7 +200,7 @@ public void testIntegerEvaluationFlagNotFound() {
@SuppressWarnings("unchecked")
@Test
public void testIntegerEvaluationOverflowReturnsMismatch() {
SelectedVariant<Object> variant = new SelectedVariant<>("v1", Long.MAX_VALUE, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("int-flag", "v1", Long.MAX_VALUE, null, null, null);
when(mockFlagsProvider.getVariant(eq("int-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand All @@ -215,7 +215,7 @@ public void testIntegerEvaluationOverflowReturnsMismatch() {
@SuppressWarnings("unchecked")
@Test
public void testDoubleEvaluationSuccess() {
SelectedVariant<Object> variant = new SelectedVariant<>("v1", 3.14, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("double-flag", "v1", 3.14, null, null, null);
when(mockFlagsProvider.getVariant(eq("double-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand All @@ -230,7 +230,7 @@ public void testDoubleEvaluationSuccess() {
@SuppressWarnings("unchecked")
@Test
public void testDoubleEvaluationFromInteger() {
SelectedVariant<Object> variant = new SelectedVariant<>("v1", 42, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("double-flag", "v1", 42, null, null, null);
when(mockFlagsProvider.getVariant(eq("double-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand All @@ -243,7 +243,7 @@ public void testDoubleEvaluationFromInteger() {
@SuppressWarnings("unchecked")
@Test
public void testDoubleEvaluationTypeMismatch() {
SelectedVariant<Object> variant = new SelectedVariant<>("v1", "not-a-number", null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("double-flag", "v1", "not-a-number", null, null, null);
when(mockFlagsProvider.getVariant(eq("double-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand Down Expand Up @@ -275,7 +275,7 @@ public void testDoubleEvaluationFlagNotFound() {
public void testObjectEvaluationSuccess() {
Map<String, Object> objValue = new HashMap<>();
objValue.put("key", "value");
SelectedVariant<Object> variant = new SelectedVariant<>("v1", objValue, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("obj-flag", "v1", objValue, null, null, null);
when(mockFlagsProvider.getVariant(eq("obj-flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand Down Expand Up @@ -307,7 +307,7 @@ public void testObjectEvaluationFlagNotFound() {
@SuppressWarnings("unchecked")
@Test
public void testPerEvaluationContextIsForwarded() {
SelectedVariant<Object> variant = new SelectedVariant<>("on", true, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("flag", "on", true, null, null, null);
ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class);
when(mockFlagsProvider.getVariant(eq("flag"), any(SelectedVariant.class), contextCaptor.capture(), eq(true)))
.thenReturn(variant);
Expand All @@ -327,7 +327,7 @@ public void testPerEvaluationContextIsForwarded() {
public void testPerEvaluationContextIsForwardedForObjectEvaluation() {
Map<String, Object> objValue = new HashMap<>();
objValue.put("key", "value");
SelectedVariant<Object> variant = new SelectedVariant<>("v1", objValue, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("obj-flag", "v1", objValue, null, null, null);
ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class);
when(mockFlagsProvider.getVariant(eq("obj-flag"), any(SelectedVariant.class), contextCaptor.capture(), eq(true)))
.thenReturn(variant);
Expand All @@ -343,7 +343,7 @@ public void testPerEvaluationContextIsForwardedForObjectEvaluation() {
@SuppressWarnings("unchecked")
@Test
public void testTargetingKeyIsRegularProperty() {
SelectedVariant<Object> variant = new SelectedVariant<>("on", true, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("flag", "on", true, null, null, null);
ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class);
when(mockFlagsProvider.getVariant(eq("flag"), any(SelectedVariant.class), contextCaptor.capture(), eq(true)))
.thenReturn(variant);
Expand All @@ -362,7 +362,7 @@ public void testTargetingKeyIsRegularProperty() {
@SuppressWarnings("unchecked")
@Test
public void testTargetingKeyFromGetTargetingKeyIsIncluded() {
SelectedVariant<Object> variant = new SelectedVariant<>("on", true, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("flag", "on", true, null, null, null);
ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class);
when(mockFlagsProvider.getVariant(eq("flag"), any(SelectedVariant.class), contextCaptor.capture(), eq(true)))
.thenReturn(variant);
Expand All @@ -382,7 +382,7 @@ public void testTargetingKeyFromGetTargetingKeyIsIncluded() {
@SuppressWarnings("unchecked")
@Test
public void testExplicitTargetingKeyAttributeOverriddenByGetTargetingKey() {
SelectedVariant<Object> variant = new SelectedVariant<>("on", true, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("flag", "on", true, null, null, null);
ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class);
when(mockFlagsProvider.getVariant(eq("flag"), any(SelectedVariant.class), contextCaptor.capture(), eq(true)))
.thenReturn(variant);
Expand Down Expand Up @@ -435,7 +435,7 @@ public void testProviderNotReadyObjectEvaluation() {
public void testProviderReadyWithLocalProvider() {
LocalFlagsProvider mockLocal = mock(LocalFlagsProvider.class);
when(mockLocal.areFlagsReady()).thenReturn(true);
SelectedVariant<Object> variant = new SelectedVariant<>("on", true, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("flag", "on", true, null, null, null);
when(mockLocal.getVariant(eq("flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);
MixpanelProvider localProvider = new MixpanelProvider(mockLocal);
Expand All @@ -451,7 +451,7 @@ public void testProviderReadyWithLocalProvider() {
@Test
public void testProviderNotReadySkippedForNonLocalProvider() {
// BaseFlagsProvider (non-local) should not check readiness
SelectedVariant<Object> variant = new SelectedVariant<>("on", true, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("flag", "on", true, null, null, null);
when(mockFlagsProvider.getVariant(eq("flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand Down Expand Up @@ -498,7 +498,7 @@ public void testObjectEvaluationException() {
@SuppressWarnings("unchecked")
@Test
public void testVariantKeyPassedThroughOnBooleanEvaluation() {
SelectedVariant<Object> variant = new SelectedVariant<>("my-variant", true, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("flag", "my-variant", true, null, null, null);
when(mockFlagsProvider.getVariant(eq("flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand All @@ -510,7 +510,7 @@ public void testVariantKeyPassedThroughOnBooleanEvaluation() {
@SuppressWarnings("unchecked")
@Test
public void testVariantKeyPassedThroughOnObjectEvaluation() {
SelectedVariant<Object> variant = new SelectedVariant<>("obj-variant", "some-value", null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("flag", "obj-variant", "some-value", null, null, null);
when(mockFlagsProvider.getVariant(eq("flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand All @@ -522,7 +522,7 @@ public void testVariantKeyPassedThroughOnObjectEvaluation() {
@SuppressWarnings("unchecked")
@Test
public void testNullVariantKeyTreatedAsFallbackOnBooleanEvaluation() {
SelectedVariant<Object> variant = new SelectedVariant<>(null, true, null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("flag", null, true, null, null, null);
when(mockFlagsProvider.getVariant(eq("flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand All @@ -536,7 +536,7 @@ public void testNullVariantKeyTreatedAsFallbackOnBooleanEvaluation() {
@SuppressWarnings("unchecked")
@Test
public void testNullVariantKeyTreatedAsFallbackOnObjectEvaluation() {
SelectedVariant<Object> variant = new SelectedVariant<>(null, "some-value", null, null, null);
SelectedVariant<Object> variant = new SelectedVariant<>("flag", null, "some-value", null, null, null);
when(mockFlagsProvider.getVariant(eq("flag"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(variant);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
/**
* Represents the result of a feature flag evaluation.
* <p>
* Contains the selected variant key and its value. Both may be null if the
* fallback was returned (e.g., flag not found, evaluation error).
* Contains the originating flag key, the selected variant key, and the variant value.
* The variant key and value may be null if the fallback was returned (e.g., flag not
* found, evaluation error); the flag key is populated by the SDK whenever a variant is
* returned from {@code getVariant}, including on fallback paths.
* </p>
* <p>
* This class is immutable and thread-safe.
Expand All @@ -15,39 +17,68 @@
* @param <T> the type of the variant value
*/
public final class SelectedVariant<T> {
private final String flagKey;
private final String variantKey;
private final T variantValue;
private final UUID experimentId;
private final Boolean isExperimentActive;
private final Boolean isQaTester;

/**
* Creates a SelectedVariant with only a value (key is null).
* This is typically used for fallback responses.
* Creates a SelectedVariant carrying only a value. Both the flag key and variant key are
* null. This is the typical form for constructing a fallback to pass into
* {@code getVariant}; the SDK will stamp the requested flag key onto the returned variant.
*
* @param variantValue the fallback value
*/
public SelectedVariant(T variantValue) {
this(null, variantValue, null, null, null);
this(null, null, variantValue, null, null, null);
}

/**
* Creates a new SelectedVariant with experimentation metadata.
* Creates a new SelectedVariant with experimentation metadata. The flag key will be null.
*
* @param variantKey the key of the selected variant (may be null for fallback)
* @param variantValue the value of the selected variant (may be null for fallback)
* @param experimentId the experiment ID (may be null)
* @param isExperimentActive whether the experiment is active (may be null)
* @param isQaTester whether the user is a QA tester (may be null)
* @deprecated Use {@link #SelectedVariant(String, String, Object, UUID, Boolean, Boolean)}
* which also accepts the originating flag key, so the resulting variant can be
* associated with the flag it was selected for.
*/
@Deprecated
public SelectedVariant(String variantKey, T variantValue, UUID experimentId, Boolean isExperimentActive, Boolean isQaTester) {
this(null, variantKey, variantValue, experimentId, isExperimentActive, isQaTester);
}

/**
* Creates a new SelectedVariant with the originating flag key and experimentation metadata.
*
* @param flagKey the key of the flag this variant was selected for (may be null for fallback)
* @param variantKey the key of the selected variant (may be null for fallback)
* @param variantValue the value of the selected variant (may be null for fallback)
* @param experimentId the experiment ID (may be null)
* @param isExperimentActive whether the experiment is active (may be null)
* @param isQaTester whether the user is a QA tester (may be null)
*/
public SelectedVariant(String flagKey, String variantKey, T variantValue, UUID experimentId, Boolean isExperimentActive, Boolean isQaTester) {
this.flagKey = flagKey;
this.variantKey = variantKey;
this.variantValue = variantValue;
this.experimentId = experimentId;
this.isExperimentActive = isExperimentActive;
this.isQaTester = isQaTester;
}

/**
* @return the flag key this variant was selected for, or null if not set (e.g., for fallbacks
* or variants returned by code paths that don't propagate the flag key)
*/
public String getFlagKey() {
return flagKey;
}

/**
* @return the variant key, or null if this is a fallback
*/
Expand Down Expand Up @@ -83,6 +114,20 @@ public Boolean getIsQaTester() {
return isQaTester;
}

/**
* Returns a SelectedVariant with the given flag key, copying all other fields from this instance.
* Returns this instance unchanged if the flag key already matches.
*
* @param flagKey the flag key to associate with the variant (may be null)
* @return a SelectedVariant with the requested flag key
*/
public SelectedVariant<T> withFlagKey(String flagKey) {
if (flagKey == null ? this.flagKey == null : flagKey.equals(this.flagKey)) {
return this;
}
return new SelectedVariant<>(flagKey, variantKey, variantValue, experimentId, isExperimentActive, isQaTester);
}

/**
* @return true if this represents a successfully selected variant (not a fallback)
*/
Expand All @@ -100,7 +145,8 @@ public boolean isFallback() {
@Override
public String toString() {
return "SelectedVariant{" +
"variantKey='" + variantKey + '\'' +
"flagKey='" + flagKey + '\'' +
", variantKey='" + variantKey + '\'' +
", variantValue=" + variantValue +
", experimentId=" + experimentId +
", isExperimentActive=" + isExperimentActive +
Expand All @@ -115,6 +161,7 @@ public boolean equals(Object o) {

SelectedVariant<?> that = (SelectedVariant<?>) o;

if (flagKey != null ? !flagKey.equals(that.flagKey) : that.flagKey != null) return false;
if (variantKey != null ? !variantKey.equals(that.variantKey) : that.variantKey != null) return false;
if (variantValue != null ? !variantValue.equals(that.variantValue) : that.variantValue != null) return false;
if (experimentId != null ? !experimentId.equals(that.experimentId) : that.experimentId != null) return false;
Expand Down
Loading
Loading