mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
feat(experiments): support moving experiment start date (#21362)
Co-authored-by: Neil Kakkar <neilkakkar@gmail.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { decideResponse } from '../fixtures/api/decide'
|
||||
|
||||
describe('Experiments', () => {
|
||||
let randomNum
|
||||
let experimentName
|
||||
@@ -8,25 +10,13 @@ describe('Experiments', () => {
|
||||
fixture: 'api/experiments/user',
|
||||
})
|
||||
|
||||
cy.intercept('/api/projects/*/experiments?limit=1000', {
|
||||
fixture: 'api/experiments/experiments',
|
||||
})
|
||||
|
||||
cy.intercept('/api/projects/*/experiments/1234/', {
|
||||
fixture: 'api/experiments/new-experiment',
|
||||
})
|
||||
|
||||
cy.intercept('POST', '/api/projects/1/experiments/', (req) => {
|
||||
req.reply({ fixture: 'api/experiments/new-experiment' })
|
||||
})
|
||||
|
||||
randomNum = Math.floor(Math.random() * 10000000)
|
||||
experimentName = `Experiment ${randomNum}`
|
||||
featureFlagKey = `experiment-${randomNum}`
|
||||
cy.visit('/experiments')
|
||||
})
|
||||
|
||||
it('create experiment', () => {
|
||||
cy.visit('/experiments')
|
||||
cy.get('[data-attr=top-bar-name]').should('contain', 'A/B testing')
|
||||
|
||||
// Name, flag key, description
|
||||
@@ -76,4 +66,76 @@ describe('Experiments', () => {
|
||||
// Save experiment
|
||||
cy.get('[data-attr="save-experiment"]').first().click()
|
||||
})
|
||||
|
||||
const createExperimentInNewUi = () => {
|
||||
cy.intercept('**/decide/*', (req) =>
|
||||
req.reply(
|
||||
decideResponse({
|
||||
'new-experiments-ui': true,
|
||||
})
|
||||
)
|
||||
)
|
||||
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')
|
||||
|
||||
// Continue creation
|
||||
cy.get('[data-attr="continue-experiment-creation"]').first().click()
|
||||
// Save experiment
|
||||
cy.get('[data-attr="save-experiment"]').first().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.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()
|
||||
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.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"]').clear().type('2020-01-01 00:00:00')
|
||||
cy.get('.ant-picker-dropdown').contains('Ok').first().click()
|
||||
cy.get('[data-attr="experiment-start-date"]').contains('years ago').should('be.visible')
|
||||
|
||||
cy.reload()
|
||||
|
||||
// Check that the start date persists
|
||||
cy.get('[data-attr="experiment-start-date"]').contains('years ago').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -386,6 +386,10 @@ export const eventUsageLogic = kea<eventUsageLogicType>([
|
||||
reportExperimentCreated: (experiment: Experiment) => ({ experiment }),
|
||||
reportExperimentViewed: (experiment: Experiment) => ({ experiment }),
|
||||
reportExperimentLaunched: (experiment: Experiment, launchDate: Dayjs) => ({ experiment, launchDate }),
|
||||
reportExperimentStartDateChange: (experiment: Experiment, newStartDate: string) => ({
|
||||
experiment,
|
||||
newStartDate,
|
||||
}),
|
||||
reportExperimentCompleted: (
|
||||
experiment: Experiment,
|
||||
endDate: Dayjs,
|
||||
@@ -978,6 +982,14 @@ export const eventUsageLogic = kea<eventUsageLogicType>([
|
||||
launch_date: launchDate.toISOString(),
|
||||
})
|
||||
},
|
||||
reportExperimentStartDateChange: ({ experiment, newStartDate }) => {
|
||||
posthog.capture('experiment start date changed', {
|
||||
name: experiment.name,
|
||||
id: experiment.id,
|
||||
old_start_date: experiment.start_date,
|
||||
new_start_date: newStartDate,
|
||||
})
|
||||
},
|
||||
reportExperimentCompleted: ({ experiment, endDate, duration, significant }) => {
|
||||
posthog.capture('experiment completed', {
|
||||
name: experiment.name,
|
||||
|
||||
@@ -31,17 +31,20 @@ const StepInfo = (): JSX.Element => {
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6 max-w-120">
|
||||
<LemonField name="name" label="Name">
|
||||
<LemonInput placeholder="Pricing page conversion" />
|
||||
<LemonInput placeholder="Pricing page conversion" data-attr="experiment-name" />
|
||||
</LemonField>
|
||||
<LemonField
|
||||
name="feature_flag_key"
|
||||
label="Feature flag key"
|
||||
help="Each experiment is backed by a feature flag. You'll use this key in your code."
|
||||
>
|
||||
<LemonInput placeholder="pricing-page-conversion" />
|
||||
<LemonInput placeholder="pricing-page-conversion" data-attr="experiment-feature-flag-key" />
|
||||
</LemonField>
|
||||
<LemonField name="description" label="Description">
|
||||
<LemonTextArea placeholder="The goal of this experiment is ..." />
|
||||
<LemonTextArea
|
||||
placeholder="The goal of this experiment is ..."
|
||||
data-attr="experiment-description"
|
||||
/>
|
||||
</LemonField>
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
@@ -132,7 +135,12 @@ const StepInfo = (): JSX.Element => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LemonButton className="mt-2" type="primary" onClick={() => moveToNextFormStep()}>
|
||||
<LemonButton
|
||||
className="mt-2"
|
||||
type="primary"
|
||||
data-attr="continue-experiment-creation"
|
||||
onClick={() => moveToNextFormStep()}
|
||||
>
|
||||
Continue
|
||||
</LemonButton>
|
||||
</div>
|
||||
@@ -255,6 +263,7 @@ const StepGoal = (): JSX.Element => {
|
||||
<LemonButton
|
||||
className="mt-2"
|
||||
type="primary"
|
||||
data-attr="save-experiment"
|
||||
onClick={() => {
|
||||
const { exposure, sampleSize } = exposureAndSampleSize
|
||||
createExperiment(true, exposure, sampleSize)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { IconPencil } from '@posthog/icons'
|
||||
import { LemonButton } from '@posthog/lemon-ui'
|
||||
import clsx from 'clsx'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { DatePicker } from 'lib/components/DatePicker'
|
||||
import { TZLabel } from 'lib/components/TZLabel'
|
||||
import { dayjs } from 'lib/dayjs'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { experimentLogic } from '../experimentLogic'
|
||||
|
||||
export function ExperimentDates(): JSX.Element {
|
||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false)
|
||||
const { experiment } = useValues(experimentLogic)
|
||||
const { changeExperimentStartDate } = useActions(experimentLogic)
|
||||
const { created_at, start_date, end_date } = experiment
|
||||
|
||||
if (!start_date) {
|
||||
if (!created_at) {
|
||||
return <></>
|
||||
}
|
||||
return (
|
||||
<div className="block" data-attr="experiment-creation-date">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide">Creation date</div>
|
||||
<TZLabel time={created_at} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="block" data-attr="experiment-start-date">
|
||||
<div
|
||||
className={clsx(
|
||||
'text-xs font-semibold uppercase tracking-wide',
|
||||
isStartDatePickerOpen && 'text-center'
|
||||
)}
|
||||
>
|
||||
Start date
|
||||
</div>
|
||||
<div className="flex">
|
||||
{isStartDatePickerOpen ? (
|
||||
<DatePicker
|
||||
showTime={true}
|
||||
showSecond={false}
|
||||
open={true}
|
||||
value={dayjs(start_date)}
|
||||
onBlur={() => setIsStartDatePickerOpen(false)}
|
||||
onOk={(newStartDate: dayjs.Dayjs) => {
|
||||
changeExperimentStartDate(newStartDate.toISOString())
|
||||
}}
|
||||
autoFocus={true}
|
||||
disabledDate={(dateMarker) => {
|
||||
return dateMarker.toDate() > new Date()
|
||||
}}
|
||||
allowClear={false}
|
||||
data-attr="experiment-start-date-picker"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<TZLabel time={start_date} />
|
||||
<LemonButton
|
||||
title="Move start date"
|
||||
data-attr="move-experiment-start-date"
|
||||
icon={<IconPencil />}
|
||||
size="small"
|
||||
onClick={() => setIsStartDatePickerOpen(true)}
|
||||
noPadding
|
||||
className="ml-2"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{end_date && (
|
||||
<div className="block" data-attr="experiment-end-date">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide">End date</div>
|
||||
{/* Flex class here is for the end date to have same appearance as the start date. */}
|
||||
<div className="flex">
|
||||
<TZLabel time={end_date} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { Link, ProfilePicture, Tooltip } from '@posthog/lemon-ui'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { CopyToClipboardInline } from 'lib/components/CopyToClipboard'
|
||||
import { EditableField } from 'lib/components/EditableField/EditableField'
|
||||
import { TZLabel } from 'lib/components/TZLabel'
|
||||
import { IconOpenInNew } from 'lib/lemon-ui/icons'
|
||||
import { urls } from 'scenes/urls'
|
||||
|
||||
@@ -15,12 +14,13 @@ import { StatusTag } from '../Experiment'
|
||||
import { experimentLogic } from '../experimentLogic'
|
||||
import { getExperimentStatus } from '../experimentsLogic'
|
||||
import { ActionBanner, ResultsTag } from './components'
|
||||
import { ExperimentDates } from './ExperimentDates'
|
||||
|
||||
export function Info(): JSX.Element {
|
||||
const { experiment } = useValues(experimentLogic)
|
||||
const { updateExperiment } = useActions(experimentLogic)
|
||||
|
||||
const { created_by, created_at } = experiment
|
||||
const { created_by } = experiment
|
||||
|
||||
if (!experiment.feature_flag) {
|
||||
return <></>
|
||||
@@ -30,7 +30,7 @@ export function Info(): JSX.Element {
|
||||
<div>
|
||||
<div className="flex">
|
||||
<div className="w-1/2 inline-flex space-x-8">
|
||||
<div className="block">
|
||||
<div className="block" data-attr="experiment-status">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide">Status</div>
|
||||
<StatusTag experiment={experiment} />
|
||||
</div>
|
||||
@@ -76,10 +76,7 @@ export function Info(): JSX.Element {
|
||||
|
||||
<div className="w-1/2 flex flex-col justify-end">
|
||||
<div className="ml-auto inline-flex space-x-8">
|
||||
<div className="block">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide">Created at</div>
|
||||
{created_at && <TZLabel time={created_at} />}
|
||||
</div>
|
||||
<ExperimentDates />
|
||||
<div className="block">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide">Created by</div>
|
||||
{created_by && <ProfilePicture user={created_by} size="md" showName />}
|
||||
|
||||
@@ -296,7 +296,11 @@ export function PageHeaderCustom(): JSX.Element {
|
||||
<LemonButton type="secondary" className="mr-2" onClick={() => setEditExperiment(true)}>
|
||||
Edit
|
||||
</LemonButton>
|
||||
<LemonButton type="primary" onClick={() => launchExperiment()}>
|
||||
<LemonButton
|
||||
type="primary"
|
||||
data-attr="launch-experiment"
|
||||
onClick={() => launchExperiment()}
|
||||
>
|
||||
Launch
|
||||
</LemonButton>
|
||||
</div>
|
||||
@@ -337,7 +341,12 @@ export function PageHeaderCustom(): JSX.Element {
|
||||
</>
|
||||
<ResetButton experiment={experiment} onConfirm={resetRunningExperiment} />
|
||||
{!experiment.end_date && (
|
||||
<LemonButton type="secondary" status="danger" onClick={() => endExperiment()}>
|
||||
<LemonButton
|
||||
type="secondary"
|
||||
data-attr="stop-experiment"
|
||||
status="danger"
|
||||
onClick={() => endExperiment()}
|
||||
>
|
||||
Stop
|
||||
</LemonButton>
|
||||
)}
|
||||
|
||||
@@ -141,6 +141,7 @@ export const experimentLogic = kea<experimentLogicType>([
|
||||
updateExperimentGoal: (filters: Partial<FilterType>) => ({ filters }),
|
||||
updateExperimentExposure: (filters: Partial<FilterType> | null) => ({ filters }),
|
||||
updateExperimentSecondaryMetrics: (metrics: SecondaryExperimentMetric[]) => ({ metrics }),
|
||||
changeExperimentStartDate: (startDate: string) => ({ startDate }),
|
||||
launchExperiment: true,
|
||||
endExperiment: true,
|
||||
addExperimentGroup: true,
|
||||
@@ -238,6 +239,7 @@ export const experimentLogic = kea<experimentLogicType>([
|
||||
{
|
||||
updateExperimentGoal: () => true,
|
||||
updateExperimentExposure: () => true,
|
||||
changeExperimentStartDate: () => true,
|
||||
loadExperimentResults: () => false,
|
||||
},
|
||||
],
|
||||
@@ -446,6 +448,10 @@ export const experimentLogic = kea<experimentLogicType>([
|
||||
actions.updateExperiment({ start_date: startDate.toISOString() })
|
||||
values.experiment && eventUsageLogic.actions.reportExperimentLaunched(values.experiment, startDate)
|
||||
},
|
||||
changeExperimentStartDate: async ({ startDate }) => {
|
||||
actions.updateExperiment({ start_date: startDate })
|
||||
values.experiment && eventUsageLogic.actions.reportExperimentStartDateChange(values.experiment, startDate)
|
||||
},
|
||||
endExperiment: async () => {
|
||||
const endDate = dayjs()
|
||||
actions.updateExperiment({ end_date: endDate.toISOString() })
|
||||
|
||||
Reference in New Issue
Block a user