Skip to content

Commit

Permalink
feat(experiments): support moving experiment start date (#21362)
Browse files Browse the repository at this point in the history
Co-authored-by: Neil Kakkar <[email protected]>
  • Loading branch information
nikitaevg and neilkakkar authored Apr 18, 2024
1 parent da361c0 commit 2530eaa
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 26 deletions.
88 changes: 75 additions & 13 deletions cypress/e2e/experiments.cy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { decideResponse } from '../fixtures/api/decide'

describe('Experiments', () => {
let randomNum
let experimentName
Expand All @@ -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
Expand Down Expand Up @@ -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')
})
})
12 changes: 12 additions & 0 deletions frontend/src/lib/utils/eventUsageLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 13 additions & 4 deletions frontend/src/scenes/experiments/ExperimentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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)
Expand Down
85 changes: 85 additions & 0 deletions frontend/src/scenes/experiments/ExperimentView/ExperimentDates.tsx
Original file line number Diff line number Diff line change
@@ -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>
)}
</>
)
}
11 changes: 4 additions & 7 deletions frontend/src/scenes/experiments/ExperimentView/Info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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 <></>
Expand All @@ -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>
Expand Down Expand Up @@ -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 />}
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/scenes/experiments/ExperimentView/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
)}
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/scenes/experiments/experimentLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -238,6 +239,7 @@ export const experimentLogic = kea<experimentLogicType>([
{
updateExperimentGoal: () => true,
updateExperimentExposure: () => true,
changeExperimentStartDate: () => true,
loadExperimentResults: () => false,
},
],
Expand Down Expand Up @@ -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() })
Expand Down

0 comments on commit 2530eaa

Please sign in to comment.