overleaf/libraries/logger/sentry-manager.js
Jakob Ackermann ea55a850f9 Merge pull request #20761 from overleaf/jpa-error-instance
[logger] forward an Error instance to Sentry.captureException

GitOrigin-RevId: 3a310994622c85f3791181b87d4d382c34f6fd32
2024-10-14 10:58:16 +00:00

106 lines
3 KiB
JavaScript

const Serializers = require('./serializers')
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 = Serializers.err(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) {
let value = attributes[key]
if (Serializers[key]) {
value = Serializers[key](value)
}
if (key.match(/_id/) && typeof value === 'string') {
tags[key] = value
}
extra[key] = value
}
// OError integration
extra.info = error.info
delete error.info
// Sentry wants to receive an Error instance.
const errInstance = new Error(error.message)
Object.assign(errInstance, error)
try {
// send the error to sentry
this.Sentry.captureException(errInstance, { 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