fix(error): add trace to browser in dev mode
also make AppError a bit easier to use
This commit is contained in:
@ -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) => {
|
||||
<use href='/svg/sadgnu.svg#sad-gnu'></use>
|
||||
</svg>
|
||||
<h1 className={`heading heading__primary ${styles.heading}`}>{title}</h1>
|
||||
{Boolean(stack && isDev) && (
|
||||
<pre className={styles.stack}>
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
{misc ? (
|
||||
<>
|
||||
<p>{misc.subtext}</p>
|
||||
@ -64,6 +72,13 @@ const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
If you think this shouldn't happen,{' '}
|
||||
<Link href='/contact'>
|
||||
<a className='link'>let it be known</a>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
|
@ -1,3 +1,3 @@
|
||||
import { AppError as AppErrorClass } from 'src/utils/helpers';
|
||||
|
||||
export type AppError = Omit<InstanceType<typeof AppErrorClass>, 'name'>;
|
||||
export type AppError = Pick<InstanceType<typeof AppErrorClass>, 'message' | 'statusCode' | 'stack'>;
|
||||
|
@ -67,13 +67,13 @@ export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = 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,
|
||||
},
|
||||
|
@ -55,11 +55,17 @@ export const getServerSideProps: GetServerSideProps<Data, Params> = 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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<Data, Params> = 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 } };
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
Reference in New Issue
Block a user