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;
|
message: string;
|
||||||
statusCode?: number;
|
statusCode?: number;
|
||||||
originalPath?: string;
|
originalPath?: string;
|
||||||
|
stack?: string;
|
||||||
/** props specific to error boundary. */
|
/** props specific to error boundary. */
|
||||||
misc?: {
|
misc?: {
|
||||||
subtext: string;
|
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;
|
const title = statusCode ? `${message} (${statusCode})` : message;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -39,6 +42,11 @@ const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
|
|||||||
<use href='/svg/sadgnu.svg#sad-gnu'></use>
|
<use href='/svg/sadgnu.svg#sad-gnu'></use>
|
||||||
</svg>
|
</svg>
|
||||||
<h1 className={`heading heading__primary ${styles.heading}`}>{title}</h1>
|
<h1 className={`heading heading__primary ${styles.heading}`}>{title}</h1>
|
||||||
|
{Boolean(stack && isDev) && (
|
||||||
|
<pre className={styles.stack}>
|
||||||
|
<code>{stack}</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
{misc ? (
|
{misc ? (
|
||||||
<>
|
<>
|
||||||
<p>{misc.subtext}</p>
|
<p>{misc.subtext}</p>
|
||||||
@ -64,6 +72,13 @@ const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
|
|||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
<p>
|
||||||
|
If you think this shouldn't happen,{' '}
|
||||||
|
<Link href='/contact'>
|
||||||
|
<a className='link'>let it be known</a>
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import { AppError as AppErrorClass } from 'src/utils/helpers';
|
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 },
|
props: { data: { title: query, results: res }, error: null, originalPath },
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { message, statusCode } = getErrorProperties(error);
|
const err = getErrorProperties(error);
|
||||||
ctx.res.statusCode = statusCode;
|
ctx.res.statusCode = err.statusCode;
|
||||||
ctx.res.statusMessage = message;
|
ctx.res.statusMessage = err.message;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
error: { message, statusCode },
|
error: { message: err.message, statusCode: err.statusCode, stack: err.format() },
|
||||||
data: { title: query, results: null },
|
data: { title: query, results: null },
|
||||||
originalPath,
|
originalPath,
|
||||||
},
|
},
|
||||||
|
@ -55,11 +55,17 @@ export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx =>
|
|||||||
|
|
||||||
return { props: { data, error: null, originalPath } };
|
return { props: { data, error: null, originalPath } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { message, statusCode } = getErrorProperties(error);
|
const err = getErrorProperties(error);
|
||||||
ctx.res.statusCode = statusCode;
|
ctx.res.statusCode = err.statusCode;
|
||||||
ctx.res.statusMessage = message;
|
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 Media from 'src/components/media/Media';
|
||||||
import { Basic, Cast, DidYouKnow, Info, MoreLikeThis, Reviews } from 'src/components/title';
|
import { Basic, Cast, DidYouKnow, Info, MoreLikeThis, Reviews } from 'src/components/title';
|
||||||
import Title from 'src/interfaces/shared/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 getOrSetApiCache from 'src/utils/getOrSetApiCache';
|
||||||
import title from 'src/utils/fetchers/title';
|
import title from 'src/utils/fetchers/title';
|
||||||
import { getErrorProperties, getProxiedIMDbImgUrl } from 'src/utils/helpers';
|
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);
|
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
|
||||||
|
|
||||||
return { props: { data, error: null, originalPath } };
|
return { props: { data, error: null, originalPath } };
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
const { message, statusCode } = getErrorProperties(error);
|
const err = getErrorProperties(e);
|
||||||
ctx.res.statusCode = statusCode;
|
ctx.res.statusCode = err.statusCode;
|
||||||
ctx.res.statusMessage = message;
|
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;
|
display: grid;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
gap: var(--spacer-1);
|
gap: var(--comp-whitespace);
|
||||||
|
|
||||||
@include helper.bp('bp-700') {
|
@include helper.bp('bp-700') {
|
||||||
--doc-whitespace: var(--spacer-5);
|
--doc-whitespace: var(--spacer-5);
|
||||||
@ -31,6 +31,19 @@
|
|||||||
text-align: center;
|
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 {
|
.button {
|
||||||
align-self: end;
|
align-self: end;
|
||||||
|
|
||||||
|
@ -74,12 +74,33 @@ export const getProxiedIMDbImgUrl = (url: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AppError = class extends Error {
|
export const AppError = class extends Error {
|
||||||
constructor(message: string, public statusCode: number, errorOptions?: unknown) {
|
constructor(message: string, public statusCode: number, cause?: unknown) {
|
||||||
const saneErrorOptions = getErrorOptions(errorOptions);
|
const _cause = cause ? AppError.toError(cause) : undefined;
|
||||||
super(message, saneErrorOptions);
|
super(message, { cause: _cause });
|
||||||
|
|
||||||
Error.captureStackTrace(this, AppError);
|
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 = (
|
export const getErrorProperties = (
|
||||||
error: unknown,
|
error: unknown,
|
||||||
message = 'Something went very wrong',
|
message = 'Something went very wrong',
|
||||||
@ -130,4 +138,4 @@ export const getErrorProperties = (
|
|||||||
) => {
|
) => {
|
||||||
if (error instanceof AppError) return error;
|
if (error instanceof AppError) return error;
|
||||||
return new AppError(message, statusCode, error);
|
return new AppError(message, statusCode, error);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user