Add AnalyticsManager function for registering email changes

GitOrigin-RevId: ad42703fdc186936866b44c47492e3f653658f4f
This commit is contained in:
Andrew Rumble
2025-08-29 08:05:12 +00:00
committed by Copybot
parent 4779b17ad1
commit ae504e8af5
3 changed files with 93 additions and 1 deletions
@@ -7,6 +7,7 @@ const crypto = require('crypto')
const _ = require('lodash')
const { expressify } = require('@overleaf/promise-utils')
const logger = require('@overleaf/logger')
const { createHash } = require('crypto')
const analyticsEventsQueue = Queues.getQueue('analytics-events')
const analyticsEditingSessionsQueue = Queues.getQueue(
@@ -18,6 +19,7 @@ const analyticsUserPropertiesQueue = Queues.getQueue(
const analyticsAccountMappingQueue = Queues.getQueue(
'analytics-account-mapping'
)
const analyticsEmailChangeQueue = Queues.getQueue('analytics-email-change')
const ONE_MINUTE_MS = 60 * 1000
@@ -148,6 +150,7 @@ function setUserPropertyForSessionInBackground(session, property, value) {
/**
* @typedef {(import('./types').AccountMapping)} AccountMapping
* @typedef {(import('./types').EmailChangePayload)} EmailChangePayload
*/
/**
@@ -193,6 +196,59 @@ function registerAccountMapping({
})
}
/**
* Registers operations relating to email addresses.
*
* Generally, this should be called through the EmailChangeHelper module to ensure
* that the right data is sent.
*
* This will be called whenever an email is created, updated or deleted.
*
* @param {EmailChangePayload} payload
*/
function registerEmailChange({
userId,
email,
action,
createdAt = new Date(),
emailCreatedAt,
isPrimary,
emailConfirmedAt,
emailDeletedAt,
}) {
Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'email-change' })
const salt = Settings.analytics?.hashedEmailSalt ?? ''
const hashedEmail = createHash('sha256')
.update(`${email}${salt}`)
.digest('hex')
analyticsEmailChangeQueue
.add('email-change', {
action,
createdAt,
emailDeletedAt,
email: hashedEmail,
emailConfirmedAt,
emailCreatedAt,
isPrimary,
userId,
})
.then(() => {
Metrics.analyticsQueue.inc({
status: 'added',
event_type: 'email-change',
})
})
.catch(() => {
Metrics.analyticsQueue.inc({
status: 'error',
event_type: 'email-change',
})
})
}
function updateEditingSession(userId, projectId, countryCode, segmentation) {
if (!userId) {
return
@@ -400,5 +456,6 @@ module.exports = {
updateEditingSession,
getIdsFromSession,
registerAccountMapping,
registerEmailChange,
analyticsIdMiddleware: expressify(analyticsIdMiddleware),
}
+11
View File
@@ -7,3 +7,14 @@ export type AccountMapping = {
targetEntityId: string
createdAt: string
}
export type EmailChangePayload = {
userId: string
email: string
isPrimary: boolean
action: 'created' | 'deleted' | 'updated'
createdAt: string
emailDeletedAt?: string | null
emailCreatedAt?: string | null
emailConfirmedAt?: string | null
}
@@ -16,7 +16,7 @@ describe('AnalyticsManager', function () {
this.fakeUserId = 'dbfc9438d14996f73dd172fb'
this.analyticsId = 'ecdb935a-52f3-4f91-aebc-7a70d2ffbb55'
this.Settings = {
analytics: { enabled: true },
analytics: { enabled: true, hashedEmailSalt: 'salt' },
}
this.analyticsEventsQueue = {
add: sinon.stub().resolves(),
@@ -307,6 +307,30 @@ describe('AnalyticsManager', function () {
message
)
})
it('email change', async function () {
const message = {
userId: this.fakeUserId,
email: 'test@example.com',
createdAt: '2021-01-01T00:00:00Z',
action: 'created',
emailCreatedAt: '2021-01-01T00:00:00Z',
isPrimary: false,
}
this.AnalyticsManager.registerEmailChange(message)
const convertedMessage = {
...message,
emailConfirmedAt: undefined,
emailDeletedAt: undefined,
email:
'1778d425d64c5259ef7b574a2488647eb51ca739a0b16bfa0e2e3e16fff362db', // sha256 hash of email + salt
}
sinon.assert.calledWithMatch(
this.analyticsEmailChangeQueue.add,
'email-change',
convertedMessage
)
})
})
describe('AnalyticsIdMiddleware', function () {