2022-04-07 07:54:38 -04:00
|
|
|
const OError = require('@overleaf/o-error')
|
2021-10-27 08:06:50 -04:00
|
|
|
const RATE_LIMIT_MAX_ERRORS = 5
|
|
|
|
const RATE_LIMIT_INTERVAL_MS = 60000
|
|
|
|
|
|
|
|
class SentryManager {
|
|
|
|
constructor(dsn, options) {
|
|
|
|
this.Sentry = require('@sentry/node')
|
|
|
|
this.Sentry.init({ dsn, ...options })
|
|
|
|
// for rate limiting on sentry reporting
|
|
|
|
this.lastErrorTimeStamp = 0
|
|
|
|
this.lastErrorCount = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
captureExceptionRateLimited(attributes, message) {
|
|
|
|
const now = Date.now()
|
|
|
|
// have we recently reported an error?
|
|
|
|
const recentSentryReport =
|
|
|
|
now - this.lastErrorTimeStamp < RATE_LIMIT_INTERVAL_MS
|
|
|
|
// if so, increment the error count
|
|
|
|
if (recentSentryReport) {
|
|
|
|
this.lastErrorCount++
|
|
|
|
} else {
|
|
|
|
this.lastErrorCount = 0
|
|
|
|
this.lastErrorTimeStamp = now
|
|
|
|
}
|
|
|
|
// only report 5 errors every minute to avoid overload
|
|
|
|
if (this.lastErrorCount < RATE_LIMIT_MAX_ERRORS) {
|
|
|
|
// add a note if the rate limit has been hit
|
|
|
|
const note =
|
|
|
|
this.lastErrorCount + 1 === RATE_LIMIT_MAX_ERRORS
|
|
|
|
? '(rate limited)'
|
|
|
|
: ''
|
|
|
|
// report the exception
|
|
|
|
this.captureException(attributes, message, `error${note}`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
captureException(attributes, message, level) {
|
|
|
|
// handle case of logger.error "message"
|
|
|
|
if (typeof attributes === 'string') {
|
|
|
|
attributes = { err: new Error(attributes) }
|
|
|
|
}
|
|
|
|
|
|
|
|
// extract any error object
|
|
|
|
let error = attributes.err || attributes.error
|
|
|
|
|
|
|
|
// avoid reporting errors twice
|
|
|
|
for (const key in attributes) {
|
|
|
|
const value = attributes[key]
|
|
|
|
if (value instanceof Error && value.reportedToSentry) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// include our log message in the error report
|
|
|
|
if (error == null) {
|
|
|
|
if (typeof message === 'string') {
|
|
|
|
error = { message }
|
|
|
|
}
|
|
|
|
} else if (message != null) {
|
|
|
|
attributes.description = message
|
|
|
|
}
|
|
|
|
|
|
|
|
// report the error
|
|
|
|
if (error != null) {
|
|
|
|
// capture attributes and use *_id objects as tags
|
|
|
|
const tags = {}
|
|
|
|
const extra = {}
|
|
|
|
for (const key in attributes) {
|
|
|
|
const value = attributes[key]
|
|
|
|
if (key.match(/_id/) && typeof value === 'string') {
|
|
|
|
tags[key] = value
|
|
|
|
}
|
|
|
|
extra[key] = value
|
|
|
|
}
|
|
|
|
|
|
|
|
// capture req object if available
|
|
|
|
const { req } = attributes
|
|
|
|
if (req != null) {
|
|
|
|
extra.req = {
|
|
|
|
method: req.method,
|
|
|
|
url: req.originalUrl,
|
|
|
|
query: req.query,
|
|
|
|
headers: req.headers,
|
2021-12-16 04:04:32 -05:00
|
|
|
ip: req.ip,
|
2021-10-27 08:06:50 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// recreate error objects that have been converted to a normal object
|
|
|
|
if (!(error instanceof Error) && typeof error === 'object') {
|
|
|
|
const newError = new Error(error.message)
|
|
|
|
for (const key of Object.keys(error || {})) {
|
|
|
|
const value = error[key]
|
|
|
|
newError[key] = value
|
|
|
|
}
|
|
|
|
error = newError
|
|
|
|
}
|
|
|
|
|
2022-04-07 07:54:38 -04:00
|
|
|
// OError integration
|
|
|
|
extra.info = OError.getFullInfo(error)
|
|
|
|
|
2021-10-27 08:06:50 -04:00
|
|
|
// filter paths from the message to avoid duplicate errors in sentry
|
|
|
|
// (e.g. errors from `fs` methods which have a path attribute)
|
|
|
|
try {
|
|
|
|
if (error.path) {
|
|
|
|
error.message = error.message.replace(` '${error.path}'`, '')
|
|
|
|
}
|
|
|
|
|
|
|
|
// send the error to sentry
|
|
|
|
this.Sentry.captureException(error, { tags, extra, level })
|
|
|
|
|
|
|
|
// put a flag on the errors to avoid reporting them multiple times
|
|
|
|
for (const key in attributes) {
|
|
|
|
const value = attributes[key]
|
|
|
|
if (value instanceof Error) {
|
|
|
|
value.reportedToSentry = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
// ignore Sentry errors
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = SentryManager
|