[ab] IJPL-186222 support product-specific experiments

GitOrigin-RevId: 3482ab936f56ad856f91924754125fa221c90154
This commit is contained in:
Konstantin Ulitin
2025-10-03 15:40:09 +00:00
committed by intellij-monorepo-bot
parent 15ca1be9f9
commit d6e4c47453
2 changed files with 42 additions and 13 deletions
@@ -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<Int>,
val controlBuckets: Set<Int>,
val majorVersion: String? = null)
val majorVersion: String? = null,
val products: Set<IntelliJPlatformProduct> = 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<ABExperimentOption>()
private val LOG = logger<ABExperimentOption>()
/**
* 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
}
}
@@ -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())
}