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") {
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 {

@ -54,6 +54,33 @@ export interface IErrorData {
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 {
const isElectron = navigator.userAgent.toLowerCase().includes(" electron/");
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 {
const metadata = getErrorMetadata(error, errorInfo, page);
const errorData = parseUnknownError(error);
const fileName = String(metadata.error.fileName);
const features =
`lang=${metadata.features.language} cookiesEnabled=${metadata.features.cookiesEnabled.toString()}` +
` doNotTrack=${metadata.features.doNotTrack ?? "null"} indexedDb=${metadata.features.indexedDb.toString()}`;
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 = `
## ${title}
@ -104,7 +144,7 @@ Please fill this information with details if relevant.
### Environment
* Error: ${String(metadata.error) ?? "n/a"}
* Error: ${errorData.errorAsString ?? "n/a"}
* Page: ${metadata.page ?? "n/a"}
* Version: ${metadata.version.toDisplay()}
* Environment: ${GameEnv[metadata.environment]}
@ -115,9 +155,9 @@ Please fill this information with details if relevant.
### Stack Trace
\`\`\`
${metadata.error.stack}
${errorData.stack}
\`\`\`
${causeAndCauseStack}
### React Component Stack
\`\`\`
${metadata.errorInfo?.componentStack}

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