sentry-manager.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. const OError = require('@overleaf/o-error')
  2. const RATE_LIMIT_MAX_ERRORS = 5
  3. const RATE_LIMIT_INTERVAL_MS = 60000
  4. class SentryManager {
  5. constructor(dsn, options) {
  6. this.Sentry = require('@sentry/node')
  7. this.Sentry.init({ dsn, ...options })
  8. // for rate limiting on sentry reporting
  9. this.lastErrorTimeStamp = 0
  10. this.lastErrorCount = 0
  11. }
  12. captureExceptionRateLimited(attributes, message) {
  13. const now = Date.now()
  14. // have we recently reported an error?
  15. const recentSentryReport =
  16. now - this.lastErrorTimeStamp < RATE_LIMIT_INTERVAL_MS
  17. // if so, increment the error count
  18. if (recentSentryReport) {
  19. this.lastErrorCount++
  20. } else {
  21. this.lastErrorCount = 0
  22. this.lastErrorTimeStamp = now
  23. }
  24. // only report 5 errors every minute to avoid overload
  25. if (this.lastErrorCount < RATE_LIMIT_MAX_ERRORS) {
  26. // add a note if the rate limit has been hit
  27. const note =
  28. this.lastErrorCount + 1 === RATE_LIMIT_MAX_ERRORS
  29. ? '(rate limited)'
  30. : ''
  31. // report the exception
  32. this.captureException(attributes, message, `error${note}`)
  33. }
  34. }
  35. captureException(attributes, message, level) {
  36. // handle case of logger.error "message"
  37. if (typeof attributes === 'string') {
  38. attributes = { err: new Error(attributes) }
  39. }
  40. // extract any error object
  41. let error = attributes.err || attributes.error
  42. // avoid reporting errors twice
  43. for (const key in attributes) {
  44. const value = attributes[key]
  45. if (value instanceof Error && value.reportedToSentry) {
  46. return
  47. }
  48. }
  49. // include our log message in the error report
  50. if (error == null) {
  51. if (typeof message === 'string') {
  52. error = { message }
  53. }
  54. } else if (message != null) {
  55. attributes.description = message
  56. }
  57. // report the error
  58. if (error != null) {
  59. // capture attributes and use *_id objects as tags
  60. const tags = {}
  61. const extra = {}
  62. for (const key in attributes) {
  63. const value = attributes[key]
  64. if (key.match(/_id/) && typeof value === 'string') {
  65. tags[key] = value
  66. }
  67. extra[key] = value
  68. }
  69. // capture req object if available
  70. const { req } = attributes
  71. if (req != null) {
  72. extra.req = {
  73. method: req.method,
  74. url: req.originalUrl,
  75. query: req.query,
  76. headers: req.headers,
  77. ip: req.ip,
  78. }
  79. }
  80. // recreate error objects that have been converted to a normal object
  81. if (!(error instanceof Error) && typeof error === 'object') {
  82. const newError = new Error(error.message)
  83. for (const key of Object.keys(error || {})) {
  84. const value = error[key]
  85. newError[key] = value
  86. }
  87. error = newError
  88. }
  89. // OError integration
  90. extra.info = OError.getFullInfo(error)
  91. // filter paths from the message to avoid duplicate errors in sentry
  92. // (e.g. errors from `fs` methods which have a path attribute)
  93. try {
  94. if (error.path) {
  95. error.message = error.message.replace(` '${error.path}'`, '')
  96. }
  97. // send the error to sentry
  98. this.Sentry.captureException(error, { tags, extra, level })
  99. // put a flag on the errors to avoid reporting them multiple times
  100. for (const key in attributes) {
  101. const value = attributes[key]
  102. if (value instanceof Error) {
  103. value.reportedToSentry = true
  104. }
  105. }
  106. } catch (err) {
  107. // ignore Sentry errors
  108. }
  109. }
  110. }
  111. }
  112. module.exports = SentryManager