MISC: Add error cause to exceptionAlert and Recovery mode (#1772)

This commit is contained in:
catloversg 2024-11-13 10:27:18 +07:00 committed by GitHub
parent 246d668951
commit 4f84a894eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 83 additions and 15 deletions

@ -50,7 +50,22 @@ export function AlertManager({ hidden }: { hidden: boolean }): React.ReactElemen
if (typeof text === "string") { if (typeof text === "string") {
return cyrb53(text); return cyrb53(text);
} }
return cyrb53(JSON.stringify(text.props)); /**
* JSON.stringify may throw an error in edge cases. One possible error is "TypeError: Converting circular structure
* to JSON". It may happen in very special cases. This is the flow of one of them:
* - An error occurred in GameRoot.tsx and we show a warning popup by calling "exceptionAlert" without delaying.
* - "exceptionAlert" constructs a React element and passes it via "dialogBoxCreate" -> "AlertEvents.emit".
* - When we receive the final React element here, the element's "props" property may contain a circular structure.
*/
let textPropsAsString;
try {
textPropsAsString = JSON.stringify(text.props);
} catch (e) {
console.error(e);
// Use the current timestamp as the fallback value.
textPropsAsString = Date.now().toString();
}
return cyrb53(textPropsAsString);
} }
function close(): void { function close(): void {

@ -54,6 +54,33 @@ export interface IErrorData {
export const newIssueUrl = `https://github.com/bitburner-official/bitburner-src/issues/new`; export const newIssueUrl = `https://github.com/bitburner-official/bitburner-src/issues/new`;
export function parseUnknownError(error: unknown): {
errorAsString: string;
stack?: string;
causeAsString?: string;
causeStack?: string;
} {
const errorAsString = String(error);
let stack: string | undefined = undefined;
let causeAsString: string | undefined = undefined;
let causeStack: string | undefined = undefined;
if (error instanceof Error) {
stack = error.stack;
if (error.cause != null) {
causeAsString = String(error.cause);
if (error.cause instanceof Error) {
causeStack = error.cause.stack;
}
}
}
return {
errorAsString,
stack,
causeAsString,
causeStack,
};
}
export function getErrorMetadata(error: unknown, errorInfo?: React.ErrorInfo, page?: Page): IErrorMetadata { export function getErrorMetadata(error: unknown, errorInfo?: React.ErrorInfo, page?: Page): IErrorMetadata {
const isElectron = navigator.userAgent.toLowerCase().includes(" electron/"); const isElectron = navigator.userAgent.toLowerCase().includes(" electron/");
const env = process.env.NODE_ENV === "development" ? GameEnv.Development : GameEnv.Production; const env = process.env.NODE_ENV === "development" ? GameEnv.Development : GameEnv.Production;
@ -85,12 +112,25 @@ export function getErrorMetadata(error: unknown, errorInfo?: React.ErrorInfo, pa
export function getErrorForDisplay(error: unknown, errorInfo?: React.ErrorInfo, page?: Page): IErrorData { export function getErrorForDisplay(error: unknown, errorInfo?: React.ErrorInfo, page?: Page): IErrorData {
const metadata = getErrorMetadata(error, errorInfo, page); const metadata = getErrorMetadata(error, errorInfo, page);
const errorData = parseUnknownError(error);
const fileName = String(metadata.error.fileName); const fileName = String(metadata.error.fileName);
const features = const features =
`lang=${metadata.features.language} cookiesEnabled=${metadata.features.cookiesEnabled.toString()}` + `lang=${metadata.features.language} cookiesEnabled=${metadata.features.cookiesEnabled.toString()}` +
` doNotTrack=${metadata.features.doNotTrack ?? "null"} indexedDb=${metadata.features.indexedDb.toString()}`; ` doNotTrack=${metadata.features.doNotTrack ?? "null"} indexedDb=${metadata.features.indexedDb.toString()}`;
const title = `${metadata.error.name}: ${metadata.error.message} (at "${metadata.page}")`; const title = `${metadata.error.name}: ${metadata.error.message} (at "${metadata.page}")`;
let causeAndCauseStack = errorData.causeAsString
? `
### Error cause: ${errorData.causeAsString}
`
: "";
if (errorData.causeStack) {
causeAndCauseStack += `Cause stack:
\`\`\`
${errorData.causeStack}
\`\`\`
`;
}
const body = ` const body = `
## ${title} ## ${title}
@ -104,7 +144,7 @@ Please fill this information with details if relevant.
### Environment ### Environment
* Error: ${String(metadata.error) ?? "n/a"} * Error: ${errorData.errorAsString ?? "n/a"}
* Page: ${metadata.page ?? "n/a"} * Page: ${metadata.page ?? "n/a"}
* Version: ${metadata.version.toDisplay()} * Version: ${metadata.version.toDisplay()}
* Environment: ${GameEnv[metadata.environment]} * Environment: ${GameEnv[metadata.environment]}
@ -115,9 +155,9 @@ Please fill this information with details if relevant.
### Stack Trace ### Stack Trace
\`\`\` \`\`\`
${metadata.error.stack} ${errorData.stack}
\`\`\` \`\`\`
${causeAndCauseStack}
### React Component Stack ### React Component Stack
\`\`\` \`\`\`
${metadata.errorInfo?.componentStack} ${metadata.errorInfo?.componentStack}

@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import { dialogBoxCreate } from "../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../ui/React/DialogBox";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { getErrorMetadata } from "../ErrorHelper"; import { parseUnknownError } from "../ErrorHelper";
import { cyrb53 } from "../StringHelperFunctions"; import { cyrb53 } from "../StringHelperFunctions";
import { commitHash } from "./commitHash";
const errorSet = new Set<string>(); const errorSet = new Set<string>();
@ -17,31 +18,43 @@ const errorSet = new Set<string>();
*/ */
export function exceptionAlert(error: unknown, showOnlyOnce = false): void { export function exceptionAlert(error: unknown, showOnlyOnce = false): void {
console.error(error); console.error(error);
const errorAsString = String(error); const errorData = parseUnknownError(error);
const errorStackTrace = error instanceof Error ? error.stack : undefined;
if (showOnlyOnce) { if (showOnlyOnce) {
// Calculate the "id" of the error. // Calculate the "id" of the error.
const errorId = cyrb53(errorAsString + errorStackTrace); const errorId = cyrb53(errorData.errorAsString + errorData.stack);
// Check if we showed it // Check if we showed it
if (errorSet.has(errorId)) { if (errorSet.has(errorId)) {
return; return;
} else {
errorSet.add(errorId);
} }
errorSet.add(errorId);
} }
const errorMetadata = getErrorMetadata(error);
dialogBoxCreate( dialogBoxCreate(
<> <>
Caught an exception: {errorAsString} Caught an exception: {errorData.errorAsString}
<br /> <br />
<br /> <br />
{errorStackTrace && ( {errorData.stack && (
<Typography component="div" style={{ whiteSpace: "pre-wrap" }}> <Typography component="div" style={{ whiteSpace: "pre-wrap" }}>
Stack: {errorStackTrace} Stack: {errorData.stack}
</Typography> </Typography>
)} )}
Commit: {errorMetadata.version.commitHash} {errorData.causeAsString && (
<>
<br />
<Typography component="div" style={{ whiteSpace: "pre-wrap" }}>
Error cause: {errorData.causeAsString}
{errorData.causeStack && (
<>
<br />
Cause stack: {errorData.causeStack}
</>
)}
</Typography>
</>
)}
<br />
Commit: {commitHash()}
<br /> <br />
UserAgent: {navigator.userAgent} UserAgent: {navigator.userAgent}
<br /> <br />