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
This commit is contained in:
Akshat Soni
2023-04-09 17:08:46 +05:30
committed by GitHub Enterprise
parent 53bad05c79
commit c7b148d856
51 changed files with 1225 additions and 238 deletions

View File

@@ -1,134 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>litmus</artifactId>
<groupId>com.navi.medici</groupId>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>litmus</artifactId>
<groupId>com.navi.medici</groupId>
<version>2.0.8-RELEASE</version>
</parent>
<artifactId>litmus-core</artifactId>
<version>2.0.8-RELEASE</version>
</parent>
<packaging>jar</packaging>
<name>litmus-core</name>
<artifactId>litmus-core</artifactId>
<version>2.0.8-RELEASE</version>
<packaging>jar</packaging>
<name>litmus-core</name>
<properties>
<java.version>17</java.version>
<nexus.host>https://nexus.cmd.navi-tech.in</nexus.host>
</properties>
<properties>
<java.version>17</java.version>
<nexus.host>https://nexus.cmd.navi-tech.in</nexus.host>
</properties>
<repositories>
<repository>
<id>nexus</id>
<name>Snapshot</name>
<url>${nexus.host}/repository/maven-snapshots</url>
</repository>
</repositories>
<repositories>
<repository>
<id>nexus</id>
<name>Snapshot</name>
<url>${nexus.host}/repository/maven-snapshots</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.navi.medici</groupId>
<artifactId>litmus-model</artifactId>
<version>2.0.8-RELEASE</version>
</dependency>
<dependencies>
<dependency>
<groupId>com.navi.medici</groupId>
<artifactId>litmus-model</artifactId>
<version>2.0.8-RELEASE</version>
</dependency>
<dependency>
<groupId>com.navi.medici</groupId>
<artifactId>litmus-db</artifactId>
<version>2.0.8-RELEASE</version>
</dependency>
<dependency>
<groupId>com.navi.medici</groupId>
<artifactId>litmus-db</artifactId>
<version>2.0.8-RELEASE</version>
</dependency>
<dependency>
<groupId>com.navi.medici</groupId>
<artifactId>litmus-cache</artifactId>
<version>2.0.8-RELEASE</version>
</dependency>
<dependency>
<groupId>com.navi.medici</groupId>
<artifactId>litmus-cache</artifactId>
<version>2.0.8-RELEASE</version>
</dependency>
<dependency>
<groupId>com.navi.medici</groupId>
<artifactId>litmus-util</artifactId>
<version>2.0.8-RELEASE</version>
</dependency>
<dependency>
<groupId>com.navi.medici</groupId>
<artifactId>litmus-util</artifactId>
<version>2.0.8-RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.17.256</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.17.256</version>
</dependency>
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.5.2</version>
</dependency>
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.5.2</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.8</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.8</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.4.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.4.Final</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</build>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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";
}

View File

@@ -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<List<Dropdown>> getOwnersDropdown() {
@@ -29,4 +33,14 @@ public class DropdownController {
public ResponseEntity<List<Dropdown>> getTeamsDropdown() {
return ResponseEntity.ok(teamService.getTeamsDropdown());
}
@GetMapping("/metrics")
public ResponseEntity<List<Dropdown>> getMetricsDropdown() {
return ResponseEntity.ok(metricService.getMetricsDropdown());
}
@GetMapping("/segments")
public ResponseEntity<List<Dropdown>> getSegmentsDropdown() {
return ResponseEntity.ok(segmentService.getSegmentsDropdown());
}
}

View File

@@ -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<LitmusExperimentCollection> 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<VariantDefinition> variantDefinitions) {
experimentService.attachVariants(experimentId, variantDefinitions);
@RequestBody List<VariantDefinition> 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)

View File

@@ -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<PaginatedSearchResponse<ExperimentResponse>> 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<PaginatedSearchResponse<DashboardExperimentResponse>> 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<ExperimentResponse> response = experimentService.getExperiments(request);
PaginatedSearchResponse<DashboardExperimentResponse> 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<ExperimentResponse> 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<List<ExperimentAuditTrailDTO>> getExperimentAuditTrail(@PathVariable("experimentId") String experimentId) {
return ResponseEntity.ok(experimentService.getExperimentAuditTrail(experimentId));
}
}

View File

@@ -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();

View File

@@ -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<ExperimentMetricMappingEntity> primaryMetric = Objects.nonNull(experimentEntity.getExperimentMetricMappingEntities())
? experimentEntity.getExperimentMetricMappingEntities().stream()
private final JacksonUtils jacksonUtils;
public DashboardExperimentResponse mapExperimentEntityToDashBoardExperimentResponse(ExperimentEntity experimentEntity) {
Optional<ExperimentMetricMappingEntity> 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<AttachMetricToExperimentRequest> getAttachMetricToExperimentRequestsFromExperimentMappings(Set<ExperimentMetricMappingEntity> 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();
}
}

View File

@@ -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<VariantDefinition> variantDefinitions);
void attachVariants(String experimentId, List<VariantDefinition> 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<ExperimentResponse> getExperiments(ExperimentSearchRequest request);
PaginatedSearchResponse<DashboardExperimentResponse> getExperiments(ExperimentSearchRequest request);
ExperimentResponse createExperiment(CreateExperimentRequest request, String emailId);
DashboardExperimentResponse createExperiment(CreateExperimentRequest request, String emailId);
long getSampleSizeForExperiment(SampleSizeRequest request);
List<Dropdown> 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<ExperimentAuditTrailDTO> getExperimentAuditTrail(String experimentId);
}

View File

@@ -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<TeamEntity> team = teamQuery.findByTeamName(request.getVertical());
List<ActivationStrategy> strategies = request.getStrategies();
List<VariantDefinition> variants = request.getVariants();
strategies.forEach(strategy -> {
Map<String, String> 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<ExperimentResponse> getExperiments(ExperimentSearchRequest request) {
public PaginatedSearchResponse<DashboardExperimentResponse> getExperiments(ExperimentSearchRequest request) {
Pageable paging = PageRequest.of(request.getPage() - 1, request.getSize());
Page<ExperimentEntity> experimentEntities = experimentQuery.findAll(ExperimentSpecification.getExperiments(request), paging);
return PaginatedSearchResponse.<ExperimentResponse>builder()
return PaginatedSearchResponse.<DashboardExperimentResponse>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<VariantDefinition> variantDefinitions) {
public void attachVariants(String experimentId, List<VariantDefinition> 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<ExperimentEntity> 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> 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<MetricEntity> metric = metricQuery.findByMetricName(request.getMetricName());
ExperimentEntity experiment = getExperimentEntityFromId(experimentId);
ExperimentMetricMappingEntity experimentMetricMapping = ExperimentMetricMappingEntity.builder()
.experiment(experiment)
.metric(metric.get())
.experimentMetricType(request.getExperimentMetricType())
.build();
Set<ExperimentMetricMappingEntity> 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<ExperimentAuditTrailDTO> getExperimentAuditTrail(String experimentId) {
ExperimentEntity experiment = getExperimentEntityFromId(experimentId);
return experiment.getExperimentAuditTrails().stream()
.map(experimentMapper::mapExperimentAuditTrailEntityToExperimentAuditTrailDTO)
.collect(Collectors.toList());
}
}

View File

@@ -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<MetricResponse> getMetrics(MetricSearchRequest request);
List<Dropdown> getMetricsDropdown();
}

View File

@@ -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<Dropdown> getMetricsDropdown() {
List<String> metricNames = metricQuery.getAllMetricNames();
return metricNames.stream()
.map(metricName -> Dropdown.builder()
.label(metricName)
.value(metricName)
.build())
.collect(Collectors.toList());
}
}

View File

@@ -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<SegmentResponse> getSegments(SegmentSearchRequest request);
List<Dropdown> getSegmentsDropdown();
}

View File

@@ -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<Dropdown> getSegmentsDropdown() {
List<String> semgentNames = segmentQuery.getAllSegmentNames();
return semgentNames.stream()
.map(semgentName -> Dropdown.builder()
.label(semgentName)
.value(semgentName)
.build())
.collect(Collectors.toList());
}
}

View File

@@ -33,7 +33,7 @@ public class ExperimentSpecification {
});
Optional.ofNullable(status).ifPresent(value -> {
Join<ExperimentEntity, ExperimentInfoEntity> experimentInfo = root.join("experimentInfoEntity");
Join<ExperimentEntity, ExperimentInfoEntity> experimentInfo = root.join("experimentInfo");
predicateList.add(
cb.equal(
experimentInfo.get("experimentStatus"),

View File

@@ -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 '<vertical>-<service>-<name>-<env>'");
@@ -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<MetricEntity> metric = metricQuery.findByMetricName(request.getMetricName());
if (metric.isEmpty()) {
throw new BadRequestException("Metric With name: " + request.getMetricName() + " does not exist");
}
}
public void validateAttachVariantsRequest(List<VariantDefinition> 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");
}
}
}

View File

@@ -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<VariantStat> getVariantStats(int count) {
List<VariantStat> 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();
}
}

View File

@@ -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());
}
}

View File

@@ -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<String, String> 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());
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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<ExperimentEntity> 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<ExperimentResponse> response = experimentService.getExperiments(request);
Mockito.when(experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(any())).thenReturn(dashboardExperimentResponse);
PaginatedSearchResponse<DashboardExperimentResponse> response = experimentService.getExperiments(request);
assertEquals(1, response.getTotalSize());
assertEquals("test experiment", response.getData().get(0).getExperimentName());
}
@@ -156,7 +166,7 @@ class ExperimentServiceImplTest {
Optional<ExperimentEntity> experiment = Optional.ofNullable(TestUtils.getExperimentEntity());
List<VariantDefinition> 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<ExperimentEntity> experiment = Optional.ofNullable(TestUtils.getExperimentEntity());
List<VariantDefinition> 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<ExperimentEntity> 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<ExperimentAuditTrailDTO> experimentAuditTrailDTOs = experimentService.getExperimentAuditTrail(experiment.getExperimentId());
assertEquals("test log", experimentAuditTrailDTOs.get(0).getLog());
}
@Test
void shouldFetchExperiment() {
Optional<ExperimentEntity> 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));
}
}

View File

@@ -72,4 +72,11 @@ class MetricServiceImplTest {
PaginatedSearchResponse<MetricResponse> response = metricService.getMetrics(request);
assertEquals("test metric", response.getData().get(0).getMetricName());
}
@Test
void shouldGetMetricsDropdown() {
List<String> metricNames = List.of(TestUtils.getMetricEntity().getMetricName());
Mockito.when(metricQuery.getAllMetricNames()).thenReturn(metricNames);
assertEquals("test metric", metricService.getMetricsDropdown().get(0).getLabel());
}
}

View File

@@ -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<String> segmentNames = List.of(TestUtils.getSegmentEntity().getSegmentName());
Mockito.when(segmentQuery.getAllSegmentNames()).thenReturn(segmentNames);
List<Dropdown> dropdowns = segmentService.getSegmentsDropdown();
assertEquals("test segment", dropdowns.get(0).getLabel());
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<ExperimentMetricMappingEntity> experimentMetricMappingEntities;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "experiment")
@Cascade(CascadeType.ALL)
Set<ExperimentMetricMappingEntity> experimentMetricMappings;
@OneToOne(mappedBy = "experiment")
ExperimentInfoEntity experimentInfoEntity;
@Cascade(CascadeType.ALL)
ExperimentInfoEntity experimentInfo;
@OneToMany(mappedBy = "experiment")
Set<ExperimentMetricResultEntity> experimentMetricResultEntities;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "experiment")
@Cascade(CascadeType.ALL)
Set<ExperimentMetricResultEntity> experimentMetricResults;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "experiment")
@Cascade(CascadeType.ALL)
Set<ExperimentAuditTrailEntity> experimentAuditTrails;
}

View File

@@ -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)

View File

@@ -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")

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
package com.navi.medici.query.experimentaudittrail;
import com.navi.medici.entity.ExperimentAuditTrailEntity;
public interface IExperimentAuditTrailQuery {
ExperimentAuditTrailEntity save(ExperimentAuditTrailEntity experimentAuditTrail);
}

View File

@@ -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<MetricEntity> findAll(Specification<MetricEntity> specification, Pageable pageable);
MetricEntity save(MetricEntity metric);
List<String> getAllMetricNames();
}

View File

@@ -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<String> getAllMetricNames() {
return metricRepository.getAllMetricNames();
}
}

View File

@@ -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<SegmentEntity> findBySegmentName(String segmentName);
Page<SegmentEntity> findAll(Specification<SegmentEntity> specification, Pageable pageable);
List<String> getAllSegmentNames();
}

View File

@@ -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<SegmentEntity> findAll(Specification<SegmentEntity> specification, Pageable pageable){
public Page<SegmentEntity> findAll(Specification<SegmentEntity> specification, Pageable pageable) {
return segmentRepository.findAll(specification, pageable);
}
@Override
public List<String> getAllSegmentNames() {
return segmentRepository.getAllSegmentNames();
}
}

View File

@@ -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<ExperimentAuditTrailEntity, Long> {
}

View File

@@ -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<MetricEntity, Long> {
Optional<MetricEntity> findByMetricName(String name);
Page<MetricEntity> findAll(Specification<MetricEntity> specification, Pageable pageable);
@Query(value = "select m.metric_name from metrics m", nativeQuery = true)
List<String> getAllMetricNames();
}

View File

@@ -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<SegmentEntity, Long> {
Optional<SegmentEntity> findBySegmentId(String segmentId);
@@ -18,4 +18,7 @@ public interface SegmentRepository extends CrudRepository<SegmentEntity, Long> {
Optional<SegmentEntity> findBySegmentName(String segmentName);
Page<SegmentEntity> findAll(Specification<SegmentEntity> specification, Pageable pageable);
@Query(value = "select s.name from segments s", nativeQuery = true)
List<String> getAllSegmentNames();
}

View File

@@ -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
)

View File

@@ -65,6 +65,10 @@
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
</dependency>
</dependencies>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<ActivationStrategy> strategies;
List<VariantDefinition> variants;
ExperimentType type;
ExperimentInfoDTO experimentInfo;
String vertical;
List<AttachMetricToExperimentRequest> metrics;
LocalDateTime createdAt;
LocalDateTime updatedAt;
String createdBy;
String primaryMetric;
ExperimentImpact impact;
long testUsers;
ExperimentStatus experimentStatus;
}

View File

@@ -28,6 +28,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}