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 +}