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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.4] - 2021-05-05

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

## [0.1.3] - 2021-04-20

### Fixed
Expand Down
8 changes: 4 additions & 4 deletions README.md
Expand Up @@ -17,7 +17,7 @@ $ npm install -g @vtex/cli-plugin-abtest
$ oclif-example COMMAND
running command...
$ oclif-example (-v|--version|version)
@vtex/cli-plugin-abtest/0.1.3 linux-x64 node-v12.22.1
@vtex/cli-plugin-abtest/0.1.4 linux-x64 node-v12.22.1
$ oclif-example --help [COMMAND]
USAGE
$ oclif-example COMMAND
Expand All @@ -44,7 +44,7 @@ OPTIONS
--trace Ensure all requests to VTEX IO are traced
```

_See code: [build/commands/workspace/abtest/finish.ts](https://github.com/vtex/cli-plugin-abtest/blob/v0.1.3/build/commands/workspace/abtest/finish.ts)_
_See code: [build/commands/workspace/abtest/finish.ts](https://github.com/vtex/cli-plugin-abtest/blob/v0.1.4/build/commands/workspace/abtest/finish.ts)_

## `oclif-example workspace:abtest:start`

Expand All @@ -60,7 +60,7 @@ OPTIONS
--trace Ensure all requests to VTEX IO are traced
```

_See code: [build/commands/workspace/abtest/start.ts](https://github.com/vtex/cli-plugin-abtest/blob/v0.1.3/build/commands/workspace/abtest/start.ts)_
_See code: [build/commands/workspace/abtest/start.ts](https://github.com/vtex/cli-plugin-abtest/blob/v0.1.4/build/commands/workspace/abtest/start.ts)_

## `oclif-example workspace:abtest:status`

Expand All @@ -76,5 +76,5 @@ OPTIONS
--trace Ensure all requests to VTEX IO are traced
```

_See code: [build/commands/workspace/abtest/status.ts](https://github.com/vtex/cli-plugin-abtest/blob/v0.1.3/build/commands/workspace/abtest/status.ts)_
_See code: [build/commands/workspace/abtest/status.ts](https://github.com/vtex/cli-plugin-abtest/blob/v0.1.4/build/commands/workspace/abtest/status.ts)_
<!-- commandsstop -->
2 changes: 1 addition & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "@vtex/cli-plugin-abtest",
"description": "vtex plugin abtest",
"version": "0.1.3",
"version": "0.1.4",
"bugs": "https://github.com/vtex/cli-plugin-abtest/issues",
"dependencies": {
"@oclif/command": "^1",
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