error-reporting.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import * as Sentry from '@sentry/nextjs'
  2. import { ResponseError } from '@/types'
  3. type CaptureMessageOptions = {
  4. context: string
  5. message: string
  6. }
  7. const WHITELIST_ERRORS = [
  8. // Common validation errors
  9. 'email must be an email',
  10. 'Password is known to be weak and easy to guess, please choose a different one',
  11. // Authentication errors
  12. 'A user with this email already exists',
  13. 'Password should contain at least one character of each',
  14. 'You attempted to send email to an inactive recipient',
  15. 'New password should be different from the old password',
  16. 'Invalid TOTP code entered',
  17. 'No SSO provider assigned for this domain',
  18. // Project creation errors
  19. 'The following organization members have reached their maximum limits for the number of active free projects',
  20. 'db_pass must be longer than or equal to 4 characters',
  21. 'There are overdue invoices in the organization(s)',
  22. 'name should not contain a . string',
  23. 'Project creation in the Briven dashboard is disabled for this Vercel-managed organization.',
  24. 'Your account, which is handled by the Fly Briven extension, cannot access this endpoint.',
  25. 'already exists in your organization.',
  26. ]
  27. /**
  28. * Captures a critical error to Sentry, filtering out whitelisted errors.
  29. *
  30. * @param error - The error object (ResponseError, Error, or any object with a message property)
  31. * @param context - The context/action that failed (e.g., 'reset password', 'sign up', 'create project')
  32. * Attached as the `context` tag on the Sentry event.
  33. */
  34. export function captureCriticalError(
  35. error: ResponseError | Error | { message: string },
  36. context: string
  37. ): void {
  38. if (!error.message) {
  39. return
  40. }
  41. if (error instanceof ResponseError) {
  42. handleResponseError(error, context)
  43. return
  44. }
  45. if (error instanceof Error) {
  46. handleError(error, context)
  47. return
  48. }
  49. handleUnknownAPIResponseError(error, context)
  50. }
  51. function handleResponseError(error: ResponseError, context: string) {
  52. const { code, message, requestPathname } = error
  53. if (!requestPathname || !code) {
  54. captureMessage({
  55. message: `Response Error (no code or requestPathname) w/ message: ${error.message}`,
  56. context,
  57. })
  58. return
  59. }
  60. if (code >= 500) {
  61. // Only capture 5XX errors as critical errors
  62. captureMessage({
  63. context,
  64. message: `requestPathname ${requestPathname} w/ message: ${message}`,
  65. })
  66. return
  67. }
  68. }
  69. function handleError(error: Error, context: string) {
  70. if (!error.message) {
  71. return
  72. }
  73. captureMessage({
  74. message: error.message,
  75. context,
  76. })
  77. }
  78. function handleUnknownAPIResponseError(error: unknown, context: string) {
  79. if (
  80. error &&
  81. typeof error === 'object' &&
  82. 'message' in error &&
  83. typeof error.message === 'string'
  84. ) {
  85. captureMessage({
  86. message: error.message,
  87. context,
  88. })
  89. }
  90. }
  91. function captureMessage({ message, context }: CaptureMessageOptions) {
  92. if (WHITELIST_ERRORS.some((whitelisted) => message.includes(whitelisted))) {
  93. return
  94. }
  95. // Use captureException (vs captureMessage) so these appear as exceptions in Sentry
  96. // and can have dedicated alert rules. Grouping is still by message since all
  97. // CriticalErrors share the same synthetic stack trace.
  98. Sentry.withScope((scope) => {
  99. scope.setTag('critical', 'true')
  100. scope.setTag('context', context)
  101. const error = new Error(message)
  102. error.name = `CriticalError`
  103. Sentry.captureException(error)
  104. })
  105. }