diff --git a/.gitignore b/.gitignore index 3615c2a..5b2cd24 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ litmus-proxy/target litmus-util/litmus-util.iml litmus-util/target -target \ No newline at end of file +target +log +.descriptions.json diff --git a/litmus-client/src/main/java/com/navi/medici/strategy/FlexibleRolloutStrategy.java b/litmus-client/src/main/java/com/navi/medici/strategy/FlexibleRolloutStrategy.java index 429dd63..f2e8974 100644 --- a/litmus-client/src/main/java/com/navi/medici/strategy/FlexibleRolloutStrategy.java +++ b/litmus-client/src/main/java/com/navi/medici/strategy/FlexibleRolloutStrategy.java @@ -33,17 +33,25 @@ public class FlexibleRolloutStrategy implements Strategy { } private Optional resolveStickiness(String stickiness, LitmusContext context) { - return switch (stickiness) { - case "userId" -> context.getUserId(); - case "sessionId" -> context.getSessionId(); - case "deviceId" -> context.getDeviceId(); - case "appVersionCode" -> context.getAppVersionCode(); - case "osType" -> context.getOsType(); - case "random" -> Optional.of(randomGenerator.get()); - case "default" -> Optional.of(context.getUserId() - .orElse(context.getSessionId().orElse(this.randomGenerator.get()))); - default -> context.getByName(stickiness); - }; + switch (stickiness) { + case "userId": + return context.getUserId(); + case "sessionId": + return context.getSessionId(); + case "deviceId": + return context.getDeviceId(); + case "appVersionCode": + return context.getAppVersionCode(); + case "osType": + return context.getOsType(); + case "random": + return Optional.of(randomGenerator.get()); + case "default": + return Optional.of(context.getUserId() + .orElse(context.getSessionId().orElse(this.randomGenerator.get()))); + default: + return context.getByName(stickiness); + } } @Override diff --git a/litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java b/litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java index d09d828..d37085a 100644 --- a/litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java +++ b/litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java @@ -12,104 +12,119 @@ import java.util.function.Predicate; import org.apache.commons.lang3.StringUtils; public final class VariantUtil { - private VariantUtil() {} - private static Predicate overrideMatchesContext(LitmusContext context) { - return (override) -> { - Optional contextValue; - switch (override.getContextName()) { - case "userId" -> contextValue = context.getUserId(); - case "sessionId" -> contextValue = context.getSessionId(); - case "remoteAddress" -> contextValue = context.getRemoteAddress(); - case "appVersionCode" -> contextValue = context.getAppVersionCode(); - case "osType" -> contextValue = context.getOsType(); - case "deviceId" -> contextValue = context.getDeviceId(); - default -> contextValue = Optional.ofNullable(context.getProperties().get(override.getContextName())); - } - return override.getValues().contains(contextValue.orElse("")); - }; - } + private VariantUtil() { + } - private static Optional getOverride( - List variants, LitmusContext context) { - return variants.stream() - .filter( - variant -> - variant.getOverrides().stream() - .anyMatch(overrideMatchesContext(context))) - .findFirst(); - } - - private static String getIdentifier(LitmusContext context) { - return context.getUserId() - .orElse( - context.getSessionId() - .orElse( - context.getRemoteAddress() - .orElse(context.getAppVersionCode() - .orElse(context.getOsType() - .orElse(context.getDeviceId() - .orElse(Double.toString(Math.random()))))) - )); - } - - private static String randomString() { - int randSeed = new Random().nextInt(100000); - return "" + randSeed; - } - - private static String getSeed(LitmusContext litmusContext, Optional stickiness) { - return stickiness - .map(s -> litmusContext.getByName(s).orElse(randomString())) - .orElse(getIdentifier(litmusContext)); - } - - public static Variant selectVariant(LitmusExperiment litmusExperiment, LitmusContext context, Variant defaultVariant) { - if (litmusExperiment == null || StringUtils.isBlank(litmusExperiment.getVariants())) { - return defaultVariant; - } - List variants = JacksonUtils.stringToListObject(litmusExperiment.getVariants(), VariantDefinition.class); - int totalWeight = variants.stream().mapToInt(VariantDefinition::getWeight).sum(); - if (totalWeight == 0) { - return defaultVariant; + private static Predicate overrideMatchesContext(LitmusContext context) { + return (override) -> { + Optional contextValue; + switch (override.getContextName()) { + case "userId": + contextValue = context.getUserId(); + break; + case "sessionId": + contextValue = context.getSessionId(); + break; + case "remoteAddress": + contextValue = context.getRemoteAddress(); + break; + case "appVersionCode": + contextValue = context.getAppVersionCode(); + break; + case "osType": + contextValue = context.getOsType(); + break; + case "deviceId": + contextValue = context.getDeviceId(); + break; + default: + contextValue = Optional.ofNullable(context.getProperties().get(override.getContextName())); } + return override.getValues().contains(contextValue.orElse("")); + }; + } - Optional variantOverride = getOverride(variants, context); - if (variantOverride.isPresent()) { - var variantDefinition = variantOverride.get(); - return Variant.builder() - .name(variantDefinition.getName()) - .payload(variantDefinition.getPayload()) - .enabled(true) - .stickiness(variantDefinition.getStickiness()) - .build(); - } - Optional customStickiness = - variants.stream() - .filter(f -> f.getStickiness() != null) - .map(VariantDefinition::getStickiness) - .findFirst(); - int target = - StrategyUtils.getNormalizedNumber( - getSeed(context, customStickiness), litmusExperiment.getName(), totalWeight); + private static Optional getOverride( + List variants, LitmusContext context) { + return variants.stream() + .filter( + variant -> + variant.getOverrides().stream() + .anyMatch(overrideMatchesContext(context))) + .findFirst(); + } - int counter = 0; - for (final VariantDefinition definition : variants) { - if (definition.getWeight() != 0) { - counter += definition.getWeight(); - if (counter >= target) { - return Variant.builder() - .name(definition.getName()) - .payload(definition.getPayload()) - .enabled(true) - .stickiness(definition.getStickiness()) - .build(); - } - } - } + private static String getIdentifier(LitmusContext context) { + return context.getUserId() + .orElse( + context.getSessionId() + .orElse( + context.getRemoteAddress() + .orElse(context.getAppVersionCode() + .orElse(context.getOsType() + .orElse(context.getDeviceId() + .orElse(Double.toString(Math.random()))))) + )); + } - // Should not happen + private static String randomString() { + int randSeed = new Random().nextInt(100000); + return "" + randSeed; + } + + private static String getSeed(LitmusContext litmusContext, Optional stickiness) { + return stickiness + .map(s -> litmusContext.getByName(s).orElse(randomString())) + .orElse(getIdentifier(litmusContext)); + } + + public static Variant selectVariant(LitmusExperiment litmusExperiment, LitmusContext context, Variant defaultVariant) { + if (litmusExperiment == null || StringUtils.isBlank(litmusExperiment.getVariants())) { + return defaultVariant; + } + List variants = JacksonUtils.stringToListObject(litmusExperiment.getVariants(), VariantDefinition.class); + int totalWeight = variants.stream().mapToInt(VariantDefinition::getWeight).sum(); + if (totalWeight == 0) { return defaultVariant; } + Optional variantOverride = getOverride(variants, context); + if (variantOverride.isPresent()) { + var variantDefinition = variantOverride.get(); + return Variant.builder() + .name(variantDefinition.getName()) + .payload(variantDefinition.getPayload()) + .enabled(true) + .stickiness(variantDefinition.getStickiness()) + .build(); + } + Optional customStickiness = + variants.stream() + .filter(f -> f.getStickiness() != null) + .map(VariantDefinition::getStickiness) + .findFirst(); + int target = + StrategyUtils.getNormalizedNumber( + getSeed(context, customStickiness), litmusExperiment.getName(), totalWeight); + + int counter = 0; + for (final VariantDefinition definition : variants) { + if (definition.getWeight() != 0) { + counter += definition.getWeight(); + if (counter >= target) { + return Variant.builder() + .name(definition.getName()) + .payload(definition.getPayload()) + .enabled(true) + .stickiness(definition.getStickiness()) + .build(); + } + } + } + + // Should not happen + return defaultVariant; + } + } diff --git a/litmus-proxy/src/main/java/com/navi/medici/container/LitmusProxyContainer.java b/litmus-proxy/src/main/java/com/navi/medici/container/LitmusProxyContainer.java index c2bb31f..54d2da6 100644 --- a/litmus-proxy/src/main/java/com/navi/medici/container/LitmusProxyContainer.java +++ b/litmus-proxy/src/main/java/com/navi/medici/container/LitmusProxyContainer.java @@ -11,13 +11,11 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @Component -@RequiredArgsConstructor -public class LitmusProxyContainer { - private final LitmusProxyConfig litmusProxyConfig; +public record LitmusProxyContainer(LitmusProxyConfig litmusProxyConfig) { @Bean public Litmus litmus(RequestMetadata requestMetadata) { - var litmusConfig = LitmusConfig.builder() + var litmusConfig = LitmusConfig.builder() .litmusAPI(litmusProxyConfig.getLitmusEndpoint()) .appName("litmus-proxy") .instanceId("test-instance") diff --git a/litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java b/litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java index efc84ff..de6226d 100644 --- a/litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java +++ b/litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java @@ -12,10 +12,8 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/v1/proxy") -@RequiredArgsConstructor @Log4j2 -public class LitmusProxyController { - private final Litmus litmus; +public record LitmusProxyController(Litmus litmus) { @GetMapping(value = "/experiment", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity fetch(@RequestParam("name") String experimentName) { @@ -24,4 +22,11 @@ public class LitmusProxyController { return ResponseEntity.ok(result); } + @GetMapping(value = "/variant", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity fetchVariants(@RequestParam("name") String variantName) { + var variant = litmus.getVariant(variantName); + + return ResponseEntity.ok(variant); + } + } diff --git a/litmus-proxy/src/main/java/com/navi/medici/provider/CustomLitmusProxyContextProvider.java b/litmus-proxy/src/main/java/com/navi/medici/provider/CustomLitmusProxyContextProvider.java index b493317..79c756e 100644 --- a/litmus-proxy/src/main/java/com/navi/medici/provider/CustomLitmusProxyContextProvider.java +++ b/litmus-proxy/src/main/java/com/navi/medici/provider/CustomLitmusProxyContextProvider.java @@ -1,5 +1,6 @@ package com.navi.medici.provider; +import com.navi.medici.config.LitmusConfig; import com.navi.medici.context.LitmusContext; import com.navi.medici.interceptor.RequestMetadata; import lombok.extern.log4j.Log4j2; @@ -9,17 +10,12 @@ import org.springframework.web.context.annotation.RequestScope; @Component @RequestScope @Log4j2 -public class CustomLitmusProxyContextProvider implements LitmusContextProvider { - - private final RequestMetadata requestMetadata; - - public CustomLitmusProxyContextProvider(RequestMetadata requestMetadata) { - this.requestMetadata = requestMetadata; - } +public record CustomLitmusProxyContextProvider(RequestMetadata requestMetadata) implements LitmusContextProvider { @Override public LitmusContext getContext() { return LitmusContext.builder() + .userId(requestMetadata.customerId().orElseGet(() -> "")) .clickStreamPayload(requestMetadata.getClickStreamData()) .appVersionCode(requestMetadata.getAppVersionCode().orElse("")) .osType(requestMetadata.getOsVersion().orElse(""))