| | import type { ErrorInfo } from 'react' |
| | import stringHash from 'next/dist/compiled/string-hash' |
| |
|
| | import { formatServerError } from '../../lib/format-server-error' |
| | import { SpanStatusCode, getTracer } from '../lib/trace/tracer' |
| |
|
| | import { isAbortError } from '../pipe-readable' |
| | import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' |
| | import { isDynamicServerError } from '../../client/components/hooks-server-context' |
| | import { isNextRouterError } from '../../client/components/is-next-router-error' |
| | import { isPrerenderInterruptedError } from './dynamic-rendering' |
| | import { getProperError } from '../../lib/is-error' |
| | import { createDigestWithErrorCode } from '../../lib/error-telemetry-utils' |
| | import { isReactLargeShellError } from './react-large-shell-error' |
| |
|
| | declare global { |
| | var __next_log_error__: undefined | ((err: unknown) => void) |
| | } |
| |
|
| | type RSCErrorHandler = (err: unknown) => string | undefined |
| | type SSRErrorHandler = ( |
| | err: unknown, |
| | errorInfo?: ErrorInfo |
| | ) => string | undefined |
| |
|
| | export type DigestedError = Error & { digest: string; environmentName?: string } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | export function getDigestForWellKnownError(error: unknown): string | undefined { |
| | |
| | if (isBailoutToCSRError(error)) return error.digest |
| |
|
| | |
| | if (isNextRouterError(error)) return error.digest |
| |
|
| | |
| | |
| | |
| | |
| | if (isDynamicServerError(error)) return error.digest |
| |
|
| | |
| | if (isPrerenderInterruptedError(error)) return error.digest |
| |
|
| | return undefined |
| | } |
| |
|
| | export function createReactServerErrorHandler( |
| | shouldFormatError: boolean, |
| | isNextExport: boolean, |
| | reactServerErrors: Map<string, DigestedError>, |
| | onReactServerRenderError: (err: DigestedError, silenceLog: boolean) => void, |
| | spanToRecordOn?: any |
| | ): RSCErrorHandler { |
| | return (thrownValue: unknown) => { |
| | if (typeof thrownValue === 'string') { |
| | |
| | return stringHash(thrownValue).toString() |
| | } |
| |
|
| | |
| | if (isAbortError(thrownValue)) return |
| |
|
| | const digest = getDigestForWellKnownError(thrownValue) |
| |
|
| | if (digest) { |
| | return digest |
| | } |
| |
|
| | if (isReactLargeShellError(thrownValue)) { |
| | |
| | console.error(thrownValue) |
| | return undefined |
| | } |
| |
|
| | let err = getProperError(thrownValue) as DigestedError |
| | let silenceLog = false |
| |
|
| | |
| | |
| | if (err.digest) { |
| | if ( |
| | process.env.NODE_ENV === 'production' && |
| | reactServerErrors.has(err.digest) |
| | ) { |
| | |
| | |
| | |
| | err = reactServerErrors.get(err.digest)! |
| | |
| | |
| | silenceLog = true |
| | } else { |
| | |
| | |
| | |
| | } |
| | } else { |
| | err.digest = createDigestWithErrorCode( |
| | err, |
| | |
| | stringHash(err.message + (err.stack || '')).toString() |
| | ) |
| | } |
| |
|
| | |
| | |
| | if (!reactServerErrors.has(err.digest)) { |
| | reactServerErrors.set(err.digest, err) |
| | } |
| |
|
| | |
| | if (shouldFormatError) { |
| | formatServerError(err) |
| | } |
| |
|
| | |
| | if ( |
| | !( |
| | isNextExport && |
| | err?.message?.includes( |
| | 'The specific message is omitted in production builds to avoid leaking sensitive details.' |
| | ) |
| | ) |
| | ) { |
| | |
| | const span = spanToRecordOn ?? getTracer().getActiveScopeSpan() |
| | if (span) { |
| | span.recordException(err) |
| | span.setAttribute('error.type', err.name) |
| | span.setStatus({ |
| | code: SpanStatusCode.ERROR, |
| | message: err.message, |
| | }) |
| | } |
| |
|
| | onReactServerRenderError(err, silenceLog) |
| | } |
| |
|
| | return err.digest |
| | } |
| | } |
| |
|
| | export function createHTMLErrorHandler( |
| | shouldFormatError: boolean, |
| | isNextExport: boolean, |
| | reactServerErrors: Map<string, DigestedError>, |
| | allCapturedErrors: Array<unknown>, |
| | onHTMLRenderSSRError: (err: DigestedError, errorInfo?: ErrorInfo) => void, |
| | spanToRecordOn?: any |
| | ): SSRErrorHandler { |
| | return (thrownValue: unknown, errorInfo?: ErrorInfo) => { |
| | if (isReactLargeShellError(thrownValue)) { |
| | |
| | console.error(thrownValue) |
| | return undefined |
| | } |
| |
|
| | let isSSRError = true |
| |
|
| | allCapturedErrors.push(thrownValue) |
| |
|
| | |
| | if (isAbortError(thrownValue)) return |
| |
|
| | const digest = getDigestForWellKnownError(thrownValue) |
| |
|
| | if (digest) { |
| | return digest |
| | } |
| |
|
| | const err = getProperError(thrownValue) as DigestedError |
| |
|
| | |
| | |
| | if (err.digest) { |
| | if (reactServerErrors.has(err.digest)) { |
| | |
| | |
| | thrownValue = reactServerErrors.get(err.digest) |
| | isSSRError = false |
| | } else { |
| | |
| | |
| | } |
| | } else { |
| | err.digest = createDigestWithErrorCode( |
| | err, |
| | stringHash( |
| | err.message + (errorInfo?.componentStack || err.stack || '') |
| | ).toString() |
| | ) |
| | } |
| |
|
| | |
| | if (shouldFormatError) { |
| | formatServerError(err) |
| | } |
| |
|
| | |
| | if ( |
| | !( |
| | isNextExport && |
| | err?.message?.includes( |
| | 'The specific message is omitted in production builds to avoid leaking sensitive details.' |
| | ) |
| | ) |
| | ) { |
| | |
| | if (isSSRError) { |
| | |
| | const span = spanToRecordOn ?? getTracer().getActiveScopeSpan() |
| | if (span) { |
| | span.recordException(err) |
| | span.setAttribute('error.type', err.name) |
| | span.setStatus({ |
| | code: SpanStatusCode.ERROR, |
| | message: err.message, |
| | }) |
| | } |
| |
|
| | onHTMLRenderSSRError(err, errorInfo) |
| | } |
| | } |
| |
|
| | return err.digest |
| | } |
| | } |
| |
|
| | export function isUserLandError(err: any): boolean { |
| | return ( |
| | !isAbortError(err) && !isBailoutToCSRError(err) && !isNextRouterError(err) |
| | ) |
| | } |
| |
|