From 74a7b5b8314f290e6971fc71fc94686c14e04451 Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Tue, 26 Mar 2024 17:24:27 +0100 Subject: [PATCH 01/11] add table --- .../src/scenes/experiments/Experiment.scss | 12 + .../src/scenes/experiments/ExperimentNext.tsx | 12 +- .../ExperimentView/SecondaryMetricsTable.tsx | 328 +++++++++++++----- .../experiments/secondaryMetricsLogic.ts | 14 +- frontend/src/scenes/experiments/utils.ts | 18 + 5 files changed, 285 insertions(+), 99 deletions(-) diff --git a/frontend/src/scenes/experiments/Experiment.scss b/frontend/src/scenes/experiments/Experiment.scss index 8d0d2c667d705..8aa584d525f4d 100644 --- a/frontend/src/scenes/experiments/Experiment.scss +++ b/frontend/src/scenes/experiments/Experiment.scss @@ -187,4 +187,16 @@ .InsightViz .LemonTable__cell--sticky::before { background: var(--bg-table); } + + .secondary-metrics-table .LemonTable__content > table > { + thead > tr > th { + padding-top: 0; + padding-bottom: 0; + } + + tbody > tr > td { + padding-top: 0; + padding-bottom: 0; + } + } } diff --git a/frontend/src/scenes/experiments/ExperimentNext.tsx b/frontend/src/scenes/experiments/ExperimentNext.tsx index 01557833d7f80..e4f716d947190 100644 --- a/frontend/src/scenes/experiments/ExperimentNext.tsx +++ b/frontend/src/scenes/experiments/ExperimentNext.tsx @@ -39,12 +39,6 @@ export function ExperimentView(): JSX.Element { - updateExperimentSecondaryMetrics(metrics)} - initialMetrics={experiment.secondary_metrics} - defaultAggregationType={experiment.parameters?.aggregation_group_type_index} - /> @@ -55,6 +49,12 @@ export function ExperimentView(): JSX.Element { {experiment.start_date && } )} + updateExperimentSecondaryMetrics(metrics)} + initialMetrics={experiment.secondary_metrics} + defaultAggregationType={experiment.parameters?.aggregation_group_type_index} + /> diff --git a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx index ea9c7befcdd7f..56195a8b9c7b3 100644 --- a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx @@ -1,113 +1,67 @@ import '../Experiment.scss' -import { IconPlus } from '@posthog/icons' -import { LemonButton, LemonInput, LemonModal, LemonTable, LemonTableColumns } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' +import { IconPencil, IconPlus } from '@posthog/icons' +import { LemonBanner, LemonButton, LemonInput, LemonModal, LemonTable, LemonTableColumns } from '@posthog/lemon-ui' +import { BindLogic, useActions, useValues } from 'kea' import { Form } from 'kea-forms' +import { EXPERIMENT_DEFAULT_DURATION } from 'lib/constants' import { IconAreaChart } from 'lib/lemon-ui/icons' import { LemonField } from 'lib/lemon-ui/LemonField' -import { capitalizeFirstLetter, humanFriendlyNumber } from 'lib/utils' +import { capitalizeFirstLetter } from 'lib/utils' +import { insightDataLogic } from 'scenes/insights/insightDataLogic' +import { insightLogic } from 'scenes/insights/insightLogic' +import { Query } from '~/queries/Query/Query' import { InsightType } from '~/types' import { SECONDARY_METRIC_INSIGHT_ID } from '../constants' import { experimentLogic, TabularSecondaryMetricResults } from '../experimentLogic' -import { MetricSelector } from '../MetricSelector' +import { ExperimentInsightCreator, MetricSelector } from '../MetricSelector' import { secondaryMetricsLogic, SecondaryMetricsProps } from '../secondaryMetricsLogic' -import { getExperimentInsightColour } from '../utils' +import { findKeyWithHighestNumber, getExperimentInsightColour } from '../utils' -export function SecondaryMetricsTable({ +export function SecondaryMetricsModal({ onMetricsChange, initialMetrics, experimentId, defaultAggregationType, }: SecondaryMetricsProps): JSX.Element { const logic = secondaryMetricsLogic({ onMetricsChange, initialMetrics, experimentId, defaultAggregationType }) - const { metrics, isModalOpen, isSecondaryMetricModalSubmitting, existingModalSecondaryMetric, metricIdx } = - useValues(logic) - const { - deleteMetric, - openModalToCreateSecondaryMetric, - openModalToEditSecondaryMetric, - closeModal, - saveSecondaryMetric, - setPreviewInsight, - } = useActions(logic) + secondaryMetricModal, + isModalOpen, + isModalReadOnly, + isSecondaryMetricModalSubmitting, + existingModalSecondaryMetric, + metricIdx, + } = useValues(logic) - const { - secondaryMetricResultsLoading, - isExperimentRunning, - getIndexForVariant, - experiment, - experimentResults, - tabularSecondaryMetricResults, - } = useValues(experimentLogic({ experimentId })) + const { deleteMetric, closeModal, saveSecondaryMetric, setPreviewInsight } = useActions(logic) - const columns: LemonTableColumns = [ - { - key: 'variant', - title: 'Variant', - render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { - return ( -
-
- {capitalizeFirstLetter(item.variant)} -
- ) - }, - }, - ] + const { isExperimentRunning } = useValues(experimentLogic({ experimentId })) - experiment.secondary_metrics?.forEach((metric, idx) => { - columns.push({ - key: `results_${idx}`, - title: ( - - } - onClick={() => openModalToEditSecondaryMetric(metric, idx)} - > - {capitalizeFirstLetter(metric.name)} - - - ), - render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { - return ( -
- {item.results?.[idx].result ? ( - item.results[idx].insightType === InsightType.FUNNELS ? ( - <>{((item.results[idx].result as number) * 100).toFixed(1)}% - ) : ( - <>{humanFriendlyNumber(item.results[idx].result as number)} - ) - ) : ( - <>-- - )} -
- ) - }, - }) - }) + const insightLogicInstance = insightLogic({ dashboardItemId: SECONDARY_METRIC_INSIGHT_ID, syncWithUrl: false }) + const { insightProps } = useValues(insightLogicInstance) + const { query } = useValues(insightDataLogic(insightProps)) return ( - <> - + Close + + ) : ( <> {existingModalSecondaryMetric && (
- } - > + ) + } + > + {isModalReadOnly ? ( +
+ + {isExperimentRunning && ( + + Preview insights are generated based on {EXPERIMENT_DEFAULT_DURATION} days of data. This can + cause a mismatch between the preview and the actual results. + + )} + + + +
+ ) : (
- + )} + + ) +} + +export function SecondaryMetricsTable({ + onMetricsChange, + initialMetrics, + experimentId, + defaultAggregationType, +}: SecondaryMetricsProps): JSX.Element { + const logic = secondaryMetricsLogic({ onMetricsChange, initialMetrics, experimentId, defaultAggregationType }) + const { metrics } = useValues(logic) + + const { openModalToCreateSecondaryMetric, openModalToEditSecondaryMetric } = useActions(logic) + + const { + secondaryMetricResultsLoading, + isExperimentRunning, + getIndexForVariant, + experiment, + experimentResults, + secondaryMetricResults, + tabularSecondaryMetricResults, + countDataForVariant, + exposureCountDataForVariant, + conversionRateForVariant, + } = useValues(experimentLogic({ experimentId })) + + const columns: LemonTableColumns = [ + { + key: 'variant', + title: 'Variant', + render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { + return ( +
+
+ {capitalizeFirstLetter(item.variant)} +
+ ) + }, + }, + ] + + experiment.secondary_metrics?.forEach((metric, idx) => { + const Header = (): JSX.Element => ( +
+
+
{capitalizeFirstLetter(metric.name)}
+
+
+ } + onClick={() => openModalToEditSecondaryMetric(metric, idx, true)} + /> + } + onClick={() => openModalToEditSecondaryMetric(metric, idx, false)} + /> +
+
+
+
+ ) + + if (metric.filters.insight === InsightType.TRENDS) { + columns.push({ + key: `results_${idx}`, + title: ( +
+
+
+
Count
+
Exposure
+
+ Win probability +
+
+
+ ), + render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { + const targetResults = secondaryMetricResults?.[idx] + + const { variant } = item + const winningVariant = findKeyWithHighestNumber(targetResults?.probability || null) + + return ( +
+
+ {targetResults ? countDataForVariant(targetResults, variant) : '--'} +
+
+ {targetResults ? exposureCountDataForVariant(targetResults, variant) : '--'} +
+
+ + {targetResults?.probability?.[variant] != undefined + ? `${(targetResults.probability?.[variant] * 100).toFixed(1)}%` + : '--'} + +
+
+ ) + }, + }) + } else { + columns.push({ + key: `results_${idx}`, + title: ( +
+
+
+
+ Conversion rate +
+
+ Win probability +
+
+
+ ), + render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { + const targetResults = secondaryMetricResults?.[idx] + + const { variant } = item + const winningVariant = findKeyWithHighestNumber(targetResults?.probability || null) + + const conversionRate = conversionRateForVariant(targetResults || null, variant) + return ( +
+
+ {conversionRate === '--' ? conversionRate : `${conversionRate}%`} +
+
+ + {targetResults?.probability?.[variant] != undefined + ? `${(targetResults.probability?.[variant] * 100).toFixed(1)}%` + : '--'} + +
+
+ ) + }, + }) + } + }) + + return ( + <>

Secondary metrics

- {metrics.length > 0 && ( -
Click a metric name to compare variants on a graph.
- )} + {metrics.length > 0 &&
Monitor side effects of your experiment.
}
@@ -183,6 +320,7 @@ export function SecondaryMetricsTable({
{metrics && metrics.length > 0 ? ( )}
+ ) } diff --git a/frontend/src/scenes/experiments/secondaryMetricsLogic.ts b/frontend/src/scenes/experiments/secondaryMetricsLogic.ts index a12bc0f4a7547..9b7a652ad171c 100644 --- a/frontend/src/scenes/experiments/secondaryMetricsLogic.ts +++ b/frontend/src/scenes/experiments/secondaryMetricsLogic.ts @@ -65,9 +65,14 @@ export const secondaryMetricsLogic = kea([ actions({ // modal openModalToCreateSecondaryMetric: true, - openModalToEditSecondaryMetric: (metric: SecondaryExperimentMetric, metricIdx: number) => ({ + openModalToEditSecondaryMetric: ( + metric: SecondaryExperimentMetric, + metricIdx: number, + readOnly: boolean = false + ) => ({ metric, metricIdx, + readOnly, }), saveSecondaryMetric: true, closeModal: true, @@ -90,6 +95,13 @@ export const secondaryMetricsLogic = kea([ closeModal: () => false, }, ], + isModalReadOnly: [ + false, + { + openModalToEditSecondaryMetric: (_, { readOnly }) => readOnly, + closeModal: () => false, + }, + ], existingModalSecondaryMetric: [ null as SecondaryExperimentMetric | null, { diff --git a/frontend/src/scenes/experiments/utils.ts b/frontend/src/scenes/experiments/utils.ts index 90d7b2c64f44b..90233f10dbef1 100644 --- a/frontend/src/scenes/experiments/utils.ts +++ b/frontend/src/scenes/experiments/utils.ts @@ -17,3 +17,21 @@ export const transformResultFilters = (filters: Partial): Partial | null): string | null { + if (!obj) { + return null + } + + let highestValue = -Infinity + let keyWithHighestValue = null + + Object.keys(obj).forEach((key) => { + if (obj[key] > highestValue) { + highestValue = obj[key] + keyWithHighestValue = key + } + }) + + return keyWithHighestValue +} From a1b892e0ff95ec9da5c02a04cef2b28256d864ef Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Wed, 27 Mar 2024 08:39:40 +0100 Subject: [PATCH 02/11] tweak --- .../ExperimentView/SecondaryMetricsTable.tsx | 201 +++++++++--------- 1 file changed, 101 insertions(+), 100 deletions(-) diff --git a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx index 56195a8b9c7b3..432e40a5c9506 100644 --- a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx @@ -4,6 +4,7 @@ import { IconPencil, IconPlus } from '@posthog/icons' import { LemonBanner, LemonButton, LemonInput, LemonModal, LemonTable, LemonTableColumns } from '@posthog/lemon-ui' import { BindLogic, useActions, useValues } from 'kea' import { Form } from 'kea-forms' +import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' import { EXPERIMENT_DEFAULT_DURATION } from 'lib/constants' import { IconAreaChart } from 'lib/lemon-ui/icons' import { LemonField } from 'lib/lemon-ui/LemonField' @@ -151,34 +152,38 @@ export function SecondaryMetricsTable({ countDataForVariant, exposureCountDataForVariant, conversionRateForVariant, + experimentMathAggregationForTrends, } = useValues(experimentLogic({ experimentId })) - const columns: LemonTableColumns = [ + const columns: LemonTableColumns = [ { - key: 'variant', - title: 'Variant', - render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { - return ( -
-
- {capitalizeFirstLetter(item.variant)} -
- ) - }, + children: [ + { + title:
Variant
, + render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { + return ( +
+
+ {capitalizeFirstLetter(item.variant)} +
+ ) + }, + }, + ], }, ] experiment.secondary_metrics?.forEach((metric, idx) => { const Header = (): JSX.Element => ( -
+
{capitalizeFirstLetter(metric.name)}
@@ -203,92 +208,88 @@ export function SecondaryMetricsTable({
) + const targetResults = secondaryMetricResults?.[idx] + const targetResultFilters = targetResults?.filters + const winningVariant = findKeyWithHighestNumber(targetResults?.probability || null) + if (metric.filters.insight === InsightType.TRENDS) { columns.push({ - key: `results_${idx}`, - title: ( -
-
-
-
Count
-
Exposure
-
- Win probability + title:
, + children: [ + { + title: ( +
+ [ + {targetResults && + targetResults.insight?.[0] && + 'action' in targetResults.insight[0] && ( + + )} + ] + + {experimentMathAggregationForTrends(targetResultFilters) ? 'metric' : 'count'} +
-
-
- ), - render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { - const targetResults = secondaryMetricResults?.[idx] - - const { variant } = item - const winningVariant = findKeyWithHighestNumber(targetResults?.probability || null) - - return ( -
-
- {targetResults ? countDataForVariant(targetResults, variant) : '--'} -
-
- {targetResults ? exposureCountDataForVariant(targetResults, variant) : '--'} -
-
- - {targetResults?.probability?.[variant] != undefined - ? `${(targetResults.probability?.[variant] * 100).toFixed(1)}%` - : '--'} - -
-
- ) - }, + ), + render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { + const { variant } = item + return
{targetResults ? countDataForVariant(targetResults, variant) : '--'}
+ }, + }, + { + title: 'Exposure', + render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { + const { variant } = item + return ( +
{targetResults ? exposureCountDataForVariant(targetResults, variant) : '--'}
+ ) + }, + }, + { + title: 'Win probability', + render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { + const { variant } = item + return ( +
+ + {targetResults?.probability?.[variant] != undefined + ? `${(targetResults.probability?.[variant] * 100).toFixed(1)}%` + : '--'} + +
+ ) + }, + }, + ], }) } else { columns.push({ - key: `results_${idx}`, - title: ( -
-
-
-
- Conversion rate -
-
- Win probability -
-
-
- ), - render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { - const targetResults = secondaryMetricResults?.[idx] - - const { variant } = item - const winningVariant = findKeyWithHighestNumber(targetResults?.probability || null) - - const conversionRate = conversionRateForVariant(targetResults || null, variant) - return ( -
-
- {conversionRate === '--' ? conversionRate : `${conversionRate}%`} -
-
- - {targetResults?.probability?.[variant] != undefined - ? `${(targetResults.probability?.[variant] * 100).toFixed(1)}%` - : '--'} - -
-
- ) - }, + title:
, + children: [ + { + title: 'Conversion rate', + render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { + const { variant } = item + const conversionRate = conversionRateForVariant(targetResults || null, variant) + return
{conversionRate === '--' ? conversionRate : `${conversionRate}%`}
+ }, + }, + { + title: 'Win probability', + render: function Key(_, item: TabularSecondaryMetricResults): JSX.Element { + const { variant } = item + return ( +
+ + {targetResults?.probability?.[variant] != undefined + ? `${(targetResults.probability?.[variant] * 100).toFixed(1)}%` + : '--'} + +
+ ) + }, + }, + ], }) } }) From c2ee762d0b69f59303cb64d6ef7da02f915c17bc Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Wed, 27 Mar 2024 08:54:05 +0100 Subject: [PATCH 03/11] clean up --- frontend/src/scenes/experiments/Experiment.scss | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/frontend/src/scenes/experiments/Experiment.scss b/frontend/src/scenes/experiments/Experiment.scss index 8aa584d525f4d..8d0d2c667d705 100644 --- a/frontend/src/scenes/experiments/Experiment.scss +++ b/frontend/src/scenes/experiments/Experiment.scss @@ -187,16 +187,4 @@ .InsightViz .LemonTable__cell--sticky::before { background: var(--bg-table); } - - .secondary-metrics-table .LemonTable__content > table > { - thead > tr > th { - padding-top: 0; - padding-bottom: 0; - } - - tbody > tr > td { - padding-top: 0; - padding-bottom: 0; - } - } } From ca2879ff6250404bc3e39edeff31fe8132529bd0 Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Wed, 27 Mar 2024 09:03:55 +0100 Subject: [PATCH 04/11] rearrange --- frontend/src/scenes/experiments/ExperimentNext.tsx | 13 +++++++++++-- .../experiments/ExperimentView/ProgressBar.tsx | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/frontend/src/scenes/experiments/ExperimentNext.tsx b/frontend/src/scenes/experiments/ExperimentNext.tsx index e4f716d947190..896a3f31f6b20 100644 --- a/frontend/src/scenes/experiments/ExperimentNext.tsx +++ b/frontend/src/scenes/experiments/ExperimentNext.tsx @@ -1,5 +1,6 @@ import './Experiment.scss' +import { LemonDivider } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { ExperimentForm } from './ExperimentForm' @@ -36,8 +37,16 @@ export function ExperimentView(): JSX.Element { ) : experimentResults && experimentResults.insight ? ( <> - - + +
+
+ +
+ +
+ +
+
diff --git a/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx b/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx index 1cedbcf500d6c..a0fc6c3c24c52 100644 --- a/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx @@ -30,6 +30,7 @@ export function ProgressBar(): JSX.Element { return (
+

Data collection

{`${ experimentProgressPercent > 100 ? 100 : experimentProgressPercent.toFixed(2) }% complete`}
From 97340d9e15a10cd0b22ed3195263fe49947b8b10 Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Wed, 27 Mar 2024 09:27:42 +0100 Subject: [PATCH 05/11] ui polish --- frontend/src/scenes/experiments/ExperimentView/Goal.tsx | 4 ++-- .../src/scenes/experiments/ExperimentView/ProgressBar.tsx | 8 ++++++-- .../experiments/ExperimentView/SecondaryMetricsTable.tsx | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/src/scenes/experiments/ExperimentView/Goal.tsx b/frontend/src/scenes/experiments/ExperimentView/Goal.tsx index d1406633d4e86..608b0a584457f 100644 --- a/frontend/src/scenes/experiments/ExperimentView/Goal.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/Goal.tsx @@ -204,8 +204,8 @@ export function Goal(): JSX.Element { return (
-

Experiment goal

-
+

Experiment goal

+
This {experimentInsightType === InsightType.FUNNELS ? 'funnel' : 'trend'}{' '} {experimentInsightType === InsightType.FUNNELS ? 'experiment measures conversion through each step of the user journey.' diff --git a/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx b/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx index a0fc6c3c24c52..229003ead99ac 100644 --- a/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/ProgressBar.tsx @@ -30,8 +30,12 @@ export function ProgressBar(): JSX.Element { return (
-

Data collection

-
{`${ +

Data collection

+
+ Estimated target for the number of participants. Actual data may reveal significance earlier or later + than predicted. +
+
{`${ experimentProgressPercent > 100 ? 100 : experimentProgressPercent.toFixed(2) }% complete`}

Secondary metrics

- {metrics.length > 0 &&
Monitor side effects of your experiment.
} + {metrics.length > 0 && ( +
Monitor side effects of your experiment.
+ )}
From bb98f8a5e20d310a2ece054adc3c0b78b5effc2b Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Wed, 27 Mar 2024 09:53:45 +0100 Subject: [PATCH 06/11] clean up --- .../experiments/ExperimentView/Overview.tsx | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/frontend/src/scenes/experiments/ExperimentView/Overview.tsx b/frontend/src/scenes/experiments/ExperimentView/Overview.tsx index 76cc2136116d4..74d0ce0e7bf97 100644 --- a/frontend/src/scenes/experiments/ExperimentView/Overview.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/Overview.tsx @@ -1,6 +1,5 @@ import '../Experiment.scss' -import { LemonDivider } from '@posthog/lemon-ui' import { useValues } from 'kea' import { getSeriesColor } from 'lib/colors' import { capitalizeFirstLetter } from 'lib/utils' @@ -19,24 +18,14 @@ export function Overview(): JSX.Element { areResultsSignificant, } = useValues(experimentLogic) - function SignificanceText(): JSX.Element { - return ( - <> - Your results are  - {`${areResultsSignificant ? 'significant' : 'not significant'}`}. - - ) - } - - if (experimentInsightType === InsightType.FUNNELS) { - const winningVariant = sortedConversionRates[0] - const secondBestVariant = sortedConversionRates[1] - const difference = winningVariant.conversionRate - secondBestVariant.conversionRate + function WinningVariantText(): JSX.Element { + if (experimentInsightType === InsightType.FUNNELS) { + const winningVariant = sortedConversionRates[0] + const secondBestVariant = sortedConversionRates[1] + const difference = winningVariant.conversionRate - secondBestVariant.conversionRate - return ( -
-

Summary

-
+ return ( +
{capitalizeFirstLetter(secondBestVariant.key)} ).  -
-
- ) - } + ) + } - const index = getIndexForVariant(experimentResults, highestProbabilityVariant || '') - if (highestProbabilityVariant && index !== null && experimentResults) { - const { probability } = experimentResults + const index = getIndexForVariant(experimentResults, highestProbabilityVariant || '') + if (highestProbabilityVariant && index !== null && experimentResults) { + const { probability } = experimentResults - return ( -
-

Overview

- + return (
of being best.  -
-
+ ) + } + + return <> + } + + function SignificanceText(): JSX.Element { + return ( + <> + Your results are  + {`${areResultsSignificant ? 'significant' : 'not significant'}`}. + ) } - return <> + return ( +
+

Summary

+
+ + +
+
+ ) } From de88d8ee58d7e6eb2c1cc1ee2bf252fd2a329f45 Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Fri, 29 Mar 2024 09:14:15 +0000 Subject: [PATCH 07/11] fix sec metric result chart --- .../ExperimentView/SecondaryMetricsTable.tsx | 64 +++++++++++-------- .../experiments/secondaryMetricsLogic.ts | 8 +-- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx index b8144db6a14d6..d57d3718a5b29 100644 --- a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx @@ -1,25 +1,24 @@ import '../Experiment.scss' import { IconPencil, IconPlus } from '@posthog/icons' -import { LemonBanner, LemonButton, LemonInput, LemonModal, LemonTable, LemonTableColumns } from '@posthog/lemon-ui' -import { BindLogic, useActions, useValues } from 'kea' +import { LemonButton, LemonInput, LemonModal, LemonTable, LemonTableColumns } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' -import { EXPERIMENT_DEFAULT_DURATION } from 'lib/constants' import { IconAreaChart } from 'lib/lemon-ui/icons' import { LemonField } from 'lib/lemon-ui/LemonField' import { capitalizeFirstLetter } from 'lib/utils' -import { insightDataLogic } from 'scenes/insights/insightDataLogic' -import { insightLogic } from 'scenes/insights/insightLogic' +import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { Query } from '~/queries/Query/Query' -import { InsightType } from '~/types' +import { NodeKind } from '~/queries/schema' +import { InsightShortId, InsightType } from '~/types' import { SECONDARY_METRIC_INSIGHT_ID } from '../constants' import { experimentLogic, TabularSecondaryMetricResults } from '../experimentLogic' -import { ExperimentInsightCreator, MetricSelector } from '../MetricSelector' +import { MetricSelector } from '../MetricSelector' import { secondaryMetricsLogic, SecondaryMetricsProps } from '../secondaryMetricsLogic' -import { findKeyWithHighestNumber, getExperimentInsightColour } from '../utils' +import { findKeyWithHighestNumber, getExperimentInsightColour, transformResultFilters } from '../utils' export function SecondaryMetricsModal({ onMetricsChange, @@ -31,19 +30,15 @@ export function SecondaryMetricsModal({ const { secondaryMetricModal, isModalOpen, - isModalReadOnly, + showResults, isSecondaryMetricModalSubmitting, existingModalSecondaryMetric, metricIdx, } = useValues(logic) const { deleteMetric, closeModal, saveSecondaryMetric, setPreviewInsight } = useActions(logic) - - const { isExperimentRunning } = useValues(experimentLogic({ experimentId })) - - const insightLogicInstance = insightLogic({ dashboardItemId: SECONDARY_METRIC_INSIGHT_ID, syncWithUrl: false }) - const { insightProps } = useValues(insightLogicInstance) - const { query } = useValues(insightDataLogic(insightProps)) + const { secondaryMetricResults, isExperimentRunning } = useValues(experimentLogic({ experimentId })) + const targetResults = secondaryMetricResults && secondaryMetricResults[metricIdx] return ( Close @@ -93,18 +88,31 @@ export function SecondaryMetricsModal({ ) } > - {isModalReadOnly ? ( + {showResults && targetResults ? (
- - {isExperimentRunning && ( - - Preview insights are generated based on {EXPERIMENT_DEFAULT_DURATION} days of data. This can - cause a mismatch between the preview and the actual results. - - )} - - - +
) : (
([ openModalToEditSecondaryMetric: ( metric: SecondaryExperimentMetric, metricIdx: number, - readOnly: boolean = false + showResults: boolean = false ) => ({ metric, metricIdx, - readOnly, + showResults, }), saveSecondaryMetric: true, closeModal: true, @@ -95,10 +95,10 @@ export const secondaryMetricsLogic = kea([ closeModal: () => false, }, ], - isModalReadOnly: [ + showResults: [ false, { - openModalToEditSecondaryMetric: (_, { readOnly }) => readOnly, + openModalToEditSecondaryMetric: (_, { showResults }) => showResults, closeModal: () => false, }, ], From ef11a540b560813eba840756391fe8e1a7b1cbae Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Fri, 29 Mar 2024 09:32:32 +0000 Subject: [PATCH 08/11] refactor getHighestProbabilityVariant --- .../experiments/ExperimentView/Overview.tsx | 3 ++- .../ExperimentView/SecondaryMetricsTable.tsx | 7 ++++--- .../scenes/experiments/experimentLogic.tsx | 21 ++++++++++--------- frontend/src/scenes/experiments/utils.ts | 18 ---------------- 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/frontend/src/scenes/experiments/ExperimentView/Overview.tsx b/frontend/src/scenes/experiments/ExperimentView/Overview.tsx index 74d0ce0e7bf97..71784f4919bd8 100644 --- a/frontend/src/scenes/experiments/ExperimentView/Overview.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/Overview.tsx @@ -14,7 +14,7 @@ export function Overview(): JSX.Element { getIndexForVariant, experimentInsightType, sortedConversionRates, - highestProbabilityVariant, + getHighestProbabilityVariant, areResultsSignificant, } = useValues(experimentLogic) @@ -50,6 +50,7 @@ export function Overview(): JSX.Element { ) } + const highestProbabilityVariant = getHighestProbabilityVariant(experimentResults) const index = getIndexForVariant(experimentResults, highestProbabilityVariant || '') if (highestProbabilityVariant && index !== null && experimentResults) { const { probability } = experimentResults diff --git a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx index d57d3718a5b29..843b2f51bdfd4 100644 --- a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx @@ -12,13 +12,13 @@ import { capitalizeFirstLetter } from 'lib/utils' import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { Query } from '~/queries/Query/Query' import { NodeKind } from '~/queries/schema' -import { InsightShortId, InsightType } from '~/types' +import { ExperimentResults, InsightShortId, InsightType } from '~/types' import { SECONDARY_METRIC_INSIGHT_ID } from '../constants' import { experimentLogic, TabularSecondaryMetricResults } from '../experimentLogic' import { MetricSelector } from '../MetricSelector' import { secondaryMetricsLogic, SecondaryMetricsProps } from '../secondaryMetricsLogic' -import { findKeyWithHighestNumber, getExperimentInsightColour, transformResultFilters } from '../utils' +import { getExperimentInsightColour, transformResultFilters } from '../utils' export function SecondaryMetricsModal({ onMetricsChange, @@ -161,6 +161,7 @@ export function SecondaryMetricsTable({ exposureCountDataForVariant, conversionRateForVariant, experimentMathAggregationForTrends, + getHighestProbabilityVariant, } = useValues(experimentLogic({ experimentId })) const columns: LemonTableColumns = [ @@ -218,7 +219,7 @@ export function SecondaryMetricsTable({ const targetResults = secondaryMetricResults?.[idx] const targetResultFilters = targetResults?.filters - const winningVariant = findKeyWithHighestNumber(targetResults?.probability || null) + const winningVariant = getHighestProbabilityVariant(targetResults as ExperimentResults['result']) if (metric.filters.insight === InsightType.TRENDS) { columns.push({ diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index d2154c637a2a6..6296cf481f7e6 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -962,26 +962,27 @@ export const experimentLogic = kea([ } }, ], - highestProbabilityVariant: [ - (s) => [s.experimentResults], - (experimentResults: ExperimentResults['result']) => { - if (experimentResults) { - const maxValue = Math.max(...Object.values(experimentResults.probability)) - return Object.keys(experimentResults.probability).find( - (key) => Math.abs(experimentResults.probability[key] - maxValue) < Number.EPSILON + getHighestProbabilityVariant: [ + () => [], + () => (results: ExperimentResults['result'] | null) => { + if (results) { + const maxValue = Math.max(...Object.values(results.probability)) + return Object.keys(results.probability).find( + (key) => Math.abs(results.probability[key] - maxValue) < Number.EPSILON ) } }, ], areTrendResultsConfusing: [ - (s) => [s.experimentResults, s.highestProbabilityVariant], - (experimentResults, highestProbabilityVariant): boolean => { + (s) => [s.experimentResults, s.getHighestProbabilityVariant], + (experimentResults, getHighestProbabilityVariant): boolean => { // Results are confusing when the top variant has a lower // absolute count than other variants. This happens because // exposure is invisible to the user if (!experimentResults) { return false } + // find variant with highest count const variantResults: TrendResult = (experimentResults?.insight as TrendResult[]).reduce( (bestVariant, currentVariant) => @@ -992,7 +993,7 @@ export const experimentLogic = kea([ return false } - return variantResults.breakdown_value !== highestProbabilityVariant + return variantResults.breakdown_value !== getHighestProbabilityVariant(experimentResults) }, ], sortedExperimentResultVariants: [ diff --git a/frontend/src/scenes/experiments/utils.ts b/frontend/src/scenes/experiments/utils.ts index 90233f10dbef1..90d7b2c64f44b 100644 --- a/frontend/src/scenes/experiments/utils.ts +++ b/frontend/src/scenes/experiments/utils.ts @@ -17,21 +17,3 @@ export const transformResultFilters = (filters: Partial): Partial | null): string | null { - if (!obj) { - return null - } - - let highestValue = -Infinity - let keyWithHighestValue = null - - Object.keys(obj).forEach((key) => { - if (obj[key] > highestValue) { - highestValue = obj[key] - keyWithHighestValue = key - } - }) - - return keyWithHighestValue -} From a3acece74e557f6e40d69ae9b50fd38656248d29 Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Fri, 29 Mar 2024 12:35:38 +0000 Subject: [PATCH 09/11] fix types --- .../experiments/ExperimentView/SecondaryMetricsTable.tsx | 4 ++-- frontend/src/scenes/experiments/experimentLogic.tsx | 3 ++- frontend/src/types.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx index 843b2f51bdfd4..4b9b03483ac6f 100644 --- a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx @@ -12,7 +12,7 @@ import { capitalizeFirstLetter } from 'lib/utils' import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { Query } from '~/queries/Query/Query' import { NodeKind } from '~/queries/schema' -import { ExperimentResults, InsightShortId, InsightType } from '~/types' +import { BaseExperimentResults, InsightShortId, InsightType } from '~/types' import { SECONDARY_METRIC_INSIGHT_ID } from '../constants' import { experimentLogic, TabularSecondaryMetricResults } from '../experimentLogic' @@ -219,7 +219,7 @@ export function SecondaryMetricsTable({ const targetResults = secondaryMetricResults?.[idx] const targetResultFilters = targetResults?.filters - const winningVariant = getHighestProbabilityVariant(targetResults as ExperimentResults['result']) + const winningVariant = getHighestProbabilityVariant(targetResults as BaseExperimentResults) if (metric.filters.insight === InsightType.TRENDS) { columns.push({ diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index 6296cf481f7e6..705a9c935830e 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -26,6 +26,7 @@ import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeT import { FunnelsQuery, InsightVizNode, TrendsQuery } from '~/queries/schema' import { ActionFilter as ActionFilterType, + BaseExperimentResults, Breadcrumb, CountPerActorMathType, Experiment, @@ -964,7 +965,7 @@ export const experimentLogic = kea([ ], getHighestProbabilityVariant: [ () => [], - () => (results: ExperimentResults['result'] | null) => { + () => (results: BaseExperimentResults | null) => { if (results) { const maxValue = Math.max(...Object.values(results.probability)) return Object.keys(results.probability).find( diff --git a/frontend/src/types.ts b/frontend/src/types.ts index a583fe34c26d2..1c489f43f59f9 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -2884,7 +2884,7 @@ export interface TrendExperimentVariant { absolute_exposure: number } -interface BaseExperimentResults { +export interface BaseExperimentResults { probability: Record fakeInsightId: string significant: boolean From 2a53bf38d386e4855f90ba96d837e10d2a1938fc Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Fri, 29 Mar 2024 12:58:13 +0000 Subject: [PATCH 10/11] fix type --- .../experiments/ExperimentView/SecondaryMetricsTable.tsx | 4 ++-- frontend/src/scenes/experiments/experimentLogic.tsx | 3 +-- frontend/src/types.ts | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx index 4b9b03483ac6f..7cd1beacc577c 100644 --- a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx @@ -12,7 +12,7 @@ import { capitalizeFirstLetter } from 'lib/utils' import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { Query } from '~/queries/Query/Query' import { NodeKind } from '~/queries/schema' -import { BaseExperimentResults, InsightShortId, InsightType } from '~/types' +import { InsightShortId, InsightType } from '~/types' import { SECONDARY_METRIC_INSIGHT_ID } from '../constants' import { experimentLogic, TabularSecondaryMetricResults } from '../experimentLogic' @@ -219,7 +219,7 @@ export function SecondaryMetricsTable({ const targetResults = secondaryMetricResults?.[idx] const targetResultFilters = targetResults?.filters - const winningVariant = getHighestProbabilityVariant(targetResults as BaseExperimentResults) + const winningVariant = getHighestProbabilityVariant(targetResults || null) if (metric.filters.insight === InsightType.TRENDS) { columns.push({ diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index 705a9c935830e..6296cf481f7e6 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -26,7 +26,6 @@ import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeT import { FunnelsQuery, InsightVizNode, TrendsQuery } from '~/queries/schema' import { ActionFilter as ActionFilterType, - BaseExperimentResults, Breadcrumb, CountPerActorMathType, Experiment, @@ -965,7 +964,7 @@ export const experimentLogic = kea([ ], getHighestProbabilityVariant: [ () => [], - () => (results: BaseExperimentResults | null) => { + () => (results: ExperimentResults['result'] | null) => { if (results) { const maxValue = Math.max(...Object.values(results.probability)) return Object.keys(results.probability).find( diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 1c489f43f59f9..d920caef65f9a 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -2884,7 +2884,7 @@ export interface TrendExperimentVariant { absolute_exposure: number } -export interface BaseExperimentResults { +interface BaseExperimentResults { probability: Record fakeInsightId: string significant: boolean @@ -2921,7 +2921,7 @@ export interface FunnelExperimentResults { export type ExperimentResults = TrendsExperimentResults | FunnelExperimentResults -export type SecondaryMetricResults = Partial & { +export type SecondaryMetricResults = ExperimentResults['result'] & { result?: Record } export interface SecondaryExperimentMetric { From 560a92c5a222be796cfeefe306365d028ddb502e Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Mon, 1 Apr 2024 10:31:10 +0200 Subject: [PATCH 11/11] follow up fixes & clean up --- .../src/scenes/experiments/ExperimentNext.tsx | 20 +++-- .../ExperimentView/DistributionTable.tsx | 16 +--- .../ExperimentView/NoResultsEmptyState.tsx | 33 ------- .../experiments/ExperimentView/Overview.tsx | 63 ++++++-------- .../experiments/ExperimentView/Results.tsx | 32 +------ .../ExperimentView/SecondaryMetricsTable.tsx | 44 +--------- .../ExperimentView/SummaryTable.tsx | 32 +++---- .../experiments/ExperimentView/components.tsx | 86 +++++++++++++++++++ .../scenes/experiments/experimentLogic.tsx | 2 +- 9 files changed, 149 insertions(+), 179 deletions(-) delete mode 100644 frontend/src/scenes/experiments/ExperimentView/NoResultsEmptyState.tsx diff --git a/frontend/src/scenes/experiments/ExperimentNext.tsx b/frontend/src/scenes/experiments/ExperimentNext.tsx index 896a3f31f6b20..78abe98ed39ac 100644 --- a/frontend/src/scenes/experiments/ExperimentNext.tsx +++ b/frontend/src/scenes/experiments/ExperimentNext.tsx @@ -6,11 +6,15 @@ import { useActions, useValues } from 'kea' import { ExperimentForm } from './ExperimentForm' import { ExperimentImplementationDetails } from './ExperimentImplementationDetails' import { experimentLogic } from './experimentLogic' -import { ExperimentLoader, ExperimentLoadingAnimation, PageHeaderCustom } from './ExperimentView/components' +import { + ExperimentLoader, + ExperimentLoadingAnimation, + NoResultsEmptyState, + PageHeaderCustom, +} from './ExperimentView/components' import { DistributionTable } from './ExperimentView/DistributionTable' import { ExperimentExposureModal, ExperimentGoalModal, Goal } from './ExperimentView/Goal' import { Info } from './ExperimentView/Info' -import { NoResultsEmptyState } from './ExperimentView/NoResultsEmptyState' import { Overview } from './ExperimentView/Overview' import { ProgressBar } from './ExperimentView/ProgressBar' import { ReleaseConditionsTable } from './ExperimentView/ReleaseConditionsTable' @@ -36,14 +40,16 @@ export function ExperimentView(): JSX.Element { ) : experimentResults && experimentResults.insight ? ( <> - - -
-
+
+ + +
+
+
-
+
diff --git a/frontend/src/scenes/experiments/ExperimentView/DistributionTable.tsx b/frontend/src/scenes/experiments/ExperimentView/DistributionTable.tsx index 43b3c50ed614b..6dbaf4a331814 100644 --- a/frontend/src/scenes/experiments/ExperimentView/DistributionTable.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/DistributionTable.tsx @@ -2,13 +2,12 @@ import '../Experiment.scss' import { LemonTable, LemonTableColumns, Link } from '@posthog/lemon-ui' import { useValues } from 'kea' -import { getSeriesColor } from 'lib/colors' -import { capitalizeFirstLetter } from 'lib/utils' import { urls } from 'scenes/urls' import { MultivariateFlagVariant } from '~/types' import { experimentLogic } from '../experimentLogic' +import { VariantTag } from './components' export function DistributionTable(): JSX.Element { const { experiment } = useValues(experimentLogic) @@ -18,17 +17,8 @@ export function DistributionTable(): JSX.Element { className: 'w-1/3', key: 'key', title: 'Variant', - render: function Key(_, item, index): JSX.Element { - return ( -
-
- {capitalizeFirstLetter(item.key)} -
- ) + render: function Key(_, item): JSX.Element { + return }, }, { diff --git a/frontend/src/scenes/experiments/ExperimentView/NoResultsEmptyState.tsx b/frontend/src/scenes/experiments/ExperimentView/NoResultsEmptyState.tsx deleted file mode 100644 index c4c021a3c382e..0000000000000 --- a/frontend/src/scenes/experiments/ExperimentView/NoResultsEmptyState.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import '../Experiment.scss' - -import { Empty } from 'antd' -import { useValues } from 'kea' - -import { experimentLogic } from '../experimentLogic' - -export function NoResultsEmptyState(): JSX.Element { - const { experimentResultsLoading, experimentResultCalculationError } = useValues(experimentLogic) - - if (experimentResultsLoading) { - return <> - } - - return ( -
-

Results

-
-
- -

There are no experiment results yet

- {!!experimentResultCalculationError && ( -
{experimentResultCalculationError}
- )} -
- Wait a bit longer for your users to be exposed to the experiment. Double check your feature flag - implementation if you're still not seeing results. -
-
-
-
- ) -} diff --git a/frontend/src/scenes/experiments/ExperimentView/Overview.tsx b/frontend/src/scenes/experiments/ExperimentView/Overview.tsx index 71784f4919bd8..1b6bef968971a 100644 --- a/frontend/src/scenes/experiments/ExperimentView/Overview.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/Overview.tsx @@ -1,73 +1,62 @@ import '../Experiment.scss' import { useValues } from 'kea' -import { getSeriesColor } from 'lib/colors' -import { capitalizeFirstLetter } from 'lib/utils' import { InsightType } from '~/types' import { experimentLogic } from '../experimentLogic' +import { VariantTag } from './components' export function Overview(): JSX.Element { const { experimentResults, getIndexForVariant, experimentInsightType, - sortedConversionRates, getHighestProbabilityVariant, areResultsSignificant, + conversionRateForVariant, } = useValues(experimentLogic) function WinningVariantText(): JSX.Element { + const winningVariant = getHighestProbabilityVariant(experimentResults) + if (experimentInsightType === InsightType.FUNNELS) { - const winningVariant = sortedConversionRates[0] - const secondBestVariant = sortedConversionRates[1] - const difference = winningVariant.conversionRate - secondBestVariant.conversionRate + const winningConversionRate = conversionRateForVariant(experimentResults, winningVariant || '') + const controlConversionRate = conversionRateForVariant(experimentResults, 'control') + const difference = parseFloat(winningConversionRate) - parseFloat(controlConversionRate) + + if (difference === 0) { + return ( + + No variant is winning at this moment.  + + ) + } return ( -
-
- {capitalizeFirstLetter(winningVariant.key)} +
+  is winning with a conversion rate  increase of {`${difference.toFixed(2)}%`}  percentage points (vs  -
- {capitalizeFirstLetter(secondBestVariant.key)} + ). 
) } - const highestProbabilityVariant = getHighestProbabilityVariant(experimentResults) - const index = getIndexForVariant(experimentResults, highestProbabilityVariant || '') - if (highestProbabilityVariant && index !== null && experimentResults) { + const index = getIndexForVariant(experimentResults, winningVariant || '') + if (winningVariant && index !== null && experimentResults) { const { probability } = experimentResults return ( -
-
- {capitalizeFirstLetter(highestProbabilityVariant)} +
+  is winning with a  - {`${(probability[highestProbabilityVariant] * 100).toFixed(2)}% probability`}  + {`${(probability[winningVariant] * 100).toFixed(2)}% probability`}  of being best. 
@@ -79,17 +68,17 @@ export function Overview(): JSX.Element { function SignificanceText(): JSX.Element { return ( - <> +
Your results are  {`${areResultsSignificant ? 'significant' : 'not significant'}`}. - +
) } return (

Summary

-
+
diff --git a/frontend/src/scenes/experiments/ExperimentView/Results.tsx b/frontend/src/scenes/experiments/ExperimentView/Results.tsx index bd0662dfea042..f330c19868096 100644 --- a/frontend/src/scenes/experiments/ExperimentView/Results.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/Results.tsx @@ -2,14 +2,8 @@ import '../Experiment.scss' import { useValues } from 'kea' -import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' -import { Query } from '~/queries/Query/Query' -import { NodeKind } from '~/queries/schema' -import { InsightShortId } from '~/types' - import { experimentLogic } from '../experimentLogic' -import { transformResultFilters } from '../utils' -import { ResultsTag } from './components' +import { ResultsQuery, ResultsTag } from './components' import { SummaryTable } from './SummaryTable' export function Results(): JSX.Element { @@ -22,29 +16,7 @@ export function Results(): JSX.Element {
- +
) } diff --git a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx index 7cd1beacc577c..069531e45ff96 100644 --- a/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/SecondaryMetricsTable.tsx @@ -9,16 +9,13 @@ import { IconAreaChart } from 'lib/lemon-ui/icons' import { LemonField } from 'lib/lemon-ui/LemonField' import { capitalizeFirstLetter } from 'lib/utils' -import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' -import { Query } from '~/queries/Query/Query' -import { NodeKind } from '~/queries/schema' -import { InsightShortId, InsightType } from '~/types' +import { InsightType } from '~/types' import { SECONDARY_METRIC_INSIGHT_ID } from '../constants' import { experimentLogic, TabularSecondaryMetricResults } from '../experimentLogic' import { MetricSelector } from '../MetricSelector' import { secondaryMetricsLogic, SecondaryMetricsProps } from '../secondaryMetricsLogic' -import { getExperimentInsightColour, transformResultFilters } from '../utils' +import { ResultsQuery, VariantTag } from './components' export function SecondaryMetricsModal({ onMetricsChange, @@ -90,29 +87,7 @@ export function SecondaryMetricsModal({ > {showResults && targetResults ? (
- +
) : ( -
- {capitalizeFirstLetter(item.variant)} +
) }, diff --git a/frontend/src/scenes/experiments/ExperimentView/SummaryTable.tsx b/frontend/src/scenes/experiments/ExperimentView/SummaryTable.tsx index b6d4b95674c2c..010ddf19db616 100644 --- a/frontend/src/scenes/experiments/ExperimentView/SummaryTable.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/SummaryTable.tsx @@ -3,14 +3,13 @@ import '../Experiment.scss' import { IconInfo } from '@posthog/icons' import { LemonTable, LemonTableColumns, Tooltip } from '@posthog/lemon-ui' import { useValues } from 'kea' -import { getSeriesColor } from 'lib/colors' import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' import { LemonProgress } from 'lib/lemon-ui/LemonProgress' -import { capitalizeFirstLetter } from 'lib/utils' import { FunnelExperimentVariant, InsightType, TrendExperimentVariant } from '~/types' import { experimentLogic } from '../experimentLogic' +import { VariantTag } from './components' export function SummaryTable(): JSX.Element { const { @@ -18,29 +17,26 @@ export function SummaryTable(): JSX.Element { experimentInsightType, exposureCountDataForVariant, conversionRateForVariant, - sortedConversionRates, experimentMathAggregationForTrends, countDataForVariant, areTrendResultsConfusing, + getHighestProbabilityVariant, } = useValues(experimentLogic) if (!experimentResults) { return <> } + const winningVariant = getHighestProbabilityVariant(experimentResults) + const columns: LemonTableColumns = [ { key: 'variants', title: 'Variant', - render: function Key(_, item, index): JSX.Element { + render: function Key(_, item): JSX.Element { return (
-
- {capitalizeFirstLetter(item.key)} +
) }, @@ -90,12 +86,8 @@ export function SummaryTable(): JSX.Element { key: 'conversionRate', title: 'Conversion rate', render: function Key(_, item): JSX.Element { - const isWinning = item.key === sortedConversionRates[0].key return ( -
{`${conversionRateForVariant( - experimentResults, - item.key - )}%`}
+
{`${conversionRateForVariant(experimentResults, item.key)}%`}
) }, }) @@ -105,16 +97,20 @@ export function SummaryTable(): JSX.Element { key: 'winProbability', title: 'Win probability', render: function Key(_, item): JSX.Element { + const variantKey = item.key const percentage = - experimentResults?.probability?.[item.key] != undefined && - experimentResults.probability?.[item.key] * 100 + experimentResults?.probability?.[variantKey] != undefined && + experimentResults.probability?.[variantKey] * 100 + const isWinning = variantKey === winningVariant return ( <> {percentage ? ( - {percentage.toFixed(2)}% + + {percentage.toFixed(2)}% + ) : ( '--' diff --git a/frontend/src/scenes/experiments/ExperimentView/components.tsx b/frontend/src/scenes/experiments/ExperimentView/components.tsx index 1a22957925e68..97378f917c1bd 100644 --- a/frontend/src/scenes/experiments/ExperimentView/components.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/components.tsx @@ -1,16 +1,41 @@ import '../Experiment.scss' import { LemonButton, LemonDivider, LemonTable, LemonTag, LemonTagType } from '@posthog/lemon-ui' +import { Empty } from 'antd' import { useActions, useValues } from 'kea' import { AnimationType } from 'lib/animations/animations' import { Animation } from 'lib/components/Animation/Animation' import { PageHeader } from 'lib/components/PageHeader' import { dayjs } from 'lib/dayjs' import { More } from 'lib/lemon-ui/LemonButton/More' +import { capitalizeFirstLetter } from 'lib/utils' import { useEffect, useState } from 'react' +import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' +import { Query } from '~/queries/Query/Query' +import { NodeKind } from '~/queries/schema' +import { ExperimentResults, InsightShortId } from '~/types' + import { ResetButton } from '../Experiment' import { experimentLogic } from '../experimentLogic' +import { getExperimentInsightColour, transformResultFilters } from '../utils' + +export function VariantTag({ variantKey }: { variantKey: string }): JSX.Element { + const { experimentResults, getIndexForVariant } = useValues(experimentLogic) + + return ( + +
+ {capitalizeFirstLetter(variantKey)} + + ) +} export function ResultsTag(): JSX.Element { const { areResultsSignificant } = useValues(experimentLogic) @@ -41,6 +66,67 @@ export function ExperimentLoader(): JSX.Element { ) } +export function ResultsQuery({ + targetResults, + showTable, +}: { + targetResults: ExperimentResults['result'] | null + showTable: boolean +}): JSX.Element { + return ( + + ) +} + +export function NoResultsEmptyState(): JSX.Element { + const { experimentResultsLoading, experimentResultCalculationError } = useValues(experimentLogic) + + if (experimentResultsLoading) { + return <> + } + + return ( +
+

Results

+
+
+ +

There are no experiment results yet

+ {!!experimentResultCalculationError && ( +
{experimentResultCalculationError}
+ )} +
+ Wait a bit longer for your users to be exposed to the experiment. Double check your feature flag + implementation if you're still not seeing results. +
+
+
+
+ ) +} + export function ExperimentLoadingAnimation(): JSX.Element { function EllipsisAnimation(): JSX.Element { const [ellipsis, setEllipsis] = useState('.') diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index 1b414bc6e05a4..7ac11fb55f043 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -973,7 +973,7 @@ export const experimentLogic = kea([ getHighestProbabilityVariant: [ () => [], () => (results: ExperimentResults['result'] | null) => { - if (results) { + if (results && results.probability) { const maxValue = Math.max(...Object.values(results.probability)) return Object.keys(results.probability).find( (key) => Math.abs(results.probability[key] - maxValue) < Number.EPSILON