Merge pull request #3302 from overleaf/em-analytics-queues

Send analytics events through a queue

GitOrigin-RevId: b9eb12e469faf16e32aba5fae665c5f85dfbc52c
This commit is contained in:
Eric Mc Sween 2020-11-04 08:35:37 -05:00 committed by Copybot
parent fc1816b0fc
commit d5a49038df
14 changed files with 420 additions and 496 deletions

View file

@ -1,5 +1,5 @@
const metrics = require('@overleaf/metrics')
const AnalyticsManager = require('./AnalyticsManager') const AnalyticsManager = require('./AnalyticsManager')
const Errors = require('../Errors/Errors')
const AuthenticationController = require('../Authentication/AuthenticationController') const AuthenticationController = require('../Authentication/AuthenticationController')
const InstitutionsAPI = require('../Institutions/InstitutionsAPI') const InstitutionsAPI = require('../Institutions/InstitutionsAPI')
const GeoIpLookup = require('../../infrastructure/GeoIpLookup') const GeoIpLookup = require('../../infrastructure/GeoIpLookup')
@ -8,7 +8,7 @@ const Features = require('../../infrastructure/Features')
module.exports = { module.exports = {
updateEditingSession(req, res, next) { updateEditingSession(req, res, next) {
if (!Features.hasFeature('analytics')) { if (!Features.hasFeature('analytics')) {
return res.sendStatus(204) return res.sendStatus(202)
} }
const userId = AuthenticationController.getLoggedInUserId(req) const userId = AuthenticationController.getLoggedInUserId(req)
const { projectId } = req.params const { projectId } = req.params
@ -16,30 +16,25 @@ module.exports = {
if (userId) { if (userId) {
GeoIpLookup.getDetails(req.ip, function(err, geoDetails) { GeoIpLookup.getDetails(req.ip, function(err, geoDetails) {
if (!err && geoDetails && geoDetails.country_code) { if (err) {
metrics.inc('analytics_geo_ip_lookup_errors')
} else if (geoDetails && geoDetails.country_code) {
countryCode = geoDetails.country_code countryCode = geoDetails.country_code
} }
AnalyticsManager.updateEditingSession( AnalyticsManager.updateEditingSession(userId, projectId, countryCode)
userId,
projectId,
countryCode,
error => respondWith(error, res, next)
)
}) })
} else {
res.sendStatus(204)
} }
res.sendStatus(202)
}, },
recordEvent(req, res, next) { recordEvent(req, res, next) {
if (!Features.hasFeature('analytics')) { if (!Features.hasFeature('analytics')) {
return res.sendStatus(204) return res.sendStatus(202)
} }
const userId = const userId =
AuthenticationController.getLoggedInUserId(req) || req.sessionID AuthenticationController.getLoggedInUserId(req) || req.sessionID
AnalyticsManager.recordEvent(userId, req.params.event, req.body, error => AnalyticsManager.recordEvent(userId, req.params.event, req.body)
respondWith(error, res, next) res.sendStatus(202)
)
}, },
licences(req, res, next) { licences(req, res, next) {
@ -72,14 +67,3 @@ module.exports = {
) )
} }
} }
var respondWith = function(error, res, next) {
if (error instanceof Errors.ServiceNotConfiguredError) {
// ignore, no-op
res.sendStatus(204)
} else if (error) {
next(error)
} else {
res.sendStatus(204)
}
}

View file

@ -1,128 +1,72 @@
const settings = require('settings-sharelatex') const Settings = require('settings-sharelatex')
const FaultTolerantRequest = require('../../infrastructure/FaultTolerantRequest') const Metrics = require('../../infrastructure/Metrics')
const Errors = require('../Errors/Errors') const Queues = require('../../infrastructure/Queues')
// check that the request should be made: ignore smoke test user and ensure the function identifyUser(userId, oldUserId) {
// analytics service is configured if (isAnalyticsDisabled() || isSmokeTestUser(userId)) {
const checkAnalyticsRequest = function(userId) { return
if (
settings.smokeTest &&
settings.smokeTest.userId &&
settings.smokeTest.userId.toString() === userId.toString()
) {
// ignore smoke test user
return { error: null, skip: true }
} }
Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'identify' })
if (!settings.apis.analytics) { Queues.analytics.events
return { .add('identify', { userId, oldUserId })
error: new Errors.ServiceNotConfiguredError( .then(() => {
'Analytics service not configured' Metrics.analyticsQueue.inc({ status: 'added', event_type: 'identify' })
), })
skip: true .catch(() => {
} Metrics.analyticsQueue.inc({ status: 'error', event_type: 'identify' })
} })
return { error: null, skip: false }
} }
// prepare the request: set `fromv2` param and full URL function recordEvent(userId, event, segmentation) {
const prepareAnalyticsRequest = function(options) { if (isAnalyticsDisabled() || isSmokeTestUser(userId)) {
if (settings.overleaf != null) { return
options.qs = Object.assign({}, options.qs, { fromV2: 1 })
} }
Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'event' })
const urlPath = options.url Queues.analytics.events
options.url = `${settings.apis.analytics.url}${urlPath}` .add('event', { userId, event, segmentation })
.then(() => {
options.timeout = options.timeout || 30000 Metrics.analyticsQueue.inc({ status: 'added', event_type: 'event' })
})
return options .catch(() => {
Metrics.analyticsQueue.inc({ status: 'error', event_type: 'event' })
})
} }
// make the request to analytics after checking and preparing it. function updateEditingSession(userId, projectId, countryCode) {
// request happens asynchronously in the background and will be retried on error if (isAnalyticsDisabled() || isSmokeTestUser(userId)) {
const makeAnalyticsBackgroundRequest = function(userId, options, callback) { return
let { error, skip } = checkAnalyticsRequest(userId)
if (error || skip) {
return callback(error)
} }
prepareAnalyticsRequest(options) Metrics.analyticsQueue.inc({
status: 'adding',
event_type: 'editing-session'
})
Queues.analytics.editingSessions
.add({ userId, projectId, countryCode })
.then(() => {
Metrics.analyticsQueue.inc({
status: 'added',
event_type: 'editing-session'
})
})
.catch(() => {
Metrics.analyticsQueue.inc({
status: 'error',
event_type: 'editing-session'
})
})
}
// With the tweaked parameter values (BACKOFF_BASE=3000, BACKOFF_MULTIPLIER=3): function isSmokeTestUser(userId) {
// - the 6th attempt (maxAttempts=6) will run after 5.5 to 11.5 minutes const smokeTestUserId = Settings.smokeTest && Settings.smokeTest.userId
// - the 9th attempt (maxAttempts=9) will run after 86 to 250 minutes return smokeTestUserId != null && userId.toString() === smokeTestUserId
options.maxAttempts = options.maxAttempts || 9 }
options.backoffBase = options.backoffBase || 3000
options.backoffMultiplier = options.backoffMultiplier || 3
FaultTolerantRequest.backgroundRequest(options, callback) function isAnalyticsDisabled() {
return !(Settings.analytics && Settings.analytics.enabled)
} }
module.exports = { module.exports = {
identifyUser(userId, oldUserId, callback) { identifyUser,
if (!callback) { recordEvent,
// callback is optional updateEditingSession
callback = () => {}
}
const opts = {
body: {
old_user_id: oldUserId
},
json: true,
method: 'POST',
url: `/user/${userId}/identify`
}
makeAnalyticsBackgroundRequest(userId, opts, callback)
},
recordEvent(userId, event, segmentation, callback) {
if (segmentation == null) {
// segmentation is optional
segmentation = {}
}
if (!callback) {
// callback is optional
callback = () => {}
}
const opts = {
body: {
event,
segmentation
},
json: true,
method: 'POST',
url: `/user/${userId}/event`
}
makeAnalyticsBackgroundRequest(userId, opts, callback)
},
updateEditingSession(userId, projectId, countryCode, callback) {
if (!callback) {
// callback is optional
callback = () => {}
}
const query = {
userId,
projectId
}
if (countryCode) {
query.countryCode = countryCode
}
const opts = {
method: 'PUT',
url: '/editingSession',
qs: query,
maxAttempts: 6 // dont retry for too long as session ping timestamp are
// recorded when the request is received on the analytics
}
makeAnalyticsBackgroundRequest(userId, opts, callback)
}
} }

View file

@ -24,7 +24,7 @@ const Settings = require('settings-sharelatex')
const EmailHelper = require('../Helpers/EmailHelper') const EmailHelper = require('../Helpers/EmailHelper')
const EditorRealTimeController = require('../Editor/EditorRealTimeController') const EditorRealTimeController = require('../Editor/EditorRealTimeController')
const NotificationsBuilder = require('../Notifications/NotificationsBuilder') const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
const AnalyticsManger = require('../Analytics/AnalyticsManager') const AnalyticsManager = require('../Analytics/AnalyticsManager')
const AuthenticationController = require('../Authentication/AuthenticationController') const AuthenticationController = require('../Authentication/AuthenticationController')
const rateLimiter = require('../../infrastructure/RateLimiter') const rateLimiter = require('../../infrastructure/RateLimiter')
const request = require('request') const request = require('request')
@ -377,7 +377,7 @@ module.exports = CollaboratorsInviteController = {
'project:membership:changed', 'project:membership:changed',
{ invites: true, members: true } { invites: true, members: true }
) )
AnalyticsManger.recordEvent(currentUser._id, 'project-invite-accept', { AnalyticsManager.recordEvent(currentUser._id, 'project-invite-accept', {
projectId, projectId,
userId: currentUser._id userId: currentUser._id
}) })

View file

@ -28,7 +28,7 @@ const fs = require('fs')
const Path = require('path') const Path = require('path')
const { promisify } = require('util') const { promisify } = require('util')
const _ = require('underscore') const _ = require('underscore')
const AnalyticsManger = require('../Analytics/AnalyticsManager') const AnalyticsManager = require('../Analytics/AnalyticsManager')
const ProjectCreationHandler = { const ProjectCreationHandler = {
createBlankProject(owner_id, projectName, attributes, callback) { createBlankProject(owner_id, projectName, attributes, callback) {
@ -56,7 +56,7 @@ const ProjectCreationHandler = {
if (error != null) { if (error != null) {
return callback(error) return callback(error)
} }
AnalyticsManger.recordEvent(owner_id, 'project-imported', { AnalyticsManager.recordEvent(owner_id, 'project-imported', {
projectId: project._id, projectId: project._id,
attributes attributes
}) })
@ -79,7 +79,7 @@ const ProjectCreationHandler = {
if (error != null) { if (error != null) {
return callback(error) return callback(error)
} }
AnalyticsManger.recordEvent(owner_id, 'project-created', { AnalyticsManager.recordEvent(owner_id, 'project-created', {
projectId: project._id, projectId: project._id,
attributes attributes
}) })

View file

@ -245,7 +245,8 @@ module.exports = function(webRouter, privateApiRouter, publicApiRouter) {
}) })
webRouter.use(function(req, res, next) { webRouter.use(function(req, res, next) {
res.locals.gaToken = Settings.analytics && Settings.analytics.ga.token res.locals.gaToken =
Settings.analytics && Settings.analytics.ga && Settings.analytics.ga.token
res.locals.gaOptimizeId = _.get(Settings, ['analytics', 'gaOptimize', 'id']) res.locals.gaOptimizeId = _.get(Settings, ['analytics', 'gaOptimize', 'id'])
next() next()
}) })

View file

@ -1,85 +0,0 @@
const logger = require('logger-sharelatex')
const request = require('requestretry')
const isProduction =
(process.env['NODE_ENV'] || '').toLowerCase() === 'production'
const isTest = process.env['MOCHA_GREP'] !== undefined
const BACKOFF_MAX_TRIES = 3
const BACKOFF_BASE = 500
const BACKOFF_MULTIPLIER = 1.5
const BACKOFF_RANDOM_FACTOR = 0.5
//
// Use an exponential backoff to retry requests
//
// This is based on what the Google HTTP client does:
// https://developers.google.com/api-client-library/java/google-http-java-client/reference/1.20.0/com/google/api/client/util/ExponentialBackOff
//
let FaultTolerantRequest
module.exports = FaultTolerantRequest = {
request: function(options, callback) {
options = Object.assign(
{
maxAttempts: BACKOFF_MAX_TRIES,
backoffBase: BACKOFF_BASE,
backoffMultiplier: BACKOFF_MULTIPLIER,
backoffRandomFactor: BACKOFF_RANDOM_FACTOR
},
options
)
options.delayStrategy = FaultTolerantRequest.exponentialDelayStrategy(
options.backoffBase,
options.backoffMultiplier,
options.backoffRandomFactor
)
request(options, callback)
},
backgroundRequest: function(options, callback) {
FaultTolerantRequest.request(options, function(err) {
if (err) {
return logger.err(
{ err, url: options.url, query: options.qs, body: options.body },
'Background request failed'
)
}
})
callback() // Do not wait for all the attempts
},
exponentialDelayStrategy: function(
backoffBase,
backoffMultiplier,
backoffRandomFactor
) {
let backoff = backoffBase
return function() {
const delay = exponentialDelay(backoff, backoffRandomFactor)
backoff *= backoffMultiplier
return delay
}
}
}
function exponentialDelay(backoff, backoffRandomFactor) {
// set delay to `backoff` initially
let delay = backoff
// adds randomness
delay *= 1 - backoffRandomFactor + 2 * Math.random() * backoffRandomFactor
// round value as it's already in milliseconds
delay = Math.round(delay)
// log retries in production
if (isProduction && !isTest) {
logger.warn(`Background request failed. Will try again in ${delay}ms`)
}
return delay
}

View file

@ -0,0 +1,7 @@
const Metrics = require('@overleaf/metrics')
exports.analyticsQueue = new Metrics.prom.Counter({
name: 'analytics_queue',
help: 'Number of events sent to the analytics queue',
labelNames: ['status', 'event_type']
})

View file

@ -0,0 +1,23 @@
const Queue = require('bull')
const Settings = require('settings-sharelatex')
function createQueue(queueName, defaultJobOptions) {
return new Queue(queueName, {
redis: Settings.redis.queues,
defaultJobOptions: {
removeOnComplete: true,
attempts: 11,
backoff: {
type: 'exponential',
delay: 3000
}
}
})
}
module.exports = {
analytics: {
events: createQueue('analytics-events'),
editingSessions: createQueue('analytics-editing-sessions')
}
}

View file

@ -93,6 +93,11 @@ module.exports = settings =
password: process.env["REDIS_PASSWORD"] or "" password: process.env["REDIS_PASSWORD"] or ""
maxRetriesPerRequest: parseInt(process.env["REDIS_MAX_RETRIES_PER_REQUEST"] || '20') maxRetriesPerRequest: parseInt(process.env["REDIS_MAX_RETRIES_PER_REQUEST"] || '20')
queues:
host: process.env['QUEUES_REDIS_HOST'] || process.env['REDIS_HOST'] || 'localhost'
port: process.env['QUEUES_REDIS_PORT'] || process.env['REDIS_PORT'] || '6379'
password: process.env['QUEUES_REDIS_PASSWORD'] || process.env['REDIS_PASSWORD'] || ''
# Service locations # Service locations
# ----------------- # -----------------
@ -616,6 +621,9 @@ module.exports = settings =
everyone: process.env['RATE_LIMIT_AUTO_COMPILE_EVERYONE'] or 100 everyone: process.env['RATE_LIMIT_AUTO_COMPILE_EVERYONE'] or 100
standard: process.env['RATE_LIMIT_AUTO_COMPILE_STANDARD'] or 25 standard: process.env['RATE_LIMIT_AUTO_COMPILE_STANDARD'] or 25
analytics:
enabled: process.env['ANALYTICS_ENABLED'] == 'true'
# currentImage: "texlive-full:2017.1" # currentImage: "texlive-full:2017.1"
# imageRoot: "<DOCKER REPOSITORY ROOT>" # without any trailing slash # imageRoot: "<DOCKER REPOSITORY ROOT>" # without any trailing slash

View file

@ -7,7 +7,7 @@
"@auth0/thumbprint": { "@auth0/thumbprint": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/@auth0/thumbprint/-/thumbprint-0.0.6.tgz", "resolved": "https://registry.npmjs.org/@auth0/thumbprint/-/thumbprint-0.0.6.tgz",
"integrity": "sha1-yrEGLGwEZizmxZLUgVfsQmiuhRg=", "integrity": "sha512-+YciWHxNUOE78T+xoXI1fMI6G1WdyyAay8ioaMZhvGOJ+lReYzj0b7mpfNr5WtjGrmtWPvPOOxh0TO+5Y2M/Hw==",
"dev": true "dev": true
}, },
"@babel/cli": { "@babel/cli": {
@ -2889,9 +2889,9 @@
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "13.13.29", "version": "13.13.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.29.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.30.tgz",
"integrity": "sha512-WPGpyEDx4/F4Rx1p1Zar8m+JsMxpSY/wNFPlyNXWV+UzJwkYt3LQg2be/qJgpsLdVJsfxTR5ipY6rv2579jStQ==" "integrity": "sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA=="
}, },
"protobufjs": { "protobufjs": {
"version": "6.10.1", "version": "6.10.1",
@ -3817,9 +3817,9 @@
} }
}, },
"@overleaf/metrics": { "@overleaf/metrics": {
"version": "3.3.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/@overleaf/metrics/-/metrics-3.3.0.tgz", "resolved": "https://registry.npmjs.org/@overleaf/metrics/-/metrics-3.4.0.tgz",
"integrity": "sha512-y+t0ZUW3WC90PrDH6KTDig4S4wRtL/0Uay+DFG2iuxFTaxYvf0CA1ebZidj2QyRZm6Yf0FNOJnSRp55fcrAadA==", "integrity": "sha512-MsD+5d7gpoz2VyJ5AnEP1VHGyHsfya7bfkdnMgi93kS+FPcYvdpcooBXPq4jmGemEVxXhdyP9lBTJDwcdjZeiQ==",
"requires": { "requires": {
"@google-cloud/debug-agent": "^5.1.2", "@google-cloud/debug-agent": "^5.1.2",
"@google-cloud/profiler": "^4.0.3", "@google-cloud/profiler": "^4.0.3",
@ -12154,6 +12154,175 @@
"integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==",
"dev": true "dev": true
}, },
"bull": {
"version": "3.18.0",
"resolved": "https://registry.npmjs.org/bull/-/bull-3.18.0.tgz",
"integrity": "sha512-nE/BKlg1dnJ/AcOy5D1nzthcmpAKqpUVXzQ43mJfnVC8ZM7mi4ZzP3spN7745UuikzmGGsbTe9px2TbEKhR+DQ==",
"requires": {
"cron-parser": "^2.13.0",
"debuglog": "^1.0.0",
"get-port": "^5.1.1",
"ioredis": "^4.14.1",
"lodash": "^4.17.19",
"p-timeout": "^3.2.0",
"promise.prototype.finally": "^3.1.2",
"semver": "^7.3.2",
"util.promisify": "^1.0.1",
"uuid": "^8.3.0"
},
"dependencies": {
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"requires": {
"object-keys": "^1.0.12"
}
},
"es-abstract": {
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
},
"es-to-primitive": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
"is-symbol": "^1.0.2"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"requires": {
"function-bind": "^1.1.1"
}
},
"has-symbols": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
},
"is-callable": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
},
"is-regex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
"integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"requires": {
"has-symbols": "^1.0.1"
}
},
"is-symbol": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"requires": {
"has-symbols": "^1.0.1"
}
},
"object-inspect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA=="
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"object.assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz",
"integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==",
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.0",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
}
}
},
"object.getownpropertydescriptors": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
"integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.1"
}
},
"p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"requires": {
"p-finally": "^1.0.0"
}
},
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
},
"util.promisify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz",
"integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==",
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.2",
"has-symbols": "^1.0.1",
"object.getownpropertydescriptors": "^2.1.0"
}
},
"uuid": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg=="
}
}
},
"bunyan": { "bunyan": {
"version": "1.8.14", "version": "1.8.14",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz",
@ -13897,6 +14066,15 @@
} }
} }
}, },
"cron-parser": {
"version": "2.16.3",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-2.16.3.tgz",
"integrity": "sha512-XNJBD1QLFeAMUkZtZQuncAAOgJFWNhBdIbwgD22hZxrcWOImBFMKgPC66GzaXpyoJs7UvYLLgPH/8BRk/7gbZg==",
"requires": {
"is-nan": "^1.3.0",
"moment-timezone": "^0.5.31"
}
},
"cross-spawn": { "cross-spawn": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
@ -14493,6 +14671,11 @@
} }
} }
}, },
"debuglog": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
"integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI="
},
"decamelize": { "decamelize": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
@ -17375,7 +17558,7 @@
"flowstate": { "flowstate": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/flowstate/-/flowstate-0.4.1.tgz", "resolved": "https://registry.npmjs.org/flowstate/-/flowstate-0.4.1.tgz",
"integrity": "sha1-tfu4t/wte9xbVL5GyYMJ73NvTsA=", "integrity": "sha512-U67AgveyMwXFIiDgs6Yz/PrUNrZGLJUUMDwJ9Q0fDFTQSzyDg8Jj9YDyZIUnFZKggQZONVueK9+grp/Gxa/scw==",
"dev": true, "dev": true,
"requires": { "requires": {
"clone": "^1.0.2", "clone": "^1.0.2",
@ -18378,11 +18561,11 @@
} }
}, },
"gcp-metadata": { "gcp-metadata": {
"version": "4.2.0", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.0.tgz", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz",
"integrity": "sha512-vQZD57cQkqIA6YPGXM/zc+PIZfNRFdukWGsGZ5+LcJzesi5xp6Gn7a02wRJi4eXPyArNMIYpPET4QMxGqtlk6Q==", "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==",
"requires": { "requires": {
"gaxios": "^3.0.0", "gaxios": "^4.0.0",
"json-bigint": "^1.0.0" "json-bigint": "^1.0.0"
}, },
"dependencies": { "dependencies": {
@ -18391,23 +18574,6 @@
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz",
"integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA=="
}, },
"gaxios": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz",
"integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==",
"requires": {
"abort-controller": "^3.0.0",
"extend": "^3.0.2",
"https-proxy-agent": "^5.0.0",
"is-stream": "^2.0.0",
"node-fetch": "^2.3.0"
}
},
"is-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
},
"json-bigint": { "json-bigint": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
@ -18441,6 +18607,11 @@
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true "dev": true
}, },
"get-port": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
"integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ=="
},
"get-stream": { "get-stream": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
@ -20527,6 +20698,34 @@
"integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==",
"dev": true "dev": true
}, },
"is-nan": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.0.tgz",
"integrity": "sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ==",
"requires": {
"define-properties": "^1.1.3"
},
"dependencies": {
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"requires": {
"object-keys": "^1.0.12"
}
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
}
}
},
"is-negative-zero": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
"integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE="
},
"is-npm": { "is-npm": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz",
@ -23660,7 +23859,7 @@
}, },
"mkdirp": { "mkdirp": {
"version": "0.5.1", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "resolved": "",
"integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==",
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
@ -23835,6 +24034,14 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
}, },
"moment-timezone": {
"version": "0.5.31",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz",
"integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==",
"requires": {
"moment": ">= 2.9.0"
}
},
"mongodb": { "mongodb": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.2.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.2.tgz",
@ -25283,8 +25490,7 @@
"object-inspect": { "object-inspect": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
"integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw=="
"dev": true
}, },
"object-is": { "object-is": {
"version": "1.1.2", "version": "1.1.2",
@ -27802,7 +28008,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.2.tgz", "resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.2.tgz",
"integrity": "sha512-A2HuJWl2opDH0EafgdjwEw7HysI8ff/n4lW4QEVBCUXFk9QeGecBWv0Deph0UmLe3tTNYegz8MOjsVuE6SMoJA==", "integrity": "sha512-A2HuJWl2opDH0EafgdjwEw7HysI8ff/n4lW4QEVBCUXFk9QeGecBWv0Deph0UmLe3tTNYegz8MOjsVuE6SMoJA==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.0", "es-abstract": "^1.17.0-next.0",
@ -27813,7 +28018,6 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": { "requires": {
"object-keys": "^1.0.12" "object-keys": "^1.0.12"
} }
@ -27822,7 +28026,6 @@
"version": "1.17.6", "version": "1.17.6",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==",
"dev": true,
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
@ -27841,7 +28044,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"dev": true,
"requires": { "requires": {
"is-callable": "^1.1.4", "is-callable": "^1.1.4",
"is-date-object": "^1.0.1", "is-date-object": "^1.0.1",
@ -27852,7 +28054,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1" "function-bind": "^1.1.1"
} }
@ -27860,20 +28061,17 @@
"has-symbols": { "has-symbols": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
"dev": true
}, },
"is-callable": { "is-callable": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz",
"integrity": "sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==", "integrity": "sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg=="
"dev": true
}, },
"is-regex": { "is-regex": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
"integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"dev": true,
"requires": { "requires": {
"has-symbols": "^1.0.1" "has-symbols": "^1.0.1"
} }
@ -27882,7 +28080,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"dev": true,
"requires": { "requires": {
"has-symbols": "^1.0.1" "has-symbols": "^1.0.1"
} }
@ -27890,8 +28087,7 @@
"object-keys": { "object-keys": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
"dev": true
} }
} }
}, },
@ -33366,7 +33562,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
"integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.5" "es-abstract": "^1.17.5"
@ -33376,7 +33571,6 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": { "requires": {
"object-keys": "^1.0.12" "object-keys": "^1.0.12"
} }
@ -33385,7 +33579,6 @@
"version": "1.17.5", "version": "1.17.5",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
"integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
"dev": true,
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
@ -33404,7 +33597,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"dev": true,
"requires": { "requires": {
"is-callable": "^1.1.4", "is-callable": "^1.1.4",
"is-date-object": "^1.0.1", "is-date-object": "^1.0.1",
@ -33415,7 +33607,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1" "function-bind": "^1.1.1"
} }
@ -33423,20 +33614,17 @@
"has-symbols": { "has-symbols": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
"dev": true
}, },
"is-callable": { "is-callable": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
"integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q=="
"dev": true
}, },
"is-regex": { "is-regex": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
"integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
"dev": true,
"requires": { "requires": {
"has": "^1.0.3" "has": "^1.0.3"
} }
@ -33445,7 +33633,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"dev": true,
"requires": { "requires": {
"has-symbols": "^1.0.1" "has-symbols": "^1.0.1"
} }
@ -33453,14 +33640,12 @@
"object-keys": { "object-keys": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
"dev": true
}, },
"string.prototype.trimleft": { "string.prototype.trimleft": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz",
"integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.5", "es-abstract": "^1.17.5",
@ -33471,7 +33656,6 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz",
"integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.5", "es-abstract": "^1.17.5",
@ -33538,7 +33722,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
"integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.5" "es-abstract": "^1.17.5"
@ -33548,7 +33731,6 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": { "requires": {
"object-keys": "^1.0.12" "object-keys": "^1.0.12"
} }
@ -33557,7 +33739,6 @@
"version": "1.17.5", "version": "1.17.5",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
"integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
"dev": true,
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
@ -33576,7 +33757,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"dev": true,
"requires": { "requires": {
"is-callable": "^1.1.4", "is-callable": "^1.1.4",
"is-date-object": "^1.0.1", "is-date-object": "^1.0.1",
@ -33587,7 +33767,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1" "function-bind": "^1.1.1"
} }
@ -33595,20 +33774,17 @@
"has-symbols": { "has-symbols": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
"dev": true
}, },
"is-callable": { "is-callable": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
"integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q=="
"dev": true
}, },
"is-regex": { "is-regex": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
"integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
"dev": true,
"requires": { "requires": {
"has": "^1.0.3" "has": "^1.0.3"
} }
@ -33617,7 +33793,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"dev": true,
"requires": { "requires": {
"has-symbols": "^1.0.1" "has-symbols": "^1.0.1"
} }
@ -33625,14 +33800,12 @@
"object-keys": { "object-keys": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
"dev": true
}, },
"string.prototype.trimleft": { "string.prototype.trimleft": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz",
"integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.5", "es-abstract": "^1.17.5",
@ -33643,7 +33816,6 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz",
"integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.5", "es-abstract": "^1.17.5",
@ -35690,7 +35862,7 @@
"utils-flatten": { "utils-flatten": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/utils-flatten/-/utils-flatten-1.0.0.tgz", "resolved": "https://registry.npmjs.org/utils-flatten/-/utils-flatten-1.0.0.tgz",
"integrity": "sha1-AfMNMZO+RkxAsxdV5nQNDbDO8kM=", "integrity": "sha512-s21PUgUZ+XPvH8Wi8aj2FEqzZWeNEdemP7LB4u8u5wTDRO4xB+7czAYd3FY2O2rnu89U//khR0Ce8ka3//6M0w==",
"dev": true "dev": true
}, },
"utils-merge": { "utils-merge": {
@ -37651,7 +37823,7 @@
"xml-name-validator": { "xml-name-validator": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz",
"integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", "integrity": "sha512-jRKe/iQYMyVJpzPH+3HL97Lgu5HrCfii+qSo+TfjKHtOnvbnvdVfMYrn9Q34YV81M2e5sviJlI6Ko9y+nByzvA==",
"dev": true "dev": true
}, },
"xml2js": { "xml2js": {

View file

@ -41,7 +41,7 @@
"@babel/core": "^7.9.0", "@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.5", "@babel/preset-env": "^7.9.5",
"@babel/preset-react": "^7.9.4", "@babel/preset-react": "^7.9.4",
"@overleaf/metrics": "^3.3.0", "@overleaf/metrics": "^3.4.0",
"@overleaf/o-error": "^3.0.0", "@overleaf/o-error": "^3.0.0",
"@pollyjs/adapter-node-http": "^4.2.1", "@pollyjs/adapter-node-http": "^4.2.1",
"@pollyjs/core": "^4.2.1", "@pollyjs/core": "^4.2.1",
@ -58,6 +58,7 @@
"bcrypt": "^5.0.0", "bcrypt": "^5.0.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"bufferedstream": "1.6.0", "bufferedstream": "1.6.0",
"bull": "^3.18.0",
"celebrate": "^10.0.1", "celebrate": "^10.0.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"codemirror": "^5.33.0", "codemirror": "^5.33.0",

View file

@ -11,8 +11,8 @@ describe('AnalyticsController', function() {
this.AuthenticationController = { getLoggedInUserId: sinon.stub() } this.AuthenticationController = { getLoggedInUserId: sinon.stub() }
this.AnalyticsManager = { this.AnalyticsManager = {
updateEditingSession: sinon.stub().callsArgWith(3), updateEditingSession: sinon.stub(),
recordEvent: sinon.stub().callsArgWith(3) recordEvent: sinon.stub()
} }
this.InstitutionsAPI = { this.InstitutionsAPI = {

View file

@ -1,106 +1,96 @@
const SandboxedModule = require('sandboxed-module') const SandboxedModule = require('sandboxed-module')
const path = require('path') const path = require('path')
const modulePath = path.join( const sinon = require('sinon')
const MODULE_PATH = path.join(
__dirname, __dirname,
'../../../../app/src/Features/Analytics/AnalyticsManager' '../../../../app/src/Features/Analytics/AnalyticsManager'
) )
const sinon = require('sinon')
const { expect } = require('chai')
const Errors = require('../../../../app/src/Features/Errors/Errors')
describe('AnalyticsManager', function() { describe('AnalyticsManager', function() {
beforeEach(function() { beforeEach(function() {
this.fakeUserId = '123abc' this.fakeUserId = '123abc'
this.settings = { this.Settings = {
overleaf: true, analytics: { enabled: true }
apis: { analytics: { url: 'analytics.test' } } }
this.Queues = {
analytics: {
events: {
add: sinon.stub().resolves()
},
editingSessions: {
add: sinon.stub().resolves()
}
}
} }
this.backgroundRequest = sinon.stub().yields() this.backgroundRequest = sinon.stub().yields()
this.request = sinon.stub().yields() this.request = sinon.stub().yields()
this.AnalyticsManager = SandboxedModule.require(modulePath, { this.AnalyticsManager = SandboxedModule.require(MODULE_PATH, {
globals: { globals: {
console: console console: console
}, },
requires: { requires: {
'settings-sharelatex': this.settings, 'settings-sharelatex': this.Settings,
'../../infrastructure/FaultTolerantRequest': {
backgroundRequest: this.backgroundRequest
},
'../Errors/Errors': Errors,
request: this.request,
'logger-sharelatex': { 'logger-sharelatex': {
log() {} warn() {}
} },
'../../infrastructure/Queues': this.Queues
} }
}) })
}) })
describe('checkAnalyticsRequest', function() { describe('ignores when', function() {
it('ignores smoke test user', function(done) { it('user is smoke test user', function() {
this.settings.smokeTest = { userId: this.fakeUserId } this.Settings.smokeTest = { userId: this.fakeUserId }
this.AnalyticsManager.identifyUser(this.fakeUserId, '', error => { this.AnalyticsManager.identifyUser(this.fakeUserId, '')
expect(error).to.not.exist sinon.assert.notCalled(this.Queues.analytics.events.add)
sinon.assert.notCalled(this.request)
done()
})
}) })
it('return error if analytics service is not configured', function(done) { it('analytics service is disabled', function() {
this.settings.apis.analytics = null this.Settings.analytics.enabled = false
this.AnalyticsManager.identifyUser(this.fakeUserId, '', error => { this.AnalyticsManager.identifyUser(this.fakeUserId, '')
expect(error).to.be.instanceof(Errors.ServiceNotConfiguredError) sinon.assert.notCalled(this.Queues.analytics.events.add)
sinon.assert.notCalled(this.request)
done()
})
}) })
}) })
describe('makes correct request to analytics', function() { describe('queues the appropriate message for', function() {
it('identifyUser', function(done) { it('identifyUser', function() {
const oldUserId = '456def' const oldUserId = '456def'
this.AnalyticsManager.identifyUser(this.fakeUserId, oldUserId, error => { this.AnalyticsManager.identifyUser(this.fakeUserId, oldUserId)
expect(error).to.not.exist sinon.assert.calledWithMatch(
sinon.assert.calledWithMatch(this.backgroundRequest, { this.Queues.analytics.events.add,
body: { old_user_id: oldUserId }, 'identify',
url: 'analytics.test/user/123abc/identify' {
}) userId: this.fakeUserId,
done() oldUserId
}) }
)
}) })
it('recordEvent', function(done) { it('recordEvent', function() {
const event = 'fake-event' const event = 'fake-event'
this.AnalyticsManager.recordEvent(this.fakeUserId, event, null, error => { this.AnalyticsManager.recordEvent(this.fakeUserId, event, null)
expect(error).to.not.exist sinon.assert.calledWithMatch(this.Queues.analytics.events.add, 'event', {
sinon.assert.calledWithMatch(this.backgroundRequest, { event,
body: { event }, userId: this.fakeUserId,
qs: { fromV2: 1 }, segmentation: null
url: 'analytics.test/user/123abc/event',
timeout: 30000,
maxAttempts: 9,
backoffBase: 3000,
backoffMultiplier: 3
})
done()
}) })
}) })
it('updateEditingSession', function(done) { it('updateEditingSession', function() {
const projectId = '789ghi' const projectId = '789ghi'
const countryCode = 'fr' const countryCode = 'fr'
this.AnalyticsManager.updateEditingSession( this.AnalyticsManager.updateEditingSession(
this.fakeUserId, this.fakeUserId,
projectId, projectId,
countryCode, countryCode
error => {
expect(error).to.not.exist
sinon.assert.calledWithMatch(this.backgroundRequest, {
qs: { userId: this.fakeUserId, projectId, countryCode, fromV2: 1 },
url: 'analytics.test/editingSession'
})
done()
}
) )
sinon.assert.calledWithMatch(this.Queues.analytics.editingSessions.add, {
userId: this.fakeUserId,
projectId,
countryCode
})
}) })
}) })
}) })

View file

@ -1,121 +0,0 @@
const SandboxedModule = require('sandboxed-module')
const path = require('path')
const modulePath = path.join(
__dirname,
'../../../../app/src/infrastructure/FaultTolerantRequest'
)
const sinon = require('sinon')
const { expect } = require('chai')
describe('FaultTolerantRequest', function() {
beforeEach(function() {
this.request = sinon.stub().yields()
this.logger = {
err: sinon.stub()
}
this.FaultTolerantRequest = SandboxedModule.require(modulePath, {
globals: {
console: console
},
requires: {
requestretry: this.request,
'logger-sharelatex': this.logger
}
})
})
describe('exponentialBackoffStrategy', function() {
it('returns delays within expected range with default options', function() {
this.FaultTolerantRequest.request({}, () => {})
const delayStrategy = this.request.lastCall.args[0].delayStrategy
expect(delayStrategy()).to.be.closeTo(500, 250) // attempt 1
expect(delayStrategy()).to.be.closeTo(750, 375) // attempt 2
expect(delayStrategy()).to.be.closeTo(1125, 563) // attempt 3
expect(delayStrategy()).to.be.closeTo(1688, 844) // attempt 4
expect(delayStrategy()).to.be.closeTo(2531, 1266) // attempt 5
expect(delayStrategy()).to.be.closeTo(3797, 1899) // attempt 6
expect(delayStrategy()).to.be.closeTo(5695, 2848) // attempt 7
expect(delayStrategy()).to.be.closeTo(8543, 4272) // attempt 8
expect(delayStrategy()).to.be.closeTo(12814, 6408) // attempt 9
expect(delayStrategy()).to.be.closeTo(19222, 9610) // attempt 10
})
it('returns delays within expected range with custom options', function() {
const delayStrategy = this.FaultTolerantRequest.exponentialDelayStrategy(
3000,
3,
0.5
)
expect(delayStrategy()).to.be.closeTo(3000, 1500) // attempt 1
expect(delayStrategy()).to.be.closeTo(9000, 4500) // attempt 2
expect(delayStrategy()).to.be.closeTo(27000, 13500) // attempt 3
expect(delayStrategy()).to.be.closeTo(81000, 40500) // attempt 4
expect(delayStrategy()).to.be.closeTo(243000, 121500) // attempt 5
expect(delayStrategy()).to.be.closeTo(729000, 364500) // attempt 6
expect(delayStrategy()).to.be.closeTo(2187000, 1093500) // attempt 7
expect(delayStrategy()).to.be.closeTo(6561000, 3280500) // attempt 8
expect(delayStrategy()).to.be.closeTo(19683000, 9841500) // attempt 9
expect(delayStrategy()).to.be.closeTo(59049000, 29524500) // attempt 10
})
})
describe('request', function() {
it('sets retry options', function(done) {
this.FaultTolerantRequest.request({}, error => {
expect(error).to.not.exist
sinon.assert.calledOnce(this.request)
const { delayStrategy, maxAttempts } = this.request.lastCall.args[0]
expect(delayStrategy).to.be.a('function')
expect(maxAttempts).to.be.a('number')
done()
})
})
it("don't overwrite retry options", function(done) {
const customMaxAttempts = Math.random()
const customBase = Math.random()
const customMultiplier = Math.random()
const customRandomFactor = Math.random()
this.FaultTolerantRequest.request(
{
maxAttempts: customMaxAttempts,
backoffBase: customBase,
backoffMultiplier: customMultiplier,
backoffRandomFactor: customRandomFactor
},
error => {
expect(error).to.not.exist
const {
maxAttempts,
backoffBase,
backoffMultiplier,
backoffRandomFactor
} = this.request.lastCall.args[0]
expect(maxAttempts).to.equal(customMaxAttempts)
expect(backoffBase).to.equal(customBase)
expect(backoffMultiplier).to.equal(customMultiplier)
expect(backoffRandomFactor).to.equal(customRandomFactor)
done()
}
)
})
})
describe('backgroundRequest', function() {
it('logs error in the background', function(done) {
this.request.yields(new Error('Nope'))
this.logger.err = (options, message) => {
expect(options.url).to.equal('test.url')
done()
}
this.FaultTolerantRequest.backgroundRequest(
{ url: 'test.url' },
error => {
expect(error).to.not.exist
}
)
})
})
})