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 Errors = require('../Errors/Errors')
const AuthenticationController = require('../Authentication/AuthenticationController')
const InstitutionsAPI = require('../Institutions/InstitutionsAPI')
const GeoIpLookup = require('../../infrastructure/GeoIpLookup')
@ -8,7 +8,7 @@ const Features = require('../../infrastructure/Features')
module.exports = {
updateEditingSession(req, res, next) {
if (!Features.hasFeature('analytics')) {
return res.sendStatus(204)
return res.sendStatus(202)
}
const userId = AuthenticationController.getLoggedInUserId(req)
const { projectId } = req.params
@ -16,30 +16,25 @@ module.exports = {
if (userId) {
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
}
AnalyticsManager.updateEditingSession(
userId,
projectId,
countryCode,
error => respondWith(error, res, next)
)
AnalyticsManager.updateEditingSession(userId, projectId, countryCode)
})
} else {
res.sendStatus(204)
}
res.sendStatus(202)
},
recordEvent(req, res, next) {
if (!Features.hasFeature('analytics')) {
return res.sendStatus(204)
return res.sendStatus(202)
}
const userId =
AuthenticationController.getLoggedInUserId(req) || req.sessionID
AnalyticsManager.recordEvent(userId, req.params.event, req.body, error =>
respondWith(error, res, next)
)
AnalyticsManager.recordEvent(userId, req.params.event, req.body)
res.sendStatus(202)
},
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 FaultTolerantRequest = require('../../infrastructure/FaultTolerantRequest')
const Errors = require('../Errors/Errors')
const Settings = require('settings-sharelatex')
const Metrics = require('../../infrastructure/Metrics')
const Queues = require('../../infrastructure/Queues')
// check that the request should be made: ignore smoke test user and ensure the
// analytics service is configured
const checkAnalyticsRequest = function(userId) {
if (
settings.smokeTest &&
settings.smokeTest.userId &&
settings.smokeTest.userId.toString() === userId.toString()
) {
// ignore smoke test user
return { error: null, skip: true }
function identifyUser(userId, oldUserId) {
if (isAnalyticsDisabled() || isSmokeTestUser(userId)) {
return
}
Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'identify' })
Queues.analytics.events
.add('identify', { userId, oldUserId })
.then(() => {
Metrics.analyticsQueue.inc({ status: 'added', event_type: 'identify' })
})
.catch(() => {
Metrics.analyticsQueue.inc({ status: 'error', event_type: 'identify' })
})
}
if (!settings.apis.analytics) {
return {
error: new Errors.ServiceNotConfiguredError(
'Analytics service not configured'
),
skip: true
function recordEvent(userId, event, segmentation) {
if (isAnalyticsDisabled() || isSmokeTestUser(userId)) {
return
}
Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'event' })
Queues.analytics.events
.add('event', { userId, event, segmentation })
.then(() => {
Metrics.analyticsQueue.inc({ status: 'added', event_type: 'event' })
})
.catch(() => {
Metrics.analyticsQueue.inc({ status: 'error', event_type: 'event' })
})
}
return { error: null, skip: false }
function updateEditingSession(userId, projectId, countryCode) {
if (isAnalyticsDisabled() || isSmokeTestUser(userId)) {
return
}
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'
})
})
}
// prepare the request: set `fromv2` param and full URL
const prepareAnalyticsRequest = function(options) {
if (settings.overleaf != null) {
options.qs = Object.assign({}, options.qs, { fromV2: 1 })
function isSmokeTestUser(userId) {
const smokeTestUserId = Settings.smokeTest && Settings.smokeTest.userId
return smokeTestUserId != null && userId.toString() === smokeTestUserId
}
const urlPath = options.url
options.url = `${settings.apis.analytics.url}${urlPath}`
options.timeout = options.timeout || 30000
return options
}
// make the request to analytics after checking and preparing it.
// request happens asynchronously in the background and will be retried on error
const makeAnalyticsBackgroundRequest = function(userId, options, callback) {
let { error, skip } = checkAnalyticsRequest(userId)
if (error || skip) {
return callback(error)
}
prepareAnalyticsRequest(options)
// With the tweaked parameter values (BACKOFF_BASE=3000, BACKOFF_MULTIPLIER=3):
// - the 6th attempt (maxAttempts=6) will run after 5.5 to 11.5 minutes
// - the 9th attempt (maxAttempts=9) will run after 86 to 250 minutes
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 = {
identifyUser(userId, oldUserId, callback) {
if (!callback) {
// callback is optional
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)
}
identifyUser,
recordEvent,
updateEditingSession
}

View file

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

View file

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

View file

@ -245,7 +245,8 @@ module.exports = function(webRouter, privateApiRouter, publicApiRouter) {
})
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'])
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 ""
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
# -----------------
@ -616,6 +621,9 @@ module.exports = settings =
everyone: process.env['RATE_LIMIT_AUTO_COMPILE_EVERYONE'] or 100
standard: process.env['RATE_LIMIT_AUTO_COMPILE_STANDARD'] or 25
analytics:
enabled: process.env['ANALYTICS_ENABLED'] == 'true'
# currentImage: "texlive-full:2017.1"
# imageRoot: "<DOCKER REPOSITORY ROOT>" # without any trailing slash

View file

@ -7,7 +7,7 @@
"@auth0/thumbprint": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@auth0/thumbprint/-/thumbprint-0.0.6.tgz",
"integrity": "sha1-yrEGLGwEZizmxZLUgVfsQmiuhRg=",
"integrity": "sha512-+YciWHxNUOE78T+xoXI1fMI6G1WdyyAay8ioaMZhvGOJ+lReYzj0b7mpfNr5WtjGrmtWPvPOOxh0TO+5Y2M/Hw==",
"dev": true
},
"@babel/cli": {
@ -2889,9 +2889,9 @@
},
"dependencies": {
"@types/node": {
"version": "13.13.29",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.29.tgz",
"integrity": "sha512-WPGpyEDx4/F4Rx1p1Zar8m+JsMxpSY/wNFPlyNXWV+UzJwkYt3LQg2be/qJgpsLdVJsfxTR5ipY6rv2579jStQ=="
"version": "13.13.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.30.tgz",
"integrity": "sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA=="
},
"protobufjs": {
"version": "6.10.1",
@ -3817,9 +3817,9 @@
}
},
"@overleaf/metrics": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@overleaf/metrics/-/metrics-3.3.0.tgz",
"integrity": "sha512-y+t0ZUW3WC90PrDH6KTDig4S4wRtL/0Uay+DFG2iuxFTaxYvf0CA1ebZidj2QyRZm6Yf0FNOJnSRp55fcrAadA==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@overleaf/metrics/-/metrics-3.4.0.tgz",
"integrity": "sha512-MsD+5d7gpoz2VyJ5AnEP1VHGyHsfya7bfkdnMgi93kS+FPcYvdpcooBXPq4jmGemEVxXhdyP9lBTJDwcdjZeiQ==",
"requires": {
"@google-cloud/debug-agent": "^5.1.2",
"@google-cloud/profiler": "^4.0.3",
@ -12154,6 +12154,175 @@
"integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==",
"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": {
"version": "1.8.14",
"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": {
"version": "5.1.0",
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
@ -17375,7 +17558,7 @@
"flowstate": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/flowstate/-/flowstate-0.4.1.tgz",
"integrity": "sha1-tfu4t/wte9xbVL5GyYMJ73NvTsA=",
"integrity": "sha512-U67AgveyMwXFIiDgs6Yz/PrUNrZGLJUUMDwJ9Q0fDFTQSzyDg8Jj9YDyZIUnFZKggQZONVueK9+grp/Gxa/scw==",
"dev": true,
"requires": {
"clone": "^1.0.2",
@ -18378,11 +18561,11 @@
}
},
"gcp-metadata": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.0.tgz",
"integrity": "sha512-vQZD57cQkqIA6YPGXM/zc+PIZfNRFdukWGsGZ5+LcJzesi5xp6Gn7a02wRJi4eXPyArNMIYpPET4QMxGqtlk6Q==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz",
"integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==",
"requires": {
"gaxios": "^3.0.0",
"gaxios": "^4.0.0",
"json-bigint": "^1.0.0"
},
"dependencies": {
@ -18391,23 +18574,6 @@
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
@ -18441,6 +18607,11 @@
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"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": {
"version": "3.0.0",
"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==",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz",
@ -23660,7 +23859,7 @@
},
"mkdirp": {
"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==",
"requires": {
"minimist": "0.0.8"
@ -23835,6 +24034,14 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"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": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.2.tgz",
@ -25283,8 +25490,7 @@
"object-inspect": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
"integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==",
"dev": true
"integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw=="
},
"object-is": {
"version": "1.1.2",
@ -27802,7 +28008,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.2.tgz",
"integrity": "sha512-A2HuJWl2opDH0EafgdjwEw7HysI8ff/n4lW4QEVBCUXFk9QeGecBWv0Deph0UmLe3tTNYegz8MOjsVuE6SMoJA==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.0",
@ -27813,7 +28018,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": {
"object-keys": "^1.0.12"
}
@ -27822,7 +28026,6 @@
"version": "1.17.6",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==",
"dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
@ -27841,7 +28044,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"dev": true,
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
@ -27852,7 +28054,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
@ -27860,20 +28061,17 @@
"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==",
"dev": true
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
},
"is-callable": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz",
"integrity": "sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==",
"dev": true
"integrity": "sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg=="
},
"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==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
@ -27882,7 +28080,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
@ -27890,8 +28087,7 @@
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
}
}
},
@ -33366,7 +33562,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
"integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
@ -33376,7 +33571,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": {
"object-keys": "^1.0.12"
}
@ -33385,7 +33579,6 @@
"version": "1.17.5",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
"integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
"dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
@ -33404,7 +33597,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"dev": true,
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
@ -33415,7 +33607,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
@ -33423,20 +33614,17 @@
"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==",
"dev": true
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
},
"is-callable": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
"integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
"dev": true
"integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q=="
},
"is-regex": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
"integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
"dev": true,
"requires": {
"has": "^1.0.3"
}
@ -33445,7 +33633,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
@ -33453,14 +33640,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==",
"dev": true
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"string.prototype.trimleft": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz",
"integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5",
@ -33471,7 +33656,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz",
"integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5",
@ -33538,7 +33722,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
"integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
@ -33548,7 +33731,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": {
"object-keys": "^1.0.12"
}
@ -33557,7 +33739,6 @@
"version": "1.17.5",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
"integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
"dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
@ -33576,7 +33757,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"dev": true,
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
@ -33587,7 +33767,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
@ -33595,20 +33774,17 @@
"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==",
"dev": true
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
},
"is-callable": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
"integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
"dev": true
"integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q=="
},
"is-regex": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
"integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
"dev": true,
"requires": {
"has": "^1.0.3"
}
@ -33617,7 +33793,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
@ -33625,14 +33800,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==",
"dev": true
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"string.prototype.trimleft": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz",
"integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5",
@ -33643,7 +33816,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz",
"integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5",
@ -35690,7 +35862,7 @@
"utils-flatten": {
"version": "1.0.0",
"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
},
"utils-merge": {
@ -37651,7 +37823,7 @@
"xml-name-validator": {
"version": "2.0.1",
"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
},
"xml2js": {

View file

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

View file

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

View file

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