Skip to content

Commit

Permalink
MISC: Add error cause to exceptionAlert and Recovery mode (bitburner-…
Browse files Browse the repository at this point in the history
  • Loading branch information
catloversg authored Nov 13, 2024
1 parent 246d668 commit 4f84a89
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 15 deletions.
17 changes: 16 additions & 1 deletion src/ui/React/AlertManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
46 changes: 43 additions & 3 deletions src/utils/ErrorHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}
Expand All @@ -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]}
Expand All @@ -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}
Expand Down
35 changes: 24 additions & 11 deletions src/utils/helpers/exceptionAlert.tsx
Original file line number Diff line number Diff line change
@@ -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>();

Expand All @@ -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 />
Expand Down

0 comments on commit 4f84a89

Please sign in to comment.