diff --git a/.github/workflows/run-test-harness.yml b/.github/workflows/run-test-harness.yml index 4751a14c..3b087340 100644 --- a/.github/workflows/run-test-harness.yml +++ b/.github/workflows/run-test-harness.yml @@ -16,5 +16,4 @@ jobs: with: sdks-to-test: java sdk-github-sha: ${{github.event.pull_request.head.sha}} - github-token: ${{ secrets.TEST_HARNESS_GH_SECRET }} - + sdk-capabilities: '{ "Java": ["cloud", "edgeDB", "clientCustomData", "v2Config", "allVariables", "allFeatures", "variablesFeatureId", "evalReason", "cloudEvalReason", "eventsEvalReason"]}' diff --git a/build.gradle b/build.gradle index d53fe565..138dab85 100644 --- a/build.gradle +++ b/build.gradle @@ -84,7 +84,7 @@ sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 def wasmResourcePath = "$projectDir/src/main/resources" -def wasmVersion = "1.35.1" +def wasmVersion = "1.41.0" def wasmUrl = "https://unpkg.com/@devcycle/bucketing-assembly-script@$wasmVersion/build/bucketing-lib.release.wasm" task downloadDVCBucketingWASM(type: Download) { src wasmUrl diff --git a/src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudClient.java b/src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudClient.java index 465803a0..6ae551e8 100755 --- a/src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudClient.java +++ b/src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudClient.java @@ -1,5 +1,12 @@ package com.devcycle.sdk.server.cloud.api; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions; import com.devcycle.sdk.server.common.api.IDevCycleApi; import com.devcycle.sdk.server.common.api.IDevCycleClient; @@ -8,19 +15,29 @@ import com.devcycle.sdk.server.common.exception.BeforeHookError; import com.devcycle.sdk.server.common.exception.DevCycleException; import com.devcycle.sdk.server.common.logging.DevCycleLogger; -import com.devcycle.sdk.server.common.model.*; +import com.devcycle.sdk.server.common.model.BaseVariable; +import com.devcycle.sdk.server.common.model.DevCycleEvent; +import com.devcycle.sdk.server.common.model.DevCycleResponse; +import com.devcycle.sdk.server.common.model.DevCycleUser; +import com.devcycle.sdk.server.common.model.DevCycleUserAndEvents; +import com.devcycle.sdk.server.common.model.ErrorResponse; +import com.devcycle.sdk.server.common.model.EvalHook; +import com.devcycle.sdk.server.common.model.EvalHooksRunner; +import com.devcycle.sdk.server.common.model.EvalReason; +import com.devcycle.sdk.server.common.model.Feature; +import com.devcycle.sdk.server.common.model.HookContext; +import com.devcycle.sdk.server.common.model.HttpResponseCode; +import com.devcycle.sdk.server.common.model.Variable; import com.devcycle.sdk.server.common.model.Variable.TypeEnum; import com.devcycle.sdk.server.openfeature.DevCycleProvider; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.MismatchedInputException; + import dev.openfeature.sdk.FeatureProvider; import retrofit2.Call; import retrofit2.Response; -import java.io.IOException; -import java.util.*; - public final class DevCycleCloudClient implements IDevCycleClient { private static final ObjectMapper OBJECT_MAPPER = ObjectMapperUtils.createDefaultObjectMapper(); @@ -100,13 +117,11 @@ public Variable variable(DevCycleUser user, String key, T defaultValue) { validateUser(user); if (key == null || key.equals("")) { - ErrorResponse errorResponse = new ErrorResponse(500, "Missing parameter: key", null); - throw new IllegalArgumentException("Missing parameter: key"); + throw new IllegalArgumentException(ErrorResponse.ErrorMessage.MISSING_PARAMETER.getMessage("key")); } if (defaultValue == null) { - ErrorResponse errorResponse = new ErrorResponse(500, "Missing parameter: defaultValue", null); - throw new IllegalArgumentException("Missing parameter: defaultValue"); + throw new IllegalArgumentException(ErrorResponse.ErrorMessage.MISSING_PARAMETER.getMessage("defaultValue")); } TypeEnum variableType = TypeEnum.fromClass(defaultValue.getClass()); @@ -128,14 +143,14 @@ public Variable variable(DevCycleUser user, String key, T defaultValue) { Call response = api.getVariableByKey(user, key, dvcOptions.getEnableEdgeDB()); variable = getResponseWithRetries(response, 5); if (variable.getType() != variableType) { - throw new IllegalArgumentException("Variable type mismatch, returning default value"); + throw new IllegalArgumentException(ErrorResponse.ErrorMessage.VARIABLE_TYPE_MISMATCH.getMessage()); } if (beforeError != null) { throw beforeError; } - evalHooksRunner.executeAfter(reversedHooks, context, variable); variable.setIsDefaulted(false); + evalHooksRunner.executeAfter(reversedHooks, context, variable); } catch (Throwable exception) { if (!(exception instanceof BeforeHookError || exception instanceof AfterHookError)) { variable = (Variable) Variable.builder() @@ -145,6 +160,12 @@ public Variable variable(DevCycleUser user, String key, T defaultValue) { .defaultValue(defaultValue) .isDefaulted(true) .build(); + + if (exception.getMessage().equals(ErrorResponse.ErrorMessage.VARIABLE_TYPE_MISMATCH.getMessage())) { + variable.setEval(EvalReason.defaultReason(EvalReason.DefaultReasonDetailsEnum.VARIABLE_TYPE_MISMATCH)); + } else { + variable.setEval(EvalReason.defaultReason(EvalReason.DefaultReasonDetailsEnum.ERROR)); + } } evalHooksRunner.executeError(reversedHooks, context, exception); @@ -204,7 +225,7 @@ public void track(DevCycleUser user, DevCycleEvent event) throws DevCycleExcepti validateUser(user); if (event == null || event.getType() == null || event.getType().equals("")) { - throw new IllegalArgumentException("Invalid DevCycleEvent"); + throw new IllegalArgumentException(ErrorResponse.ErrorMessage.INVALID_EVENT.getMessage()); } DevCycleUserAndEvents userAndEvents = DevCycleUserAndEvents.builder() @@ -315,10 +336,10 @@ private boolean isValidServerKey(String serverKey) { private void validateUser(DevCycleUser user) { if (user == null) { - throw new IllegalArgumentException("DevCycleUser cannot be null"); + throw new IllegalArgumentException(ErrorResponse.ErrorMessage.NULL_USER.getMessage()); } if (user.getUserId().equals("")) { - throw new IllegalArgumentException("userId cannot be empty"); + throw new IllegalArgumentException(ErrorResponse.ErrorMessage.USER_ID_MISSING.getMessage()); } } } diff --git a/src/main/java/com/devcycle/sdk/server/common/api/IDevCycleApi.java b/src/main/java/com/devcycle/sdk/server/common/api/IDevCycleApi.java index bdc2e91f..58d41fec 100755 --- a/src/main/java/com/devcycle/sdk/server/common/api/IDevCycleApi.java +++ b/src/main/java/com/devcycle/sdk/server/common/api/IDevCycleApi.java @@ -1,11 +1,24 @@ package com.devcycle.sdk.server.common.api; -import com.devcycle.sdk.server.common.model.*; +import java.util.Map; + +import com.devcycle.sdk.server.common.model.BaseVariable; +import com.devcycle.sdk.server.common.model.DevCycleResponse; +import com.devcycle.sdk.server.common.model.DevCycleUser; +import com.devcycle.sdk.server.common.model.DevCycleUserAndEvents; +import com.devcycle.sdk.server.common.model.Feature; +import com.devcycle.sdk.server.common.model.ProjectConfig; +import com.devcycle.sdk.server.common.model.Variable; import com.devcycle.sdk.server.local.model.EventsBatch; -import retrofit2.Call; -import retrofit2.http.*; -import java.util.Map; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Headers; +import retrofit2.http.POST; +import retrofit2.http.Path; +import retrofit2.http.Query; public interface IDevCycleApi { /** diff --git a/src/main/java/com/devcycle/sdk/server/common/model/BaseVariable.java b/src/main/java/com/devcycle/sdk/server/common/model/BaseVariable.java index 1c0055c2..d9303a3f 100644 --- a/src/main/java/com/devcycle/sdk/server/common/model/BaseVariable.java +++ b/src/main/java/com/devcycle/sdk/server/common/model/BaseVariable.java @@ -3,6 +3,7 @@ import com.devcycle.sdk.server.common.model.Variable.TypeEnum; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; + import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; @@ -27,4 +28,11 @@ public class BaseVariable { @Schema(required = true, description = "Variable value can be a string, number, boolean, or JSON") private Object value; + + @Schema(description = "Evaluation reason") + private EvalReason eval; + + @Schema(description = "Feature ID") + @JsonProperty("_feature") + private String featureId; } diff --git a/src/main/java/com/devcycle/sdk/server/common/model/ErrorResponse.java b/src/main/java/com/devcycle/sdk/server/common/model/ErrorResponse.java index 24234421..0162f89c 100755 --- a/src/main/java/com/devcycle/sdk/server/common/model/ErrorResponse.java +++ b/src/main/java/com/devcycle/sdk/server/common/model/ErrorResponse.java @@ -13,6 +13,7 @@ package com.devcycle.sdk.server.common.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; @@ -33,4 +34,22 @@ public class ErrorResponse { @Schema(description = "Additional error information detailing the error reasoning") private Object data; + + public enum ErrorMessage { + MISSING_PARAMETER("Missing parameter: %s"), + NULL_USER("DevCycleUser cannot be null"), + USER_ID_MISSING("userId cannot be empty"), + INVALID_EVENT("Invalid DevCycleEvent"), + VARIABLE_TYPE_MISMATCH("Variable type mismatch, returning default value"); + + private final String message; + + ErrorMessage(String message) { + this.message = message; + } + + public String getMessage(String... args) { + return String.format(message, args); + } + } } diff --git a/src/main/java/com/devcycle/sdk/server/common/model/EvalReason.java b/src/main/java/com/devcycle/sdk/server/common/model/EvalReason.java new file mode 100644 index 00000000..e089b127 --- /dev/null +++ b/src/main/java/com/devcycle/sdk/server/common/model/EvalReason.java @@ -0,0 +1,59 @@ +package com.devcycle.sdk.server.common.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class EvalReason { + @Schema(description = "Evaluation reason", required = true) + @JsonProperty("reason") + private String reason; + + @Schema(description = "Details") + @JsonProperty("details") + private String details; + + @Schema(description = "Target ID") + @JsonProperty("target_id") + private String targetId; + + private EvalReason(String reason, String details) { + this.reason = reason; + this.details = details; + } + + public static EvalReason defaultReason(DefaultReasonDetailsEnum details) { + return new EvalReason("DEFAULT", details.getValue()); + } + + public String getReason() { + return reason == null ? "UNKNOWN" : reason; + } + + public enum DefaultReasonDetailsEnum { + MISSING_CONFIG("Missing Config"), + USER_NOT_TARGETED("User Not Targeted"), + VARIABLE_TYPE_MISMATCH("Variable Type Mismatch"), + ERROR("Error"); + + private final String value; + + DefaultReasonDetailsEnum(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/src/main/java/com/devcycle/sdk/server/common/model/Variable.java b/src/main/java/com/devcycle/sdk/server/common/model/Variable.java index 326842f6..a0dabc64 100755 --- a/src/main/java/com/devcycle/sdk/server/common/model/Variable.java +++ b/src/main/java/com/devcycle/sdk/server/common/model/Variable.java @@ -1,16 +1,19 @@ package com.devcycle.sdk.server.common.model; +import java.util.HashMap; +import java.util.LinkedHashMap; + +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; + import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.HashMap; -import java.util.LinkedHashMap; - @Data @Builder @AllArgsConstructor @@ -32,6 +35,14 @@ public class Variable { @Builder.Default private Boolean isDefaulted = false; + @Schema(description = "Evaluation reason") + @JsonProperty("eval") + private EvalReason eval; + + @Deprecated() + @JsonIgnore + private final String evalReason = null; + public enum TypeEnum { STRING("String"), BOOLEAN("Boolean"), diff --git a/src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalClient.java b/src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalClient.java index f9c2e05b..377bcd71 100755 --- a/src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalClient.java +++ b/src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalClient.java @@ -1,9 +1,25 @@ package com.devcycle.sdk.server.local.api; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + import com.devcycle.sdk.server.common.api.IDevCycleClient; import com.devcycle.sdk.server.common.exception.BeforeHookError; import com.devcycle.sdk.server.common.logging.DevCycleLogger; -import com.devcycle.sdk.server.common.model.*; +import com.devcycle.sdk.server.common.model.BaseVariable; +import com.devcycle.sdk.server.common.model.DevCycleEvent; +import com.devcycle.sdk.server.common.model.DevCycleUser; +import com.devcycle.sdk.server.common.model.ErrorResponse; +import com.devcycle.sdk.server.common.model.EvalHook; +import com.devcycle.sdk.server.common.model.EvalHooksRunner; +import com.devcycle.sdk.server.common.model.EvalReason; +import com.devcycle.sdk.server.common.model.Feature; +import com.devcycle.sdk.server.common.model.HookContext; +import com.devcycle.sdk.server.common.model.PlatformData; +import com.devcycle.sdk.server.common.model.Variable; import com.devcycle.sdk.server.common.model.Variable.TypeEnum; import com.devcycle.sdk.server.local.bucketing.LocalBucketing; import com.devcycle.sdk.server.local.managers.EnvironmentConfigManager; @@ -18,9 +34,8 @@ import com.devcycle.sdk.server.openfeature.DevCycleProvider; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import dev.openfeature.sdk.FeatureProvider; -import java.util.*; +import dev.openfeature.sdk.FeatureProvider; public final class DevCycleLocalClient implements IDevCycleClient { @@ -123,11 +138,11 @@ public Variable variable(DevCycleUser user, String key, T defaultValue) { validateUser(user); if (key == null || key.equals("")) { - throw new IllegalArgumentException("Missing parameter: key"); + throw new IllegalArgumentException(ErrorResponse.ErrorMessage.MISSING_PARAMETER.getMessage("key")); } if (defaultValue == null) { - throw new IllegalArgumentException("Missing parameter: defaultValue"); + throw new IllegalArgumentException(ErrorResponse.ErrorMessage.MISSING_PARAMETER.getMessage("defaultValue")); } TypeEnum variableType = TypeEnum.fromClass(defaultValue.getClass()); @@ -141,8 +156,14 @@ public Variable variable(DevCycleUser user, String key, T defaultValue) { if (!isInitialized()) { DevCycleLogger.info("Variable called before DevCycleLocalClient has initialized, returning default value"); + defaultVariable.setEval(EvalReason.defaultReason(EvalReason.DefaultReasonDetailsEnum.MISSING_CONFIG)); try { - eventQueueManager.queueAggregateEvent(DevCycleEvent.builder().type("aggVariableDefaulted").target(key).build(), null); + eventQueueManager.queueAggregateEvent(DevCycleEvent.builder() + .type("aggVariableDefaulted") + .target(key) + .metaData(Map.of("evalReason", defaultVariable.getEval().getReason())) + .build(), + null); } catch (Exception e) { DevCycleLogger.error("Unable to parse aggVariableDefaulted event for Variable " + key + " due to error: " + e, e); } @@ -177,11 +198,13 @@ public Variable variable(DevCycleUser user, String key, T defaultValue) { if (variableData == null || variableData.length == 0) { variable = defaultVariable; + variable.setEval(EvalReason.defaultReason(EvalReason.DefaultReasonDetailsEnum.USER_NOT_TARGETED)); } else { SDKVariable_PB sdkVariable = SDKVariable_PB.parseFrom(variableData); if (sdkVariable.getType() != pbVariableType) { DevCycleLogger.warning("Variable type mismatch, returning default value"); variable = defaultVariable; + variable.setEval(EvalReason.defaultReason(EvalReason.DefaultReasonDetailsEnum.VARIABLE_TYPE_MISMATCH)); } else { variable = ProtobufUtils.createVariable(sdkVariable, defaultValue); } @@ -200,6 +223,7 @@ public Variable variable(DevCycleUser user, String key, T defaultValue) { } finally { if (variable == null) { variable = defaultVariable; + variable.setEval(EvalReason.defaultReason(EvalReason.DefaultReasonDetailsEnum.USER_NOT_TARGETED)); } evalHooksRunner.executeFinally(reversedHooks, hookContext, Optional.of(variable)); } @@ -243,7 +267,7 @@ public void track(DevCycleUser user, DevCycleEvent event) { validateUser(user); if (event == null || event.getType().equals("")) { - throw new IllegalArgumentException("Invalid DevCycleEvent"); + throw new IllegalArgumentException(ErrorResponse.ErrorMessage.INVALID_EVENT.getMessage()); } try { @@ -324,10 +348,10 @@ public String getSDKPlatform() { private void validateUser(DevCycleUser user) { if (user == null) { - throw new IllegalArgumentException("DevCycleUser cannot be null"); + throw new IllegalArgumentException(ErrorResponse.ErrorMessage.NULL_USER.getMessage()); } if (user.getUserId().equals("")) { - throw new IllegalArgumentException("userId cannot be empty"); + throw new IllegalArgumentException(ErrorResponse.ErrorMessage.USER_ID_MISSING.getMessage()); } } diff --git a/src/main/java/com/devcycle/sdk/server/local/managers/EventQueueManager.java b/src/main/java/com/devcycle/sdk/server/local/managers/EventQueueManager.java index a26440b0..59f2089d 100644 --- a/src/main/java/com/devcycle/sdk/server/local/managers/EventQueueManager.java +++ b/src/main/java/com/devcycle/sdk/server/local/managers/EventQueueManager.java @@ -1,5 +1,11 @@ package com.devcycle.sdk.server.local.managers; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + import com.devcycle.sdk.server.common.api.IDevCycleApi; import com.devcycle.sdk.server.common.logging.DevCycleLogger; import com.devcycle.sdk.server.common.model.DevCycleEvent; @@ -13,15 +19,10 @@ import com.devcycle.sdk.server.local.model.FlushPayload; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; + import retrofit2.Call; import retrofit2.Response; -import java.io.IOException; -import java.util.Arrays; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - public class EventQueueManager { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -115,10 +116,13 @@ public void queueAggregateEvent(DevCycleEvent event, BucketedUserConfig bucketed return; } + String eventString = OBJECT_MAPPER.writeValueAsString(event); + if (bucketedConfig != null) { - this.localBucketing.queueAggregateEvent(this.sdkKey, OBJECT_MAPPER.writeValueAsString(event), OBJECT_MAPPER.writeValueAsString(bucketedConfig.variableVariationMap)); + String variableVariationMapString = OBJECT_MAPPER.writeValueAsString(bucketedConfig.variableVariationMap); + this.localBucketing.queueAggregateEvent(this.sdkKey, eventString, variableVariationMapString); } else { - this.localBucketing.queueAggregateEvent(this.sdkKey, OBJECT_MAPPER.writeValueAsString(event), "{}"); + this.localBucketing.queueAggregateEvent(this.sdkKey, eventString, "{}"); } } diff --git a/src/main/java/com/devcycle/sdk/server/local/utils/ProtobufUtils.java b/src/main/java/com/devcycle/sdk/server/local/utils/ProtobufUtils.java index 56b94526..7eb3457f 100644 --- a/src/main/java/com/devcycle/sdk/server/local/utils/ProtobufUtils.java +++ b/src/main/java/com/devcycle/sdk/server/local/utils/ProtobufUtils.java @@ -1,16 +1,25 @@ package com.devcycle.sdk.server.local.utils; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + import com.devcycle.sdk.server.common.model.DevCycleUser; +import com.devcycle.sdk.server.common.model.EvalReason; import com.devcycle.sdk.server.common.model.Variable; -import com.devcycle.sdk.server.local.protobuf.*; +import com.devcycle.sdk.server.local.protobuf.CustomDataType; +import com.devcycle.sdk.server.local.protobuf.CustomDataValue; +import com.devcycle.sdk.server.local.protobuf.DVCUser_PB; +import com.devcycle.sdk.server.local.protobuf.EvalReason_PB; +import com.devcycle.sdk.server.local.protobuf.NullableCustomData; +import com.devcycle.sdk.server.local.protobuf.NullableDouble; +import com.devcycle.sdk.server.local.protobuf.NullableString; +import com.devcycle.sdk.server.local.protobuf.SDKVariable_PB; +import com.devcycle.sdk.server.local.protobuf.VariableType_PB; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - public class ProtobufUtils { public static DVCUser_PB createDVCUserPB(DevCycleUser user) { double appBuild = Double.NaN; @@ -50,6 +59,7 @@ public static Variable createVariable(SDKVariable_PB sdkVariable, T defau .value(sdkVariable.getBoolValue()) .defaultValue(defaultValue) .isDefaulted(false) + .eval(convertToEvalReason(sdkVariable.getEval())) .build(); break; case String: @@ -59,6 +69,7 @@ public static Variable createVariable(SDKVariable_PB sdkVariable, T defau .value(sdkVariable.getStringValue()) .defaultValue(defaultValue) .isDefaulted(false) + .eval(convertToEvalReason(sdkVariable.getEval())) .build(); break; case Number: @@ -68,6 +79,7 @@ public static Variable createVariable(SDKVariable_PB sdkVariable, T defau .value(sdkVariable.getDoubleValue()) .defaultValue(defaultValue) .isDefaulted(false) + .eval(convertToEvalReason(sdkVariable.getEval())) .build(); break; case JSON: @@ -80,6 +92,7 @@ public static Variable createVariable(SDKVariable_PB sdkVariable, T defau .value(jsonObject) .defaultValue(defaultValue) .isDefaulted(false) + .eval(convertToEvalReason(sdkVariable.getEval())) .build(); break; default: @@ -143,5 +156,8 @@ public static VariableType_PB convertTypeEnumToVariableType(Variable.TypeEnum ty } } + public static EvalReason convertToEvalReason(EvalReason_PB eval) { + return new EvalReason(eval.getReason(), eval.getDetails(), eval.getTargetId()); + } } diff --git a/src/main/proto/variableForUserParams.proto b/src/main/proto/variableForUserParams.proto index 0c73af78..e79b9036 100644 --- a/src/main/proto/variableForUserParams.proto +++ b/src/main/proto/variableForUserParams.proto @@ -70,4 +70,12 @@ message SDKVariable_PB { double doubleValue = 5; string stringValue = 6; NullableString evalReason = 7; + NullableString _feature = 8; + EvalReason_PB eval = 9; } + +message EvalReason_PB { + string reason = 1; + string details = 2; + string target_id = 3; +} \ No newline at end of file diff --git a/src/main/resources/bucketing-lib.release.wasm b/src/main/resources/bucketing-lib.release.wasm index f3c33d6a..80994caa 100644 Binary files a/src/main/resources/bucketing-lib.release.wasm and b/src/main/resources/bucketing-lib.release.wasm differ diff --git a/src/test/java/com/devcycle/sdk/server/cloud/DevCycleCloudClientTest.java b/src/test/java/com/devcycle/sdk/server/cloud/DevCycleCloudClientTest.java index 3c28b336..7434d01d 100755 --- a/src/test/java/com/devcycle/sdk/server/cloud/DevCycleCloudClientTest.java +++ b/src/test/java/com/devcycle/sdk/server/cloud/DevCycleCloudClientTest.java @@ -1,32 +1,42 @@ package com.devcycle.sdk.server.cloud; -import com.devcycle.sdk.server.cloud.api.DevCycleCloudClient; -import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions; -import com.devcycle.sdk.server.common.api.DVCApiMock; -import com.devcycle.sdk.server.common.api.IDevCycleApi; -import com.devcycle.sdk.server.common.exception.DevCycleException; -import com.devcycle.sdk.server.common.model.*; -import com.devcycle.sdk.server.helpers.WhiteBox; -import dev.openfeature.sdk.Hook; -import net.bytebuddy.implementation.bytecode.Throw; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import retrofit2.Call; -import retrofit2.mock.Calls; +import static org.mockito.Mockito.when; import java.math.BigDecimal; import java.time.Instant; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.*; -import static org.mockito.Mockito.when; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.devcycle.sdk.server.cloud.api.DevCycleCloudClient; +import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions; +import com.devcycle.sdk.server.common.api.DVCApiMock; +import com.devcycle.sdk.server.common.api.IDevCycleApi; +import com.devcycle.sdk.server.common.exception.DevCycleException; +import com.devcycle.sdk.server.common.model.BaseVariable; +import com.devcycle.sdk.server.common.model.DevCycleEvent; +import com.devcycle.sdk.server.common.model.DevCycleUser; +import com.devcycle.sdk.server.common.model.DevCycleUserAndEvents; +import com.devcycle.sdk.server.common.model.EvalHook; +import com.devcycle.sdk.server.common.model.EvalReason; +import com.devcycle.sdk.server.common.model.Feature; +import com.devcycle.sdk.server.common.model.HookContext; +import com.devcycle.sdk.server.common.model.Meta; +import com.devcycle.sdk.server.common.model.PlatformData; +import com.devcycle.sdk.server.common.model.Variable; +import com.devcycle.sdk.server.helpers.WhiteBox; + +import retrofit2.mock.Calls; /** * API tests for DevcycleApi @@ -91,6 +101,39 @@ public void getVariableByKeyTest() { assertUserDefaultsCorrect(user); Assert.assertFalse(variable.getValue()); + + EvalReason varEval = variable.getEval(); + Assert.assertEquals("TARGETING_MATCH", varEval.getReason()); + Assert.assertEquals("All Users", varEval.getDetails()); + Assert.assertEquals("test_cloud_target_id", varEval.getTargetId()); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + } + + @Test + public void getVariableTypeMismatchTest() { + DevCycleUser user = DevCycleUser.builder() + .userId("j_test") + .build(); + + String key = "type-mismatch"; + + when(apiInterface.getVariableByKey(user, key, dvcOptions.getEnableEdgeDB())).thenReturn(dvcApiMock.getVariableByKey(user, key, dvcOptions.getEnableEdgeDB())); + + Variable variable; + try { + variable = api.variable(user, key, true); + + assertUserDefaultsCorrect(user); + + Assert.assertTrue(variable.getValue()); + Assert.assertTrue(variable.getIsDefaulted()); + + EvalReason varEval = variable.getEval(); + Assert.assertEquals("DEFAULT", varEval.getReason()); + Assert.assertEquals("Variable Type Mismatch", varEval.getDetails()); + Assert.assertNull(varEval.getTargetId()); } catch (IllegalArgumentException e) { e.printStackTrace(); } @@ -118,7 +161,7 @@ public void getVariableValueByKeyTest() { } @Test - public void getVariablesTest() throws DevCycleException { + public void allVariablesTest() throws DevCycleException { DevCycleUser user = DevCycleUser.builder() .userId("j_test") .build(); @@ -130,6 +173,15 @@ public void getVariablesTest() throws DevCycleException { assertUserDefaultsCorrect(user); Assert.assertNotNull(variables); + Assert.assertEquals(4, variables.size()); + BaseVariable testNumber = variables.get("test-number"); + Assert.assertNotNull(testNumber); + Assert.assertEquals(100, (int) testNumber.getValue()); + Assert.assertEquals("62fbf6566f1ba302829f9e32", testNumber.getFeatureId()); + Assert.assertEquals(Variable.TypeEnum.NUMBER, testNumber.getType()); + Assert.assertEquals("SPLIT", testNumber.getEval().getReason()); + Assert.assertEquals("Random Distribution | User Id", testNumber.getEval().getDetails()); + Assert.assertEquals("test_cloud_target_id", testNumber.getEval().getTargetId()); } @Test @@ -567,6 +619,7 @@ public void onFinally(HookContext ctx, Optional> variab .type(Variable.TypeEnum.STRING) .isDefaulted(false) .defaultValue("default string") + .eval(new EvalReason("TARGETING_MATCH", "All Users", "test_target_id")) .build(); when(apiInterface.getVariableByKey(user, "test-string", dvcOptions.getEnableEdgeDB())).thenReturn(Calls.response(expected)); diff --git a/src/test/java/com/devcycle/sdk/server/common/api/DVCApiMock.java b/src/test/java/com/devcycle/sdk/server/common/api/DVCApiMock.java index d866c255..244e4a8d 100644 --- a/src/test/java/com/devcycle/sdk/server/common/api/DVCApiMock.java +++ b/src/test/java/com/devcycle/sdk/server/common/api/DVCApiMock.java @@ -1,11 +1,18 @@ package com.devcycle.sdk.server.common.api; -import com.devcycle.sdk.server.common.model.*; +import java.util.Map; + +import com.devcycle.sdk.server.common.model.BaseVariable; +import com.devcycle.sdk.server.common.model.DevCycleResponse; +import com.devcycle.sdk.server.common.model.DevCycleUser; +import com.devcycle.sdk.server.common.model.DevCycleUserAndEvents; +import com.devcycle.sdk.server.common.model.Feature; +import com.devcycle.sdk.server.common.model.ProjectConfig; +import com.devcycle.sdk.server.common.model.Variable; import com.devcycle.sdk.server.helpers.TestResponse; import com.devcycle.sdk.server.local.model.EventsBatch; -import retrofit2.Call; -import java.util.Map; +import retrofit2.Call; public class DVCApiMock implements IDevCycleApi { @@ -16,7 +23,7 @@ public Call> getFeatures(DevCycleUser user, Boolean enabled @Override public Call getVariableByKey(DevCycleUser user, String key, Boolean enabledEdgeDB) { - return TestResponse.getVariableByKey(); + return TestResponse.getVariableByKey(key); } @Override diff --git a/src/test/java/com/devcycle/sdk/server/helpers/TestResponse.java b/src/test/java/com/devcycle/sdk/server/helpers/TestResponse.java index 7df4ec19..05adbdc5 100644 --- a/src/test/java/com/devcycle/sdk/server/helpers/TestResponse.java +++ b/src/test/java/com/devcycle/sdk/server/helpers/TestResponse.java @@ -1,16 +1,18 @@ package com.devcycle.sdk.server.helpers; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + import com.devcycle.sdk.server.common.model.BaseVariable; import com.devcycle.sdk.server.common.model.DevCycleResponse; +import com.devcycle.sdk.server.common.model.EvalReason; import com.devcycle.sdk.server.common.model.Feature; import com.devcycle.sdk.server.common.model.Variable; + import retrofit2.Call; import retrofit2.mock.Calls; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - public final class TestResponse { private TestResponse() { @@ -55,36 +57,55 @@ public static Call> getVariables() { .key("test-false") .value(false) .type(Variable.TypeEnum.BOOLEAN) + .eval(new EvalReason("SPLIT", "Random Distribution | User Id", "test_cloud_target_id")) + .featureId("62fbf6566f1ba302829f9e32") .build()); variables.put("test-true", BaseVariable.builder() .id(UUID.randomUUID().toString()) .key("test-true") .value(true) .type(Variable.TypeEnum.BOOLEAN) + .eval(new EvalReason("SPLIT", "Random Distribution | User Id", "test_cloud_target_id")) + .featureId("62fbf6566f1ba302829f9e32") .build()); variables.put("test-number", BaseVariable.builder() .id(UUID.randomUUID().toString()) .key("test-number") .value(100) .type(Variable.TypeEnum.NUMBER) + .eval(new EvalReason("SPLIT", "Random Distribution | User Id", "test_cloud_target_id")) + .featureId("62fbf6566f1ba302829f9e32") .build()); variables.put("test-json", BaseVariable.builder() .id(UUID.randomUUID().toString()) .key("test-json") .value("{'some':'json''}") .type(Variable.TypeEnum.JSON) + .eval(new EvalReason("SPLIT", "Random Distribution | User Id", "test_cloud_target_id")) + .featureId("62fbf6566f1ba302829f9e32") .build()); return Calls.response(variables); } @SuppressWarnings("unchecked") - public static Call getVariableByKey() { - Variable variable = (Variable) Variable.builder() + public static Call getVariableByKey(String key) { + Variable variable = null; + if (key.equals("type-mismatch")) { + variable = (Variable) Variable.builder() + .key("type-mismatch") + .value(100) + .type(Variable.TypeEnum.NUMBER) + .eval(new EvalReason("TARGETING_MATCH", "All Users", "test_cloud_target_id")) + .build(); + } else { + variable = (Variable) Variable.builder() .key("test-false") .value(false) .type(Variable.TypeEnum.BOOLEAN) + .eval(new EvalReason("TARGETING_MATCH", "All Users", "test_cloud_target_id")) .build(); + } return Calls.response(variable); } diff --git a/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java b/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java index 50ae80b6..30156a65 100644 --- a/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java +++ b/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java @@ -1,39 +1,43 @@ package com.devcycle.sdk.server.local; -import com.devcycle.sdk.server.cloud.api.DevCycleCloudClient; -import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + import com.devcycle.sdk.server.common.api.IRestOptions; import com.devcycle.sdk.server.common.exception.DevCycleException; import com.devcycle.sdk.server.common.logging.IDevCycleLogger; -import com.devcycle.sdk.server.common.model.*; +import com.devcycle.sdk.server.common.model.BaseVariable; +import com.devcycle.sdk.server.common.model.DevCycleEvent; +import com.devcycle.sdk.server.common.model.DevCycleUser; +import com.devcycle.sdk.server.common.model.EvalHook; +import com.devcycle.sdk.server.common.model.EvalReason; +import com.devcycle.sdk.server.common.model.Feature; +import com.devcycle.sdk.server.common.model.HookContext; +import com.devcycle.sdk.server.common.model.Variable; import com.devcycle.sdk.server.helpers.LocalConfigServer; -import com.devcycle.sdk.server.local.model.Project; -import com.devcycle.sdk.server.local.model.Environment; import com.devcycle.sdk.server.helpers.TestDataFixtures; -import com.devcycle.sdk.server.helpers.WhiteBox; import com.devcycle.sdk.server.local.api.DevCycleLocalClient; import com.devcycle.sdk.server.local.model.ConfigMetadata; import com.devcycle.sdk.server.local.model.DevCycleLocalOptions; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.After; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; -import retrofit2.mock.Calls; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.*; - -import static org.mockito.Mockito.when; +import com.devcycle.sdk.server.local.model.Environment; +import com.devcycle.sdk.server.local.model.Project; @RunWith(MockitoJUnitRunner.class) public class DevCycleLocalClientTest { @@ -151,6 +155,11 @@ public void variableTest() { var = client.variable(user, "string-var", "default string"); Assert.assertNotNull(var); Assert.assertEquals("variationOn", var.getValue()); + + EvalReason varEval = var.getEval(); + Assert.assertEquals("TARGETING_MATCH", varEval.getReason()); + Assert.assertEquals("All Users", varEval.getDetails()); + Assert.assertEquals("63125321d31c601f992288bc", varEval.getTargetId()); } @Test @@ -162,6 +171,11 @@ public void variableBooleanValueTest() { Assert.assertNotNull(var); Assert.assertFalse(var.getIsDefaulted()); Assert.assertEquals(true, var.getValue()); + + EvalReason varEval = var.getEval(); + Assert.assertEquals("TARGETING_MATCH", varEval.getReason()); + Assert.assertEquals("All Users", varEval.getDetails()); + Assert.assertEquals("63125321d31c601f992288bc", varEval.getTargetId()); } @Test @@ -172,6 +186,11 @@ public void variableNumberValueTest() { Assert.assertNotNull(var); Assert.assertFalse(var.getIsDefaulted()); Assert.assertEquals(12345.0, var.getValue().doubleValue(), 0.0); + + EvalReason varEval = var.getEval(); + Assert.assertEquals("TARGETING_MATCH", varEval.getReason()); + Assert.assertEquals("All Users", varEval.getDetails()); + Assert.assertEquals("63125321d31c601f992288bc", varEval.getTargetId()); } @Test @@ -192,6 +211,11 @@ public void variableJsonValueTest() { Assert.assertEquals("This variation is on", variableData.get("displayText")); Assert.assertEquals(true, variableData.get("showDialog")); Assert.assertEquals(100, variableData.get("maxUsers")); + + EvalReason varEval = var.getEval(); + Assert.assertEquals("TARGETING_MATCH", varEval.getReason()); + Assert.assertEquals("All Users", varEval.getDetails()); + Assert.assertEquals("63125321d31c601f992288bc", varEval.getTargetId()); } @Test @@ -203,6 +227,7 @@ public void variableTestNotInitialized() { Assert.assertNotNull(var); Assert.assertTrue(var.getIsDefaulted()); Assert.assertEquals("default string", var.getValue()); + Assert.assertEquals(EvalReason.DefaultReasonDetailsEnum.MISSING_CONFIG.getValue(), var.getEval().getDetails()); } @Test @@ -226,6 +251,11 @@ public void variableTestWithCustomData() { Assert.assertNotNull(var); Assert.assertFalse(var.getIsDefaulted()); Assert.assertEquals("variationOn", var.getValue()); + + EvalReason varEval = var.getEval(); + Assert.assertEquals("TARGETING_MATCH", varEval.getReason()); + Assert.assertEquals("All Users", varEval.getDetails()); + Assert.assertEquals("63125321d31c601f992288bc", varEval.getTargetId()); } @Test @@ -243,6 +273,11 @@ public void variableTestBucketingWithCustomData() { Assert.assertNotNull(var); Assert.assertFalse(var.getIsDefaulted()); Assert.assertEquals("↑↑↓↓←→←→BA 🤖", var.getValue()); + + EvalReason varEval = var.getEval(); + Assert.assertEquals("TARGETING_MATCH", varEval.getReason()); + Assert.assertEquals("Custom Data -> should-bucket", varEval.getDetails()); + Assert.assertEquals("638680d659f1b81cc9e6c5ab", varEval.getTargetId()); } @Test @@ -251,6 +286,11 @@ public void variableTestUnknownVariableKey() { Assert.assertNotNull(var); Assert.assertTrue(var.getIsDefaulted()); Assert.assertEquals(true, var.getValue()); + + EvalReason varEval = var.getEval(); + Assert.assertEquals("DEFAULT", varEval.getReason()); + Assert.assertEquals("User Not Targeted", varEval.getDetails()); + Assert.assertNull(varEval.getTargetId()); } @Test @@ -259,6 +299,11 @@ public void variableTestTypeMismatch() { Assert.assertNotNull(var); Assert.assertTrue(var.getIsDefaulted()); Assert.assertEquals(true, var.getValue()); + + EvalReason varEval = var.getEval(); + Assert.assertEquals("DEFAULT", varEval.getReason()); + Assert.assertEquals("User Not Targeted", varEval.getDetails()); + Assert.assertNull(varEval.getTargetId()); } @Test @@ -332,6 +377,11 @@ public void allVariablesTest() { Assert.assertEquals(variables.get("num-var").getId(), "65272363125123fca69d3a7d"); Assert.assertEquals(variables.get("json-var").getId(), "64372363125123fca69d3f7b"); Assert.assertEquals(variables.size(), 4); + + Assert.assertEquals(variables.get("string-var").getFeatureId(), "62fbf6566f1ba302829f9e32"); + Assert.assertEquals(variables.get("a-cool-new-feature").getFeatureId(), "62fbf6566f1ba302829f9e32"); + Assert.assertEquals(variables.get("num-var").getFeatureId(), "62fbf6566f1ba302829f9e32"); + Assert.assertEquals(variables.get("json-var").getFeatureId(), "62fbf6566f1ba302829f9e32"); } @Test @@ -371,6 +421,11 @@ public void SetClientCustomDataWithBucketingTest() { Assert.assertNotNull(var); Assert.assertFalse(var.getIsDefaulted()); Assert.assertEquals("↑↑↓↓←→←→BA 🤖", var.getValue()); + + EvalReason varEval = var.getEval(); + Assert.assertEquals("TARGETING_MATCH", varEval.getReason()); + Assert.assertEquals("Custom Data -> should-bucket", varEval.getDetails()); + Assert.assertEquals("638680d659f1b81cc9e6c5ab", varEval.getTargetId()); } @Test @@ -664,6 +719,7 @@ public void onFinally(HookContext ctx, Optional> variab .type(Variable.TypeEnum.STRING) .isDefaulted(false) .defaultValue("default string") + .eval(new EvalReason("TARGETING_MATCH", "All Users", "63125321d31c601f992288bc")) .build(); Variable result = client.variable(user, "string-var", "default string"); @@ -934,6 +990,7 @@ public void error(HookContext ctx, Throwable e) { .type(Variable.TypeEnum.STRING) .isDefaulted(false) .defaultValue("default string") + .eval(new EvalReason("TARGETING_MATCH", "All Users", "63125321d31c601f992288bc")) .build(); Variable result = client.variable(user, "string-var", "default string"); diff --git a/src/test/java/com/devcycle/sdk/server/local/LocalBucketingTest.java b/src/test/java/com/devcycle/sdk/server/local/LocalBucketingTest.java index 25ad3f4b..b8bf04a8 100644 --- a/src/test/java/com/devcycle/sdk/server/local/LocalBucketingTest.java +++ b/src/test/java/com/devcycle/sdk/server/local/LocalBucketingTest.java @@ -1,5 +1,15 @@ package com.devcycle.sdk.server.local; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + import com.devcycle.sdk.server.common.model.DevCycleEvent; import com.devcycle.sdk.server.common.model.DevCycleUser; import com.devcycle.sdk.server.common.model.PlatformData; @@ -8,15 +18,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; @RunWith(MockitoJUnitRunner.class) public class LocalBucketingTest { @@ -73,13 +74,21 @@ public void testStoreConfig() { @Test public void testEventQueue() throws JsonProcessingException { - DevCycleEvent event = DevCycleEvent.builder().type("test").target("target").build(); + DevCycleEvent customEvent = DevCycleEvent.builder() + .type("test") + .target("target") + .build(); + DevCycleEvent aggEvent = DevCycleEvent.builder() + .type("aggVariableDefaulted") + .target("string-var") + .metaData(Map.of("evalReason", "DEFAULT")) + .build(); localBucketing.initEventQueue(apiKey, UUID.randomUUID().toString(),"{}"); // Add 2 events, aggregated by same target (should create 1 event with eventCount 2) - localBucketing.queueEvent(apiKey, mapper.writeValueAsString(getUser()), mapper.writeValueAsString(event)); - localBucketing.queueAggregateEvent(apiKey, mapper.writeValueAsString(event), varMap); + localBucketing.queueEvent(apiKey, mapper.writeValueAsString(getUser()), mapper.writeValueAsString(customEvent)); + localBucketing.queueAggregateEvent(apiKey, mapper.writeValueAsString(aggEvent), varMap); FlushPayload[] payloads = localBucketing.flushEventQueue(apiKey); Assert.assertEquals(payloads.length, 1); Assert.assertEquals(payloads[0].eventCount, 2); @@ -99,7 +108,7 @@ public void testEventQueue() throws JsonProcessingException { Assert.assertEquals(payloads.length, 0); // failed events deleted // Add another event - localBucketing.queueEvent(apiKey, mapper.writeValueAsString(getUser()), mapper.writeValueAsString(event)); + localBucketing.queueEvent(apiKey, mapper.writeValueAsString(getUser()), mapper.writeValueAsString(customEvent)); payloads = localBucketing.flushEventQueue(apiKey); Assert.assertEquals(payloads.length, 1);