From 4f84a894eb655a1ac88da7380a513cfb993f932f Mon Sep 17 00:00:00 2001 From: catloversg <152669316+catloversg@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:27:18 +0700 Subject: [PATCH] MISC: Add error cause to exceptionAlert and Recovery mode (#1772) --- src/ui/React/AlertManager.tsx | 17 +++++++++- src/utils/ErrorHelper.ts | 46 ++++++++++++++++++++++++++-- src/utils/helpers/exceptionAlert.tsx | 35 ++++++++++++++------- 3 files changed, 83 insertions(+), 15 deletions(-) diff --git a/src/ui/React/AlertManager.tsx b/src/ui/React/AlertManager.tsx index bd62cc0ede..736ce5a01b 100644 --- a/src/ui/React/AlertManager.tsx +++ b/src/ui/React/AlertManager.tsx @@ -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 { diff --git a/src/utils/ErrorHelper.ts b/src/utils/ErrorHelper.ts index 884078d410..91443dc661 100644 --- a/src/utils/ErrorHelper.ts +++ b/src/utils/ErrorHelper.ts @@ -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} diff --git a/src/utils/helpers/exceptionAlert.tsx b/src/utils/helpers/exceptionAlert.tsx index 1352dd9b0f..e70f58d2ef 100644 --- a/src/utils/helpers/exceptionAlert.tsx +++ b/src/utils/helpers/exceptionAlert.tsx @@ -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(); @@ -17,31 +18,43 @@ const errorSet = new Set(); */ 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}

- {errorStackTrace && ( + {errorData.stack && ( - Stack: {errorStackTrace} + Stack: {errorData.stack} )} - Commit: {errorMetadata.version.commitHash} + {errorData.causeAsString && ( + <> +
+ + Error cause: {errorData.causeAsString} + {errorData.causeStack && ( + <> +
+ Cause stack: {errorData.causeStack} + + )} +
+ + )} +
+ Commit: {commitHash()}
UserAgent: {navigator.userAgent}