diff --git a/frontend/src/scenes/experiments/ExperimentView/Info.tsx b/frontend/src/scenes/experiments/ExperimentView/Info.tsx index dcd6234bcc497..fa0a2e5cf9503 100644 --- a/frontend/src/scenes/experiments/ExperimentView/Info.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/Info.tsx @@ -14,7 +14,7 @@ import { ProgressStatus } from '~/types' import { StatusTag } from '../Experiment' import { experimentLogic } from '../experimentLogic' import { getExperimentStatus } from '../experimentsLogic' -import { ResultsTag } from './components' +import { ActionBanner, ResultsTag } from './components' export function Info(): JSX.Element { const { experiment } = useValues(experimentLogic) @@ -102,6 +102,7 @@ export function Info(): JSX.Element { compactButtons /> + ) } diff --git a/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx b/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx index 229003ead99ac..568c37b254758 100644 --- a/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx @@ -1,32 +1,28 @@ import '../Experiment.scss' import { useValues } from 'kea' -import { dayjs } from 'lib/dayjs' import { LemonProgress } from 'lib/lemon-ui/LemonProgress' import { humanFriendlyNumber } from 'lib/utils' -import { FunnelStep, InsightType } from '~/types' +import { InsightType } from '~/types' import { experimentLogic } from '../experimentLogic' +import { formatUnitByQuantity } from '../utils' export function ProgressBar(): JSX.Element { - const { experiment, experimentResults, experimentInsightType } = useValues(experimentLogic) - - // Parameters for experiment results - // don't use creation variables in results - const funnelResultsPersonsTotal = - experimentInsightType === InsightType.FUNNELS && experimentResults?.insight - ? (experimentResults.insight as FunnelStep[][]).reduce( - (sum: number, variantResult: FunnelStep[]) => variantResult[0]?.count + sum, - 0 - ) - : 0 + const { + experiment, + experimentInsightType, + funnelResultsPersonsTotal, + recommendedSampleSize, + actualRunningTime, + recommendedRunningTime, + } = useValues(experimentLogic) const experimentProgressPercent = experimentInsightType === InsightType.FUNNELS - ? ((funnelResultsPersonsTotal || 0) / (experiment?.parameters?.recommended_sample_size || 1)) * 100 - : (dayjs().diff(experiment?.start_date, 'day') / (experiment?.parameters?.recommended_running_time || 1)) * - 100 + ? (funnelResultsPersonsTotal / recommendedSampleSize) * 100 + : (actualRunningTime / recommendedRunningTime) * 100 return (
@@ -48,15 +44,15 @@ export function ProgressBar(): JSX.Element {
{experiment.end_date ? (
- Ran for {dayjs(experiment.end_date).diff(experiment.start_date, 'day')} days + Ran for {actualRunningTime} {formatUnitByQuantity(actualRunningTime, 'day')}
) : (
- {dayjs().diff(experiment.start_date, 'day')} days running + {actualRunningTime} {formatUnitByQuantity(actualRunningTime, 'day')} running
)}
- Goal: {experiment?.parameters?.recommended_running_time ?? 'Unknown'} days + Goal: {recommendedRunningTime} {formatUnitByQuantity(recommendedRunningTime, 'day')}
)} @@ -64,16 +60,18 @@ export function ProgressBar(): JSX.Element {
{experiment.end_date ? (
- Saw {humanFriendlyNumber(funnelResultsPersonsTotal)} participants + Saw {humanFriendlyNumber(funnelResultsPersonsTotal)}{' '} + {formatUnitByQuantity(funnelResultsPersonsTotal, 'participant')}
) : (
- {humanFriendlyNumber(funnelResultsPersonsTotal)} participants seen + {humanFriendlyNumber(funnelResultsPersonsTotal)}{' '} + {formatUnitByQuantity(funnelResultsPersonsTotal, 'participant')} seen
)}
- Goal: {humanFriendlyNumber(experiment?.parameters?.recommended_sample_size || 0)}{' '} - participants + Goal: {humanFriendlyNumber(recommendedSampleSize)}{' '} + {formatUnitByQuantity(recommendedSampleSize, 'participant')}
)} diff --git a/frontend/src/scenes/experiments/ExperimentView/components.tsx b/frontend/src/scenes/experiments/ExperimentView/components.tsx index 6f2d5dcecb004..6818e1d516256 100644 --- a/frontend/src/scenes/experiments/ExperimentView/components.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/components.tsx @@ -1,7 +1,7 @@ import '../Experiment.scss' import { IconCheckbox } from '@posthog/icons' -import { LemonButton, LemonDivider, LemonTag, LemonTagType } from '@posthog/lemon-ui' +import { LemonBanner, LemonButton, LemonDivider, LemonTag, LemonTagType, Link } from '@posthog/lemon-ui' import { Empty } from 'antd' import { useActions, useValues } from 'kea' import { AnimationType } from 'lib/animations/animations' @@ -335,3 +335,159 @@ export function PageHeaderCustom(): JSX.Element { /> ) } + +export function ActionBanner(): JSX.Element { + const { + experiment, + experimentInsightType, + experimentResults, + experimentLoading, + experimentResultsLoading, + isExperimentRunning, + areResultsSignificant, + isExperimentStopped, + funnelResultsPersonsTotal, + recommendedSampleSize, + actualRunningTime, + recommendedRunningTime, + getHighestProbabilityVariant, + } = useValues(experimentLogic) + + const { archiveExperiment } = useActions(experimentLogic) + + if (!experiment || experimentLoading || experimentResultsLoading) { + return <> + } + + // Draft + if (!isExperimentRunning) { + return ( + + Your experiment is in draft mode. You can edit your variants, adjust release conditions, and{' '} + + test your feature flag + + . Once everything works as expected, you can launch your experiment. From that point, any new experiment + events will be counted towards the results. + + ) + } + + // Running, results present, not significant + if (isExperimentRunning && experimentResults && !isExperimentStopped && !areResultsSignificant) { + // Results insignificant, but a large enough sample/running time has been achieved + // Further collection unlikely to change the result -> recommmend cutting the losses + if ( + experimentInsightType === InsightType.FUNNELS && + funnelResultsPersonsTotal > Math.max(recommendedSampleSize, 500) && + dayjs().diff(experiment.start_date, 'day') > 2 // at least 2 days running + ) { + return ( + + You've reached a robust sample size for your experiment, but the results are still inconclusive. + Continuing the experiment is unlikely to yield significant findings. It may be time to stop this + experiment. + + ) + } + if (experimentInsightType === InsightType.TRENDS && actualRunningTime > Math.max(recommendedRunningTime, 7)) { + return ( + + Your experiment has been running long enough, but the results are still inconclusive. Continuing the + experiment is unlikely to yield significant findings. It may be time to stop this experiment. + + ) + } + + return ( + + Your experiment is live and is collecting data, but hasn't yet reached the statistical significance + needed to make reliable decisions. It's important to wait for more data to avoid premature conclusions. + + ) + } + + // Running, results significant + if (isExperimentRunning && !isExperimentStopped && areResultsSignificant && experimentResults) { + const { probability } = experimentResults + const winningVariant = getHighestProbabilityVariant(experimentResults) + if (!winningVariant) { + return <> + } + + const winProbability = probability[winningVariant] + + // Win probability only slightly over 0.9 and the recommended sample/time just met -> proceed with caution + if ( + experimentInsightType === InsightType.FUNNELS && + funnelResultsPersonsTotal > recommendedSampleSize + 50 && + winProbability < 0.93 + ) { + return ( + + You've achieved significant results, however, the sample size just meets the minimum requirements, + and the win probability is only marginally above 90%. To ensure more reliable outcomes, consider + running the experiment a bit longer. + + ) + } + + if ( + experimentInsightType === InsightType.TRENDS && + actualRunningTime > recommendedRunningTime + 2 && + winProbability < 0.93 + ) { + return ( + + You've achieved significant results, however, the running time just meets the minimum requirements, + and the win probability is only marginally above 90%. To ensure more reliable outcomes, consider + running the experiment a bit longer. + + ) + } + + return ( + + Good news! Your experiment has gathered enough data to reach statistical significance, providing + reliable results for decision making. Before taking any action, review relevant secondary metrics for + any unintended side effects. Once you're done, you can stop the experiment. + + ) + } + + // Stopped, results significant + if (isExperimentStopped && areResultsSignificant) { + return ( + + You have stopped this experiment, and it is no longer collecting data. With significant results in hand, + you can now roll out the winning variant to all your users by adjusting the{' '} + + {experiment.feature_flag?.key} + {' '} + feature flag. + + ) + } + + // Stopped, results not significant + if (isExperimentStopped && experimentResults && !areResultsSignificant) { + return ( + + You have stopped this experiment, and it is no longer collecting data. Because your results are not + significant, we don't recommend drawing any conclusions from them. You can reset the experiment + (deleting the data collected so far) and restart the experiment at any point again. If this experiment + is no longer relevant, you can{' '} + archiveExperiment()}> + archive it + + . + + ) + } + + return <> +} diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index 7b2b10ed36540..31de23b9d696f 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -1107,6 +1107,44 @@ export const experimentLogic = kea([ }) }, ], + recommendedSampleSize: [ + (s) => [s.experiment], + (experiment: Experiment): number => experiment?.parameters?.recommended_sample_size || 100, + ], + funnelResultsPersonsTotal: [ + (s) => [s.experimentResults, s.experimentInsightType], + (experimentResults: ExperimentResults['result'], experimentInsightType: InsightType): number => { + if (experimentInsightType !== InsightType.FUNNELS || !experimentResults?.insight) { + return 0 + } + + let sum = 0 + experimentResults.insight.forEach((variantResult) => { + if (variantResult[0]?.count) { + sum += variantResult[0].count + } + }) + return sum + }, + ], + actualRunningTime: [ + (s) => [s.experiment], + (experiment: Experiment): number => { + if (!experiment.start_date) { + return 0 + } + + if (experiment.end_date) { + return dayjs(experiment.end_date).diff(experiment.start_date, 'day') + } + + return dayjs().diff(experiment.start_date, 'day') + }, + ], + recommendedRunningTime: [ + (s) => [s.experiment], + (experiment: Experiment): number => experiment?.parameters?.recommended_running_time || 1, + ], }), forms(({ actions, values }) => ({ experiment: { diff --git a/frontend/src/scenes/experiments/utils.ts b/frontend/src/scenes/experiments/utils.ts index 90d7b2c64f44b..6f71d6c1829b2 100644 --- a/frontend/src/scenes/experiments/utils.ts +++ b/frontend/src/scenes/experiments/utils.ts @@ -17,3 +17,7 @@ export const transformResultFilters = (filters: Partial): Partial