Skip to content

Commit

Permalink
fix(experiments): Few no-code web experiments fixes (#28722)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielbachhuber authored Feb 14, 2025
1 parent 58de002 commit 9da4130
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 93 deletions.
27 changes: 1 addition & 26 deletions frontend/src/scenes/experiments/experimentLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useMocks } from '~/mocks/jest'
import { initKeaTests } from '~/test/init'
import { Experiment } from '~/types'

import { experimentLogic, percentageDistribution } from './experimentLogic'
import { experimentLogic } from './experimentLogic'

const RUNNING_EXP_ID = 45
const RUNNING_FUNNEL_EXP_ID = 46
Expand Down Expand Up @@ -268,29 +268,4 @@ describe('experimentLogic', () => {
expect(logic.values.recommendedExposureForCountData(0)).toEqual(Infinity)
})
})

describe('percentageDistribution', () => {
it('given variant count, calculates correct rollout percentages', async () => {
expect(percentageDistribution(1)).toEqual([100])
expect(percentageDistribution(2)).toEqual([50, 50])
expect(percentageDistribution(3)).toEqual([34, 33, 33])
expect(percentageDistribution(4)).toEqual([25, 25, 25, 25])
expect(percentageDistribution(5)).toEqual([20, 20, 20, 20, 20])
expect(percentageDistribution(6)).toEqual([17, 17, 17, 17, 16, 16])
expect(percentageDistribution(7)).toEqual([15, 15, 14, 14, 14, 14, 14])
expect(percentageDistribution(8)).toEqual([13, 13, 13, 13, 12, 12, 12, 12])
expect(percentageDistribution(9)).toEqual([12, 11, 11, 11, 11, 11, 11, 11, 11])
expect(percentageDistribution(10)).toEqual([10, 10, 10, 10, 10, 10, 10, 10, 10, 10])
expect(percentageDistribution(11)).toEqual([10, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9])
expect(percentageDistribution(12)).toEqual([9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8])
expect(percentageDistribution(13)).toEqual([8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7])
expect(percentageDistribution(14)).toEqual([8, 8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7])
expect(percentageDistribution(15)).toEqual([7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6])
expect(percentageDistribution(16)).toEqual([7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6])
expect(percentageDistribution(17)).toEqual([6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5])
expect(percentageDistribution(18)).toEqual([6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5])
expect(percentageDistribution(19)).toEqual([6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5])
expect(percentageDistribution(20)).toEqual([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5])
})
})
})
12 changes: 1 addition & 11 deletions frontend/src/scenes/experiments/experimentLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { sharedMetricsLogic } from './SharedMetrics/sharedMetricsLogic'
import {
featureFlagEligibleForExperiment,
getMinimumDetectableEffect,
percentageDistribution,
transformFiltersForWinningVariant,
} from './utils'

Expand Down Expand Up @@ -2021,14 +2022,3 @@ export const experimentLogic = kea<experimentLogicType>([
},
})),
])

export function percentageDistribution(variantCount: number): number[] {
const basePercentage = Math.floor(100 / variantCount)
const percentages = new Array(variantCount).fill(basePercentage)
let remaining = 100 - basePercentage * variantCount
for (let i = 0; remaining > 0; i++, remaining--) {
// try to equally distribute `remaining` across variants
percentages[i] += 1
}
return percentages
}
26 changes: 26 additions & 0 deletions frontend/src/scenes/experiments/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,36 @@ import {
featureFlagEligibleForExperiment,
getMinimumDetectableEffect,
getViewRecordingFilters,
percentageDistribution,
transformFiltersForWinningVariant,
} from './utils'

describe('utils', () => {
describe('percentageDistribution', () => {
it('given variant count, calculates correct rollout percentages', async () => {
expect(percentageDistribution(1)).toEqual([100])
expect(percentageDistribution(2)).toEqual([50, 50])
expect(percentageDistribution(3)).toEqual([34, 33, 33])
expect(percentageDistribution(4)).toEqual([25, 25, 25, 25])
expect(percentageDistribution(5)).toEqual([20, 20, 20, 20, 20])
expect(percentageDistribution(6)).toEqual([17, 17, 17, 17, 16, 16])
expect(percentageDistribution(7)).toEqual([15, 15, 14, 14, 14, 14, 14])
expect(percentageDistribution(8)).toEqual([13, 13, 13, 13, 12, 12, 12, 12])
expect(percentageDistribution(9)).toEqual([12, 11, 11, 11, 11, 11, 11, 11, 11])
expect(percentageDistribution(10)).toEqual([10, 10, 10, 10, 10, 10, 10, 10, 10, 10])
expect(percentageDistribution(11)).toEqual([10, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9])
expect(percentageDistribution(12)).toEqual([9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8])
expect(percentageDistribution(13)).toEqual([8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7])
expect(percentageDistribution(14)).toEqual([8, 8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7])
expect(percentageDistribution(15)).toEqual([7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6])
expect(percentageDistribution(16)).toEqual([7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6])
expect(percentageDistribution(17)).toEqual([6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5])
expect(percentageDistribution(18)).toEqual([6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5])
expect(percentageDistribution(19)).toEqual([6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5])
expect(percentageDistribution(20)).toEqual([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5])
})
})

it('Funnel experiment returns correct MDE', async () => {
const metricType = InsightType.FUNNELS
const trendResults = [
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/scenes/experiments/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ export function formatUnitByQuantity(value: number, unit: string): string {
return value === 1 ? unit : unit + 's'
}

export function percentageDistribution(variantCount: number): number[] {
const basePercentage = Math.floor(100 / variantCount)
const percentages = new Array(variantCount).fill(basePercentage)
let remaining = 100 - basePercentage * variantCount
for (let i = 0; remaining > 0; i++, remaining--) {
// try to equally distribute `remaining` across variants
percentages[i] += 1
}
return percentages
}

export function getMinimumDetectableEffect(
metricType: InsightType,
conversionMetrics: FunnelTimeConversionMetrics,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export const ExperimentsEditingToolbarMenu = (): JSX.Element => {
>
<ToolbarMenu.Header className="border-b">
{selectedExperimentId === 'new' ? (
<div className="w-full p-4">
<div className="w-full px-2 pb-4 pt-2">
<LemonLabel>Experiment name</LemonLabel>
<LemonInput
className="w-2/3"
className="w-2/3 mt-1"
placeholder="Example: Pricing page conversion"
onChange={(newName: string) => {
setExperimentFormValue('name', newName)
Expand Down
78 changes: 38 additions & 40 deletions frontend/src/toolbar/experiments/WebExperimentVariant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,51 +45,49 @@ export function WebExperimentVariant({ variant }: WebExperimentVariantProps): JS
/>
</div>
)}
<div className="flex items-center justify-between mb-2">
<LemonLabel>Transformations</LemonLabel>
<LemonButton
type="secondary"
size="xsmall"
icon={<IconPlus />}
onClick={(e) => {
e.stopPropagation()
addNewTransformation(variant)
}}
>
Add transformation
</LemonButton>
</div>
{experimentForm?.variants &&
experimentForm?.variants[variant] &&
experimentForm?.variants[variant].transforms &&
experimentForm?.variants[variant].transforms?.length > 0 ? (
<div>
<div className="flex items-center justify-between mb-2">
<LemonLabel>Transformations</LemonLabel>
<LemonButton
type="secondary"
size="xsmall"
icon={<IconPlus />}
onClick={(e) => {
e.stopPropagation()
addNewTransformation(variant)
}}
>
Add transformation
</LemonButton>
</div>
<LemonCollapse
size="small"
activeKey={experimentForm?.variants[variant].transforms.length === 1 ? 0 : undefined}
panels={experimentForm?.variants[variant].transforms.map((transform, transformIndex) => {
return {
key: transformIndex,
header: (
<WebExperimentTransformHeader
variant={variant}
transformIndex={transformIndex}
transform={transform}
/>
),
content: (
<WebExperimentTransformField
transformIndex={transformIndex}
variant={variant}
transform={transform}
/>
),
}
})}
/>
</div>
<LemonCollapse
size="small"
activeKey={experimentForm?.variants[variant].transforms.length === 1 ? 0 : undefined}
panels={experimentForm?.variants[variant].transforms.map((transform, transformIndex) => {
return {
key: transformIndex,
header: (
<WebExperimentTransformHeader
variant={variant}
transformIndex={transformIndex}
transform={transform}
/>
),
content: (
<WebExperimentTransformField
transformIndex={transformIndex}
variant={variant}
transform={transform}
/>
),
}
})}
/>
) : (
<span className="m-2"> This experiment variant doesn't modify any elements. </span>
<span className="my-2"> This experiment variant doesn't modify any elements. </span>
)}
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function WebExperimentVariantHeader({ variant }: WebExperimentVariantHead
} %`}
</span>
</LemonTag>
{removeVariantAvailable && (
{removeVariantAvailable && variant !== 'control' && (
<LemonButton
icon={<IconTrash />}
size="small"
Expand Down
27 changes: 14 additions & 13 deletions frontend/src/toolbar/experiments/experimentsTabLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import api, { ApiError } from 'lib/api'
import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast'
import { urls } from 'scenes/urls'

import { percentageDistribution } from '~/scenes/experiments/utils'
import { toolbarLogic } from '~/toolbar/bar/toolbarLogic'
import { experimentsLogic } from '~/toolbar/experiments/experimentsLogic'
import { toolbarConfigLogic } from '~/toolbar/toolbarConfigLogic'
Expand Down Expand Up @@ -232,27 +233,24 @@ export const experimentsTabLogic = kea<experimentsTabLogicType>([

selectors({
removeVariantAvailable: [
(s) => [s.experimentForm, s.selectedExperimentId],
(experimentForm: WebExperimentForm, selectedExperimentId: number | 'new' | null): boolean | undefined => {
(s) => [s.experimentForm],
(experimentForm: WebExperimentForm): boolean | undefined => {
/*Only show the remove button if all of these conditions are met:
1. Its a new Experiment
2. The experiment is still in draft form
3. there's more than one test variant, and the variant is not control*/
1. The experiment is still in draft form
2. there's more than one test variant, and the variant is not control*/
return (
selectedExperimentId === 'new' &&
experimentForm.start_date == null &&
experimentForm.variants &&
Object.keys(experimentForm.variants).length > 2
)
},
],
addVariantAvailable: [
(s) => [s.experimentForm, s.selectedExperimentId],
(experimentForm: WebExperimentForm, selectedExperimentId: number | 'new' | null): boolean | undefined => {
(s) => [s.experimentForm],
(experimentForm: WebExperimentForm): boolean | undefined => {
/*Only show the add button if all of these conditions are met:
1. Its a new Experiment
2. The experiment is still in draft form*/
return selectedExperimentId === 'new' || experimentForm.start_date == null
1. The experiment is still in draft form*/
return experimentForm.start_date == null
},
],
selectedExperiment: [
Expand Down Expand Up @@ -424,10 +422,13 @@ export const experimentsTabLogic = kea<experimentsTabLogicType>([
}
},
rebalanceRolloutPercentage: () => {
const perVariantRollout = Math.round(100 / Object.keys(values.experimentForm.variants || {}).length)
const perVariantRollout = percentageDistribution(Object.keys(values.experimentForm.variants || {}).length)

let i = 0
for (const existingVariant in values.experimentForm.variants) {
if (values.experimentForm.variants[existingVariant]) {
values.experimentForm.variants[existingVariant].rollout_percentage = Number(perVariantRollout)
values.experimentForm.variants[existingVariant].rollout_percentage = Number(perVariantRollout[i])
i++
}
}
actions.setExperimentFormValue('variants', values.experimentForm.variants)
Expand Down

0 comments on commit 9da4130

Please sign in to comment.