Skip to content

Commit 730487e

Browse files
feat: adds support for sending DevCycle Eval Reasons to the OpenFeature interface (#180)
1 parent 86a1614 commit 730487e

File tree

2 files changed

+143
-17
lines changed

2 files changed

+143
-17
lines changed

src/main/java/com/devcycle/sdk/server/openfeature/DevCycleProvider.java

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
package com.devcycle.sdk.server.openfeature;
22

3+
import java.math.BigDecimal;
4+
import java.util.Map;
5+
import java.util.Optional;
6+
37
import com.devcycle.sdk.server.common.api.IDevCycleClient;
48
import com.devcycle.sdk.server.common.exception.DevCycleException;
59
import com.devcycle.sdk.server.common.model.DevCycleEvent;
610
import com.devcycle.sdk.server.common.model.DevCycleUser;
11+
import com.devcycle.sdk.server.common.model.EvalReason;
712
import com.devcycle.sdk.server.common.model.Variable;
8-
import dev.openfeature.sdk.*;
9-
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
13+
14+
import dev.openfeature.sdk.ErrorCode;
15+
import dev.openfeature.sdk.EvaluationContext;
16+
import dev.openfeature.sdk.FeatureProvider;
17+
import dev.openfeature.sdk.ImmutableMetadata;
18+
import dev.openfeature.sdk.ImmutableMetadata.ImmutableMetadataBuilder;
19+
import dev.openfeature.sdk.Metadata;
20+
import dev.openfeature.sdk.ProviderEvaluation;
21+
import dev.openfeature.sdk.Reason;
22+
import dev.openfeature.sdk.Structure;
23+
import dev.openfeature.sdk.TrackingEventDetails;
24+
import dev.openfeature.sdk.Value;
1025
import dev.openfeature.sdk.exceptions.GeneralError;
26+
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
1127
import dev.openfeature.sdk.exceptions.TypeMismatchError;
1228

13-
import java.math.BigDecimal;
14-
import java.util.Map;
15-
import java.util.Optional;
16-
1729
public class DevCycleProvider implements FeatureProvider {
1830
private static final String PROVIDER_NAME = "DevCycle";
1931

@@ -97,19 +109,36 @@ public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultVa
97109
DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx);
98110

99111
Variable<Object> variable = devcycleClient.variable(user, key, defaultValue.asStructure().asObjectMap());
112+
100113

101114
if (variable == null || variable.getIsDefaulted()) {
115+
ImmutableMetadata flagMetadata = null;
116+
if (variable != null && variable.getEval() != null) {
117+
EvalReason eval = variable.getEval();
118+
flagMetadata = getFlagMetadata(eval);
119+
}
102120
return ProviderEvaluation.<Value>builder()
103121
.value(defaultValue)
104122
.reason(Reason.DEFAULT.toString())
123+
.flagMetadata(flagMetadata)
105124
.build();
106125
} else {
107126
if (variable.getValue() instanceof Map) {
108127
// JSON objects are managed as Map implementations and must be converted to an OpenFeature structure
109128
Value objectValue = new Value(Structure.mapToStructure((Map) variable.getValue()));
129+
130+
ImmutableMetadata flagMetadata = null;
131+
String evalReason = Reason.TARGETING_MATCH.toString();
132+
if (variable.getEval() != null) {
133+
EvalReason eval = variable.getEval();
134+
evalReason = eval.getReason();
135+
flagMetadata = getFlagMetadata(eval);
136+
}
137+
110138
return ProviderEvaluation.<Value>builder()
111139
.value(objectValue)
112-
.reason(Reason.TARGETING_MATCH.toString())
140+
.reason(evalReason)
141+
.flagMetadata(flagMetadata)
113142
.build();
114143
} else {
115144
throw new TypeMismatchError("DevCycle variable for key " + key + " is not a JSON object");
@@ -136,9 +165,15 @@ <T> ProviderEvaluation<T> resolvePrimitiveVariable(String key, T defaultValue, E
136165
Variable<T> variable = devcycleClient.variable(user, key, defaultValue);
137166

138167
if (variable == null || variable.getIsDefaulted()) {
168+
ImmutableMetadata flagMetadata = null;
169+
if (variable != null && variable.getEval() != null) {
170+
EvalReason eval = variable.getEval();
171+
flagMetadata = getFlagMetadata(eval);
172+
}
139173
return ProviderEvaluation.<T>builder()
140174
.value(defaultValue)
141175
.reason(Reason.DEFAULT.toString())
176+
.flagMetadata(flagMetadata)
142177
.build();
143178
} else {
144179
T value = variable.getValue();
@@ -149,9 +184,18 @@ <T> ProviderEvaluation<T> resolvePrimitiveVariable(String key, T defaultValue, E
149184
value = (T) Integer.valueOf(numVal.intValue());
150185
}
151186

187+
ImmutableMetadata flagMetadata = null;
188+
String evalReason = Reason.TARGETING_MATCH.toString();
189+
if (variable.getEval() != null) {
190+
EvalReason eval = variable.getEval();
191+
evalReason = eval.getReason();
192+
flagMetadata = getFlagMetadata(eval);
193+
}
194+
152195
return ProviderEvaluation.<T>builder()
153196
.value(value)
154-
.reason(Reason.TARGETING_MATCH.toString())
197+
.reason(evalReason)
198+
.flagMetadata(flagMetadata)
155199
.build();
156200
}
157201
} catch (IllegalArgumentException e) {
@@ -206,4 +250,17 @@ private Map<String, Object> getMetadataWithoutValue(TrackingEventDetails details
206250
metaData.remove("value");
207251
return metaData;
208252
}
253+
254+
private ImmutableMetadata getFlagMetadata(EvalReason evalReason) {
255+
ImmutableMetadataBuilder flagMetadataBuilder = ImmutableMetadata.builder();
256+
257+
if (evalReason.getDetails() != null) {
258+
flagMetadataBuilder.addString("evalReasonDetails", evalReason.getDetails());
259+
}
260+
261+
if (evalReason.getTargetId() != null) {
262+
flagMetadataBuilder.addString("evalReasonTargetId", evalReason.getTargetId());
263+
}
264+
return flagMetadataBuilder.build();
265+
}
209266
}

src/test/java/com/devcycle/sdk/server/openfeature/DevCycleProviderTest.java

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
11
package com.devcycle.sdk.server.openfeature;
22

3-
import com.devcycle.sdk.server.common.api.IDevCycleClient;
4-
import com.devcycle.sdk.server.common.model.Variable;
5-
import dev.openfeature.sdk.*;
6-
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
7-
import dev.openfeature.sdk.exceptions.TargetingKeyMissingError;
8-
import dev.openfeature.sdk.exceptions.TypeMismatchError;
3+
import static org.mockito.ArgumentMatchers.any;
4+
import static org.mockito.Mockito.mock;
5+
import static org.mockito.Mockito.when;
6+
7+
import java.util.ArrayList;
8+
import java.util.HashMap;
9+
import java.util.LinkedHashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
913
import org.junit.Assert;
1014
import org.junit.Test;
1115
import org.junit.runner.RunWith;
1216
import org.mockito.junit.MockitoJUnitRunner;
1317

14-
import java.util.*;
18+
import com.devcycle.sdk.server.common.api.IDevCycleClient;
19+
import com.devcycle.sdk.server.common.model.EvalReason;
20+
import com.devcycle.sdk.server.common.model.Variable;
1521

16-
import static org.mockito.Mockito.*;
22+
import dev.openfeature.sdk.ErrorCode;
23+
import dev.openfeature.sdk.ImmutableContext;
24+
import dev.openfeature.sdk.ProviderEvaluation;
25+
import dev.openfeature.sdk.Reason;
26+
import dev.openfeature.sdk.Structure;
27+
import dev.openfeature.sdk.Value;
28+
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
29+
import dev.openfeature.sdk.exceptions.TargetingKeyMissingError;
30+
import dev.openfeature.sdk.exceptions.TypeMismatchError;
1731

1832
@RunWith(MockitoJUnitRunner.class)
1933
public class DevCycleProviderTest {
@@ -90,7 +104,7 @@ public void testResolveVariableDefaulted() {
90104
IDevCycleClient dvcClient = mock(IDevCycleClient.class);
91105
when(dvcClient.isInitialized()).thenReturn(true);
92106

93-
when(dvcClient.variable(any(), any(), any())).thenReturn(Variable.builder().key("some-flag").value("unused value 1").defaultValue("default value").isDefaulted(true).type(Variable.TypeEnum.STRING).build());
107+
when(dvcClient.variable(any(), any(), any())).thenReturn(Variable.builder().key("some-flag").value("unused value 1").defaultValue("default value").isDefaulted(true).type(Variable.TypeEnum.STRING).eval(EvalReason.defaultReason(EvalReason.DefaultReasonDetailsEnum.USER_NOT_TARGETED)).build());
94108

95109
DevCycleProvider provider = new DevCycleProvider(dvcClient);
96110

@@ -99,6 +113,8 @@ public void testResolveVariableDefaulted() {
99113
Assert.assertEquals(result.getValue(), "default value");
100114
Assert.assertEquals(result.getReason(), Reason.DEFAULT.toString());
101115
Assert.assertNull(result.getErrorCode());
116+
Assert.assertNotNull(result.getFlagMetadata());
117+
Assert.assertEquals(result.getFlagMetadata().getString("evalReasonDetails"), EvalReason.DefaultReasonDetailsEnum.USER_NOT_TARGETED.getValue());
102118
}
103119

104120
@Test
@@ -117,6 +133,25 @@ public void testResolveBooleanVariable() {
117133
Assert.assertNull(result.getErrorCode());
118134
}
119135

136+
@Test
137+
public void testResolveBooleanVariableWithDevCycleEvalReason() {
138+
IDevCycleClient dvcClient = mock(IDevCycleClient.class);
139+
when(dvcClient.isInitialized()).thenReturn(true);
140+
141+
when(dvcClient.variable(any(), any(), any())).thenReturn(Variable.builder().key("some-flag").value(true).defaultValue(false).type(Variable.TypeEnum.BOOLEAN).eval(new EvalReason("SPLIT", "User ID", "bool_target_id")).build());
142+
143+
DevCycleProvider provider = new DevCycleProvider(dvcClient);
144+
145+
ProviderEvaluation<Boolean> result = provider.resolvePrimitiveVariable("some-flag", false, new ImmutableContext("user-1234"));
146+
Assert.assertNotNull(result);
147+
Assert.assertEquals(result.getValue(), true);
148+
Assert.assertEquals(result.getReason(), "SPLIT");
149+
Assert.assertNull(result.getErrorCode());
150+
Assert.assertNotNull(result.getFlagMetadata());
151+
Assert.assertEquals(result.getFlagMetadata().getString("evalReasonDetails"), "User ID");
152+
Assert.assertEquals(result.getFlagMetadata().getString("evalReasonTargetId"), "bool_target_id");
153+
}
154+
120155
@Test
121156
public void testResolveIntegerVariable() {
122157
IDevCycleClient dvcClient = mock(IDevCycleClient.class);
@@ -249,4 +284,38 @@ public void testGetObjectEvaluation() {
249284
Assert.assertEquals(result.getReason(), Reason.TARGETING_MATCH.toString());
250285
Assert.assertNull(result.getErrorCode());
251286
}
287+
288+
@Test
289+
public void testGetObjectEvaluationWithDevCycleEvalReason() {
290+
Map<String, Object> jsonData = new LinkedHashMap<>();
291+
jsonData.put("strVal", "some string");
292+
jsonData.put("boolVal", true);
293+
jsonData.put("numVal", 123);
294+
295+
Map<String, Object> defaultJsonData = new LinkedHashMap<>();
296+
297+
IDevCycleClient dvcClient = mock(IDevCycleClient.class);
298+
when(dvcClient.isInitialized()).thenReturn(true);
299+
when(dvcClient.variable(any(), any(), any())).thenReturn(Variable.builder().key("some-flag").value(jsonData).defaultValue(defaultJsonData).type(Variable.TypeEnum.JSON).eval(new EvalReason("SPLIT", "User ID", "json_target_id")).build());
300+
301+
DevCycleProvider provider = new DevCycleProvider(dvcClient);
302+
303+
Value defaultValue = new Value(Structure.mapToStructure(defaultJsonData));
304+
305+
ProviderEvaluation<Value> result = provider.getObjectEvaluation("some-flag", defaultValue, new ImmutableContext("user-1234"));
306+
Assert.assertNotNull(result);
307+
Assert.assertNotNull(result.getValue());
308+
Assert.assertTrue(result.getValue().isStructure());
309+
310+
result.getValue().asStructure().asObjectMap().forEach((k, v) -> {
311+
Assert.assertTrue(jsonData.containsKey(k));
312+
Assert.assertEquals(jsonData.get(k), v);
313+
});
314+
315+
Assert.assertEquals(result.getReason(), "SPLIT");
316+
Assert.assertNull(result.getErrorCode());
317+
Assert.assertNotNull(result.getFlagMetadata());
318+
Assert.assertEquals(result.getFlagMetadata().getString("evalReasonDetails"), "User ID");
319+
Assert.assertEquals(result.getFlagMetadata().getString("evalReasonTargetId"), "json_target_id");
320+
}
252321
}

0 commit comments

Comments
 (0)