diff --git a/services/web/app/src/Features/Analytics/AnalyticsManager.mjs b/services/web/app/src/Features/Analytics/AnalyticsManager.mjs index 202a3446ea..3130daced3 100644 --- a/services/web/app/src/Features/Analytics/AnalyticsManager.mjs +++ b/services/web/app/src/Features/Analytics/AnalyticsManager.mjs @@ -82,7 +82,7 @@ async function recordEventForUser(userId, event, segmentation) { userId, event, segmentation, - isLabsUser: Boolean(labsProgram), + isLabsUser: labsProgram, isLoggedIn: true, }) } @@ -97,6 +97,31 @@ function recordEventForUserInBackground(userId, event, segmentation) { }) } +async function recordEventForMongoUser(user, event, segmentation) { + const { userId, analyticsId, labsProgram } = _getIdsFromMongoUser(user) + if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) { + return + } + _recordEvent({ + analyticsId, + userId, + event, + segmentation, + isLabsUser: labsProgram, + isLoggedIn: true, + }) +} + +function recordEventForMongoUserInBackground(user, event, segmentation) { + const { userId } = _getIdsFromMongoUser(user) // throw before going into the background. + recordEventForMongoUser(user, event, segmentation).catch(err => { + logger.warn( + { err, userId, event, segmentation }, + 'failed to record event for user' + ) + }) +} + function recordEventForSession(session, event, segmentation) { const { analyticsId, userId } = getIdsFromSession(session) if (!analyticsId) { @@ -142,7 +167,7 @@ async function setUserPropertyForUser(userId, propertyName, propertyValue) { if (analyticsId) { await _setUserProperty({ analyticsId, - isLabsUser: Boolean(labsProgram), + isLabsUser: labsProgram, propertyName, propertyValue, }) @@ -158,6 +183,41 @@ function setUserPropertyForUserInBackground(userId, property, value) { }) } +/** + * @param {{_id: ObjectId, analyticsId: string, labsProgram: boolean}} user + * @param {string} propertyName + * @param {any} propertyValue + * @return {Promise} + */ +async function setUserPropertyForMongoUser(user, propertyName, propertyValue) { + const { userId, analyticsId, labsProgram } = _getIdsFromMongoUser(user) + if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) { + return + } + _checkPropertyValue(propertyValue) + await _setUserProperty({ + analyticsId, + isLabsUser: labsProgram, + propertyName, + propertyValue, + }) +} + +/** + * @param {{_id: ObjectId, analyticsId: string, labsProgram: boolean}} user + * @param {string} property + * @param {any} value + */ +function setUserPropertyForMongoUserInBackground(user, property, value) { + const { userId } = _getIdsFromMongoUser(user) // throw before going into the background. + setUserPropertyForMongoUser(user, property, value).catch(err => { + logger.warn( + { err, userId, property, value }, + 'failed to set user property for user' + ) + }) +} + async function setUserPropertyForAnalyticsId( analyticsId, propertyName, @@ -192,8 +252,8 @@ async function setUserPropertyForSession(session, propertyName, propertyValue) { } function setUserPropertyForSessionInBackground(session, property, value) { + const { analyticsId, userId } = getIdsFromSession(session) // throw before going into the background. setUserPropertyForSession(session, property, value).catch(err => { - const { analyticsId, userId } = getIdsFromSession(session) logger.warn( { err, analyticsId, userId, property, value }, 'failed to set user property for session' @@ -452,6 +512,26 @@ function _isAnalyticsDisabled() { return !(Settings.analytics && Settings.analytics.enabled) } +/** + * @param {{_id: ObjectId, analyticsId: string, labsProgram: boolean}} user + * @return {{userId: string, analyticsId: string, labsProgram: boolean}} + */ +function _getIdsFromMongoUser(user) { + const userId = user?._id?.toString() + if (!userId) { + throw new Error('bug: include db.users._id in projection') + } + const analyticsId = user?.analyticsId + if (!analyticsId) { + throw new Error('bug: include db.users.analyticsId in projection') + } + const labsProgram = user?.labsProgram + if (typeof labsProgram !== 'boolean') { + throw new Error('bug: include db.users.labsProgram in projection') + } + return { userId, analyticsId, labsProgram } +} + function _checkPropertyValue(propertyValue) { if (propertyValue === undefined) { throw new Error( @@ -498,13 +578,15 @@ async function analyticsIdMiddleware(req, res, next) { if (sessionUser) { // For old sessions, session.analyticsId is the anon id immediately after login. Do not use it! session.analyticsId = sessionUser.analyticsId - if (!session.analyticsId) { - session.analyticsId = sessionUser.analyticsId = - await UserAnalyticsDataCache.getAnalyticsId( + if (!session.analyticsId || typeof sessionUser.labsProgram !== 'boolean') { + const { analyticsId, labsProgram } = + await UserAnalyticsDataCache.getAnalyticsData( sessionUser._id, // Do not drill down further, this middleware is on all endpoints. 'analyticsIdMiddleware' ) + session.analyticsId = sessionUser.analyticsId = analyticsId + sessionUser.labsProgram = labsProgram } } else if (!session.analyticsId) { // generate an `analyticsId` if needed @@ -521,9 +603,13 @@ export default { recordEventForSession, recordEventForUser, recordEventForUserInBackground, + recordEventForMongoUser, + recordEventForMongoUserInBackground, emitPackageUsage, setUserPropertyForUser, setUserPropertyForUserInBackground, + setUserPropertyForMongoUser, + setUserPropertyForMongoUserInBackground, setUserPropertyForSession, setUserPropertyForSessionInBackground, setUserPropertyForAnalyticsId, diff --git a/services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceHelper.mjs b/services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceHelper.mjs index 450cf607d7..9a5ca5eaae 100644 --- a/services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceHelper.mjs +++ b/services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceHelper.mjs @@ -25,32 +25,32 @@ function clearInbound(session) { } } -function addUserProperties(userId, session) { +function addUserProperties(user, session) { if (!session) { return } if (session.required_login_from_product_medium) { - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForMongoUserInBackground( + user, `registered-from-product-medium`, session.required_login_from_product_medium ) if (session.required_login_from_product_source) { - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForMongoUserInBackground( + user, `registered-from-product-source`, session.required_login_from_product_source ) } } else if (session.referal_id) { - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForMongoUserInBackground( + user, `registered-from-bonus-scheme`, true ) - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForMongoUserInBackground( + user, `registered-from-product-medium`, 'bonus-scheme' ) @@ -58,16 +58,16 @@ function addUserProperties(userId, session) { if (session.inbound) { if (session.inbound.referrer && session.inbound.referrer.medium) { - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForMongoUserInBackground( + user, `registered-from-referrer-medium`, `${session.inbound.referrer.medium .charAt(0) .toUpperCase()}${session.inbound.referrer.medium.slice(1)}` ) if (session.inbound.referrer.source) { - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForMongoUserInBackground( + user, `registered-from-referrer-source`, session.inbound.referrer.source ) @@ -77,8 +77,8 @@ function addUserProperties(userId, session) { if (session.inbound.utm) { for (const utmKey of RequestHelper.REGISTRATION_UTM_KEYS) { if (session.inbound.utm[utmKey]) { - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForMongoUserInBackground( + user, `registered-from-${utmKey.replace('_', '-')}`, session.inbound.utm[utmKey] ) diff --git a/services/web/app/src/Features/Authentication/AuthenticationController.mjs b/services/web/app/src/Features/Authentication/AuthenticationController.mjs index 89477058bc..8201f01247 100644 --- a/services/web/app/src/Features/Authentication/AuthenticationController.mjs +++ b/services/web/app/src/Features/Authentication/AuthenticationController.mjs @@ -684,7 +684,7 @@ function _loginAsyncHandlers(req, user, anonymousAnalyticsId, isNewUser) { LoginRateLimiter.recordSuccessfulLogin(user.email, () => {}) AuthenticationController._recordSuccessfulLogin(user._id, () => {}) AuthenticationController.ipMatchCheck(req, user) - Analytics.recordEventForUserInBackground(user._id, 'user-logged-in', { + Analytics.recordEventForMongoUserInBackground(user, 'user-logged-in', { source: req.session.saml ? 'saml' : req.user_info?.auth_provider || 'email-password', diff --git a/services/web/app/src/Features/BetaProgram/BetaProgramController.mjs b/services/web/app/src/Features/BetaProgram/BetaProgramController.mjs index 4e71d10bdf..9a103e8c53 100644 --- a/services/web/app/src/Features/BetaProgram/BetaProgramController.mjs +++ b/services/web/app/src/Features/BetaProgram/BetaProgramController.mjs @@ -8,7 +8,7 @@ import { expressify } from '@overleaf/promise-utils' async function optIn(req, res) { const userId = SessionManager.getLoggedInUserId(req.session) - await BetaProgramHandler.promises.optIn(userId) + await BetaProgramHandler.promises.optIn(req.session, userId) try { await SplitTestSessionHandler.promises.sessionMaintenance(req, null) } catch (error) { @@ -22,7 +22,7 @@ async function optIn(req, res) { async function optOut(req, res) { const userId = SessionManager.getLoggedInUserId(req.session) - await BetaProgramHandler.promises.optOut(userId) + await BetaProgramHandler.promises.optOut(req.session, userId) try { await SplitTestSessionHandler.promises.sessionMaintenance(req, null) } catch (error) { diff --git a/services/web/app/src/Features/BetaProgram/BetaProgramHandler.mjs b/services/web/app/src/Features/BetaProgram/BetaProgramHandler.mjs index 520a634068..90a529117c 100644 --- a/services/web/app/src/Features/BetaProgram/BetaProgramHandler.mjs +++ b/services/web/app/src/Features/BetaProgram/BetaProgramHandler.mjs @@ -3,23 +3,23 @@ import metrics from '@overleaf/metrics' import UserUpdater from '../User/UserUpdater.mjs' import AnalyticsManager from '../Analytics/AnalyticsManager.mjs' -async function optIn(userId) { +async function optIn(session, userId) { await UserUpdater.promises.updateUser(userId, { $set: { betaProgram: true } }) metrics.inc('beta-program.opt-in') - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForSessionInBackground( + session, 'beta-program', true ) } -async function optOut(userId) { +async function optOut(session, userId) { await UserUpdater.promises.updateUser(userId, { $set: { betaProgram: false }, }) metrics.inc('beta-program.opt-out') - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForSessionInBackground( + session, 'beta-program', false ) diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs index b909c5d563..a42b3473ea 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs +++ b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs @@ -493,17 +493,13 @@ async function acceptInvite(req, res) { } else if (invite.privileges === PrivilegeLevels.READ_ONLY) { editMode = 'view' } - AnalyticsManager.recordEventForUserInBackground( - currentUser._id, - 'project-joined', - { - projectId, - ownerId: invite.sendingUserId, // only owner can invite others - mode: editMode, - role: invite.privileges, - source: urlToken ? 'email-invite' : 'sharing-link', - } - ) + AnalyticsManager.recordEventForSession(req.session, 'project-joined', { + projectId, + ownerId: invite.sendingUserId, // only owner can invite others + mode: editMode, + role: invite.privileges, + source: urlToken ? 'email-invite' : 'sharing-link', + }) if (req.xhr) { res.sendStatus(204) // Done async via project page notification diff --git a/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs b/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs index 128911b4d3..cdedaf9ba5 100644 --- a/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs +++ b/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs @@ -98,14 +98,14 @@ async function exportProjectConversion(req, res) { type, { compileFromHistory, rootResourcePath } ) - AnalyticsManager.recordEventForUserInBackground(userId, 'convert-format', { + AnalyticsManager.recordEventForSession(req.session, 'convert-format', { sourceFormat: 'latex', targetFormat: type, status: 'success', operation: 'export', }) } catch (error) { - AnalyticsManager.recordEventForUserInBackground(userId, 'convert-format', { + AnalyticsManager.recordEventForSession(req.session, 'convert-format', { sourceFormat: 'latex', targetFormat: type, status: 'failure', diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesController.mjs b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.mjs index 8a0a465a9d..3c88ccfca0 100644 --- a/services/web/app/src/Features/LinkedFiles/LinkedFilesController.mjs +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.mjs @@ -82,13 +82,9 @@ async function createLinkedFile(req, res, next) { userId ) if (name.endsWith('.bib')) { - AnalyticsManager.recordEventForUserInBackground( - userId, - 'linked-bib-file', - { - integration: provider, - } - ) + AnalyticsManager.recordEventForSession(req.session, 'linked-bib-file', { + integration: provider, + }) } return res.json({ new_file_id: newFileId }) } catch (err) { diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index bce4a5699a..e27b08a622 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/services/web/app/src/Features/Project/ProjectController.mjs @@ -756,8 +756,8 @@ const _ProjectController = { planLimit, exceedAtLimit, } - AnalyticsManager.recordEventForUserInBackground( - userId, + AnalyticsManager.recordEventForSession( + req.session, 'project-opened', projectOpenedSegmentation ) @@ -843,8 +843,8 @@ const _ProjectController = { userIsMemberOfGroupSubscription ) - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForSessionInBackground( + req.session, 'customer-io-integration', true ) diff --git a/services/web/app/src/Features/Project/ProjectListController.mjs b/services/web/app/src/Features/Project/ProjectListController.mjs index fd433541c3..e2a3daf542 100644 --- a/services/web/app/src/Features/Project/ProjectListController.mjs +++ b/services/web/app/src/Features/Project/ProjectListController.mjs @@ -273,8 +273,8 @@ async function projectListPage(req, res, next) { customerIoEnabled = true - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForSessionInBackground( + req.session, 'customer-io-integration', true ) diff --git a/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs b/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs index 75ed733620..c5da72262e 100644 --- a/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs +++ b/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs @@ -39,10 +39,14 @@ function featuresEpochIsCurrent(user) { /** * Refresh features for the given user + * @param {string} userId + * @param {string} reason */ async function refreshFeatures(userId, reason) { const user = await UserGetter.promises.getUser(userId, { _id: 1, + analyticsId: 1, + labsProgram: 1, features: 1, email: 1, }) @@ -51,8 +55,8 @@ async function refreshFeatures(userId, reason) { logger.debug({ userId, features, reason }, 'updating user features') const matchedFeatureSet = FeaturesHelper.getMatchedFeatureSet(features) - AnalyticsManager.setUserPropertyForUserInBackground( - userId, + AnalyticsManager.setUserPropertyForMongoUserInBackground( + user, 'feature-set', matchedFeatureSet ) diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.mjs b/services/web/app/src/Features/Subscription/SubscriptionController.mjs index 989c780fca..b9f4b597ad 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionController.mjs @@ -427,8 +427,8 @@ async function pauseSubscription(req, res, next) { const { subscription } = await LimitationsManager.promises.userHasSubscription(user) - AnalyticsManager.recordEventForUserInBackground( - user._id, + AnalyticsManager.recordEventForSession( + req.session, 'subscription-pause-scheduled', { pause_length: pauseCycles, diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessController.mjs b/services/web/app/src/Features/TokenAccess/TokenAccessController.mjs index b880829982..ae7a670949 100644 --- a/services/web/app/src/Features/TokenAccess/TokenAccessController.mjs +++ b/services/web/app/src/Features/TokenAccess/TokenAccessController.mjs @@ -342,7 +342,7 @@ async function grantTokenAccessReadAndWrite(req, res, next) { ...(pendingEditor && { pendingEditor: true }), } ) - AnalyticsManager.recordEventForUserInBackground(userId, 'project-joined', { + AnalyticsManager.recordEventForSession(req.session, 'project-joined', { role: pendingEditor ? PrivilegeLevels.READ_ONLY : PrivilegeLevels.READ_AND_WRITE, @@ -452,7 +452,8 @@ async function grantTokenAccessReadOnly(req, res, next) { await TokenAccessHandler.promises.addReadOnlyUserToProject( userId, project._id, - project.owner_ref + project.owner_ref, + req.session ) return res.json({ diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessHandler.mjs b/services/web/app/src/Features/TokenAccess/TokenAccessHandler.mjs index 5d0c004dfc..c76d572e5d 100644 --- a/services/web/app/src/Features/TokenAccess/TokenAccessHandler.mjs +++ b/services/web/app/src/Features/TokenAccess/TokenAccessHandler.mjs @@ -154,13 +154,13 @@ const TokenAccessHandler = { throw new Error('invalid token type') }, - async addReadOnlyUserToProject(userId, projectId, ownerId) { + async addReadOnlyUserToProject(userId, projectId, ownerId, session) { if (!Features.hasFeature('link-sharing')) { throw new Error('link sharing is disabled') } userId = new ObjectId(userId.toString()) projectId = new ObjectId(projectId.toString()) - Analytics.recordEventForUserInBackground(userId, 'project-joined', { + Analytics.recordEventForSession(session, 'project-joined', { role: PrivilegeLevels.READ_ONLY, projectId: projectId.toString(), source: 'link-sharing', diff --git a/services/web/app/src/Features/Uploads/ProjectUploadController.mjs b/services/web/app/src/Features/Uploads/ProjectUploadController.mjs index cecbac5ea2..9fa4cffcd0 100644 --- a/services/web/app/src/Features/Uploads/ProjectUploadController.mjs +++ b/services/web/app/src/Features/Uploads/ProjectUploadController.mjs @@ -210,16 +210,12 @@ async function importDocument(req, res, next) { archivePath ) await ProjectOptionsHandler.promises.setCompiler(project._id, 'lualatex') - AnalyticsManager.recordEventForUserInBackground( - userId, - 'convert-format', - { - sourceFormat: conversionType, - targetFormat: 'latex', - status: 'success', - operation: 'import', - } - ) + AnalyticsManager.recordEventForSession(req.session, 'convert-format', { + sourceFormat: conversionType, + targetFormat: 'latex', + status: 'success', + operation: 'import', + }) res.json({ success: true, project_id: project._id }) } finally { await fsPromises.unlink(archivePath).catch(unlinkErr => { @@ -230,7 +226,7 @@ async function importDocument(req, res, next) { }) } } catch (error) { - AnalyticsManager.recordEventForUserInBackground(userId, 'convert-format', { + AnalyticsManager.recordEventForSession(req.session, 'convert-format', { sourceFormat: conversionType, targetFormat: 'latex', status: 'failure', diff --git a/services/web/app/src/Features/User/UserCreator.mjs b/services/web/app/src/Features/User/UserCreator.mjs index 1171678252..379858e739 100644 --- a/services/web/app/src/Features/User/UserCreator.mjs +++ b/services/web/app/src/Features/User/UserCreator.mjs @@ -43,8 +43,8 @@ async function recordRegistrationEvent(user) { if (user.thirdPartyIdentifiers && user.thirdPartyIdentifiers.length > 0) { segmentation.provider = user.thirdPartyIdentifiers[0].providerId } - Analytics.recordEventForUserInBackground( - user._id, + Analytics.recordEventForMongoUserInBackground( + user, 'user-registered', segmentation ) @@ -110,11 +110,11 @@ async function createNewUser(attributes, options = {}) { } await recordRegistrationEvent(user) - await Analytics.setUserPropertyForUser(user._id, 'created-at', new Date()) - await Analytics.setUserPropertyForUser(user._id, 'user-id', user._id) + await Analytics.setUserPropertyForMongoUser(user, 'created-at', new Date()) + await Analytics.setUserPropertyForMongoUser(user, 'user-id', user._id) if (attributes.analyticsId) { - await Analytics.setUserPropertyForUser( - user._id, + await Analytics.setUserPropertyForMongoUser( + user, 'analytics-id', attributes.analyticsId ) diff --git a/services/web/app/src/Features/User/UserEmailsController.mjs b/services/web/app/src/Features/User/UserEmailsController.mjs index cf9e8adf74..6ece5b63fe 100644 --- a/services/web/app/src/Features/User/UserEmailsController.mjs +++ b/services/web/app/src/Features/User/UserEmailsController.mjs @@ -247,15 +247,11 @@ const _checkConfirmationCode = delete req.session[sessionKey] - AnalyticsManager.recordEventForUserInBackground( - user._id, - 'email-verified', - { - provider: 'email', - verification_type: 'token', - isPrimary: user.email === emailToCheck, - } - ) + AnalyticsManager.recordEventForSession(req.session, 'email-verified', { + provider: 'email', + verification_type: 'token', + isPrimary: user.email === emailToCheck, + }) const redirectUrl = AuthenticationController.getRedirectFromSession(req) || '/project' @@ -391,16 +387,14 @@ const resendExistingSecondaryEmailConfirmationCode = _resendConfirmationCode( ) async function confirmSecondaryEmailPage(req, res) { - const userId = SessionManager.getLoggedInUserId(req.session) - if (!req.session.pendingSecondaryEmail) { const redirectURL = AuthenticationController.getRedirectFromSession(req) || '/project' return res.redirect(redirectURL) } - AnalyticsManager.recordEventForUserInBackground( - userId, + AnalyticsManager.recordEventForSession( + req.session, 'confirm-secondary-email-page-displayed' ) @@ -421,8 +415,8 @@ async function addSecondaryEmailPage(req, res) { return res.redirect(redirectURL) } - AnalyticsManager.recordEventForUserInBackground( - userId, + AnalyticsManager.recordEventForSession( + req.session, 'add-secondary-email-page-displayed' ) @@ -442,8 +436,8 @@ async function primaryEmailCheckPage(req, res) { return res.redirect('/project') } - AnalyticsManager.recordEventForUserInBackground( - userId, + AnalyticsManager.recordEventForSession( + req.session, 'primary-email-check-page-displayed' ) @@ -456,8 +450,8 @@ async function primaryEmailCheck(req, res) { $set: { lastPrimaryEmailCheck: new Date() }, }) - AnalyticsManager.recordEventForUserInBackground( - userId, + AnalyticsManager.recordEventForSession( + req.session, 'primary-email-check-done' ) @@ -687,7 +681,7 @@ const UserEmailsController = { } UserGetter.getUser( userData.userId, - { email: 1 }, + { _id: 1, email: 1, analyticsId: 1, labsProgram: 1 }, function (error, user) { if (error) { logger.error( @@ -696,8 +690,8 @@ const UserEmailsController = { ) } const isPrimary = user?.email === userData.email - AnalyticsManager.recordEventForUserInBackground( - userData.userId, + AnalyticsManager.recordEventForMongoUserInBackground( + user, 'email-verified', { provider: 'email', diff --git a/services/web/app/src/Features/User/UserPostRegistrationAnalyticsManager.mjs b/services/web/app/src/Features/User/UserPostRegistrationAnalyticsManager.mjs index a1c55f9fe2..22012167e6 100644 --- a/services/web/app/src/Features/User/UserPostRegistrationAnalyticsManager.mjs +++ b/services/web/app/src/Features/User/UserPostRegistrationAnalyticsManager.mjs @@ -14,24 +14,28 @@ async function schedulePostRegistrationAnalytics(user) { } async function postRegistrationAnalytics(userId) { - const user = await UserGetter.promises.getUser({ _id: userId }, { email: 1 }) + const user = await UserGetter.promises.getUser( + { _id: userId }, + { email: 1, _id: 1, analyticsId: 1, labsProgram: 1 } + ) if (!user) { return } - await checkAffiliations(userId) + await checkAffiliations(user) } -async function checkAffiliations(userId) { - const affiliationsData = - await InstitutionsAPI.promises.getUserAffiliations(userId) +async function checkAffiliations(user) { + const affiliationsData = await InstitutionsAPI.promises.getUserAffiliations( + user._id.toString() + ) const hasCommonsAccountAffiliation = affiliationsData.some( affiliationData => affiliationData.institution && affiliationData.institution.commonsAccount ) if (hasCommonsAccountAffiliation) { - await AnalyticsManager.setUserPropertyForUser( - userId, + await AnalyticsManager.setUserPropertyForMongoUser( + user, 'registered-from-commons-account', true ) diff --git a/services/web/app/src/infrastructure/rate-limiters/TokenUsageRateLimiter.mjs b/services/web/app/src/infrastructure/rate-limiters/TokenUsageRateLimiter.mjs index 0bc0ebbf1d..3ac2fa27d1 100644 --- a/services/web/app/src/infrastructure/rate-limiters/TokenUsageRateLimiter.mjs +++ b/services/web/app/src/infrastructure/rate-limiters/TokenUsageRateLimiter.mjs @@ -166,10 +166,11 @@ export default class TokenUsageRateLimiter { /** * * @param {string} userId + * @param {import('express').Request} req * @param {import('express').Response} res * @param {{ auditLogTool?: string }} [options] - if `auditLogTool` is set, an `ai-quota-breach` audit log entry is written with `{ tool }` in the info payload when the request is blocked */ - async checkUsage(userId, res, options = {}) { + async checkUsage(userId, req, res, options = {}) { const allowance = await this._getAllowance(userId) const currentUsage = await this.getCurrentUsage(userId) const periodStart = currentUsage.periodStart ?? new Date() @@ -190,8 +191,8 @@ export default class TokenUsageRateLimiter { ) } - await AnalyticsManager.recordEventForUser( - userId, + AnalyticsManager.recordEventForSession( + req.session, 'ai-token-usage-limit-exceeded' ) diff --git a/services/web/scripts/remove_unwanted_ieee_collabratec_users.mjs b/services/web/scripts/remove_unwanted_ieee_collabratec_users.mjs index 5b35576d79..81b37e7a91 100644 --- a/services/web/scripts/remove_unwanted_ieee_collabratec_users.mjs +++ b/services/web/scripts/remove_unwanted_ieee_collabratec_users.mjs @@ -58,6 +58,8 @@ async function getIEEEUsers() { _id: 1, teamName: 1, 'member_details._id': 1, + 'member_details.analyticsId': 1, + 'member_details.labsProgram': 1, 'member_details.email': 1, 'member_details.emails.email': 1, }, @@ -122,8 +124,8 @@ async function main() { index = index + 1 if (COMMIT) { - await AnalyticsManager.setUserPropertyForUser( - userDetails._id.toString(), + await AnalyticsManager.setUserPropertyForMongoUser( + userDetails, 'ieee-retirement', true ) diff --git a/services/web/test/unit/src/Analytics/AnalyticsManager.test.mjs b/services/web/test/unit/src/Analytics/AnalyticsManager.test.mjs index 85a260371c..c0206eb838 100644 --- a/services/web/test/unit/src/Analytics/AnalyticsManager.test.mjs +++ b/services/web/test/unit/src/Analytics/AnalyticsManager.test.mjs @@ -438,6 +438,7 @@ describe('AnalyticsManager', function () { ctx.req.session.user = { _id: ctx.userId, analyticsId: ctx.analyticsId, + labsProgram: false, } await ctx.AnalyticsManager.analyticsIdMiddleware(ctx.req, ctx.res, () => { assert.equal(ctx.analyticsId, ctx.req.session.analyticsId) @@ -445,10 +446,14 @@ describe('AnalyticsManager', function () { }) it('sets session.analyticsId with a legacy user session without an analyticsId', async function (ctx) { - ctx.UserAnalyticsDataCache.getAnalyticsId.resolves(ctx.userId) + ctx.UserAnalyticsDataCache.getAnalyticsData.resolves({ + analyticsId: ctx.userId, + labsProgram: false, + }) ctx.req.session.user = { _id: ctx.userId, analyticsId: undefined, + labsProgram: false, } await ctx.AnalyticsManager.analyticsIdMiddleware(ctx.req, ctx.res, () => { assert.equal(ctx.userId, ctx.req.session.analyticsId) @@ -456,10 +461,14 @@ describe('AnalyticsManager', function () { }) it('updates session.analyticsId with a legacy user session without an analyticsId if different', async function (ctx) { - ctx.UserAnalyticsDataCache.getAnalyticsId.resolves(ctx.userId) + ctx.UserAnalyticsDataCache.getAnalyticsData.resolves({ + analyticsId: ctx.userId, + labsProgram: false, + }) ctx.req.session.user = { _id: ctx.userId, analyticsId: undefined, + labsProgram: false, } ctx.req.analyticsId = 'foo' ctx.AnalyticsManager.analyticsIdMiddleware(ctx.req, ctx.res, () => { @@ -467,11 +476,30 @@ describe('AnalyticsManager', function () { }) }) + it('updates session.user.labsProgram when not defined', async function (ctx) { + ctx.UserAnalyticsDataCache.getAnalyticsData.resolves({ + analyticsId: ctx.userId, + labsProgram: false, + }) + ctx.req.session.user = { + _id: ctx.userId, + analyticsId: ctx.userId, + } + ctx.req.session.analyticsId = ctx.userId + ctx.AnalyticsManager.analyticsIdMiddleware(ctx.req, ctx.res, () => { + assert.equal(ctx.req.session.user.labsProgram, false) + }) + }) + it('does not update session.analyticsId with a legacy user session without an analyticsId if same', async function (ctx) { - ctx.UserAnalyticsDataCache.getAnalyticsId.resolves(ctx.userId) + ctx.UserAnalyticsDataCache.getAnalyticsData.resolves({ + analyticsId: ctx.userId, + labsProgram: false, + }) ctx.req.session.user = { _id: ctx.userId, analyticsId: undefined, + labsProgram: false, } ctx.req.analyticsId = ctx.userId await ctx.AnalyticsManager.analyticsIdMiddleware(ctx.req, ctx.res, () => { diff --git a/services/web/test/unit/src/Authentication/AuthenticationController.test.mjs b/services/web/test/unit/src/Authentication/AuthenticationController.test.mjs index bafac07c38..4f08479fe9 100644 --- a/services/web/test/unit/src/Authentication/AuthenticationController.test.mjs +++ b/services/web/test/unit/src/Authentication/AuthenticationController.test.mjs @@ -144,7 +144,7 @@ describe('AuthenticationController', function () { '../../../../app/src/Features/Analytics/AnalyticsManager', () => ({ default: (ctx.AnalyticsManager = { - recordEventForUserInBackground: sinon.stub(), + recordEventForMongoUserInBackground: sinon.stub(), identifyUser: sinon.stub(), getIdsFromSession: sinon.stub().returns({ userId: ctx.user._id }), }), @@ -1646,8 +1646,8 @@ describe('AuthenticationController', function () { it('should track the login event', function (ctx) { sinon.assert.calledWith( - ctx.AnalyticsManager.recordEventForUserInBackground, - ctx.user._id, + ctx.AnalyticsManager.recordEventForMongoUserInBackground, + ctx.user, 'user-logged-in' ) }) diff --git a/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs b/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs index 640838582a..1c62596706 100644 --- a/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs +++ b/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs @@ -34,11 +34,12 @@ describe('BetaProgramHandler', function () { '../../../../app/src/Features/Analytics/AnalyticsManager', () => ({ default: (ctx.AnalyticsManager = { - setUserPropertyForUserInBackground: sinon.stub(), + setUserPropertyForSessionInBackground: sinon.stub(), }), }) ) + ctx.session = {} ctx.handler = (await import(modulePath)).default }) @@ -46,7 +47,7 @@ describe('BetaProgramHandler', function () { beforeEach(function (ctx) { ctx.user.betaProgram = false ctx.call = callback => { - ctx.handler.optIn(ctx.user_id, callback) + ctx.handler.optIn(ctx.session, ctx.user_id, callback) } }) @@ -65,8 +66,8 @@ describe('BetaProgramHandler', function () { ctx.call(err => { expect(err).to.not.exist sinon.assert.calledWith( - ctx.AnalyticsManager.setUserPropertyForUserInBackground, - ctx.user_id, + ctx.AnalyticsManager.setUserPropertyForSessionInBackground, + ctx.session, 'beta-program', true ) @@ -105,7 +106,7 @@ describe('BetaProgramHandler', function () { beforeEach(function (ctx) { ctx.user.betaProgram = true ctx.call = callback => { - ctx.handler.optOut(ctx.user_id, callback) + ctx.handler.optOut(ctx.session, ctx.user_id, callback) } }) @@ -124,8 +125,8 @@ describe('BetaProgramHandler', function () { ctx.call(err => { expect(err).to.not.exist sinon.assert.calledWith( - ctx.AnalyticsManager.setUserPropertyForUserInBackground, - ctx.user_id, + ctx.AnalyticsManager.setUserPropertyForSessionInBackground, + ctx.session, 'beta-program', false ) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs index f8c4249b5a..b8d965b9d3 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs @@ -51,7 +51,7 @@ describe('CollaboratorsInviteController', function () { getSessionUser: sinon.stub().returns(ctx.currentUser), } - ctx.AnalyticsManger = { recordEventForUserInBackground: sinon.stub() } + ctx.AnalyticsManger = { recordEventForSession: sinon.stub() } ctx.rateLimiter = { consume: sinon.stub().resolves(), @@ -1756,8 +1756,8 @@ describe('CollaboratorsInviteController', function () { ) }) - ctx.AnalyticsManger.recordEventForUserInBackground.should.have.been.calledWith( - ctx.currentUser._id, + ctx.AnalyticsManger.recordEventForSession.should.have.been.calledWith( + ctx.req.session, 'project-joined', sinon.match({ source: 'sharing-link' }) ) diff --git a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs index c65f86186b..1c1d337176 100644 --- a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs +++ b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs @@ -104,6 +104,7 @@ describe('ProjectDownloadsController', function () { '../../../../app/src/Features/Analytics/AnalyticsManager.mjs', () => ({ default: (ctx.AnalyticsManager = { + recordEventForSession: sinon.stub(), recordEventForUserInBackground: sinon.stub(), }), }) @@ -369,8 +370,8 @@ describe('ProjectDownloadsController', function () { it('should record a successful convert-format analytics event', function (ctx) { sinon.assert.calledWith( - ctx.AnalyticsManager.recordEventForUserInBackground, - ctx.userId, + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, 'convert-format', { sourceFormat: 'latex', diff --git a/services/web/test/unit/src/Project/ProjectController.test.mjs b/services/web/test/unit/src/Project/ProjectController.test.mjs index 8de721ded5..95c157178c 100644 --- a/services/web/test/unit/src/Project/ProjectController.test.mjs +++ b/services/web/test/unit/src/Project/ProjectController.test.mjs @@ -439,8 +439,10 @@ describe('ProjectController', function () { '../../../../app/src/Features/Analytics/AnalyticsManager', () => ({ default: { + recordEventForSession: () => {}, recordEventForUserInBackground: () => {}, setUserPropertyForUserInBackground: () => {}, + setUserPropertyForSessionInBackground: () => {}, }, }) ) diff --git a/services/web/test/unit/src/Project/ProjectListController.test.mjs b/services/web/test/unit/src/Project/ProjectListController.test.mjs index d7c9746b84..62adf8b7b4 100644 --- a/services/web/test/unit/src/Project/ProjectListController.test.mjs +++ b/services/web/test/unit/src/Project/ProjectListController.test.mjs @@ -13,6 +13,7 @@ vi.mock('../../../../app/src/Features/Analytics/AnalyticsManager.mjs', () => { return { default: { setUserPropertyForUserInBackground: () => {}, + setUserPropertyForSessionInBackground: () => {}, }, } }) diff --git a/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs b/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs index fd3ef1c0e4..596b40b186 100644 --- a/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs +++ b/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs @@ -130,7 +130,7 @@ describe('FeaturesUpdater', function () { .resolves(ctx.user) ctx.AnalyticsManager = { - setUserPropertyForUserInBackground: sinon.stub(), + setUserPropertyForMongoUserInBackground: sinon.stub(), } ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves([]) } }, @@ -443,8 +443,8 @@ describe('FeaturesUpdater', function () { it('should send the corresponding feature set user property', function (ctx) { expect( - ctx.AnalyticsManager.setUserPropertyForUserInBackground - ).to.have.been.calledWith(ctx.user._id, 'feature-set', 'all') + ctx.AnalyticsManager.setUserPropertyForMongoUserInBackground + ).to.have.been.calledWith(ctx.user, 'feature-set', 'all') }) it('should sync subscription properties to customer.io', function (ctx) { @@ -919,8 +919,8 @@ describe('FeaturesUpdater', function () { it('should send mixed feature set user property', function (ctx) { sinon.assert.calledWith( - ctx.AnalyticsManager.setUserPropertyForUserInBackground, - ctx.user._id, + ctx.AnalyticsManager.setUserPropertyForMongoUserInBackground, + ctx.user, 'feature-set', 'mixed' ) diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs b/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs index 6283e67e3d..ac0aec3fad 100644 --- a/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs +++ b/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs @@ -329,8 +329,8 @@ describe('TokenAccessController', function () { it('records a project-joined event for the user', function (ctx) { expect( - ctx.AnalyticsManager.recordEventForUserInBackground - ).to.have.been.calledWith(ctx.user._id, 'project-joined', { + ctx.AnalyticsManager.recordEventForSession + ).to.have.been.calledWith(ctx.req.session, 'project-joined', { mode: 'edit', projectId: ctx.project._id.toString(), ownerId: ctx.project.owner_ref.toString(), @@ -406,8 +406,8 @@ describe('TokenAccessController', function () { it('records a project-joined event for the user', function (ctx) { expect( - ctx.AnalyticsManager.recordEventForUserInBackground - ).to.have.been.calledWith(ctx.user._id, 'project-joined', { + ctx.AnalyticsManager.recordEventForSession + ).to.have.been.calledWith(ctx.req.session, 'project-joined', { mode: 'view', projectId: ctx.project._id.toString(), pendingEditor: true, @@ -863,7 +863,8 @@ describe('TokenAccessController', function () { ).to.have.been.calledWith( ctx.user._id, ctx.project._id, - ctx.project.owner_ref + ctx.project.owner_ref, + ctx.req.session ) }) diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessHandler.test.mjs b/services/web/test/unit/src/TokenAccess/TokenAccessHandler.test.mjs index ce949ff814..cc572c07db 100644 --- a/services/web/test/unit/src/TokenAccess/TokenAccessHandler.test.mjs +++ b/services/web/test/unit/src/TokenAccess/TokenAccessHandler.test.mjs @@ -59,7 +59,7 @@ describe('TokenAccessHandler', function () { '../../../../app/src/Features/Analytics/AnalyticsManager', () => ({ default: (ctx.Analytics = { - recordEventForUserInBackground: sinon.stub(), + recordEventForSession: sinon.stub(), }), }) ) @@ -153,7 +153,8 @@ describe('TokenAccessHandler', function () { await ctx.TokenAccessHandler.promises.addReadOnlyUserToProject( ctx.userId, ctx.projectId, - ctx.project.owner_ref + ctx.project.owner_ref, + ctx.req.session ) expect(ctx.Project.updateOne.callCount).to.equal(1) expect( @@ -165,8 +166,8 @@ describe('TokenAccessHandler', function () { 'tokenAccessReadOnly_refs' ) sinon.assert.calledWith( - ctx.Analytics.recordEventForUserInBackground, - ctx.userId, + ctx.Analytics.recordEventForSession, + ctx.req.session, 'project-joined', { mode: 'view', @@ -516,7 +517,8 @@ describe('TokenAccessHandler', function () { ctx.TokenAccessHandler.promises.addReadOnlyUserToProject( ctx.userId, ctx.projectId, - ctx.project.owner_ref + ctx.project.owner_ref, + ctx.req.session ) ).to.be.rejectedWith('link sharing is disabled') expect(ctx.Project.updateOne.callCount).to.equal(0) diff --git a/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs b/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs index b12630052c..367b246370 100644 --- a/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs +++ b/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs @@ -120,6 +120,7 @@ describe('ProjectUploadController', function () { ) ctx.AnalyticsManager = { + recordEventForSession: sinon.stub(), recordEventForUserInBackground: sinon.stub(), } vi.doMock( @@ -545,8 +546,8 @@ describe('ProjectUploadController', function () { it('should record a successful convert-format analytics event', function (ctx) { sinon.assert.calledWith( - ctx.AnalyticsManager.recordEventForUserInBackground, - ctx.user_id, + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, 'convert-format', { sourceFormat: 'docx', @@ -615,8 +616,8 @@ describe('ProjectUploadController', function () { it('should record a successful convert-format analytics event', function (ctx) { sinon.assert.calledWith( - ctx.AnalyticsManager.recordEventForUserInBackground, - ctx.user_id, + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, 'convert-format', { sourceFormat: 'markdown', @@ -687,8 +688,8 @@ describe('ProjectUploadController', function () { it('should record a failed convert-format analytics event', function (ctx) { sinon.assert.calledWith( - ctx.AnalyticsManager.recordEventForUserInBackground, - ctx.user_id, + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, 'convert-format', { sourceFormat: 'docx', diff --git a/services/web/test/unit/src/User/UserCreator.test.mjs b/services/web/test/unit/src/User/UserCreator.test.mjs index a0fdddd0cf..497b93372f 100644 --- a/services/web/test/unit/src/User/UserCreator.test.mjs +++ b/services/web/test/unit/src/User/UserCreator.test.mjs @@ -6,7 +6,7 @@ const modulePath = '../../../../app/src/Features/User/UserCreator.mjs' describe('UserCreator', function () { beforeEach(async function (ctx) { const self = ctx - ctx.user = { _id: '12390i', ace: {} } + ctx.user = { _id: '12390i', ace: {}, analyticsId: 'uuid' } ctx.user.save = sinon.stub().resolves(self.user) ctx.UserModel = class Project { constructor() { @@ -64,8 +64,9 @@ describe('UserCreator', function () { '../../../../app/src/Features/Analytics/AnalyticsManager', () => ({ default: (ctx.Analytics = { + recordEventForMongoUserInBackground: sinon.stub(), recordEventForUserInBackground: sinon.stub(), - setUserPropertyForUser: sinon.stub(), + setUserPropertyForMongoUser: sinon.stub(), }), }) ) @@ -320,13 +321,13 @@ describe('UserCreator', function () { ) assert.equal(user.email, ctx.email) sinon.assert.calledWith( - ctx.Analytics.recordEventForUserInBackground, - user._id, + ctx.Analytics.recordEventForMongoUserInBackground, + user, 'user-registered' ) sinon.assert.calledWith( - ctx.Analytics.setUserPropertyForUser, - user._id, + ctx.Analytics.setUserPropertyForMongoUser, + user, 'created-at' ) }) diff --git a/services/web/test/unit/src/User/UserEmailsController.test.mjs b/services/web/test/unit/src/User/UserEmailsController.test.mjs index 4171257078..72d7f9388d 100644 --- a/services/web/test/unit/src/User/UserEmailsController.test.mjs +++ b/services/web/test/unit/src/User/UserEmailsController.test.mjs @@ -65,7 +65,8 @@ describe('UserEmailsController', function () { } ctx.HttpErrorHandler = { conflict: vi.fn() } ctx.AnalyticsManager = { - recordEventForUserInBackground: vi.fn(), + recordEventForSession: vi.fn(), + recordEventForMongoUserInBackground: vi.fn(), } ctx.UserAuditLogHandler = { addEntry: vi.fn((userId, op, initiatorId, ip, info, callback) => @@ -810,13 +811,15 @@ describe('UserEmailsController', function () { ctx.req, { json: vi.fn() } ) - expect( - ctx.AnalyticsManager.recordEventForUserInBackground - ).toHaveBeenCalledWith(ctx.user._id, 'email-verified', { - provider: 'email', - verification_type: 'token', - isPrimary: ctx.user.email === ctx.email, - }) + expect(ctx.AnalyticsManager.recordEventForSession).toHaveBeenCalledWith( + ctx.req.session, + 'email-verified', + { + provider: 'email', + verification_type: 'token', + isPrimary: ctx.user.email === ctx.email, + } + ) }) it('removes pendingExistingEmail from session', async function (ctx) { diff --git a/services/web/test/unit/src/User/UserPostRegistrationAnalyticsManager.test.mjs b/services/web/test/unit/src/User/UserPostRegistrationAnalyticsManager.test.mjs index 3d1cf859b1..2ce0594e85 100644 --- a/services/web/test/unit/src/User/UserPostRegistrationAnalyticsManager.test.mjs +++ b/services/web/test/unit/src/User/UserPostRegistrationAnalyticsManager.test.mjs @@ -15,16 +15,17 @@ describe('UserPostRegistrationAnalyticsManager', function () { getUser: sinon.stub().resolves(), }, } + ctx.fakeUser = { _id: ctx.fakeUserId, analyticsId: 'uuid' } ctx.UserGetter.promises.getUser .withArgs({ _id: ctx.fakeUserId }) - .resolves({ _id: ctx.fakeUserId }) + .resolves(ctx.fakeUser) ctx.InstitutionsAPI = { promises: { getUserAffiliations: sinon.stub().resolves([]), }, } ctx.AnalyticsManager = { - setUserPropertyForUser: sinon.stub().resolves(), + setUserPropertyForMongoUser: sinon.stub().resolves(), } vi.doMock('../../../../app/src/infrastructure/Queues', () => ({ @@ -77,7 +78,7 @@ describe('UserPostRegistrationAnalyticsManager', function () { ) expect(ctx.InstitutionsAPI.promises.getUserAffiliations).not.to.have.been .called - expect(ctx.AnalyticsManager.setUserPropertyForUser).not.to.have.been + expect(ctx.AnalyticsManager.setUserPropertyForMongoUser).not.to.have.been .called }) @@ -99,9 +100,9 @@ describe('UserPostRegistrationAnalyticsManager', function () { ctx.fakeUserId ) expect( - ctx.AnalyticsManager.setUserPropertyForUser + ctx.AnalyticsManager.setUserPropertyForMongoUser ).to.have.been.calledWith( - ctx.fakeUserId, + ctx.fakeUser, 'registered-from-commons-account', true ) @@ -118,7 +119,7 @@ describe('UserPostRegistrationAnalyticsManager', function () { await ctx.UserPostRegistrationAnalyticsManager.postRegistrationAnalytics( ctx.fakeUserId ) - expect(ctx.AnalyticsManager.setUserPropertyForUser).not.to.have.been + expect(ctx.AnalyticsManager.setUserPropertyForMongoUser).not.to.have.been .called }) }) diff --git a/services/web/test/unit/src/infrastructure/WorkbenchRateLimiter.sequential.test.mjs b/services/web/test/unit/src/infrastructure/WorkbenchRateLimiter.sequential.test.mjs index 131442ecb7..a7569734da 100644 --- a/services/web/test/unit/src/infrastructure/WorkbenchRateLimiter.sequential.test.mjs +++ b/services/web/test/unit/src/infrastructure/WorkbenchRateLimiter.sequential.test.mjs @@ -90,7 +90,7 @@ describe('WorkbenchRateLimiter', function () { '../../../../app/src/Features/Analytics/AnalyticsManager', () => ({ default: { - recordEventForUser: sinon.stub(), + recordEventForSession: sinon.stub(), }, }) ) @@ -164,6 +164,7 @@ describe('WorkbenchRateLimiter', function () { describe('with no data', function () { beforeEach(async function (ctx) { await UserFeatureUsage.deleteMany({}).exec() + ctx.req = { session: {} } ctx.res = { set: sinon.stub(), headersSent: false, @@ -172,12 +173,16 @@ describe('WorkbenchRateLimiter', function () { it('should not throw', async function (ctx) { await expect( - ctx.WorkbenchRateLimiter.checkUsage(ctx.alphaUserId, ctx.res) + ctx.WorkbenchRateLimiter.checkUsage(ctx.alphaUserId, ctx.req, ctx.res) ).to.eventually.be.fulfilled }) it('sets rate limit headers', async function (ctx) { - await ctx.WorkbenchRateLimiter.checkUsage(ctx.alphaUserId, ctx.res) + await ctx.WorkbenchRateLimiter.checkUsage( + ctx.alphaUserId, + ctx.req, + ctx.res + ) expect(ctx.res.set).to.have.been.calledWith( 'Token-RateLimit-Limit', '8000000' @@ -197,6 +202,7 @@ describe('WorkbenchRateLimiter', function () { describe('with existing usage', function () { beforeEach(async function (ctx) { await UserFeatureUsage.deleteMany({}).exec() + ctx.req = { session: {} } ctx.res = { set: sinon.stub(), headersSent: false, @@ -215,12 +221,16 @@ describe('WorkbenchRateLimiter', function () { it('should not throw if under limit', async function (ctx) { await expect( - ctx.WorkbenchRateLimiter.checkUsage(ctx.alphaUserId, ctx.res) + ctx.WorkbenchRateLimiter.checkUsage(ctx.alphaUserId, ctx.req, ctx.res) ).to.eventually.be.fulfilled }) it('sets rate limit headers', async function (ctx) { - await ctx.WorkbenchRateLimiter.checkUsage(ctx.alphaUserId, ctx.res) + await ctx.WorkbenchRateLimiter.checkUsage( + ctx.alphaUserId, + ctx.req, + ctx.res + ) expect(ctx.res.set).to.have.been.calledWith( 'Token-RateLimit-Limit', '8000000' @@ -243,7 +253,7 @@ describe('WorkbenchRateLimiter', function () { await usageRecord.save() await expect( - ctx.WorkbenchRateLimiter.checkUsage(ctx.alphaUserId, ctx.res) + ctx.WorkbenchRateLimiter.checkUsage(ctx.alphaUserId, ctx.req, ctx.res) ).to.eventually.be.rejectedWith(/rate limit exceeded/i) }) }) @@ -269,12 +279,16 @@ describe('WorkbenchRateLimiter', function () { it('should not throw', async function (ctx) { await expect( - ctx.WorkbenchRateLimiter.checkUsage(ctx.alphaUserId, ctx.res) + ctx.WorkbenchRateLimiter.checkUsage(ctx.alphaUserId, ctx.req, ctx.res) ).to.eventually.be.fulfilled }) it('sets rate limit headers', async function (ctx) { - await ctx.WorkbenchRateLimiter.checkUsage(ctx.alphaUserId, ctx.res) + await ctx.WorkbenchRateLimiter.checkUsage( + ctx.alphaUserId, + ctx.req, + ctx.res + ) expect(ctx.res.set).to.have.been.calledWith( 'Token-RateLimit-Limit', '8000000' diff --git a/tools/migrations/20260615090000_back_fill_labsProgram.mjs b/tools/migrations/20260615090000_back_fill_labsProgram.mjs index ce54e0f579..b4be217c63 100644 --- a/tools/migrations/20260615090000_back_fill_labsProgram.mjs +++ b/tools/migrations/20260615090000_back_fill_labsProgram.mjs @@ -1,6 +1,6 @@ import { batchedUpdate } from '@overleaf/mongo-utils/batchedUpdate.js' -const tags = ['server-ce', 'server-pro', 'saas', 'nonblocking'] +const tags = ['server-ce', 'server-pro', 'saas'] const migrate = async client => { const { db } = client diff --git a/tools/migrations/20260616070000_back_fill_users_analyticsId.mjs b/tools/migrations/20260616070000_back_fill_users_analyticsId.mjs index c7327a55da..dd20bf234d 100644 --- a/tools/migrations/20260616070000_back_fill_users_analyticsId.mjs +++ b/tools/migrations/20260616070000_back_fill_users_analyticsId.mjs @@ -1,7 +1,7 @@ import { db } from './lib/mongodb.mjs' import { batchedUpdate } from '@overleaf/mongo-utils/batchedUpdate.js' -const tags = ['saas', 'server-ce', 'server-pro', 'nonblocking'] +const tags = ['saas', 'server-ce', 'server-pro'] const migrate = async () => { await batchedUpdate(db.users, { analyticsId: { $exists: false } }, [