diff --git a/src/components/error/ErrorInfo.tsx b/src/components/error/ErrorInfo.tsx index d2bc50d..8043afd 100644 --- a/src/components/error/ErrorInfo.tsx +++ b/src/components/error/ErrorInfo.tsx @@ -11,6 +11,7 @@ type Props = { message: string; statusCode?: number; originalPath?: string; + stack?: string; /** props specific to error boundary. */ misc?: { subtext: string; @@ -19,7 +20,9 @@ type Props = { }; }; -const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => { +const isDev = process.env.NODE_ENV === 'development'; + +const ErrorInfo = ({ message, statusCode, misc, originalPath, stack }: Props) => { const title = statusCode ? `${message} (${statusCode})` : message; return ( <> @@ -39,6 +42,11 @@ const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {

{title}

+ {Boolean(stack && isDev) && ( +
+            {stack}
+          
+ )} {misc ? ( <>

{misc.subtext}

@@ -64,6 +72,13 @@ const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => { .

)} +

+ If you think this shouldn't happen,{' '} + + let it be known + + . +

); diff --git a/src/interfaces/shared/error.ts b/src/interfaces/shared/error.ts index 9beabf8..1d2d82f 100644 --- a/src/interfaces/shared/error.ts +++ b/src/interfaces/shared/error.ts @@ -1,3 +1,3 @@ import { AppError as AppErrorClass } from 'src/utils/helpers'; -export type AppError = Omit, 'name'>; +export type AppError = Pick, 'message' | 'statusCode' | 'stack'>; diff --git a/src/pages/find/index.tsx b/src/pages/find/index.tsx index 7d8dd0a..444d176 100644 --- a/src/pages/find/index.tsx +++ b/src/pages/find/index.tsx @@ -67,13 +67,13 @@ export const getServerSideProps: GetServerSideProps = asy props: { data: { title: query, results: res }, error: null, originalPath }, }; } catch (error) { - const { message, statusCode } = getErrorProperties(error); - ctx.res.statusCode = statusCode; - ctx.res.statusMessage = message; + const err = getErrorProperties(error); + ctx.res.statusCode = err.statusCode; + ctx.res.statusMessage = err.message; return { props: { - error: { message, statusCode }, + error: { message: err.message, statusCode: err.statusCode, stack: err.format() }, data: { title: query, results: null }, originalPath, }, diff --git a/src/pages/name/[nameId]/index.tsx b/src/pages/name/[nameId]/index.tsx index b3967d9..73f2060 100644 --- a/src/pages/name/[nameId]/index.tsx +++ b/src/pages/name/[nameId]/index.tsx @@ -55,11 +55,17 @@ export const getServerSideProps: GetServerSideProps = async ctx => return { props: { data, error: null, originalPath } }; } catch (error) { - const { message, statusCode } = getErrorProperties(error); - ctx.res.statusCode = statusCode; - ctx.res.statusMessage = message; + const err = getErrorProperties(error); + ctx.res.statusCode = err.statusCode; + ctx.res.statusMessage = err.message; - return { props: { error: { message, statusCode }, data: null, originalPath } }; + return { + props: { + error: { message: err.message, statusCode: err.statusCode, stack: err.format() }, + data: null, + originalPath, + }, + }; } }; diff --git a/src/pages/title/[titleId]/index.tsx b/src/pages/title/[titleId]/index.tsx index 51ea836..a6ed616 100644 --- a/src/pages/title/[titleId]/index.tsx +++ b/src/pages/title/[titleId]/index.tsx @@ -5,7 +5,7 @@ import ErrorInfo from 'src/components/error/ErrorInfo'; import Media from 'src/components/media/Media'; import { Basic, Cast, DidYouKnow, Info, MoreLikeThis, Reviews } from 'src/components/title'; import Title from 'src/interfaces/shared/title'; -import { AppError } from 'src/interfaces/shared/error'; +import type { AppError } from 'src/interfaces/shared/error'; import getOrSetApiCache from 'src/utils/getOrSetApiCache'; import title from 'src/utils/fetchers/title'; import { getErrorProperties, getProxiedIMDbImgUrl } from 'src/utils/helpers'; @@ -63,12 +63,18 @@ export const getServerSideProps: GetServerSideProps = async ctx => const data = await getOrSetApiCache(titleKey(titleId), title, titleId); return { props: { data, error: null, originalPath } }; - } catch (error) { - const { message, statusCode } = getErrorProperties(error); - ctx.res.statusCode = statusCode; - ctx.res.statusMessage = message; + } catch (e) { + const err = getErrorProperties(e); + ctx.res.statusCode = err.statusCode; + ctx.res.statusMessage = err.message; - return { props: { error: { message, statusCode }, data: null, originalPath } }; + const error = { + message: err.message, + statusCode: err.statusCode, + stack: err.format(), + }; + console.error(err); + return { props: { error, data: null, originalPath } }; } }; diff --git a/src/styles/modules/components/error/error-info.module.scss b/src/styles/modules/components/error/error-info.module.scss index 1cfa76b..51f3533 100644 --- a/src/styles/modules/components/error/error-info.module.scss +++ b/src/styles/modules/components/error/error-info.module.scss @@ -8,7 +8,7 @@ display: grid; justify-content: center; justify-items: center; - gap: var(--spacer-1); + gap: var(--comp-whitespace); @include helper.bp('bp-700') { --doc-whitespace: var(--spacer-5); @@ -31,6 +31,19 @@ text-align: center; } +.stack { + max-width: 90%; + max-height: 20rem; + padding: var(--spacer-3); + white-space: pre-wrap; + overflow: scroll; + + user-select: all; + + border-radius: var(--spacer-1); + background-color: var(--clr-bg-muted); +} + .button { align-self: end; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 2a483ed..a19b133 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -74,12 +74,33 @@ export const getProxiedIMDbImgUrl = (url: string) => { }; export const AppError = class extends Error { - constructor(message: string, public statusCode: number, errorOptions?: unknown) { - const saneErrorOptions = getErrorOptions(errorOptions); - super(message, saneErrorOptions); + constructor(message: string, public statusCode: number, cause?: unknown) { + const _cause = cause ? AppError.toError(cause) : undefined; + super(message, { cause: _cause }); Error.captureStackTrace(this, AppError); - if (process.env.NODE_ENV === 'development') console.error(this); + } + + static toError(err: unknown) { + if (err instanceof Error) return err; + return new Error(`Unexpected: ${JSON.stringify(err)}`); + } + + format() { + let str = ''; + let cur: Error | null = this; + let depth = 0; + + while (cur && depth <= 4) { + if (cur.stack) str += `${cur.stack}\n`; + else str += `${cur.name}: ${cur.message}\n`; + + cur = cur.cause instanceof Error ? cur.cause : null; + if (cur) str += 'Caused by:\n'; + depth++; + } + + return str.trimEnd(); } }; @@ -110,19 +131,6 @@ export const isLocalStorageAvailable = () => { } }; -const getErrorOptions = (error: unknown): ErrorOptions | undefined => { - if (!error || typeof error !== 'object') return undefined; - - let cause: unknown; - // @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded - if ('cause' in error) cause = error.cause; - // @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded - else if ('stack' in error) cause = error.stack; - - // @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded - return { cause }; -}; - export const getErrorProperties = ( error: unknown, message = 'Something went very wrong', @@ -130,4 +138,4 @@ export const getErrorProperties = ( ) => { if (error instanceof AppError) return error; return new AppError(message, statusCode, error); -}; \ No newline at end of file +};