From c7b148d8564b648fac2c6edd80921caeca13077f Mon Sep 17 00:00:00 2001 From: Akshat Soni Date: Sun, 9 Apr 2023 17:08:46 +0530 Subject: [PATCH] Akshat | TP-19381 : add sample size api and added Update flow (#38) * TP-19381 | add sample size api * TP-19381 | refactor sample size api and fixed bug for all filter in teams * TP-19381 | add tests * Akshat | TP-23049 | Add Update Apis for Experiment Flow (#48) * TP-23049 | add metric and segment dropdown * TP-23049 | change ExperimentResponse to DashboardExperimentResponse * TP-23049 | add fetch experiment by name on portal in edit flow * TP-23049 | attach metric to an experiment * TP-23049 | add release, pause and rollback api for experiment * TP-23049 | add required email in release, pause and rollback apiss * TP-23049 | add createdAt, updatedAt and createdBy in experiment response * TP-23049 | add experiment audit trails * TP-23049 | change experimentInfoEntity * TP-23049 | add controller tests * TP-23049 | add variant sum validations * TP-23049 | add mapper test * TP-19381 | change sample size api from POST to GET * TP-19381 | set updated by * TP-19381 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r105137 * TP-19381 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r105138 * TP-19381 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r105139 * TP-19381 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r105140 * TP-19381 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r105143 * TP-19381 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r105146 * TP-19381 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r105147 * TP-19381 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r105153 * TP-19381 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r105153 * TP-19381 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r105159 * TP-19381 | convert Dto to DTO * TP-19381 | Handle null pointer for old experiments * TP-19860 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r105145 * TP-19381 | add groupId while creating experiment * TP-19381 | logged old and new configurations when updating an experiment * TP-19381 | resolve https://github.cmd.navi-tech.in/medici/litmus/pull/38#discussion_r106617 * TP-19381 | add lazy loading for experiment entity for less memory usage on client side * TP-19381 | add tests --- litmus-core/pom.xml | 222 +++++++++--------- .../main/java/com/navi/medici/Constants.java | 9 + .../controller/v1/DropdownController.java | 14 ++ .../controller/v1/ExperimentController.java | 16 +- .../controller/v2/ExperimentControllerV2.java | 76 +++++- .../controller/v2/SegmentControllerV2.java | 3 +- .../navi/medici/mapper/ExperimentMapper.java | 74 +++++- .../service/experiment/ExperimentService.java | 25 +- .../experiment/ExperimentServiceImpl.java | 179 ++++++++++++-- .../medici/service/metric/MetricService.java | 5 + .../service/metric/MetricServiceImpl.java | 12 + .../service/segment/SegmentService.java | 5 + .../service/segment/SegmentServiceImpl.java | 12 + .../ExperimentSpecification.java | 2 +- .../medici/validator/ExperimentValidator.java | 40 +++- .../test/java/com/navi/medici/TestUtils.java | 112 +++++++-- .../controller/v1/DropdownControllerTest.java | 22 ++ .../v2/ExperimentControllerV2Test.java | 87 ++++++- .../medici/mapper/ExperimentMapperTest.java | 48 +++- .../navi/medici/mapper/SegmentMapperTest.java | 25 ++ .../experiment/ExperimentServiceImplTest.java | 101 ++++++-- .../service/metric/MetricServiceImplTest.java | 7 + .../segment/SegmentServiceImplTest.java | 9 + .../java/com/navi/medici/dto/VariantStat.java | 14 -- .../entity/ExperimentAuditTrailEntity.java | 32 +++ .../navi/medici/entity/ExperimentEntity.java | 20 +- .../entity/ExperimentMetricMappingEntity.java | 4 + .../entity/ExperimentMetricResultEntity.java | 4 + .../ExperimentAuditTrailQueryImpl.java | 18 ++ .../IExperimentAuditTrailQuery.java | 7 + .../medici/query/metric/IMetricQuery.java | 3 + .../medici/query/metric/MetricQueryImpl.java | 6 + .../medici/query/segment/ISegmentQuery.java | 3 + .../query/segment/SegmentQueryImpl.java | 16 +- .../ExperimentAuditTrailRepository.java | 9 + .../medici/repository/MetricRepository.java | 6 + .../medici/repository/SegmentRepository.java | 11 +- ...32-create-experiment-audit-trail-table.sql | 13 + litmus-model/pom.xml | 4 + .../medici/dto/ExperimentAuditTrailDTO.java | 21 ++ .../navi/medici/dto/ExperimentInfoDTO.java | 21 ++ .../navi/medici/dto/ExperimentMetadata.java | 0 .../com/navi/medici/dto/MetricResult.java | 0 .../v1/AttachMetricToExperimentRequest.java | 19 ++ .../medici/request/v1/SampleSizeRequest.java | 16 ++ .../response/DashboardExperimentResponse.java | 30 +++ .../medici/response/ExperimentResponse.java | 33 ++- litmus-util/pom.xml | 6 + ...ableToChangeExperimentStatusException.java | 11 + .../VariantWeightSumNotHundredException.java | 10 + .../medici/stats/SampleSizeCalculator.java | 21 ++ 51 files changed, 1225 insertions(+), 238 deletions(-) create mode 100644 litmus-core/src/test/java/com/navi/medici/mapper/SegmentMapperTest.java delete mode 100644 litmus-db/src/main/java/com/navi/medici/dto/VariantStat.java create mode 100644 litmus-db/src/main/java/com/navi/medici/entity/ExperimentAuditTrailEntity.java create mode 100644 litmus-db/src/main/java/com/navi/medici/query/experimentaudittrail/ExperimentAuditTrailQueryImpl.java create mode 100644 litmus-db/src/main/java/com/navi/medici/query/experimentaudittrail/IExperimentAuditTrailQuery.java create mode 100644 litmus-db/src/main/java/com/navi/medici/repository/ExperimentAuditTrailRepository.java create mode 100644 litmus-liquibase/src/main/resources/db/changelog/202303311332-create-experiment-audit-trail-table.sql create mode 100644 litmus-model/src/main/java/com/navi/medici/dto/ExperimentAuditTrailDTO.java create mode 100644 litmus-model/src/main/java/com/navi/medici/dto/ExperimentInfoDTO.java rename {litmus-db => litmus-model}/src/main/java/com/navi/medici/dto/ExperimentMetadata.java (100%) rename {litmus-db => litmus-model}/src/main/java/com/navi/medici/dto/MetricResult.java (100%) create mode 100644 litmus-model/src/main/java/com/navi/medici/request/v1/AttachMetricToExperimentRequest.java create mode 100644 litmus-model/src/main/java/com/navi/medici/request/v1/SampleSizeRequest.java create mode 100644 litmus-model/src/main/java/com/navi/medici/response/DashboardExperimentResponse.java create mode 100644 litmus-util/src/main/java/com/navi/medici/exceptions/UnableToChangeExperimentStatusException.java create mode 100644 litmus-util/src/main/java/com/navi/medici/exceptions/VariantWeightSumNotHundredException.java create mode 100644 litmus-util/src/main/java/com/navi/medici/stats/SampleSizeCalculator.java diff --git a/litmus-core/pom.xml b/litmus-core/pom.xml index 9a5ee77..af1aa91 100644 --- a/litmus-core/pom.xml +++ b/litmus-core/pom.xml @@ -1,134 +1,134 @@ - 4.0.0 - - litmus - com.navi.medici + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + litmus + com.navi.medici + 2.0.8-RELEASE + + + litmus-core 2.0.8-RELEASE - + jar + litmus-core - litmus-core - 2.0.8-RELEASE - jar - litmus-core + + 17 + https://nexus.cmd.navi-tech.in + - - 17 - https://nexus.cmd.navi-tech.in - - - - - nexus - Snapshot - ${nexus.host}/repository/maven-snapshots - - + + + nexus + Snapshot + ${nexus.host}/repository/maven-snapshots + + - - - com.navi.medici - litmus-model - 2.0.8-RELEASE - + + + com.navi.medici + litmus-model + 2.0.8-RELEASE + - - com.navi.medici - litmus-db - 2.0.8-RELEASE - + + com.navi.medici + litmus-db + 2.0.8-RELEASE + - - com.navi.medici - litmus-cache - 2.0.8-RELEASE - + + com.navi.medici + litmus-cache + 2.0.8-RELEASE + - - com.navi.medici - litmus-util - 2.0.8-RELEASE - + + com.navi.medici + litmus-util + 2.0.8-RELEASE + - - org.springframework.boot - spring-boot-starter-web - + + org.springframework.boot + spring-boot-starter-web + - - javax.servlet - javax.servlet-api - 4.0.1 - + + javax.servlet + javax.servlet-api + 4.0.1 + - - software.amazon.awssdk - s3 - 2.17.256 - + + software.amazon.awssdk + s3 + 2.17.256 + - - com.opencsv - opencsv - 5.5.2 - + + com.opencsv + opencsv + 5.5.2 + - - org.springdoc - springdoc-openapi-ui - 1.6.8 - - - org.junit.jupiter - junit-jupiter-api - test - - - org.mockito - mockito-junit-jupiter - test - - - org.springframework - spring-test - test - - - javax.persistence - javax.persistence-api - 2.2 - + + org.springdoc + springdoc-openapi-ui + 1.6.8 + + + org.junit.jupiter + junit-jupiter-api + test + + + org.mockito + mockito-junit-jupiter + test + + + org.springframework + spring-test + test + + + javax.persistence + javax.persistence-api + 2.2 + - - org.hibernate - hibernate-validator - 7.0.4.Final - + + org.hibernate + hibernate-validator + 7.0.4.Final + - - org.mockito - mockito-inline - test - + + org.mockito + mockito-inline + test + - + - - - - org.springframework.boot - spring-boot-maven-plugin - + + + + org.springframework.boot + spring-boot-maven-plugin + - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - - + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + diff --git a/litmus-core/src/main/java/com/navi/medici/Constants.java b/litmus-core/src/main/java/com/navi/medici/Constants.java index b81f6f0..76ac181 100644 --- a/litmus-core/src/main/java/com/navi/medici/Constants.java +++ b/litmus-core/src/main/java/com/navi/medici/Constants.java @@ -3,4 +3,13 @@ package com.navi.medici; public class Constants { public static final String ALL = "ALL"; public static final String HEADER_EMAIL_ID = "X-Email-Id"; + + //Update Messages + public static final String EXPERIMENT_CREATED = "Experiment created"; + public static final String STRATEGIES_UPDATED = "Strategies updated"; + public static final String VARIANTS_UPDATED = "Variants updated"; + public static final String EXPERIMENT_RELEASED = "Experiment has been released"; + public static final String EXPERIMENT_ROLL_BACKED = "Experiment has been roll-backed"; + public static final String PAUSE_EXPERIMENT = "Experiment has been paused"; + public static final String METRIC_ATTACHED = "Metric %s attached to experiment"; } diff --git a/litmus-core/src/main/java/com/navi/medici/controller/v1/DropdownController.java b/litmus-core/src/main/java/com/navi/medici/controller/v1/DropdownController.java index 50c78fc..fb135db 100644 --- a/litmus-core/src/main/java/com/navi/medici/controller/v1/DropdownController.java +++ b/litmus-core/src/main/java/com/navi/medici/controller/v1/DropdownController.java @@ -2,6 +2,8 @@ package com.navi.medici.controller.v1; import com.navi.medici.dto.Dropdown; import com.navi.medici.service.experiment.ExperimentService; +import com.navi.medici.service.metric.MetricService; +import com.navi.medici.service.segment.SegmentService; import com.navi.medici.service.team.TeamService; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -19,6 +21,8 @@ import java.util.List; public class DropdownController { private final ExperimentService experimentService; private final TeamService teamService; + private final MetricService metricService; + private final SegmentService segmentService; @GetMapping("/owners") public ResponseEntity> getOwnersDropdown() { @@ -29,4 +33,14 @@ public class DropdownController { public ResponseEntity> getTeamsDropdown() { return ResponseEntity.ok(teamService.getTeamsDropdown()); } + + @GetMapping("/metrics") + public ResponseEntity> getMetricsDropdown() { + return ResponseEntity.ok(metricService.getMetricsDropdown()); + } + + @GetMapping("/segments") + public ResponseEntity> getSegmentsDropdown() { + return ResponseEntity.ok(segmentService.getSegmentsDropdown()); + } } diff --git a/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java b/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java index 2e45bcd..edf9057 100644 --- a/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java +++ b/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java @@ -1,5 +1,6 @@ package com.navi.medici.controller.v1; +import com.navi.medici.Constants; import com.navi.medici.request.v1.LitmusExperiment; import com.navi.medici.request.v1.LitmusExperimentStrategyUpdate; import com.navi.medici.request.v1.VerticalUpdateRequest; @@ -21,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.HttpServletRequest; import java.util.List; @@ -56,7 +58,7 @@ public class ExperimentController { @GetMapping(value = "/vertical") @Timed(value = "litmus.fetch.vertical.experiment", percentiles = {0.95, 0.99}) public ResponseEntity fetchExperimentsForVertical(@RequestParam("vertical") String vertical, - @RequestParam("polling_time") Long pollingTime) { + @RequestParam("polling_time") Long pollingTime) { log.info("fetch experiment with polling time received. vertical: {}, polling_time: {}", vertical, pollingTime); var collection = experimentService.fetchAllExperimentsForVertical(vertical); @@ -72,16 +74,20 @@ public class ExperimentController { @PutMapping(value = "/attach/variants/{experiment_id}", consumes = MediaType.APPLICATION_JSON_VALUE) @Timed(value = "litmus.attach.variants", percentiles = {0.95, 0.99}) public void attachVariants(@PathVariable("experiment_id") String experimentId, - @RequestBody List variantDefinitions) { - experimentService.attachVariants(experimentId, variantDefinitions); + @RequestBody List variantDefinitions, + HttpServletRequest httpServletRequest) { + String emailId = httpServletRequest.getHeader(Constants.HEADER_EMAIL_ID); + experimentService.attachVariants(experimentId, variantDefinitions, emailId); } @PutMapping(value = "/update/strategies", consumes = MediaType.APPLICATION_JSON_VALUE) @Timed(value = "litmus.update.strategies", percentiles = {0.95, 0.99}) - public void updateStrategies(@RequestBody LitmusExperimentStrategyUpdate litmusExperimentStrategyUpdate) { + public void updateStrategies(@RequestBody LitmusExperimentStrategyUpdate litmusExperimentStrategyUpdate, + HttpServletRequest httpServletRequest) { + String emailId = httpServletRequest.getHeader(Constants.HEADER_EMAIL_ID); log.info("strategy update request received. experiment_id: {}", litmusExperimentStrategyUpdate.getExperimentId()); - experimentService.updateStrategies(litmusExperimentStrategyUpdate); + experimentService.updateStrategies(litmusExperimentStrategyUpdate, emailId); } @PostMapping(value = "/update/vertical", consumes = MediaType.APPLICATION_JSON_VALUE) diff --git a/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java b/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java index 70e1219..1a35649 100644 --- a/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java +++ b/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java @@ -1,9 +1,13 @@ package com.navi.medici.controller.v2; import com.navi.medici.Constants; +import com.navi.medici.dto.ExperimentAuditTrailDTO; import com.navi.medici.enums.ExperimentStatus; +import com.navi.medici.request.v1.AttachMetricToExperimentRequest; import com.navi.medici.request.v1.CreateExperimentRequest; import com.navi.medici.request.v1.ExperimentSearchRequest; +import com.navi.medici.request.v1.SampleSizeRequest; +import com.navi.medici.response.DashboardExperimentResponse; import com.navi.medici.response.ExperimentResponse; import com.navi.medici.response.PaginatedSearchResponse; import com.navi.medici.service.experiment.ExperimentService; @@ -14,13 +18,18 @@ import lombok.extern.log4j.Log4j2; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + @RestController @RequestMapping("/v2/experiments") @Log4j2 @@ -31,11 +40,11 @@ public class ExperimentControllerV2 { @GetMapping @Timed(value = "litmus.get.experiments", percentiles = {0.95, 0.99}) - public ResponseEntity> getExperiments(@RequestParam(value = "query", required = false) String query, - @RequestParam(value = "page", required = false, defaultValue = "1") int page, - @RequestParam(value = "size", required = false, defaultValue = "10") int size, - @RequestParam(value = "status", required = false) String experimentStatus, - @RequestParam(value = "owner", required = false) String owner) { + public ResponseEntity> getExperiments(@RequestParam(value = "query", required = false) String query, + @RequestParam(value = "page", required = false, defaultValue = "1") int page, + @RequestParam(value = "size", required = false, defaultValue = "10") int size, + @RequestParam(value = "status", required = false) String experimentStatus, + @RequestParam(value = "owner", required = false) String owner) { ExperimentSearchRequest request = ExperimentSearchRequest.builder() .query(query) .experimentStatus(ControllerUtils.checkForAllFilter(experimentStatus) ? ExperimentStatus.valueOf(experimentStatus) : null) @@ -44,7 +53,7 @@ public class ExperimentControllerV2 { .size(size) .build(); log.info("fetching experiments with query:{}, experimentStatus:{}, owner:{}, page:{} and size:{}", query, experimentStatus, owner, page, size); - PaginatedSearchResponse response = experimentService.getExperiments(request); + PaginatedSearchResponse response = experimentService.getExperiments(request); return ResponseEntity.ok(response); } @@ -54,4 +63,59 @@ public class ExperimentControllerV2 { @RequestHeader(Constants.HEADER_EMAIL_ID) String emailId) { return ResponseEntity.ok(experimentService.createExperiment(request, emailId)); } + + @GetMapping("/sample-size") + public long getSampleSizeOfExperiment(@RequestParam("baselineConversion") double baselineConversion, + @RequestParam("minimumDetectableEffect") double minimumDetectableEffect, + @RequestParam("confidenceInterval") double confidenceInterval) { + SampleSizeRequest request = SampleSizeRequest.builder() + .baselineConversion(baselineConversion) + .minimumDetectableEffect(minimumDetectableEffect) + .confidenceInterval(confidenceInterval) + .build(); + return experimentService.getSampleSizeForExperiment(request); + } + + @GetMapping("/fetch") + @Timed(value = "litmus.fetch.experiment.api", percentiles = {0.95, 0.99}) + public ResponseEntity fetchExperiment(@RequestParam("name") String name) { + log.info("fetching details of experiment: {}", name); + return ResponseEntity.ok(experimentService.fetchExperiment(name)); + } + + @PutMapping("/attach/metric/{experimentId}") + @Timed(value = "litmus.attach.metric.api", percentiles = {0.95, 0.99}) + public void attachMetric(@PathVariable("experimentId") String experimentId, + @RequestBody AttachMetricToExperimentRequest request, + HttpServletRequest httpServletRequest) { + log.info("Attach metric request received for experimentId: {}, request: {}", experimentId, request); + String emailId = httpServletRequest.getHeader(Constants.HEADER_EMAIL_ID); + experimentService.attachMetric(experimentId, request, emailId); + } + + @PutMapping("/release/{experimentId}") + public void releaseExperiment(@PathVariable("experimentId") String experimentId, + @RequestHeader(Constants.HEADER_EMAIL_ID) String emailId) { + log.info("Release request received for experimentId: {}", experimentId); + experimentService.releaseExperiment(experimentId, emailId); + } + + @PutMapping("/pause/{experimentId}") + public void pauseExperiment(@PathVariable("experimentId") String experimentId, + @RequestHeader(Constants.HEADER_EMAIL_ID) String emailId) { + log.info("Pause request received for experimentId: {}", experimentId); + experimentService.pauseExperiment(experimentId, emailId); + } + + @PutMapping("/rollback/{experimentId}") + public void rollbackExperiment(@PathVariable("experimentId") String experimentId, + @RequestHeader(Constants.HEADER_EMAIL_ID) String emailId) { + log.info("Release request received for experimentId: {}", experimentId); + experimentService.rollbackExperiment(experimentId, emailId); + } + + @GetMapping("/audit-trail/{experimentId}") + public ResponseEntity> getExperimentAuditTrail(@PathVariable("experimentId") String experimentId) { + return ResponseEntity.ok(experimentService.getExperimentAuditTrail(experimentId)); + } } diff --git a/litmus-core/src/main/java/com/navi/medici/controller/v2/SegmentControllerV2.java b/litmus-core/src/main/java/com/navi/medici/controller/v2/SegmentControllerV2.java index 2029581..45e8a19 100644 --- a/litmus-core/src/main/java/com/navi/medici/controller/v2/SegmentControllerV2.java +++ b/litmus-core/src/main/java/com/navi/medici/controller/v2/SegmentControllerV2.java @@ -6,6 +6,7 @@ import com.navi.medici.request.v1.SegmentSearchRequest; import com.navi.medici.response.PaginatedSearchResponse; import com.navi.medici.response.SegmentResponse; import com.navi.medici.service.segment.SegmentService; +import com.navi.medici.util.ControllerUtils; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.http.ResponseEntity; @@ -41,7 +42,7 @@ public class SegmentControllerV2 { @RequestParam(value = "team", required = false) String teamName) { SegmentSearchRequest request = SegmentSearchRequest.builder() .query(query) - .teamName(teamName) + .teamName(ControllerUtils.checkForAllFilter(teamName) ? teamName : null) .page(page) .size(size) .build(); diff --git a/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java b/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java index 6e648b2..965806f 100644 --- a/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java +++ b/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java @@ -1,39 +1,97 @@ package com.navi.medici.mapper; +import com.navi.medici.dto.ExperimentAuditTrailDTO; import com.navi.medici.dto.ExperimentImpact; +import com.navi.medici.dto.ExperimentInfoDTO; +import com.navi.medici.entity.ExperimentAuditTrailEntity; import com.navi.medici.entity.ExperimentEntity; +import com.navi.medici.entity.ExperimentInfoEntity; import com.navi.medici.entity.ExperimentMetricMappingEntity; import com.navi.medici.enums.ExperimentMetricType; +import com.navi.medici.request.v1.AttachMetricToExperimentRequest; +import com.navi.medici.response.DashboardExperimentResponse; import com.navi.medici.response.ExperimentResponse; +import com.navi.medici.strategy.ActivationStrategy; +import com.navi.medici.util.JacksonUtils; +import com.navi.medici.variants.VariantDefinition; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; @Component @RequiredArgsConstructor public class ExperimentMapper { - public ExperimentResponse mapExperimentEntityToExperimentResponse(ExperimentEntity experimentEntity) { - Optional primaryMetric = Objects.nonNull(experimentEntity.getExperimentMetricMappingEntities()) - ? experimentEntity.getExperimentMetricMappingEntities().stream() + private final JacksonUtils jacksonUtils; + + public DashboardExperimentResponse mapExperimentEntityToDashBoardExperimentResponse(ExperimentEntity experimentEntity) { + Optional primaryMetric = Objects.nonNull(experimentEntity.getExperimentMetricMappings()) + ? experimentEntity.getExperimentMetricMappings().stream() .filter(mapping -> ExperimentMetricType.PRIMARY.equals(mapping.getExperimentMetricType())) .findFirst() : Optional.empty(); - return ExperimentResponse.builder() + return DashboardExperimentResponse.builder() .experimentId(experimentEntity.getExperimentId()) .experimentName(experimentEntity.getName()) - .experimentStatus(Objects.nonNull(experimentEntity.getExperimentInfoEntity()) - ? experimentEntity.getExperimentInfoEntity().getExperimentStatus() + .experimentStatus(Objects.nonNull(experimentEntity.getExperimentInfo()) + ? experimentEntity.getExperimentInfo().getExperimentStatus() : null) .impact(ExperimentImpact.builder().build()) .createdBy(experimentEntity.getCreatedBy()) - .testUsers(Objects.nonNull(experimentEntity.getExperimentInfoEntity()) - ? experimentEntity.getExperimentInfoEntity().getTestUsers() + .testUsers(Objects.nonNull(experimentEntity.getExperimentInfo()) + ? experimentEntity.getExperimentInfo().getTestUsers() : 0) .primaryMetric(primaryMetric.isPresent() ? primaryMetric.get().getMetric().getMetricName() : null) .build(); } + + private List getAttachMetricToExperimentRequestsFromExperimentMappings(Set experimentMetricMapping) { + return experimentMetricMapping.stream() + .map(entity -> AttachMetricToExperimentRequest.builder() + .metricName(entity.getMetric().getMetricName()) + .experimentMetricType(entity.getExperimentMetricType()) + .build()) + .toList(); + } + + public ExperimentResponse mapExperimentEntityToExperimentResponse(ExperimentEntity experimentEntity) { + return ExperimentResponse.builder() + .experimentId(experimentEntity.getExperimentId()) + .experimentName(experimentEntity.getName()) + .description(experimentEntity.getDescription()) + .strategies(jacksonUtils.stringToListObject(experimentEntity.getStrategies(), ActivationStrategy.class)) + .variants(jacksonUtils.stringToListObject(experimentEntity.getVariants(), VariantDefinition.class)) + .type(experimentEntity.getType()) + .experimentInfo(mapExperimentInfoEntityToExperimentInfoDTO(experimentEntity.getExperimentInfo())) + .vertical(experimentEntity.getVertical()) + .metrics(getAttachMetricToExperimentRequestsFromExperimentMappings(experimentEntity.getExperimentMetricMappings())) + .createdAt(experimentEntity.getCreatedAt()) + .updatedAt(experimentEntity.getUpdatedAt()) + .createdBy(experimentEntity.getCreatedBy()) + .build(); + } + + public ExperimentInfoDTO mapExperimentInfoEntityToExperimentInfoDTO(ExperimentInfoEntity experimentInfo) { + if (Objects.isNull(experimentInfo)) { + return null; + } + return ExperimentInfoDTO.builder() + .experimentMetadata(experimentInfo.getExperimentMetadata()) + .testUsers(experimentInfo.getTestUsers()) + .experimentStatus(experimentInfo.getExperimentStatus()) + .build(); + } + + public ExperimentAuditTrailDTO mapExperimentAuditTrailEntityToExperimentAuditTrailDTO(ExperimentAuditTrailEntity experimentAuditTrail) { + return ExperimentAuditTrailDTO.builder() + .log(experimentAuditTrail.getLog()) + .createdAt(experimentAuditTrail.getCreatedAt()) + .createdBy(experimentAuditTrail.getCreatedBy()) + .build(); + } } diff --git a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java index fa6bec7..456ddd0 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java +++ b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java @@ -1,11 +1,15 @@ package com.navi.medici.service.experiment; import com.navi.medici.dto.Dropdown; +import com.navi.medici.dto.ExperimentAuditTrailDTO; +import com.navi.medici.request.v1.AttachMetricToExperimentRequest; import com.navi.medici.request.v1.CreateExperimentRequest; import com.navi.medici.request.v1.ExperimentSearchRequest; import com.navi.medici.request.v1.LitmusExperiment; import com.navi.medici.request.v1.LitmusExperimentStrategyUpdate; +import com.navi.medici.request.v1.SampleSizeRequest; import com.navi.medici.request.v1.VerticalUpdateRequest; +import com.navi.medici.response.DashboardExperimentResponse; import com.navi.medici.response.ExperimentResponse; import com.navi.medici.response.LitmusExperimentCollection; import com.navi.medici.response.PaginatedSearchResponse; @@ -21,20 +25,33 @@ public interface ExperimentService { LitmusExperiment fetchExperimentByName(String experimentName); - void attachVariants(String experimentId, List variantDefinitions); + void attachVariants(String experimentId, List variantDefinitions, String emailId); LitmusExperimentCollection fetchAllExperimentsForVerticalInPollingTime(String vertical, Long pollingTime); - void updateStrategies(LitmusExperimentStrategyUpdate litmusExperimentStrategyUpdate); + void updateStrategies(LitmusExperimentStrategyUpdate litmusExperimentStrategyUpdate, String emailId); void updateVertical(VerticalUpdateRequest verticalUpdateRequest); LitmusExperimentCollection fetchAllExperimentsForVertical(String vertical); - PaginatedSearchResponse getExperiments(ExperimentSearchRequest request); + PaginatedSearchResponse getExperiments(ExperimentSearchRequest request); - ExperimentResponse createExperiment(CreateExperimentRequest request, String emailId); + DashboardExperimentResponse createExperiment(CreateExperimentRequest request, String emailId); + + long getSampleSizeForExperiment(SampleSizeRequest request); List getOwnersDropdown(); + ExperimentResponse fetchExperiment(String name); + + void attachMetric(String experimentId, AttachMetricToExperimentRequest request, String emailId); + + void releaseExperiment(String experimentId, String emailId); + + void pauseExperiment(String experimentId, String emailId); + + void rollbackExperiment(String experimentId, String emailId); + + List getExperimentAuditTrail(String experimentId); } diff --git a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java index c480d4a..9ae078b 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java +++ b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java @@ -1,8 +1,11 @@ package com.navi.medici.service.experiment; +import com.navi.medici.Constants; import com.navi.medici.config.LitmusCoreConfig; import com.navi.medici.dto.Dropdown; +import com.navi.medici.dto.ExperimentAuditTrailDTO; import com.navi.medici.dto.ExperimentMetadata; +import com.navi.medici.entity.ExperimentAuditTrailEntity; import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentInfoEntity; import com.navi.medici.entity.ExperimentMetricMappingEntity; @@ -14,23 +17,28 @@ import com.navi.medici.enums.ExperimentType; import com.navi.medici.exceptions.BadRequestException; import com.navi.medici.exceptions.ExperimentAlreadyExistException; import com.navi.medici.exceptions.LitmusExperimentNotFoundException; -import com.navi.medici.exceptions.NoExperimentsFoundForVerticalException; +import com.navi.medici.exceptions.UnableToChangeExperimentStatusException; import com.navi.medici.mapper.ExperimentMapper; import com.navi.medici.query.experiment.IExperimentQuery; +import com.navi.medici.query.experimentaudittrail.IExperimentAuditTrailQuery; import com.navi.medici.query.experimentinfo.IExperimentInfoQuery; import com.navi.medici.query.experimentmetricmapping.IExperimentMetricMappingQuery; import com.navi.medici.query.experimentmetricresult.IExperimentMetricResultQuery; import com.navi.medici.query.metric.IMetricQuery; import com.navi.medici.query.team.ITeamQuery; +import com.navi.medici.request.v1.AttachMetricToExperimentRequest; import com.navi.medici.request.v1.CreateExperimentRequest; import com.navi.medici.request.v1.ExperimentSearchRequest; import com.navi.medici.request.v1.LitmusExperiment; import com.navi.medici.request.v1.LitmusExperimentStrategyUpdate; +import com.navi.medici.request.v1.SampleSizeRequest; import com.navi.medici.request.v1.VerticalUpdateRequest; +import com.navi.medici.response.DashboardExperimentResponse; import com.navi.medici.response.ExperimentResponse; import com.navi.medici.response.LitmusExperimentCollection; import com.navi.medici.response.PaginatedSearchResponse; import com.navi.medici.specification.ExperimentSpecification; +import com.navi.medici.stats.SampleSizeCalculator; import com.navi.medici.strategy.ActivationStrategy; import com.navi.medici.util.JacksonUtils; import com.navi.medici.validator.ExperimentValidator; @@ -46,8 +54,12 @@ import org.springframework.stereotype.Service; import javax.transaction.Transactional; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -65,6 +77,7 @@ public class ExperimentServiceImpl implements ExperimentService { private final ExperimentValidator experimentValidator; private final IMetricQuery metricQuery; private final ITeamQuery teamQuery; + private final IExperimentAuditTrailQuery experimentAuditTrailQuery; private final IExperimentInfoQuery experimentInfoQuery; private final IExperimentMetricResultQuery experimentMetricResultQuery; @@ -72,18 +85,29 @@ public class ExperimentServiceImpl implements ExperimentService { @Override @Transactional - public ExperimentResponse createExperiment(CreateExperimentRequest request, String emailId) { + public DashboardExperimentResponse createExperiment(CreateExperimentRequest request, String emailId) { log.info("Received create experiment request : {}", request.toString()); experimentValidator.validateCreateExperimentRequest(request); Optional team = teamQuery.findByTeamName(request.getVertical()); + List strategies = request.getStrategies(); + List variants = request.getVariants(); + strategies.forEach(strategy -> { + Map parameters = new HashMap<>(strategy.getParameters()); + if (strategy.getName().equals("flexibleRollout")) { + parameters.put("groupId", request.getExperimentName() + "-group-id"); + } else { + parameters.put("groupId", strategy.getParameters().get("segment") + "-group-id"); + } + strategy.setParameters(parameters); + }); ExperimentEntity experiment = ExperimentEntity.builder() .name(request.getExperimentName()) .experimentId(UUID.randomUUID().toString()) .description(request.getDescription()) .enabled(true) .archived(false) - .strategies(jacksonUtils.objectToString(request.getStrategies())) - .variants(jacksonUtils.objectToString(request.getVariants())) + .strategies(jacksonUtils.objectToString(strategies)) + .variants(jacksonUtils.objectToString(variants)) .type(ExperimentType.EXPERIMENT) .vertical(request.getVertical()) .createdBy(emailId) @@ -119,7 +143,8 @@ public class ExperimentServiceImpl implements ExperimentService { experimentMetricMappingQuery.save(secondaryMetricMapping); } ExperimentEntity savedExperiment = experimentQuery.save(experiment); - return experimentMapper.mapExperimentEntityToExperimentResponse(savedExperiment); + saveAuditTrail(experiment.getExperimentId(), Constants.EXPERIMENT_CREATED, emailId); + return experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(savedExperiment); } @Override @@ -153,12 +178,12 @@ public class ExperimentServiceImpl implements ExperimentService { } @Override - public PaginatedSearchResponse getExperiments(ExperimentSearchRequest request) { + public PaginatedSearchResponse getExperiments(ExperimentSearchRequest request) { Pageable paging = PageRequest.of(request.getPage() - 1, request.getSize()); Page experimentEntities = experimentQuery.findAll(ExperimentSpecification.getExperiments(request), paging); - return PaginatedSearchResponse.builder() + return PaginatedSearchResponse.builder() .data(experimentEntities.stream() - .map(experimentMapper::mapExperimentEntityToExperimentResponse) + .map(experimentMapper::mapExperimentEntityToDashBoardExperimentResponse) .collect(Collectors.toList())) .page(experimentEntities.getNumber() + 1) .size(experimentEntities.getSize()) @@ -191,7 +216,8 @@ public class ExperimentServiceImpl implements ExperimentService { } @Override - public void attachVariants(String experimentId, List variantDefinitions) { + public void attachVariants(String experimentId, List variantDefinitions, String emailId) { + experimentValidator.validateAttachVariantsRequest(variantDefinitions); var experimentOptional = experimentQuery.findByExperimentId(experimentId); if (experimentOptional.isEmpty()) { log.error("experiment_id is not found. experiment_id: {}", experimentId); @@ -200,6 +226,11 @@ public class ExperimentServiceImpl implements ExperimentService { var experiment = experimentOptional.get(); experiment.setVariants(jacksonUtils.objectToString(variantDefinitions)); + log.info("updating variants of experiment: {}, new_variants: {}, old_variants: {}", + experiment.getName(), + jacksonUtils.objectToString(variantDefinitions), + experiment.getVariants()); + saveAuditTrail(experimentId, Constants.VARIANTS_UPDATED, emailId); experimentQuery.save(experiment); } @@ -222,19 +253,21 @@ public class ExperimentServiceImpl implements ExperimentService { } @Override - public void updateStrategies(LitmusExperimentStrategyUpdate litmusExperimentStrategyUpdate) { + public void updateStrategies(LitmusExperimentStrategyUpdate litmusExperimentStrategyUpdate, String emailId) { var experimentExist = experimentQuery.findByExperimentId(litmusExperimentStrategyUpdate.getExperimentId()); if (experimentExist.isEmpty()) { throw new LitmusExperimentNotFoundException(String.format("experiment %s not found in system", litmusExperimentStrategyUpdate.getExperimentId())); } - log.info("updating strategies. experiment_id: {}, strategies: {}", - litmusExperimentStrategyUpdate.getExperimentId(), - jacksonUtils.objectToString(litmusExperimentStrategyUpdate.getStrategies())); + log.info("updating strategies. experiment: {}, new_strategies: {}, old_strategies:{}", + experimentExist.get().getName(), + jacksonUtils.objectToString(litmusExperimentStrategyUpdate.getStrategies()), + experimentExist.get().getStrategies()); var litmusExperiment = experimentExist.get(); litmusExperiment.setStrategies(jacksonUtils.objectToString(litmusExperimentStrategyUpdate.getStrategies())); - + saveAuditTrail(litmusExperiment.getExperimentId(), Constants.STRATEGIES_UPDATED, emailId); + litmusExperiment.setUpdatedBy(emailId); experimentQuery.save(litmusExperiment); } @@ -256,7 +289,7 @@ public class ExperimentServiceImpl implements ExperimentService { public LitmusExperimentCollection fetchAllExperimentsForVertical(String vertical) { var experimentEntities = experimentQuery.findByVertical(vertical); if (experimentEntities.isEmpty()) { - throw new NoExperimentsFoundForVerticalException(String.format("no experiments found for this vertical %s", + throw new LitmusExperimentNotFoundException(String.format("no experiments found for this vertical %s", vertical)); } @@ -307,4 +340,120 @@ public class ExperimentServiceImpl implements ExperimentService { .vertical(experimentEntity.getVertical()) .build(); } + + @Override + public long getSampleSizeForExperiment(SampleSizeRequest request) { + double baselineConversion = request.getBaselineConversion(); + double minimumDetectableEffect = request.getMinimumDetectableEffect(); + double confidenceInterval = request.getConfidenceInterval(); + log.info("Calculating sample size for baselineConversion = {}, minimumDetectableEffect = {}, confidenceInterval = {}", + baselineConversion, + minimumDetectableEffect, + confidenceInterval); + double alpha = (100.0 - confidenceInterval) / 100; + double power = 0.8; + return SampleSizeCalculator.sampleCalculator(baselineConversion, minimumDetectableEffect, alpha, power); + } + + @Override + public ExperimentResponse fetchExperiment(String name) { + Optional experiment = experimentQuery.findByName(name); + if (experiment.isEmpty()) { + throw new LitmusExperimentNotFoundException("Experiment with name : " + name + "is not present"); + } + return experimentMapper.mapExperimentEntityToExperimentResponse(experiment.get()); + } + + private ExperimentEntity getExperimentEntityFromId(String experimentId) { + Optional experimentEntity = experimentQuery.findByExperimentId(experimentId); + if (experimentEntity.isEmpty()) { + throw new LitmusExperimentNotFoundException("Experiment with id : " + experimentId + " is not present"); + } + return experimentEntity.get(); + } + + @Override + public void attachMetric(String experimentId, AttachMetricToExperimentRequest request, String emailId) { + experimentValidator.validateAttachMetricRequest(request); + Optional metric = metricQuery.findByMetricName(request.getMetricName()); + ExperimentEntity experiment = getExperimentEntityFromId(experimentId); + ExperimentMetricMappingEntity experimentMetricMapping = ExperimentMetricMappingEntity.builder() + .experiment(experiment) + .metric(metric.get()) + .experimentMetricType(request.getExperimentMetricType()) + .build(); + Set experimentMetricMappings = new HashSet<>(experiment.getExperimentMetricMappings()); + experimentMetricMappings.add(experimentMetricMapping); + experiment.setExperimentMetricMappings(experimentMetricMappings); + experiment.setUpdatedBy(emailId); + saveAuditTrail(experimentId, String.format(Constants.METRIC_ATTACHED, request.getMetricName()), emailId); + + experimentQuery.save(experiment); + } + + @Override + public void releaseExperiment(String experimentId, String emailId) { + ExperimentEntity experiment = getExperimentEntityFromId(experimentId); + log.info("Releasing experiment with experimentId: {}", experimentId); + ExperimentStatus experimentStatus = experiment.getExperimentInfo().getExperimentStatus(); + if (!experimentStatus.equals(ExperimentStatus.RUNNING)) { + throw new UnableToChangeExperimentStatusException("Unable to change release experiment as experiment is in " + experimentStatus.toString() + " state"); + } + experiment.getExperimentInfo().setExperimentStatus(ExperimentStatus.DONE); + experiment.setType(RELEASE); + experiment.setUpdatedBy(emailId); + saveAuditTrail(experimentId, Constants.EXPERIMENT_RELEASED, emailId); + experiment.setUpdatedBy(emailId); + experimentQuery.save(experiment); + } + + @Override + public void pauseExperiment(String experimentId, String emailId) { + ExperimentEntity experiment = getExperimentEntityFromId(experimentId); + log.info("Pausing experiment with experimentId: {}", experimentId); + ExperimentStatus experimentStatus = experiment.getExperimentInfo().getExperimentStatus(); + if (!experimentStatus.equals(ExperimentStatus.RUNNING)) { + throw new UnableToChangeExperimentStatusException("Unable to change pause experiment as experiment is in " + experimentStatus.toString() + " state"); + } + experiment.getExperimentInfo().setExperimentStatus(ExperimentStatus.HOLD); + experiment.setType(KILL_SWITCH); + experiment.setUpdatedBy(emailId); + saveAuditTrail(experimentId, Constants.PAUSE_EXPERIMENT, emailId); + experiment.setUpdatedBy(emailId); + experimentQuery.save(experiment); + } + + @Override + public void rollbackExperiment(String experimentId, String emailId) { + ExperimentEntity experiment = getExperimentEntityFromId(experimentId); + log.info("Rollbacking experiment with experimentId: {}", experimentId); + ExperimentStatus experimentStatus = experiment.getExperimentInfo().getExperimentStatus(); + if (!experimentStatus.equals(ExperimentStatus.RUNNING)) { + throw new UnableToChangeExperimentStatusException("Unable to change rollback experiment as experiment is in " + experimentStatus.toString() + " state"); + } + experiment.getExperimentInfo().setExperimentStatus(ExperimentStatus.DONE); + experiment.setType(KILL_SWITCH); + experiment.setUpdatedBy(emailId); + saveAuditTrail(experimentId, Constants.EXPERIMENT_ROLL_BACKED, emailId); + experiment.setUpdatedBy(emailId); + experimentQuery.save(experiment); + } + + private void saveAuditTrail(String experimentId, String log, String emailId) { + ExperimentEntity experiment = getExperimentEntityFromId(experimentId); + ExperimentAuditTrailEntity experimentAuditTrail = ExperimentAuditTrailEntity.builder() + .experiment(experiment) + .log(log) + .createdBy(emailId) + .build(); + experimentAuditTrailQuery.save(experimentAuditTrail); + } + + @Override + public List getExperimentAuditTrail(String experimentId) { + ExperimentEntity experiment = getExperimentEntityFromId(experimentId); + return experiment.getExperimentAuditTrails().stream() + .map(experimentMapper::mapExperimentAuditTrailEntityToExperimentAuditTrailDTO) + .collect(Collectors.toList()); + } } diff --git a/litmus-core/src/main/java/com/navi/medici/service/metric/MetricService.java b/litmus-core/src/main/java/com/navi/medici/service/metric/MetricService.java index ff2362f..5478110 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/metric/MetricService.java +++ b/litmus-core/src/main/java/com/navi/medici/service/metric/MetricService.java @@ -1,12 +1,17 @@ package com.navi.medici.service.metric; +import com.navi.medici.dto.Dropdown; import com.navi.medici.request.v1.CreateMetricRequest; import com.navi.medici.request.v1.MetricSearchRequest; import com.navi.medici.response.MetricResponse; import com.navi.medici.response.PaginatedSearchResponse; +import java.util.List; + public interface MetricService { MetricResponse createMetric(CreateMetricRequest createMetricRequest, String emailId); PaginatedSearchResponse getMetrics(MetricSearchRequest request); + + List getMetricsDropdown(); } \ No newline at end of file diff --git a/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java index 6f56fcd..d6b31d5 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java +++ b/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java @@ -1,5 +1,6 @@ package com.navi.medici.service.metric; +import com.navi.medici.dto.Dropdown; import com.navi.medici.entity.MetricEntity; import com.navi.medici.mapper.MetricMapper; import com.navi.medici.query.experimentmetricmapping.IExperimentMetricMappingQuery; @@ -69,4 +70,15 @@ public class MetricServiceImpl implements MetricService { .totalSize(metricEntities.getTotalElements()) .build(); } + + @Override + public List getMetricsDropdown() { + List metricNames = metricQuery.getAllMetricNames(); + return metricNames.stream() + .map(metricName -> Dropdown.builder() + .label(metricName) + .value(metricName) + .build()) + .collect(Collectors.toList()); + } } diff --git a/litmus-core/src/main/java/com/navi/medici/service/segment/SegmentService.java b/litmus-core/src/main/java/com/navi/medici/service/segment/SegmentService.java index aa1c4bf..3cdb5cb 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/segment/SegmentService.java +++ b/litmus-core/src/main/java/com/navi/medici/service/segment/SegmentService.java @@ -1,5 +1,6 @@ package com.navi.medici.service.segment; +import com.navi.medici.dto.Dropdown; import com.navi.medici.request.v1.AddIdToSegmentRequest; import com.navi.medici.request.v1.CreateSegmentRequest; import com.navi.medici.request.v1.DeleteIdsFromSegmentRequest; @@ -9,6 +10,8 @@ import com.navi.medici.response.PaginatedSearchResponse; import com.navi.medici.response.SegmentResponse; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + public interface SegmentService { void create(SegmentRequest segmentRequest); @@ -26,4 +29,6 @@ public interface SegmentService { SegmentResponse createSegment(CreateSegmentRequest request, String emailId); PaginatedSearchResponse getSegments(SegmentSearchRequest request); + + List getSegmentsDropdown(); } diff --git a/litmus-core/src/main/java/com/navi/medici/service/segment/SegmentServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/segment/SegmentServiceImpl.java index 3b65fff..3d8e984 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/segment/SegmentServiceImpl.java +++ b/litmus-core/src/main/java/com/navi/medici/service/segment/SegmentServiceImpl.java @@ -1,6 +1,7 @@ package com.navi.medici.service.segment; import com.navi.medici.command.CacheCommands; +import com.navi.medici.dto.Dropdown; import com.navi.medici.entity.SegmentEntity; import com.navi.medici.entity.TeamEntity; import com.navi.medici.exceptions.SegmentAlreadyExistException; @@ -176,4 +177,15 @@ public class SegmentServiceImpl implements SegmentService { .totalSize(segmentEntities.getTotalElements()) .build(); } + + @Override + public List getSegmentsDropdown() { + List semgentNames = segmentQuery.getAllSegmentNames(); + return semgentNames.stream() + .map(semgentName -> Dropdown.builder() + .label(semgentName) + .value(semgentName) + .build()) + .collect(Collectors.toList()); + } } diff --git a/litmus-core/src/main/java/com/navi/medici/specification/ExperimentSpecification.java b/litmus-core/src/main/java/com/navi/medici/specification/ExperimentSpecification.java index 4cd2312..5c06e4e 100644 --- a/litmus-core/src/main/java/com/navi/medici/specification/ExperimentSpecification.java +++ b/litmus-core/src/main/java/com/navi/medici/specification/ExperimentSpecification.java @@ -33,7 +33,7 @@ public class ExperimentSpecification { }); Optional.ofNullable(status).ifPresent(value -> { - Join experimentInfo = root.join("experimentInfoEntity"); + Join experimentInfo = root.join("experimentInfo"); predicateList.add( cb.equal( experimentInfo.get("experimentStatus"), diff --git a/litmus-core/src/main/java/com/navi/medici/validator/ExperimentValidator.java b/litmus-core/src/main/java/com/navi/medici/validator/ExperimentValidator.java index b4c4d9e..52c0363 100644 --- a/litmus-core/src/main/java/com/navi/medici/validator/ExperimentValidator.java +++ b/litmus-core/src/main/java/com/navi/medici/validator/ExperimentValidator.java @@ -2,17 +2,20 @@ package com.navi.medici.validator; import com.navi.medici.entity.MetricEntity; import com.navi.medici.exceptions.BadRequestException; +import com.navi.medici.exceptions.VariantWeightSumNotHundredException; import com.navi.medici.query.experiment.IExperimentQuery; import com.navi.medici.query.metric.IMetricQuery; +import com.navi.medici.request.v1.AttachMetricToExperimentRequest; import com.navi.medici.request.v1.CreateExperimentRequest; +import com.navi.medici.variants.VariantDefinition; import lombok.Builder; import lombok.Data; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.ResponseStatus; +import java.util.List; +import java.util.Objects; import java.util.Optional; @Data @@ -23,7 +26,6 @@ public class ExperimentValidator { private final IExperimentQuery experimentQuery; private final IMetricQuery metricQuery; - @ResponseStatus(HttpStatus.BAD_REQUEST) public void validateCreateExperimentRequest(CreateExperimentRequest request) { if (StringUtils.isBlank(request.getExperimentName()) || request.getExperimentName().contains(" ")) { throw new BadRequestException("Experiment Name should be of the format '---'"); @@ -35,5 +37,37 @@ public class ExperimentValidator { if (primaryMetric.isEmpty()) { throw new BadRequestException("Primary metric required for creation of experiment"); } + if (Objects.nonNull(request.getVariants())) { + int sum = 0; + for (VariantDefinition variant : request.getVariants()) { + sum += variant.getWeight(); + } + if (sum != 100) { + throw new VariantWeightSumNotHundredException("Sum of weights of variants should be 100"); + } + } + } + + public void validateAttachMetricRequest(AttachMetricToExperimentRequest request) { + if (StringUtils.isBlank(request.getMetricName())) { + throw new BadRequestException("Metric Name should not be empty or null"); + } + if (Objects.isNull(request.getExperimentMetricType())) { + throw new BadRequestException("experiment-metric Type can't be null"); + } + Optional metric = metricQuery.findByMetricName(request.getMetricName()); + if (metric.isEmpty()) { + throw new BadRequestException("Metric With name: " + request.getMetricName() + " does not exist"); + } + } + + public void validateAttachVariantsRequest(List variantDefinitions) { + int sum = 0; + for (VariantDefinition variant : variantDefinitions) { + sum += variant.getWeight(); + } + if (sum != 100) { + throw new VariantWeightSumNotHundredException("Sum of weights of variants should be 100"); + } } } diff --git a/litmus-core/src/test/java/com/navi/medici/TestUtils.java b/litmus-core/src/test/java/com/navi/medici/TestUtils.java index bd574a9..99c4296 100644 --- a/litmus-core/src/test/java/com/navi/medici/TestUtils.java +++ b/litmus-core/src/test/java/com/navi/medici/TestUtils.java @@ -2,10 +2,13 @@ package com.navi.medici; import com.fasterxml.jackson.databind.ObjectMapper; import com.navi.medici.dto.Dropdown; +import com.navi.medici.dto.ExperimentAuditTrailDTO; import com.navi.medici.dto.ExperimentImpact; +import com.navi.medici.dto.ExperimentInfoDTO; import com.navi.medici.dto.ExperimentMetadata; -import com.navi.medici.dto.VariantStat; +import com.navi.medici.entity.ExperimentAuditTrailEntity; import com.navi.medici.entity.ExperimentEntity; +import com.navi.medici.entity.ExperimentInfoEntity; import com.navi.medici.entity.ExperimentMetricMappingEntity; import com.navi.medici.entity.MetricEntity; import com.navi.medici.entity.SegmentEntity; @@ -16,6 +19,7 @@ import com.navi.medici.enums.ExperimentType; import com.navi.medici.enums.MetricType; import com.navi.medici.enums.SegmentType; import com.navi.medici.request.v1.AddIdToSegmentRequest; +import com.navi.medici.request.v1.AttachMetricToExperimentRequest; import com.navi.medici.request.v1.CreateExperimentRequest; import com.navi.medici.request.v1.CreateMetricRequest; import com.navi.medici.request.v1.CreateSegmentRequest; @@ -25,9 +29,11 @@ import com.navi.medici.request.v1.ExperimentSearchRequest; import com.navi.medici.request.v1.LitmusExperiment; import com.navi.medici.request.v1.LitmusExperimentStrategyUpdate; import com.navi.medici.request.v1.MetricSearchRequest; +import com.navi.medici.request.v1.SampleSizeRequest; import com.navi.medici.request.v1.SegmentRequest; import com.navi.medici.request.v1.SegmentSearchRequest; import com.navi.medici.request.v1.VerticalUpdateRequest; +import com.navi.medici.response.DashboardExperimentResponse; import com.navi.medici.response.ExperimentResponse; import com.navi.medici.response.MetricResponse; import com.navi.medici.response.SegmentResponse; @@ -47,6 +53,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; @Component @RequiredArgsConstructor @@ -92,19 +99,6 @@ public class TestUtils { .build(); } - public static List getVariantStats(int count) { - List variantStats = new ArrayList<>(); - for (int i = 1; i <= count; i++) { - variantStats.add( - VariantStat.builder() - .variantName(String.format("test variant %d", i)) - .variantMetricValue(1.0) - .build() - ); - } - return variantStats; - } - public static MetricEntity getMetricEntity() { return MetricEntity.builder() .id(1L) @@ -129,14 +123,35 @@ public class TestUtils { .build(); } + public static ExperimentMetricMappingEntity getExperimentMetricMappingEntity() { return ExperimentMetricMappingEntity.builder() - .experiment(getExperimentEntity()) - .metric(getMetricEntity()) + .experiment(ExperimentEntity.builder() + .experimentId("test-uuid-experiment") + .name("test experiment") + .build()) .experimentMetricType(ExperimentMetricType.PRIMARY) + .metric(MetricEntity.builder() + .metricId("test-uuid-metric") + .metricName("test metric") + .build()) .build(); } + + public static ExperimentInfoEntity getExperimentInfoEntity() { + return ExperimentInfoEntity.builder() + .experiment(ExperimentEntity.builder() + .experimentId("test-uuid-experiment") + .name("test experiment") + .build()) + .experimentMetadata(getExperimentMetadata()) + .experimentStatus(ExperimentStatus.CREATED) + .team(getTeamEntity()) + .build(); + } + + public static ExperimentEntity getExperimentEntity() { return ExperimentEntity.builder() .id(1L) @@ -153,8 +168,11 @@ public class TestUtils { .vertical(getTeamEntity().getTeamName()) .createdBy(TEST_USER) .updatedBy(TEST_USER) + .experimentInfo(getExperimentInfoEntity()) + .experimentMetricMappings(Set.of(getExperimentMetricMappingEntity())) .createdAt(getLocalDateTime()) .updatedAt(getLocalDateTime()) + .experimentAuditTrails(Set.of(getExperimentAuditTrailEntity())) .build(); } @@ -165,7 +183,7 @@ public class TestUtils { .archived(false) .primaryMetric("test metric") .secondaryMetric("test metric") - .strategies(new ArrayList<>()) + .strategies(List.of(getActivationStrategy())) .variants(new ArrayList<>()) .type("EXPERIMENT") .vertical("PL") @@ -185,8 +203,8 @@ public class TestUtils { .build(); } - public static ExperimentResponse getExperimentResponse() { - return ExperimentResponse.builder() + public static DashboardExperimentResponse getDashboardExperimentResponse() { + return DashboardExperimentResponse.builder() .experimentId("test-uuid-experiment") .experimentName("test experiment") .createdBy(TEST_USER) @@ -366,4 +384,60 @@ public class TestUtils { .size(15) .build(); } + + public static SampleSizeRequest getSampleSizeRequest() { + return SampleSizeRequest.builder() + .baselineConversion(9.0) + .confidenceInterval(95) + .minimumDetectableEffect(0.3) + .build(); + } + + public static ExperimentAuditTrailEntity getExperimentAuditTrailEntity() { + return ExperimentAuditTrailEntity.builder() + .id(1L) + .log("test log") + .createdBy(TEST_USER) + .createdAt(getLocalDateTime()) + .updatedAt(getLocalDateTime()) + .build(); + } + + public static ExperimentAuditTrailDTO getExperimentAuditTrailDto() { + return ExperimentAuditTrailDTO.builder() + .log("test log") + .createdBy(TEST_USER) + .createdAt(getLocalDateTime()) + .build(); + } + + public static AttachMetricToExperimentRequest getAttachMetricToExperimentRequest() { + return AttachMetricToExperimentRequest.builder() + .metricName(getMetricEntity().getMetricName()) + .experimentMetricType(ExperimentMetricType.SECONDARY) + .build(); + } + + public static ExperimentInfoDTO getExperimentInfoDTO() { + return ExperimentInfoDTO.builder() + .experimentStatus(getExperimentInfoEntity().getExperimentStatus()) + .testUsers(0L) + .experimentMetadata(getExperimentMetadata()) + .build(); + } + + public static ExperimentResponse getExperimentResponse() { + return ExperimentResponse.builder() + .experimentId(getExperimentEntity().getExperimentId()) + .experimentName(getExperimentEntity().getName()) + .description(getExperimentEntity().getDescription()) + .strategies(List.of(getActivationStrategy())) + .variants(getVariants(2)) + .type(getExperimentEntity().getType()) + .experimentInfo(getExperimentInfoDTO()) + .createdBy(TEST_USER) + .createdAt(getLocalDateTime()) + .updatedAt(getLocalDateTime()) + .build(); + } } diff --git a/litmus-core/src/test/java/com/navi/medici/controller/v1/DropdownControllerTest.java b/litmus-core/src/test/java/com/navi/medici/controller/v1/DropdownControllerTest.java index cf457f2..cd45e51 100644 --- a/litmus-core/src/test/java/com/navi/medici/controller/v1/DropdownControllerTest.java +++ b/litmus-core/src/test/java/com/navi/medici/controller/v1/DropdownControllerTest.java @@ -1,6 +1,8 @@ package com.navi.medici.controller.v1; import com.navi.medici.service.experiment.ExperimentService; +import com.navi.medici.service.metric.MetricService; +import com.navi.medici.service.segment.SegmentService; import com.navi.medici.service.team.TeamService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,6 +29,12 @@ class DropdownControllerTest { @Mock private TeamService teamService; + + @Mock + private SegmentService segmentService; + + @Mock + private MetricService metricService; private MockMvc mockMvc; @BeforeEach @@ -47,4 +55,18 @@ class DropdownControllerTest { mockMvc.perform(request) .andExpect(status().isOk()); } + + @Test + void shouldGetMetricsDropdown() throws Exception { + MockHttpServletRequestBuilder request = MockMvcRequestBuilders.get("/v1/dropdown/metrics"); + mockMvc.perform(request) + .andExpect(status().isOk()); + } + + @Test + void shouldGetSegmentsDropdown() throws Exception { + MockHttpServletRequestBuilder request = MockMvcRequestBuilders.get("/v1/dropdown/segments"); + mockMvc.perform(request) + .andExpect(status().isOk()); + } } \ No newline at end of file diff --git a/litmus-core/src/test/java/com/navi/medici/controller/v2/ExperimentControllerV2Test.java b/litmus-core/src/test/java/com/navi/medici/controller/v2/ExperimentControllerV2Test.java index 76ff4ec..d265073 100644 --- a/litmus-core/src/test/java/com/navi/medici/controller/v2/ExperimentControllerV2Test.java +++ b/litmus-core/src/test/java/com/navi/medici/controller/v2/ExperimentControllerV2Test.java @@ -2,10 +2,13 @@ package com.navi.medici.controller.v2; import com.fasterxml.jackson.databind.ObjectMapper; import com.navi.medici.Constants; +import com.navi.medici.TestUtils; import com.navi.medici.enums.ExperimentStatus; +import com.navi.medici.request.v1.AttachMetricToExperimentRequest; import com.navi.medici.request.v1.CreateExperimentRequest; import com.navi.medici.request.v1.ExperimentSearchRequest; -import com.navi.medici.response.ExperimentResponse; +import com.navi.medici.request.v1.SampleSizeRequest; +import com.navi.medici.response.DashboardExperimentResponse; import com.navi.medici.service.experiment.ExperimentService; import com.navi.medici.util.JacksonUtils; import org.junit.jupiter.api.BeforeEach; @@ -20,8 +23,11 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import java.util.ArrayList; +import java.util.Collections; import static org.mockito.ArgumentMatchers.any; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -76,7 +82,7 @@ public class ExperimentControllerV2Test { .minimumDetectableEffect(0.3) .confidenceInterval(95) .build(); - ExperimentResponse response = ExperimentResponse.builder() + DashboardExperimentResponse response = DashboardExperimentResponse.builder() .experimentName("test-experiment") .primaryMetric("primary-metric") .build(); @@ -91,4 +97,81 @@ public class ExperimentControllerV2Test { mockMvc.perform(httpRequest) .andExpect(status().isOk()); } + + @Test + void shouldGetSampleSize() throws Exception { + SampleSizeRequest request = TestUtils.getSampleSizeRequest(); + MultiValueMap map = new LinkedMultiValueMap(); + map.put("baselineConversion", Collections.singletonList("9.0")); + map.put("confidenceInterval", Collections.singletonList("95.0")); + map.put("minimumDetectableEffect", Collections.singletonList("0.3")); + MockHttpServletRequestBuilder httpRequest = MockMvcRequestBuilders.get("/v2/experiments/sample-size") + .params(map); + + mockMvc.perform(httpRequest) + .andExpect(status().isOk()); + } + + @Test + void shouldFetchExperiment() throws Exception { + String name = TestUtils.getExperimentEntity().getName(); + MockHttpServletRequestBuilder httpRequest = MockMvcRequestBuilders.get("/v2/experiments/fetch") + .param("name", name); + + mockMvc.perform(httpRequest) + .andExpect(status().isOk()); + } + + @Test + void shouldAttachMetric() throws Exception { + AttachMetricToExperimentRequest request = TestUtils.getAttachMetricToExperimentRequest(); + MockHttpServletRequestBuilder httpRequest = MockMvcRequestBuilders.put("/v2/experiments/attach/metric/experimentId") + .contentType(MediaType.APPLICATION_JSON) + .content(jacksonUtils.objectToString(request)); + + mockMvc.perform(httpRequest) + .andExpect(status().isOk()); + } + + @Test + void shouldReleaseExperiment() throws Exception { + String experimentId = TestUtils.getExperimentEntity().getExperimentId(); + String emailId = TestUtils.TEST_USER; + MockHttpServletRequestBuilder httpRequest = MockMvcRequestBuilders.put("/v2/experiments/release/" + experimentId) + .header(Constants.HEADER_EMAIL_ID, TestUtils.TEST_USER); + + mockMvc.perform(httpRequest) + .andExpect(status().isOk()); + } + + @Test + void shouldPauseExperiment() throws Exception { + String experimentId = TestUtils.getExperimentEntity().getExperimentId(); + String emailId = TestUtils.TEST_USER; + MockHttpServletRequestBuilder httpRequest = MockMvcRequestBuilders.put("/v2/experiments/pause/" + experimentId) + .header(Constants.HEADER_EMAIL_ID, TestUtils.TEST_USER); + + mockMvc.perform(httpRequest) + .andExpect(status().isOk()); + } + + @Test + void shouldRollbackExperiment() throws Exception { + String experimentId = TestUtils.getExperimentEntity().getExperimentId(); + String emailId = TestUtils.TEST_USER; + MockHttpServletRequestBuilder httpRequest = MockMvcRequestBuilders.put("/v2/experiments/rollback/" + experimentId) + .header(Constants.HEADER_EMAIL_ID, TestUtils.TEST_USER); + + mockMvc.perform(httpRequest) + .andExpect(status().isOk()); + } + + @Test + void shouldGetExperimentAuditTrail() throws Exception { + String experimentId = TestUtils.getExperimentEntity().getExperimentId(); + MockHttpServletRequestBuilder httpRequest = MockMvcRequestBuilders.get("/v2/experiments/audit-trail/" + experimentId); + + mockMvc.perform(httpRequest) + .andExpect(status().isOk()); + } } \ No newline at end of file diff --git a/litmus-core/src/test/java/com/navi/medici/mapper/ExperimentMapperTest.java b/litmus-core/src/test/java/com/navi/medici/mapper/ExperimentMapperTest.java index 1622756..e8d86c8 100644 --- a/litmus-core/src/test/java/com/navi/medici/mapper/ExperimentMapperTest.java +++ b/litmus-core/src/test/java/com/navi/medici/mapper/ExperimentMapperTest.java @@ -1,13 +1,21 @@ package com.navi.medici.mapper; import com.navi.medici.TestUtils; +import com.navi.medici.dto.ExperimentAuditTrailDTO; import com.navi.medici.dto.ExperimentImpact; +import com.navi.medici.dto.ExperimentInfoDTO; +import com.navi.medici.entity.ExperimentAuditTrailEntity; import com.navi.medici.entity.ExperimentEntity; +import com.navi.medici.entity.ExperimentInfoEntity; +import com.navi.medici.enums.ExperimentStatus; +import com.navi.medici.response.DashboardExperimentResponse; import com.navi.medici.response.ExperimentResponse; +import com.navi.medici.util.JacksonUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Set; @@ -17,23 +25,47 @@ class ExperimentMapperTest { @InjectMocks private ExperimentMapper experimentMapper; + @Mock + private JacksonUtils jacksonUtils; + @Test - void shouldMapExperimentEntityToExperimentResponse() { + void shouldMapExperimentEntityToDashboardExperimentResponse() { ExperimentEntity experimentEntity = ExperimentEntity.builder() .name("experiment-name") .experimentId("uuid") .createdBy("sample-user") - .experimentMetricMappingEntities(Set.of(TestUtils.getExperimentMetricMappingEntity())) + .experimentMetricMappings(Set.of(TestUtils.getExperimentMetricMappingEntity())) .build(); - ExperimentResponse experimentResponse = experimentMapper.mapExperimentEntityToExperimentResponse(experimentEntity); - Assertions.assertEquals("test metric", experimentResponse.getPrimaryMetric()); + DashboardExperimentResponse dashboardExperimentResponse = experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(experimentEntity); + Assertions.assertEquals("test metric", dashboardExperimentResponse.getPrimaryMetric()); Assertions.assertEquals(ExperimentImpact.builder() .control(0.0) .treatment(0.0) .flag(null) - .build().toString(), experimentResponse.getImpact().toString()); - Assertions.assertEquals("uuid", experimentResponse.getExperimentId()); - Assertions.assertEquals("experiment-name", experimentResponse.getExperimentName()); - Assertions.assertEquals(0, experimentResponse.getTestUsers()); + .build().toString(), dashboardExperimentResponse.getImpact().toString()); + Assertions.assertEquals("uuid", dashboardExperimentResponse.getExperimentId()); + Assertions.assertEquals("experiment-name", dashboardExperimentResponse.getExperimentName()); + Assertions.assertEquals(0, dashboardExperimentResponse.getTestUsers()); + } + + @Test + void shouldMapExperimentEntityToExperimentResponse() { + ExperimentEntity experiment = TestUtils.getExperimentEntity(); + ExperimentResponse experimentResponse = experimentMapper.mapExperimentEntityToExperimentResponse(experiment); + Assertions.assertEquals("test experiment", experimentResponse.getExperimentName()); + } + + @Test + void shouldMapExperimentInfoEntityToExperimentInfoDto() { + ExperimentInfoEntity experimentInfo = TestUtils.getExperimentInfoEntity(); + ExperimentInfoDTO experimentInfoDto = experimentMapper.mapExperimentInfoEntityToExperimentInfoDTO(experimentInfo); + Assertions.assertEquals(ExperimentStatus.CREATED, experimentInfoDto.getExperimentStatus()); + } + + @Test + void shouldMapExperimentAuditTrailEntityToExperimentAuditTrailDto() { + ExperimentAuditTrailEntity experimentAuditTrail = TestUtils.getExperimentAuditTrailEntity(); + ExperimentAuditTrailDTO experimentAuditTrailDto = experimentMapper.mapExperimentAuditTrailEntityToExperimentAuditTrailDTO(experimentAuditTrail); + Assertions.assertEquals("test log", experimentAuditTrailDto.getLog()); } } \ No newline at end of file diff --git a/litmus-core/src/test/java/com/navi/medici/mapper/SegmentMapperTest.java b/litmus-core/src/test/java/com/navi/medici/mapper/SegmentMapperTest.java new file mode 100644 index 0000000..0cfc678 --- /dev/null +++ b/litmus-core/src/test/java/com/navi/medici/mapper/SegmentMapperTest.java @@ -0,0 +1,25 @@ +package com.navi.medici.mapper; + +import com.navi.medici.TestUtils; +import com.navi.medici.entity.SegmentEntity; +import com.navi.medici.response.SegmentResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(MockitoExtension.class) +class SegmentMapperTest { + + @InjectMocks + private SegmentMapper segmentMapper; + + @Test + void shouldMapSegmentEntityToSegmentResponse() { + SegmentEntity segment = TestUtils.getSegmentEntity(); + SegmentResponse response = segmentMapper.mapSegmentEntityToSegmentResponse(segment, 2); + assertEquals("test-uuid-segment", response.getSegmentId()); + } +} \ No newline at end of file diff --git a/litmus-core/src/test/java/com/navi/medici/service/experiment/ExperimentServiceImplTest.java b/litmus-core/src/test/java/com/navi/medici/service/experiment/ExperimentServiceImplTest.java index b6e7632..d305178 100644 --- a/litmus-core/src/test/java/com/navi/medici/service/experiment/ExperimentServiceImplTest.java +++ b/litmus-core/src/test/java/com/navi/medici/service/experiment/ExperimentServiceImplTest.java @@ -4,25 +4,31 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.navi.medici.TestUtils; import com.navi.medici.config.LitmusCoreConfig; import com.navi.medici.dto.Dropdown; +import com.navi.medici.dto.ExperimentAuditTrailDTO; +import com.navi.medici.entity.ExperimentAuditTrailEntity; import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.MetricEntity; import com.navi.medici.entity.TeamEntity; +import com.navi.medici.enums.ExperimentStatus; import com.navi.medici.enums.ExperimentType; import com.navi.medici.exceptions.BadRequestException; import com.navi.medici.exceptions.ExperimentAlreadyExistException; import com.navi.medici.exceptions.LitmusExperimentNotFoundException; -import com.navi.medici.exceptions.NoExperimentsFoundForVerticalException; import com.navi.medici.mapper.ExperimentMapper; import com.navi.medici.query.experiment.IExperimentQuery; +import com.navi.medici.query.experimentaudittrail.IExperimentAuditTrailQuery; import com.navi.medici.query.experimentinfo.IExperimentInfoQuery; import com.navi.medici.query.experimentmetricmapping.IExperimentMetricMappingQuery; import com.navi.medici.query.metric.IMetricQuery; import com.navi.medici.query.team.ITeamQuery; +import com.navi.medici.request.v1.AttachMetricToExperimentRequest; import com.navi.medici.request.v1.CreateExperimentRequest; import com.navi.medici.request.v1.ExperimentSearchRequest; import com.navi.medici.request.v1.LitmusExperiment; import com.navi.medici.request.v1.LitmusExperimentStrategyUpdate; +import com.navi.medici.request.v1.SampleSizeRequest; import com.navi.medici.request.v1.VerticalUpdateRequest; +import com.navi.medici.response.DashboardExperimentResponse; import com.navi.medici.response.ExperimentResponse; import com.navi.medici.response.LitmusExperimentCollection; import com.navi.medici.response.PaginatedSearchResponse; @@ -48,6 +54,7 @@ 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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; @ExtendWith(MockitoExtension.class) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) @@ -82,22 +89,25 @@ class ExperimentServiceImplTest { @Mock private JacksonUtils jacksonUtils; + @Mock + private IExperimentAuditTrailQuery experimentAuditTrailQuery; @Test void shouldCreateExperiment() { CreateExperimentRequest request = TestUtils.getCreateExperimentRequest(); String emailId = TestUtils.TEST_USER; - ExperimentResponse response = TestUtils.getExperimentResponse(); + DashboardExperimentResponse response = TestUtils.getDashboardExperimentResponse(); MetricEntity metricEntity = TestUtils.getMetricEntity(); TeamEntity team = TestUtils.getTeamEntity(); Mockito.when(metricQuery.findByMetricName(any())).thenReturn(Optional.ofNullable(metricEntity)); - Mockito.when(experimentMapper.mapExperimentEntityToExperimentResponse(any())).thenReturn(response); + Mockito.when(experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(any())).thenReturn(response); Mockito.when(jacksonUtils.objectToString(any())).thenReturn(" "); Mockito.when(teamQuery.findByTeamName(any())).thenReturn(Optional.ofNullable(team)); - ExperimentResponse experimentResponse = experimentService.createExperiment(request, emailId); - assertEquals("test experiment", experimentResponse.getExperimentName()); - assertEquals("test metric", experimentResponse.getPrimaryMetric()); + Mockito.when(experimentQuery.findByExperimentId(any())).thenReturn(Optional.ofNullable(TestUtils.getExperimentEntity())); + DashboardExperimentResponse dashboardExperimentResponse = experimentService.createExperiment(request, emailId); + assertEquals("test experiment", dashboardExperimentResponse.getExperimentName()); + assertEquals("test metric", dashboardExperimentResponse.getPrimaryMetric()); } @Test @@ -125,10 +135,10 @@ class ExperimentServiceImplTest { ExperimentSearchRequest request = TestUtils.getExperimentSearchRequest(); Pageable paging = PageRequest.of(request.getPage() - 1, request.getSize()); Page experimentEntities = new PageImpl<>(List.of(TestUtils.getExperimentEntity()), paging, 1); - ExperimentResponse experimentResponse = TestUtils.getExperimentResponse(); + DashboardExperimentResponse dashboardExperimentResponse = TestUtils.getDashboardExperimentResponse(); Mockito.when(experimentQuery.findAll(any(), any())).thenReturn(experimentEntities); - Mockito.when(experimentMapper.mapExperimentEntityToExperimentResponse(any())).thenReturn(experimentResponse); - PaginatedSearchResponse response = experimentService.getExperiments(request); + Mockito.when(experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(any())).thenReturn(dashboardExperimentResponse); + PaginatedSearchResponse response = experimentService.getExperiments(request); assertEquals(1, response.getTotalSize()); assertEquals("test experiment", response.getData().get(0).getExperimentName()); } @@ -156,7 +166,7 @@ class ExperimentServiceImplTest { Optional experiment = Optional.ofNullable(TestUtils.getExperimentEntity()); List variants = TestUtils.getVariants(2); Mockito.when(experimentQuery.findByExperimentId(any(String.class))).thenReturn(experiment); - assertDoesNotThrow(() -> experimentService.attachVariants(experiment.get().getExperimentId(), variants)); + assertDoesNotThrow(() -> experimentService.attachVariants(experiment.get().getExperimentId(), variants, TestUtils.TEST_USER)); } @Test @@ -164,7 +174,7 @@ class ExperimentServiceImplTest { Optional experiment = Optional.ofNullable(TestUtils.getExperimentEntity()); List variants = TestUtils.getVariants(2); Mockito.when(experimentQuery.findByExperimentId(any(String.class))).thenReturn(Optional.empty()); - assertThrows(BadRequestException.class, () -> experimentService.attachVariants(experiment.get().getExperimentId(), variants)); + assertThrows(BadRequestException.class, () -> experimentService.attachVariants(experiment.get().getExperimentId(), variants, TestUtils.TEST_USER)); } @Test @@ -181,14 +191,14 @@ class ExperimentServiceImplTest { LitmusExperimentStrategyUpdate litmusExperimentStrategyUpdate = TestUtils.getLitmusExperimentStrategyUpdate(); Optional experiment = Optional.ofNullable(TestUtils.getExperimentEntity()); Mockito.when(experimentQuery.findByExperimentId(any(String.class))).thenReturn(experiment); - assertDoesNotThrow(() -> experimentService.updateStrategies(litmusExperimentStrategyUpdate)); + assertDoesNotThrow(() -> experimentService.updateStrategies(litmusExperimentStrategyUpdate, TestUtils.TEST_USER)); } @Test void shouldThrowWhenUpdateStrategies() { LitmusExperimentStrategyUpdate litmusExperimentStrategyUpdate = TestUtils.getLitmusExperimentStrategyUpdate(); Mockito.when(experimentQuery.findByExperimentId(any(String.class))).thenReturn(Optional.empty()); - assertThrows(LitmusExperimentNotFoundException.class, () -> experimentService.updateStrategies(litmusExperimentStrategyUpdate)); + assertThrows(LitmusExperimentNotFoundException.class, () -> experimentService.updateStrategies(litmusExperimentStrategyUpdate, TestUtils.TEST_USER)); } @Test @@ -221,7 +231,7 @@ class ExperimentServiceImplTest { void shouldThrowWhenFetchAllExperimentsForVertical() { String vertical = TestUtils.getTeamEntity().getTeamName(); Mockito.when(experimentQuery.findByVertical(any(String.class))).thenReturn(List.of()); - assertThrows(NoExperimentsFoundForVerticalException.class, () -> experimentService.fetchAllExperimentsForVertical(vertical)); + assertThrows(LitmusExperimentNotFoundException.class, () -> experimentService.fetchAllExperimentsForVertical(vertical)); } @Test @@ -232,4 +242,67 @@ class ExperimentServiceImplTest { assertEquals("test user", ownerDropdown.get(1).getLabel()); assertEquals("test user", ownerDropdown.get(1).getValue()); } + + @Test + void shouldGetSampleSizeForExperiment() { + SampleSizeRequest request = TestUtils.getSampleSizeRequest(); + assertEquals(143491, experimentService.getSampleSizeForExperiment(request)); + } + + @Test + void shouldGetExperimentAuditTrail() { + ExperimentEntity experiment = TestUtils.getExperimentEntity(); + Mockito.when(experimentQuery.findByExperimentId(anyString())).thenReturn(Optional.ofNullable(experiment)); + Mockito.when(experimentMapper.mapExperimentAuditTrailEntityToExperimentAuditTrailDTO(any(ExperimentAuditTrailEntity.class))) + .thenReturn(TestUtils.getExperimentAuditTrailDto()); + List experimentAuditTrailDTOs = experimentService.getExperimentAuditTrail(experiment.getExperimentId()); + assertEquals("test log", experimentAuditTrailDTOs.get(0).getLog()); + } + + @Test + void shouldFetchExperiment() { + Optional experiment = Optional.ofNullable(TestUtils.getExperimentEntity()); + Mockito.when(experimentQuery.findByName(anyString())).thenReturn(experiment); + Mockito.when(experimentMapper.mapExperimentEntityToExperimentResponse(experiment.get())).thenReturn(TestUtils.getExperimentResponse()); + ExperimentResponse response = experimentService.fetchExperiment(experiment.get().getName()); + assertEquals("test experiment", response.getExperimentName()); + } + + @Test + void shouldAttachMetric() { + ExperimentEntity experiment = TestUtils.getExperimentEntity(); + AttachMetricToExperimentRequest request = TestUtils.getAttachMetricToExperimentRequest(); + String emailId = TestUtils.TEST_USER; + MetricEntity metric = TestUtils.getMetricEntity(); + Mockito.when(metricQuery.findByMetricName(metric.getMetricName())).thenReturn(Optional.of(metric)); + Mockito.when(experimentQuery.findByExperimentId(experiment.getExperimentId())).thenReturn(Optional.of(experiment)); + assertDoesNotThrow(() -> experimentService.attachMetric(experiment.getExperimentId(), request, emailId)); + } + + @Test + void shouldReleaseExperiment() { + ExperimentEntity experiment = TestUtils.getExperimentEntity(); + experiment.getExperimentInfo().setExperimentStatus(ExperimentStatus.RUNNING); + Mockito.when(experimentQuery.findByExperimentId(experiment.getExperimentId())) + .thenReturn(Optional.of(experiment)); + assertDoesNotThrow(() -> experimentService.releaseExperiment(experiment.getExperimentId(), TestUtils.TEST_USER)); + } + + @Test + void shouldPauseExperiment() { + ExperimentEntity experiment = TestUtils.getExperimentEntity(); + experiment.getExperimentInfo().setExperimentStatus(ExperimentStatus.RUNNING); + Mockito.when(experimentQuery.findByExperimentId(experiment.getExperimentId())) + .thenReturn(Optional.of(experiment)); + assertDoesNotThrow(() -> experimentService.pauseExperiment(experiment.getExperimentId(), TestUtils.TEST_USER)); + } + + @Test + void shouldRollBackExperiment() { + ExperimentEntity experiment = TestUtils.getExperimentEntity(); + experiment.getExperimentInfo().setExperimentStatus(ExperimentStatus.RUNNING); + Mockito.when(experimentQuery.findByExperimentId(experiment.getExperimentId())) + .thenReturn(Optional.of(experiment)); + assertDoesNotThrow(() -> experimentService.rollbackExperiment(experiment.getExperimentId(), TestUtils.TEST_USER)); + } } \ No newline at end of file diff --git a/litmus-core/src/test/java/com/navi/medici/service/metric/MetricServiceImplTest.java b/litmus-core/src/test/java/com/navi/medici/service/metric/MetricServiceImplTest.java index 893da72..6de1bde 100644 --- a/litmus-core/src/test/java/com/navi/medici/service/metric/MetricServiceImplTest.java +++ b/litmus-core/src/test/java/com/navi/medici/service/metric/MetricServiceImplTest.java @@ -72,4 +72,11 @@ class MetricServiceImplTest { PaginatedSearchResponse response = metricService.getMetrics(request); assertEquals("test metric", response.getData().get(0).getMetricName()); } + + @Test + void shouldGetMetricsDropdown() { + List metricNames = List.of(TestUtils.getMetricEntity().getMetricName()); + Mockito.when(metricQuery.getAllMetricNames()).thenReturn(metricNames); + assertEquals("test metric", metricService.getMetricsDropdown().get(0).getLabel()); + } } \ No newline at end of file diff --git a/litmus-core/src/test/java/com/navi/medici/service/segment/SegmentServiceImplTest.java b/litmus-core/src/test/java/com/navi/medici/service/segment/SegmentServiceImplTest.java index 7928393..8fb0fba 100644 --- a/litmus-core/src/test/java/com/navi/medici/service/segment/SegmentServiceImplTest.java +++ b/litmus-core/src/test/java/com/navi/medici/service/segment/SegmentServiceImplTest.java @@ -2,6 +2,7 @@ package com.navi.medici.service.segment; import com.navi.medici.TestUtils; import com.navi.medici.command.CacheCommands; +import com.navi.medici.dto.Dropdown; import com.navi.medici.entity.SegmentEntity; import com.navi.medici.entity.TeamEntity; import com.navi.medici.exceptions.SegmentAlreadyExistException; @@ -180,4 +181,12 @@ class SegmentServiceImplTest { assertEquals(1, response.getData().size()); assertEquals("test segment", response.getData().get(0).getSegmentName()); } + + @Test + void shouldGetSegmentsDropdown() { + List segmentNames = List.of(TestUtils.getSegmentEntity().getSegmentName()); + Mockito.when(segmentQuery.getAllSegmentNames()).thenReturn(segmentNames); + List dropdowns = segmentService.getSegmentsDropdown(); + assertEquals("test segment", dropdowns.get(0).getLabel()); + } } \ No newline at end of file diff --git a/litmus-db/src/main/java/com/navi/medici/dto/VariantStat.java b/litmus-db/src/main/java/com/navi/medici/dto/VariantStat.java deleted file mode 100644 index 028949d..0000000 --- a/litmus-db/src/main/java/com/navi/medici/dto/VariantStat.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.navi.medici.dto; - -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Data; -import lombok.experimental.FieldDefaults; - -@Builder -@Data -@FieldDefaults(level = AccessLevel.PRIVATE) -public class VariantStat { - String variantName; - double variantMetricValue; -} diff --git a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentAuditTrailEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentAuditTrailEntity.java new file mode 100644 index 0000000..bf8be0c --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentAuditTrailEntity.java @@ -0,0 +1,32 @@ +package com.navi.medici.entity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; + +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "experiment_audit_trail") +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ExperimentAuditTrailEntity extends BaseEntity { + + @ManyToOne + @JoinColumn(name = "experiment_id") + ExperimentEntity experiment; + + String log; + String createdBy; +} diff --git a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java index 7ac47d8..688bcec 100644 --- a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java +++ b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java @@ -8,11 +8,14 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.FieldDefaults; import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.Cascade; +import org.hibernate.annotations.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; +import javax.persistence.FetchType; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.Table; @@ -68,12 +71,19 @@ public class ExperimentEntity extends BaseEntity implements Serializable { String updatedBy; - @OneToMany(mappedBy = "experiment") - Set experimentMetricMappingEntities; + @OneToMany(fetch = FetchType.LAZY, mappedBy = "experiment") + @Cascade(CascadeType.ALL) + Set experimentMetricMappings; @OneToOne(mappedBy = "experiment") - ExperimentInfoEntity experimentInfoEntity; + @Cascade(CascadeType.ALL) + ExperimentInfoEntity experimentInfo; - @OneToMany(mappedBy = "experiment") - Set experimentMetricResultEntities; + @OneToMany(fetch = FetchType.LAZY, mappedBy = "experiment") + @Cascade(CascadeType.ALL) + Set experimentMetricResults; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "experiment") + @Cascade(CascadeType.ALL) + Set experimentAuditTrails; } diff --git a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricMappingEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricMappingEntity.java index 2719c13..1456b63 100644 --- a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricMappingEntity.java +++ b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricMappingEntity.java @@ -8,6 +8,8 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.FieldDefaults; import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.Cascade; +import org.hibernate.annotations.CascadeType; import javax.persistence.Entity; import javax.persistence.EnumType; @@ -28,10 +30,12 @@ public class ExperimentMetricMappingEntity extends BaseEntity { @ManyToOne @JoinColumn(name = "experiment_id") + @Cascade(CascadeType.ALL) ExperimentEntity experiment; @ManyToOne @JoinColumn(name = "metric_id") + @Cascade(CascadeType.ALL) MetricEntity metric; @Enumerated(EnumType.STRING) diff --git a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java index 6fb2bf5..d035327 100644 --- a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java +++ b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java @@ -9,6 +9,8 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.FieldDefaults; import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.Cascade; +import org.hibernate.annotations.CascadeType; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; @@ -30,10 +32,12 @@ import java.util.List; public class ExperimentMetricResultEntity extends BaseEntity { @ManyToOne @JoinColumn(name = "experiment_id") + @Cascade(CascadeType.ALL) ExperimentEntity experiment; @ManyToOne @JoinColumn(name = "metric_id") + @Cascade(CascadeType.ALL) MetricEntity metric; @Type(type = "jsonb") diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentaudittrail/ExperimentAuditTrailQueryImpl.java b/litmus-db/src/main/java/com/navi/medici/query/experimentaudittrail/ExperimentAuditTrailQueryImpl.java new file mode 100644 index 0000000..ac36617 --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentaudittrail/ExperimentAuditTrailQueryImpl.java @@ -0,0 +1,18 @@ +package com.navi.medici.query.experimentaudittrail; + +import com.navi.medici.entity.ExperimentAuditTrailEntity; +import com.navi.medici.repository.ExperimentAuditTrailRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ExperimentAuditTrailQueryImpl implements IExperimentAuditTrailQuery { + + private final ExperimentAuditTrailRepository experimentAuditTrailRepository; + + @Override + public ExperimentAuditTrailEntity save(ExperimentAuditTrailEntity experimentAuditTrail) { + return experimentAuditTrailRepository.save(experimentAuditTrail); + } +} diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentaudittrail/IExperimentAuditTrailQuery.java b/litmus-db/src/main/java/com/navi/medici/query/experimentaudittrail/IExperimentAuditTrailQuery.java new file mode 100644 index 0000000..86afcce --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentaudittrail/IExperimentAuditTrailQuery.java @@ -0,0 +1,7 @@ +package com.navi.medici.query.experimentaudittrail; + +import com.navi.medici.entity.ExperimentAuditTrailEntity; + +public interface IExperimentAuditTrailQuery { + ExperimentAuditTrailEntity save(ExperimentAuditTrailEntity experimentAuditTrail); +} diff --git a/litmus-db/src/main/java/com/navi/medici/query/metric/IMetricQuery.java b/litmus-db/src/main/java/com/navi/medici/query/metric/IMetricQuery.java index 75d2a43..ea0d917 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/metric/IMetricQuery.java +++ b/litmus-db/src/main/java/com/navi/medici/query/metric/IMetricQuery.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; +import java.util.List; import java.util.Optional; public interface IMetricQuery { @@ -13,4 +14,6 @@ public interface IMetricQuery { Page findAll(Specification specification, Pageable pageable); MetricEntity save(MetricEntity metric); + + List getAllMetricNames(); } diff --git a/litmus-db/src/main/java/com/navi/medici/query/metric/MetricQueryImpl.java b/litmus-db/src/main/java/com/navi/medici/query/metric/MetricQueryImpl.java index 3c25ec8..e22f55f 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/metric/MetricQueryImpl.java +++ b/litmus-db/src/main/java/com/navi/medici/query/metric/MetricQueryImpl.java @@ -8,6 +8,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Component; +import java.util.List; import java.util.Optional; @Component @@ -29,4 +30,9 @@ public class MetricQueryImpl implements IMetricQuery { public MetricEntity save(MetricEntity metric) { return metricRepository.save(metric); } + + @Override + public List getAllMetricNames() { + return metricRepository.getAllMetricNames(); + } } diff --git a/litmus-db/src/main/java/com/navi/medici/query/segment/ISegmentQuery.java b/litmus-db/src/main/java/com/navi/medici/query/segment/ISegmentQuery.java index 34475c3..3dd1ae1 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/segment/ISegmentQuery.java +++ b/litmus-db/src/main/java/com/navi/medici/query/segment/ISegmentQuery.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; +import java.util.List; import java.util.Optional; public interface ISegmentQuery { @@ -15,4 +16,6 @@ public interface ISegmentQuery { Optional findBySegmentName(String segmentName); Page findAll(Specification specification, Pageable pageable); + + List getAllSegmentNames(); } diff --git a/litmus-db/src/main/java/com/navi/medici/query/segment/SegmentQueryImpl.java b/litmus-db/src/main/java/com/navi/medici/query/segment/SegmentQueryImpl.java index b7aec43..49ebbb9 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/segment/SegmentQueryImpl.java +++ b/litmus-db/src/main/java/com/navi/medici/query/segment/SegmentQueryImpl.java @@ -2,18 +2,19 @@ package com.navi.medici.query.segment; import com.navi.medici.entity.SegmentEntity; import com.navi.medici.repository.SegmentRepository; - -import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Optional; + @Component @RequiredArgsConstructor -public class SegmentQueryImpl implements ISegmentQuery{ +public class +SegmentQueryImpl implements ISegmentQuery { private final SegmentRepository segmentRepository; @Override @@ -32,7 +33,12 @@ public class SegmentQueryImpl implements ISegmentQuery{ } @Override - public Page findAll(Specification specification, Pageable pageable){ + public Page findAll(Specification specification, Pageable pageable) { return segmentRepository.findAll(specification, pageable); } + + @Override + public List getAllSegmentNames() { + return segmentRepository.getAllSegmentNames(); + } } diff --git a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentAuditTrailRepository.java b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentAuditTrailRepository.java new file mode 100644 index 0000000..39859bb --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentAuditTrailRepository.java @@ -0,0 +1,9 @@ +package com.navi.medici.repository; + +import com.navi.medici.entity.ExperimentAuditTrailEntity; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ExperimentAuditTrailRepository extends CrudRepository { +} diff --git a/litmus-db/src/main/java/com/navi/medici/repository/MetricRepository.java b/litmus-db/src/main/java/com/navi/medici/repository/MetricRepository.java index 1cd00ae..e7711a7 100644 --- a/litmus-db/src/main/java/com/navi/medici/repository/MetricRepository.java +++ b/litmus-db/src/main/java/com/navi/medici/repository/MetricRepository.java @@ -4,13 +4,19 @@ import com.navi.medici.entity.MetricEntity; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface MetricRepository extends CrudRepository { Optional findByMetricName(String name); + Page findAll(Specification specification, Pageable pageable); + + @Query(value = "select m.metric_name from metrics m", nativeQuery = true) + List getAllMetricNames(); } diff --git a/litmus-db/src/main/java/com/navi/medici/repository/SegmentRepository.java b/litmus-db/src/main/java/com/navi/medici/repository/SegmentRepository.java index aeab7e6..83b2049 100644 --- a/litmus-db/src/main/java/com/navi/medici/repository/SegmentRepository.java +++ b/litmus-db/src/main/java/com/navi/medici/repository/SegmentRepository.java @@ -1,16 +1,16 @@ package com.navi.medici.repository; import com.navi.medici.entity.SegmentEntity; - -import java.util.List; -import java.util.Optional; - import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface SegmentRepository extends CrudRepository { Optional findBySegmentId(String segmentId); @@ -18,4 +18,7 @@ public interface SegmentRepository extends CrudRepository { Optional findBySegmentName(String segmentName); Page findAll(Specification specification, Pageable pageable); + + @Query(value = "select s.name from segments s", nativeQuery = true) + List getAllSegmentNames(); } diff --git a/litmus-liquibase/src/main/resources/db/changelog/202303311332-create-experiment-audit-trail-table.sql b/litmus-liquibase/src/main/resources/db/changelog/202303311332-create-experiment-audit-trail-table.sql new file mode 100644 index 0000000..ade6f55 --- /dev/null +++ b/litmus-liquibase/src/main/resources/db/changelog/202303311332-create-experiment-audit-trail-table.sql @@ -0,0 +1,13 @@ +--liquibase formatted sql + +--changeset author:akshatsoni id:202303311332 +CREATE TABLE experiment_audit_trail +( + id SERIAL PRIMARY KEY, + experiment_id int references experiments (id), + log text, + created_by varchar(100), + created_at timestamp, + updated_at timestamp, + version int +) \ No newline at end of file diff --git a/litmus-model/pom.xml b/litmus-model/pom.xml index fba0713..8f143b1 100644 --- a/litmus-model/pom.xml +++ b/litmus-model/pom.xml @@ -65,6 +65,10 @@ org.springframework spring-web + + javax.persistence + javax.persistence-api + diff --git a/litmus-model/src/main/java/com/navi/medici/dto/ExperimentAuditTrailDTO.java b/litmus-model/src/main/java/com/navi/medici/dto/ExperimentAuditTrailDTO.java new file mode 100644 index 0000000..d927a50 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/dto/ExperimentAuditTrailDTO.java @@ -0,0 +1,21 @@ +package com.navi.medici.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ExperimentAuditTrailDTO { + String log; + String createdBy; + LocalDateTime createdAt; +} diff --git a/litmus-model/src/main/java/com/navi/medici/dto/ExperimentInfoDTO.java b/litmus-model/src/main/java/com/navi/medici/dto/ExperimentInfoDTO.java new file mode 100644 index 0000000..1ae5a23 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/dto/ExperimentInfoDTO.java @@ -0,0 +1,21 @@ +package com.navi.medici.dto; + +import com.navi.medici.enums.ExperimentStatus; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ExperimentInfoDTO { + ExperimentStatus experimentStatus; + long testUsers; + ExperimentMetadata experimentMetadata; + +} diff --git a/litmus-db/src/main/java/com/navi/medici/dto/ExperimentMetadata.java b/litmus-model/src/main/java/com/navi/medici/dto/ExperimentMetadata.java similarity index 100% rename from litmus-db/src/main/java/com/navi/medici/dto/ExperimentMetadata.java rename to litmus-model/src/main/java/com/navi/medici/dto/ExperimentMetadata.java diff --git a/litmus-db/src/main/java/com/navi/medici/dto/MetricResult.java b/litmus-model/src/main/java/com/navi/medici/dto/MetricResult.java similarity index 100% rename from litmus-db/src/main/java/com/navi/medici/dto/MetricResult.java rename to litmus-model/src/main/java/com/navi/medici/dto/MetricResult.java diff --git a/litmus-model/src/main/java/com/navi/medici/request/v1/AttachMetricToExperimentRequest.java b/litmus-model/src/main/java/com/navi/medici/request/v1/AttachMetricToExperimentRequest.java new file mode 100644 index 0000000..302fc59 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/request/v1/AttachMetricToExperimentRequest.java @@ -0,0 +1,19 @@ +package com.navi.medici.request.v1; + +import com.navi.medici.enums.ExperimentMetricType; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class AttachMetricToExperimentRequest { + String metricName; + ExperimentMetricType experimentMetricType; +} diff --git a/litmus-model/src/main/java/com/navi/medici/request/v1/SampleSizeRequest.java b/litmus-model/src/main/java/com/navi/medici/request/v1/SampleSizeRequest.java new file mode 100644 index 0000000..c557932 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/request/v1/SampleSizeRequest.java @@ -0,0 +1,16 @@ +package com.navi.medici.request.v1; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SampleSizeRequest { + double baselineConversion; + double minimumDetectableEffect; + double confidenceInterval; +} diff --git a/litmus-model/src/main/java/com/navi/medici/response/DashboardExperimentResponse.java b/litmus-model/src/main/java/com/navi/medici/response/DashboardExperimentResponse.java new file mode 100644 index 0000000..53bedb3 --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/response/DashboardExperimentResponse.java @@ -0,0 +1,30 @@ +package com.navi.medici.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.navi.medici.dto.ExperimentImpact; +import com.navi.medici.enums.ExperimentStatus; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.FieldDefaults; + +//TODO: make a generic experiment response +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DashboardExperimentResponse { + String experimentId; + String experimentName; + String createdBy; + String primaryMetric; + ExperimentImpact impact; + long testUsers; + ExperimentStatus experimentStatus; +} diff --git a/litmus-model/src/main/java/com/navi/medici/response/ExperimentResponse.java b/litmus-model/src/main/java/com/navi/medici/response/ExperimentResponse.java index 07af17e..651d57b 100644 --- a/litmus-model/src/main/java/com/navi/medici/response/ExperimentResponse.java +++ b/litmus-model/src/main/java/com/navi/medici/response/ExperimentResponse.java @@ -1,29 +1,36 @@ package com.navi.medici.response; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.navi.medici.dto.ExperimentImpact; -import com.navi.medici.enums.ExperimentStatus; +import com.navi.medici.dto.ExperimentInfoDTO; +import com.navi.medici.enums.ExperimentType; +import com.navi.medici.request.v1.AttachMetricToExperimentRequest; +import com.navi.medici.strategy.ActivationStrategy; +import com.navi.medici.variants.VariantDefinition; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; +import lombok.Data; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.experimental.FieldDefaults; +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder @AllArgsConstructor @NoArgsConstructor -@Builder -@Getter -@Setter -@JsonIgnoreProperties(ignoreUnknown = true) @FieldDefaults(level = AccessLevel.PRIVATE) public class ExperimentResponse { String experimentId; String experimentName; + String description; + List strategies; + List variants; + ExperimentType type; + ExperimentInfoDTO experimentInfo; + String vertical; + List metrics; + LocalDateTime createdAt; + LocalDateTime updatedAt; String createdBy; - String primaryMetric; - ExperimentImpact impact; - long testUsers; - ExperimentStatus experimentStatus; } diff --git a/litmus-util/pom.xml b/litmus-util/pom.xml index f4104c7..7bda28e 100644 --- a/litmus-util/pom.xml +++ b/litmus-util/pom.xml @@ -28,6 +28,12 @@ org.springframework.boot spring-boot-starter-aop + + org.apache.commons + commons-math3 + 3.6.1 + compile + diff --git a/litmus-util/src/main/java/com/navi/medici/exceptions/UnableToChangeExperimentStatusException.java b/litmus-util/src/main/java/com/navi/medici/exceptions/UnableToChangeExperimentStatusException.java new file mode 100644 index 0000000..f3c81b4 --- /dev/null +++ b/litmus-util/src/main/java/com/navi/medici/exceptions/UnableToChangeExperimentStatusException.java @@ -0,0 +1,11 @@ +package com.navi.medici.exceptions; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +public class UnableToChangeExperimentStatusException extends RuntimeException { + public UnableToChangeExperimentStatusException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/litmus-util/src/main/java/com/navi/medici/exceptions/VariantWeightSumNotHundredException.java b/litmus-util/src/main/java/com/navi/medici/exceptions/VariantWeightSumNotHundredException.java new file mode 100644 index 0000000..89bad2c --- /dev/null +++ b/litmus-util/src/main/java/com/navi/medici/exceptions/VariantWeightSumNotHundredException.java @@ -0,0 +1,10 @@ +package com.navi.medici.exceptions; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +public class VariantWeightSumNotHundredException extends RuntimeException { + public VariantWeightSumNotHundredException(String message) { + super(message); + } +} diff --git a/litmus-util/src/main/java/com/navi/medici/stats/SampleSizeCalculator.java b/litmus-util/src/main/java/com/navi/medici/stats/SampleSizeCalculator.java new file mode 100644 index 0000000..1ca4a03 --- /dev/null +++ b/litmus-util/src/main/java/com/navi/medici/stats/SampleSizeCalculator.java @@ -0,0 +1,21 @@ +package com.navi.medici.stats; + +import org.apache.commons.math3.distribution.NormalDistribution; + +public class SampleSizeCalculator { + + private static NormalDistribution normalDistribution; + + public static long sampleCalculator(double baselineConversion, double minimumDetectableEffect, double alpha, double power) { + normalDistribution = new NormalDistribution(0, 1); + double zPower = normalDistribution.inverseCumulativeProbability(power); + double zAlpha = normalDistribution.inverseCumulativeProbability(1.0 - alpha / 2); + double bcr = baselineConversion / 100; + double mde = minimumDetectableEffect / 100; + double dcr = bcr + mde; + double bcrConfidence = -zAlpha * Math.sqrt(2 * bcr * (1.0 - bcr)); + double dcrConfidence = -zPower * Math.sqrt((bcr * (1.0 - bcr)) + (dcr * (1.0 - dcr))); + long numberOfSamples = (long) Math.ceil(Math.pow(bcrConfidence + dcrConfidence, 2) / Math.pow(dcr - bcr, 2)); + return numberOfSamples; + } +}