diff --git a/cypress/e2e/experiments.cy.ts b/cypress/e2e/experiments.cy.ts index e0e8339920bee..bd94cbf57d545 100644 --- a/cypress/e2e/experiments.cy.ts +++ b/cypress/e2e/experiments.cy.ts @@ -43,89 +43,4 @@ describe('Experiments', () => { // Save experiment cy.get('[data-attr="save-experiment"]').first().click() }) - - const createExperimentInNewUi = (): void => { - cy.visit('/experiments') - - // Name, flag key, description - cy.get('[data-attr=create-experiment]').first().click() - cy.get('[data-attr=experiment-name]').click().type(`${experimentName}`).should('have.value', experimentName) - cy.get('[data-attr=experiment-feature-flag-key]') - .click() - .type(`${featureFlagKey}`) - .should('have.value', featureFlagKey) - cy.get('[data-attr=experiment-description]') - .click() - .type('This is the description of the experiment') - .should('have.value', 'This is the description of the experiment') - - // Edit variants - cy.get('[data-attr="add-test-variant"]').click() - cy.get('input[data-attr="experiment-variant-key"][data-key-index="1"]') - .clear() - .type('test-variant-1') - .should('have.value', 'test-variant-1') - cy.get('input[data-attr="experiment-variant-key"][data-key-index="2"]') - .clear() - .type('test-variant-2') - .should('have.value', 'test-variant-2') - - // Save experiment - cy.get('[data-attr="save-experiment"]').first().click() - - // Set the experiment goal once the experiment is drafted - cy.get('[data-attr="add-experiment-goal"]').click() - - // Wait for the goal modal to open and click the confirmation button - cy.get('.LemonModal__layout').should('be.visible') - cy.contains('Change experiment goal').should('be.visible') - cy.get('.LemonModal__content').contains('button', 'Add funnel step').click() - cy.get('.LemonModal__footer').contains('button', 'Save').click() - } - - it('create, launch and stop experiment with new ui', () => { - createExperimentInNewUi() - cy.get('[data-attr="experiment-status"]').contains('draft').should('be.visible') - - cy.get('[data-attr="experiment-creation-date"]').contains('a few seconds ago').should('be.visible') - cy.get('[data-attr="experiment-start-date"]').should('not.exist') - - cy.wait(1000) - cy.get('[data-attr="launch-experiment"]').first().click() - cy.get('[data-attr="experiment-creation-date"]').should('not.exist') - cy.get('[data-attr="experiment-start-date"]').contains('a few seconds ago').should('be.visible') - - cy.get('[data-attr="stop-experiment"]').first().click() - // Wait for the dialog to appear and click the confirmation button - cy.get('.LemonModal__layout').should('be.visible') - cy.contains('Stop this experiment?').should('be.visible') - cy.get('.LemonModal__footer').contains('button', 'Stop').click() - // Wait for the dialog to disappear - cy.get('[data-attr="experiment-creation-date"]').should('not.exist') - cy.get('[data-attr="experiment-start-date"]').contains('a few seconds ago').should('be.visible') - cy.get('[data-attr="experiment-end-date"]').contains('a few seconds ago').should('be.visible') - }) - - it('move start date', () => { - createExperimentInNewUi() - - cy.wait(1000) - cy.get('[data-attr="launch-experiment"]').first().click() - - cy.get('[data-attr="move-experiment-start-date"]').first().click() - cy.get('[data-attr="experiment-start-date-picker"]').should('exist') - cy.get('[data-attr="lemon-calendar-month-previous"]').first().click() - cy.get('[data-attr="lemon-calendar-day"]').first().click() - cy.get('[data-attr="lemon-calendar-select-apply"]').first().click() - cy.get('[data-attr="experiment-start-date"]') - .contains(/months? ago/) - .should('be.visible') - - cy.reload() - - // Check that the start date persists - cy.get('[data-attr="experiment-start-date"]') - .contains(/months? ago/) - .should('be.visible') - }) }) diff --git a/frontend/src/scenes/experiments/Metrics/PrimaryGoalTrends.tsx b/frontend/src/scenes/experiments/Metrics/PrimaryGoalTrends.tsx index 8f7f5fe17df4b..7ab7bc0880b5a 100644 --- a/frontend/src/scenes/experiments/Metrics/PrimaryGoalTrends.tsx +++ b/frontend/src/scenes/experiments/Metrics/PrimaryGoalTrends.tsx @@ -1,8 +1,11 @@ -import { LemonInput, LemonLabel } from '@posthog/lemon-ui' +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' @@ -10,17 +13,18 @@ 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, NodeKind } from '~/queries/schema' -import { FilterType } from '~/types' +import { ExperimentTrendsQuery, InsightQueryNode, NodeKind } from '~/queries/schema' +import { BaseMathType, ChartDisplayType, FilterType } from '~/types' import { experimentLogic } from '../experimentLogic' import { commonActionFilterProps } from './Selectors' export function PrimaryGoalTrends(): JSX.Element { const { experiment, isExperimentRunning, editingPrimaryMetricIndex } = useValues(experimentLogic) - const { setTrendsMetric } = useActions(experimentLogic) + const { setTrendsMetric, setTrendsExposureMetric, setExperiment } = useActions(experimentLogic) const { currentTeam } = useValues(teamLogic) const hasFilters = (currentTeam?.test_account_filters || []).length > 0 + const [activeTab, setActiveTab] = useState('main') if (!editingPrimaryMetricIndex && editingPrimaryMetricIndex !== 0) { return <> @@ -31,70 +35,246 @@ export function PrimaryGoalTrends(): JSX.Element { return ( <> -
- Name (optional) - { - setTrendsMetric({ - metricIdx, - name: newName, - }) - }} - /> -
- ): void => { - const series = actionsAndEventsToSeries( - { actions, events, data_warehouse } as any, - true, - MathAvailability.All - ) + setActiveTab(newKey)} + tabs={[ + { + key: 'main', + label: 'Main metric', + content: ( + <> +
+ Name (optional) + { + setTrendsMetric({ + metricIdx, + name: newName, + }) + }} + /> +
+ ): void => { + const series = actionsAndEventsToSeries( + { actions, events, data_warehouse } as any, + true, + MathAvailability.All + ) - setTrendsMetric({ - metricIdx, - series, - }) - }} - typeKey="experiment-metric" - buttonCopy="Add graph series" - showSeriesIndicator={true} - entitiesLimit={1} - showNumericalPropsOnly={true} - {...commonActionFilterProps} + setTrendsMetric({ + metricIdx, + series, + }) + }} + typeKey="experiment-metric" + buttonCopy="Add graph series" + showSeriesIndicator={true} + entitiesLimit={1} + showNumericalPropsOnly={true} + {...commonActionFilterProps} + /> +
+ { + setTrendsMetric({ + metricIdx, + filterTestAccounts: 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. + + )} +
+ +
+ + ), + }, + { + key: 'exposure', + label: 'Exposure', + content: ( + <> +
+
{ + setExperiment({ + ...experiment, + metrics: experiment.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. +
+
+
{ + setExperiment({ + ...experiment, + metrics: experiment.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, + }) + }} + 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, + }) + }} + 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. + + )} +
+ +
+ + )} + + ), + }, + ]} /> -
- { - setTrendsMetric({ - metricIdx, - filterTestAccounts: 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. - - )} -
- -
) }