sentry-manager.js 3.5 KB

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