@guardian/ab-core
Advanced tools
Comparing version 0.0.0-canary-20240429155350 to 0.0.0-canary-20240502162806
@@ -1,91 +0,2 @@ | ||
type CoreAPIConfig = { | ||
mvtMaxValue?: number; | ||
mvtId: number; | ||
pageIsSensitive: boolean; | ||
abTestSwitches: Record<string, boolean>; | ||
forcedTestVariants?: Participations; | ||
forcedTestException?: ABTest['id']; | ||
arrayOfTestObjects: ABTest[]; | ||
}; | ||
type CoreAPI = { | ||
allRunnableTests: (tests: readonly ABTest[]) => ReadonlyArray<Runnable<ABTest>> | []; | ||
runnableTest: (test: ABTest) => Runnable<ABTest & { | ||
variantToRun: Variant; | ||
}> | null; | ||
firstRunnableTest: (tests: readonly ABTest[]) => Runnable<ABTest> | null; | ||
isUserInVariant: (testId: ABTest['id'], variantId?: Variant['id']) => boolean; | ||
}; | ||
type OphanAPIConfig = { | ||
serverSideTests: ServerSideTests; | ||
errorReporter: ErrorReporterFunc; | ||
ophanRecord: OphanRecordFunction; | ||
}; | ||
type OphanAPI = { | ||
registerCompleteEvents: (tests: ReadonlyArray<Runnable<ABTest>>) => void; | ||
registerImpressionEvents: (tests: ReadonlyArray<Runnable<ABTest>>) => void; | ||
trackABTests: (tests: ReadonlyArray<Runnable<ABTest>>) => void; | ||
}; | ||
type ABTestAPI = CoreAPI & OphanAPI; | ||
type AbTestConfig = CoreAPIConfig & OphanAPIConfig; | ||
interface OphanABEvent { | ||
variantName: string; | ||
complete: string | boolean; | ||
campaignCodes?: readonly string[]; | ||
} | ||
type OphanABPayload = Record<string, OphanABEvent>; | ||
type OphanRecordFunction = (send: Record<string, OphanABPayload>) => void; | ||
type ListenerFunction = (f: () => void) => void; | ||
interface Variant { | ||
id: string; | ||
test: (x: Record<string, unknown>) => void; | ||
campaignCode?: string; | ||
canRun?: () => boolean; | ||
impression?: ListenerFunction; | ||
success?: ListenerFunction; | ||
} | ||
interface ABTest { | ||
id: string; | ||
start: string; | ||
expiry: string; | ||
author: string; | ||
description: string; | ||
audience: number; | ||
audienceOffset: number; | ||
successMeasure: string; | ||
audienceCriteria: string; | ||
showForSensitive?: boolean; | ||
idealOutcome?: string; | ||
dataLinkNames?: string; | ||
variants: readonly Variant[]; | ||
canRun: () => boolean; | ||
notInTest?: () => void; | ||
} | ||
type Participations = Record<string, { | ||
variant: string; | ||
}>; | ||
type Runnable<ABTest> = ABTest & { | ||
variantToRun: Variant; | ||
}; | ||
type ServerSideTests = Record<string, string>; | ||
/** | ||
* Anything can be throw in JS, so you might want to narrow your return type | ||
* with `error instanceof Error` | ||
*/ | ||
type ErrorReporterFunc = (error: unknown) => void; | ||
declare class AB implements ABTestAPI { | ||
private readonly _core; | ||
private readonly _ophan; | ||
constructor({ abTestSwitches, arrayOfTestObjects, errorReporter, forcedTestException, forcedTestVariants, mvtId, mvtMaxValue, ophanRecord, pageIsSensitive, serverSideTests, }: AbTestConfig); | ||
get allRunnableTests(): (tests: readonly ABTest[]) => readonly Runnable<ABTest>[] | []; | ||
get firstRunnableTest(): (tests: readonly ABTest[]) => Runnable<ABTest> | null; | ||
get runnableTest(): (test: ABTest) => Runnable<ABTest & { | ||
variantToRun: Variant; | ||
}> | null; | ||
get isUserInVariant(): (testId: string, variantId?: string | undefined) => boolean; | ||
get registerCompleteEvents(): (tests: readonly Runnable<ABTest>[]) => void; | ||
get registerImpressionEvents(): (tests: readonly Runnable<ABTest>[]) => void; | ||
get trackABTests(): (tests: readonly Runnable<ABTest>[]) => void; | ||
} | ||
export { AB, type ABTest, type ABTestAPI, type AbTestConfig, type CoreAPIConfig, type Participations, type Runnable, type Variant }; | ||
export { ABTest, ABTestAPI, AbTestConfig, CoreAPIConfig, Participations, Runnable, Variant } from './@types/index.js'; | ||
export { AB } from './ab.js'; |
@@ -1,204 +0,1 @@ | ||
const isExpired = (testExpiry) => { | ||
const currentTime = (/* @__PURE__ */ new Date()).valueOf(); | ||
const theTestExpiry = new Date(testExpiry).setHours(23, 59, 59, 59); | ||
return currentTime > theTestExpiry; | ||
}; | ||
const initCore = ({ | ||
mvtMaxValue = 1e6, | ||
mvtId, | ||
pageIsSensitive, | ||
abTestSwitches, | ||
forcedTestVariants, | ||
forcedTestException, | ||
arrayOfTestObjects = [] | ||
}) => { | ||
const variantCanBeRun = (variant) => { | ||
const isInTest = variant.id !== "notintest"; | ||
if (variant.canRun) { | ||
return variant.canRun() && isInTest; | ||
} else { | ||
return isInTest; | ||
} | ||
}; | ||
const testCanBeRun = (test) => { | ||
const expired = isExpired(test.expiry); | ||
const testShouldShowForSensitive = !!test.showForSensitive; | ||
const isTestOn = abTestSwitches[`ab${test.id}`] && !!abTestSwitches[`ab${test.id}`]; | ||
const canTestBeRun = test.canRun(); | ||
return (pageIsSensitive ? testShouldShowForSensitive : true) && !!isTestOn && !expired && canTestBeRun; | ||
}; | ||
const computeVariantFromMvtCookie = (test) => { | ||
const smallestTestId = mvtMaxValue * test.audienceOffset; | ||
const largestTestId = smallestTestId + mvtMaxValue * test.audience; | ||
if (mvtId && mvtId > smallestTestId && mvtId <= largestTestId) { | ||
return test.variants[mvtId % test.variants.length] ?? null; | ||
} | ||
return null; | ||
}; | ||
const getForcedTestVariant = (test, forcedTestVariants2) => { | ||
const testId = test.id; | ||
const getVariantFromIds = (test2, variantId) => test2.variants.find((variant) => variant.id === variantId) ?? false; | ||
const forcedTest = forcedTestVariants2?.[testId]?.variant; | ||
return forcedTest ? getVariantFromIds(test, forcedTest) : false; | ||
}; | ||
const runnableTest = (test) => { | ||
const fromCookie = computeVariantFromMvtCookie(test); | ||
const variantFromForcedTest = getForcedTestVariant( | ||
test, | ||
forcedTestVariants | ||
); | ||
const forcedOutOfTest = forcedTestException === test.id; | ||
const variantToRun = variantFromForcedTest || fromCookie; | ||
if (!forcedOutOfTest && (variantFromForcedTest || testCanBeRun(test)) && // We ignore the test's canRun if the test is forced | ||
variantToRun && (variantFromForcedTest || variantCanBeRun(variantToRun))) { | ||
return { | ||
...test, | ||
variantToRun | ||
}; | ||
} | ||
return null; | ||
}; | ||
const allRunnableTests = (tests) => tests.reduce((prev, currentValue) => { | ||
const rt = runnableTest(currentValue); | ||
return rt ? [...prev, rt] : prev; | ||
}, []); | ||
const firstRunnableTest = (tests) => tests.map((test) => runnableTest(test)).find((rt) => rt !== null) ?? null; | ||
const isUserInVariant = (testId, variantId) => allRunnableTests(arrayOfTestObjects).some( | ||
(runnableTest2) => { | ||
return runnableTest2.id === testId && runnableTest2.variantToRun.id === variantId; | ||
} | ||
); | ||
return { | ||
allRunnableTests, | ||
runnableTest, | ||
firstRunnableTest, | ||
isUserInVariant | ||
}; | ||
}; | ||
const submit = (payload, ophanRecord) => ophanRecord({ | ||
abTestRegister: payload | ||
}); | ||
const makeABEvent = (variant, complete) => { | ||
const event = { | ||
variantName: variant.id, | ||
complete | ||
}; | ||
if (variant.campaignCode) { | ||
event.campaignCodes = [variant.campaignCode]; | ||
} | ||
return event; | ||
}; | ||
const defersImpression = (test) => test.variants.every( | ||
(variant) => typeof variant.impression === "function" | ||
); | ||
const buildOphanSubmitter = (test, variant, complete, ophanRecord) => { | ||
const data = { | ||
[test.id]: makeABEvent(variant, complete) | ||
}; | ||
return () => submit(data, ophanRecord); | ||
}; | ||
const registerCompleteEvent = (complete, errorReporter, ophanRecord) => (test) => { | ||
const variant = test.variantToRun; | ||
const listener = complete ? variant.success : variant.impression; | ||
if (!listener) { | ||
return; | ||
} | ||
try { | ||
listener(buildOphanSubmitter(test, variant, complete, ophanRecord)); | ||
} catch (error) { | ||
errorReporter(error); | ||
} | ||
}; | ||
const buildOphanPayload = (tests, errorReporter, serverSideTestObj) => { | ||
try { | ||
const log = {}; | ||
const serverSideTests = Object.keys(serverSideTestObj).filter( | ||
(test) => !!serverSideTestObj[test] | ||
); | ||
tests.filter((test) => !defersImpression(test)).forEach((test) => { | ||
log[test.id] = makeABEvent(test.variantToRun, false); | ||
}); | ||
serverSideTests.forEach((test) => { | ||
const serverSideVariant = { | ||
id: "inTest", | ||
test: () => void 0 | ||
}; | ||
log[`ab${test}`] = makeABEvent(serverSideVariant, false); | ||
}); | ||
return log; | ||
} catch (error) { | ||
errorReporter(error); | ||
return {}; | ||
} | ||
}; | ||
const initOphan = ({ | ||
serverSideTests, | ||
errorReporter, | ||
ophanRecord | ||
}) => ({ | ||
registerCompleteEvents: (tests) => tests.forEach(registerCompleteEvent(true, errorReporter, ophanRecord)), | ||
registerImpressionEvents: (tests) => tests.filter(defersImpression).forEach(registerCompleteEvent(false, errorReporter, ophanRecord)), | ||
trackABTests: (tests) => submit( | ||
buildOphanPayload(tests, errorReporter, serverSideTests), | ||
ophanRecord | ||
) | ||
}); | ||
class AB { | ||
_core; | ||
_ophan; | ||
constructor({ | ||
abTestSwitches, | ||
arrayOfTestObjects, | ||
errorReporter, | ||
forcedTestException, | ||
forcedTestVariants, | ||
mvtId, | ||
mvtMaxValue, | ||
ophanRecord, | ||
pageIsSensitive, | ||
serverSideTests | ||
}) { | ||
this._core = initCore({ | ||
abTestSwitches, | ||
arrayOfTestObjects, | ||
forcedTestException, | ||
forcedTestVariants, | ||
mvtId, | ||
mvtMaxValue, | ||
pageIsSensitive | ||
}); | ||
this._ophan = initOphan({ | ||
errorReporter, | ||
ophanRecord, | ||
serverSideTests | ||
}); | ||
} | ||
// CoreAPI | ||
get allRunnableTests() { | ||
return this._core.allRunnableTests; | ||
} | ||
get firstRunnableTest() { | ||
return this._core.firstRunnableTest; | ||
} | ||
get runnableTest() { | ||
return this._core.runnableTest; | ||
} | ||
get isUserInVariant() { | ||
return this._core.isUserInVariant; | ||
} | ||
// OphanAPI | ||
get registerCompleteEvents() { | ||
return this._ophan.registerCompleteEvents; | ||
} | ||
get registerImpressionEvents() { | ||
return this._ophan.registerImpressionEvents; | ||
} | ||
get trackABTests() { | ||
return this._ophan.trackABTests; | ||
} | ||
} | ||
export { AB }; | ||
export { AB } from './ab.js'; |
{ | ||
"name": "@guardian/ab-core", | ||
"version": "0.0.0-canary-20240429155350", | ||
"version": "0.0.0-canary-20240502162806", | ||
"private": false, | ||
@@ -20,3 +20,3 @@ "description": "A client-side library for A/B & multivariate testing", | ||
"devDependencies": { | ||
"pkgroll": "2.0.2", | ||
"rollup": "^4.16.4", | ||
"tslib": "2.6.2", | ||
@@ -85,3 +85,3 @@ "typescript": "5.3.3" | ||
"scripts": { | ||
"build": "rm -rf dist && pkgroll", | ||
"build": "rm -rf dist && rollup -c", | ||
"dev": "jest --watch", | ||
@@ -88,0 +88,0 @@ "fix": "pnpm lint --fix", |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
43203
16
525
1