[web] add metrics for mongo access in split test system (#32920)

GitOrigin-RevId: cd93401bace60c003a63914e2898cf1f0defdabc
This commit is contained in:
Jakob Ackermann
2026-04-20 10:22:36 +02:00
committed by Copybot
parent ad79c85cea
commit 4ce5620b1d
11 changed files with 74 additions and 18 deletions
+1 -1
View File
@@ -75,4 +75,4 @@ class RequestLogger {
}
}
module.exports = { monitor, RequestLogger }
module.exports = { monitor, RequestLogger, getRoutePath }
@@ -65,7 +65,10 @@ async function recordEventForUser(userId, event, segmentation) {
if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
return
}
const analyticsId = await UserAnalyticsIdCache.get(userId)
const analyticsId = await UserAnalyticsIdCache.getWithMetrics(
userId,
`recordEventForUser:${event}`
)
if (analyticsId) {
_recordEvent({ analyticsId, userId, event, segmentation, isLoggedIn: true })
}
@@ -113,7 +116,10 @@ async function setUserPropertyForUser(userId, propertyName, propertyValue) {
_checkPropertyValue(propertyValue)
const analyticsId = await UserAnalyticsIdCache.get(userId)
const analyticsId = await UserAnalyticsIdCache.getWithMetrics(
userId,
`setUserPropertyForUser:${propertyName}`
)
if (analyticsId) {
await _setUserProperty({ analyticsId, propertyName, propertyValue })
}
@@ -447,7 +453,11 @@ async function analyticsIdMiddleware(req, res, next) {
const sessionUser = SessionManager.getSessionUser(session)
if (sessionUser) {
session.analyticsId = await UserAnalyticsIdCache.get(sessionUser._id)
session.analyticsId = await UserAnalyticsIdCache.getWithMetrics(
sessionUser._id,
// Do not drill down further, this middleware is on all endpoints.
'analyticsIdMiddleware'
)
} else if (!session.analyticsId) {
// generate an `analyticsId` if needed
session.analyticsId = crypto.randomUUID()
@@ -1,6 +1,7 @@
import UserGetter from '../User/UserGetter.mjs'
import { CacheLoader } from 'cache-flow'
import { callbackify } from 'node:util'
import Metrics from '@overleaf/metrics'
class UserAnalyticsIdCache extends CacheLoader {
constructor() {
@@ -22,6 +23,19 @@ class UserAnalyticsIdCache extends CacheLoader {
return userId.toString()
}
}
get() {
throw new Error('use UserAnalyticsIdCache.getWithMetrics')
}
async getWithMetrics(userId, path) {
const { value, cached } = await this.getWithMetadata(userId)
Metrics.inc('user_analytics_id_cache', 1, {
status: cached ? 'hit' : 'miss',
path,
})
return value
}
}
const userAnalyticsIdCache = new UserAnalyticsIdCache()
@@ -138,7 +138,10 @@ async function _getUserCompileLimits(userId) {
ownerFeatures.compileGroup = 'alpha'
}
const analyticsId = await UserAnalyticsIdCache.get(owner._id)
const analyticsId = await UserAnalyticsIdCache.getWithMetrics(
owner._id,
'_getUserCompileLimits'
)
const compileGroup =
ownerFeatures.compileGroup || Settings.defaultFeatures.compileGroup
@@ -120,7 +120,10 @@ async function getAssignmentForUser(
return _getNonSaasAssignment(splitTestName)
}
const analyticsId = await UserAnalyticsIdCache.get(userId)
const analyticsId = await UserAnalyticsIdCache.getWithMetrics(
userId,
`getAssignmentForUser:${splitTestName}`
)
return _getAssignment(splitTestName, { analyticsId, userId, sync })
} catch (error) {
logger.error({ err: error }, 'Failed to get split test assignment for user')
@@ -227,7 +230,11 @@ async function getActiveAssignmentsForUser(
return {}
}
const user = await SplitTestUserGetter.promises.getUser(userId)
const user = await SplitTestUserGetter.promises.getUser(
userId,
null,
'getActiveAssignmentsForUser'
)
if (user == null) {
return {}
}
@@ -402,7 +409,11 @@ async function _getAssignment(
user =
user ||
(userId &&
(await SplitTestUserGetter.promises.getUser(userId, splitTestName)))
(await SplitTestUserGetter.promises.getUser(
userId,
splitTestName,
`_getAssignment:${splitTestName}`
)))
const metadata = await _getAssignmentMetadata(analyticsId, user, splitTest)
const { activeForUser, selectedVariantName, phase, versionNumber } = metadata
@@ -651,7 +662,12 @@ async function _recordAssignment({
assignedAt: new Date(),
}
user =
user || (await SplitTestUserGetter.promises.getUser(userId, splitTestName))
user ||
(await SplitTestUserGetter.promises.getUser(
userId,
splitTestName,
`_recordAssignment:${splitTestName}`
))
if (user) {
const assignedSplitTests = user.splitTests || []
const assignmentLog = assignedSplitTests[splitTestName] || []
@@ -164,7 +164,13 @@ async function sessionMaintenance(req, user) {
Metrics.inc('split_test_session_maintenance', 1, { status: 'start' })
if (sessionUser) {
user = user || (await SplitTestUserGetter.promises.getUser(sessionUser._id))
user =
user ||
(await SplitTestUserGetter.promises.getUser(
sessionUser._id,
null,
`sessionMaintenance:${Metrics.http.getRoutePath(req)}`
))
if (
Boolean(sessionUser.alphaProgram) !== Boolean(user.alphaProgram) ||
Boolean(sessionUser.betaProgram) !== Boolean(user.betaProgram)
@@ -2,7 +2,8 @@ import { callbackify } from 'node:util'
import Metrics from '@overleaf/metrics'
import UserGetter from '../User/UserGetter.mjs'
async function getUser(id, splitTestName) {
async function getUser(id, splitTestName, path) {
Metrics.inc('split_test_get_user', 1, { path })
const projection = {
analyticsId: 1,
alphaProgram: 1,
@@ -437,7 +437,10 @@ async function processMigration(input, commit) {
}
// 7. If commit mode, perform migration
const analyticsId = await UserAnalyticsIdCache.get(overleafUserId)
const analyticsId = await UserAnalyticsIdCache.getWithMetrics(
overleafUserId,
'script' // no-op, metrics are not collected from scripts.
)
const mongoUser = await User.findOne({
_id: overleafUserId,
}).exec()
+3
View File
@@ -58,6 +58,9 @@ vi.mock('@overleaf/metrics', () => {
return 1
}
},
http: {
getRoutePath: sinon.stub().returns('project_Project_id'),
},
prom: { Counter: sinon.stub(), Histogram: sinon.stub() },
mongodb: { monitor: sinon.stub() },
},
@@ -92,7 +92,7 @@ describe('AnalyticsManager', function () {
'../../../../app/src/Features/Analytics/UserAnalyticsIdCache',
() => ({
default: (ctx.UserAnalyticsIdCache = {
get: sinon.stub().resolves(ctx.analyticsId),
getWithMetrics: sinon.stub().resolves(ctx.analyticsId),
}),
})
)
@@ -391,7 +391,7 @@ describe('AnalyticsManager', function () {
'../../../../app/src/Features/Analytics/UserAnalyticsIdCache',
() => ({
default: (ctx.UserAnalyticsIdCache = {
get: sinon.stub().resolves(ctx.analyticsId),
getWithMetrics: sinon.stub().resolves(ctx.analyticsId),
}),
})
)
@@ -439,7 +439,7 @@ describe('AnalyticsManager', function () {
})
it('sets session.analyticsId with a legacy user session without an analyticsId', async function (ctx) {
ctx.UserAnalyticsIdCache.get.resolves(ctx.userId)
ctx.UserAnalyticsIdCache.getWithMetrics.resolves(ctx.userId)
ctx.req.session.user = {
_id: ctx.userId,
analyticsId: undefined,
@@ -450,7 +450,7 @@ describe('AnalyticsManager', function () {
})
it('updates session.analyticsId with a legacy user session without an analyticsId if different', async function (ctx) {
ctx.UserAnalyticsIdCache.get.resolves(ctx.userId)
ctx.UserAnalyticsIdCache.getWithMetrics.resolves(ctx.userId)
ctx.req.session.user = {
_id: ctx.userId,
analyticsId: undefined,
@@ -462,7 +462,7 @@ describe('AnalyticsManager', function () {
})
it('does not update session.analyticsId with a legacy user session without an analyticsId if same', async function (ctx) {
ctx.UserAnalyticsIdCache.get.resolves(ctx.userId)
ctx.UserAnalyticsIdCache.getWithMetrics.resolves(ctx.userId)
ctx.req.session.user = {
_id: ctx.userId,
analyticsId: undefined,
@@ -68,7 +68,7 @@ describe('CompileManager', function () {
'../../../../app/src/Features/Analytics/UserAnalyticsIdCache',
() => ({
default: (ctx.UserAnalyticsIdCache = {
get: sinon.stub().resolves('abc'),
getWithMetrics: sinon.stub().resolves('abc'),
}),
})
)