From 62c98eaa0853918f47a2d384efdf1cd15850285f Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Fri, 20 Dec 2024 12:01:34 +0100 Subject: [PATCH 1/2] init --- .../ExperimentView/ExperimentView.tsx | 2 + .../experiments/ExperimentView/Goal.tsx | 2 - .../experiments/Metrics/PrimaryGoalTrends.tsx | 311 ++++++++++++++---- 3 files changed, 247 insertions(+), 68 deletions(-) diff --git a/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx b/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx index 9a9ad237665f4..c5dc5eb43cb4b 100644 --- a/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx @@ -6,6 +6,7 @@ import { WebExperimentImplementationDetails } from 'scenes/experiments/WebExperi import { ExperimentImplementationDetails } from '../ExperimentImplementationDetails' import { experimentLogic } from '../experimentLogic' +import { PrimaryMetricModal } from '../Metrics/PrimaryMetricModal' import { MetricsView } from '../MetricsView/MetricsView' import { ExperimentLoadingAnimation, @@ -129,6 +130,7 @@ export function ExperimentView(): JSX.Element { /> )} + diff --git a/frontend/src/scenes/experiments/ExperimentView/Goal.tsx b/frontend/src/scenes/experiments/ExperimentView/Goal.tsx index 9eb7e01ea3bd9..ac41a2c663b4c 100644 --- a/frontend/src/scenes/experiments/ExperimentView/Goal.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/Goal.tsx @@ -11,7 +11,6 @@ import { ExperimentFunnelsQuery, ExperimentTrendsQuery, FunnelsQuery, NodeKind, import { ActionFilter, AnyPropertyFilter, ChartDisplayType, Experiment, FilterType, InsightType } from '~/types' import { experimentLogic, getDefaultFilters, getDefaultFunnelsMetric } from '../experimentLogic' -import { PrimaryMetricModal } from '../Metrics/PrimaryMetricModal' import { PrimaryTrendsExposureModal } from '../Metrics/PrimaryTrendsExposureModal' export function MetricDisplayTrends({ query }: { query: TrendsQuery | undefined }): JSX.Element { @@ -341,7 +340,6 @@ export function Goal(): JSX.Element { )} )} - ) } diff --git a/frontend/src/scenes/experiments/Metrics/PrimaryGoalTrends.tsx b/frontend/src/scenes/experiments/Metrics/PrimaryGoalTrends.tsx index 8f7f5fe17df4b..374f3fc385866 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 { 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,245 @@ 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: '$pageview', + event: '$pageview', + }, + ], + 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. - - )} -
- -
) } From 3f7a8abaf5112fada63602be93a8a89d322a07cd Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Mon, 23 Dec 2024 10:39:19 +0100 Subject: [PATCH 2/2] update --- cypress/e2e/experiments.cy.ts | 85 ------------------- .../experiments/Metrics/PrimaryGoalTrends.tsx | 7 +- 2 files changed, 4 insertions(+), 88 deletions(-) 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 374f3fc385866..7ab7bc0880b5a 100644 --- a/frontend/src/scenes/experiments/Metrics/PrimaryGoalTrends.tsx +++ b/frontend/src/scenes/experiments/Metrics/PrimaryGoalTrends.tsx @@ -14,7 +14,7 @@ import { actionsAndEventsToSeries } from '~/queries/nodes/InsightQuery/utils/fil import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { Query } from '~/queries/Query/Query' import { ExperimentTrendsQuery, InsightQueryNode, NodeKind } from '~/queries/schema' -import { ChartDisplayType, FilterType } from '~/types' +import { BaseMathType, ChartDisplayType, FilterType } from '~/types' import { experimentLogic } from '../experimentLogic' import { commonActionFilterProps } from './Selectors' @@ -165,8 +165,9 @@ export function PrimaryGoalTrends(): JSX.Element { series: [ { kind: NodeKind.EventsNode, - name: '$pageview', - event: '$pageview', + name: '$feature_flag_called', + event: '$feature_flag_called', + math: BaseMathType.UniqueUsers, }, ], interval: 'day',