diff --git a/platform/experiment/src/com/intellij/platform/experiment/ab/impl/experiments.kt b/platform/experiment/src/com/intellij/platform/experiment/ab/impl/experiments.kt index 9dfafa65b6b2..37c55e906903 100644 --- a/platform/experiment/src/com/intellij/platform/experiment/ab/impl/experiments.kt +++ b/platform/experiment/src/com/intellij/platform/experiment/ab/impl/experiments.kt @@ -6,9 +6,10 @@ import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.diagnostic.debug import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.diagnostic.runAndLogException -import com.intellij.platform.experiment.ab.impl.ABExperimentOption.* +import com.intellij.platform.experiment.ab.impl.ABExperimentOption.UNASSIGNED import com.intellij.platform.experiment.ab.impl.statistic.ABExperimentCountCollector import org.jetbrains.annotations.VisibleForTesting +import java.util.* import kotlin.math.absoluteValue /** @@ -77,18 +78,22 @@ internal fun ABExperimentOption.reportableName(): String { private val thisUserDecision: ABExperimentDecision by lazy { val currentBucket = getUserBucketNumber() val currentVersion = ApplicationInfo.getInstance().fullVersion + val currentProduct = IntelliJPlatformProduct.get() val option = experimentsPartition.find { (it.majorVersion == null || currentVersion.startsWith(it.majorVersion)) && - (it.experimentBuckets.contains(currentBucket) || it.controlBuckets.contains(currentBucket)) + (it.experimentBuckets.contains(currentBucket) || it.controlBuckets.contains(currentBucket)) && + it.products.contains(currentProduct) } ?: return@lazy ABExperimentDecision(option = UNASSIGNED, isControlGroup = true, bucketNumber = currentBucket) ABExperimentDecision(option = option.experiment, isControlGroup = option.controlBuckets.contains(currentBucket), bucketNumber = currentBucket) } -data class ExperimentAssignment( +internal data class ExperimentAssignment( val experiment: ABExperimentOption, val experimentBuckets: Set, val controlBuckets: Set, - val majorVersion: String? = null) + val majorVersion: String? = null, + val products: Set = EnumSet.allOf(IntelliJPlatformProduct::class.java) +) internal fun getUserBucketNumber(): Int { val overridingBucket = Integer.getInteger("ide.ab.test.overriding.bucket") @@ -109,4 +114,17 @@ private fun getDeviceIdPurpose(): String { return "A/B Experiment" + ApplicationInfo.getInstance().shortVersion } -private val LOG = logger() \ No newline at end of file +private val LOG = logger() + +/** + * There is no need to list all available products. Add if a product-specific experiment is needed. + */ +internal enum class IntelliJPlatformProduct { + OTHER, + ; + + companion object { + fun get(): IntelliJPlatformProduct = + OTHER + } +} \ No newline at end of file diff --git a/platform/experiment/test/com/intellij/platform/experiment/ab/test.kt b/platform/experiment/test/com/intellij/platform/experiment/ab/test.kt index d26289b09de6..40997ed8f4bb 100644 --- a/platform/experiment/test/com/intellij/platform/experiment/ab/test.kt +++ b/platform/experiment/test/com/intellij/platform/experiment/ab/test.kt @@ -1,6 +1,7 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.experiment.ab +import com.intellij.platform.experiment.ab.impl.IntelliJPlatformProduct import com.intellij.platform.experiment.ab.impl.NUMBER_OF_BUCKETS import com.intellij.platform.experiment.ab.impl.experimentsPartition import org.junit.jupiter.api.Assertions @@ -12,14 +13,17 @@ class ABExperimentSanityTest { fun `no intersections in experiment partitions`() { for (experiment1 in experimentsPartition) { for (experiment2 in experimentsPartition) { - if (experiment1.experiment == experiment2.experiment) { - assertEmptyIntersection(experiment1.controlBuckets, experiment2.experimentBuckets) - } - else { - assertEmptyIntersection(experiment1.experimentBuckets, experiment2.controlBuckets) - assertEmptyIntersection(experiment1.controlBuckets, experiment2.controlBuckets) - assertEmptyIntersection(experiment1.experimentBuckets, experiment2.experimentBuckets) - assertEmptyIntersection(experiment1.controlBuckets, experiment2.experimentBuckets) + for (product in IntelliJPlatformProduct.entries) { + if (!experiment1.products.contains(product) || !experiment2.products.contains(product)) continue + if (experiment1.experiment == experiment2.experiment) { + assertEmptyIntersection(experiment1.controlBuckets, experiment2.experimentBuckets) + } + else { + assertEmptyIntersection(experiment1.experimentBuckets, experiment2.controlBuckets) + assertEmptyIntersection(experiment1.controlBuckets, experiment2.controlBuckets) + assertEmptyIntersection(experiment1.experimentBuckets, experiment2.experimentBuckets) + assertEmptyIntersection(experiment1.controlBuckets, experiment2.experimentBuckets) + } } } } @@ -33,6 +37,13 @@ class ABExperimentSanityTest { } } + @Test + fun `no experiment without associated products`() { + for (experiment in experimentsPartition) { + Assertions.assertTrue(experiment.products.isNotEmpty()) + } + } + fun assertEmptyIntersection(range1: Set<*>, range2: Set<*>) { Assertions.assertTrue(range1.intersect(range2).isEmpty()) }