Skip to content

Commit 86a1614

Browse files
feat: eval reasons (#179)
* chore: update WASM Bucketing lib to 1.41.0 * feat: add eval reasons to Local Bucketing Client to support parsing from WASM and implements default Eval Reasons * chore: update Local Bucketing tests to verify Eval Reasons are properly set and returned * feat: add eval reasons to Cloud Bucketing Client * chore: add temporary override to Test Harness Capabilities * chore: refactor to ensure Default events before the SDK is initialized are tracked correctly * fix: properly set variable type mismatch as eval details on Cloud Bucketing Client * feat: adds support for allVariables calls to return eval reasons * chore: simplify error reasons to use ErrorMessage enum * chore: add _feature to Variable protos for encoding/decoding from WASM Bucketing * chore: add featureId as property on BaseVariable * fix: update EvalReason class to only parse non_null attributes
1 parent 84f5360 commit 86a1614

File tree

18 files changed

+443
-114
lines changed

18 files changed

+443
-114
lines changed

.github/workflows/run-test-harness.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,4 @@ jobs:
1616
with:
1717
sdks-to-test: java
1818
sdk-github-sha: ${{github.event.pull_request.head.sha}}
19-
github-token: ${{ secrets.TEST_HARNESS_GH_SECRET }}
20-
19+
sdk-capabilities: '{ "Java": ["cloud", "edgeDB", "clientCustomData", "v2Config", "allVariables", "allFeatures", "variablesFeatureId", "evalReason", "cloudEvalReason", "eventsEvalReason"]}'

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ sourceCompatibility = JavaVersion.VERSION_11
8484
targetCompatibility = JavaVersion.VERSION_11
8585

8686
def wasmResourcePath = "$projectDir/src/main/resources"
87-
def wasmVersion = "1.35.1"
87+
def wasmVersion = "1.41.0"
8888
def wasmUrl = "https://unpkg.com/@devcycle/bucketing-assembly-script@$wasmVersion/build/bucketing-lib.release.wasm"
8989
task downloadDVCBucketingWASM(type: Download) {
9090
src wasmUrl

src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudClient.java

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
package com.devcycle.sdk.server.cloud.api;
22

3+
import java.io.IOException;
4+
import java.util.ArrayList;
5+
import java.util.Collections;
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
import java.util.Optional;
9+
310
import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions;
411
import com.devcycle.sdk.server.common.api.IDevCycleApi;
512
import com.devcycle.sdk.server.common.api.IDevCycleClient;
@@ -8,19 +15,29 @@
815
import com.devcycle.sdk.server.common.exception.BeforeHookError;
916
import com.devcycle.sdk.server.common.exception.DevCycleException;
1017
import com.devcycle.sdk.server.common.logging.DevCycleLogger;
11-
import com.devcycle.sdk.server.common.model.*;
18+
import com.devcycle.sdk.server.common.model.BaseVariable;
19+
import com.devcycle.sdk.server.common.model.DevCycleEvent;
20+
import com.devcycle.sdk.server.common.model.DevCycleResponse;
21+
import com.devcycle.sdk.server.common.model.DevCycleUser;
22+
import com.devcycle.sdk.server.common.model.DevCycleUserAndEvents;
23+
import com.devcycle.sdk.server.common.model.ErrorResponse;
24+
import com.devcycle.sdk.server.common.model.EvalHook;
25+
import com.devcycle.sdk.server.common.model.EvalHooksRunner;
26+
import com.devcycle.sdk.server.common.model.EvalReason;
27+
import com.devcycle.sdk.server.common.model.Feature;
28+
import com.devcycle.sdk.server.common.model.HookContext;
29+
import com.devcycle.sdk.server.common.model.HttpResponseCode;
30+
import com.devcycle.sdk.server.common.model.Variable;
1231
import com.devcycle.sdk.server.common.model.Variable.TypeEnum;
1332
import com.devcycle.sdk.server.openfeature.DevCycleProvider;
1433
import com.fasterxml.jackson.core.JsonProcessingException;
1534
import com.fasterxml.jackson.databind.ObjectMapper;
1635
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
36+
1737
import dev.openfeature.sdk.FeatureProvider;
1838
import retrofit2.Call;
1939
import retrofit2.Response;
2040

21-
import java.io.IOException;
22-
import java.util.*;
23-
2441
public final class DevCycleCloudClient implements IDevCycleClient {
2542

2643
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperUtils.createDefaultObjectMapper();
@@ -100,13 +117,11 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {
100117
validateUser(user);
101118

102119
if (key == null || key.equals("")) {
103-
ErrorResponse errorResponse = new ErrorResponse(500, "Missing parameter: key", null);
104-
throw new IllegalArgumentException("Missing parameter: key");
120+
throw new IllegalArgumentException(ErrorResponse.ErrorMessage.MISSING_PARAMETER.getMessage("key"));
105121
}
106122

107123
if (defaultValue == null) {
108-
ErrorResponse errorResponse = new ErrorResponse(500, "Missing parameter: defaultValue", null);
109-
throw new IllegalArgumentException("Missing parameter: defaultValue");
124+
throw new IllegalArgumentException(ErrorResponse.ErrorMessage.MISSING_PARAMETER.getMessage("defaultValue"));
110125
}
111126

112127
TypeEnum variableType = TypeEnum.fromClass(defaultValue.getClass());
@@ -128,14 +143,14 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {
128143
Call<Variable> response = api.getVariableByKey(user, key, dvcOptions.getEnableEdgeDB());
129144
variable = getResponseWithRetries(response, 5);
130145
if (variable.getType() != variableType) {
131-
throw new IllegalArgumentException("Variable type mismatch, returning default value");
146+
throw new IllegalArgumentException(ErrorResponse.ErrorMessage.VARIABLE_TYPE_MISMATCH.getMessage());
132147
}
133148
if (beforeError != null) {
134149
throw beforeError;
135150
}
136151

137-
evalHooksRunner.executeAfter(reversedHooks, context, variable);
138152
variable.setIsDefaulted(false);
153+
evalHooksRunner.executeAfter(reversedHooks, context, variable);
139154
} catch (Throwable exception) {
140155
if (!(exception instanceof BeforeHookError || exception instanceof AfterHookError)) {
141156
variable = (Variable<T>) Variable.builder()
@@ -145,6 +160,12 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {
145160
.defaultValue(defaultValue)
146161
.isDefaulted(true)
147162
.build();
163+
164+
if (exception.getMessage().equals(ErrorResponse.ErrorMessage.VARIABLE_TYPE_MISMATCH.getMessage())) {
165+
variable.setEval(EvalReason.defaultReason(EvalReason.DefaultReasonDetailsEnum.VARIABLE_TYPE_MISMATCH));
166+
} else {
167+
variable.setEval(EvalReason.defaultReason(EvalReason.DefaultReasonDetailsEnum.ERROR));
168+
}
148169
}
149170

150171
evalHooksRunner.executeError(reversedHooks, context, exception);
@@ -204,7 +225,7 @@ public void track(DevCycleUser user, DevCycleEvent event) throws DevCycleExcepti
204225
validateUser(user);
205226

206227
if (event == null || event.getType() == null || event.getType().equals("")) {
207-
throw new IllegalArgumentException("Invalid DevCycleEvent");
228+
throw new IllegalArgumentException(ErrorResponse.ErrorMessage.INVALID_EVENT.getMessage());
208229
}
209230

210231
DevCycleUserAndEvents userAndEvents = DevCycleUserAndEvents.builder()
@@ -315,10 +336,10 @@ private boolean isValidServerKey(String serverKey) {
315336

316337
private void validateUser(DevCycleUser user) {
317338
if (user == null) {
318-
throw new IllegalArgumentException("DevCycleUser cannot be null");
339+
throw new IllegalArgumentException(ErrorResponse.ErrorMessage.NULL_USER.getMessage());
319340
}
320341
if (user.getUserId().equals("")) {
321-
throw new IllegalArgumentException("userId cannot be empty");
342+
throw new IllegalArgumentException(ErrorResponse.ErrorMessage.USER_ID_MISSING.getMessage());
322343
}
323344
}
324345
}

src/main/java/com/devcycle/sdk/server/common/api/IDevCycleApi.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
package com.devcycle.sdk.server.common.api;
22

3-
import com.devcycle.sdk.server.common.model.*;
3+
import java.util.Map;
4+
5+
import com.devcycle.sdk.server.common.model.BaseVariable;
6+
import com.devcycle.sdk.server.common.model.DevCycleResponse;
7+
import com.devcycle.sdk.server.common.model.DevCycleUser;
8+
import com.devcycle.sdk.server.common.model.DevCycleUserAndEvents;
9+
import com.devcycle.sdk.server.common.model.Feature;
10+
import com.devcycle.sdk.server.common.model.ProjectConfig;
11+
import com.devcycle.sdk.server.common.model.Variable;
412
import com.devcycle.sdk.server.local.model.EventsBatch;
5-
import retrofit2.Call;
6-
import retrofit2.http.*;
713

8-
import java.util.Map;
14+
import retrofit2.Call;
15+
import retrofit2.http.Body;
16+
import retrofit2.http.GET;
17+
import retrofit2.http.Header;
18+
import retrofit2.http.Headers;
19+
import retrofit2.http.POST;
20+
import retrofit2.http.Path;
21+
import retrofit2.http.Query;
922

1023
public interface IDevCycleApi {
1124
/**

src/main/java/com/devcycle/sdk/server/common/model/BaseVariable.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.devcycle.sdk.server.common.model.Variable.TypeEnum;
44
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
55
import com.fasterxml.jackson.annotation.JsonProperty;
6+
67
import io.swagger.v3.oas.annotations.media.Schema;
78
import lombok.AllArgsConstructor;
89
import lombok.Builder;
@@ -27,4 +28,11 @@ public class BaseVariable {
2728

2829
@Schema(required = true, description = "Variable value can be a string, number, boolean, or JSON")
2930
private Object value;
31+
32+
@Schema(description = "Evaluation reason")
33+
private EvalReason eval;
34+
35+
@Schema(description = "Feature ID")
36+
@JsonProperty("_feature")
37+
private String featureId;
3038
}

src/main/java/com/devcycle/sdk/server/common/model/ErrorResponse.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package com.devcycle.sdk.server.common.model;
1414

1515
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
16+
1617
import io.swagger.v3.oas.annotations.media.Schema;
1718
import lombok.AllArgsConstructor;
1819
import lombok.Builder;
@@ -33,4 +34,22 @@ public class ErrorResponse {
3334

3435
@Schema(description = "Additional error information detailing the error reasoning")
3536
private Object data;
37+
38+
public enum ErrorMessage {
39+
MISSING_PARAMETER("Missing parameter: %s"),
40+
NULL_USER("DevCycleUser cannot be null"),
41+
USER_ID_MISSING("userId cannot be empty"),
42+
INVALID_EVENT("Invalid DevCycleEvent"),
43+
VARIABLE_TYPE_MISMATCH("Variable type mismatch, returning default value");
44+
45+
private final String message;
46+
47+
ErrorMessage(String message) {
48+
this.message = message;
49+
}
50+
51+
public String getMessage(String... args) {
52+
return String.format(message, args);
53+
}
54+
}
3655
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.devcycle.sdk.server.common.model;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonInclude;
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import lombok.AllArgsConstructor;
9+
import lombok.Data;
10+
import lombok.RequiredArgsConstructor;
11+
12+
@Data
13+
@AllArgsConstructor
14+
@RequiredArgsConstructor
15+
@JsonIgnoreProperties(ignoreUnknown = true)
16+
@JsonInclude(JsonInclude.Include.NON_NULL)
17+
public class EvalReason {
18+
@Schema(description = "Evaluation reason", required = true)
19+
@JsonProperty("reason")
20+
private String reason;
21+
22+
@Schema(description = "Details")
23+
@JsonProperty("details")
24+
private String details;
25+
26+
@Schema(description = "Target ID")
27+
@JsonProperty("target_id")
28+
private String targetId;
29+
30+
private EvalReason(String reason, String details) {
31+
this.reason = reason;
32+
this.details = details;
33+
}
34+
35+
public static EvalReason defaultReason(DefaultReasonDetailsEnum details) {
36+
return new EvalReason("DEFAULT", details.getValue());
37+
}
38+
39+
public String getReason() {
40+
return reason == null ? "UNKNOWN" : reason;
41+
}
42+
43+
public enum DefaultReasonDetailsEnum {
44+
MISSING_CONFIG("Missing Config"),
45+
USER_NOT_TARGETED("User Not Targeted"),
46+
VARIABLE_TYPE_MISMATCH("Variable Type Mismatch"),
47+
ERROR("Error");
48+
49+
private final String value;
50+
51+
DefaultReasonDetailsEnum(String value) {
52+
this.value = value;
53+
}
54+
55+
public String getValue() {
56+
return value;
57+
}
58+
}
59+
}

src/main/java/com/devcycle/sdk/server/common/model/Variable.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package com.devcycle.sdk.server.common.model;
22

3+
import java.util.HashMap;
4+
import java.util.LinkedHashMap;
5+
6+
import com.fasterxml.jackson.annotation.JsonIgnore;
37
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
8+
import com.fasterxml.jackson.annotation.JsonProperty;
49
import com.fasterxml.jackson.annotation.JsonValue;
10+
511
import io.swagger.v3.oas.annotations.media.Schema;
612
import lombok.AllArgsConstructor;
713
import lombok.Builder;
814
import lombok.Data;
915
import lombok.NoArgsConstructor;
1016

11-
import java.util.HashMap;
12-
import java.util.LinkedHashMap;
13-
1417
@Data
1518
@Builder
1619
@AllArgsConstructor
@@ -32,6 +35,14 @@ public class Variable<T> {
3235
@Builder.Default
3336
private Boolean isDefaulted = false;
3437

38+
@Schema(description = "Evaluation reason")
39+
@JsonProperty("eval")
40+
private EvalReason eval;
41+
42+
@Deprecated()
43+
@JsonIgnore
44+
private final String evalReason = null;
45+
3546
public enum TypeEnum {
3647
STRING("String"),
3748
BOOLEAN("Boolean"),

0 commit comments

Comments
 (0)