diff --git a/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx b/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx index 1d66bee399d04..b8e2ce7a0eab8 100644 --- a/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx @@ -6,8 +6,7 @@ import { WebExperimentImplementationDetails } from 'scenes/experiments/WebExperi import { ExperimentImplementationDetails } from '../ExperimentImplementationDetails' import { experimentLogic } from '../experimentLogic' -import { PrimaryMetricModal } from '../Metrics/PrimaryMetricModal' -import { SecondaryMetricModal } from '../Metrics/SecondaryMetricModal' +import { MetricModal } from '../Metrics/MetricModal' import { MetricsView } from '../MetricsView/MetricsView' import { ExperimentLoadingAnimation, @@ -142,8 +141,9 @@ export function ExperimentView(): JSX.Element { /> )} - - + + + diff --git a/frontend/src/scenes/experiments/Metrics/SecondaryGoalFunnels.tsx b/frontend/src/scenes/experiments/Metrics/FunnelsMetricForm.tsx similarity index 83% rename from frontend/src/scenes/experiments/Metrics/SecondaryGoalFunnels.tsx rename to frontend/src/scenes/experiments/Metrics/FunnelsMetricForm.tsx index fe704d9f7a953..46d6cccce4c5e 100644 --- a/frontend/src/scenes/experiments/Metrics/SecondaryGoalFunnels.tsx +++ b/frontend/src/scenes/experiments/Metrics/FunnelsMetricForm.tsx @@ -1,6 +1,7 @@ import { LemonLabel } from '@posthog/lemon-ui' import { LemonInput } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { TestAccountFilterSwitch } from 'lib/components/TestAccountFiltersSwitch' import { EXPERIMENT_DEFAULT_DURATION } from 'lib/constants' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' @@ -22,13 +23,26 @@ import { FunnelAttributionSelect, FunnelConversionWindowFilter, } from './Selectors' - -export function SecondaryGoalFunnels({ metricIdx }: { metricIdx: number }): JSX.Element { +export function FunnelsMetricForm({ isSecondary = false }: { isSecondary?: boolean }): JSX.Element { const { currentTeam } = useValues(teamLogic) - const { experiment, isExperimentRunning } = useValues(experimentLogic) + const { experiment, isExperimentRunning, editingPrimaryMetricIndex, editingSecondaryMetricIndex } = + useValues(experimentLogic) const { setFunnelsMetric } = useActions(experimentLogic) const hasFilters = (currentTeam?.test_account_filters || []).length > 0 - const currentMetric = experiment.metrics_secondary[metricIdx] as ExperimentFunnelsQuery + + const metrics = isSecondary ? experiment.metrics_secondary : experiment.metrics + const metricIdx = isSecondary ? editingSecondaryMetricIndex : editingPrimaryMetricIndex + + if (!metricIdx && metricIdx !== 0) { + return <> + } + + const currentMetric = metrics[metricIdx] as ExperimentFunnelsQuery + + const actionFilterProps = { + ...commonActionFilterProps, + actionsTaxonomicGroupTypes: [TaxonomicFilterGroupType.Events, TaxonomicFilterGroupType.Actions], + } return ( <> @@ -40,7 +54,7 @@ export function SecondaryGoalFunnels({ metricIdx }: { metricIdx: number }): JSX. setFunnelsMetric({ metricIdx, name: newName, - isSecondary: true, + isSecondary, }) }} /> @@ -58,7 +72,7 @@ export function SecondaryGoalFunnels({ metricIdx }: { metricIdx: number }): JSX. setFunnelsMetric({ metricIdx, series, - isSecondary: true, + isSecondary, }) }} typeKey="experiment-metric" @@ -68,7 +82,7 @@ export function SecondaryGoalFunnels({ metricIdx }: { metricIdx: number }): JSX. seriesIndicatorType="numeric" sortable={true} showNestedArrow={true} - {...commonActionFilterProps} + {...actionFilterProps} />
@@ -91,14 +105,14 @@ export function SecondaryGoalFunnels({ metricIdx }: { metricIdx: number }): JSX. setFunnelsMetric({ metricIdx, funnelWindowInterval: funnelWindowInterval, - isSecondary: true, + isSecondary, }) }} onFunnelWindowIntervalUnitChange={(funnelWindowIntervalUnit) => { setFunnelsMetric({ metricIdx, funnelWindowIntervalUnit: funnelWindowIntervalUnit || undefined, - isSecondary: true, + isSecondary, }) }} /> @@ -126,18 +140,21 @@ export function SecondaryGoalFunnels({ metricIdx }: { metricIdx: number }): JSX. breakdownAttributionValue: breakdownAttributionValue ? parseInt(breakdownAttributionValue) : undefined, - isSecondary: true, + isSecondary, }) }} stepsLength={currentMetric.funnels_query?.series?.length} /> { + const val = currentMetric.funnels_query?.filterTestAccounts + return hasFilters ? !!val : false + })()} onChange={(checked: boolean) => { setFunnelsMetric({ metricIdx, filterTestAccounts: checked, - isSecondary: true, + isSecondary, }) }} fullWidth diff --git a/frontend/src/scenes/experiments/Metrics/SecondaryMetricModal.tsx b/frontend/src/scenes/experiments/Metrics/MetricModal.tsx similarity index 54% rename from frontend/src/scenes/experiments/Metrics/SecondaryMetricModal.tsx rename to frontend/src/scenes/experiments/Metrics/MetricModal.tsx index b61af3fff9c7e..dde6c2e1b6d00 100644 --- a/frontend/src/scenes/experiments/Metrics/SecondaryMetricModal.tsx +++ b/frontend/src/scenes/experiments/Metrics/MetricModal.tsx @@ -1,48 +1,61 @@ import { LemonButton, LemonModal, LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { ExperimentFunnelsQuery } from '~/queries/schema' import { Experiment, InsightType } from '~/types' import { experimentLogic, getDefaultFunnelsMetric, getDefaultTrendsMetric } from '../experimentLogic' -import { SecondaryGoalFunnels } from './SecondaryGoalFunnels' -import { SecondaryGoalTrends } from './SecondaryGoalTrends' +import { FunnelsMetricForm } from './FunnelsMetricForm' +import { TrendsMetricForm } from './TrendsMetricForm' -export function SecondaryMetricModal({ experimentId }: { experimentId: Experiment['id'] }): JSX.Element { +export function MetricModal({ + experimentId, + isSecondary, +}: { + experimentId: Experiment['id'] + isSecondary?: boolean +}): JSX.Element { const { experiment, experimentLoading, + getMetricType, getSecondaryMetricType, + isPrimaryMetricModalOpen, isSecondaryMetricModalOpen, + editingPrimaryMetricIndex, editingSecondaryMetricIndex, } = useValues(experimentLogic({ experimentId })) - const { setExperiment, updateExperimentGoal, closeSecondaryMetricModal } = useActions( + const { updateExperimentGoal, setExperiment, closePrimaryMetricModal, closeSecondaryMetricModal } = useActions( experimentLogic({ experimentId }) ) - if (!editingSecondaryMetricIndex && editingSecondaryMetricIndex !== 0) { + const metricIdx = isSecondary ? editingSecondaryMetricIndex : editingPrimaryMetricIndex + const metricsField = isSecondary ? 'metrics_secondary' : 'metrics' + + if (!metricIdx && metricIdx !== 0) { return <> } - const metricIdx = editingSecondaryMetricIndex - const metricType = getSecondaryMetricType(metricIdx) + const metricType = isSecondary ? getSecondaryMetricType(metricIdx) : getMetricType(metricIdx) + const metrics = experiment[metricsField] + const metric = metrics[metricIdx] + const funnelStepsLength = (metric as ExperimentFunnelsQuery)?.funnels_query?.series?.length || 0 return ( { - const newMetricsSecondary = experiment.metrics_secondary.filter( - (_, idx) => idx !== metricIdx - ) + const newMetrics = metrics.filter((_, idx) => idx !== metricIdx) setExperiment({ - metrics_secondary: newMetricsSecondary, + [metricsField]: newMetrics, }) updateExperimentGoal() }} @@ -50,10 +63,20 @@ export function SecondaryMetricModal({ experimentId }: { experimentId: Experimen Delete
- + Cancel { updateExperimentGoal() }} @@ -75,12 +98,12 @@ export function SecondaryMetricModal({ experimentId }: { experimentId: Experimen onChange={(newMetricType) => { setExperiment({ ...experiment, - metrics_secondary: [ - ...experiment.metrics_secondary.slice(0, metricIdx), + [metricsField]: [ + ...metrics.slice(0, metricIdx), newMetricType === InsightType.TRENDS ? getDefaultTrendsMetric() : getDefaultFunnelsMetric(), - ...experiment.metrics_secondary.slice(metricIdx + 1), + ...metrics.slice(metricIdx + 1), ], }) }} @@ -91,9 +114,9 @@ export function SecondaryMetricModal({ experimentId }: { experimentId: Experimen />
{metricType === InsightType.TRENDS ? ( - + ) : ( - + )}
) diff --git a/frontend/src/scenes/experiments/Metrics/PrimaryGoalFunnels.tsx b/frontend/src/scenes/experiments/Metrics/PrimaryGoalFunnels.tsx deleted file mode 100644 index 2c5fe6f2da780..0000000000000 --- a/frontend/src/scenes/experiments/Metrics/PrimaryGoalFunnels.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import { LemonLabel } from '@posthog/lemon-ui' -import { LemonInput } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import { TestAccountFilterSwitch } from 'lib/components/TestAccountFiltersSwitch' -import { EXPERIMENT_DEFAULT_DURATION, FEATURE_FLAGS } from 'lib/constants' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' -import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' -import { getHogQLValue } from 'scenes/insights/filters/AggregationSelect' -import { teamLogic } from 'scenes/teamLogic' - -import { actionsAndEventsToSeries, filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' -import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { Query } from '~/queries/Query/Query' -import { ExperimentFunnelsQuery, NodeKind } from '~/queries/schema' -import { BreakdownAttributionType, FilterType, FunnelsFilterType } from '~/types' - -import { experimentLogic } from '../experimentLogic' -import { - commonActionFilterProps, - FunnelAggregationSelect, - FunnelAttributionSelect, - FunnelConversionWindowFilter, -} from './Selectors' -export function PrimaryGoalFunnels(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - const { experiment, isExperimentRunning, featureFlags, editingPrimaryMetricIndex } = useValues(experimentLogic) - const { setExperiment, setFunnelsMetric } = useActions(experimentLogic) - const hasFilters = (currentTeam?.test_account_filters || []).length > 0 - - if (!editingPrimaryMetricIndex && editingPrimaryMetricIndex !== 0) { - return <> - } - - const metricIdx = editingPrimaryMetricIndex - const currentMetric = experiment.metrics[metricIdx] as ExperimentFunnelsQuery - - const actionFilterProps = { - ...commonActionFilterProps, - // Remove data warehouse from the list because it's not supported in experiments - actionsTaxonomicGroupTypes: [TaxonomicFilterGroupType.Events, TaxonomicFilterGroupType.Actions], - } - - return ( - <> -
- Name (optional) - {featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL] && ( - { - setFunnelsMetric({ - metricIdx, - name: newName, - }) - }} - /> - )} -
- { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - return queryNodeToFilter(currentMetric.funnels_query) - } - return experiment.filters - })()} - setFilters={({ actions, events, data_warehouse }: Partial): void => { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - const series = actionsAndEventsToSeries( - { actions, events, data_warehouse } as any, - true, - MathAvailability.None - ) - - setFunnelsMetric({ - metricIdx, - series, - }) - } else { - if (actions?.length) { - setExperiment({ - filters: { - ...experiment.filters, - actions, - events: undefined, - data_warehouse: undefined, - }, - }) - } else if (events?.length) { - setExperiment({ - filters: { - ...experiment.filters, - events, - actions: undefined, - data_warehouse: undefined, - }, - }) - } else if (data_warehouse?.length) { - setExperiment({ - filters: { - ...experiment.filters, - data_warehouse, - actions: undefined, - events: undefined, - }, - }) - } - } - }} - typeKey="experiment-metric" - mathAvailability={MathAvailability.None} - buttonCopy="Add funnel step" - showSeriesIndicator={true} - seriesIndicatorType="numeric" - sortable={true} - showNestedArrow={true} - {...actionFilterProps} - /> -
- { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - return getHogQLValue( - currentMetric.funnels_query.aggregation_group_type_index ?? undefined, - currentMetric.funnels_query.funnelsFilter?.funnelAggregateByHogQL ?? undefined - ) - } - return getHogQLValue( - experiment.filters.aggregation_group_type_index, - (experiment.filters as FunnelsFilterType).funnel_aggregate_by_hogql - ) - })()} - onChange={(value) => { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - setFunnelsMetric({ - metricIdx, - funnelAggregateByHogQL: value, - }) - } else { - setExperiment({ - filters: { - ...experiment.filters, - funnel_aggregate_by_hogql: value, - }, - }) - } - }} - /> - { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - return currentMetric.funnels_query?.funnelsFilter?.funnelWindowInterval - } - return (experiment.filters as FunnelsFilterType).funnel_window_interval - })()} - funnelWindowIntervalUnit={(() => { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - return currentMetric.funnels_query?.funnelsFilter?.funnelWindowIntervalUnit - } - return (experiment.filters as FunnelsFilterType).funnel_window_interval_unit - })()} - onFunnelWindowIntervalChange={(funnelWindowInterval) => { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - setFunnelsMetric({ - metricIdx, - funnelWindowInterval: funnelWindowInterval, - }) - } else { - setExperiment({ - filters: { - ...experiment.filters, - funnel_window_interval: funnelWindowInterval, - }, - }) - } - }} - onFunnelWindowIntervalUnitChange={(funnelWindowIntervalUnit) => { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - setFunnelsMetric({ - metricIdx, - funnelWindowIntervalUnit: funnelWindowIntervalUnit || undefined, - }) - } else { - setExperiment({ - filters: { - ...experiment.filters, - funnel_window_interval_unit: funnelWindowIntervalUnit || undefined, - }, - }) - } - }} - /> - { - // :FLAG: CLEAN UP AFTER MIGRATION - let breakdownAttributionType - let breakdownAttributionValue - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - breakdownAttributionType = - currentMetric.funnels_query?.funnelsFilter?.breakdownAttributionType - breakdownAttributionValue = - currentMetric.funnels_query?.funnelsFilter?.breakdownAttributionValue - } else { - breakdownAttributionType = (experiment.filters as FunnelsFilterType) - .breakdown_attribution_type - breakdownAttributionValue = (experiment.filters as FunnelsFilterType) - .breakdown_attribution_value - } - - const currentValue: BreakdownAttributionType | `${BreakdownAttributionType.Step}/${number}` = - !breakdownAttributionType - ? BreakdownAttributionType.FirstTouch - : breakdownAttributionType === BreakdownAttributionType.Step - ? `${breakdownAttributionType}/${breakdownAttributionValue || 0}` - : breakdownAttributionType - - return currentValue - })()} - onChange={(value) => { - const [breakdownAttributionType, breakdownAttributionValue] = (value || '').split('/') - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - setFunnelsMetric({ - metricIdx, - breakdownAttributionType: breakdownAttributionType as BreakdownAttributionType, - breakdownAttributionValue: breakdownAttributionValue - ? parseInt(breakdownAttributionValue) - : undefined, - }) - } else { - setExperiment({ - filters: { - ...experiment.filters, - breakdown_attribution_type: breakdownAttributionType as BreakdownAttributionType, - breakdown_attribution_value: breakdownAttributionValue - ? parseInt(breakdownAttributionValue) - : 0, - }, - }) - } - }} - stepsLength={(() => { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - return currentMetric.funnels_query?.series?.length - } - return Math.max( - experiment.filters.actions?.length ?? 0, - experiment.filters.events?.length ?? 0, - experiment.filters.data_warehouse?.length ?? 0 - ) - })()} - /> - { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - const val = (experiment.metrics[metricIdx] as ExperimentFunnelsQuery).funnels_query - ?.filterTestAccounts - return hasFilters ? !!val : false - } - return hasFilters ? !!experiment.filters.filter_test_accounts : false - })()} - onChange={(checked: boolean) => { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - setFunnelsMetric({ - metricIdx, - filterTestAccounts: checked, - }) - } else { - setExperiment({ - filters: { - ...experiment.filters, - filter_test_accounts: checked, - }, - }) - } - }} - fullWidth - /> -
- {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. - - )} -
- {/* :FLAG: CLEAN UP AFTER MIGRATION */} - { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - return currentMetric.funnels_query - } - return filtersToQueryNode(experiment.filters) - })(), - showTable: false, - showLastComputation: true, - showLastComputationRefresh: false, - }} - readOnly - /> -
- - ) -} diff --git a/frontend/src/scenes/experiments/Metrics/PrimaryMetricModal.tsx b/frontend/src/scenes/experiments/Metrics/PrimaryMetricModal.tsx deleted file mode 100644 index 31d4eec71265b..0000000000000 --- a/frontend/src/scenes/experiments/Metrics/PrimaryMetricModal.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { LemonButton, LemonModal, LemonSelect } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { FEATURE_FLAGS } from 'lib/constants' - -import { ExperimentFunnelsQuery } from '~/queries/schema' -import { Experiment, InsightType } from '~/types' - -import { experimentLogic, getDefaultFilters, getDefaultFunnelsMetric, getDefaultTrendsMetric } from '../experimentLogic' -import { PrimaryGoalFunnels } from '../Metrics/PrimaryGoalFunnels' -import { PrimaryGoalTrends } from '../Metrics/PrimaryGoalTrends' - -export function PrimaryMetricModal({ experimentId }: { experimentId: Experiment['id'] }): JSX.Element { - const { - experiment, - experimentLoading, - getMetricType, - featureFlags, - isPrimaryMetricModalOpen, - editingPrimaryMetricIndex, - } = useValues(experimentLogic({ experimentId })) - const { updateExperimentGoal, setExperiment, closePrimaryMetricModal } = useActions( - experimentLogic({ experimentId }) - ) - - if (!editingPrimaryMetricIndex && editingPrimaryMetricIndex !== 0) { - return <> - } - - const metricIdx = editingPrimaryMetricIndex - const metricType = getMetricType(metricIdx) - - let funnelStepsLength = 0 - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL] && metricType === InsightType.FUNNELS) { - const metric = experiment.metrics[metricIdx] as ExperimentFunnelsQuery - funnelStepsLength = metric?.funnels_query?.series?.length || 0 - } else { - funnelStepsLength = (experiment.filters?.events?.length || 0) + (experiment.filters?.actions?.length || 0) - } - - return ( - - { - const newMetrics = experiment.metrics.filter((_, idx) => idx !== metricIdx) - setExperiment({ - metrics: newMetrics, - }) - updateExperimentGoal() - }} - > - Delete - -
- - Cancel - - { - updateExperimentGoal() - }} - type="primary" - loading={experimentLoading} - data-attr="create-annotation-submit" - > - Save - -
-
- } - > -
- Metric type - { - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - setExperiment({ - ...experiment, - metrics: [ - ...experiment.metrics.slice(0, metricIdx), - newMetricType === InsightType.TRENDS - ? getDefaultTrendsMetric() - : getDefaultFunnelsMetric(), - ...experiment.metrics.slice(metricIdx + 1), - ], - }) - } else { - setExperiment({ - ...experiment, - filters: getDefaultFilters(newMetricType, undefined), - }) - } - }} - options={[ - { value: InsightType.TRENDS, label: Trends }, - { value: InsightType.FUNNELS, label: Funnels }, - ]} - /> -
- {metricType === InsightType.TRENDS ? : } - - ) -} diff --git a/frontend/src/scenes/experiments/Metrics/SecondaryGoalTrends.tsx b/frontend/src/scenes/experiments/Metrics/SecondaryGoalTrends.tsx deleted file mode 100644 index 20aae645e6e1e..0000000000000 --- a/frontend/src/scenes/experiments/Metrics/SecondaryGoalTrends.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { LemonLabel } from '@posthog/lemon-ui' -import { LemonInput } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { TestAccountFilterSwitch } from 'lib/components/TestAccountFiltersSwitch' -import { EXPERIMENT_DEFAULT_DURATION, FEATURE_FLAGS } from 'lib/constants' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' -import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' -import { teamLogic } from 'scenes/teamLogic' - -import { actionsAndEventsToSeries, filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' -import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { Query } from '~/queries/Query/Query' -import { ExperimentTrendsQuery, NodeKind } from '~/queries/schema' -import { FilterType } from '~/types' - -import { experimentLogic } from '../experimentLogic' -import { commonActionFilterProps } from './Selectors' - -export function SecondaryGoalTrends({ metricIdx }: { metricIdx: number }): JSX.Element { - const { experiment, isExperimentRunning, featureFlags } = useValues(experimentLogic) - const { setExperiment, setTrendsMetric } = useActions(experimentLogic) - const { currentTeam } = useValues(teamLogic) - const hasFilters = (currentTeam?.test_account_filters || []).length > 0 - const currentMetric = experiment.metrics_secondary[metricIdx] as ExperimentTrendsQuery - - return ( - <> -
- Name (optional) - { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - return currentMetric.name - } - return experiment.secondary_metrics[metricIdx].name - })()} - onChange={(newName) => { - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - setTrendsMetric({ - metricIdx, - name: newName, - isSecondary: true, - }) - } else { - setExperiment({ - secondary_metrics: experiment.secondary_metrics.map((metric, idx) => - idx === metricIdx ? { ...metric, name: newName } : metric - ), - }) - } - }} - /> -
- { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - return queryNodeToFilter(currentMetric.count_query) - } - return experiment.secondary_metrics[metricIdx].filters - })()} - setFilters={({ actions, events, data_warehouse }: Partial): void => { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - const series = actionsAndEventsToSeries( - { actions, events, data_warehouse } as any, - true, - MathAvailability.All - ) - - setTrendsMetric({ - metricIdx, - series, - isSecondary: true, - }) - } else { - if (actions?.length) { - setExperiment({ - secondary_metrics: experiment.secondary_metrics.map((metric, idx) => - idx === metricIdx - ? { - ...metric, - filters: { - ...metric.filters, - actions, - events: undefined, - data_warehouse: undefined, - }, - } - : metric - ), - }) - } else if (events?.length) { - setExperiment({ - secondary_metrics: experiment.secondary_metrics.map((metric, idx) => - idx === metricIdx - ? { - ...metric, - filters: { - ...metric.filters, - events, - actions: undefined, - data_warehouse: undefined, - }, - } - : metric - ), - }) - } else if (data_warehouse?.length) { - setExperiment({ - secondary_metrics: experiment.secondary_metrics.map((metric, idx) => - idx === metricIdx - ? { - ...metric, - filters: { - ...metric.filters, - data_warehouse, - actions: undefined, - events: undefined, - }, - } - : metric - ), - }) - } - } - }} - typeKey="experiment-metric" - buttonCopy="Add graph series" - showSeriesIndicator={true} - entitiesLimit={1} - showNumericalPropsOnly={true} - {...commonActionFilterProps} - /> -
- { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - const val = currentMetric.count_query?.filterTestAccounts - return hasFilters ? !!val : false - } - return hasFilters - ? !!experiment.secondary_metrics[metricIdx].filters.filter_test_accounts - : false - })()} - onChange={(checked: boolean) => { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - setTrendsMetric({ - metricIdx, - filterTestAccounts: checked, - isSecondary: true, - }) - } else { - setExperiment({ - secondary_metrics: experiment.secondary_metrics.map((metric, idx) => - idx === metricIdx - ? { - ...metric, - filters: { - ...metric.filters, - filter_test_accounts: checked, - }, - } - : metric - ), - }) - } - }} - fullWidth - /> -
- {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. - - )} -
- {/* :FLAG: CLEAN UP AFTER MIGRATION */} - { - // :FLAG: CLEAN UP AFTER MIGRATION - if (featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { - return currentMetric.count_query - } - return filtersToQueryNode(experiment.secondary_metrics[metricIdx].filters) - })(), - showTable: false, - showLastComputation: true, - showLastComputationRefresh: false, - }} - readOnly - /> -
- - ) -} diff --git a/frontend/src/scenes/experiments/Metrics/TrendsMetricForm.tsx b/frontend/src/scenes/experiments/Metrics/TrendsMetricForm.tsx new file mode 100644 index 0000000000000..f76d69664f008 --- /dev/null +++ b/frontend/src/scenes/experiments/Metrics/TrendsMetricForm.tsx @@ -0,0 +1,290 @@ +import { IconCheckCircle } from '@posthog/icons' +import { LemonInput, LemonLabel, LemonTabs, LemonTag } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { TestAccountFilterSwitch } from 'lib/components/TestAccountFiltersSwitch' +import { EXPERIMENT_DEFAULT_DURATION } from 'lib/constants' +import { dayjs } from 'lib/dayjs' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { useState } from 'react' +import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' +import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' +import { teamLogic } from 'scenes/teamLogic' + +import { actionsAndEventsToSeries } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { Query } from '~/queries/Query/Query' +import { ExperimentTrendsQuery, InsightQueryNode, NodeKind } from '~/queries/schema' +import { BaseMathType, ChartDisplayType, FilterType } from '~/types' + +import { experimentLogic } from '../experimentLogic' +import { commonActionFilterProps } from './Selectors' + +export function TrendsMetricForm({ isSecondary = false }: { isSecondary?: boolean }): JSX.Element { + const { experiment, isExperimentRunning, editingPrimaryMetricIndex, editingSecondaryMetricIndex } = + useValues(experimentLogic) + const { setTrendsMetric, setTrendsExposureMetric, setExperiment } = useActions(experimentLogic) + const { currentTeam } = useValues(teamLogic) + const hasFilters = (currentTeam?.test_account_filters || []).length > 0 + const [activeTab, setActiveTab] = useState('main') + + const metrics = isSecondary ? experiment.metrics_secondary : experiment.metrics + const metricIdx = isSecondary ? editingSecondaryMetricIndex : editingPrimaryMetricIndex + + if (!metricIdx && metricIdx !== 0) { + return <> + } + + const currentMetric = metrics[metricIdx] as ExperimentTrendsQuery + + return ( + <> + setActiveTab(newKey)} + tabs={[ + { + key: 'main', + label: 'Main metric', + content: ( + <> +
+ Name (optional) + { + setTrendsMetric({ + metricIdx, + name: newName, + isSecondary, + }) + }} + /> +
+ ): void => { + const series = actionsAndEventsToSeries( + { actions, events, data_warehouse } as any, + true, + MathAvailability.All + ) + + setTrendsMetric({ + metricIdx, + series, + isSecondary, + }) + }} + typeKey="experiment-metric" + buttonCopy="Add graph series" + showSeriesIndicator={true} + entitiesLimit={1} + showNumericalPropsOnly={true} + {...commonActionFilterProps} + /> +
+ { + setTrendsMetric({ + metricIdx, + filterTestAccounts: checked, + isSecondary, + }) + }} + fullWidth + /> +
+ {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. + + )} +
+ +
+ + ), + }, + { + key: 'exposure', + label: 'Exposure', + content: ( + <> +
+
{ + const metricsField = isSecondary ? 'metrics_secondary' : 'metrics' + setExperiment({ + ...experiment, + [metricsField]: metrics.map((metric, idx) => + idx === metricIdx + ? { ...metric, exposure_query: undefined } + : metric + ), + }) + }} + > +
+ Default + {!currentMetric.exposure_query && ( + + )} +
+
+ Uses the number of unique users who trigger the{' '} + $feature_flag_called event as your exposure count. This + is the recommended setting for most experiments, as it accurately tracks + variant exposure. +
+
+
{ + const metricsField = isSecondary ? 'metrics_secondary' : 'metrics' + setExperiment({ + ...experiment, + [metricsField]: metrics.map((metric, idx) => + idx === metricIdx + ? { + ...metric, + exposure_query: { + kind: NodeKind.TrendsQuery, + series: [ + { + kind: NodeKind.EventsNode, + name: '$feature_flag_called', + event: '$feature_flag_called', + math: BaseMathType.UniqueUsers, + }, + ], + interval: 'day', + dateRange: { + date_from: dayjs() + .subtract(EXPERIMENT_DEFAULT_DURATION, 'day') + .format('YYYY-MM-DDTHH:mm'), + date_to: dayjs() + .endOf('d') + .format('YYYY-MM-DDTHH:mm'), + explicitDate: true, + }, + trendsFilter: { + display: ChartDisplayType.ActionsLineGraph, + }, + filterTestAccounts: true, + }, + } + : metric + ), + }) + }} + > +
+ Custom + {currentMetric.exposure_query && ( + + )} +
+
+ Define your own exposure metric for specific use cases, such as counting by + sessions instead of users. This gives you full control but requires careful + configuration. +
+
+
+ {currentMetric.exposure_query && ( + <> + ): void => { + const series = actionsAndEventsToSeries( + { actions, events, data_warehouse } as any, + true, + MathAvailability.All + ) + + setTrendsExposureMetric({ + metricIdx, + series, + isSecondary, + }) + }} + typeKey="experiment-metric" + buttonCopy="Add graph series" + showSeriesIndicator={true} + entitiesLimit={1} + showNumericalPropsOnly={true} + {...commonActionFilterProps} + /> +
+ { + const val = currentMetric.exposure_query?.filterTestAccounts + return hasFilters ? !!val : false + })()} + onChange={(checked: boolean) => { + setTrendsExposureMetric({ + metricIdx, + filterTestAccounts: checked, + isSecondary, + }) + }} + fullWidth + /> +
+ {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. + + )} +
+ +
+ + )} + + ), + }, + ]} + /> + + ) +} diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index 44753c32582a8..beb7356103c97 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -220,12 +220,14 @@ export const experimentLogic = kea([ name, series, filterTestAccounts, + isSecondary = false, }: { metricIdx: number name?: string series?: any[] filterTestAccounts?: boolean - }) => ({ metricIdx, name, series, filterTestAccounts }), + isSecondary?: boolean + }) => ({ metricIdx, name, series, filterTestAccounts, isSecondary }), setFunnelsMetric: ({ metricIdx, name, @@ -348,8 +350,9 @@ export const experimentLogic = kea([ [metricsKey]: metrics, } }, - setTrendsExposureMetric: (state, { metricIdx, name, series, filterTestAccounts }) => { - const metrics = [...(state?.metrics || [])] + setTrendsExposureMetric: (state, { metricIdx, name, series, filterTestAccounts, isSecondary }) => { + const metricsKey = isSecondary ? 'metrics_secondary' : 'metrics' + const metrics = [...(state?.[metricsKey] || [])] const metric = metrics[metricIdx] metrics[metricIdx] = { @@ -364,7 +367,7 @@ export const experimentLogic = kea([ return { ...state, - metrics, + [metricsKey]: metrics, } }, setFunnelsMetric: (