import * as Sentry from '@sentry/sveltekit' import { XrpcResponseError } from '@atproto/lex' import { BlobRateLimitError, isAuthFactorError } from '@pds-moover/moover' /** * XRPC error codes that describe something the user did or has to do, not something broken: a * wrong password, a 2FA code to enter, a mistyped invite code or PLC token, a handle somebody * else holds. The page shows these as its own message and the user fixes them; Sentry would only * see the same handful of codes over and over. */ const USER_FACING_XRPC_ERRORS = new Set([ 'AuthFactorTokenRequired', 'AuthenticationRequired', 'InvalidToken', 'ExpiredToken', 'InvalidInviteCode', 'HandleNotAvailable', 'InvalidHandle', 'InvalidPassword', 'InvalidEmail', 'UnsupportedDomain', 'RateLimitExceeded', ]) /** * True for an error the UI already expects and handles on its own, so it should not be sent to * Sentry: a 2FA prompt, a user choosing to stop at a rate limit, a request the page cancelled, or * one of the user-facing XRPC codes above. */ export function isExpectedError(error: unknown): boolean { if (isAuthFactorError(error)) return true if (error instanceof BlobRateLimitError) return true if (error instanceof XrpcResponseError) { if (error.status === 429) return true if (USER_FACING_XRPC_ERRORS.has(error.error)) return true } // A fetch the page itself cancelled (superseded typeahead, navigation away). if (error instanceof Error && error.name === 'AbortError') return true return false } export interface ReportOptions { /** 'warning' for best-effort work the page recovers from on its own; 'error' otherwise. */ level?: 'error' | 'warning' /** Extra, non-identifying context for the event. Never handles, DIDs or emails. */ extra?: Record } /** * Sends a caught error to Sentry, unless it is one the UI expects (see {@link isExpectedError}). * `op` names what was being attempted (e.g. `migration.run`) and becomes the `op` tag, so events * group by step rather than by whichever message the PDS happened to answer with. Does not log: * callers keep their own console output. A no-op while the DSN is unset (local dev, e2e). * * @returns true when the error was reported, false when it was filtered as expected. */ export function reportError( error: unknown, op: string, { level = 'error', extra }: ReportOptions = {}, ): boolean { if (isExpectedError(error)) return false const context: Record = { ...extra } if (error instanceof XrpcResponseError) { context.xrpc_error = error.error context.xrpc_status = error.status } Sentry.captureException(error, { level, tags: { op }, extra: context }) return true }