Skip to content
Draft
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
10 changes: 8 additions & 2 deletions openfeature-provider/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>com.mixpanel</groupId>
<artifactId>mixpanel-java-openfeature</artifactId>
<version>0.1.1</version>
<version>0.2.0</version>
<packaging>jar</packaging>
<name>Mixpanel Java SDK - OpenFeature Provider</name>
<description>
Expand Down Expand Up @@ -126,7 +126,13 @@
<dependency>
<groupId>com.mixpanel</groupId>
<artifactId>mixpanel-java</artifactId>
<version>1.8.0</version>
<!--
1.10.0+ ships Source.Fallback.Reason (SDK-79 / #89) which
MixpanelProvider now dispatches on. Earlier versions lack
the Source model class entirely. Release manager may pick
a different next-version number — update accordingly.
-->
<version>1.10.0</version>
</dependency>

<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.mixpanel.mixpanelapi.featureflags.config.LocalFlagsConfig;
import com.mixpanel.mixpanelapi.featureflags.config.RemoteFlagsConfig;
import com.mixpanel.mixpanelapi.featureflags.model.SelectedVariant;
import com.mixpanel.mixpanelapi.featureflags.model.Source;
import com.mixpanel.mixpanelapi.featureflags.provider.BaseFlagsProvider;
import com.mixpanel.mixpanelapi.featureflags.provider.LocalFlagsProvider;
import dev.openfeature.sdk.*;
Expand Down Expand Up @@ -131,8 +132,14 @@ private <T> ProviderEvaluation<T> evaluate(String key, T defaultValue, Evaluatio
return errorResult(defaultValue, ErrorCode.GENERAL, e.getMessage());
}

if (result.isFallback()) {
return errorResult(defaultValue, ErrorCode.FLAG_NOT_FOUND, "Flag not found: " + key);
// A fallback source means the SDK had no real variant to serve.
// The reason discriminates why (flag missing, context key missing,
// no rollout matched, backend error) so we translate each to the
// OpenFeature error the spec assigns to it instead of collapsing
// every fallback to FLAG_NOT_FOUND.
Source source = result.getSource();
if (source instanceof Source.Fallback) {
return mapFallback(((Source.Fallback) source).reason, key, defaultValue);
}

T value = mapper.apply(result);
Expand All @@ -144,12 +151,44 @@ private <T> ProviderEvaluation<T> evaluate(String key, T defaultValue, Evaluatio
return successResult(value, result.getVariantKey());
}

private <T> ProviderEvaluation<T> mapFallback(Source.Fallback.Reason reason, String flagKey, T defaultValue) {
switch (reason) {
case FLAG_NOT_FOUND:
return errorResult(defaultValue, ErrorCode.FLAG_NOT_FOUND, "Flag not found: " + flagKey);
case MISSING_CONTEXT_KEY:
return errorResult(defaultValue, ErrorCode.TARGETING_KEY_MISSING,
"Missing targeting key for flag: " + flagKey);
case NO_ROLLOUT_MATCH:
// Flag exists but no rollout matched — per the OpenFeature spec
// this is DEFAULT with no error, distinct from FLAG_NOT_FOUND.
return defaultResult(defaultValue);
case BACKEND_ERROR:
return errorResult(defaultValue, ErrorCode.GENERAL,
"Backend error evaluating flag: " + flagKey);
default:
// Fail closed on unrecognized reasons so a new value added to
// the base SDK enum without wiring here doesn't silently
// produce a successful-looking evaluation.
return errorResult(defaultValue, ErrorCode.GENERAL,
"Unrecognized fallback reason " + reason + " for flag: " + flagKey);
}
}

private <T> ProviderEvaluation<T> defaultResult(T defaultValue) {
return ProviderEvaluation.<T>builder()
.value(defaultValue)
.reason("DEFAULT")
.build();
}

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

private <T> ProviderEvaluation<T> errorResult(T defaultValue, ErrorCode errorCode, String errorMessage) {
// FLAG_NOT_FOUND is spec-defined DEFAULT (flag missing → fell back to
// default); every other error condition is ERROR reason.
String reason = errorCode == ErrorCode.FLAG_NOT_FOUND ? "DEFAULT" : "ERROR";
return ProviderEvaluation.<T>builder()
.value(defaultValue)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.mixpanel.openfeature;

import com.mixpanel.mixpanelapi.featureflags.model.SelectedVariant;
import com.mixpanel.mixpanelapi.featureflags.model.Source;
import com.mixpanel.mixpanelapi.featureflags.provider.BaseFlagsProvider;
import com.mixpanel.mixpanelapi.featureflags.provider.LocalFlagsProvider;
import dev.openfeature.sdk.*;
Expand Down Expand Up @@ -548,6 +549,66 @@ public void testNullVariantKeyTreatedAsFallbackOnObjectEvaluation() {
assertEquals("DEFAULT", result.getReason());
}

// Fallback.Reason mapping (SDK-79 / SDK-130)

@Test
public void testFallbackWithFlagNotFoundReasonMapsToFlagNotFound() {
SelectedVariant<Object> fallback = new SelectedVariant<>(null, false, null, null, null,
Source.fallback(Source.Fallback.Reason.FLAG_NOT_FOUND));
when(mockFlagsProvider.getVariant(eq("missing"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(fallback);

ProviderEvaluation<Boolean> result = provider.getBooleanEvaluation("missing", false, new ImmutableContext());

assertFalse(result.getValue());
assertEquals(ErrorCode.FLAG_NOT_FOUND, result.getErrorCode());
assertEquals("DEFAULT", result.getReason());
}

@Test
public void testFallbackWithMissingContextKeyMapsToTargetingKeyMissing() {
SelectedVariant<Object> fallback = new SelectedVariant<>(null, false, null, null, null,
Source.fallback(Source.Fallback.Reason.MISSING_CONTEXT_KEY));
when(mockFlagsProvider.getVariant(eq("needs-key"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(fallback);

ProviderEvaluation<Boolean> result = provider.getBooleanEvaluation("needs-key", false, new ImmutableContext());

assertFalse(result.getValue());
assertEquals(ErrorCode.TARGETING_KEY_MISSING, result.getErrorCode());
assertEquals("ERROR", result.getReason());
}

@Test
public void testFallbackWithNoRolloutMatchReturnsDefaultWithoutError() {
SelectedVariant<Object> fallback = new SelectedVariant<>(null, "fallback-val", null, null, null,
Source.fallback(Source.Fallback.Reason.NO_ROLLOUT_MATCH));
when(mockFlagsProvider.getVariant(eq("no-match"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(fallback);

ProviderEvaluation<String> result = provider.getStringEvaluation("no-match", "fallback-val", new ImmutableContext());

// NO_ROLLOUT_MATCH is spec: DEFAULT reason with no error — the flag
// exists, the user just didn't match any rollout.
assertEquals("fallback-val", result.getValue());
assertNull(result.getErrorCode());
assertEquals("DEFAULT", result.getReason());
}

@Test
public void testFallbackWithBackendErrorMapsToGeneral() {
SelectedVariant<Object> fallback = new SelectedVariant<>(null, false, null, null, null,
Source.fallback(Source.Fallback.Reason.BACKEND_ERROR));
when(mockFlagsProvider.getVariant(eq("backend-fail"), any(SelectedVariant.class), anyMap(), eq(true)))
.thenReturn(fallback);

ProviderEvaluation<Boolean> result = provider.getBooleanEvaluation("backend-fail", false, new ImmutableContext());

assertFalse(result.getValue());
assertEquals(ErrorCode.GENERAL, result.getErrorCode());
assertEquals("ERROR", result.getReason());
}

// Shutdown

@Test
Expand Down
Loading