Assignment by id for recurly webhook (#6465)

* Allow split test assignment by ID for recurly webhook

* Small refactoring of assignment logic

* Add tests for getAssignmentForUser

* Cleanup following review comments

* Provide default value for sync option in split test handler

GitOrigin-RevId: 828cad3a1f3a0f3efd25f427d00a3c530ae2f087
This commit is contained in:
Alexandre Bourdin 2022-01-24 17:03:53 +01:00 committed by Copybot
parent b3d4bd397f
commit 4c49edd89b
3 changed files with 74 additions and 45 deletions

View file

@ -41,7 +41,7 @@ const DEFAULT_ASSIGNMENT = {
* @param options {Object<sync: boolean>} - for test purposes only, to force the synchronous update of the user's profile
* @returns {Promise<{variant: string, analytics: {segmentation: {splitTest: string, variant: string, phase: string, versionNumber: number}|{}}}>}
*/
async function getAssignment(req, splitTestName, options) {
async function getAssignment(req, splitTestName, { sync = false } = {}) {
const query = req.query || {}
if (query[splitTestName]) {
return {
@ -54,13 +54,30 @@ async function getAssignment(req, splitTestName, options) {
const { userId, analyticsId } = AnalyticsManager.getIdsFromSession(
req.session
)
return _getAssignment(
return _getAssignment(splitTestName, {
analyticsId,
userId,
req.session,
splitTestName,
options
)
session: req.session,
sync,
})
}
/**
* Get the assignment of a user to a split test by their user ID.
*
* Warning: this does not support query parameters override. Wherever possible, `getAssignment` should be used instead.
*
* @param userId the user ID
* @param splitTestName the unique name of the split test
* @param options {Object<sync: boolean>} - for test purposes only, to force the synchronous update of the user's profile
* @returns {Promise<{variant: string, analytics: {segmentation: {splitTest: string, variant: string, phase: string, versionNumber: number}|{}}}>}
*/
async function getAssignmentForUser(
userId,
splitTestName,
{ sync = false } = {}
) {
return _getAssignment(splitTestName, { userId, sync })
}
/**
@ -72,8 +89,13 @@ async function getAssignment(req, splitTestName, options) {
* @param options {Object<sync: boolean>} - for test purposes only, to force the synchronous update of the user's profile
* @returns {Promise<void>}
*/
async function assignInLocalsContext(req, res, splitTestName, options) {
const assignment = await getAssignment(req, splitTestName, options)
async function assignInLocalsContext(
req,
res,
splitTestName,
{ sync = false } = {}
) {
const assignment = await getAssignment(req, splitTestName, { sync })
LocalsHelper.setSplitTestVariant(
res.locals,
splitTestName,
@ -82,48 +104,49 @@ async function assignInLocalsContext(req, res, splitTestName, options) {
}
async function _getAssignment(
analyticsId,
userId,
session,
splitTestName,
options
{ analyticsId, userId, session, sync }
) {
if (!analyticsId && !userId) {
return DEFAULT_ASSIGNMENT
}
const splitTest = await splitTestCache.get(splitTestName)
if (splitTest) {
const currentVersion = splitTest.getCurrentVersion()
const currentVersion = splitTest?.getCurrentVersion()
if (!splitTest || !currentVersion?.active) {
return DEFAULT_ASSIGNMENT
}
if (session) {
const cachedVariant = _getCachedVariantFromSession(
session,
splitTest.name,
currentVersion
)
if (currentVersion.active) {
if (cachedVariant) {
return _makeAssignment(splitTest, cachedVariant, currentVersion)
}
const { activeForUser, selectedVariantName, phase, versionNumber } =
await _getAssignmentMetadata(analyticsId, userId, splitTest)
if (activeForUser) {
const assignmentConfig = {
userId,
analyticsId,
session,
splitTestName,
variantName: selectedVariantName,
phase,
versionNumber,
}
if (options && options.sync === true) {
await _updateVariantAssignment(assignmentConfig)
} else {
_updateVariantAssignment(assignmentConfig)
}
return _makeAssignment(splitTest, selectedVariantName, currentVersion)
}
if (cachedVariant) {
return _makeAssignment(splitTest, cachedVariant, currentVersion)
}
}
const { activeForUser, selectedVariantName, phase, versionNumber } =
await _getAssignmentMetadata(analyticsId, userId, splitTest)
if (activeForUser) {
const assignmentConfig = {
userId,
analyticsId,
session,
splitTestName,
variantName: selectedVariantName,
phase,
versionNumber,
}
if (sync === true) {
await _updateVariantAssignment(assignmentConfig)
} else {
_updateVariantAssignment(assignmentConfig)
}
return _makeAssignment(splitTest, selectedVariantName, currentVersion)
}
return DEFAULT_ASSIGNMENT
}
@ -147,7 +170,11 @@ async function _getAssignmentMetadata(analyticsId, userId, splitTest) {
}
}
}
const percentile = _getPercentile(analyticsId, splitTest.name, phase)
const percentile = _getPercentile(
analyticsId || userId,
splitTest.name,
phase
)
const selectedVariantName = _getVariantFromPercentile(
currentVersion.variants,
percentile
@ -210,7 +237,7 @@ async function _updateVariantAssignment({
},
})
AnalyticsManager.setUserPropertyForAnalyticsId(
analyticsId,
user.analyticsId || analyticsId || userId,
`split-test-${splitTestName}-${versionNumber}`,
variantName
)
@ -276,9 +303,11 @@ async function _getUser(id) {
module.exports = {
getAssignment: callbackify(getAssignment),
getAssignmentForUser: callbackify(getAssignmentForUser),
assignInLocalsContext: callbackify(assignInLocalsContext),
promises: {
getAssignment,
getAssignmentForUser,
assignInLocalsContext,
},
}

View file

@ -63,7 +63,7 @@ async function sendSubscriptionStartedEvent(eventData) {
// send the trial onboarding email
if (isTrial) {
const assignment = await SplitTestHandler.promises.getAssignment(
const assignment = await SplitTestHandler.promises.getAssignmentForUser(
userId,
'trial-onboarding-email'
)

View file

@ -31,7 +31,7 @@ describe('RecurlyEventHandler', function () {
}),
'../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
promises: {
getAssignment: sinon.stub().resolves({ active: false }),
getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }),
},
}),
'../Analytics/AnalyticsManager': (this.AnalyticsManager = {
@ -76,16 +76,16 @@ describe('RecurlyEventHandler', function () {
true
)
sinon.assert.calledWith(
this.SplitTestHandler.promises.getAssignment,
this.SplitTestHandler.promises.getAssignmentForUser,
this.userId,
'trial-onboarding-email'
)
})
it('sends free trial onboarding email if user in ab group', async function () {
this.SplitTestHandler.promises.getAssignment = sinon
this.SplitTestHandler.promises.getAssignmentForUser = sinon
.stub()
.resolves({ active: true, variant: 'send-email' })
.resolves({ variant: 'send-email' })
this.userId = '123456789trial'
this.eventData.account.account_code = this.userId
@ -94,7 +94,7 @@ describe('RecurlyEventHandler', function () {
await this.RecurlyEventHandler.sendSubscriptionStartedEvent(this.eventData)
sinon.assert.calledWith(
this.SplitTestHandler.promises.getAssignment,
this.SplitTestHandler.promises.getAssignmentForUser,
this.userId,
'trial-onboarding-email'
)