Merge pull request #4113 from overleaf/ta-post-registration-analytics

Add Post Registration Analytics Job

GitOrigin-RevId: f0d83eeea2e32915782e916cb40a768d5c1b6116
This commit is contained in:
Alexandre Bourdin 2021-06-10 10:13:06 +02:00 committed by Copybot
parent ca1e828ea7
commit 5af039eef0
5 changed files with 195 additions and 2 deletions

View file

@ -8,6 +8,7 @@ const UserGetter = require('./UserGetter')
const UserUpdater = require('./UserUpdater') const UserUpdater = require('./UserUpdater')
const Analytics = require('../Analytics/AnalyticsManager') const Analytics = require('../Analytics/AnalyticsManager')
const UserOnboardingEmailQueueManager = require('./UserOnboardingEmailManager') const UserOnboardingEmailQueueManager = require('./UserOnboardingEmailManager')
const UserPostRegistrationAnalyticsManager = require('./UserPostRegistrationAnalyticsManager')
const OError = require('@overleaf/o-error') const OError = require('@overleaf/o-error')
async function _addAffiliation(user, affiliationOptions) { async function _addAffiliation(user, affiliationOptions) {
@ -89,6 +90,9 @@ async function createNewUser(attributes, options = {}) {
if (Features.hasFeature('saas')) { if (Features.hasFeature('saas')) {
try { try {
await UserOnboardingEmailQueueManager.scheduleOnboardingEmail(user) await UserOnboardingEmailQueueManager.scheduleOnboardingEmail(user)
await UserPostRegistrationAnalyticsManager.schedulePostRegistrationAnalytics(
user
)
} catch (error) { } catch (error) {
logger.error( logger.error(
OError.tag(error, 'Failed to schedule sending of onboarding email', { OError.tag(error, 'Failed to schedule sending of onboarding email', {

View file

@ -0,0 +1,50 @@
const Queues = require('../../infrastructure/Queues')
const UserGetter = require('./UserGetter')
const {
promises: InstitutionsAPIPromises,
} = require('../Institutions/InstitutionsAPI')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const ONE_DAY_MS = 24 * 60 * 60 * 1000
class UserPostRegistrationAnalyticsManager {
constructor() {
this.queue = Queues.getPostRegistrationAnalyticsQueue()
this.queue.process(async job => {
const { userId } = job.data
await postRegistrationAnalytics(userId)
})
}
async schedulePostRegistrationAnalytics(user) {
await this.queue.add({ userId: user._id }, { delay: ONE_DAY_MS })
}
}
async function postRegistrationAnalytics(userId) {
const user = await UserGetter.promises.getUser({ _id: userId }, { email: 1 })
if (!user) {
return
}
await checkAffiliations(userId)
}
async function checkAffiliations(userId) {
const affiliationsData = await InstitutionsAPIPromises.getUserAffiliations(
userId
)
const hasCommonsAccountAffiliation = affiliationsData.some(
affiliationData =>
affiliationData.institution && affiliationData.institution.commonsAccount
)
if (hasCommonsAccountAffiliation) {
await AnalyticsManager.setUserProperty(
userId,
'registered-from-commons-account',
true
)
}
}
module.exports = new UserPostRegistrationAnalyticsManager()

View file

@ -31,6 +31,10 @@ function getOnboardingEmailsQueue() {
return getOrCreateQueue('emails-onboarding') return getOrCreateQueue('emails-onboarding')
} }
function getPostRegistrationAnalyticsQueue() {
return getOrCreateQueue('post-registration-analytics')
}
function getOrCreateQueue(queueName, defaultJobOptions) { function getOrCreateQueue(queueName, defaultJobOptions) {
if (!queues[queueName]) { if (!queues[queueName]) {
queues[queueName] = new Queue(queueName, { queues[queueName] = new Queue(queueName, {
@ -54,4 +58,5 @@ module.exports = {
getAnalyticsEditingSessionsQueue, getAnalyticsEditingSessionsQueue,
getAnalyticsUserPropertiesQueue, getAnalyticsUserPropertiesQueue,
getOnboardingEmailsQueue, getOnboardingEmailsQueue,
getPostRegistrationAnalyticsQueue,
} }

View file

@ -48,6 +48,9 @@ describe('UserCreator', function () {
'./UserOnboardingEmailManager': (this.UserOnboardingEmailManager = { './UserOnboardingEmailManager': (this.UserOnboardingEmailManager = {
scheduleOnboardingEmail: sinon.stub(), scheduleOnboardingEmail: sinon.stub(),
}), }),
'./UserPostRegistrationAnalyticsManager': (this.UserPostRegistrationAnalyticsManager = {
schedulePostRegistrationAnalytics: sinon.stub(),
}),
}, },
}) })
@ -279,7 +282,7 @@ describe('UserCreator', function () {
) )
}) })
it('should schedule an onboarding email on registration with saas feature', async function () { it('should schedule post registration jobs on registration with saas feature', async function () {
this.Features.hasFeature = sinon.stub().withArgs('saas').returns(true) this.Features.hasFeature = sinon.stub().withArgs('saas').returns(true)
const user = await this.UserCreator.promises.createNewUser({ const user = await this.UserCreator.promises.createNewUser({
email: this.email, email: this.email,
@ -289,14 +292,23 @@ describe('UserCreator', function () {
this.UserOnboardingEmailManager.scheduleOnboardingEmail, this.UserOnboardingEmailManager.scheduleOnboardingEmail,
user user
) )
sinon.assert.calledWith(
this.UserPostRegistrationAnalyticsManager
.schedulePostRegistrationAnalytics,
user
)
}) })
it('should not add schedule onboarding email when without saas feature', async function () { it('should not schedule post registration checks when without saas feature', async function () {
const attributes = { email: this.email } const attributes = { email: this.email }
await this.UserCreator.promises.createNewUser(attributes) await this.UserCreator.promises.createNewUser(attributes)
sinon.assert.notCalled( sinon.assert.notCalled(
this.UserOnboardingEmailManager.scheduleOnboardingEmail this.UserOnboardingEmailManager.scheduleOnboardingEmail
) )
sinon.assert.notCalled(
this.UserPostRegistrationAnalyticsManager
.schedulePostRegistrationAnalytics
)
}) })
}) })
}) })

View file

@ -0,0 +1,122 @@
const SandboxedModule = require('sandboxed-module')
const path = require('path')
const sinon = require('sinon')
const MODULE_PATH = path.join(
__dirname,
'../../../../app/src/Features/User/UserPostRegistrationAnalyticsManager'
)
describe('UserPostRegistrationAnalyticsManager', function () {
beforeEach(function () {
this.fakeUserId = '123abc'
this.postRegistrationAnalyticsQueue = {
add: sinon.stub().resolves(),
process: callback => {
this.queueProcessFunction = callback
},
}
const self = this
this.Queues = {
getPostRegistrationAnalyticsQueue: () => {
return self.postRegistrationAnalyticsQueue
},
}
this.UserGetter = {
promises: {
getUser: sinon.stub().resolves({ _id: this.fakeUserId }),
},
}
this.InstitutionsAPI = {
promises: {
getUserAffiliations: sinon.stub().resolves([]),
},
}
this.AnalyticsManager = {
setUserProperty: sinon.stub().resolves(),
}
this.UserPostRegistrationAnalyticsManager = SandboxedModule.require(
MODULE_PATH,
{
globals: {
console: console,
},
requires: {
'../../infrastructure/Queues': this.Queues,
'./UserGetter': this.UserGetter,
'../Institutions/InstitutionsAPI': this.InstitutionsAPI,
'../Analytics/AnalyticsManager': this.AnalyticsManager,
},
}
)
})
describe('schedule jobs', function () {
it('should schedule delayed job on queue', function () {
this.UserPostRegistrationAnalyticsManager.schedulePostRegistrationAnalytics(
{
_id: this.fakeUserId,
}
)
sinon.assert.calledWithMatch(
this.postRegistrationAnalyticsQueue.add,
{ userId: this.fakeUserId },
{ delay: 24 * 60 * 60 * 1000 }
)
})
})
describe('process jobs', function () {
it('stops without errors if user is not found', async function () {
this.UserGetter.promises.getUser.resolves(null)
await this.queueProcessFunction({ data: { userId: this.fakeUserId } })
sinon.assert.calledWith(this.UserGetter.promises.getUser, {
_id: this.fakeUserId,
})
sinon.assert.notCalled(this.InstitutionsAPI.promises.getUserAffiliations)
sinon.assert.notCalled(this.AnalyticsManager.setUserProperty)
})
it('sets user property if user has commons account affiliationd', async function () {
this.InstitutionsAPI.promises.getUserAffiliations.resolves([
{},
{
institution: {
commonsAccount: true,
},
},
{
institution: {
commonsAccount: false,
},
},
])
await this.queueProcessFunction({ data: { userId: this.fakeUserId } })
sinon.assert.calledWith(this.UserGetter.promises.getUser, {
_id: this.fakeUserId,
})
sinon.assert.calledWith(
this.InstitutionsAPI.promises.getUserAffiliations,
this.fakeUserId
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty,
this.fakeUserId,
'registered-from-commons-account',
true
)
})
it('does not set user property if user has no commons account affiliation', async function () {
this.InstitutionsAPI.promises.getUserAffiliations.resolves([
{
institution: {
commonsAccount: false,
},
},
])
await this.queueProcessFunction({ data: { userId: this.fakeUserId } })
sinon.assert.notCalled(this.AnalyticsManager.setUserProperty)
})
})
})