mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[web] Expose metric for active users in SP (#20130)
* [web] Expose metric for active users in SP * Removed redundant UserHandler.setupLoginData() In the past this method was also calling a now deleted notifyDomainLicence(), but now this is just an alias for populateTeamInvites() * Added migration for `lastActive` * Added secondary read precedence to count active users GitOrigin-RevId: 86d6db31e1ae74ae40c6599e6acb731d8c4a04bd
This commit is contained in:
parent
8309671a8c
commit
3ff142d478
9 changed files with 142 additions and 21 deletions
|
@ -80,8 +80,13 @@ if (!module.parent) {
|
|||
})
|
||||
}
|
||||
|
||||
// monitor site maintenance file
|
||||
SiteAdminHandler.initialise()
|
||||
// initialise site admin tasks
|
||||
Promise.all([mongodb.waitForDb(), mongoose.connectionPromise])
|
||||
.then(() => SiteAdminHandler.initialise())
|
||||
.catch(err => {
|
||||
logger.fatal({ err }, 'Cannot connect to mongo. Exiting.')
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
// handle SIGTERM for graceful shutdown in kubernetes
|
||||
process.on('SIGTERM', function (signal) {
|
||||
|
|
|
@ -638,7 +638,7 @@ function _afterLoginSessionSetup(req, user, callback) {
|
|||
const _afterLoginSessionSetupAsync = promisify(_afterLoginSessionSetup)
|
||||
|
||||
function _loginAsyncHandlers(req, user, anonymousAnalyticsId, isNewUser) {
|
||||
UserHandler.setupLoginData(user, err => {
|
||||
UserHandler.populateTeamInvites(user, err => {
|
||||
if (err != null) {
|
||||
logger.warn({ err }, 'error setting up login data')
|
||||
}
|
||||
|
|
|
@ -1,17 +1,33 @@
|
|||
const { promisifyAll } = require('@overleaf/promise-utils')
|
||||
const { callbackify, promisify } = require('@overleaf/promise-utils')
|
||||
const TeamInvitesHandler = require('../Subscription/TeamInvitesHandler')
|
||||
const {
|
||||
db,
|
||||
READ_PREFERENCE_SECONDARY,
|
||||
} = require('../../infrastructure/mongodb')
|
||||
|
||||
const UserHandler = {
|
||||
populateTeamInvites(user, callback) {
|
||||
TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail(
|
||||
user.email,
|
||||
callback
|
||||
)
|
||||
},
|
||||
|
||||
setupLoginData(user, callback) {
|
||||
this.populateTeamInvites(user, callback)
|
||||
},
|
||||
function populateTeamInvites(user, callback) {
|
||||
TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail(
|
||||
user.email,
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
async function countActiveUsers() {
|
||||
const oneYearAgo = new Date()
|
||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1)
|
||||
return await db.users
|
||||
.find(
|
||||
{ lastActive: { $gte: oneYearAgo } },
|
||||
{ readPreference: READ_PREFERENCE_SECONDARY }
|
||||
)
|
||||
.count()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
populateTeamInvites,
|
||||
countActiveUsers: callbackify(countActiveUsers),
|
||||
}
|
||||
module.exports.promises = {
|
||||
populateTeamInvites: promisify(populateTeamInvites),
|
||||
countActiveUsers,
|
||||
}
|
||||
module.exports = UserHandler
|
||||
module.exports.promises = promisifyAll(UserHandler)
|
||||
|
|
|
@ -3,7 +3,11 @@ const settings = require('@overleaf/settings')
|
|||
const fs = require('fs')
|
||||
const {
|
||||
addOptionalCleanupHandlerAfterDrainingConnections,
|
||||
addRequiredCleanupHandlerBeforeDrainingConnections,
|
||||
} = require('./GracefulShutdown')
|
||||
const Features = require('./Features')
|
||||
const UserHandler = require('../Features/User/UserHandler')
|
||||
const metrics = require('@overleaf/metrics')
|
||||
|
||||
// Monitor a site maintenance file (e.g. /etc/site_status) periodically and
|
||||
// close the site if the file contents contain the string "closed".
|
||||
|
@ -42,6 +46,16 @@ function checkSiteMaintenanceFileSync() {
|
|||
updateSiteMaintenanceStatus(content)
|
||||
}
|
||||
|
||||
const SERVER_PRO_ACTIVE_USERS_METRIC_INTERVAL =
|
||||
settings.activeUserMetricInterval || 1000 * 60 * 60
|
||||
|
||||
function publishActiveUsersMetric() {
|
||||
UserHandler.promises
|
||||
.countActiveUsers()
|
||||
.then(activeUserCount => metrics.gauge('num_active_users', activeUserCount))
|
||||
.catch(error => logger.error({ error }, 'error counting active users'))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialise() {
|
||||
if (settings.enabledServices.includes('web') && statusFile) {
|
||||
|
@ -61,5 +75,18 @@ module.exports = {
|
|||
}
|
||||
)
|
||||
}
|
||||
if (!Features.hasFeature('saas')) {
|
||||
publishActiveUsersMetric()
|
||||
const intervalHandle = setInterval(
|
||||
publishActiveUsersMetric,
|
||||
SERVER_PRO_ACTIVE_USERS_METRIC_INTERVAL
|
||||
)
|
||||
addRequiredCleanupHandlerBeforeDrainingConnections(
|
||||
'publish server pro usage metrics',
|
||||
() => {
|
||||
clearInterval(intervalHandle)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
const Helpers = require('./lib/helpers')
|
||||
|
||||
exports.tags = ['server-ce', 'server-pro', 'saas']
|
||||
|
||||
const indexes = [
|
||||
{
|
||||
key: { lastActive: 1 },
|
||||
name: 'lastActive_1',
|
||||
},
|
||||
]
|
||||
|
||||
exports.migrate = async client => {
|
||||
const { db } = client
|
||||
await Helpers.addIndexesToCollection(db.users, indexes)
|
||||
}
|
||||
|
||||
exports.rollback = async client => {
|
||||
const { db } = client
|
||||
await Helpers.dropIndexesFromCollection(db.users, indexes)
|
||||
}
|
|
@ -4,6 +4,7 @@ const base = require('./settings.test.defaults')
|
|||
|
||||
module.exports = base.mergeWith({
|
||||
defaultFeatures: ServerCEDefaults.defaultFeatures,
|
||||
activeUserMetricInterval: 100,
|
||||
})
|
||||
|
||||
module.exports.mergeWith = function (overrides) {
|
||||
|
|
34
services/web/test/acceptance/src/ActiveUsersMetricTests.js
Normal file
34
services/web/test/acceptance/src/ActiveUsersMetricTests.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const { promisify } = require('util')
|
||||
const { expect } = require('chai')
|
||||
const Features = require('../../../app/src/infrastructure/Features')
|
||||
const {
|
||||
promises: { getMetric },
|
||||
} = require('./helpers/metrics')
|
||||
const User = require('./helpers/User').promises
|
||||
const sleep = promisify(setTimeout)
|
||||
|
||||
async function getActiveUsersMetric() {
|
||||
return getMetric(line => line.startsWith('num_active_users'))
|
||||
}
|
||||
|
||||
describe('ActiveUsersMetricTests', function () {
|
||||
before(async function () {
|
||||
if (Features.hasFeature('saas')) {
|
||||
this.skip()
|
||||
}
|
||||
})
|
||||
|
||||
it('updates "num_active_users" metric after a new user opens a project', async function () {
|
||||
expect(await getActiveUsersMetric()).to.equal(0)
|
||||
|
||||
this.user = new User()
|
||||
await this.user.login()
|
||||
const projectId = await this.user.createProject('test project')
|
||||
await this.user.openProject(projectId)
|
||||
|
||||
// settings.activeUserMetricInterval is configured to 100ms
|
||||
await sleep(110)
|
||||
|
||||
expect(await getActiveUsersMetric()).to.equal(1)
|
||||
})
|
||||
})
|
|
@ -94,7 +94,7 @@ describe('AuthenticationController', function () {
|
|||
},
|
||||
}),
|
||||
'../User/UserHandler': (this.UserHandler = {
|
||||
setupLoginData: sinon.stub(),
|
||||
populateTeamInvites: sinon.stub(),
|
||||
}),
|
||||
'../Analytics/AnalyticsManager': (this.AnalyticsManager = {
|
||||
recordEventForUserInBackground: sinon.stub(),
|
||||
|
@ -556,7 +556,7 @@ describe('AuthenticationController', function () {
|
|||
})
|
||||
|
||||
it('should not setup the user data in the background', function () {
|
||||
this.UserHandler.setupLoginData.called.should.equal(false)
|
||||
this.UserHandler.populateTeamInvites.called.should.equal(false)
|
||||
})
|
||||
|
||||
it('should record a failed login', function () {
|
||||
|
@ -1134,7 +1134,7 @@ describe('AuthenticationController', function () {
|
|||
this.AuthenticationController._clearRedirectFromSession = sinon.stub()
|
||||
this.AuthenticationController._redirectToReconfirmPage = sinon.stub()
|
||||
this.UserSessionsManager.trackSession = sinon.stub()
|
||||
this.UserHandler.setupLoginData = sinon.stub()
|
||||
this.UserHandler.populateTeamInvites = sinon.stub()
|
||||
this.LoginRateLimiter.recordSuccessfulLogin = sinon.stub()
|
||||
this.AuthenticationController._recordSuccessfulLogin = sinon.stub()
|
||||
this.AnalyticsManager.recordEvent = sinon.stub()
|
||||
|
@ -1461,7 +1461,9 @@ describe('AuthenticationController', function () {
|
|||
})
|
||||
|
||||
it('should setup the user data in the background', function () {
|
||||
this.UserHandler.setupLoginData.calledWith(this.user).should.equal(true)
|
||||
this.UserHandler.populateTeamInvites
|
||||
.calledWith(this.user)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should set res.session.justLoggedIn', function () {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const modulePath = '../../../../app/src/Features/User/UserHandler.js'
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
|
||||
|
@ -14,9 +15,18 @@ describe('UserHandler', function () {
|
|||
createTeamInvitesForLegacyInvitedEmail: sinon.stub().yields(),
|
||||
}
|
||||
|
||||
this.db = {
|
||||
users: {
|
||||
find: sinon.stub().returns({
|
||||
count: sinon.stub().resolves(2),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
this.UserHandler = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'../Subscription/TeamInvitesHandler': this.TeamInvitesHandler,
|
||||
'../../infrastructure/mongodb': { db: this.db },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -32,4 +42,10 @@ describe('UserHandler', function () {
|
|||
.should.eq(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('countActiveUsers', function () {
|
||||
it('return user count from DB lookup', async function () {
|
||||
expect(await this.UserHandler.promises.countActiveUsers()).to.equal(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue