Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix / prohibit the use of old ABTester versions #12

Merged
merged 9 commits into from May 5, 2021
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- [abtest] Prohibit the use of old versions of vtex.ab-tester

## [0.1.3] - 2021-04-20

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions src/modules/abtest/finish.ts
Expand Up @@ -5,7 +5,7 @@ import { map, prop, filter } from 'ramda'
import { logger, promptConfirm, SessionManager } from 'vtex'

import { default as abTestStatus } from './status'
import { abtester, installedABTester } from './utils'
import { abtester, checkABTester } from './utils'

const { account } = SessionManager.getSingleton()

Expand Down Expand Up @@ -33,7 +33,7 @@ const promptWorkspaceToFinishABTest = () =>
.then(prop('workspace'))

export default async () => {
await installedABTester()
await checkABTester()
const workspace = await promptWorkspaceToFinishABTest()
const promptAnswer = await promptContinue(workspace)

Expand Down
71 changes: 12 additions & 59 deletions src/modules/abtest/start.ts
@@ -1,45 +1,15 @@
import chalk from 'chalk'
import enquirer from 'enquirer'
import { compose, fromPairs, keys, map, mapObjIndexed, prop, values, zip } from 'ramda'
import semver from 'semver'

import { logger, promptConfirm } from 'vtex'

import {
abtester,
installedABTester,
formatDays,
checkABTester,
promptConstraintDuration,
promptProductionWorkspace,
promptProportionTrafic,
SIGNIFICANCE_LEVELS,
} from './utils'

const promptSignificanceLevel = async (): Promise<string> => {
const significanceTimePreviews = await Promise.all(
compose<any, number[], Array<Promise<number>>>(
map((value) => abtester.preview(value as number)),
values
)(SIGNIFICANCE_LEVELS)
)

const significanceTimePreviewMap = fromPairs(zip(keys(SIGNIFICANCE_LEVELS), significanceTimePreviews))

return enquirer
.prompt<{ level: string }>({
name: 'level',
message: 'Choose the significance level:',
type: 'select',
choices: values(
mapObjIndexed((value, key) => ({
message: `${key} (~ ${formatDays(value as number)})`,
value: key,
}))(significanceTimePreviewMap)
) as any,
})
.then(prop('level'))
}

const promptContinue = (workspace: string, significanceLevel?: string) => {
return significanceLevel
? promptConfirm(
Expand All @@ -56,41 +26,24 @@ ${chalk.green('master')} and ${chalk.green(workspace)}. Proceed?`,
}

export default async () => {
const abTesterManifest = await installedABTester()
await checkABTester()
const workspace = await promptProductionWorkspace('Choose production workspace to start A/B test:')

try {
const [version] = abTesterManifest.version.split('-')

if (semver.satisfies(version, '>=0.10.0')) {
logger.info(`Setting workspace ${chalk.green(workspace)} to A/B test`)
const promptAnswer = await promptContinue(workspace)

if (!promptAnswer) return
const proportion = Number(await promptProportionTrafic())
const timeLength = Number(await promptConstraintDuration())
logger.info(`Setting workspace ${chalk.green(workspace)} to A/B test`)
const promptAnswer = await promptContinue(workspace)

await abtester.customStart(workspace, timeLength, proportion)
logger.info(`Workspace ${chalk.green(String(workspace))} in A/B test`)
logger.info(`You can stop the test using ${chalk.blue('vtex workspace abtest finish')}`)
if (!promptAnswer) return
const proportion = Number(await promptProportionTrafic())
const timeLength = Number(await promptConstraintDuration())

return
}

const significanceLevel = await promptSignificanceLevel()
const promptAnswer = await promptContinue(workspace, significanceLevel)

if (!promptAnswer) return
const significanceLevelValue = SIGNIFICANCE_LEVELS[significanceLevel]

logger.info(`Setting workspace ${chalk.green(workspace)} to A/B test with \
${significanceLevel} significance level`)
await abtester.startLegacy(workspace, significanceLevelValue)
logger.info(`Workspace ${chalk.green(workspace)} in A/B test`)
logger.info(`You can stop the test using ${chalk.blue('vtex workspace abtest finish')}`)
try {
await abtester.customStart(workspace, timeLength, proportion)
} catch (err) {
if (err.message === 'Workspace not found') {
console.log(`Test not initialized due to workspace ${workspace} not found by ab-tester.`)
}
}

logger.info(`Workspace ${chalk.green(String(workspace))} in A/B test`)
logger.info(`You can stop the test using ${chalk.blue('vtex workspace abtest finish')}`)
}
4 changes: 2 additions & 2 deletions src/modules/abtest/status.ts
Expand Up @@ -4,7 +4,7 @@ import numbro from 'numbro'
import R from 'ramda'
import { SessionManager, logger, createTable } from 'vtex'

import { abtester, formatDuration, installedABTester } from './utils'
import { abtester, formatDuration, checkABTester } from './utils'

interface ABTestStatus {
ABTestBeginning: string
Expand Down Expand Up @@ -119,7 +119,7 @@ const printResultsTable = (testInfo: ABTestStatus) => {
export default async () => {
const { account } = SessionManager.getSingleton()

await installedABTester()
await checkABTester()
let abTestInfo = []

abTestInfo = await abtester.status()
Expand Down
40 changes: 21 additions & 19 deletions src/modules/abtest/utils.ts
@@ -1,19 +1,15 @@
import { AppManifest } from '@vtex/api'
import chalk from 'chalk'
import enquirer from 'enquirer'
import numbro from 'numbro'
import { compose, filter, map, prop } from 'ramda'
import * as env from 'vtex'
import { createFlowIssueError, createAppsClient, createWorkspacesClient, SessionManager } from 'vtex'
import { createFlowIssueError, createAppsClient, createWorkspacesClient, SessionManager, COLORS } from 'vtex'
import { ABTester } from '../../clients/apps/ABTester'
import semver from 'semver'

const DEFAULT_TIMEOUT = 15000
const VERSION_THRESHOLD = '0.12.0'

export const SIGNIFICANCE_LEVELS: Record<string, number> = {
low: 0.5,
mid: 0.7,
high: 0.9,
}
const DEFAULT_TIMEOUT = 15000

const { account } = SessionManager.getSingleton()

Expand All @@ -23,16 +19,6 @@ const options = { timeout: (env.envTimeout || DEFAULT_TIMEOUT) as number }
export const abtester = ABTester.createClient({ workspace: 'master' }, { ...options, retries: 3 })
export const apps = createAppsClient({ workspace: 'master' })

export const formatDays = (days: number) => {
let suffix = 'days'

if (days === 1) {
suffix = 'day'
}

return `${numbro(days).format('0,0')} ${suffix}`
}

export const formatDuration = (durationInMinutes: number) => {
const minutes = durationInMinutes % 60
const hours = Math.trunc(durationInMinutes / 60) % 24
Expand All @@ -41,7 +27,7 @@ export const formatDuration = (durationInMinutes: number) => {
return `${days} days, ${hours} hours and ${minutes} minutes`
}

export const installedABTester = async (): Promise<AppManifest> => {
const installedABTester = async (): Promise<AppManifest> => {
try {
return await apps.getApp('vtex.ab-tester@x')
} catch (e) {
Expand All @@ -56,6 +42,22 @@ testing functionality`)
}
}

const checkABTesterVersion = (version: string) => {
const [versionNumber] = version.split('-')

if (!semver.satisfies(versionNumber, `>${VERSION_THRESHOLD}`)) {
throw createFlowIssueError(`You are using ${chalk.yellow(`vtex.ab-tester@${version}`)}, \
which is of an excessively old version. Please, use a version newer than ${chalk.green(VERSION_THRESHOLD)} \
\nTo get the latest version, run ${chalk.hex(COLORS.PINK)('vtex install vtex.ab-tester')}`)
}
}

export const checkABTester = async () => {
const abTesterManifest = await installedABTester()

checkABTesterVersion(abTesterManifest.version)
}

export const promptProductionWorkspace = async (promptMessage: string): Promise<string> => {
const workspaces = createWorkspacesClient()
const productionWorkspaces = await workspaces.list(account).then(
Expand Down