diff --git a/.gitignore b/.gitignore index 2aa7171..83583d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ +src/test/resources/shared .gradle build/ +target/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ @@ -41,5 +43,3 @@ bin/ ### Mac OS ### .DS_Store .idea/ -src/test/resources/assignment-v2/ -src/test/resources/rac-experiments-v3.json diff --git a/Makefile b/Makefile index 1a06534..f5041c8 100644 --- a/Makefile +++ b/Makefile @@ -29,22 +29,20 @@ build: test-data ./gradlew assemble ## test-data -testDataDir := src/test/resources -banditsDataDir := ${testDataDir}/bandits +testDataDir := src/test/resources/shared tempDir := ${testDataDir}/temp -tempBanditsDir := ${tempDir}/bandits gitDataDir := ${tempDir}/sdk-test-data branchName := main githubRepoLink := https://github.com/Eppo-exp/sdk-test-data.git .PHONY: test-data test-data: - find ${testDataDir} -mindepth 1 ! -regex '^${banditsDataDir}.*' -delete + rm -rf $(testDataDir) mkdir -p ${tempDir} git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${gitDataDir} - cp ${gitDataDir}/rac-experiments-v3.json ${testDataDir} - cp -r ${gitDataDir}/assignment-v2 ${testDataDir} + cp -r ${gitDataDir}/ufc ${testDataDir} + rm ${testDataDir}/ufc/bandit-tests/*.dynamic-typing.json rm -rf ${tempDir} .PHONY: test test: test-data build - ./gradlew check + ./gradlew check --no-daemon diff --git a/build.gradle b/build.gradle index aaff44f..ea1e46a 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group = 'cloud.eppo' -version = '2.5.0-SNAPSHOT' +version = '3.0.0-SNAPSHOT' ext.isReleaseVersion = !version.endsWith("SNAPSHOT") import org.apache.tools.ant.filters.ReplaceTokens @@ -25,7 +25,7 @@ repositories { } dependencies { - implementation 'cloud.eppo:sdk-common-jvm:2.0.0-SNAPSHOT' + implementation 'cloud.eppo:sdk-common-jvm:3.0.1-SNAPSHOT' implementation 'com.github.zafarkhaja:java-semver:0.10.2' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' implementation 'org.apache.httpcomponents:httpclient:4.5.14' @@ -34,15 +34,23 @@ dependencies { // Logback classic 1.3.x is compatible with java 8 implementation 'ch.qos.logback:logback-classic:1.3.14' - testImplementation 'org.projectlombok:lombok:1.18.24' + testImplementation 'cloud.eppo:sdk-common-jvm:3.0.0-SNAPSHOT:tests' testImplementation platform('org.junit:junit-bom:5.10.2') testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'com.github.tomakehurst:wiremock-jre8:2.35.2' testImplementation 'org.mockito:mockito-core:4.11.0' + testImplementation 'com.squareup.okhttp3:okhttp:4.9.1' } test { useJUnitPlatform() + testLogging { + events "started", "passed", "skipped", "failed" + exceptionFormat "full" + showExceptions true + showCauses true + showStackTraces true + } } spotless { diff --git a/src/main/java/com/eppo/sdk/EppoClient.java b/src/main/java/com/eppo/sdk/EppoClient.java index 3773045..78b0609 100644 --- a/src/main/java/com/eppo/sdk/EppoClient.java +++ b/src/main/java/com/eppo/sdk/EppoClient.java @@ -1,589 +1,131 @@ package com.eppo.sdk; -import cloud.eppo.ShardUtils; -import cloud.eppo.Utils; -import cloud.eppo.rac.Constants; -import cloud.eppo.rac.dto.*; -import cloud.eppo.rac.exception.EppoClientIsNotInitializedException; -import cloud.eppo.rac.exception.InvalidInputException; -import com.eppo.sdk.helpers.*; -import com.eppo.sdk.helpers.bandit.BanditEvaluator; -import com.fasterxml.jackson.databind.JsonNode; -import java.util.*; -import java.util.stream.Collectors; -import org.ehcache.Cache; +import cloud.eppo.BaseEppoClient; +import cloud.eppo.logging.AssignmentLogger; +import cloud.eppo.logging.BanditLogger; +import com.eppo.sdk.helpers.AppDetails; +import com.eppo.sdk.helpers.FetchConfigurationsTask; +import java.util.Timer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class EppoClient { +public class EppoClient extends BaseEppoClient { private static final Logger log = LoggerFactory.getLogger(EppoClient.class); - private static EppoClient instance = null; - private final ConfigurationStore configurationStore; - private final Timer poller; - private final EppoClientConfig eppoClientConfig; - private EppoClient( - ConfigurationStore configurationStore, Timer poller, EppoClientConfig eppoClientConfig) { - this.configurationStore = configurationStore; - this.poller = poller; - this.eppoClientConfig = eppoClientConfig; - } - - /** This function is used to get assignment Value */ - protected Optional getAssignmentValue( - String subjectKey, - String flagKey, - EppoAttributes subjectAttributes, - Map actionsWithAttributes) { - Optional assignedVariation = - getAssignmentVariation(subjectKey, flagKey, subjectAttributes, actionsWithAttributes); - return assignedVariation.map(Variation::getTypedValue); - } - - /** Returns the assigned variation. */ - public Optional getAssignmentVariation( - String subjectKey, String flagKey, EppoAttributes subjectAttributes) { - return getAssignmentVariation(subjectKey, flagKey, subjectAttributes, null); - } + private static final String DEFAULT_HOST = "https://fscdn.eppo.cloud"; + private static final boolean DEFAULT_IS_GRACEFUL_MODE = true; + private static final boolean DEFAULT_FORCE_REINITIALIZE = false; + private static final long DEFAULT_POLLING_INTERVAL_MS = 30 * 1000; + private static final long DEFAULT_JITTER_INTERVAL_RATIO = 10; - /** Returns the assigned variation. */ - protected Optional getAssignmentVariation( - String subjectKey, - String flagKey, - EppoAttributes subjectAttributes, - Map actionsWithAttributes) { - // Validate Input Values - InputValidator.validateNotBlank(subjectKey, "Invalid argument: subjectKey cannot be blank"); - InputValidator.validateNotBlank(flagKey, "Invalid argument: flagKey cannot be blank"); + private static EppoClient instance; + private static Timer pollTimer; - VariationAssignmentResult assignmentResult = - this.getAssignedVariation(flagKey, subjectKey, subjectAttributes); - - if (assignmentResult == null) { - return Optional.empty(); + public static EppoClient getInstance() { + if (instance == null) { + throw new IllegalStateException("Eppo SDK has not been initialized"); } + return instance; + } - Variation assignedVariation = assignmentResult.getVariation(); - - // Below is used for logging - String experimentKey = assignmentResult.getExperimentKey(); - String allocationKey = assignmentResult.getAllocationKey(); - String assignedVariationString = assignedVariation.getTypedValue().stringValue(); - AlgorithmType algorithmType = assignedVariation.getAlgorithmType(); - - if (algorithmType == AlgorithmType.OVERRIDE) { - // Assigned variation was from an override; return its value without logging - return Optional.of(assignedVariation); - } else if (algorithmType == AlgorithmType.CONTEXTUAL_BANDIT) { - // Assigned variation is a bandit; need to use the bandit to determine its value - Optional banditValue = - this.determineAndLogBanditAction(assignmentResult, actionsWithAttributes); - assignedVariation.setTypedValue(banditValue.orElse(null)); - } + private EppoClient( + String apiKey, + String host, + String sdkName, + String sdkVersion, + AssignmentLogger assignmentLogger, + BanditLogger banditLogger, + boolean isGracefulModel) { + super( + apiKey, host, sdkName, sdkVersion, assignmentLogger, banditLogger, isGracefulModel, false); + } - // Log the assignment - try { - this.eppoClientConfig - .getAssignmentLogger() - .logAssignment( - new AssignmentLogData( - experimentKey, - flagKey, - allocationKey, - assignedVariationString, - subjectKey, - subjectAttributes)); - } catch (Exception e) { - log.warn("Error logging assignment", e); + public static void stopPolling() { + if (pollTimer != null) { + pollTimer.cancel(); } - - return Optional.of(assignedVariation); } - private VariationAssignmentResult getAssignedVariation( - String flagKey, String subjectKey, EppoAttributes subjectAttributes) { + public static class Builder { + private String apiKey; + private String host = DEFAULT_HOST; + private AssignmentLogger assignmentLogger; + private BanditLogger banditLogger; + private boolean isGracefulMode = DEFAULT_IS_GRACEFUL_MODE; + private boolean forceReinitialize = DEFAULT_FORCE_REINITIALIZE; + private long pollingIntervalMs = DEFAULT_POLLING_INTERVAL_MS; - // Fetch Experiment Configuration - ExperimentConfiguration configuration = - this.configurationStore.getExperimentConfiguration(flagKey); - if (configuration == null) { - log.warn("[Eppo SDK] No configuration found for key: {}", flagKey); - return null; + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; } - // Check if subject has override variations - EppoValue subjectVariationOverride = - this.getSubjectVariationOverride(subjectKey, configuration); - if (!subjectVariationOverride.isNull()) { - // Create placeholder variation for the override - Variation overrideVariation = - new Variation(null, subjectVariationOverride, null, AlgorithmType.OVERRIDE); - return new VariationAssignmentResult(overrideVariation); + public Builder host(String host) { + this.host = host; + return this; } - // Check if disabled - if (!configuration.isEnabled()) { - log.info( - "[Eppo SDK] No assigned variation because the experiment or feature flag {} is disabled", - flagKey); - return null; + public Builder assignmentLogger(AssignmentLogger assignmentLogger) { + this.assignmentLogger = assignmentLogger; + return this; } - // Find matched rule - Optional rule = - RuleValidator.findMatchingRule(subjectAttributes, configuration.getRules()); - if (!rule.isPresent()) { - log.info( - "[Eppo SDK] No assigned variation. The subject attributes did not match any targeting rules"); - return null; + public Builder banditLogger(BanditLogger banditLogger) { + this.banditLogger = banditLogger; + return this; } - // Check if in experiment sample - String allocationKey = rule.get().getAllocationKey(); - Allocation allocation = configuration.getAllocation(allocationKey); - int subjectShards = configuration.getSubjectShards(); - if (!this.isInExperimentSample( - subjectKey, flagKey, subjectShards, allocation.getPercentExposure())) { - log.info( - "[Eppo SDK] No assigned variation. The subject is not part of the sample population"); - return null; + public Builder isGracefulMode(boolean isGracefulMode) { + this.isGracefulMode = isGracefulMode; + return this; } - List variations = allocation.getVariations(); - - String experimentKey = ExperimentHelper.generateKey(flagKey, allocationKey); // Used for logging - - // Get assigned variation - String assignmentKey = "assignment-" + subjectKey + "-" + flagKey; - Variation assignedVariation = - VariationHelper.selectVariation(assignmentKey, subjectShards, variations); - - return new VariationAssignmentResult( - assignedVariation, - subjectKey, - subjectAttributes, - flagKey, - allocationKey, - experimentKey, - subjectShards); - } - - private Optional determineAndLogBanditAction( - VariationAssignmentResult assignmentResult, Map assignmentOptions) { - String banditName = assignmentResult.getVariation().getTypedValue().stringValue(); - - String banditKey = assignmentResult.getVariation().getTypedValue().stringValue(); - BanditParameters banditParameters = this.configurationStore.getBanditParameters(banditKey); - - List actionVariations = - BanditEvaluator.evaluateBanditActions( - assignmentResult.getExperimentKey(), - banditParameters, - assignmentOptions, - assignmentResult.getSubjectKey(), - assignmentResult.getSubjectAttributes(), - assignmentResult.getSubjectShards()); - - String actionSelectionKey = - "bandit-" - + banditName - + "-" - + assignmentResult.getSubjectKey() - + "-" - + assignmentResult.getFlagKey(); - Variation selectedAction = - VariationHelper.selectVariation( - actionSelectionKey, assignmentResult.getSubjectShards(), actionVariations); - - EppoValue actionValue = selectedAction.getTypedValue(); - String actionString = actionValue.stringValue(); - double actionProbability = - VariationHelper.variationProbability(selectedAction, assignmentResult.getSubjectShards()); - - if (this.eppoClientConfig.getBanditLogger() != null) { - // Do bandit-specific logging - - String modelVersionToLog = - "uninitialized"; // Default model "version" if we have not seen this bandit before or - // don't have model parameters for it - if (banditParameters != null) { - modelVersionToLog = - banditParameters.getModelName() + " " + banditParameters.getModelVersion(); - } - - // Get the action-related attributes - EppoAttributes actionAttributes = new EppoAttributes(); - if (assignmentOptions != null && !assignmentOptions.isEmpty()) { - actionAttributes = assignmentOptions.get(actionString); - } - - Map subjectNumericAttributes = - numericAttributes(assignmentResult.getSubjectAttributes()); - Map subjectCategoricalAttributes = - categoricalAttributes(assignmentResult.getSubjectAttributes()); - Map actionNumericAttributes = numericAttributes(actionAttributes); - Map actionCategoricalAttributes = categoricalAttributes(actionAttributes); - - this.eppoClientConfig - .getBanditLogger() - .logBanditAction( - new BanditLogData( - assignmentResult.getExperimentKey(), - banditName, - assignmentResult.getSubjectKey(), - actionString, - actionProbability, - modelVersionToLog, - subjectNumericAttributes, - subjectCategoricalAttributes, - actionNumericAttributes, - actionCategoricalAttributes)); + public Builder forceReinitialize(boolean forceReinitialize) { + this.forceReinitialize = forceReinitialize; + return this; } - return Optional.of(actionValue); - } - - /** This function will return typed assignment value */ - private Optional getTypedAssignment( - EppoValueType type, - String subjectKey, - String experimentKey, - EppoAttributes subjectAttributes, - Map actionsWithAttributes) { - try { - Optional value = - this.getAssignmentValue( - subjectKey, experimentKey, subjectAttributes, actionsWithAttributes); - if (!value.isPresent()) { - return Optional.empty(); - } - - EppoValue eppoValue = value.get(); - - switch (type) { - case NUMBER: - return Optional.of(eppoValue.doubleValue()); - case BOOLEAN: - return Optional.of(eppoValue.boolValue()); - case ARRAY_OF_STRING: - return Optional.of(eppoValue.arrayValue()); - case JSON_NODE: - return Optional.of(eppoValue.jsonNodeValue()); - default: // strings and null - return Optional.of(eppoValue.stringValue()); - } - } catch (Exception e) { - // if graceful mode - if (this.eppoClientConfig.isGracefulMode()) { - log.warn("[Eppo SDK] Error getting assignment value: {}", e.getMessage()); - return Optional.empty(); - } - throw e; + public Builder pollingIntervalMs(long pollingIntervalMs) { + this.pollingIntervalMs = pollingIntervalMs; + return this; } - } - - /** This function will return string assignment value */ - public Optional getAssignment( - String subjectKey, String experimentKey, EppoAttributes subjectAttributes) { - return this.getStringAssignment(subjectKey, experimentKey, subjectAttributes); - } - - /** This function will return string assignment value without passing subjectAttributes */ - public Optional getAssignment(String subjectKey, String experimentKey) { - return this.getStringAssignment(subjectKey, experimentKey, new EppoAttributes()); - } - - /** - * Maps a subject to a variation for a given flag/experiment. - * - * @param subjectKey identifier of the experiment subject, for example a user ID. - * @param flagKey flagKey feature flag, bandit, or experiment identifier - * @return the variation string assigned to the subject, or null if an unrecoverable error was - * encountered. - */ - public Optional getStringAssignment(String subjectKey, String flagKey) { - return this.getStringAssignment(subjectKey, flagKey, new EppoAttributes()); - } - - /** - * Maps a subject to a variation for a given flag/experiment. - * - * @param subjectKey identifier of the experiment subject, for example a user ID. - * @param flagKey flagKey feature flag, bandit, or experiment identifier - * @param subjectAttributes optional attributes associated with the subject, for example name, - * email, account age, etc. The subject attributes are used for evaluating any targeting rules - * as well as weighting assignment choices for bandits. - * @return the variation string assigned to the subject, or null if an unrecoverable error was - * encountered. - */ - public Optional getStringAssignment( - String subjectKey, String flagKey, EppoAttributes subjectAttributes) { - return (Optional) - this.getTypedAssignment( - EppoValueType.STRING, subjectKey, flagKey, subjectAttributes, new HashMap<>()); - } - - /** - * Maps a subject to a variation for a given flag/experiment that has bandit variation. - * - * @param subjectKey identifier of the experiment subject, for example a user ID. - * @param flagKey flagKey feature flag, bandit, or experiment identifier - * @param subjectAttributes optional attributes associated with the subject, for example name, - * email, account age, etc. The subject attributes are used for evaluating any targeting rules - * as well as weighting assignment choices for bandits. - * @param actions used by bandits to know the actions (potential assignments) available. - * @return the variation string assigned to the subject, or null if an unrecoverable error was - * encountered. - */ - public Optional getBanditAssignment( - String subjectKey, String flagKey, EppoAttributes subjectAttributes, Set actions) { - Map actionsWithEmptyAttributes = - actions.stream().collect(Collectors.toMap(key -> key, value -> new EppoAttributes())); - return this.getBanditAssignment( - subjectKey, flagKey, subjectAttributes, actionsWithEmptyAttributes); - } - - /** - * Maps a subject to a variation for a given flag/experiment that contains a bandit variation. - * - * @param subjectKey identifier of the experiment subject, for example a user ID. - * @param flagKey flagKey feature flag, bandit, or experiment identifier - * @param subjectAttributes optional attributes associated with the subject, for example name, - * email, account age, etc. The subject attributes are used for evaluating any targeting rules - * as well as weighting assignment choices for bandits. - * @param actionsWithAttributes used by bandits to know the actions (assignment options) available - * and any attributes associated with that option. - * @return the variation string assigned to the subject, or null if an unrecoverable error was - * encountered. - */ - public Optional getBanditAssignment( - String subjectKey, - String flagKey, - EppoAttributes subjectAttributes, - Map actionsWithAttributes) { - @SuppressWarnings("unchecked") - Optional typedAssignment = - (Optional) - this.getTypedAssignment( - EppoValueType.STRING, - subjectKey, - flagKey, - subjectAttributes, - actionsWithAttributes); - return typedAssignment; - } - - /** - * This function will return boolean assignment value - * - * @param subjectKey - * @param experimentKey - * @param subjectAttributes - * @return - */ - public Optional getBooleanAssignment( - String subjectKey, String experimentKey, EppoAttributes subjectAttributes) { - return (Optional) - this.getTypedAssignment( - EppoValueType.BOOLEAN, subjectKey, experimentKey, subjectAttributes, null); - } - /** This function will return boolean assignment value without passing subjectAttributes */ - public Optional getBooleanAssignment(String subjectKey, String experimentKey) { - return this.getBooleanAssignment(subjectKey, experimentKey, new EppoAttributes()); - } - - /** This function will return double assignment value */ - public Optional getDoubleAssignment( - String subjectKey, String experimentKey, EppoAttributes subjectAttributes) { - return (Optional) - this.getTypedAssignment( - EppoValueType.NUMBER, subjectKey, experimentKey, subjectAttributes, null); - } + public EppoClient buildAndInit() { + AppDetails appDetails = AppDetails.getInstance(); + String sdkName = appDetails.getName(); + String sdkVersion = appDetails.getVersion(); - /** This function will return long assignment value without passing subjectAttributes */ - public Optional getDoubleAssignment(String subjectKey, String experimentKey) { - return this.getDoubleAssignment(subjectKey, experimentKey, new EppoAttributes()); - } - - /** This function will return json string assignment value */ - public Optional getJSONStringAssignment( - String subjectKey, String experimentKey, EppoAttributes subjectAttributes) { - return this.getStringAssignment(subjectKey, experimentKey, subjectAttributes); - } - - /** This function will return json string assignment value without passing subjectAttributes */ - public Optional getJSONStringAssignment(String subjectKey, String experimentKey) { - return this.getJSONStringAssignment(subjectKey, experimentKey, new EppoAttributes()); - } - - /** This function will return JSON assignment value */ - public Optional getParsedJSONAssignment( - String subjectKey, String experimentKey, EppoAttributes subjectAttributes) { - return (Optional) - this.getTypedAssignment( - EppoValueType.JSON_NODE, subjectKey, experimentKey, subjectAttributes, null); - } - - /** This function will return JSON assignment value without passing subjectAttributes */ - public Optional getParsedJSONAssignment(String subjectKey, String experimentKey) { - return this.getParsedJSONAssignment(subjectKey, experimentKey, new EppoAttributes()); - } - - /** This function is used to check if the Experiment is in the same */ - private boolean isInExperimentSample( - String subjectKey, String experimentKey, int subjectShards, double percentageExposure) { - int shard = ShardUtils.getShard("exposure-" + subjectKey + "-" + experimentKey, subjectShards); - return shard <= percentageExposure * subjectShards; - } - - /** This function is used to get override variations. */ - private EppoValue getSubjectVariationOverride( - String subjectKey, ExperimentConfiguration experimentConfiguration) { - String hexedSubjectKey = Utils.getMD5Hex(subjectKey); - return experimentConfiguration - .getTypedOverrides() - .getOrDefault(hexedSubjectKey, EppoValue.nullValue()); - } - - /** - * * Logs an action taken that was not selected by the bandit. Useful for full transparency on - * what users experienced. - * - * @param subjectKey subjectKey identifier of the experiment subject, for example a user ID. - * @param flagKey feature flag, bandit, or experiment identifier - * @param subjectAttributes optional attributes associated with the subject, for example name, - * email, account age, etc. - * @param actionString name of the action taken for the subject - * @param actionAttributes attributes associated with the given action - * @return null if no exception was encountered by logging; otherwise, the encountered exception - */ - public Exception logNonBanditAction( - String subjectKey, - String flagKey, - EppoAttributes subjectAttributes, - String actionString, - EppoAttributes actionAttributes) { - Exception loggingException = null; - try { - VariationAssignmentResult assignmentResult = - this.getAssignedVariation(flagKey, subjectKey, subjectAttributes); - - if (assignmentResult == null) { - // No bandit at play - return null; + if (instance != null) { + if (forceReinitialize) { // TODO: unit test this + log.warn( + "Eppo SDK is already initialized, reinitializing since forceReinitialize is true"); + } else { + log.warn( + "Eppo SDK is already initialized, skipping reinitialization since forceReinitialize is false"); + return instance; + } } - String variationValue = assignmentResult.getVariation().getTypedValue().toString(); - - Map subjectNumericAttributes = numericAttributes(subjectAttributes); - Map subjectCategoricalAttributes = categoricalAttributes(subjectAttributes); - Map actionNumericAttributes = numericAttributes(actionAttributes); - Map actionCategoricalAttributes = categoricalAttributes(actionAttributes); - - this.eppoClientConfig - .getBanditLogger() - .logBanditAction( - new BanditLogData( - assignmentResult.getExperimentKey(), - variationValue, - subjectKey, - actionString, - null, - null, - subjectNumericAttributes, - subjectCategoricalAttributes, - actionNumericAttributes, - actionCategoricalAttributes)); - } catch (Exception ex) { - loggingException = ex; - } - return loggingException; - } - - private Map numericAttributes(EppoAttributes eppoAttributes) { - if (eppoAttributes == null) { - return new HashMap<>(); - } - return eppoAttributes.entrySet().stream() - .filter(e -> e.getValue().isNumeric()) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().doubleValue())); - } - - private Map categoricalAttributes(EppoAttributes eppoAttributes) { - if (eppoAttributes == null) { - return new HashMap<>(); - } - return eppoAttributes.entrySet().stream() - .filter(e -> !e.getValue().isNumeric() && !e.getValue().isNull()) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())); - } + instance = + new EppoClient( + apiKey, sdkName, sdkVersion, host, assignmentLogger, banditLogger, isGracefulMode); - /** * This function is used to initialize the Eppo Client */ - public static synchronized EppoClient init(EppoClientConfig eppoClientConfig) { - InputValidator.validateNotBlank(eppoClientConfig.getApiKey(), "An API key is required"); - if (eppoClientConfig.getAssignmentLogger() == null) { - throw new InvalidInputException("An assignment logging implementation is required"); - } - // Create eppo http client - AppDetails appDetails = AppDetails.getInstance(); - EppoHttpClient eppoHttpClient = - new EppoHttpClient( - eppoClientConfig.getApiKey(), - appDetails.getName(), - appDetails.getVersion(), - eppoClientConfig.getBaseUrl(), - Constants.REQUEST_TIMEOUT_MILLIS); + // Stop any active polling + stopPolling(); - // Create wrapper for fetching experiment and bandit configuration - ConfigurationRequestor expConfigRequestor = - new ConfigurationRequestor<>( - ExperimentConfigurationResponse.class, eppoHttpClient, Constants.RAC_ENDPOINT); - ConfigurationRequestor banditParametersRequestor = - new ConfigurationRequestor<>( - BanditParametersResponse.class, eppoHttpClient, Constants.BANDIT_ENDPOINT); - // Create Caching for Experiment Configuration and Bandit Parameters - CacheHelper cacheHelper = new CacheHelper(); - Cache experimentConfigurationCache = - cacheHelper.createExperimentConfigurationCache(Constants.MAX_CACHE_ENTRIES); - Cache banditParametersCache = - cacheHelper.createBanditParameterCache(Constants.MAX_CACHE_ENTRIES); - // Create ExperimentConfiguration Store - ConfigurationStore configurationStore = - ConfigurationStore.init( - experimentConfigurationCache, - expConfigRequestor, - banditParametersCache, - banditParametersRequestor); + // Set up polling for experiment configurations + pollTimer = new Timer(true); + FetchConfigurationsTask fetchConfigurationsTask = + new FetchConfigurationsTask( + () -> instance.loadConfiguration(), + pollTimer, + pollingIntervalMs, + pollingIntervalMs / DEFAULT_JITTER_INTERVAL_RATIO); - // Stop the polling process of any previously initialized client - if (instance != null) { - instance.poller.cancel(); - } + // Kick off the first fetch + fetchConfigurationsTask.run(); - // Start polling for experiment configurations - Timer poller = new Timer(true); - FetchConfigurationsTask fetchConfigurationsTask = - new FetchConfigurationsTask( - configurationStore, - poller, - Constants.TIME_INTERVAL_IN_MILLIS, - Constants.JITTER_INTERVAL_IN_MILLIS); - fetchConfigurationsTask.run(); - instance = new EppoClient(configurationStore, poller, eppoClientConfig); - return instance; - } - - /** - * This function is used to get EppoClient instance - * - * @throws EppoClientIsNotInitializedException - */ - public static EppoClient getInstance() throws EppoClientIsNotInitializedException { - if (instance == null) { - throw new EppoClientIsNotInitializedException("Eppo client is not initialized!"); + return instance; } - - return instance; } } diff --git a/src/main/java/com/eppo/sdk/helpers/AppDetails.java b/src/main/java/com/eppo/sdk/helpers/AppDetails.java index d028ef9..531679d 100644 --- a/src/main/java/com/eppo/sdk/helpers/AppDetails.java +++ b/src/main/java/com/eppo/sdk/helpers/AppDetails.java @@ -26,7 +26,7 @@ public AppDetails() { } catch (Exception ex) { log.warn("Unable to read properties file", ex); } - this.version = prop.getProperty("app.version", "1.0.0"); + this.version = prop.getProperty("app.version", "3.0.0"); this.name = prop.getProperty("app.name", "java-server-sdk"); } diff --git a/src/main/java/com/eppo/sdk/helpers/CacheHelper.java b/src/main/java/com/eppo/sdk/helpers/CacheHelper.java deleted file mode 100644 index b84d183..0000000 --- a/src/main/java/com/eppo/sdk/helpers/CacheHelper.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.eppo.sdk.helpers; - -import cloud.eppo.rac.Constants; -import cloud.eppo.rac.dto.BanditParameters; -import cloud.eppo.rac.dto.ExperimentConfiguration; -import org.ehcache.Cache; -import org.ehcache.CacheManager; -import org.ehcache.config.builders.CacheConfigurationBuilder; -import org.ehcache.config.builders.CacheManagerBuilder; -import org.ehcache.config.builders.ResourcePoolsBuilder; - -/** CacheHelper class */ -public class CacheHelper { - private final CacheManager cacheManager; - - public CacheHelper() { - this.cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(); - this.cacheManager.init(); - } - - /** Create caching for Experiment Configuration */ - public Cache createExperimentConfigurationCache(int maxEntries) { - return this.cacheManager.createCache( - Constants.EXPERIMENT_CONFIGURATION_CACHE_KEY, - CacheConfigurationBuilder.newCacheConfigurationBuilder( - String.class, ExperimentConfiguration.class, ResourcePoolsBuilder.heap(maxEntries))); - } - - public Cache createBanditParameterCache(int maxEntries) { - return this.cacheManager.createCache( - Constants.BANDIT_PARAMETER_CACHE_KEY, - CacheConfigurationBuilder.newCacheConfigurationBuilder( - String.class, BanditParameters.class, ResourcePoolsBuilder.heap(maxEntries))); - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/ConfigurationRequestor.java b/src/main/java/com/eppo/sdk/helpers/ConfigurationRequestor.java deleted file mode 100644 index ddbd352..0000000 --- a/src/main/java/com/eppo/sdk/helpers/ConfigurationRequestor.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.eppo.sdk.helpers; - -import cloud.eppo.rac.exception.InvalidApiKeyException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.Optional; -import org.apache.http.HttpResponse; -import org.apache.http.util.EntityUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ConfigurationRequestor { - private static final ObjectMapper OBJECT_MAPPER = - new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - private static final Logger log = LoggerFactory.getLogger(ConfigurationRequestor.class); - private final Class responseClass; - private final EppoHttpClient eppoHttpClient; - private final String endpoint; - - public ConfigurationRequestor( - Class responseClass, EppoHttpClient eppoHttpClient, String endpoint) { - this.responseClass = responseClass; - this.eppoHttpClient = eppoHttpClient; - this.endpoint = endpoint; - } - - public Optional fetchConfiguration() { - T config = null; - try { - HttpResponse response = this.eppoHttpClient.get(this.endpoint); - int statusCode = response.getStatusLine().getStatusCode(); - if (statusCode == 200) { - config = - OBJECT_MAPPER.readValue(EntityUtils.toString(response.getEntity()), this.responseClass); - } - if (statusCode == 401) { // unauthorized - invalid API key - throw new InvalidApiKeyException("Unauthorized: invalid Eppo API key."); - } - } catch (InvalidApiKeyException e) { - throw e; - } catch (Exception e) { - log.warn("Unable to Fetch Experiment Configuration: {}", e.getMessage()); - } - - return Optional.ofNullable(config); - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/ConfigurationStore.java b/src/main/java/com/eppo/sdk/helpers/ConfigurationStore.java deleted file mode 100644 index fa1b40c..0000000 --- a/src/main/java/com/eppo/sdk/helpers/ConfigurationStore.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.eppo.sdk.helpers; - -import cloud.eppo.rac.dto.*; -import cloud.eppo.rac.exception.ExperimentConfigurationNotFound; -import cloud.eppo.rac.exception.NetworkException; -import cloud.eppo.rac.exception.NetworkRequestNotAllowed; -import java.util.Map; -import java.util.Optional; -import org.ehcache.Cache; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Configuration Store Class */ -public class ConfigurationStore { - private static final Logger log = LoggerFactory.getLogger(ConfigurationStore.class); - Cache experimentConfigurationCache; - Cache banditParametersCache; - ConfigurationRequestor experimentConfigurationRequestor; - ConfigurationRequestor banditParametersRequestor; - static ConfigurationStore instance = null; - - public ConfigurationStore( - Cache experimentConfigurationCache, - ConfigurationRequestor experimentConfigurationRequestor, - Cache banditParametersCache, - ConfigurationRequestor banditParametersRequestor) { - this.experimentConfigurationRequestor = experimentConfigurationRequestor; - this.experimentConfigurationCache = experimentConfigurationCache; - this.banditParametersCache = banditParametersCache; - this.banditParametersRequestor = banditParametersRequestor; - } - - public static ConfigurationStore init( - Cache experimentConfigurationCache, - ConfigurationRequestor experimentConfigurationRequestor, - Cache banditParametersCache, - ConfigurationRequestor banditParametersRequestor) { - if (ConfigurationStore.instance == null) { - ConfigurationStore.instance = - new ConfigurationStore( - experimentConfigurationCache, - experimentConfigurationRequestor, - banditParametersCache, - banditParametersRequestor); - } - instance.experimentConfigurationCache.clear(); - return ConfigurationStore.instance; - } - - /** This function is used to get initialized instance */ - public static ConfigurationStore getInstance() { - return ConfigurationStore.instance; - } - - /** - * This function is used to set experiment configuration to cache - * - * @param experimentConfiguration - */ - protected void setExperimentConfiguration( - String key, ExperimentConfiguration experimentConfiguration) { - this.experimentConfigurationCache.put(key, experimentConfiguration); - } - - /** - * This function is used to fetch experiment configuration - * - * @throws ExperimentConfigurationNotFound - */ - public ExperimentConfiguration getExperimentConfiguration(String key) - throws ExperimentConfigurationNotFound { - try { - return this.experimentConfigurationCache.get(key); - } catch (Exception e) { - throw new ExperimentConfigurationNotFound("Experiment configuration not found!"); - } - } - - public BanditParameters getBanditParameters(String banditKey) { - return this.banditParametersCache.get(banditKey); - } - - /** - * This function is used to set experiment configuration int the cache - * - * @throws NetworkException - * @throws NetworkRequestNotAllowed - */ - public void fetchAndSetExperimentConfiguration() - throws NetworkException, NetworkRequestNotAllowed { - Optional response = - this.experimentConfigurationRequestor.fetchConfiguration(); - - boolean loadBandits = false; - if (response.isPresent()) { - for (Map.Entry entry : - response.get().getFlags().entrySet()) { - ExperimentConfiguration configuration = entry.getValue(); - this.setExperimentConfiguration(entry.getKey(), configuration); - boolean hasBanditVariation = - configuration.getAllocations().values().stream() - .anyMatch( - a -> - a.getVariations().stream() - .anyMatch( - v -> v.getAlgorithmType() == AlgorithmType.CONTEXTUAL_BANDIT)); - - if (configuration.isEnabled() && hasBanditVariation) { - loadBandits = true; - } - } - } - - if (loadBandits) { - Optional banditResponse = - this.banditParametersRequestor.fetchConfiguration(); - if (!banditResponse.isPresent() || banditResponse.get().getBandits() == null) { - log.warn("Unexpected empty bandit parameter response"); - return; - } - for (Map.Entry entry : - banditResponse.get().getBandits().entrySet()) { - this.banditParametersCache.put(entry.getKey(), entry.getValue()); - } - } - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/Converter.java b/src/main/java/com/eppo/sdk/helpers/Converter.java deleted file mode 100644 index fe0df65..0000000 --- a/src/main/java/com/eppo/sdk/helpers/Converter.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.eppo.sdk.helpers; - -import java.util.List; -import java.util.stream.Collectors; - -public class Converter { - public static List convertToDecimal(List input) { - return input.stream().map(Double::parseDouble).collect(Collectors.toList()); - } - - public static List convertToBoolean(List input) { - return input.stream().map(Boolean::parseBoolean).collect(Collectors.toList()); - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/EppoHttpClient.java b/src/main/java/com/eppo/sdk/helpers/EppoHttpClient.java deleted file mode 100644 index d3b7d6c..0000000 --- a/src/main/java/com/eppo/sdk/helpers/EppoHttpClient.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.eppo.sdk.helpers; - -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.HttpClientBuilder; - -/** Eppo Http Client Class */ -public class EppoHttpClient { - private final Map defaultParams = new HashMap<>(); - private final String baseUrl; - private int requestTimeOutMillis = 3000; // 3 secs - private final RequestConfig config = - RequestConfig.custom() - .setConnectTimeout(requestTimeOutMillis) - .setConnectionRequestTimeout(requestTimeOutMillis) - .setSocketTimeout(requestTimeOutMillis) - .build(); - - private final HttpClient httpClient = - HttpClientBuilder.create().setDefaultRequestConfig(config).build(); - - public EppoHttpClient(String apikey, String sdkName, String sdkVersion, String baseUrl) { - this.defaultParams.put("apiKey", apikey); - this.defaultParams.put("sdkName", sdkName); - this.defaultParams.put("sdkVersion", sdkVersion); - this.baseUrl = baseUrl; - } - - public EppoHttpClient( - String apikey, String sdkName, String sdkVersion, String baseUrl, int requestTimeOutMillis) { - this.defaultParams.put("apiKey", apikey); - this.defaultParams.put("sdkName", sdkName); - this.defaultParams.put("sdkVersion", sdkVersion); - this.baseUrl = baseUrl; - this.requestTimeOutMillis = requestTimeOutMillis; - } - - /** - * This function is used to add default query parameters - * - * @param key - * @param value - */ - public void addDefaultParam(String key, String value) { - this.defaultParams.put(key, value); - } - - /** - * This function is used to make get request - * - * @param url - * @param params - * @param headers - * @return - * @throws Exception - */ - public HttpResponse get(String url, Map params, Map headers) - throws Exception { - // Merge both the query parameters - Map allParams = - Stream.of(params, this.defaultParams) - .flatMap(map -> map.entrySet().stream()) - .collect( - Collectors.toMap( - Map.Entry::getKey, Map.Entry::getValue, (value1, value2) -> value1)); - - // Build URL - final String newUrl = this.urlBuilder(this.baseUrl + url, allParams); - HttpGet getRequest = new HttpGet(newUrl); - - for (Map.Entry entry : headers.entrySet()) { - getRequest.setHeader(entry.getKey(), entry.getValue()); - } - - return httpClient.execute(getRequest); - } - - /** - * This function is used to make a get request - * - * @param url - * @param params - * @return - * @throws Exception - */ - public HttpResponse get(String url, Map params) throws Exception { - return this.get(url, params, new HashMap<>()); - } - - /** - * This function is used to make get request - * - * @param url - * @return - * @throws Exception - */ - public HttpResponse get(String url) throws Exception { - return this.get(url, new HashMap<>(), new HashMap<>()); - } - - /** - * This function is used to build url with query parameters - * - * @param url - * @param params - * @return - */ - public String urlBuilder(String url, Map params) { - StringBuilder newUrlBuilder = new StringBuilder(url); - boolean isFirst = true; - for (Map.Entry entry : params.entrySet()) { - if (isFirst) { - newUrlBuilder.append("?"); - } - isFirst = false; - newUrlBuilder.append(entry.getKey()); - newUrlBuilder.append("="); - newUrlBuilder.append(entry.getValue()); - newUrlBuilder.append("&"); - } - - return newUrlBuilder.toString(); - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/ExperimentHelper.java b/src/main/java/com/eppo/sdk/helpers/ExperimentHelper.java deleted file mode 100644 index 443dbc9..0000000 --- a/src/main/java/com/eppo/sdk/helpers/ExperimentHelper.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.eppo.sdk.helpers; - -public class ExperimentHelper { - public static String generateKey(String flagKey, String allocationKey) { - return flagKey + '-' + allocationKey; - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/FetchConfigurationsTask.java b/src/main/java/com/eppo/sdk/helpers/FetchConfigurationsTask.java index 86356af..be49821 100644 --- a/src/main/java/com/eppo/sdk/helpers/FetchConfigurationsTask.java +++ b/src/main/java/com/eppo/sdk/helpers/FetchConfigurationsTask.java @@ -7,17 +7,14 @@ public class FetchConfigurationsTask extends TimerTask { private static final Logger log = LoggerFactory.getLogger(FetchConfigurationsTask.class); - private final ConfigurationStore configurationStore; + private final Runnable runnable; private final Timer timer; private final long intervalInMillis; private final long jitterInMillis; public FetchConfigurationsTask( - ConfigurationStore configurationStore, - Timer timer, - long intervalInMillis, - long jitterInMillis) { - this.configurationStore = configurationStore; + Runnable runnable, Timer timer, long intervalInMillis, long jitterInMillis) { + this.runnable = runnable; this.timer = timer; this.intervalInMillis = intervalInMillis; this.jitterInMillis = jitterInMillis; @@ -25,20 +22,16 @@ public FetchConfigurationsTask( @Override public void run() { - // Uncaught runtime exceptions will prevent this task from being rescheduled. - // As a result, the SDK will continue functioning using the in-memory cache, but will never - // attempt - // to synchronize with Eppo Cloud again. // TODO: retry on failed fetches try { - configurationStore.fetchAndSetExperimentConfiguration(); + runnable.run(); } catch (Exception e) { log.error("[Eppo SDK] Error fetching experiment configuration", e); } long delay = this.intervalInMillis - (long) (Math.random() * this.jitterInMillis); FetchConfigurationsTask nextTask = - new FetchConfigurationsTask(configurationStore, timer, intervalInMillis, jitterInMillis); + new FetchConfigurationsTask(runnable, timer, intervalInMillis, jitterInMillis); timer.schedule(nextTask, delay); } } diff --git a/src/main/java/com/eppo/sdk/helpers/IPollerTask.java b/src/main/java/com/eppo/sdk/helpers/IPollerTask.java index d424fac..8b13789 100644 --- a/src/main/java/com/eppo/sdk/helpers/IPollerTask.java +++ b/src/main/java/com/eppo/sdk/helpers/IPollerTask.java @@ -1,6 +1 @@ -package com.eppo.sdk.helpers; -/** Poller Task Interface */ -public interface IPollerTask { - boolean run(); -} diff --git a/src/main/java/com/eppo/sdk/helpers/InputValidator.java b/src/main/java/com/eppo/sdk/helpers/InputValidator.java deleted file mode 100644 index 1043ba5..0000000 --- a/src/main/java/com/eppo/sdk/helpers/InputValidator.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.eppo.sdk.helpers; - -import cloud.eppo.rac.exception.InvalidInputException; - -/** Input Validator Class */ -public class InputValidator { - /** - * This function is used to validate input - * - * @throws InvalidInputException - */ - public static boolean validateNotBlank(String input, String errorMsg) - throws InvalidInputException { - if (input.trim().isEmpty()) { - throw new InvalidInputException(errorMsg); - } - - return true; - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/RuleValidator.java b/src/main/java/com/eppo/sdk/helpers/RuleValidator.java deleted file mode 100644 index 9be2098..0000000 --- a/src/main/java/com/eppo/sdk/helpers/RuleValidator.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.eppo.sdk.helpers; - -import cloud.eppo.rac.dto.Condition; -import cloud.eppo.rac.dto.EppoAttributes; -import cloud.eppo.rac.dto.EppoValue; -import cloud.eppo.rac.dto.Rule; -import cloud.eppo.rac.exception.InvalidSubjectAttribute; -import com.github.zafarkhaja.semver.Version; -import java.util.List; -import java.util.Optional; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** Compare Class */ -class Compare { - /** - * This function is used to compare Regex - * - * @param a - * @param pattern - * @return - */ - public static boolean compareRegex(String a, Pattern pattern) { - return pattern.matcher(a).matches(); - } - - /** - * This function is used to compare one of - * - * @param a - * @param values - * @return - */ - public static boolean isOneOf(String a, List values) { - return values.stream() - .map(String::toLowerCase) - .collect(Collectors.toList()) - .contains(a.toLowerCase()); - } -} - -/** Rule Validator Class */ -public class RuleValidator { - /** - * This function is used to check if any rule is matched - * - * @param subjectAttributes - * @param rules - * @return - */ - public static Optional findMatchingRule( - EppoAttributes subjectAttributes, List rules) { - for (Rule rule : rules) { - if (RuleValidator.matchesRule(subjectAttributes, rule)) { - return Optional.of(rule); - } - } - return Optional.empty(); - } - - /** - * This function is used to check if rule is matched - * - * @param subjectAttributes - * @param rule - * @return - * @throws InvalidSubjectAttribute - */ - private static boolean matchesRule(EppoAttributes subjectAttributes, Rule rule) - throws InvalidSubjectAttribute { - List conditionEvaluations = - RuleValidator.evaluateRuleConditions(subjectAttributes, rule.getConditions()); - return !conditionEvaluations.contains(false); - } - - /** - * This function is used to check condition - * - * @param subjectAttributes - * @param condition - * @return - * @throws InvalidSubjectAttribute - */ - private static boolean evaluateCondition(EppoAttributes subjectAttributes, Condition condition) - throws InvalidSubjectAttribute { - if (subjectAttributes.containsKey(condition.getAttribute())) { - EppoValue value = subjectAttributes.get(condition.getAttribute()); - Optional valueSemVer = Version.tryParse(value.stringValue()); - Optional conditionSemVer = Version.tryParse(condition.getValue().stringValue()); - - try { - switch (condition.getOperator()) { - case GTE: - if (value.isNumeric() && condition.getValue().isNumeric()) { - return value.doubleValue() >= condition.getValue().doubleValue(); - } - - if (valueSemVer.isPresent() && conditionSemVer.isPresent()) { - return valueSemVer.get().isHigherThanOrEquivalentTo(conditionSemVer.get()); - } - - return false; - case GT: - if (value.isNumeric() && condition.getValue().isNumeric()) { - return value.doubleValue() > condition.getValue().doubleValue(); - } - - if (valueSemVer.isPresent() && conditionSemVer.isPresent()) { - return valueSemVer.get().isHigherThan(conditionSemVer.get()); - } - - return false; - case LTE: - if (value.isNumeric() && condition.getValue().isNumeric()) { - return value.doubleValue() <= condition.getValue().doubleValue(); - } - - if (valueSemVer.isPresent() && conditionSemVer.isPresent()) { - return valueSemVer.get().isLowerThanOrEquivalentTo(conditionSemVer.get()); - } - - return false; - case LT: - if (value.isNumeric() && condition.getValue().isNumeric()) { - return value.doubleValue() < condition.getValue().doubleValue(); - } - - if (valueSemVer.isPresent() && conditionSemVer.isPresent()) { - return valueSemVer.get().isLowerThan(conditionSemVer.get()); - } - - return false; - case MATCHES: - return Compare.compareRegex( - value.stringValue(), Pattern.compile(condition.getValue().stringValue())); - case ONE_OF: - return Compare.isOneOf(value.toString(), condition.getValue().arrayValue()); - case NOT_ONE_OF: - return !Compare.isOneOf(value.toString(), condition.getValue().arrayValue()); - } - } catch (Exception e) { - return false; - } - } - return false; - } - - /** - * This function is used to check conditions - * - * @param subjectAttributes - * @param conditions - * @return - * @throws InvalidSubjectAttribute - */ - private static List evaluateRuleConditions( - EppoAttributes subjectAttributes, List conditions) throws InvalidSubjectAttribute { - return conditions.stream() - .map( - (condition) -> { - try { - return RuleValidator.evaluateCondition(subjectAttributes, condition); - } catch (Exception e) { - throw e; - } - }) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/VariationAssignmentResult.java b/src/main/java/com/eppo/sdk/helpers/VariationAssignmentResult.java deleted file mode 100644 index 1d230df..0000000 --- a/src/main/java/com/eppo/sdk/helpers/VariationAssignmentResult.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.eppo.sdk.helpers; - -import cloud.eppo.rac.dto.EppoAttributes; -import cloud.eppo.rac.dto.Variation; - -public class VariationAssignmentResult { - private final Variation variation; - private final String subjectKey; - private final EppoAttributes subjectAttributes; - private final String flagKey; - private final String experimentKey; - private final String allocationKey; - private final Integer subjectShards; - - public VariationAssignmentResult(Variation variation) { - this(variation, null, null, null, null, null, null); - } - - public VariationAssignmentResult( - Variation assignedVariation, - String subjectKey, - EppoAttributes subjectAttributes, - String flagKey, - String allocationKey, - String experimentKey, - Integer subjectShards) { - this.variation = assignedVariation; - this.subjectKey = subjectKey; - this.subjectAttributes = subjectAttributes; - this.flagKey = flagKey; - this.allocationKey = allocationKey; - this.experimentKey = experimentKey; - this.subjectShards = subjectShards; - } - - public Variation getVariation() { - return variation; - } - - public String getSubjectKey() { - return subjectKey; - } - - public EppoAttributes getSubjectAttributes() { - return subjectAttributes; - } - - public String getFlagKey() { - return flagKey; - } - - public String getExperimentKey() { - return experimentKey; - } - - public String getAllocationKey() { - return allocationKey; - } - - public Integer getSubjectShards() { - return subjectShards; - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/VariationHelper.java b/src/main/java/com/eppo/sdk/helpers/VariationHelper.java deleted file mode 100644 index bcdd264..0000000 --- a/src/main/java/com/eppo/sdk/helpers/VariationHelper.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.eppo.sdk.helpers; - -import cloud.eppo.ShardUtils; -import cloud.eppo.rac.dto.Variation; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Optional; - -public class VariationHelper { - - public static Variation selectVariation( - String inputKey, int subjectShards, List variations) { - int shard = ShardUtils.getShard(inputKey, subjectShards); - - Optional variation = - variations.stream() - .filter(config -> ShardUtils.isShardInRange(shard, config.getShardRange())) - .findFirst(); - - if (!variation.isPresent()) { - throw new NoSuchElementException( - "Variation shards configured incorrectly for input " + inputKey); - } - - return variation.get(); - } - - public static double variationProbability(Variation variation, int subjectShards) { - return (double) (variation.getShardRange().getEnd() - variation.getShardRange().getStart()) - / subjectShards; - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/bandit/BanditEvaluator.java b/src/main/java/com/eppo/sdk/helpers/bandit/BanditEvaluator.java deleted file mode 100644 index 9040f47..0000000 --- a/src/main/java/com/eppo/sdk/helpers/bandit/BanditEvaluator.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.eppo.sdk.helpers.bandit; - -import cloud.eppo.ShardUtils; -import cloud.eppo.model.ShardRange; -import cloud.eppo.rac.dto.*; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -public class BanditEvaluator { - - public static List evaluateBanditActions( - String experimentKey, - BanditParameters modelParameters, - Map actions, - String subjectKey, - EppoAttributes subjectAttributes, - int subjectShards) { - String modelName = - modelParameters != null - ? modelParameters.getModelName() - : RandomBanditModel.MODEL_IDENTIFIER; // Default to random model for unknown bandits - - BanditModel model = BanditModelFactory.build(modelName); - Map actionWeights = - model.weighActions(modelParameters, actions, subjectAttributes); - List shuffledActions = shuffleActions(actions.keySet(), experimentKey, subjectKey); - return generateVariations(shuffledActions, actionWeights, subjectShards); - } - - private static List shuffleActions( - Set actionKeys, String experimentKey, String subjectKey) { - // Shuffle randomly (but deterministically) using a hash, tie-breaking with name - return actionKeys.stream() - .sorted( - Comparator.comparingInt( - (String actionKey) -> hashToPositiveInt(experimentKey, subjectKey, actionKey)) - .thenComparing(actionKey -> actionKey)) - .collect(Collectors.toList()); - } - - private static int hashToPositiveInt(String experimentKey, String subjectKey, String actionKey) { - int SHUFFLE_SHARDS = 10000; - return ShardUtils.getShard(experimentKey + "-" + subjectKey + "-" + actionKey, SHUFFLE_SHARDS); - } - - private static List generateVariations( - List shuffledActions, Map actionWeights, int subjectShards) { - - final AtomicInteger lastShard = new AtomicInteger(0); - - List variations = - shuffledActions.stream() - .map( - actionName -> { - double weight = actionWeights.get(actionName); - int numShards = Double.valueOf(Math.floor(weight * subjectShards)).intValue(); - int shardStart = lastShard.get(); - int shardEnd = shardStart + numShards; - lastShard.set(shardEnd); - - ShardRange shardRange = new ShardRange(shardStart, shardEnd); - return new Variation(null, EppoValue.valueOf(actionName), shardRange, null); - }) - .collect(Collectors.toList()); - - // Pad last shard if needed due to rounding of weights - Variation lastVariation = variations.get(variations.size() - 1); - lastVariation - .getShardRange() - .setEnd(Math.max(lastVariation.getShardRange().getEnd(), subjectShards)); - - return variations; - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/bandit/BanditModel.java b/src/main/java/com/eppo/sdk/helpers/bandit/BanditModel.java deleted file mode 100644 index 9bbb76e..0000000 --- a/src/main/java/com/eppo/sdk/helpers/bandit/BanditModel.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.eppo.sdk.helpers.bandit; - -import cloud.eppo.rac.dto.BanditParameters; -import cloud.eppo.rac.dto.EppoAttributes; -import java.util.Map; - -public interface BanditModel { - Map weighActions( - BanditParameters parameters, - Map actions, - EppoAttributes subjectAttributes); -} diff --git a/src/main/java/com/eppo/sdk/helpers/bandit/BanditModelFactory.java b/src/main/java/com/eppo/sdk/helpers/bandit/BanditModelFactory.java deleted file mode 100644 index a3bf221..0000000 --- a/src/main/java/com/eppo/sdk/helpers/bandit/BanditModelFactory.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.eppo.sdk.helpers.bandit; - -public class BanditModelFactory { - - public static BanditModel build(String modelName) { - switch(modelName) { - case RandomBanditModel.MODEL_IDENTIFIER: - return new RandomBanditModel(); - case FalconBanditModel.MODEL_IDENTIFIER: - return new FalconBanditModel(); - default: - throw new IllegalArgumentException("Unknown bandit model " + modelName); - } - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/bandit/FalconBanditModel.java b/src/main/java/com/eppo/sdk/helpers/bandit/FalconBanditModel.java deleted file mode 100644 index 36cf074..0000000 --- a/src/main/java/com/eppo/sdk/helpers/bandit/FalconBanditModel.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.eppo.sdk.helpers.bandit; - -import cloud.eppo.rac.dto.*; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -public class FalconBanditModel implements BanditModel { - - public static final String MODEL_IDENTIFIER = "falcon"; - - public Map weighActions( - BanditParameters parameters, - Map actions, - EppoAttributes subjectAttributes) { - - BanditModelData modelData = parameters.getModelData(); - - // For each action we need to compute its score using the model coefficients - Map actionScores = - actions.entrySet().stream() - .collect( - Collectors.toMap( - Map.Entry::getKey, - e -> { - String actionName = e.getKey(); - EppoAttributes actionAttributes = e.getValue(); - double actionScore = modelData.getDefaultActionScore(); - - // get all coefficients known to the model for this action - BanditCoefficients banditCoefficients = - modelData.getCoefficients().get(actionName); - - if (banditCoefficients == null) { - // Unknown action; return default score of 0 - return actionScore; - } - - actionScore += banditCoefficients.getIntercept(); - actionScore += - scoreContextForCoefficients( - actionAttributes, banditCoefficients.getActionNumericCoefficients()); - actionScore += - scoreContextForCoefficients( - actionAttributes, - banditCoefficients.getActionCategoricalCoefficients()); - actionScore += - scoreContextForCoefficients( - subjectAttributes, - banditCoefficients.getSubjectNumericCoefficients()); - actionScore += - scoreContextForCoefficients( - subjectAttributes, - banditCoefficients.getSubjectCategoricalCoefficients()); - - return actionScore; - })); - - // Convert scores to weights (probabilities between 0 and 1 that collectively add up to 1.0) - Map actionWeights = - computeActionWeights( - actionScores, modelData.getGamma(), modelData.getActionProbabilityFloor()); - return actionWeights; - } - - private static double scoreContextForCoefficients( - EppoAttributes context, Map coefficients) { - - double totalScore = 0.0; - - for (AttributeCoefficients attributeCoefficients : coefficients.values()) { - EppoValue contextValue = context.get(attributeCoefficients.getAttributeKey()); - double attributeScore = attributeCoefficients.scoreForAttributeValue(contextValue); - totalScore += attributeScore; - } - - return totalScore; - } - - private static Map computeActionWeights( - Map actionScores, double gamma, double actionProbabilityFloor) { - Double highestScore = null; - String highestScoredAction = null; - for (Map.Entry actionScore : actionScores.entrySet()) { - if (highestScore == null || actionScore.getValue() > highestScore) { - highestScore = actionScore.getValue(); - highestScoredAction = actionScore.getKey(); - } - } - - // Weigh all the actions using their score - Map actionWeights = new HashMap<>(); - double totalNonHighestWeight = 0.0; - for (Map.Entry actionScore : actionScores.entrySet()) { - if (actionScore.getKey().equals(highestScoredAction)) { - // The highest scored action is weighed at the end - continue; - } - - // Compute weight and round to four decimal places - double unroundedProbability = - 1 / (actionScores.size() + (gamma * (highestScore - actionScore.getValue()))); - double boundedProbability = Math.max(unroundedProbability, actionProbabilityFloor); - double roundedProbability = Math.round(boundedProbability * 10000d) / 10000d; - totalNonHighestWeight += roundedProbability; - - actionWeights.put(actionScore.getKey(), boundedProbability); - } - - // Weigh the highest scoring action (defensively preventing a negative probability) - double weightForHighestScore = Math.max(1 - totalNonHighestWeight, 0); - actionWeights.put(highestScoredAction, weightForHighestScore); - return actionWeights; - } -} diff --git a/src/main/java/com/eppo/sdk/helpers/bandit/RandomBanditModel.java b/src/main/java/com/eppo/sdk/helpers/bandit/RandomBanditModel.java deleted file mode 100644 index 579021a..0000000 --- a/src/main/java/com/eppo/sdk/helpers/bandit/RandomBanditModel.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.eppo.sdk.helpers.bandit; - -import cloud.eppo.rac.dto.BanditParameters; -import cloud.eppo.rac.dto.EppoAttributes; -import java.util.Map; -import java.util.stream.Collectors; - -public class RandomBanditModel implements BanditModel { - public static final String MODEL_IDENTIFIER = "random"; - - public Map weighActions( - BanditParameters parameters, - Map actions, - EppoAttributes subjectAttributes) { - final double weightPerAction = 1 / (double) actions.size(); - return actions.keySet().stream() - .collect(Collectors.toMap(key -> key, value -> weightPerAction)); - } -} diff --git a/src/test/java/com/eppo/sdk/EppoClientTest.java b/src/test/java/com/eppo/sdk/EppoClientTest.java index e905138..7f7ef12 100644 --- a/src/test/java/com/eppo/sdk/EppoClientTest.java +++ b/src/test/java/com/eppo/sdk/EppoClientTest.java @@ -1,34 +1,33 @@ package com.eppo.sdk; +import static cloud.eppo.helpers.AssignmentTestCase.parseTestCaseFile; +import static cloud.eppo.helpers.AssignmentTestCase.runTestCase; +import static cloud.eppo.helpers.BanditTestCase.parseBanditTestCaseFile; +import static cloud.eppo.helpers.BanditTestCase.runBanditTestCase; import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.spy; - -import cloud.eppo.rac.dto.*; -import cloud.eppo.rac.exception.ExperimentConfigurationNotFound; -import com.eppo.sdk.helpers.Converter; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.node.JsonNodeType; + +import cloud.eppo.BaseEppoClient; +import cloud.eppo.EppoHttpClient; +import cloud.eppo.helpers.AssignmentTestCase; +import cloud.eppo.helpers.BanditTestCase; +import cloud.eppo.helpers.TestUtils; +import cloud.eppo.logging.Assignment; +import cloud.eppo.logging.AssignmentLogger; +import cloud.eppo.logging.BanditAssignment; +import cloud.eppo.logging.BanditLogger; +import cloud.eppo.ufc.dto.*; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import java.io.File; -import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; +import java.lang.reflect.Field; import java.util.stream.Stream; -import lombok.Data; import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -40,654 +39,246 @@ public class EppoClientTest { private static final int TEST_PORT = 4001; - - private WireMockServer mockServer; - private static final ObjectMapper MAPPER = new ObjectMapper(); - - static { - MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - } - - @Data - static class SubjectWithAttributes { - @JsonProperty String subjectKey; - @JsonProperty EppoAttributes subjectAttributes; - } - - @Data - static class AssignmentTestCase { - @JsonProperty String experiment; - - @JsonDeserialize(using = AssignmentValueTypeDeserializer.class) - AssignmentValueType valueType = AssignmentValueType.STRING; - - @JsonProperty List subjectsWithAttributes; - @JsonProperty List subjects; - @JsonProperty List expectedAssignments; - } - - enum AssignmentValueType { - STRING("string"), - BOOLEAN("boolean"), - JSON("json"), - NUMERIC("numeric"); - - private final String strValue; - - AssignmentValueType(String value) { - this.strValue = value; - } - - String value() { - return this.strValue; - } - - static AssignmentValueType getByString(String str) { - for (AssignmentValueType valueType : AssignmentValueType.values()) { - if (valueType.value().compareTo(str) == 0) { - return valueType; - } - } - return null; - } + private static final String TEST_HOST = "http://localhost:" + TEST_PORT; + private static WireMockServer mockServer; + + private static final String DUMMY_FLAG_API_KEY = "dummy-flags-api-key"; // Will load flags-v1 + private static final String DUMMY_BANDIT_API_KEY = + "dummy-bandits-api-key"; // Will load bandit-flags-v1 + private AssignmentLogger mockAssignmentLogger; + private BanditLogger mockBanditLogger; + + @BeforeAll + public static void initMockServer() { + mockServer = new WireMockServer(TEST_PORT); + mockServer.start(); + + // If we get the dummy flag API key, return flags-v1.json + String ufcFlagsResponseJson = readConfig("src/test/resources/shared/ufc/flags-v1.json"); + mockServer.stubFor( + WireMock.get( + WireMock.urlMatching( + ".*flag-config/v1/config\\?.*apiKey=" + DUMMY_FLAG_API_KEY + ".*")) + .willReturn(WireMock.okJson(ufcFlagsResponseJson))); + + // If we get the dummy bandit API key, return bandit-flags-v1.json + String banditFlagsResponseJson = + readConfig("src/test/resources/shared/ufc/bandit-flags-v1.json"); + mockServer.stubFor( + WireMock.get( + WireMock.urlMatching( + ".*flag-config/v1/config\\?.*apiKey=" + DUMMY_BANDIT_API_KEY + ".*")) + .willReturn(WireMock.okJson(banditFlagsResponseJson))); + + // Return bandit models (no need to switch on API key) + String banditModelsResponseJson = + readConfig("src/test/resources/shared/ufc/bandit-models-v1.json"); + mockServer.stubFor( + WireMock.get(WireMock.urlMatching(".*flag-config/v1/bandits\\?.*")) + .willReturn(WireMock.okJson(banditModelsResponseJson))); } - static class AssignmentValueTypeDeserializer extends StdDeserializer { - - public AssignmentValueTypeDeserializer() { - this((Class) null); - } - - protected AssignmentValueTypeDeserializer(Class vc) { - super(vc); - } - - protected AssignmentValueTypeDeserializer(JavaType valueType) { - super(valueType); - } - - protected AssignmentValueTypeDeserializer(StdDeserializer src) { - super(src); - } - - @Override - public AssignmentValueType deserialize(JsonParser jsonParser, DeserializationContext ctxt) - throws IOException { - JsonNode node = jsonParser.getCodec().readTree(jsonParser); - AssignmentValueType value = AssignmentValueType.getByString(node.asText()); - if (value == null) { - throw new RuntimeException("Invalid assignment value type"); - } - - return value; + private static String readConfig(String jsonToReturnFilePath) { + File mockResponseFile = new File(jsonToReturnFilePath); + try { + return FileUtils.readFileToString(mockResponseFile, "UTF8"); + } catch (Exception e) { + throw new RuntimeException("Error reading mock data: " + e.getMessage(), e); } } - private IAssignmentLogger mockAssignmentLogger; - private IBanditLogger mockBanditLogger; - - @BeforeEach - void init() { - mockAssignmentLogger = mock(IAssignmentLogger.class); - mockBanditLogger = mock(IBanditLogger.class); - - // For now, use our special bandits RAC until we fold it into the shared test case suite - this.mockServer = new WireMockServer(TEST_PORT); - this.mockServer.start(); - String racResponseJson = - getMockRandomizedAssignmentResponse( - "src/test/resources/bandits/rac-experiments-bandits-beta.json"); - this.mockServer.stubFor( - WireMock.get(WireMock.urlMatching(".*randomized_assignment/v3/config\\?.*")) - .willReturn(WireMock.okJson(racResponseJson))); - String banditResponseJson = - getMockRandomizedAssignmentResponse("src/test/resources/bandits/bandits-parameters-1.json"); - this.mockServer.stubFor( - WireMock.get(WireMock.urlMatching(".*flag-config/v1/bandits\\?.*")) - .willReturn(WireMock.okJson(banditResponseJson))); - - // Initialize our client with the mock loggers we can spy on - EppoClientConfig config = - new EppoClientConfig( - "mock-api-key", "http://localhost:4001", mockAssignmentLogger, mockBanditLogger); - EppoClient.init(config); - } - @AfterEach - void teardown() { - this.mockServer.stop(); + public void cleanUp() { + TestUtils.setBaseClientHttpClientOverrideField(null); + EppoClient.stopPolling(); } - @Test() - void testGracefulModeOn() { - EppoClientConfig config = - new EppoClientConfig("mock-api-key", "http://localhost:4001", logData -> {}, null); - EppoClient realClient = EppoClient.init(config); - EppoClient spyClient = spy(realClient); - - doThrow(new ExperimentConfigurationNotFound("Exception thrown by mock")) - .when(spyClient) - .getAssignmentValue(anyString(), anyString(), any(EppoAttributes.class), anyMap()); - - assertDoesNotThrow(() -> spyClient.getBooleanAssignment("subject1", "experiment1")); - assertDoesNotThrow(() -> spyClient.getDoubleAssignment("subject1", "experiment1")); - assertDoesNotThrow(() -> spyClient.getParsedJSONAssignment("subject1", "experiment1")); - assertDoesNotThrow(() -> spyClient.getJSONStringAssignment("subject1", "experiment1")); - assertDoesNotThrow(() -> spyClient.getStringAssignment("subject1", "experiment1")); - - assertEquals(Optional.empty(), spyClient.getBooleanAssignment("subject1", "experiment1")); - assertEquals(Optional.empty(), spyClient.getDoubleAssignment("subject1", "experiment1")); - assertEquals(Optional.empty(), spyClient.getParsedJSONAssignment("subject1", "experiment1")); - assertEquals(Optional.empty(), spyClient.getJSONStringAssignment("subject1", "experiment1")); - assertEquals(Optional.empty(), spyClient.getStringAssignment("subject1", "experiment1")); - } - - @Test() - void testGracefulModeOff() { - EppoClientConfig config = - new EppoClientConfig( - "mock-api-key", - "http://localhost:4001", - logData -> { - // Auto-generated method stub - }, - null); - config.setGracefulMode(false); - EppoClient.init(config); - EppoClient realClient = EppoClient.getInstance(); - - EppoClient spyClient = spy(realClient); - - doThrow(new ExperimentConfigurationNotFound("Exception thrown by mock")) - .when(spyClient) - .getAssignmentValue(anyString(), anyString(), any(EppoAttributes.class), any()); - - assertThrows( - ExperimentConfigurationNotFound.class, - () -> spyClient.getBooleanAssignment("subject1", "experiment1")); - assertThrows( - ExperimentConfigurationNotFound.class, - () -> spyClient.getDoubleAssignment("subject1", "experiment1")); - assertThrows( - ExperimentConfigurationNotFound.class, - () -> spyClient.getParsedJSONAssignment("subject1", "experiment1")); - assertThrows( - ExperimentConfigurationNotFound.class, - () -> spyClient.getJSONStringAssignment("subject1", "experiment1")); - assertThrows( - ExperimentConfigurationNotFound.class, - () -> spyClient.getStringAssignment("subject1", "experiment1")); + @AfterAll + public static void tearDown() { + if (mockServer != null) { + mockServer.stop(); + } } @ParameterizedTest @MethodSource("getAssignmentTestData") - void testAssignments(AssignmentTestCase testCase) { - - // These test cases rely on the currently shared non-bandit RAC, so we need to re-initialize our - // client to use that - String racResponseJson = - getMockRandomizedAssignmentResponse("src/test/resources/rac-experiments-v3.json"); - this.mockServer.stubFor( - WireMock.get(WireMock.urlMatching(".*randomized_assignment/v3/config\\?.*")) - .willReturn(WireMock.okJson(racResponseJson))); - EppoClientConfig config = - new EppoClientConfig("mock-api-key", "http://localhost:4001", mockAssignmentLogger, null); - EppoClient.init(config); - - switch (testCase.valueType) { - case NUMERIC: - List expectedDoubleAssignments = - Converter.convertToDecimal(testCase.expectedAssignments); - List actualDoubleAssignments = this.getDoubleAssignments(testCase); - assertEquals(expectedDoubleAssignments, actualDoubleAssignments); - break; - case BOOLEAN: - List expectedBooleanAssignments = - Converter.convertToBoolean(testCase.expectedAssignments); - List actualBooleanAssignments = this.getBooleanAssignments(testCase); - assertEquals(expectedBooleanAssignments, actualBooleanAssignments); - break; - case JSON: - List actualJSONAssignments = this.getJSONAssignments(testCase); - for (JsonNode node : actualJSONAssignments) { - assertEquals(JsonNodeType.OBJECT, node.getNodeType()); - } - - assertEquals( - testCase.expectedAssignments, - actualJSONAssignments.stream().map(JsonNode::toString).collect(Collectors.toList())); - break; - default: - List actualStringAssignments = this.getStringAssignments(testCase); - assertEquals(testCase.expectedAssignments, actualStringAssignments); - } + public void testUnobfuscatedAssignments(File testFile) { + AssignmentTestCase testCase = parseTestCaseFile(testFile); + EppoClient eppoClient = initClient(DUMMY_FLAG_API_KEY); + runTestCase(testCase, eppoClient); } - private List getAssignments(AssignmentTestCase testCase, AssignmentValueType valueType) { - EppoClient client = EppoClient.getInstance(); - if (testCase.subjectsWithAttributes != null) { - return testCase.subjectsWithAttributes.stream() - .map( - subject -> { - try { - switch (valueType) { - case NUMERIC: - return client - .getDoubleAssignment( - subject.subjectKey, testCase.experiment, subject.subjectAttributes) - .orElse(null); - case BOOLEAN: - return client - .getBooleanAssignment( - subject.subjectKey, testCase.experiment, subject.subjectAttributes) - .orElse(null); - case JSON: - return client - .getParsedJSONAssignment( - subject.subjectKey, testCase.experiment, subject.subjectAttributes) - .orElse(null); - default: - return client - .getStringAssignment( - subject.subjectKey, testCase.experiment, subject.subjectAttributes) - .orElse(null); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - }) - .collect(Collectors.toList()); - } - return testCase.subjects.stream() - .map( - subject -> { - try { - switch (valueType) { - case NUMERIC: - return client.getDoubleAssignment(subject, testCase.experiment).orElse(null); - case BOOLEAN: - return client.getBooleanAssignment(subject, testCase.experiment).orElse(null); - case JSON: - return client - .getParsedJSONAssignment(subject, testCase.experiment) - .orElse(null); - default: - return client.getStringAssignment(subject, testCase.experiment).orElse(null); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - }) - .collect(Collectors.toList()); + private static Stream getAssignmentTestData() { + return AssignmentTestCase.getAssignmentTestData(); } - private List getStringAssignments(AssignmentTestCase testCase) { - //noinspection unchecked - return (List) this.getAssignments(testCase, AssignmentValueType.STRING); + @ParameterizedTest + @MethodSource("getBanditTestData") + public void testUnobfuscatedBanditAssignments(File testFile) { + BanditTestCase testCase = parseBanditTestCaseFile(testFile); + EppoClient eppoClient = initClient(DUMMY_BANDIT_API_KEY); + runBanditTestCase(testCase, eppoClient); } - private List getDoubleAssignments(AssignmentTestCase testCase) { - //noinspection unchecked - return (List) this.getAssignments(testCase, AssignmentValueType.NUMERIC); + private static Stream getBanditTestData() { + return BanditTestCase.getBanditTestData(); } - private List getBooleanAssignments(AssignmentTestCase testCase) { - //noinspection unchecked - return (List) this.getAssignments(testCase, AssignmentValueType.BOOLEAN); - } + @SuppressWarnings("ExtractMethodRecommender") + @Test + public void testLoggers() { + EppoClient eppoClient = initClient(DUMMY_BANDIT_API_KEY); - private List getJSONAssignments(AssignmentTestCase testCase) { - //noinspection unchecked - return (List) this.getAssignments(testCase, AssignmentValueType.JSON); - } + String flagKey = "banner_bandit_flag"; + String subjectKey = "bob"; + Attributes subjectAttributes = new Attributes(); + subjectAttributes.put("age", 25); + subjectAttributes.put("country", "USA"); + subjectAttributes.put("gender_identity", "female"); - private static Stream getAssignmentTestData() throws IOException { - File testCaseFolder = new File("src/test/resources/assignment-v2/"); - File[] testCaseFiles = testCaseFolder.listFiles(); - assertNotNull(testCaseFiles); - List arguments = new ArrayList<>(); - for (File testCaseFile : testCaseFiles) { - String json = FileUtils.readFileToString(testCaseFile, "UTF8"); - AssignmentTestCase testCase = MAPPER.readValue(json, AssignmentTestCase.class); - arguments.add(Arguments.of(testCase)); - } - return arguments.stream(); - } + BanditActions actions = new BanditActions(); - private static String getMockRandomizedAssignmentResponse(String jsonToReturnFilePath) { - File mockRacResponse = new File(jsonToReturnFilePath); - try { - return FileUtils.readFileToString(mockRacResponse, "UTF8"); - } catch (Exception e) { - throw new RuntimeException("Error reading mock RAC data: " + e.getMessage(), e); - } - } + Attributes nikeAttributes = new Attributes(); + nikeAttributes.put("brand_affinity", 1.5); + nikeAttributes.put("loyalty_tier", "silver"); + actions.put("nike", nikeAttributes); - @Test - public void testBanditColdStartAction() { - Set banditActions = - Stream.of("option1", "option2", "option3").collect(Collectors.toSet()); + Attributes adidasAttributes = new Attributes(); + adidasAttributes.put("brand_affinity", -1.0); + adidasAttributes.put("loyalty_tier", "bronze"); + actions.put("adidas", adidasAttributes); + + Attributes rebookAttributes = new Attributes(); + rebookAttributes.put("brand_affinity", 0.5); + rebookAttributes.put("loyalty_tier", "gold"); + actions.put("reebok", rebookAttributes); - // Attempt to get a bandit assignment - Optional stringAssignment = - EppoClient.getInstance() - .getBanditAssignment( - "subject1", "cold-start-bandit-experiment", new EppoAttributes(), banditActions); + BanditResult banditResult = + eppoClient.getBanditAction(flagKey, subjectKey, subjectAttributes, actions, "control"); // Verify assignment - assertTrue(stringAssignment.isPresent()); - assertTrue(banditActions.contains(stringAssignment.get())); + assertEquals("banner_bandit", banditResult.getVariation()); + assertEquals("adidas", banditResult.getAction()); - // Verify experiment assignment log - ArgumentCaptor assignmentLogCaptor = - ArgumentCaptor.forClass(AssignmentLogData.class); + // Verify experiment assignment logger called + ArgumentCaptor assignmentLogCaptor = ArgumentCaptor.forClass(Assignment.class); verify(mockAssignmentLogger, times(1)).logAssignment(assignmentLogCaptor.capture()); - AssignmentLogData capturedAssignmentLog = assignmentLogCaptor.getValue(); - assertEquals("cold-start-bandit-experiment-bandit", capturedAssignmentLog.getExperiment()); - assertEquals("cold-start-bandit-experiment", capturedAssignmentLog.getFeatureFlag()); - assertEquals("bandit", capturedAssignmentLog.getAllocation()); - assertEquals("cold-start-bandit", capturedAssignmentLog.getVariation()); - assertEquals("subject1", capturedAssignmentLog.getSubject()); - assertEquals(new EppoAttributes(), capturedAssignmentLog.getSubjectAttributes()); - - // Verify bandit log - ArgumentCaptor banditLogCaptor = ArgumentCaptor.forClass(BanditLogData.class); - verify(mockBanditLogger, times(1)).logBanditAction(banditLogCaptor.capture()); - BanditLogData capturedBanditLog = banditLogCaptor.getValue(); - assertEquals("cold-start-bandit-experiment-bandit", capturedBanditLog.getExperiment()); - assertEquals("cold-start-bandit", capturedBanditLog.getBanditKey()); - assertEquals("subject1", capturedBanditLog.getSubject()); - assertEquals(new HashMap<>(), capturedBanditLog.getSubjectNumericAttributes()); - assertEquals(new HashMap<>(), capturedBanditLog.getSubjectCategoricalAttributes()); - assertEquals("option1", capturedBanditLog.getAction()); - assertEquals(new HashMap<>(), capturedBanditLog.getActionNumericAttributes()); - assertEquals(new HashMap<>(), capturedBanditLog.getActionCategoricalAttributes()); - assertEquals(0.3333, capturedBanditLog.getActionProbability(), 0.0002); - assertEquals("falcon cold start", capturedBanditLog.getModelVersion()); + + // Verify bandit logger called + ArgumentCaptor banditLogCaptor = + ArgumentCaptor.forClass(BanditAssignment.class); + verify(mockBanditLogger, times(1)).logBanditAssignment(banditLogCaptor.capture()); } @Test - public void testBanditUninitializedAction() { - Set banditActions = - Stream.of("option1", "option2", "option3").collect(Collectors.toSet()); - - // Attempt to get a bandit assignment - Optional stringAssignment = - EppoClient.getInstance() - .getBanditAssignment( - "subject8", "uninitialized-bandit-experiment", new EppoAttributes(), banditActions); - - // Verify assignment - assertTrue(stringAssignment.isPresent()); - assertTrue(banditActions.contains(stringAssignment.get())); - - // Verify experiment assignment log - ArgumentCaptor assignmentLogCaptor = - ArgumentCaptor.forClass(AssignmentLogData.class); - verify(mockAssignmentLogger, times(1)).logAssignment(assignmentLogCaptor.capture()); - AssignmentLogData capturedAssignmentLog = assignmentLogCaptor.getValue(); - assertEquals("uninitialized-bandit-experiment-bandit", capturedAssignmentLog.getExperiment()); - assertEquals("uninitialized-bandit-experiment", capturedAssignmentLog.getFeatureFlag()); - assertEquals("bandit", capturedAssignmentLog.getAllocation()); - assertEquals("this-bandit-does-not-exist", capturedAssignmentLog.getVariation()); - assertEquals("subject8", capturedAssignmentLog.getSubject()); - assertEquals(new EppoAttributes(), capturedAssignmentLog.getSubjectAttributes()); - - // Verify bandit log - ArgumentCaptor banditLogCaptor = ArgumentCaptor.forClass(BanditLogData.class); - verify(mockBanditLogger, times(1)).logBanditAction(banditLogCaptor.capture()); - BanditLogData capturedBanditLog = banditLogCaptor.getValue(); - assertEquals("uninitialized-bandit-experiment-bandit", capturedBanditLog.getExperiment()); - assertEquals("this-bandit-does-not-exist", capturedBanditLog.getBanditKey()); - assertEquals("subject8", capturedBanditLog.getSubject()); - assertEquals(new HashMap<>(), capturedBanditLog.getSubjectNumericAttributes()); - assertEquals(new HashMap<>(), capturedBanditLog.getSubjectCategoricalAttributes()); - assertEquals("option1", capturedBanditLog.getAction()); - assertEquals(new HashMap<>(), capturedBanditLog.getActionNumericAttributes()); - assertEquals(new HashMap<>(), capturedBanditLog.getActionCategoricalAttributes()); - assertEquals(0.3333, capturedBanditLog.getActionProbability(), 0.0002); - assertEquals("uninitialized", capturedBanditLog.getModelVersion()); + public void getInstanceWhenUninitialized() { + uninitClient(); + assertThrows(RuntimeException.class, EppoClient::getInstance); } @Test - public void testBanditModelActionLogging() { - // Note: some of the passed in attributes are not used for scoring, but we do still want to make - // sure they are logged - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("gender_identity", EppoValue.valueOf("female")); - subjectAttributes.put( - "days_since_signup", - EppoValue.valueOf(130)); // unused for scoring (which looks for account_age) - subjectAttributes.put("is_premium", EppoValue.valueOf(false)); // unused for scoring - subjectAttributes.put("numeric_string", EppoValue.valueOf("123")); // unused for scoring - subjectAttributes.put("unpopulated", EppoValue.nullValue()); // unused for scoring - - Map actionsWithAttributes = new HashMap<>(); - - EppoAttributes nikeAttributes = new EppoAttributes(); - nikeAttributes.put("brand_affinity", EppoValue.valueOf(0.25)); - actionsWithAttributes.put("nike", nikeAttributes); - - EppoAttributes adidasAttributes = new EppoAttributes(); - adidasAttributes.put("brand_affinity", EppoValue.valueOf(0.1)); - adidasAttributes.put("num_brand_purchases", EppoValue.valueOf(5)); // unused for scoring - adidasAttributes.put("in_email_campaign", EppoValue.valueOf(true)); // unused for scoring - adidasAttributes.put("also_unpopulated", EppoValue.nullValue()); // unused for scoring - actionsWithAttributes.put("adidas", adidasAttributes); - - actionsWithAttributes.put("puma", new EppoAttributes()); - - // Get our assigned action - Optional stringAssignment = - EppoClient.getInstance() - .getBanditAssignment( - "subject2", "banner-bandit-experiment", subjectAttributes, actionsWithAttributes); - - // Verify assignment - assertTrue(stringAssignment.isPresent()); - assertEquals("adidas", stringAssignment.get()); - - // Verify experiment assignment log - ArgumentCaptor assignmentLogCaptor = - ArgumentCaptor.forClass(AssignmentLogData.class); - verify(mockAssignmentLogger, times(1)).logAssignment(assignmentLogCaptor.capture()); - AssignmentLogData capturedAssignmentLog = assignmentLogCaptor.getValue(); - assertEquals("banner-bandit-experiment-bandit", capturedAssignmentLog.getExperiment()); - assertEquals("banner-bandit-experiment", capturedAssignmentLog.getFeatureFlag()); - assertEquals("bandit", capturedAssignmentLog.getAllocation()); - assertEquals("banner-bandit", capturedAssignmentLog.getVariation()); - assertEquals("subject2", capturedAssignmentLog.getSubject()); - assertEquals(subjectAttributes, capturedAssignmentLog.getSubjectAttributes()); - - // Verify bandit log - ArgumentCaptor banditLogCaptor = ArgumentCaptor.forClass(BanditLogData.class); - verify(mockBanditLogger, times(1)).logBanditAction(banditLogCaptor.capture()); - BanditLogData capturedBanditLog = banditLogCaptor.getValue(); - assertEquals("banner-bandit-experiment-bandit", capturedBanditLog.getExperiment()); - assertEquals("banner-bandit", capturedBanditLog.getBanditKey()); - assertEquals("subject2", capturedBanditLog.getSubject()); - assertEquals("adidas", capturedBanditLog.getAction()); - assertEquals(0.2899, capturedBanditLog.getActionProbability(), 0.0002); - assertEquals("falcon v123", capturedBanditLog.getModelVersion()); - - Map expectedSubjectNumericAttributes = new HashMap<>(); - expectedSubjectNumericAttributes.put("days_since_signup", 130.0); - assertEquals(expectedSubjectNumericAttributes, capturedBanditLog.getSubjectNumericAttributes()); - - Map expectedSubjectCategoricalAttributes = new HashMap<>(); - expectedSubjectCategoricalAttributes.put("gender_identity", "female"); - expectedSubjectCategoricalAttributes.put("is_premium", "false"); - expectedSubjectCategoricalAttributes.put("numeric_string", "123"); + public void testErrorGracefulModeOn() { + initBuggyClient(); + EppoClient.getInstance().setIsGracefulFailureMode(true); assertEquals( - expectedSubjectCategoricalAttributes, capturedBanditLog.getSubjectCategoricalAttributes()); - - Map expectedActionNumericAttributes = new HashMap<>(); - expectedActionNumericAttributes.put("brand_affinity", 0.1); - expectedActionNumericAttributes.put("num_brand_purchases", 5.0); - assertEquals(expectedActionNumericAttributes, capturedBanditLog.getActionNumericAttributes()); + 1.234, EppoClient.getInstance().getDoubleAssignment("numeric_flag", "subject1", 1.234)); + } - Map expectedActionCategoricalAttributes = new HashMap<>(); - expectedActionCategoricalAttributes.put("in_email_campaign", "true"); - assertEquals( - expectedActionCategoricalAttributes, capturedBanditLog.getActionCategoricalAttributes()); + @Test + public void testErrorGracefulModeOff() { + initBuggyClient(); + EppoClient.getInstance().setIsGracefulFailureMode(false); + assertThrows( + Exception.class, + () -> EppoClient.getInstance().getDoubleAssignment("numeric_flag", "subject1", 1.234)); } @Test - public void testBanditModelActionAssignmentFullContext() { - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("gender_identity", EppoValue.valueOf("male")); - subjectAttributes.put("account_age", EppoValue.valueOf(3)); - - Map actionAttributes = new HashMap<>(); - - EppoAttributes nikeAttributes = new EppoAttributes(); - nikeAttributes.put("brand_affinity", EppoValue.valueOf(0.05)); - nikeAttributes.put("purchased_last_30_days", EppoValue.valueOf(true)); - nikeAttributes.put("loyalty_tier", EppoValue.valueOf("gold")); - actionAttributes.put("nike", nikeAttributes); - - EppoAttributes adidasAttributes = new EppoAttributes(); - adidasAttributes.put("brand_affinity", EppoValue.valueOf(0.30)); - adidasAttributes.put("purchased_last_30_days", EppoValue.valueOf(true)); - adidasAttributes.put("loyalty_tier", EppoValue.valueOf("gold")); - actionAttributes.put("adidas", adidasAttributes); - - EppoAttributes pumaAttributes = new EppoAttributes(); - pumaAttributes.put("brand_affinity", EppoValue.valueOf(1.00)); - pumaAttributes.put("purchased_last_30_days", EppoValue.valueOf(false)); - pumaAttributes.put("loyalty_tier", EppoValue.valueOf("bronze")); - actionAttributes.put("puma", pumaAttributes); - - // Get our assigned action - Optional stringAssignment = - EppoClient.getInstance() - .getBanditAssignment( - "subject30", "banner-bandit-experiment", subjectAttributes, actionAttributes); + public void testReinitializeWithoutForcing() { + EppoClient firstInstance = initClient(DUMMY_FLAG_API_KEY); + EppoClient secondInstance = new EppoClient.Builder().apiKey(DUMMY_FLAG_API_KEY).buildAndInit(); - // Verify assignment - assertTrue(stringAssignment.isPresent()); - assertEquals("adidas", stringAssignment.get()); - ArgumentCaptor banditLogCaptor = ArgumentCaptor.forClass(BanditLogData.class); - verify(mockBanditLogger, times(1)).logBanditAction(banditLogCaptor.capture()); - BanditLogData capturedBanditLog = banditLogCaptor.getValue(); - assertEquals(0.8043, capturedBanditLog.getActionProbability(), 0.0002); + assertSame(firstInstance, secondInstance); } @Test - public void testBanditModelActionAssignmentNoContext() { - EppoAttributes subjectAttributes = new EppoAttributes(); - Set actions = Stream.of("nike", "adidas", "puma").collect(Collectors.toSet()); - - // Get our assigned action - Optional stringAssignment = - EppoClient.getInstance() - .getBanditAssignment( - "subject39", "banner-bandit-experiment", subjectAttributes, actions); + public void testReinitializeWitForcing() { + EppoClient firstInstance = initClient(DUMMY_FLAG_API_KEY); + EppoClient secondInstance = + new EppoClient.Builder().apiKey(DUMMY_FLAG_API_KEY).forceReinitialize(true).buildAndInit(); - // Verify assignment - assertTrue(stringAssignment.isPresent()); - assertEquals("puma", stringAssignment.get()); - ArgumentCaptor banditLogCaptor = ArgumentCaptor.forClass(BanditLogData.class); - verify(mockBanditLogger, times(1)).logBanditAction(banditLogCaptor.capture()); - BanditLogData capturedBanditLog = banditLogCaptor.getValue(); - assertEquals(0.1613, capturedBanditLog.getActionProbability(), 0.0002); + assertNotSame(firstInstance, secondInstance); } @Test - public void testBanditControlAction() { + public void testPolling() { + EppoHttpClient httpClient = new EppoHttpClient(TEST_HOST, DUMMY_FLAG_API_KEY, "java", "3.0.0"); + EppoHttpClient httpClientSpy = spy(httpClient); + TestUtils.setBaseClientHttpClientOverrideField(httpClientSpy); - Set banditActions = - Stream.of("option1", "option2", "option3").collect(Collectors.toSet()); + new EppoClient.Builder() + .apiKey(DUMMY_FLAG_API_KEY) + .pollingIntervalMs(20) + .forceReinitialize(true) + .buildAndInit(); - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("account_age", EppoValue.valueOf(90)); - subjectAttributes.put("loyalty_tier", EppoValue.valueOf("gold")); - subjectAttributes.put("is_account_admin", EppoValue.valueOf(false)); + // Method will be called immediately on init + verify(httpClientSpy, times(1)).get(anyString()); - // Attempt to get a bandit assignment - Optional stringAssignment = - EppoClient.getInstance() - .getBanditAssignment( - "subject10", "cold-start-bandit-experiment", subjectAttributes, banditActions); + // Sleep for 25 ms to allow another polling cycle to complete + sleepUninterruptedly(25); - // Verify assignment - assertTrue(stringAssignment.isPresent()); - assertEquals("control", stringAssignment.get()); - - // Manually log an action - - EppoAttributes controlActionAttributes = new EppoAttributes(); - controlActionAttributes.put("brand", EppoValue.valueOf("skechers")); - controlActionAttributes.put("num_past_purchases", EppoValue.valueOf(0)); - controlActionAttributes.put("has_promo_code", EppoValue.valueOf(true)); - - Exception banditLoggingException = - EppoClient.getInstance() - .logNonBanditAction( - "subject10", - "cold-start-bandit-experiment", - subjectAttributes, - "option0", - controlActionAttributes); - assertNull(banditLoggingException); - - // Verify experiment assignment log - ArgumentCaptor assignmentLogCaptor = - ArgumentCaptor.forClass(AssignmentLogData.class); - verify(mockAssignmentLogger, times(1)).logAssignment(assignmentLogCaptor.capture()); - AssignmentLogData capturedAssignmentLog = assignmentLogCaptor.getValue(); - assertEquals("cold-start-bandit-experiment-bandit", capturedAssignmentLog.getExperiment()); - assertEquals("cold-start-bandit-experiment", capturedAssignmentLog.getFeatureFlag()); - assertEquals("bandit", capturedAssignmentLog.getAllocation()); - assertEquals("control", capturedAssignmentLog.getVariation()); - assertEquals("subject10", capturedAssignmentLog.getSubject()); - assertEquals(subjectAttributes, capturedAssignmentLog.getSubjectAttributes()); - - // Verify bandit log - ArgumentCaptor banditLogCaptor = ArgumentCaptor.forClass(BanditLogData.class); - verify(mockBanditLogger, times(1)).logBanditAction(banditLogCaptor.capture()); - BanditLogData capturedBanditLog = banditLogCaptor.getValue(); - assertEquals("cold-start-bandit-experiment-bandit", capturedBanditLog.getExperiment()); - assertEquals("control", capturedBanditLog.getBanditKey()); - assertEquals("subject10", capturedBanditLog.getSubject()); - assertEquals("option0", capturedBanditLog.getAction()); - assertNull(capturedBanditLog.getActionProbability()); - assertNull(capturedBanditLog.getModelVersion()); - - Map expectedSubjectNumericAttributes = new HashMap<>(); - expectedSubjectNumericAttributes.put("account_age", 90.0); - assertEquals(expectedSubjectNumericAttributes, capturedBanditLog.getSubjectNumericAttributes()); - - Map expectedSubjectCategoricalAttributes = new HashMap<>(); - expectedSubjectCategoricalAttributes.put("loyalty_tier", "gold"); - expectedSubjectCategoricalAttributes.put("is_account_admin", "false"); - assertEquals( - expectedSubjectCategoricalAttributes, capturedBanditLog.getSubjectCategoricalAttributes()); + // Now, the method should have been called twice + verify(httpClientSpy, times(2)).get(anyString()); - Map expectedActionNumericAttributes = new HashMap<>(); - expectedActionNumericAttributes.put("num_past_purchases", 0.0); - assertEquals(expectedActionNumericAttributes, capturedBanditLog.getActionNumericAttributes()); + EppoClient.stopPolling(); + sleepUninterruptedly(25); - Map expectedActionCategoricalAttributes = new HashMap<>(); - expectedActionCategoricalAttributes.put("brand", "skechers"); - expectedActionCategoricalAttributes.put("has_promo_code", "true"); - assertEquals( - expectedActionCategoricalAttributes, capturedBanditLog.getActionCategoricalAttributes()); + // No more calls since stopped + verify(httpClientSpy, times(2)).get(anyString()); } - @Test - public void testBanditNotInAllocation() { - Set banditActions = - Stream.of("option1", "option2", "option3").collect(Collectors.toSet()); + @SuppressWarnings("SameParameterValue") + private void sleepUninterruptedly(long sleepMs) { + try { + Thread.sleep(sleepMs); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } - // Attempt to get a bandit assignment - Optional stringAssignment = - EppoClient.getInstance() - .getBanditAssignment( - "subject2", "cold-start-bandit", new EppoAttributes(), banditActions); + private EppoClient initClient(String apiKey) { + mockAssignmentLogger = mock(AssignmentLogger.class); + mockBanditLogger = mock(BanditLogger.class); + + return new EppoClient.Builder() + .apiKey(apiKey) + .host(TEST_HOST) + .assignmentLogger(mockAssignmentLogger) + .banditLogger(mockBanditLogger) + .isGracefulMode(false) + .forceReinitialize(true) // Useful for tests + .buildAndInit(); + } - // Verify assignment - assertFalse(stringAssignment.isPresent()); + private void uninitClient() { + try { + Field httpClientOverrideField = EppoClient.class.getDeclaredField("instance"); + httpClientOverrideField.setAccessible(true); + httpClientOverrideField.set(null, null); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private void initBuggyClient() { + try { + EppoClient eppoClient = initClient(DUMMY_FLAG_API_KEY); + Field configurationStoreField = BaseEppoClient.class.getDeclaredField("requestor"); + configurationStoreField.setAccessible(true); + configurationStoreField.set(eppoClient, null); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } } } diff --git a/src/test/java/com/eppo/sdk/deserializer/BanditsDeserializerTest.java b/src/test/java/com/eppo/sdk/deserializer/BanditsDeserializerTest.java deleted file mode 100644 index 467c6b0..0000000 --- a/src/test/java/com/eppo/sdk/deserializer/BanditsDeserializerTest.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.eppo.sdk.deserializer; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import cloud.eppo.rac.dto.*; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.File; -import java.io.IOException; -import java.util.Map; -import org.apache.commons.io.FileUtils; -import org.junit.jupiter.api.Test; - -class BanditsDeserializerTest { - private final ObjectMapper mapper = new ObjectMapper(); - - @Test - public void testDeserializingBandits() throws IOException { - String jsonString = - FileUtils.readFileToString( - new File("src/test/resources/bandits/bandits-parameters-1.json"), "UTF8"); - BanditParametersResponse responseObject = - this.mapper.readValue(jsonString, BanditParametersResponse.class); - - assertEquals(2, responseObject.getBandits().size()); - BanditParameters parameters = responseObject.getBandits().get("banner-bandit"); - assertEquals("banner-bandit", parameters.getBanditKey()); - assertEquals("falcon", parameters.getModelName()); - assertEquals("v123", parameters.getModelVersion()); - - BanditModelData modelData = parameters.getModelData(); - assertEquals(1.0, modelData.getGamma()); - assertEquals(0.0, modelData.getDefaultActionScore()); - assertEquals(0.0, modelData.getActionProbabilityFloor()); - - Map coefficients = modelData.getCoefficients(); - assertEquals(2, coefficients.size()); - - // Nike - - BanditCoefficients nikeCoefficients = coefficients.get("nike"); - assertEquals("nike", nikeCoefficients.getActionKey()); - assertEquals(1.0, nikeCoefficients.getIntercept()); - - // Nike subject coefficients - - Map nikeSubjectNumericCoefficients = - nikeCoefficients.getSubjectNumericCoefficients(); - assertEquals(1, nikeSubjectNumericCoefficients.size()); - - BanditNumericAttributeCoefficients nikeAccountAgeCoefficients = - nikeSubjectNumericCoefficients.get("account_age"); - assertEquals("account_age", nikeAccountAgeCoefficients.getAttributeKey()); - assertEquals(0.3, nikeAccountAgeCoefficients.getCoefficient()); - assertEquals(0.0, nikeAccountAgeCoefficients.getMissingValueCoefficient()); - - Map nikeSubjectCategoricalCoefficients = - nikeCoefficients.getSubjectCategoricalCoefficients(); - assertEquals(1, nikeSubjectCategoricalCoefficients.size()); - - BanditCategoricalAttributeCoefficients nikeGenderIdentityCoefficients = - nikeSubjectCategoricalCoefficients.get("gender_identity"); - assertEquals("gender_identity", nikeGenderIdentityCoefficients.getAttributeKey()); - assertEquals(2.3, nikeGenderIdentityCoefficients.getMissingValueCoefficient()); - Map nikeGenderIdentityCoefficientValues = - nikeGenderIdentityCoefficients.getValueCoefficients(); - assertEquals(2, nikeGenderIdentityCoefficientValues.size()); - assertEquals(0.5, nikeGenderIdentityCoefficientValues.get("female")); - assertEquals(-0.5, nikeGenderIdentityCoefficientValues.get("male")); - - // Nike action coefficients - - Map nikeActionNumericCoefficients = - nikeCoefficients.getActionNumericCoefficients(); - assertEquals(1, nikeActionNumericCoefficients.size()); - - BanditNumericAttributeCoefficients nikeBrandAffinityCoefficient = - nikeActionNumericCoefficients.get("brand_affinity"); - assertEquals("brand_affinity", nikeBrandAffinityCoefficient.getAttributeKey()); - assertEquals(1.0, nikeBrandAffinityCoefficient.getCoefficient()); - assertEquals(-0.1, nikeBrandAffinityCoefficient.getMissingValueCoefficient()); - - Map nikeActionCategoricalCoefficients = - nikeCoefficients.getActionCategoricalCoefficients(); - assertEquals(1, nikeActionCategoricalCoefficients.size()); - - BanditCategoricalAttributeCoefficients nikeLoyaltyCoefficients = - nikeActionCategoricalCoefficients.get("loyalty_tier"); - assertEquals("loyalty_tier", nikeLoyaltyCoefficients.getAttributeKey()); - assertEquals(0.0, nikeLoyaltyCoefficients.getMissingValueCoefficient()); - Map nikeLoyaltyCoefficientValues = - nikeLoyaltyCoefficients.getValueCoefficients(); - assertEquals(3, nikeLoyaltyCoefficientValues.size()); - assertEquals(4.5, nikeLoyaltyCoefficientValues.get("gold")); - assertEquals(3.2, nikeLoyaltyCoefficientValues.get("silver")); - assertEquals(1.9, nikeLoyaltyCoefficientValues.get("bronze")); - - // Adidas - - BanditCoefficients adidasCoefficients = coefficients.get("adidas"); - assertEquals("adidas", adidasCoefficients.getActionKey()); - assertEquals(1.1, adidasCoefficients.getIntercept()); - - // Adidas subject coefficients - - Map adidasSubjectNumericCoefficients = - adidasCoefficients.getSubjectNumericCoefficients(); - assertEquals(0, adidasSubjectNumericCoefficients.size()); - - Map adidasSubjectCategoricalCoefficients = - adidasCoefficients.getSubjectCategoricalCoefficients(); - assertEquals(1, adidasSubjectCategoricalCoefficients.size()); - - BanditCategoricalAttributeCoefficients adidasGenderIdentityCoefficient = - adidasSubjectCategoricalCoefficients.get("gender_identity"); - assertEquals("gender_identity", adidasGenderIdentityCoefficient.getAttributeKey()); - assertEquals(0.45, adidasGenderIdentityCoefficient.getMissingValueCoefficient()); - Map adidasGenderIdentityCoefficientValues = - adidasGenderIdentityCoefficient.getValueCoefficients(); - assertEquals(2, adidasGenderIdentityCoefficientValues.size()); - assertEquals(0.0, adidasGenderIdentityCoefficientValues.get("female")); - assertEquals(0.3, adidasGenderIdentityCoefficientValues.get("male")); - - // Adidas action coefficients - - Map adidasActionNumericCoefficients = - adidasCoefficients.getActionNumericCoefficients(); - assertEquals(1, nikeActionNumericCoefficients.size()); - - BanditNumericAttributeCoefficients adidasBrandAffinityCoefficient = - adidasActionNumericCoefficients.get("brand_affinity"); - assertEquals("brand_affinity", adidasBrandAffinityCoefficient.getAttributeKey()); - assertEquals(2.0, adidasBrandAffinityCoefficient.getCoefficient()); - assertEquals(1.2, adidasBrandAffinityCoefficient.getMissingValueCoefficient()); - - Map adidasActionCategoricalCoefficients = - adidasCoefficients.getActionCategoricalCoefficients(); - assertEquals(1, adidasActionCategoricalCoefficients.size()); - - BanditCategoricalAttributeCoefficients adidasPurchasedLast30Coefficient = - adidasActionCategoricalCoefficients.get("purchased_last_30_days"); - assertEquals("purchased_last_30_days", adidasPurchasedLast30Coefficient.getAttributeKey()); - assertEquals(0.0, adidasPurchasedLast30Coefficient.getMissingValueCoefficient()); - Map adidasPurchasedLast30CoefficientValues = - adidasPurchasedLast30Coefficient.getValueCoefficients(); - assertEquals(2, adidasPurchasedLast30CoefficientValues.size()); - assertEquals(9.0, adidasPurchasedLast30CoefficientValues.get("true")); - assertEquals(0.0, adidasPurchasedLast30CoefficientValues.get("false")); - } -} diff --git a/src/test/java/com/eppo/sdk/helpers/AppDetailsTest.java b/src/test/java/com/eppo/sdk/helpers/AppDetailsTest.java index f284cd6..7286f8e 100644 --- a/src/test/java/com/eppo/sdk/helpers/AppDetailsTest.java +++ b/src/test/java/com/eppo/sdk/helpers/AppDetailsTest.java @@ -49,6 +49,6 @@ public void testAppPropertyReadFailure() { AppDetails appDetails = AppDetails.getInstance(); assertEquals("java-server-sdk", appDetails.getName()); - assertEquals("1.0.0", appDetails.getVersion()); + assertEquals("3.0.0", appDetails.getVersion()); } } diff --git a/src/test/java/com/eppo/sdk/helpers/ConfigurationStoreTest.java b/src/test/java/com/eppo/sdk/helpers/ConfigurationStoreTest.java deleted file mode 100644 index 14e4525..0000000 --- a/src/test/java/com/eppo/sdk/helpers/ConfigurationStoreTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.eppo.sdk.helpers; - -import cloud.eppo.rac.dto.ExperimentConfiguration; -import org.ehcache.Cache; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -class ConfigurationStoreTest { - - ConfigurationStore createConfigurationStore( - Cache experimentConfigurationCache, - ConfigurationRequestor requestor) { - return new ConfigurationStore( - experimentConfigurationCache, - requestor, - // This test doesn't check bandit cache - null, - null); - } - - Cache createExperimentConfigurationCache(int maxEntries) { - CacheHelper cacheHelper = new CacheHelper(); - return cacheHelper.createExperimentConfigurationCache(maxEntries); - } - - @DisplayName("Test ConfigurationStore.setExperimentConfiguration()") - @Test() - void testSetExperimentConfiguration() { - Cache cache = createExperimentConfigurationCache(10); - ConfigurationRequestor requestor = Mockito.mock(ConfigurationRequestor.class); - - ConfigurationStore store = createConfigurationStore(cache, requestor); - store.setExperimentConfiguration( - "key1", new ExperimentConfiguration(null, false, 0, null, null)); - - Assertions.assertInstanceOf( - ExperimentConfiguration.class, store.getExperimentConfiguration("key1")); - } -} diff --git a/src/test/java/com/eppo/sdk/helpers/InputValidatorTest.java b/src/test/java/com/eppo/sdk/helpers/InputValidatorTest.java deleted file mode 100644 index ab54d9a..0000000 --- a/src/test/java/com/eppo/sdk/helpers/InputValidatorTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.eppo.sdk.helpers; - -import cloud.eppo.rac.exception.InvalidInputException; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class InputValidatorTest { - - @DisplayName("Test InputValidator.validateNotBlank() with correct Input") - @Test - void testValidateNotBlankWithCorrectInput() { - Assertions.assertTrue(InputValidator.validateNotBlank("testing", "Testing")); - } - - @DisplayName("Test InputValidator.validateNotBlank() with incorrect Input") - @Test - void testValidateNotBlankWithIncorrectInput() { - Assertions.assertThrows( - InvalidInputException.class, - () -> InputValidator.validateNotBlank("", "Testing data is invalid"), - "Testing data is invalid"); - } -} diff --git a/src/test/java/com/eppo/sdk/helpers/RuleValidatorTest.java b/src/test/java/com/eppo/sdk/helpers/RuleValidatorTest.java deleted file mode 100644 index 47fa163..0000000 --- a/src/test/java/com/eppo/sdk/helpers/RuleValidatorTest.java +++ /dev/null @@ -1,271 +0,0 @@ -package com.eppo.sdk.helpers; - -import cloud.eppo.rac.dto.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class RuleValidatorTest { - public Rule createRule(List conditions) { - return new Rule(null, conditions); - } - - public void addConditionToRule(Rule rule, Condition condition) { - rule.getConditions().add(condition); - } - - public void addNumericConditionToRule(Rule rule) { - Condition condition1 = new Condition(OperatorType.GTE, "price", EppoValue.valueOf(10)); - Condition condition2 = new Condition(OperatorType.LTE, "price", EppoValue.valueOf(20)); - addConditionToRule(rule, condition1); - addConditionToRule(rule, condition2); - } - - public void addSemVerConditionToRule(Rule rule) { - Condition condition1 = new Condition(OperatorType.GTE, "version", EppoValue.valueOf("1.5.0")); - Condition condition2 = new Condition(OperatorType.LT, "version", EppoValue.valueOf("2.2.0")); - addConditionToRule(rule, condition1); - addConditionToRule(rule, condition2); - } - - public void addRegexConditionToRule(Rule rule) { - Condition condition = new Condition(OperatorType.MATCHES, "match", EppoValue.valueOf("[a-z]+")); - addConditionToRule(rule, condition); - } - - public void addOneOfCondition(Rule rule) { - Condition condition = - new Condition( - OperatorType.ONE_OF, "oneOf", EppoValue.valueOf(Arrays.asList("value1", "value2"))); - addConditionToRule(rule, condition); - } - - public void addOneOfConditionWithIntegers(Rule rule) { - Condition condition = - new Condition(OperatorType.ONE_OF, "oneOf", EppoValue.valueOf(Arrays.asList("1", "2"))); - addConditionToRule(rule, condition); - } - - public void addOneOfConditionWithDoubles(Rule rule) { - Condition condition = - new Condition(OperatorType.ONE_OF, "oneOf", EppoValue.valueOf(Arrays.asList("1.5", "2.7"))); - addConditionToRule(rule, condition); - } - - public void addOneOfConditionWithBoolean(Rule rule) { - Condition condition = - new Condition( - OperatorType.ONE_OF, "oneOf", EppoValue.valueOf(Collections.singletonList("true"))); - addConditionToRule(rule, condition); - } - - public void addNotOneOfCondition(Rule rule) { - Condition condition = - new Condition( - OperatorType.NOT_ONE_OF, "oneOf", EppoValue.valueOf(Arrays.asList("value1", "value2"))); - addConditionToRule(rule, condition); - } - - public void addNameToSubjectAttribute(EppoAttributes subjectAttributes) { - subjectAttributes.put("name", EppoValue.valueOf("test")); - } - - public void addPriceToSubjectAttribute(EppoAttributes subjectAttributes) { - subjectAttributes.put("price", EppoValue.valueOf("30")); - } - - @DisplayName("findMatchingRule() with empty conditions") - @Test - void testMatchesAnyRuleWithEmptyConditions() { - List rules = new ArrayList<>(); - final Rule ruleWithEmptyConditions = createRule(new ArrayList<>()); - rules.add(ruleWithEmptyConditions); - EppoAttributes subjectAttributes = new EppoAttributes(); - addNameToSubjectAttribute(subjectAttributes); - - Assertions.assertEquals( - ruleWithEmptyConditions, RuleValidator.findMatchingRule(subjectAttributes, rules).get()); - } - - @DisplayName("findMatchingRule() with empty rules") - @Test - void testMatchesAnyRuleWithEmptyRules() { - List rules = new ArrayList<>(); - EppoAttributes subjectAttributes = new EppoAttributes(); - addNameToSubjectAttribute(subjectAttributes); - - Assertions.assertFalse(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); - } - - @DisplayName("findMatchingRule() when no rule matches") - @Test - void testMatchesAnyRuleWhenNoRuleMatches() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addNumericConditionToRule(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - addPriceToSubjectAttribute(subjectAttributes); - - Assertions.assertFalse(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); - } - - @DisplayName("findMatchingRule() when rule matches") - @Test - void testMatchesAnyRuleWhenRuleMatches() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addNumericConditionToRule(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("price", EppoValue.valueOf(15)); - - Assertions.assertEquals(rule, RuleValidator.findMatchingRule(subjectAttributes, rules).get()); - } - - @DisplayName("findMatchingRule() when rule matches with semver") - @Test - void testMatchesAnyRuleWhenRuleMatchesWithSemVer() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addSemVerConditionToRule(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("version", EppoValue.valueOf("1.15.5")); - - Assertions.assertEquals(rule, RuleValidator.findMatchingRule(subjectAttributes, rules).get()); - } - - @DisplayName("findMatchingRule() throw InvalidSubjectAttribute") - @Test - void testMatchesAnyRuleWhenThrowInvalidSubjectAttribute() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addNumericConditionToRule(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("price", EppoValue.valueOf("abcd")); - - Assertions.assertFalse(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); - } - - @DisplayName("findMatchingRule() with regex condition") - @Test - void testMatchesAnyRuleWithRegexCondition() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addRegexConditionToRule(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("match", EppoValue.valueOf("abcd")); - - Assertions.assertEquals(rule, RuleValidator.findMatchingRule(subjectAttributes, rules).get()); - } - - @DisplayName("findMatchingRule() with regex condition not matched") - @Test - void testMatchesAnyRuleWithRegexConditionNotMatched() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addRegexConditionToRule(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("match", EppoValue.valueOf("123")); - - Assertions.assertFalse(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); - } - - @DisplayName("findMatchingRule() with not oneOf rule") - @Test - void testMatchesAnyRuleWithNotOneOfRule() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addNotOneOfCondition(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("oneOf", EppoValue.valueOf("value3")); - - Assertions.assertEquals(rule, RuleValidator.findMatchingRule(subjectAttributes, rules).get()); - } - - @DisplayName("findMatchingRule() with not oneOf rule not passed") - @Test - void testMatchesAnyRuleWithNotOneOfRuleNotPassed() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addNotOneOfCondition(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("oneOf", EppoValue.valueOf("value1")); - - Assertions.assertFalse(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); - } - - @DisplayName("findMatchingRule() with oneOf rule on a string") - @Test - void testMatchesAnyRuleWithOneOfRuleOnString() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addOneOfCondition(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("oneOf", EppoValue.valueOf("value1")); - - Assertions.assertTrue(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); - } - - @DisplayName("findMatchingRule() with oneOf rule on an integer") - @Test - void testMatchesAnyRuleWithOneOfRuleOnInteger() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addOneOfConditionWithIntegers(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("oneOf", EppoValue.valueOf(2)); - - Assertions.assertTrue(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); - } - - @DisplayName("findMatchingRule() with oneOf rule on a double") - @Test - void testMatchesAnyRuleWithOneOfRuleOnDouble() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addOneOfConditionWithDoubles(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("oneOf", EppoValue.valueOf(1.5)); - - Assertions.assertTrue(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); - } - - @DisplayName("findMatchingRule() with oneOf rule on a boolean") - @Test - void testMatchesAnyRuleWithOneOfRuleOnBoolean() { - List rules = new ArrayList<>(); - Rule rule = createRule(new ArrayList<>()); - addOneOfConditionWithBoolean(rule); - rules.add(rule); - - EppoAttributes subjectAttributes = new EppoAttributes(); - subjectAttributes.put("oneOf", EppoValue.valueOf(true)); - - Assertions.assertTrue(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); - } -} diff --git a/src/test/resources/bandits/bandits-parameters-1.json b/src/test/resources/bandits/bandits-parameters-1.json deleted file mode 100644 index 82d20fc..0000000 --- a/src/test/resources/bandits/bandits-parameters-1.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "updatedAt": "2023-09-13T04:52:06.462Z", - "bandits": [ - { - "banditKey": "banner-bandit", - "modelName": "falcon", - "updatedAt": "2023-09-13T04:52:06.462Z", - "modelVersion": "v123", - "modelData": { - "gamma": 1.0, - "defaultActionScore": 0.0, - "actionProbabilityFloor": 0.0, - "coefficients": { - "nike": { - "actionKey": "nike", - "intercept": 1.0, - "actionNumericCoefficients": [ - { - "attributeKey": "brand_affinity", - "coefficient": 1.0, - "missingValueCoefficient": -0.1 - } - ], - "actionCategoricalCoefficients": [ - { - "attributeKey": "loyalty_tier", - "valueCoefficients": { - "gold": 4.5, - "silver": 3.2, - "bronze": 1.9 - }, - "missingValueCoefficient": 0.0 - } - ], - "subjectNumericCoefficients": [ - { - "attributeKey": "account_age", - "coefficient": 0.3, - "missingValueCoefficient": 0.0 - } - ], - "subjectCategoricalCoefficients": [ - { - "attributeKey": "gender_identity", - "valueCoefficients": { - "female": 0.5, - "male": -0.5 - }, - "missingValueCoefficient": 2.3 - } - ] - }, - "adidas": { - "actionKey": "adidas", - "intercept": 1.1, - "actionNumericCoefficients": [ - { - "attributeKey": "brand_affinity", - "coefficient": 2.0, - "missingValueCoefficient": 1.2 - } - ], - "actionCategoricalCoefficients": [ - { - "attributeKey": "purchased_last_30_days", - "valueCoefficients": { - "true": 9.0, - "false": 0.0 - }, - "missingValueCoefficient": 0.0 - } - ], - "subjectNumericCoefficients": [], - "subjectCategoricalCoefficients": [ - { - "attributeKey": "gender_identity", - "valueCoefficients": { - "female": 0.0, - "male": 0.3 - }, - "missingValueCoefficient": 0.45 - } - ] - } - } - } - }, - { - "banditKey": "cold-start-bandit", - "modelName": "falcon", - "updatedAt": "2023-09-13T04:52:06.462Z", - "modelVersion": "cold start", - "modelData": { - "gamma": 1.0, - "defaultActionScore": 0.0, - "actionProbabilityFloor": 0.0, - "coefficients": {} - } - } - ] -} diff --git a/src/test/resources/bandits/rac-experiments-bandits-beta.json b/src/test/resources/bandits/rac-experiments-bandits-beta.json deleted file mode 100644 index 5e1c281..0000000 --- a/src/test/resources/bandits/rac-experiments-bandits-beta.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "flags": { - "cold-start-bandit-experiment": { - "subjectShards": 10000, - "overrides": {}, - "typedOverrides": {}, - "enabled": true, - "rules": [ - { - "allocationKey": "bandit", - "conditions": [] - } - ], - "allocations": { - "bandit": { - "percentExposure": 1.0, - "variations": [ - { - "name": "control", - "value": "control", - "typedValue": "control", - "shardRange": { - "start": 0, - "end": 2000 - } - }, - { - "name": "bandit", - "value": "cold-start-bandit", - "typedValue": "cold-start-bandit", - "shardRange": { - "start": 2000, - "end": 10000 - }, - "algorithmType": "CONTEXTUAL_BANDIT" - } - ] - } - } - }, - "uninitialized-bandit-experiment": { - "subjectShards": 10000, - "overrides": {}, - "typedOverrides": {}, - "enabled": true, - "rules": [ - { - "allocationKey": "bandit", - "conditions": [] - } - ], - "allocations": { - "bandit": { - "percentExposure": 0.4533, - "variations": [ - { - "name": "control", - "value": "control", - "typedValue": "control", - "shardRange": { - "start": 0, - "end": 2000 - } - }, - { - "name": "bandit", - "value": "this-bandit-does-not-exist", - "typedValue": "this-bandit-does-not-exist", - "shardRange": { - "start": 2000, - "end": 10000 - }, - "algorithmType": "CONTEXTUAL_BANDIT" - } - ] - } - } - }, - "banner-bandit-experiment": { - "subjectShards": 10000, - "overrides": {}, - "typedOverrides": {}, - "enabled": true, - "rules": [ - { - "allocationKey": "bandit", - "conditions": [] - } - ], - "allocations": { - "bandit": { - "percentExposure": 1.0, - "variations": [ - { - "name": "control", - "value": "control", - "typedValue": "control", - "shardRange": { - "start": 0, - "end": 2000 - } - }, - { - "name": "bandit", - "value": "banner-bandit", - "typedValue": "banner-bandit", - "shardRange": { - "start": 2000, - "end": 10000 - }, - "algorithmType": "CONTEXTUAL_BANDIT" - } - ] - } - } - } - } -}