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