diff --git a/litmus-core/pom.xml b/litmus-core/pom.xml
index af1aa91..de62724 100644
--- a/litmus-core/pom.xml
+++ b/litmus-core/pom.xml
@@ -40,6 +40,24 @@
2.0.8-RELEASE
+
+ com.navi.medici
+ litmus-util
+ 2.0.8-RELEASE
+
+
+
+ io.github.resilience4j
+ resilience4j-micrometer
+ 1.7.1
+
+
+
+ io.github.resilience4j
+ resilience4j-retry
+ 1.7.0
+
+
com.navi.medici
litmus-cache
@@ -63,6 +81,11 @@
4.0.1
+
+ org.springframework.kafka
+ spring-kafka
+
+
software.amazon.awssdk
s3
@@ -113,6 +136,22 @@
test
+
+ net.javacrumbs.shedlock
+ shedlock-spring
+ 2.2.0
+
+
+ net.javacrumbs.shedlock
+ shedlock-provider-jdbc-template
+ 2.1.0
+
+
+ com.h2database
+ h2
+ 1.4.200
+
+
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 bbc42ae..0794b51 100644
--- a/litmus-core/src/main/java/com/navi/medici/Constants.java
+++ b/litmus-core/src/main/java/com/navi/medici/Constants.java
@@ -3,6 +3,8 @@ package com.navi.medici;
public class Constants {
public static final String ALL = "ALL";
public static final String HEADER_EMAIL_ID = "X-Email-Id";
+ public static final String CONTROL = "CONTROL";
+ public static final String TREATMENT = "TREATMENT";
//Update Messages
public static final String EXPERIMENT_CREATED = "Experiment created";
@@ -13,4 +15,15 @@ public class Constants {
public static final String PAUSE_EXPERIMENT = "Experiment has been paused";
public static final String METRIC_ATTACHED = "Metric %s attached to experiment";
public static final String EXPERIMENT_RESTARTED = "Experiment Restarted";
+
+ //Necessary Query Variables
+ public static final String EXPERIMENT_NAME = "experimentName";
+ public static final String EXPERIMENT_CREATED_AT = "experimentCreatedAt";
+ public static final String EXPERIMENT_ID = "experimentId";
+ public static final String METRIC_ID = "metricId";
+ public static final String SCHEDULE_TIME = "scheduleTime";
+ public static final String START_TIME = "startTime";
+ public static final String END_TIME = "endTime";
+ public static final String DAY_INTERVAL_FOR_TOTAL_USERS = "dayIntervalForTotalUsers";
+
}
diff --git a/litmus-core/src/main/java/com/navi/medici/LitmusApp.java b/litmus-core/src/main/java/com/navi/medici/LitmusApp.java
index a87497e..0e07e3a 100644
--- a/litmus-core/src/main/java/com/navi/medici/LitmusApp.java
+++ b/litmus-core/src/main/java/com/navi/medici/LitmusApp.java
@@ -1,9 +1,13 @@
package com.navi.medici;
+import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
+@EnableScheduling
+@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class LitmusApp {
public static void main(String[] args) {
SpringApplication.run(LitmusApp.class, args);
diff --git a/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java b/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java
index ea8cdf4..3e42f0e 100644
--- a/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java
+++ b/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java
@@ -21,4 +21,52 @@ public class LitmusCoreConfig {
@Value("#{'${litmus.portal.allowed.methods}'.split(',')}")
String[] litmusPortalAllowedMethods;
+
+ @Value("${tesseract.register.job.max.retries}")
+ int tesseractRegisterJobMaxRetries;
+
+ @Value("${tesseract.host}")
+ String tesseractHost;
+
+ @Value("${tesseract.register.path}")
+ String tesseractRegisterPath;
+
+ @Value("${tesseract.team.name}")
+ String tesserractTeamName;
+
+ @Value("${tesseract.update.path}")
+ String tesseractUpdatePath;
+
+ @Value("${tesseract.status.path}")
+ String tesseractStatusPath;
+
+ @Value("${tesseract.stats.path}")
+ String tesseractStatsPath;
+
+ @Value("${kafka.experiment.metric.result.topic}")
+ private String experimentMetricResultTopic;
+
+ @Value("${tesseract.litmus.cluster.name}")
+ private String tesseractClusterName;
+
+ @Value("${population.graph.data.days.interval}")
+ private int populationGraphDataDaysInterval;
+
+ @Value("${litmus.core.base.url}")
+ private String litmusCoreBaseUrl;
+
+ @Value("${query.schedule.and.end.time.diff.hours}")
+ int queryScheduleAndEndTimeDiff;
+
+ @Value("${tesseract.interval.function.initial.interval}")
+ long intervalFunctionInitialInterval;
+
+ @Value("${tesseract.interval.function.max.interval}")
+ long intervalFunctionMaxInterval;
+
+ @Value("${tesseract.interval.function.multiplier}")
+ double intervalFunctionMultiplier;
+
+ @Value("${query.day.interval.for.total.users}")
+ int queryDayIntervalForTotalUsers;
}
diff --git a/litmus-core/src/main/java/com/navi/medici/config/RestClient.java b/litmus-core/src/main/java/com/navi/medici/config/RestClient.java
new file mode 100644
index 0000000..d646ff2
--- /dev/null
+++ b/litmus-core/src/main/java/com/navi/medici/config/RestClient.java
@@ -0,0 +1,14 @@
+package com.navi.medici.config;
+
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestClient {
+ @Bean
+ public RestTemplate restTemplate(RestTemplateBuilder builder) {
+ return builder.build();
+ }
+}
\ No newline at end of file
diff --git a/litmus-core/src/main/java/com/navi/medici/config/SchedulerConfig.java b/litmus-core/src/main/java/com/navi/medici/config/SchedulerConfig.java
new file mode 100644
index 0000000..68971d0
--- /dev/null
+++ b/litmus-core/src/main/java/com/navi/medici/config/SchedulerConfig.java
@@ -0,0 +1,21 @@
+package com.navi.medici.config;
+
+import net.javacrumbs.shedlock.core.LockProvider;
+import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import javax.sql.DataSource;
+
+@Configuration
+public class SchedulerConfig {
+ @Bean
+ public LockProvider lockProvider(DataSource dataSource) {
+ return new JdbcTemplateLockProvider(
+ JdbcTemplateLockProvider.Configuration.builder()
+ .withJdbcTemplate(new JdbcTemplate(dataSource))
+ .build()
+ );
+ }
+}
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 53df3ff..36aec45 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
@@ -2,6 +2,7 @@ package com.navi.medici.controller.v2;
import com.navi.medici.Constants;
import com.navi.medici.dto.ExperimentAuditTrailDTO;
+import com.navi.medici.dto.ExperimentDataDTO;
import com.navi.medici.enums.ExperimentStatus;
import com.navi.medici.request.v1.AttachMetricToExperimentRequest;
import com.navi.medici.request.v1.CreateExperimentRequest;
@@ -11,6 +12,8 @@ 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;
+import com.navi.medici.service.experimentmetricresult.ExperimentMetricResultService;
+import com.navi.medici.tesseract.response.TesseractIdStatusResponse;
import com.navi.medici.util.ControllerUtils;
import io.micrometer.core.annotation.Timed;
import lombok.RequiredArgsConstructor;
@@ -37,6 +40,7 @@ import java.util.List;
public class ExperimentControllerV2 {
private final ExperimentService experimentService;
+ private final ExperimentMetricResultService experimentMetricResultService;
@GetMapping
@Timed(value = "litmus.get.experiments", percentiles = {0.95, 0.99})
@@ -122,7 +126,22 @@ public class ExperimentControllerV2 {
}
@GetMapping("/audit-trail/{experimentId}")
+ @Timed(value = "litmus.get.experiments.audit.trail", percentiles = {0.95, 0.99})
public ResponseEntity> getExperimentAuditTrail(@PathVariable("experimentId") String experimentId) {
return ResponseEntity.ok(experimentService.getExperimentAuditTrail(experimentId));
}
+
+ @GetMapping("/data/{experimentId}")
+ @Timed(value = "litmus.get.experiments.stats.data", percentiles = {0.95, 0.99})
+ public ResponseEntity getExperimentStatsData(@PathVariable("experimentId") String experimentId) {
+ log.info("fetch request received for data of experimentId: {}", experimentId);
+ return ResponseEntity.ok(experimentService.getExperimentStatsData(experimentId));
+ }
+
+ @PostMapping("/tesseract-id-status")
+ @Timed(value = "tesseract.id.status.update", percentiles = {0.95, 0.99})
+ public void updateStatusOfTesseractId(TesseractIdStatusResponse response) {
+ log.info("updating status of tesseract_id: {} to {}", response.getTesseractId(), response.getStatus().toString());
+ experimentMetricResultService.updateStatusOfTesseractId(response);
+ }
}
diff --git a/litmus-core/src/main/java/com/navi/medici/handler/RetryableException.java b/litmus-core/src/main/java/com/navi/medici/handler/RetryableException.java
new file mode 100644
index 0000000..df7ec9c
--- /dev/null
+++ b/litmus-core/src/main/java/com/navi/medici/handler/RetryableException.java
@@ -0,0 +1,7 @@
+package com.navi.medici.handler;
+
+public class RetryableException extends RuntimeException {
+ public RetryableException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/litmus-core/src/main/java/com/navi/medici/listener/ExperimentMetricResultListener.java b/litmus-core/src/main/java/com/navi/medici/listener/ExperimentMetricResultListener.java
new file mode 100644
index 0000000..09d8213
--- /dev/null
+++ b/litmus-core/src/main/java/com/navi/medici/listener/ExperimentMetricResultListener.java
@@ -0,0 +1,38 @@
+package com.navi.medici.listener;
+
+import com.navi.medici.metrics.MetricsUtils;
+import com.navi.medici.service.experimentmetricresult.ExperimentMetricResultService;
+import io.micrometer.core.instrument.MeterRegistry;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.support.KafkaHeaders;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.stereotype.Component;
+
+@Component
+@Log4j2
+@RequiredArgsConstructor
+public class ExperimentMetricResultListener {
+
+ private final ExperimentMetricResultService experimentMetricResultService;
+ private final MeterRegistry meterRegistry;
+
+ @KafkaListener(topics = "${kafka.experiment.metric.result.topic}", groupId = "${kafka.litmus.consumer.group}")
+ public void listenMetricResultFromTesseract(@Header(value = KafkaHeaders.CORRELATION_ID, required = false) String correlationId,
+ @Header(value = "job_run_id", required = false) String jobRunId,
+ @Header(value = "tesseract_id", required = false) String tesseractId,
+ @Payload String payload
+ ) {
+ try {
+ log.info("Payload received from tesseract, metric result: {} for tesseract_id: {} , correlation_id: {}", payload, tesseractId, correlationId);
+ experimentMetricResultService.persistMetricResultForExperiment(payload, tesseractId);
+ } catch (Exception e) {
+ log.error("Error while processing metric result for tesseract_id: " + tesseractId, e);
+ MetricsUtils.tesseractMetrics("tesseract_metric_result_listener_failed")
+ .register(meterRegistry)
+ .increment();
+ }
+ }
+}
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 9513bfd..9b05ab3 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
@@ -7,7 +7,7 @@ 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.entity.MetricEntity;
import com.navi.medici.request.v1.AttachMetricToExperimentRequest;
import com.navi.medici.response.DashboardExperimentResponse;
import com.navi.medici.response.ExperimentResponse;
@@ -28,26 +28,29 @@ import java.util.Set;
public class ExperimentMapper {
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();
+ public DashboardExperimentResponse mapExperimentEntityToDashBoardExperimentResponse(ExperimentEntity experimentEntity, Optional primaryMetric, ExperimentImpact experimentImpact) {
+ long testUsers = Objects.nonNull(experimentEntity.getExperimentInfo())
+ ? experimentEntity.getExperimentInfo().getTestUsers()
+ : 0;
+ List variants = Objects.nonNull(experimentEntity.getVariants())
+ ? jacksonUtils.stringToListObject(experimentEntity.getVariants(), VariantDefinition.class)
+ : List.of();
return DashboardExperimentResponse.builder()
.experimentId(experimentEntity.getExperimentId())
.experimentName(experimentEntity.getName())
.experimentStatus(Objects.nonNull(experimentEntity.getExperimentInfo())
? experimentEntity.getExperimentInfo().getExperimentStatus()
: null)
- .impact(ExperimentImpact.builder().build())
+ .impact(experimentImpact)
.createdBy(experimentEntity.getCreatedBy())
- .testUsers(Objects.nonNull(experimentEntity.getExperimentInfo())
- ? experimentEntity.getExperimentInfo().getTestUsers()
- : 0)
- .primaryMetric(primaryMetric.isPresent()
- ? primaryMetric.get().getMetric().getMetricName()
- : null)
+ .testUsers(testUsers)
+ .progressPercent(
+ (Objects.nonNull(experimentEntity.getExperimentInfo())
+ && experimentEntity.getExperimentInfo().getExperimentMetadata().getSampleSizeRequired() > 0)
+ ? (double) testUsers / ((variants.size() > 0 ? variants.size() : 1) * experimentEntity.getExperimentInfo().getExperimentMetadata().getSampleSizeRequired())
+ : 0
+ )
+ .primaryMetric(primaryMetric.map(MetricEntity::getMetricName).orElse(null))
.build();
}
diff --git a/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentMetricResultScheduler.java b/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentMetricResultScheduler.java
new file mode 100644
index 0000000..05c290f
--- /dev/null
+++ b/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentMetricResultScheduler.java
@@ -0,0 +1,38 @@
+package com.navi.medici.scheduler;
+
+import com.navi.medici.entity.ExperimentMetricMappingEntity;
+import com.navi.medici.query.experimentmetricmapping.IExperimentMetricMappingQuery;
+import com.navi.medici.service.experimentmetricresult.ExperimentMetricResultService;
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.MeterRegistry;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import net.javacrumbs.shedlock.core.SchedulerLock;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class ExperimentMetricResultScheduler {
+
+ private final IExperimentMetricMappingQuery experimentMetricMappingQuery;
+ private final ExperimentMetricResultService experimentMetricResultService;
+ private final MeterRegistry meterRegistry;
+
+ @Value("${experiment.metric.fetch.limit.per.interval}")
+ private Integer experimentMetricFetchLimitPerInterval;
+
+ @Scheduled(cron = "${experiment.metric.fetch.cron}")
+ @SchedulerLock(name = "ExperimentMetricResultScheduler_fetchExperimentMetricResult", lockAtMostForString = "PT10S")
+ public void fetchExperimentMetricResult() {
+ log.info("Scheduling query registration");
+ List experimentMetricMappings = experimentMetricMappingQuery.findByIsJobRunLimitTo(false, experimentMetricFetchLimitPerInterval);
+ experimentMetricResultService.registerExperimentMetricQueries(experimentMetricMappings);
+ Gauge.builder("queries_scheduled_per_interval", experimentMetricMappings, List::size)
+ .register(meterRegistry);
+ }
+}
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 cf35d8d..e3d30c9 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
@@ -2,6 +2,7 @@ package com.navi.medici.service.experiment;
import com.navi.medici.dto.Dropdown;
import com.navi.medici.dto.ExperimentAuditTrailDTO;
+import com.navi.medici.dto.ExperimentDataDTO;
import com.navi.medici.request.v1.AttachMetricToExperimentRequest;
import com.navi.medici.request.v1.CreateExperimentRequest;
import com.navi.medici.request.v1.ExperimentSearchRequest;
@@ -56,4 +57,6 @@ public interface ExperimentService {
void restartExperiment(String experimentId, String emailId);
List getExperimentAuditTrail(String experimentId);
+
+ ExperimentDataDTO getExperimentStatsData(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 5954ee1..ba29f99 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
@@ -2,13 +2,18 @@ package com.navi.medici.service.experiment;
import com.navi.medici.Constants;
import com.navi.medici.config.LitmusCoreConfig;
+import com.navi.medici.dto.DoughnutSectionDTO;
import com.navi.medici.dto.Dropdown;
import com.navi.medici.dto.ExperimentAuditTrailDTO;
+import com.navi.medici.dto.ExperimentDataDTO;
+import com.navi.medici.dto.ExperimentImpact;
import com.navi.medici.dto.ExperimentMetadata;
+import com.navi.medici.dto.GraphDTO;
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.ExperimentMetricResultEntity;
import com.navi.medici.entity.MetricEntity;
import com.navi.medici.entity.TeamEntity;
import com.navi.medici.enums.ExperimentMetricType;
@@ -38,13 +43,16 @@ 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.ChiSquaredTest;
import com.navi.medici.stats.SampleSizeCalculator;
import com.navi.medici.strategy.ActivationStrategy;
+import com.navi.medici.util.DateTimeUtil;
import com.navi.medici.util.JacksonUtils;
import com.navi.medici.validator.ExperimentValidator;
import com.navi.medici.variants.VariantDefinition;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
+import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -52,6 +60,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
+import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -59,6 +68,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@@ -146,7 +156,7 @@ public class ExperimentServiceImpl implements ExperimentService {
}
ExperimentEntity savedExperiment = experimentQuery.save(experiment);
saveAuditTrail(experiment.getExperimentId(), Constants.EXPERIMENT_CREATED, emailId);
- return experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(savedExperiment);
+ return experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(savedExperiment, primaryMetric, ExperimentImpact.builder().build());
}
@Override
@@ -185,7 +195,15 @@ public class ExperimentServiceImpl implements ExperimentService {
Page experimentEntities = experimentQuery.findAll(ExperimentSpecification.getExperiments(request), paging);
return PaginatedSearchResponse.builder()
.data(experimentEntities.stream()
- .map(experimentMapper::mapExperimentEntityToDashBoardExperimentResponse)
+ .map(experimentEntity -> {
+ Optional primaryMetric = Objects.nonNull(experimentEntity.getExperimentMetricMappings())
+ ? Optional.ofNullable(experimentEntity.getExperimentMetricMappings().stream()
+ .filter(mapping -> ExperimentMetricType.PRIMARY.equals(mapping.getExperimentMetricType()))
+ .findFirst().get().getMetric())
+ : Optional.empty();
+ ExperimentImpact experimentImpact = getExperimentImpact(experimentEntity, primaryMetric);
+ return experimentMapper.mapExperimentEntityToDashBoardExperimentResponse(experimentEntity, primaryMetric, experimentImpact);
+ })
.collect(Collectors.toList()))
.page(experimentEntities.getNumber() + 1)
.size(experimentEntities.getSize())
@@ -193,6 +211,57 @@ public class ExperimentServiceImpl implements ExperimentService {
.build();
}
+ private ExperimentImpact getExperimentImpact(ExperimentEntity experimentEntity, Optional primaryMetric) {
+ List experimentMetricResults = new ArrayList<>();
+ if (primaryMetric.isPresent()) {
+ experimentMetricResults.addAll(experimentMetricResultQuery.findByExperimentAndMetricOrderByIdAsc(experimentEntity, primaryMetric.get()));
+ }
+ List variantNames = getVariantNamesForStats(experimentEntity);
+ Map converted = new HashMap<>();
+ Map notConverted = new HashMap<>();
+ variantNames.forEach(variant -> {
+ converted.put(variant, 0L);
+ notConverted.put(variant, 0L);
+ });
+ experimentMetricResults.forEach(experimentMetricResult -> {
+ String variantName = experimentMetricResult.getVariantName();
+ converted.put(variantName, converted.get(experimentMetricResult.getVariantName()) + experimentMetricResult.getConverted());
+ notConverted.put(variantName, notConverted.get(experimentMetricResult.getVariantName()) + experimentMetricResult.getNotConverted());
+ });
+ ExperimentImpact experimentImpact = ExperimentImpact.builder()
+ .control(converted.get(Constants.CONTROL) != 0
+ ? (double) (converted.get(Constants.CONTROL) * 100) / (converted.get(Constants.CONTROL) + notConverted.get(Constants.CONTROL))
+ : 0)
+ .treatment(0.0)
+ .variantName(Constants.TREATMENT)
+ .build();
+ if (Objects.nonNull(experimentEntity.getExperimentInfo())) {
+ variantNames.stream()
+ .filter(name -> !name.equals(Constants.CONTROL))
+ .forEach(variant -> {
+ boolean flag = ChiSquaredTest.chiSquaredTest(
+ List.of(converted.get(Constants.CONTROL), converted.get(variant)),
+ List.of(notConverted.get(Constants.CONTROL), notConverted.get(variant)),
+ (double) 1 - (double) (experimentEntity.getExperimentInfo().getExperimentMetadata().getConfidenceInterval() / 100)
+ );
+ double conversion = (converted.get(variant) == 0)
+ ? 0
+ : (double) converted.get(variant) * 100 / (converted.get(variant) + notConverted.get(variant));
+ if (conversion >= experimentImpact.getTreatment()) {
+ experimentImpact.setVariantName(variant);
+ experimentImpact.setTreatment(conversion);
+ if (flag) {
+ experimentImpact.setFlag(conversion > experimentImpact.getControl());
+ } else {
+ experimentImpact.setFlag(null);
+ }
+ }
+ }
+ );
+ }
+ return experimentImpact;
+ }
+
@Override
public LitmusExperimentCollection fetchAllExperiments() {
List experimentEntities = experimentQuery.findByEnabled(true);
@@ -388,9 +457,9 @@ public class ExperimentServiceImpl implements ExperimentService {
@Override
public void attachMetric(String experimentId, AttachMetricToExperimentRequest request, String emailId) {
- experimentValidator.validateAttachMetricRequest(request);
- Optional metric = metricQuery.findByMetricName(request.getMetricName());
ExperimentEntity experiment = getExperimentEntityFromId(experimentId);
+ experimentValidator.validateAttachMetricRequest(request, experiment);
+ Optional metric = metricQuery.findByMetricName(request.getMetricName());
ExperimentMetricMappingEntity experimentMetricMapping = ExperimentMetricMappingEntity.builder()
.experiment(experiment)
.metric(metric.get())
@@ -492,4 +561,153 @@ public class ExperimentServiceImpl implements ExperimentService {
experimentAuditTrail.sort(Comparator.comparing(ExperimentAuditTrailDTO::getCreatedAt));
return experimentAuditTrail;
}
+
+ @Override
+ public ExperimentDataDTO getExperimentStatsData(String experimentId) {
+
+ ExperimentEntity experiment = getExperimentEntityFromId(experimentId);
+ List variantNames = getVariantNamesForStats(experiment);
+ List metricMappings = experimentMetricMappingQuery.findByExperiment(experiment);
+ List