Merge pull request #5051 from overleaf/ab-web-mono-analytics-id

Analytics ID Support (v2)

GitOrigin-RevId: 707f62697f6566d8aad22e424684d97f7bc147df
This commit is contained in:
Alexandre Bourdin 2021-09-10 10:30:01 +02:00 committed by Copybot
parent 3f4295b070
commit 3577f25ba2
36 changed files with 557 additions and 249 deletions

View file

@ -30,10 +30,12 @@ module.exports = {
if (!Features.hasFeature('analytics')) { if (!Features.hasFeature('analytics')) {
return res.sendStatus(202) return res.sendStatus(202)
} }
const userId =
SessionManager.getLoggedInUserId(req.session) || req.sessionID
delete req.body._csrf delete req.body._csrf
AnalyticsManager.recordEvent(userId, req.params.event, req.body) AnalyticsManager.recordEventForSession(
req.session,
req.params.event,
req.body
)
res.sendStatus(202) res.sendStatus(202)
}, },
} }

View file

@ -1,21 +1,32 @@
const SessionManager = require('../Authentication/SessionManager')
const UserAnalyticsIdCache = require('./UserAnalyticsIdCache')
const Settings = require('@overleaf/settings') const Settings = require('@overleaf/settings')
const Metrics = require('../../infrastructure/Metrics') const Metrics = require('../../infrastructure/Metrics')
const Queues = require('../../infrastructure/Queues') const Queues = require('../../infrastructure/Queues')
const uuid = require('uuid')
const _ = require('lodash')
const { expressify } = require('../../util/promises')
const analyticsEventsQueue = Queues.getAnalyticsEventsQueue() const analyticsEventsQueue = Queues.getAnalyticsEventsQueue()
const analyticsEditingSessionsQueue = Queues.getAnalyticsEditingSessionsQueue() const analyticsEditingSessionsQueue = Queues.getAnalyticsEditingSessionsQueue()
const analyticsUserPropertiesQueue = Queues.getAnalyticsUserPropertiesQueue() const analyticsUserPropertiesQueue = Queues.getAnalyticsUserPropertiesQueue()
function identifyUser(userId, oldUserId) { const ONE_MINUTE_MS = 60 * 1000
if (!userId || !oldUserId) {
function identifyUser(userId, analyticsId, isNewUser) {
if (!userId || !analyticsId) {
return return
} }
if (isAnalyticsDisabled() || isSmokeTestUser(userId)) { if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
return return
} }
Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'identify' }) Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'identify' })
analyticsEventsQueue analyticsEventsQueue
.add('identify', { userId, oldUserId }) .add(
'identify',
{ userId, analyticsId, isNewUser },
{ delay: ONE_MINUTE_MS }
)
.then(() => { .then(() => {
Metrics.analyticsQueue.inc({ status: 'added', event_type: 'identify' }) Metrics.analyticsQueue.inc({ status: 'added', event_type: 'identify' })
}) })
@ -24,29 +35,81 @@ function identifyUser(userId, oldUserId) {
}) })
} }
function recordEvent(userId, event, segmentation) { async function recordEventForUser(userId, event, segmentation) {
if (!userId) { if (!userId) {
return return
} }
if (isAnalyticsDisabled() || isSmokeTestUser(userId)) { if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
return return
} }
Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'event' }) const analyticsId = await UserAnalyticsIdCache.get(userId)
analyticsEventsQueue if (analyticsId) {
.add('event', { userId, event, segmentation }) _recordEvent({ analyticsId, userId, event, segmentation, isLoggedIn: true })
.then(() => { }
Metrics.analyticsQueue.inc({ status: 'added', event_type: 'event' }) }
})
.catch(() => { function recordEventForSession(session, event, segmentation) {
Metrics.analyticsQueue.inc({ status: 'error', event_type: 'event' }) const { analyticsId, userId } = getIdsFromSession(session)
if (!analyticsId) {
return
}
if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
return
}
_recordEvent({
analyticsId,
userId,
event,
segmentation,
isLoggedIn: !!userId,
}) })
} }
async function setUserPropertyForUser(userId, propertyName, propertyValue) {
if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
return
}
_checkPropertyValue(propertyValue)
const analyticsId = await UserAnalyticsIdCache.get(userId)
if (analyticsId) {
_setUserProperty({ analyticsId, propertyName, propertyValue })
}
}
async function setUserPropertyForAnalyticsId(
analyticsId,
propertyName,
propertyValue
) {
if (_isAnalyticsDisabled()) {
return
}
_checkPropertyValue(propertyValue)
_setUserProperty({ analyticsId, propertyName, propertyValue })
}
async function setUserPropertyForSession(session, propertyName, propertyValue) {
const { analyticsId, userId } = getIdsFromSession(session)
if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
return
}
_checkPropertyValue(propertyValue)
if (analyticsId) {
_setUserProperty({ analyticsId, propertyName, propertyValue })
}
}
function updateEditingSession(userId, projectId, countryCode) { function updateEditingSession(userId, projectId, countryCode) {
if (!userId) { if (!userId) {
return return
} }
if (isAnalyticsDisabled() || isSmokeTestUser(userId)) { if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
return return
} }
Metrics.analyticsQueue.inc({ Metrics.analyticsQueue.inc({
@ -69,26 +132,36 @@ function updateEditingSession(userId, projectId, countryCode) {
}) })
} }
function setUserProperty(userId, propertyName, propertyValue) { function _recordEvent(
if (!userId) { { analyticsId, userId, event, segmentation, isLoggedIn },
return { delay } = {}
} ) {
if (isAnalyticsDisabled() || isSmokeTestUser(userId)) { Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'event' })
return analyticsEventsQueue
} .add(
'event',
if (propertyValue === undefined) { { analyticsId, userId, event, segmentation, isLoggedIn },
throw new Error( { delay }
'propertyValue cannot be undefined, use null to unset a property'
) )
} .then(() => {
Metrics.analyticsQueue.inc({ status: 'added', event_type: 'event' })
})
.catch(() => {
Metrics.analyticsQueue.inc({ status: 'error', event_type: 'event' })
})
}
function _setUserProperty({ analyticsId, propertyName, propertyValue }) {
Metrics.analyticsQueue.inc({ Metrics.analyticsQueue.inc({
status: 'adding', status: 'adding',
event_type: 'user-property', event_type: 'user-property',
}) })
analyticsUserPropertiesQueue analyticsUserPropertiesQueue
.add({ userId, propertyName, propertyValue }) .add({
analyticsId,
propertyName,
propertyValue,
})
.then(() => { .then(() => {
Metrics.analyticsQueue.inc({ Metrics.analyticsQueue.inc({
status: 'added', status: 'added',
@ -103,18 +176,54 @@ function setUserProperty(userId, propertyName, propertyValue) {
}) })
} }
function isSmokeTestUser(userId) { function _isSmokeTestUser(userId) {
const smokeTestUserId = Settings.smokeTest && Settings.smokeTest.userId const smokeTestUserId = Settings.smokeTest && Settings.smokeTest.userId
return smokeTestUserId != null && userId.toString() === smokeTestUserId return smokeTestUserId != null && userId.toString() === smokeTestUserId
} }
function isAnalyticsDisabled() { function _isAnalyticsDisabled() {
return !(Settings.analytics && Settings.analytics.enabled) return !(Settings.analytics && Settings.analytics.enabled)
} }
function _checkPropertyValue(propertyValue) {
if (propertyValue === undefined) {
throw new Error(
'propertyValue cannot be undefined, use null to unset a property'
)
}
}
function getIdsFromSession(session) {
const analyticsId = _.get(session, ['analyticsId'])
const userId = SessionManager.getLoggedInUserId(session)
return { analyticsId, userId }
}
async function analyticsIdMiddleware(req, res, next) {
const session = req.session
const sessionUser = SessionManager.getSessionUser(session)
if (session.analyticsId) {
if (sessionUser && session.analyticsId !== sessionUser.analyticsId) {
session.analyticsId = sessionUser.analyticsId
}
} else {
if (sessionUser) {
session.analyticsId = sessionUser.analyticsId
} else {
session.analyticsId = uuid.v4()
}
}
next()
}
module.exports = { module.exports = {
identifyUser, identifyUser,
recordEvent, recordEventForSession,
recordEventForUser,
setUserPropertyForUser,
setUserPropertyForSession,
setUserPropertyForAnalyticsId,
updateEditingSession, updateEditingSession,
setUserProperty, getIdsFromSession,
analyticsIdMiddleware: expressify(analyticsIdMiddleware),
} }

View file

@ -79,7 +79,7 @@ function addUserProperties(userId, session) {
} }
if (session.referal_id) { if (session.referal_id) {
AnalyticsManager.setUserProperty( AnalyticsManager.setUserPropertyForUser(
userId, userId,
`registered-from-bonus-scheme`, `registered-from-bonus-scheme`,
true true
@ -87,7 +87,7 @@ function addUserProperties(userId, session) {
} }
if (session.required_login_for) { if (session.required_login_for) {
AnalyticsManager.setUserProperty( AnalyticsManager.setUserPropertyForUser(
userId, userId,
`registered-from-${session.required_login_for}`, `registered-from-${session.required_login_for}`,
true true
@ -96,7 +96,7 @@ function addUserProperties(userId, session) {
if (session.inbound) { if (session.inbound) {
if (session.inbound.referrer) { if (session.inbound.referrer) {
AnalyticsManager.setUserProperty( AnalyticsManager.setUserPropertyForUser(
userId, userId,
`registered-from-referrer-${session.inbound.referrer.medium}`, `registered-from-referrer-${session.inbound.referrer.medium}`,
session.inbound.referrer.detail || 'other' session.inbound.referrer.detail || 'other'
@ -106,7 +106,7 @@ function addUserProperties(userId, session) {
if (session.inbound.utm) { if (session.inbound.utm) {
for (const utmKey of UTM_KEYS) { for (const utmKey of UTM_KEYS) {
if (session.inbound.utm[utmKey]) { if (session.inbound.utm[utmKey]) {
AnalyticsManager.setUserProperty( AnalyticsManager.setUserPropertyForUser(
userId, userId,
`registered-from-${utmKey.replace('_', '-')}`, `registered-from-${utmKey.replace('_', '-')}`,
session.inbound.utm[utmKey] session.inbound.utm[utmKey]

View file

@ -0,0 +1,26 @@
const UserGetter = require('../User/UserGetter')
const { CacheLoader } = require('cache-flow')
class UserAnalyticsIdCache extends CacheLoader {
constructor() {
super('user-analytics-id', {
expirationTime: 60,
maxSize: 10000,
})
}
async load(userId) {
const user = await UserGetter.promises.getUser(userId, { analyticsId: 1 })
if (user) {
return user.analyticsId || user._id
}
}
keyToString(userId) {
if (userId) {
return userId.toString()
}
}
}
module.exports = new UserAnalyticsIdCache()

View file

@ -48,6 +48,7 @@ const AuthenticationController = {
ip_address: user._login_req_ip, ip_address: user._login_req_ip,
must_reconfirm: user.must_reconfirm, must_reconfirm: user.must_reconfirm,
v1_id: user.overleaf != null ? user.overleaf.id : undefined, v1_id: user.overleaf != null ? user.overleaf.id : undefined,
analyticsId: user.analyticsId || user._id,
} }
callback(null, lightUser) callback(null, lightUser)
}, },
@ -82,6 +83,9 @@ const AuthenticationController = {
return res.redirect('/login') return res.redirect('/login')
} // OAuth2 'state' mismatch } // OAuth2 'state' mismatch
const anonymousAnalyticsId = req.session.analyticsId
const isNewUser = req.session.justRegistered || false
const Modules = require('../../infrastructure/Modules') const Modules = require('../../infrastructure/Modules')
Modules.hooks.fire( Modules.hooks.fire(
'preFinishLogin', 'preFinishLogin',
@ -106,7 +110,7 @@ const AuthenticationController = {
const redir = const redir =
AuthenticationController._getRedirectFromSession(req) || '/project' AuthenticationController._getRedirectFromSession(req) || '/project'
_loginAsyncHandlers(req, user) _loginAsyncHandlers(req, user, anonymousAnalyticsId, isNewUser)
const userId = user._id const userId = user._id
UserAuditLogHandler.addEntry(userId, 'login', userId, req.ip, err => { UserAuditLogHandler.addEntry(userId, 'login', userId, req.ip, err => {
if (err) { if (err) {
@ -486,7 +490,8 @@ function _afterLoginSessionSetup(req, user, callback) {
}) })
}) })
} }
function _loginAsyncHandlers(req, user) {
function _loginAsyncHandlers(req, user, anonymousAnalyticsId, isNewUser) {
UserHandler.setupLoginData(user, err => { UserHandler.setupLoginData(user, err => {
if (err != null) { if (err != null) {
logger.warn({ err }, 'error setting up login data') logger.warn({ err }, 'error setting up login data')
@ -495,12 +500,14 @@ function _loginAsyncHandlers(req, user) {
LoginRateLimiter.recordSuccessfulLogin(user.email) LoginRateLimiter.recordSuccessfulLogin(user.email)
AuthenticationController._recordSuccessfulLogin(user._id) AuthenticationController._recordSuccessfulLogin(user._id)
AuthenticationController.ipMatchCheck(req, user) AuthenticationController.ipMatchCheck(req, user)
Analytics.recordEvent(user._id, 'user-logged-in') Analytics.recordEventForUser(user._id, 'user-logged-in')
Analytics.identifyUser(user._id, req.sessionID) Analytics.identifyUser(user._id, anonymousAnalyticsId, isNewUser)
logger.log( logger.log(
{ email: user.email, user_id: user._id.toString() }, { email: user.email, user_id: user._id.toString() },
'successful log in' 'successful log in'
) )
req.session.justLoggedIn = true req.session.justLoggedIn = true
// capture the request ip for use when creating the session // capture the request ip for use when creating the session
return (user._login_req_ip = req.ip) return (user._login_req_ip = req.ip)

View file

@ -377,10 +377,14 @@ module.exports = CollaboratorsInviteController = {
'project:membership:changed', 'project:membership:changed',
{ invites: true, members: true } { invites: true, members: true }
) )
AnalyticsManager.recordEvent(currentUser._id, 'project-invite-accept', { AnalyticsManager.recordEventForUser(
currentUser._id,
'project-invite-accept',
{
projectId, projectId,
userId: currentUser._id, userId: currentUser._id,
}) }
)
if (req.xhr) { if (req.xhr) {
return res.sendStatus(204) // Done async via project page notification return res.sendStatus(204) // Done async via project page notification
} else { } else {

View file

@ -780,7 +780,7 @@ const ProjectController = {
metrics.inc(metricName) metrics.inc(metricName)
if (userId) { if (userId) {
AnalyticsManager.recordEvent(userId, 'project-opened', { AnalyticsManager.recordEventForUser(userId, 'project-opened', {
projectId: project._id, projectId: project._id,
}) })
} }

View file

@ -35,12 +35,12 @@ async function createBlankProject(ownerId, projectName, attributes = {}) {
const isImport = attributes && attributes.overleaf const isImport = attributes && attributes.overleaf
const project = await _createBlankProject(ownerId, projectName, attributes) const project = await _createBlankProject(ownerId, projectName, attributes)
if (isImport) { if (isImport) {
AnalyticsManager.recordEvent(ownerId, 'project-imported', { AnalyticsManager.recordEventForUser(ownerId, 'project-imported', {
projectId: project._id, projectId: project._id,
attributes, attributes,
}) })
} else { } else {
AnalyticsManager.recordEvent(ownerId, 'project-created', { AnalyticsManager.recordEventForUser(ownerId, 'project-created', {
projectId: project._id, projectId: project._id,
attributes, attributes,
}) })
@ -50,7 +50,7 @@ async function createBlankProject(ownerId, projectName, attributes = {}) {
async function createProjectFromSnippet(ownerId, projectName, docLines) { async function createProjectFromSnippet(ownerId, projectName, docLines) {
const project = await _createBlankProject(ownerId, projectName) const project = await _createBlankProject(ownerId, projectName)
AnalyticsManager.recordEvent(ownerId, 'project-created', { AnalyticsManager.recordEventForUser(ownerId, 'project-created', {
projectId: project._id, projectId: project._id,
}) })
await _createRootDoc(project, ownerId, docLines) await _createRootDoc(project, ownerId, docLines)
@ -63,7 +63,7 @@ async function createBasicProject(ownerId, projectName) {
const docLines = await _buildTemplate('mainbasic.tex', ownerId, projectName) const docLines = await _buildTemplate('mainbasic.tex', ownerId, projectName)
await _createRootDoc(project, ownerId, docLines) await _createRootDoc(project, ownerId, docLines)
AnalyticsManager.recordEvent(ownerId, 'project-created', { AnalyticsManager.recordEventForUser(ownerId, 'project-created', {
projectId: project._id, projectId: project._id,
}) })
@ -75,7 +75,7 @@ async function createExampleProject(ownerId, projectName) {
await _addExampleProjectFiles(ownerId, projectName, project) await _addExampleProjectFiles(ownerId, projectName, project)
AnalyticsManager.recordEvent(ownerId, 'project-created', { AnalyticsManager.recordEventForUser(ownerId, 'project-created', {
projectId: project._id, projectId: project._id,
}) })

View file

@ -96,7 +96,7 @@ async function assignUserToVariant(userId, splitTest) {
[`splitTests.${splitTest.id}`]: selectedVariant, [`splitTests.${splitTest.id}`]: selectedVariant,
}, },
}) })
AnalyticsManager.setUserProperty( AnalyticsManager.setUserPropertyForUser(
userId, userId,
`split-test-${splitTest.id}`, `split-test-${splitTest.id}`,
selectedVariant selectedVariant

View file

@ -1,6 +1,7 @@
const UserGetter = require('../User/UserGetter') const UserGetter = require('../User/UserGetter')
const UserUpdater = require('../User/UserUpdater') const UserUpdater = require('../User/UserUpdater')
const AnalyticsManager = require('../Analytics/AnalyticsManager') const AnalyticsManager = require('../Analytics/AnalyticsManager')
const UserAnalyticsIdCache = require('../Analytics/UserAnalyticsIdCache')
const crypto = require('crypto') const crypto = require('crypto')
const _ = require('lodash') const _ = require('lodash')
const { callbackify } = require('util') const { callbackify } = require('util')
@ -24,7 +25,7 @@ const BETA_PHASE = 'beta'
* // execute the default behaviour (control group) * // execute the default behaviour (control group)
* } * }
* // then record an event * // then record an event
* AnalyticsManager.recordEvent(userId, 'example-project-created', { * AnalyticsManager.recordEventForUser(userId, 'example-project-created', {
* projectId: project._id, * projectId: project._id,
* ...assignment.analytics.segmentation * ...assignment.analytics.segmentation
* }) * })
@ -35,8 +36,50 @@ const BETA_PHASE = 'beta'
* @returns {Promise<{variant: string, analytics: {segmentation: {splitTest: string, variant: string, phase: string, versionNumber: number}|{}}}>} * @returns {Promise<{variant: string, analytics: {segmentation: {splitTest: string, variant: string, phase: string, versionNumber: number}|{}}}>}
*/ */
async function getAssignment(userId, splitTestName, options) { async function getAssignment(userId, splitTestName, options) {
const splitTest = await splitTestCache.get(splitTestName) if (!userId) {
return { variant: 'default', analytics: { segmentation: {} } }
}
const analyticsId = await UserAnalyticsIdCache.get(userId)
return _getAssignment(analyticsId, userId, undefined, splitTestName, options)
}
/**
* Get the assignment of a user to a split test by their session.
*
* @example
* // Assign user and record an event
*
* const assignment = await SplitTestV2Handler.getAssignment(req.session, 'example-project')
* if (assignment.variant === 'awesome-new-version') {
* // execute my awesome change
* }
* else {
* // execute the default behaviour (control group)
* }
* // then record an event
* AnalyticsManager.recordEventForSession(req.session, 'example-project-created', {
* projectId: project._id,
* ...assignment.analytics.segmentation
* })
*
* @param session the request session
* @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 getAssignmentForSession(session, splitTestName, options) {
const { userId, analyticsId } = AnalyticsManager.getIdsFromSession(session)
return _getAssignment(analyticsId, userId, session, splitTestName, options)
}
async function _getAssignment(
analyticsId,
userId,
session,
splitTestName,
options
) {
const splitTest = await splitTestCache.get(splitTestName)
if (splitTest) { if (splitTest) {
const currentVersion = splitTest.getCurrentVersion() const currentVersion = splitTest.getCurrentVersion()
if (currentVersion.active) { if (currentVersion.active) {
@ -45,10 +88,12 @@ async function getAssignment(userId, splitTestName, options) {
selectedVariantName, selectedVariantName,
phase, phase,
versionNumber, versionNumber,
} = await _getAssignmentMetadata(userId, splitTest) } = await _getAssignmentMetadata(analyticsId, userId, splitTest)
if (activeForUser) { if (activeForUser) {
const assignmentConfig = { const assignmentConfig = {
userId, userId,
analyticsId,
session,
splitTestName, splitTestName,
variantName: selectedVariantName, variantName: selectedVariantName,
phase, phase,
@ -89,10 +134,28 @@ async function assignInLocalsContext(res, userId, splitTestName, options) {
res.locals.splitTestVariants[splitTestName] = assignment.variant res.locals.splitTestVariants[splitTestName] = assignment.variant
} }
async function _getAssignmentMetadata(userId, splitTest) { async function assignInLocalsContextForSession(
res,
session,
splitTestName,
options
) {
const assignment = await getAssignmentForSession(
session,
splitTestName,
options
)
if (!res.locals.splitTestVariants) {
res.locals.splitTestVariants = {}
}
res.locals.splitTestVariants[splitTestName] = assignment.variant
}
async function _getAssignmentMetadata(analyticsId, userId, splitTest) {
const currentVersion = splitTest.getCurrentVersion() const currentVersion = splitTest.getCurrentVersion()
const phase = currentVersion.phase const phase = currentVersion.phase
if ([ALPHA_PHASE, BETA_PHASE].includes(phase)) { if ([ALPHA_PHASE, BETA_PHASE].includes(phase)) {
if (userId) {
const user = await _getUser(userId) const user = await _getUser(userId)
if ( if (
(phase === ALPHA_PHASE && !(user && user.alphaProgram)) || (phase === ALPHA_PHASE && !(user && user.alphaProgram)) ||
@ -102,8 +165,13 @@ async function _getAssignmentMetadata(userId, splitTest) {
activeForUser: false, activeForUser: false,
} }
} }
} else {
return {
activeForUser: false,
} }
const percentile = _getPercentile(userId, splitTest.name, phase) }
}
const percentile = _getPercentile(analyticsId, splitTest.name, phase)
const selectedVariantName = _getVariantFromPercentile( const selectedVariantName = _getVariantFromPercentile(
currentVersion.variants, currentVersion.variants,
percentile percentile
@ -116,10 +184,10 @@ async function _getAssignmentMetadata(userId, splitTest) {
} }
} }
function _getPercentile(userId, splitTestName, splitTestPhase) { function _getPercentile(analyticsId, splitTestName, splitTestPhase) {
const hash = crypto const hash = crypto
.createHash('md5') .createHash('md5')
.update(userId + splitTestName + splitTestPhase) .update(analyticsId + splitTestName + splitTestPhase)
.digest('hex') .digest('hex')
const hashPrefix = hash.substr(0, 8) const hashPrefix = hash.substr(0, 8)
return Math.floor( return Math.floor(
@ -139,11 +207,20 @@ function _getVariantFromPercentile(variants, percentile) {
async function _updateVariantAssignment({ async function _updateVariantAssignment({
userId, userId,
analyticsId,
session,
splitTestName, splitTestName,
phase, phase,
versionNumber, versionNumber,
variantName, variantName,
}) { }) {
const persistedAssignment = {
variantName,
versionNumber,
phase,
assignedAt: new Date(),
}
if (userId) {
const user = await _getUser(userId) const user = await _getUser(userId)
if (user) { if (user) {
const assignedSplitTests = user.splitTests || [] const assignedSplitTests = user.splitTests || []
@ -152,16 +229,30 @@ async function _updateVariantAssignment({
if (!existingAssignment) { if (!existingAssignment) {
await UserUpdater.promises.updateUser(userId, { await UserUpdater.promises.updateUser(userId, {
$addToSet: { $addToSet: {
[`splitTests.${splitTestName}`]: { [`splitTests.${splitTestName}`]: persistedAssignment,
variantName,
versionNumber,
phase,
assignedAt: new Date(),
},
}, },
}) })
AnalyticsManager.setUserProperty( AnalyticsManager.setUserPropertyForAnalyticsId(
userId, analyticsId,
`split-test-${splitTestName}-${versionNumber}`,
variantName
)
}
}
} else if (session) {
if (!session.splitTests) {
session.splitTests = {}
}
if (!session.splitTests[splitTestName]) {
session.splitTests[splitTestName] = []
}
const existingAssignment = _.find(session.splitTests[splitTestName], {
versionNumber,
})
if (!existingAssignment) {
session.splitTests[splitTestName].push(persistedAssignment)
AnalyticsManager.setUserPropertyForAnalyticsId(
analyticsId,
`split-test-${splitTestName}-${versionNumber}`, `split-test-${splitTestName}-${versionNumber}`,
variantName variantName
) )
@ -179,9 +270,13 @@ async function _getUser(id) {
module.exports = { module.exports = {
getAssignment: callbackify(getAssignment), getAssignment: callbackify(getAssignment),
getAssignmentForSession: callbackify(getAssignmentForSession),
assignInLocalsContext: callbackify(assignInLocalsContext), assignInLocalsContext: callbackify(assignInLocalsContext),
assignInLocalsContextForSession: callbackify(assignInLocalsContextForSession),
promises: { promises: {
getAssignment, getAssignment,
getAssignmentForSession,
assignInLocalsContext, assignInLocalsContext,
assignInLocalsContextForSession,
}, },
} }

View file

@ -28,7 +28,7 @@ const FeaturesUpdater = {
const matchedFeatureSet = FeaturesUpdater._getMatchedFeatureSet( const matchedFeatureSet = FeaturesUpdater._getMatchedFeatureSet(
features features
) )
AnalyticsManager.setUserProperty( AnalyticsManager.setUserPropertyForUser(
userId, userId,
'feature-set', 'feature-set',
matchedFeatureSet matchedFeatureSet

View file

@ -39,85 +39,133 @@ function sendRecurlyAnalyticsEvent(event, eventData) {
} }
} }
function _sendSubscriptionStartedEvent(eventData) { async function _sendSubscriptionStartedEvent(eventData) {
const userId = _getUserId(eventData) const userId = _getUserId(eventData)
const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData)
AnalyticsManager.recordEvent(userId, 'subscription-started', { AnalyticsManager.recordEventForUser(userId, 'subscription-started', {
plan_code: planCode, plan_code: planCode,
quantity, quantity,
is_trial: isTrial, is_trial: isTrial,
}) })
AnalyticsManager.setUserProperty(userId, 'subscription-plan-code', planCode) AnalyticsManager.setUserPropertyForUser(
AnalyticsManager.setUserProperty(userId, 'subscription-state', state) userId,
AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) 'subscription-plan-code',
planCode
)
AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
AnalyticsManager.setUserPropertyForUser(
userId,
'subscription-is-trial',
isTrial
)
} }
function _sendSubscriptionUpdatedEvent(eventData) { async function _sendSubscriptionUpdatedEvent(eventData) {
const userId = _getUserId(eventData) const userId = _getUserId(eventData)
const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData)
AnalyticsManager.recordEvent(userId, 'subscription-updated', { AnalyticsManager.recordEventForUser(userId, 'subscription-updated', {
plan_code: planCode, plan_code: planCode,
quantity, quantity,
}) })
AnalyticsManager.setUserProperty(userId, 'subscription-plan-code', planCode) AnalyticsManager.setUserPropertyForUser(
AnalyticsManager.setUserProperty(userId, 'subscription-state', state) userId,
AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) 'subscription-plan-code',
planCode
)
AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
AnalyticsManager.setUserPropertyForUser(
userId,
'subscription-is-trial',
isTrial
)
} }
function _sendSubscriptionCancelledEvent(eventData) { async function _sendSubscriptionCancelledEvent(eventData) {
const userId = _getUserId(eventData) const userId = _getUserId(eventData)
const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData)
AnalyticsManager.recordEvent(userId, 'subscription-cancelled', { AnalyticsManager.recordEventForUser(userId, 'subscription-cancelled', {
plan_code: planCode, plan_code: planCode,
quantity, quantity,
is_trial: isTrial, is_trial: isTrial,
}) })
AnalyticsManager.setUserProperty(userId, 'subscription-state', state) AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) AnalyticsManager.setUserPropertyForUser(
userId,
'subscription-is-trial',
isTrial
)
} }
function _sendSubscriptionExpiredEvent(eventData) { async function _sendSubscriptionExpiredEvent(eventData) {
const userId = _getUserId(eventData) const userId = _getUserId(eventData)
const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData)
AnalyticsManager.recordEvent(userId, 'subscription-expired', { AnalyticsManager.recordEventForUser(userId, 'subscription-expired', {
plan_code: planCode, plan_code: planCode,
quantity, quantity,
is_trial: isTrial, is_trial: isTrial,
}) })
AnalyticsManager.setUserProperty(userId, 'subscription-plan-code', planCode) AnalyticsManager.setUserPropertyForUser(
AnalyticsManager.setUserProperty(userId, 'subscription-state', state) userId,
AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) 'subscription-plan-code',
planCode
)
AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
AnalyticsManager.setUserPropertyForUser(
userId,
'subscription-is-trial',
isTrial
)
} }
function _sendSubscriptionRenewedEvent(eventData) { async function _sendSubscriptionRenewedEvent(eventData) {
const userId = _getUserId(eventData) const userId = _getUserId(eventData)
const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData)
AnalyticsManager.recordEvent(userId, 'subscription-renewed', { AnalyticsManager.recordEventForUser(userId, 'subscription-renewed', {
plan_code: planCode, plan_code: planCode,
quantity, quantity,
is_trial: isTrial, is_trial: isTrial,
}) })
AnalyticsManager.setUserProperty(userId, 'subscription-plan-code', planCode) AnalyticsManager.setUserPropertyForUser(
AnalyticsManager.setUserProperty(userId, 'subscription-state', state) userId,
AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) 'subscription-plan-code',
planCode
)
AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
AnalyticsManager.setUserPropertyForUser(
userId,
'subscription-is-trial',
isTrial
)
} }
function _sendSubscriptionReactivatedEvent(eventData) { async function _sendSubscriptionReactivatedEvent(eventData) {
const userId = _getUserId(eventData) const userId = _getUserId(eventData)
const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData)
AnalyticsManager.recordEvent(userId, 'subscription-reactivated', { AnalyticsManager.recordEventForUser(userId, 'subscription-reactivated', {
plan_code: planCode, plan_code: planCode,
quantity, quantity,
}) })
AnalyticsManager.setUserProperty(userId, 'subscription-plan-code', planCode) AnalyticsManager.setUserPropertyForUser(
AnalyticsManager.setUserProperty(userId, 'subscription-state', state) userId,
AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) 'subscription-plan-code',
planCode
)
AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
AnalyticsManager.setUserPropertyForUser(
userId,
'subscription-is-trial',
isTrial
)
} }
function _sendInvoicePaidEvent(eventData) { async function _sendInvoicePaidEvent(eventData) {
const userId = _getUserId(eventData) const userId = _getUserId(eventData)
AnalyticsManager.recordEvent(userId, 'subscription-invoice-collected') AnalyticsManager.recordEventForUser(userId, 'subscription-invoice-collected')
AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', false) AnalyticsManager.setUserPropertyForUser(
userId,
'subscription-is-trial',
false
)
} }
function _getUserId(eventData) { function _getUserId(eventData) {

View file

@ -116,7 +116,7 @@ async function userSubscriptionPage(req, res) {
personalSubscription ? personalSubscription.plan : undefined personalSubscription ? personalSubscription.plan : undefined
) )
AnalyticsManager.recordEvent(user._id, 'subscription-page-view') AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view')
const data = { const data = {
title: 'your_subscription', title: 'your_subscription',

View file

@ -360,7 +360,7 @@ async function _sendUserGroupPlanCodeUserProperty(userId) {
bestFeatures = plan.features bestFeatures = plan.features
} }
} }
AnalyticsManager.setUserProperty( AnalyticsManager.setUserPropertyForUser(
userId, userId,
'group-subscription-plan-code', 'group-subscription-plan-code',
bestPlanCode bestPlanCode

View file

@ -164,7 +164,9 @@ const TokenAccessHandler = {
addReadOnlyUserToProject(userId, projectId, callback) { addReadOnlyUserToProject(userId, projectId, callback) {
userId = ObjectId(userId.toString()) userId = ObjectId(userId.toString())
projectId = ObjectId(projectId.toString()) projectId = ObjectId(projectId.toString())
Analytics.recordEvent(userId, 'project-joined', { mode: 'read-only' }) Analytics.recordEventForUser(userId, 'project-joined', {
mode: 'read-only',
})
Project.updateOne( Project.updateOne(
{ {
_id: projectId, _id: projectId,
@ -179,7 +181,9 @@ const TokenAccessHandler = {
addReadAndWriteUserToProject(userId, projectId, callback) { addReadAndWriteUserToProject(userId, projectId, callback) {
userId = ObjectId(userId.toString()) userId = ObjectId(userId.toString())
projectId = ObjectId(projectId.toString()) projectId = ObjectId(projectId.toString())
Analytics.recordEvent(userId, 'project-joined', { mode: 'read-write' }) Analytics.recordEventForUser(userId, 'project-joined', {
mode: 'read-write',
})
Project.updateOne( Project.updateOne(
{ {
_id: projectId, _id: projectId,

View file

@ -84,8 +84,16 @@ async function createNewUser(attributes, options = {}) {
} }
} }
Analytics.recordEvent(user._id, 'user-registered') await Analytics.recordEventForUser(user._id, 'user-registered')
Analytics.setUserProperty(user._id, 'created-at', new Date()) await Analytics.setUserPropertyForUser(user._id, 'created-at', new Date())
await Analytics.setUserPropertyForUser(user._id, 'user-id', user._id)
if (attributes.analyticsId) {
await Analytics.setUserPropertyForUser(
user._id,
'analytics-id',
attributes.analyticsId
)
}
if (Features.hasFeature('saas')) { if (Features.hasFeature('saas')) {
try { try {

View file

@ -32,7 +32,7 @@ async function checkAffiliations(userId) {
) )
if (hasCommonsAccountAffiliation) { if (hasCommonsAccountAffiliation) {
await AnalyticsManager.setUserProperty( await AnalyticsManager.setUserPropertyForUser(
userId, userId,
'registered-from-commons-account', 'registered-from-commons-account',
true true

View file

@ -8,7 +8,6 @@ const logger = require('logger-sharelatex')
const crypto = require('crypto') const crypto = require('crypto')
const EmailHandler = require('../Email/EmailHandler') const EmailHandler = require('../Email/EmailHandler')
const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler') const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler')
const Analytics = require('../Analytics/AnalyticsManager')
const settings = require('@overleaf/settings') const settings = require('@overleaf/settings')
const EmailHelper = require('../Helpers/EmailHelper') const EmailHelper = require('../Helpers/EmailHelper')
@ -31,6 +30,7 @@ const UserRegistrationHandler = {
email: userDetails.email, email: userDetails.email,
first_name: userDetails.first_name, first_name: userDetails.first_name,
last_name: userDetails.last_name, last_name: userDetails.last_name,
analyticsId: userDetails.analyticsId,
}, },
{}, {},
callback callback
@ -87,7 +87,6 @@ const UserRegistrationHandler = {
}, // this can be slow, just fire it off }, // this can be slow, just fire it off
], ],
error => { error => {
Analytics.recordEvent(user._id, 'user-registered')
callback(error, user) callback(error, user)
} }
) )

View file

@ -15,6 +15,7 @@ const sessionsRedisClient = UserSessionsRedis.client()
const SessionAutostartMiddleware = require('./SessionAutostartMiddleware') const SessionAutostartMiddleware = require('./SessionAutostartMiddleware')
const SessionStoreManager = require('./SessionStoreManager') const SessionStoreManager = require('./SessionStoreManager')
const AnalyticsManager = require('../Features/Analytics/AnalyticsManager')
const session = require('express-session') const session = require('express-session')
const RedisStore = require('connect-redis')(session) const RedisStore = require('connect-redis')(session)
const bodyParser = require('./BodyParserWrapper') const bodyParser = require('./BodyParserWrapper')
@ -32,6 +33,7 @@ const ProxyManager = require('./ProxyManager')
const translations = require('./Translations') const translations = require('./Translations')
const Modules = require('./Modules') const Modules = require('./Modules')
const Views = require('./Views') const Views = require('./Views')
const Features = require('./Features')
const ErrorController = require('../Features/Errors/ErrorController') const ErrorController = require('../Features/Errors/ErrorController')
const HttpErrorHandler = require('../Features/Errors/HttpErrorHandler') const HttpErrorHandler = require('../Features/Errors/HttpErrorHandler')
@ -125,6 +127,9 @@ webRouter.use(
rolling: true, rolling: true,
}) })
) )
if (Features.hasFeature('saas')) {
webRouter.use(AnalyticsManager.analyticsIdMiddleware)
}
// patch the session store to generate a validation token for every new session // patch the session store to generate a validation token for every new session
SessionStoreManager.enableValidationToken(sessionStore) SessionStoreManager.enableValidationToken(sessionStore)

View file

@ -166,6 +166,7 @@ const UserSchema = new Schema({
onboardingEmailSentAt: { type: Date }, onboardingEmailSentAt: { type: Date },
auditLog: [AuditLogEntrySchema], auditLog: [AuditLogEntrySchema],
splitTests: Schema.Types.Mixed, splitTests: Schema.Types.Mixed,
analyticsId: { type: String },
}) })
exports.User = mongoose.model('User', UserSchema) exports.User = mongoose.model('User', UserSchema)

View file

@ -12,7 +12,7 @@ describe('AnalyticsController', function () {
this.AnalyticsManager = { this.AnalyticsManager = {
updateEditingSession: sinon.stub(), updateEditingSession: sinon.stub(),
recordEvent: sinon.stub(), recordEventForSession: sinon.stub(),
} }
this.Features = { this.Features = {
@ -42,6 +42,7 @@ describe('AnalyticsController', function () {
params: { params: {
projectId: 'a project id', projectId: 'a project id',
}, },
session: {},
} }
this.GeoIpLookup.getDetails = sinon this.GeoIpLookup.getDetails = sinon
.stub() .stub()
@ -78,35 +79,18 @@ describe('AnalyticsController', function () {
delete this.expectedData._csrf delete this.expectedData._csrf
}) })
it('should use the user_id', function (done) { it('should use the session', function (done) {
this.SessionManager.getLoggedInUserId.returns('1234')
this.controller.recordEvent(this.req, this.res) this.controller.recordEvent(this.req, this.res)
this.AnalyticsManager.recordEvent this.AnalyticsManager.recordEventForSession
.calledWith('1234', this.req.params.event, this.expectedData) .calledWith(this.req.session, this.req.params.event, this.expectedData)
.should.equal(true)
done()
})
it('should use the session id', function (done) {
this.controller.recordEvent(this.req, this.res)
this.AnalyticsManager.recordEvent
.calledWith(
this.req.sessionID,
this.req.params.event,
this.expectedData
)
.should.equal(true) .should.equal(true)
done() done()
}) })
it('should remove the CSRF token before sending to the manager', function (done) { it('should remove the CSRF token before sending to the manager', function (done) {
this.controller.recordEvent(this.req, this.res) this.controller.recordEvent(this.req, this.res)
this.AnalyticsManager.recordEvent this.AnalyticsManager.recordEventForSession
.calledWith( .calledWith(this.req.session, this.req.params.event, this.expectedData)
this.req.sessionID,
this.req.params.event,
this.expectedData
)
.should.equal(true) .should.equal(true)
done() done()
}) })

View file

@ -10,6 +10,7 @@ const MODULE_PATH = path.join(
describe('AnalyticsManager', function () { describe('AnalyticsManager', function () {
beforeEach(function () { beforeEach(function () {
this.fakeUserId = '123abc' this.fakeUserId = '123abc'
this.analyticsId = '123456'
this.Settings = { this.Settings = {
analytics: { enabled: true }, analytics: { enabled: true },
} }
@ -50,6 +51,9 @@ describe('AnalyticsManager', function () {
requires: { requires: {
'@overleaf/settings': this.Settings, '@overleaf/settings': this.Settings,
'../../infrastructure/Queues': this.Queues, '../../infrastructure/Queues': this.Queues,
'./UserAnalyticsIdCache': (this.UserAnalyticsIdCache = {
get: sinon.stub().resolves(this.analyticsId),
}),
}, },
}) })
}) })
@ -70,21 +74,26 @@ describe('AnalyticsManager', function () {
describe('queues the appropriate message for', function () { describe('queues the appropriate message for', function () {
it('identifyUser', function () { it('identifyUser', function () {
const oldUserId = '456def' const analyticsId = '456def'
this.AnalyticsManager.identifyUser(this.fakeUserId, oldUserId) this.AnalyticsManager.identifyUser(this.fakeUserId, analyticsId)
sinon.assert.calledWithMatch(this.analyticsEventsQueue.add, 'identify', { sinon.assert.calledWithMatch(this.analyticsEventsQueue.add, 'identify', {
userId: this.fakeUserId, userId: this.fakeUserId,
oldUserId, analyticsId,
}) })
}) })
it('recordEvent', function () { it('recordEventForUser', async function () {
const event = 'fake-event' const event = 'fake-event'
this.AnalyticsManager.recordEvent(this.fakeUserId, event, null) await this.AnalyticsManager.recordEventForUser(
sinon.assert.calledWithMatch(this.analyticsEventsQueue.add, 'event', { this.fakeUserId,
event,
null
)
sinon.assert.calledWithMatch(this.analyticsEventsQueue.add, 'event', {
analyticsId: this.analyticsId,
event, event,
userId: this.fakeUserId,
segmentation: null, segmentation: null,
isLoggedIn: true,
}) })
}) })

View file

@ -28,6 +28,7 @@ describe('AuthenticationController', function () {
this.res = new MockResponse() this.res = new MockResponse()
this.callback = sinon.stub() this.callback = sinon.stub()
this.next = sinon.stub() this.next = sinon.stub()
this.req.session.analyticsId = 'abc-123'
this.AuthenticationController = SandboxedModule.require(modulePath, { this.AuthenticationController = SandboxedModule.require(modulePath, {
requires: { requires: {
@ -53,8 +54,9 @@ describe('AuthenticationController', function () {
setupLoginData: sinon.stub(), setupLoginData: sinon.stub(),
}), }),
'../Analytics/AnalyticsManager': (this.AnalyticsManager = { '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
recordEvent: sinon.stub(), recordEventForUser: sinon.stub(),
identifyUser: sinon.stub(), identifyUser: sinon.stub(),
getIdsFromSession: sinon.stub().returns({ userId: this.user._id }),
}), }),
'../../infrastructure/SessionStoreManager': (this.SessionStoreManager = {}), '../../infrastructure/SessionStoreManager': (this.SessionStoreManager = {}),
'@overleaf/settings': (this.Settings = { '@overleaf/settings': (this.Settings = {
@ -1236,9 +1238,11 @@ describe('AuthenticationController', function () {
}) })
it('should call identifyUser', function () { it('should call identifyUser', function () {
this.AnalyticsManager.identifyUser sinon.assert.calledWith(
.calledWith(this.user._id, this.req.sessionID) this.AnalyticsManager.identifyUser,
.should.equal(true) this.user._id,
this.req.session.analyticsId
)
}) })
it('should setup the user data in the background', function () { it('should setup the user data in the background', function () {
@ -1271,9 +1275,11 @@ describe('AuthenticationController', function () {
}) })
it('should track the login event', function () { it('should track the login event', function () {
this.AnalyticsManager.recordEvent sinon.assert.calledWith(
.calledWith(this.user._id, 'user-logged-in') this.AnalyticsManager.recordEventForUser,
.should.equal(true) this.user._id,
'user-logged-in'
)
}) })
}) })
}) })

View file

@ -24,7 +24,7 @@ const { ObjectId } = require('mongodb')
describe('CollaboratorsInviteController', function () { describe('CollaboratorsInviteController', function () {
beforeEach(function () { beforeEach(function () {
this.user = { _id: 'id' } this.user = { _id: 'id' }
this.AnalyticsManger = { recordEvent: sinon.stub() } this.AnalyticsManger = { recordEventForUser: sinon.stub() }
this.sendingUser = null this.sendingUser = null
this.AuthenticationController = { this.AuthenticationController = {
getSessionUser: req => { getSessionUser: req => {

View file

@ -173,7 +173,7 @@ describe('ProjectController', function () {
.BrandVariationsHandler, .BrandVariationsHandler,
'../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher, '../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
'../../models/Project': {}, '../../models/Project': {},
'../Analytics/AnalyticsManager': { recordEvent: () => {} }, '../Analytics/AnalyticsManager': { recordEventForUser: () => {} },
'../../infrastructure/Modules': { '../../infrastructure/Modules': {
hooks: { fire: sinon.stub().yields(null, []) }, hooks: { fire: sinon.stub().yields(null, []) },
}, },

View file

@ -40,7 +40,7 @@ describe('FeaturesUpdater', function () {
'../Institutions/InstitutionsFeatures': (this.InstitutionsFeatures = {}), '../Institutions/InstitutionsFeatures': (this.InstitutionsFeatures = {}),
'../User/UserGetter': (this.UserGetter = {}), '../User/UserGetter': (this.UserGetter = {}),
'../Analytics/AnalyticsManager': (this.AnalyticsManager = { '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
setUserProperty: sinon.stub(), setUserPropertyForUser: sinon.stub(),
}), }),
'../../infrastructure/Modules': (this.Modules = { '../../infrastructure/Modules': (this.Modules = {
hooks: { fire: sinon.stub() }, hooks: { fire: sinon.stub() },
@ -182,7 +182,7 @@ describe('FeaturesUpdater', function () {
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.user_id, this.user_id,
'feature-set', 'feature-set',
'personal' 'personal'
@ -201,7 +201,7 @@ describe('FeaturesUpdater', function () {
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.user_id, this.user_id,
'feature-set', 'feature-set',
'mixed' 'mixed'

View file

@ -27,8 +27,8 @@ describe('RecurlyEventHandler', function () {
this.RecurlyEventHandler = SandboxedModule.require(modulePath, { this.RecurlyEventHandler = SandboxedModule.require(modulePath, {
requires: { requires: {
'../Analytics/AnalyticsManager': (this.AnalyticsManager = { '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
recordEvent: sinon.stub(), recordEventForUser: sinon.stub(),
setUserProperty: sinon.stub(), setUserPropertyForUser: sinon.stub(),
}), }),
}, },
}) })
@ -40,7 +40,7 @@ describe('RecurlyEventHandler', function () {
this.eventData this.eventData
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.recordEvent, this.AnalyticsManager.recordEventForUser,
this.userId, this.userId,
'subscription-started', 'subscription-started',
{ {
@ -50,19 +50,19 @@ describe('RecurlyEventHandler', function () {
} }
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-plan-code', 'subscription-plan-code',
this.planCode this.planCode
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-state', 'subscription-state',
'active' 'active'
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-is-trial', 'subscription-is-trial',
true true
@ -83,7 +83,7 @@ describe('RecurlyEventHandler', function () {
this.eventData this.eventData
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.recordEvent, this.AnalyticsManager.recordEventForUser,
this.userId, this.userId,
'subscription-started', 'subscription-started',
{ {
@ -93,13 +93,13 @@ describe('RecurlyEventHandler', function () {
} }
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-state', 'subscription-state',
'active' 'active'
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-is-trial', 'subscription-is-trial',
false false
@ -114,7 +114,7 @@ describe('RecurlyEventHandler', function () {
this.eventData this.eventData
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.recordEvent, this.AnalyticsManager.recordEventForUser,
this.userId, this.userId,
'subscription-updated', 'subscription-updated',
{ {
@ -123,19 +123,19 @@ describe('RecurlyEventHandler', function () {
} }
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-plan-code', 'subscription-plan-code',
this.planCode this.planCode
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-state', 'subscription-state',
'active' 'active'
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-is-trial', 'subscription-is-trial',
true true
@ -149,7 +149,7 @@ describe('RecurlyEventHandler', function () {
this.eventData this.eventData
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.recordEvent, this.AnalyticsManager.recordEventForUser,
this.userId, this.userId,
'subscription-cancelled', 'subscription-cancelled',
{ {
@ -159,13 +159,13 @@ describe('RecurlyEventHandler', function () {
} }
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-state', 'subscription-state',
'cancelled' 'cancelled'
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-is-trial', 'subscription-is-trial',
true true
@ -179,7 +179,7 @@ describe('RecurlyEventHandler', function () {
this.eventData this.eventData
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.recordEvent, this.AnalyticsManager.recordEventForUser,
this.userId, this.userId,
'subscription-expired', 'subscription-expired',
{ {
@ -189,19 +189,19 @@ describe('RecurlyEventHandler', function () {
} }
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-plan-code', 'subscription-plan-code',
this.planCode this.planCode
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-state', 'subscription-state',
'expired' 'expired'
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.userId, this.userId,
'subscription-is-trial', 'subscription-is-trial',
true true
@ -214,7 +214,7 @@ describe('RecurlyEventHandler', function () {
this.eventData this.eventData
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.recordEvent, this.AnalyticsManager.recordEventForUser,
this.userId, this.userId,
'subscription-renewed', 'subscription-renewed',
{ {
@ -231,7 +231,7 @@ describe('RecurlyEventHandler', function () {
this.eventData this.eventData
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.recordEvent, this.AnalyticsManager.recordEventForUser,
this.userId, this.userId,
'subscription-reactivated', 'subscription-reactivated',
{ {
@ -255,7 +255,7 @@ describe('RecurlyEventHandler', function () {
} }
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.recordEvent, this.AnalyticsManager.recordEventForUser,
this.userId, this.userId,
'subscription-invoice-collected' 'subscription-invoice-collected'
) )
@ -274,7 +274,7 @@ describe('RecurlyEventHandler', function () {
}, },
} }
) )
sinon.assert.notCalled(this.AnalyticsManager.recordEvent) sinon.assert.notCalled(this.AnalyticsManager.recordEventForUser)
}) })
it('with closed_invoice_notification', function () { it('with closed_invoice_notification', function () {
@ -291,7 +291,7 @@ describe('RecurlyEventHandler', function () {
} }
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.recordEvent, this.AnalyticsManager.recordEventForUser,
this.userId, this.userId,
'subscription-invoice-collected' 'subscription-invoice-collected'
) )
@ -310,6 +310,6 @@ describe('RecurlyEventHandler', function () {
}, },
} }
) )
sinon.assert.notCalled(this.AnalyticsManager.recordEvent) sinon.assert.notCalled(this.AnalyticsManager.recordEventForUser)
}) })
}) })

View file

@ -141,8 +141,9 @@ describe('SubscriptionController', function () {
}), }),
'./Errors': SubscriptionErrors, './Errors': SubscriptionErrors,
'../Analytics/AnalyticsManager': (this.AnalyticsManager = { '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
recordEvent: sinon.stub(), recordEventForUser: sinon.stub(),
setUserProperty: sinon.stub(), recordEventForSession: sinon.stub(),
setUserPropertyForUser: sinon.stub(),
}), }),
'../SplitTests/SplitTestHandler': (this.SplitTestHandler = { '../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
getTestSegmentation: () => {}, getTestSegmentation: () => {},

View file

@ -108,7 +108,7 @@ describe('SubscriptionHandler', function () {
this.EmailHandler = { sendEmail: sinon.stub() } this.EmailHandler = { sendEmail: sinon.stub() }
this.AnalyticsManager = { recordEvent: sinon.stub() } this.AnalyticsManager = { recordEventForUser: sinon.stub() }
this.PlansLocator = { this.PlansLocator = {
findLocalPlanInSettings: sinon.stub().returns({ planCode: 'plan' }), findLocalPlanInSettings: sinon.stub().returns({ planCode: 'plan' }),

View file

@ -105,7 +105,7 @@ describe('SubscriptionUpdater', function () {
findOneAndUpdate: sinon.stub().yields(), findOneAndUpdate: sinon.stub().yields(),
} }
this.AnalyticsManager = { this.AnalyticsManager = {
setUserProperty: sinon.stub(), setUserPropertyForUser: sinon.stub(),
} }
this.SubscriptionUpdater = SandboxedModule.require(modulePath, { this.SubscriptionUpdater = SandboxedModule.require(modulePath, {
requires: { requires: {
@ -527,7 +527,7 @@ describe('SubscriptionUpdater', function () {
this.otherUserId, this.otherUserId,
() => { () => {
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.otherUserId, this.otherUserId,
'group-subscription-plan-code', 'group-subscription-plan-code',
'group_subscription' 'group_subscription'
@ -547,7 +547,7 @@ describe('SubscriptionUpdater', function () {
this.otherUserId this.otherUserId
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.otherUserId, this.otherUserId,
'group-subscription-plan-code', 'group-subscription-plan-code',
'group_subscription' 'group_subscription'
@ -564,7 +564,7 @@ describe('SubscriptionUpdater', function () {
this.otherUserId this.otherUserId
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.otherUserId, this.otherUserId,
'group-subscription-plan-code', 'group-subscription-plan-code',
'better_group_subscription' 'better_group_subscription'
@ -581,7 +581,7 @@ describe('SubscriptionUpdater', function () {
this.otherUserId this.otherUserId
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.otherUserId, this.otherUserId,
'group-subscription-plan-code', 'group-subscription-plan-code',
'better_group_subscription' 'better_group_subscription'
@ -622,7 +622,7 @@ describe('SubscriptionUpdater', function () {
this.otherUserId, this.otherUserId,
() => { () => {
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.otherUserId, this.otherUserId,
'group-subscription-plan-code', 'group-subscription-plan-code',
null null
@ -635,7 +635,7 @@ describe('SubscriptionUpdater', function () {
it('should set the group plan code user property when removing user from all groups', function (done) { it('should set the group plan code user property when removing user from all groups', function (done) {
this.SubscriptionUpdater.removeUserFromAllGroups(this.otherUserId, () => { this.SubscriptionUpdater.removeUserFromAllGroups(this.otherUserId, () => {
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty, this.AnalyticsManager.setUserPropertyForUser,
this.otherUserId, this.otherUserId,
'group-subscription-plan-code', 'group-subscription-plan-code',
null null

View file

@ -12,7 +12,6 @@
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
const SandboxedModule = require('sandboxed-module') const SandboxedModule = require('sandboxed-module')
const assert = require('assert')
const path = require('path') const path = require('path')
const sinon = require('sinon') const sinon = require('sinon')
const modulePath = path.join( const modulePath = path.join(
@ -43,7 +42,7 @@ describe('TokenAccessHandler', function () {
}), }),
crypto: (this.Crypto = require('crypto')), crypto: (this.Crypto = require('crypto')),
'../Analytics/AnalyticsManager': (this.Analytics = { '../Analytics/AnalyticsManager': (this.Analytics = {
recordEvent: sinon.stub(), recordEventForUser: sinon.stub(),
}), }),
}, },
})) }))
@ -138,7 +137,7 @@ describe('TokenAccessHandler', function () {
this.Project.updateOne.lastCall.args[1].$addToSet this.Project.updateOne.lastCall.args[1].$addToSet
).to.have.keys('tokenAccessReadOnly_refs') ).to.have.keys('tokenAccessReadOnly_refs')
sinon.assert.calledWith( sinon.assert.calledWith(
this.Analytics.recordEvent, this.Analytics.recordEventForUser,
this.userId, this.userId,
'project-joined', 'project-joined',
{ mode: 'read-only' } { mode: 'read-only' }
@ -199,7 +198,7 @@ describe('TokenAccessHandler', function () {
this.Project.updateOne.lastCall.args[1].$addToSet this.Project.updateOne.lastCall.args[1].$addToSet
).to.have.keys('tokenAccessReadAndWrite_refs') ).to.have.keys('tokenAccessReadAndWrite_refs')
sinon.assert.calledWith( sinon.assert.calledWith(
this.Analytics.recordEvent, this.Analytics.recordEventForUser,
this.userId, this.userId,
'project-joined', 'project-joined',
{ mode: 'read-write' } { mode: 'read-write' }

View file

@ -24,6 +24,7 @@ describe('UserController', function () {
_id: this.user_id, _id: this.user_id,
email: 'old@something.com', email: 'old@something.com',
}, },
analyticsId: this.user_id,
}, },
sessionID: '123', sessionID: '123',
body: {}, body: {},
@ -544,9 +545,10 @@ describe('UserController', function () {
}) })
it('should register the user and send them an email', function () { it('should register the user and send them an email', function () {
this.UserRegistrationHandler.registerNewUserAndSendActivationEmail sinon.assert.calledWith(
.calledWith(this.email) this.UserRegistrationHandler.registerNewUserAndSendActivationEmail,
.should.equal(true) this.email
)
}) })
it('should return the user and activation url', function () { it('should return the user and activation url', function () {

View file

@ -42,8 +42,8 @@ describe('UserCreator', function () {
}, },
}), }),
'../Analytics/AnalyticsManager': (this.Analytics = { '../Analytics/AnalyticsManager': (this.Analytics = {
recordEvent: sinon.stub(), recordEventForUser: sinon.stub(),
setUserProperty: sinon.stub(), setUserPropertyForUser: sinon.stub(),
}), }),
'./UserOnboardingEmailManager': (this.UserOnboardingEmailManager = { './UserOnboardingEmailManager': (this.UserOnboardingEmailManager = {
scheduleOnboardingEmail: sinon.stub(), scheduleOnboardingEmail: sinon.stub(),
@ -271,12 +271,12 @@ describe('UserCreator', function () {
}) })
assert.equal(user.email, this.email) assert.equal(user.email, this.email)
sinon.assert.calledWith( sinon.assert.calledWith(
this.Analytics.recordEvent, this.Analytics.recordEventForUser,
user._id, user._id,
'user-registered' 'user-registered'
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.Analytics.setUserProperty, this.Analytics.setUserPropertyForUser,
user._id, user._id,
'created-at' 'created-at'
) )

View file

@ -36,7 +36,7 @@ describe('UserPostRegistrationAnalyticsManager', function () {
}, },
} }
this.AnalyticsManager = { this.AnalyticsManager = {
setUserProperty: sinon.stub().resolves(), setUserPropertyForUser: sinon.stub().resolves(),
} }
this.UserPostRegistrationAnalyticsManager = SandboxedModule.require( this.UserPostRegistrationAnalyticsManager = SandboxedModule.require(
MODULE_PATH, MODULE_PATH,
@ -72,7 +72,8 @@ describe('UserPostRegistrationAnalyticsManager', function () {
) )
expect(this.InstitutionsAPI.promises.getUserAffiliations).not.to.have.been expect(this.InstitutionsAPI.promises.getUserAffiliations).not.to.have.been
.called .called
expect(this.AnalyticsManager.setUserProperty).not.to.have.been.called expect(this.AnalyticsManager.setUserPropertyForUser).not.to.have.been
.called
}) })
it('sets user property if user has commons account affiliationd', async function () { it('sets user property if user has commons account affiliationd', async function () {
@ -92,7 +93,9 @@ describe('UserPostRegistrationAnalyticsManager', function () {
await this.UserPostRegistrationAnalyticsManager.postRegistrationAnalytics( await this.UserPostRegistrationAnalyticsManager.postRegistrationAnalytics(
this.fakeUserId this.fakeUserId
) )
expect(this.AnalyticsManager.setUserProperty).to.have.been.calledWith( expect(
this.AnalyticsManager.setUserPropertyForUser
).to.have.been.calledWith(
this.fakeUserId, this.fakeUserId,
'registered-from-commons-account', 'registered-from-commons-account',
true true
@ -110,7 +113,8 @@ describe('UserPostRegistrationAnalyticsManager', function () {
await this.UserPostRegistrationAnalyticsManager.postRegistrationAnalytics( await this.UserPostRegistrationAnalyticsManager.postRegistrationAnalytics(
this.fakeUserId this.fakeUserId
) )
expect(this.AnalyticsManager.setUserProperty).not.to.have.been.called expect(this.AnalyticsManager.setUserPropertyForUser).not.to.have.been
.called
}) })
}) })
}) })

View file

@ -24,7 +24,11 @@ const EmailHelper = require('../../../../app/src/Features/Helpers/EmailHelper')
describe('UserRegistrationHandler', function () { describe('UserRegistrationHandler', function () {
beforeEach(function () { beforeEach(function () {
this.user = { _id: (this.user_id = '31j2lk21kjl') } this.analyticsId = '123456'
this.user = {
_id: (this.user_id = '31j2lk21kjl'),
analyticsId: this.analyticsId,
}
this.User = { updateOne: sinon.stub().callsArgWith(2) } this.User = { updateOne: sinon.stub().callsArgWith(2) }
this.UserGetter = { getUserByAnyEmail: sinon.stub() } this.UserGetter = { getUserByAnyEmail: sinon.stub() }
this.UserCreator = { this.UserCreator = {
@ -49,7 +53,9 @@ describe('UserRegistrationHandler', function () {
'../Email/EmailHandler': this.EmailHandler, '../Email/EmailHandler': this.EmailHandler,
'../Security/OneTimeTokenHandler': this.OneTimeTokenHandler, '../Security/OneTimeTokenHandler': this.OneTimeTokenHandler,
'../Analytics/AnalyticsManager': (this.AnalyticsManager = { '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
recordEvent: sinon.stub(), recordEventForUser: sinon.stub(),
setUserPropertyForUser: sinon.stub(),
identifyUser: sinon.stub(),
}), }),
'@overleaf/settings': (this.settings = { '@overleaf/settings': (this.settings = {
siteUrl: 'http://sl.example.com', siteUrl: 'http://sl.example.com',
@ -58,10 +64,11 @@ describe('UserRegistrationHandler', function () {
}, },
}) })
return (this.passingRequest = { this.passingRequest = {
email: 'something@email.com', email: 'something@email.com',
password: '123', password: '123',
}) analyticsId: this.analyticsId,
}
}) })
describe('validate Register Request', function () { describe('validate Register Request', function () {
@ -167,16 +174,15 @@ describe('UserRegistrationHandler', function () {
}) })
it('should create a new user', function (done) { it('should create a new user', function (done) {
return this.handler.registerNewUser(this.passingRequest, err => { this.handler.registerNewUser(this.passingRequest, err => {
this.UserCreator.createNewUser sinon.assert.calledWith(this.UserCreator.createNewUser, {
.calledWith({
email: this.passingRequest.email, email: this.passingRequest.email,
holdingAccount: false, holdingAccount: false,
first_name: this.passingRequest.first_name, first_name: this.passingRequest.first_name,
last_name: this.passingRequest.last_name, last_name: this.passingRequest.last_name,
analyticsId: this.user.analyticsId,
}) })
.should.equal(true) done()
return done()
}) })
}) })
@ -227,15 +233,6 @@ describe('UserRegistrationHandler', function () {
return done() return done()
}) })
}) })
it('should track the registration event', function (done) {
return this.handler.registerNewUser(this.passingRequest, err => {
this.AnalyticsManager.recordEvent
.calledWith(this.user._id, 'user-registered')
.should.equal(true)
return done()
})
})
}) })
it('should call the ReferalAllocator', function (done) { it('should call the ReferalAllocator', function (done) {
@ -270,12 +267,10 @@ describe('UserRegistrationHandler', function () {
}) })
it('should ask the UserRegistrationHandler to register user', function () { it('should ask the UserRegistrationHandler to register user', function () {
return this.handler.registerNewUser sinon.assert.calledWith(this.handler.registerNewUser, {
.calledWith({
email: this.email, email: this.email,
password: this.password, password: this.password,
}) })
.should.equal(true)
}) })
it('should generate a new password reset token', function () { it('should generate a new password reset token', function () {