mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-22 15:43:49 +01:00
Add information to the recovery page
Adds error & environment information to the recovery page when available. The info will be displayed when the error boundary catches an error only. Otherwise, does a few minor tweaks to the UI of the page. - Add DevPage button to throw an uncaught error to go into recovery - Add "Delete Save" button in recovery from Game Options (refactored into its own component) - Use "Soft Reset" button from Game Options (refactored into its own component) - The "Soft Reset" & "Delete Save" buttons now have confirmations - Add tooltip on "Disable Recovery Mode" button - Add timestamp to the RECOVERY.json filename - Add textarea containing markdown with the current error details, if available - Error - Page - Version - Environment - Platform - UserAgent - Features - Source - Stack Trace - Change GitHub new issue link to contain default body & title, if possible - Change links to not take the full width (they were clickable by mistake) - Fix "Disable Recovery Mode" not resetting the ErrorBoundary's state, making going back to terminal impossible
This commit is contained in:
parent
8b69fd7faa
commit
65964c84b2
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
@ -17,6 +17,8 @@ interface IProps {
|
||||
}
|
||||
|
||||
export function General(props: IProps): React.ReactElement {
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
function addMoney(n: number) {
|
||||
return function () {
|
||||
props.player.gainMoney(n, "other");
|
||||
@ -43,6 +45,10 @@ export function General(props: IProps): React.ReactElement {
|
||||
props.router.toBitVerse(false, false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (error) throw new ReferenceError('Manually thrown error');
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
@ -81,6 +87,7 @@ export function General(props: IProps): React.ReactElement {
|
||||
<Button onClick={b1tflum3}>Run b1t_flum3.exe</Button>
|
||||
<Button onClick={quickHackW0r1dD43m0n}>Quick w0rld_d34m0n</Button>
|
||||
<Button onClick={hackW0r1dD43m0n}>Hack w0rld_d34m0n</Button>
|
||||
<Button onClick={() => setError(true)}>Throw Error</Button>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
|
@ -1,29 +1,58 @@
|
||||
import React, { ErrorInfo } from "react";
|
||||
|
||||
import { IErrorData, getErrorForDisplay } from "../utils/ErrorHelper";
|
||||
import { RecoveryRoot } from "./React/RecoveryRoot";
|
||||
import { IRouter } from "./Router";
|
||||
import { IRouter, Page } from "./Router";
|
||||
|
||||
interface IProps {
|
||||
router: IRouter;
|
||||
softReset: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
error?: Error;
|
||||
errorInfo?: React.ErrorInfo;
|
||||
page?: Page;
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends React.Component<IProps, IState> {
|
||||
state: IState
|
||||
|
||||
export class ErrorBoundary extends React.Component<IProps> {
|
||||
state: { hasError: boolean }
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
this.state = { hasError: false } as IState;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.setState( { hasError: false } as IState);
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
this.setState({
|
||||
errorInfo,
|
||||
page: this.props.router.page(),
|
||||
});
|
||||
console.error(error, errorInfo);
|
||||
}
|
||||
render(): React.ReactNode {
|
||||
if (this.state.hasError) {
|
||||
return <RecoveryRoot router={this.props.router} softReset={this.props.softReset} />;
|
||||
let errorData: IErrorData | undefined;
|
||||
if (this.state.error) {
|
||||
try {
|
||||
// We don't want recursive errors, so in case this fails, it's in a try catch.
|
||||
errorData = getErrorForDisplay(this.state.error, this.state.errorInfo, this.state.page);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return <RecoveryRoot router={this.props.router} softReset={this.props.softReset}
|
||||
errorData={errorData} resetError={() => this.reset()} />;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
static getDerivedStateFromError(): { hasError: true} {
|
||||
return { hasError: true };
|
||||
static getDerivedStateFromError(error: Error): IState {
|
||||
return { hasError: true, error};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
32
src/ui/React/DeleteGameButton.tsx
Normal file
32
src/ui/React/DeleteGameButton.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React, { useState } from 'react';
|
||||
import { deleteGame } from "../../db";
|
||||
import { ConfirmationModal } from "./ConfirmationModal";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Tooltip } from '@mui/material';
|
||||
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
interface IProps {
|
||||
color?: "primary" | "warning" | "error";
|
||||
}
|
||||
|
||||
export function DeleteGameButton({ color = "primary" }: IProps): React.ReactElement {
|
||||
const [modalOpened, setModalOpened] = useState(false);
|
||||
|
||||
return (<>
|
||||
<Tooltip title="This will permanently delete your local save game. Did you export it before?">
|
||||
<Button startIcon={<DeleteIcon />} color={color} onClick={() => setModalOpened(true)}>Delete Save</Button>
|
||||
</Tooltip>
|
||||
<ConfirmationModal
|
||||
onConfirm={() => {
|
||||
setModalOpened(false);
|
||||
deleteGame()
|
||||
.then(() => setTimeout(() => location.reload(), 1000))
|
||||
.catch((r) => console.error(`Could not delete game: ${r}`));
|
||||
}}
|
||||
open={modalOpened}
|
||||
onClose={() => setModalOpened(false)}
|
||||
confirmationText={"Really delete your game? (It's permanent!)"}
|
||||
/>
|
||||
</>)
|
||||
}
|
@ -21,6 +21,7 @@ import TextField from "@mui/material/TextField";
|
||||
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
|
||||
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
|
||||
import { dialogBoxCreate } from "./DialogBox";
|
||||
@ -31,9 +32,11 @@ import { StyleEditorModal } from "./StyleEditorModal";
|
||||
import { SnackbarEvents } from "./Snackbar";
|
||||
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { save, deleteGame } from "../../db";
|
||||
import { save } from "../../db";
|
||||
import { formatTime } from "../../utils/helpers/formatTime";
|
||||
import { OptionSwitch } from "./OptionSwitch";
|
||||
import { DeleteGameButton } from "./DeleteGameButton";
|
||||
import { SoftResetButton } from "./SoftResetButton";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
@ -71,10 +74,8 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
|
||||
const [locale, setLocale] = useState(Settings.Locale);
|
||||
const [diagnosticOpen, setDiagnosticOpen] = useState(false);
|
||||
const [deleteGameOpen, setDeleteOpen] = useState(false);
|
||||
const [themeEditorOpen, setThemeEditorOpen] = useState(false);
|
||||
const [styleEditorOpen, setStyleEditorOpen] = useState(false);
|
||||
const [softResetOpen, setSoftResetOpen] = useState(false);
|
||||
const [importSaveOpen, setImportSaveOpen] = useState(false);
|
||||
const [importData, setImportData] = useState<ImportData | null>(null);
|
||||
|
||||
@ -194,14 +195,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
});
|
||||
}
|
||||
|
||||
function doSoftReset(): void {
|
||||
if (!Settings.SuppressBuyAugmentationConfirmation) {
|
||||
setSoftResetOpen(true);
|
||||
} else {
|
||||
props.softReset();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root} style={{ width: "90%" }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
@ -531,19 +524,19 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
</Grid>
|
||||
<Box sx={{ display: 'grid', width: 'fit-content', height: 'fit-content' }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
|
||||
<Button onClick={() => props.save()}>Save Game</Button>
|
||||
<Button onClick={() => setDeleteOpen(true)}>Delete Game</Button>
|
||||
<Button onClick={() => props.save()} startIcon={<SaveIcon />} >
|
||||
Save Game
|
||||
</Button>
|
||||
<DeleteGameButton />
|
||||
</Box>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
|
||||
<Tooltip title={<Typography>Export your game to a text file.</Typography>}>
|
||||
<Button onClick={() => props.export()}>
|
||||
<DownloadIcon color="primary" />
|
||||
<Button onClick={() => props.export()} startIcon={<DownloadIcon />}>
|
||||
Export Game
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={<Typography>Import your game from a text file.<br />This will <strong>overwrite</strong> your current game. Back it up first!</Typography>}>
|
||||
<Button onClick={startImport}>
|
||||
<UploadIcon color="primary" />
|
||||
<Button onClick={startImport} startIcon={<UploadIcon />}>
|
||||
Import Game
|
||||
<input ref={importInput} id="import-game-file-selector" type="file" hidden onChange={onImport} />
|
||||
</Button>
|
||||
@ -587,21 +580,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
Perform a soft reset. Resets everything as if you had just purchased an Augmentation.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Button onClick={doSoftReset}>Soft Reset</Button>
|
||||
</Tooltip>
|
||||
<ConfirmationModal
|
||||
open={softResetOpen}
|
||||
onClose={() => setSoftResetOpen(false)}
|
||||
onConfirm={props.softReset}
|
||||
confirmationText={"This will perform the same action as installing Augmentations, are you sure?"}
|
||||
/>
|
||||
<SoftResetButton noConfirmation={Settings.SuppressBuyAugmentationConfirmation} onTriggered={props.softReset} />
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
@ -640,17 +619,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
</Box>
|
||||
</Grid>
|
||||
<FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} />
|
||||
<ConfirmationModal
|
||||
onConfirm={() => {
|
||||
setDeleteOpen(false);
|
||||
deleteGame()
|
||||
.then(() => setTimeout(() => location.reload(), 1000))
|
||||
.catch((r) => console.error(`Could not delete game: ${r}`));
|
||||
}}
|
||||
open={deleteGameOpen}
|
||||
onClose={() => setDeleteOpen(false)}
|
||||
confirmationText={"Really delete your game? (It's permanent!)"}
|
||||
/>
|
||||
<ThemeEditorModal open={themeEditorOpen} onClose={() => setThemeEditorOpen(false)} />
|
||||
<StyleEditorModal open={styleEditorOpen} onClose={() => setStyleEditorOpen(false)} />
|
||||
</div>
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Link from "@mui/material/Link";
|
||||
import Button from "@mui/material/Button";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { Typography, Link, Button, ButtonGroup, Tooltip, Box, Paper, TextField } from "@mui/material";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { load } from "../../db";
|
||||
import { IRouter } from "../Router";
|
||||
import { download } from "../../SaveObject";
|
||||
import { IErrorData, newIssueUrl } from "../../utils/ErrorHelper";
|
||||
import { DeleteGameButton } from "./DeleteGameButton";
|
||||
import { SoftResetButton } from "./SoftResetButton";
|
||||
|
||||
import DirectionsRunIcon from '@mui/icons-material/DirectionsRun';
|
||||
import GitHubIcon from "@mui/icons-material/GitHub";
|
||||
|
||||
export let RecoveryMode = false;
|
||||
|
||||
@ -16,40 +21,79 @@ export function ActivateRecoveryMode(): void {
|
||||
interface IProps {
|
||||
router: IRouter;
|
||||
softReset: () => void;
|
||||
errorData?: IErrorData;
|
||||
resetError?: () => void;
|
||||
}
|
||||
|
||||
export function RecoveryRoot({ router, softReset }: IProps): React.ReactElement {
|
||||
export function RecoveryRoot({ router, softReset, errorData, resetError }: IProps): React.ReactElement {
|
||||
function recover(): void {
|
||||
if (resetError) resetError();
|
||||
RecoveryMode = false;
|
||||
router.toTerminal();
|
||||
}
|
||||
Settings.AutosaveInterval = 0;
|
||||
load().then((content) => {
|
||||
download("RECOVERY.json", content);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
load().then((content) => {
|
||||
const epochTime = Math.round(Date.now() / 1000);
|
||||
const filename = `RECOVERY_BITBURNER_${epochTime}.json`;
|
||||
download(filename, content);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ padding: "8px 16px", minHeight: "100vh", maxWidth: '1200px', boxSizing: "border-box",}}>
|
||||
<Typography variant="h3">RECOVERY MODE ACTIVATED</Typography>
|
||||
<Typography>
|
||||
There was an error loading your save file and the game went into recovery mode. In this mode saving is disabled
|
||||
There was an error with your save file and the game went into recovery mode. In this mode saving is disabled
|
||||
and the game will automatically export your save file (to prevent corruption).
|
||||
</Typography>
|
||||
<Typography>At this point it is recommended to alert a developer.</Typography>
|
||||
<Link href="https://github.com/danielyxie/bitburner/issues/new" target="_blank">
|
||||
<Typography>File an issue on github</Typography>
|
||||
</Link>
|
||||
<Link href="https://www.reddit.com/r/Bitburner/" target="_blank">
|
||||
<Typography>Make a reddit post</Typography>
|
||||
</Link>
|
||||
<Link href="https://discord.gg/TFc3hKD" target="_blank">
|
||||
<Typography>Post in the #bug-report channel on Discord.</Typography>
|
||||
</Link>
|
||||
<Typography>
|
||||
<Link href={errorData?.issueUrl ?? newIssueUrl} target="_blank">File an issue on github</Link>
|
||||
</Typography>
|
||||
<Typography>
|
||||
<Link href="https://www.reddit.com/r/Bitburner/" target="_blank">Make a reddit post</Link>
|
||||
</Typography>
|
||||
<Typography>
|
||||
<Link href="https://discord.gg/TFc3hKD" target="_blank">Post in the #bug-report channel on Discord.</Link>
|
||||
</Typography>
|
||||
<Typography>Please include your save file.</Typography>
|
||||
<br />
|
||||
<br />
|
||||
<Typography>You can disable recovery mode now. But chances are the game will not work correctly.</Typography>
|
||||
<Button onClick={recover}>DISABLE RECOVERY MODE</Button>
|
||||
<Button onClick={softReset}>PERFORM SOFT RESET</Button>
|
||||
</>
|
||||
<ButtonGroup sx={{ my: 2 }}>
|
||||
<Tooltip title="Disables the recovery mode & attempt to head back to the terminal page. This may or may not work. Ensure you have saved the recovery file.">
|
||||
<Button onClick={recover} startIcon={<DirectionsRunIcon />}>Disable Recovery Mode</Button>
|
||||
</Tooltip>
|
||||
<SoftResetButton color="warning" onTriggered={softReset} />
|
||||
<DeleteGameButton color="error" />
|
||||
</ButtonGroup>
|
||||
|
||||
{errorData && (
|
||||
<Paper sx={{ px: 2, pt: 1, pb: 2, mt: 2}}>
|
||||
<Typography variant="h5">
|
||||
{errorData.title}
|
||||
</Typography>
|
||||
<Box sx={{ my: 2 }}>
|
||||
<TextField
|
||||
label="Bug Report Text"
|
||||
value={errorData.body}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
multiline
|
||||
fullWidth
|
||||
rows={12}
|
||||
sx={{ "& .MuiOutlinedInput-root": { color: Settings.theme.secondary }}} />
|
||||
</Box>
|
||||
<Tooltip title="Submitting an issue to GitHub really help us improve the game!">
|
||||
<Button component={Link} startIcon={<GitHubIcon />} color="info" sx={{ px: 2 }}
|
||||
href={errorData.issueUrl ?? newIssueUrl} target={"_blank"} >
|
||||
Submit Issue to GitHub
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
36
src/ui/React/SoftResetButton.tsx
Normal file
36
src/ui/React/SoftResetButton.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ConfirmationModal } from "./ConfirmationModal";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Tooltip } from '@mui/material';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
|
||||
interface IProps {
|
||||
color?: "primary" | "warning" | "error";
|
||||
noConfirmation?: boolean;
|
||||
onTriggered: () => void;
|
||||
}
|
||||
|
||||
export function SoftResetButton({ color = "primary", noConfirmation = false, onTriggered }: IProps): React.ReactElement {
|
||||
const [modalOpened, setModalOpened] = useState(false);
|
||||
|
||||
function handleButtonClick(): void {
|
||||
if (noConfirmation) {
|
||||
onTriggered();
|
||||
} else {
|
||||
setModalOpened(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (<>
|
||||
<Tooltip title="Perform a soft reset. Resets everything as if you had just purchased an Augmentation.">
|
||||
<Button startIcon={<RestartAltIcon />} color={color} onClick={handleButtonClick}>Soft Reset</Button>
|
||||
</Tooltip>
|
||||
<ConfirmationModal
|
||||
onConfirm={onTriggered}
|
||||
open={modalOpened}
|
||||
onClose={() => setModalOpened(false)}
|
||||
confirmationText={"This will perform the same action as installing Augmentations, are you sure?"}
|
||||
/>
|
||||
</>)
|
||||
}
|
135
src/utils/ErrorHelper.ts
Normal file
135
src/utils/ErrorHelper.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import React from "react";
|
||||
|
||||
import { Page } from "../ui/Router";
|
||||
import { hash } from "../hash/hash";
|
||||
import { CONSTANTS } from "../Constants";
|
||||
|
||||
enum GameEnv {
|
||||
Production,
|
||||
Development,
|
||||
}
|
||||
|
||||
enum Platform {
|
||||
Browser,
|
||||
Steam,
|
||||
}
|
||||
|
||||
interface GameVersion {
|
||||
version: string;
|
||||
hash: string;
|
||||
|
||||
toDisplay: () => string;
|
||||
}
|
||||
|
||||
interface BrowserFeatures {
|
||||
userAgent: string;
|
||||
language: string;
|
||||
cookiesEnabled: boolean;
|
||||
doNotTrack: string | null;
|
||||
indexedDb: boolean;
|
||||
}
|
||||
|
||||
interface IErrorMetadata {
|
||||
error: Error;
|
||||
errorInfo?: React.ErrorInfo;
|
||||
page?: Page;
|
||||
|
||||
environment: GameEnv;
|
||||
platform: Platform;
|
||||
version: GameVersion;
|
||||
features: BrowserFeatures;
|
||||
}
|
||||
|
||||
export interface IErrorData {
|
||||
metadata: IErrorMetadata;
|
||||
|
||||
title: string;
|
||||
body: string;
|
||||
|
||||
features: string;
|
||||
fileName?: string;
|
||||
|
||||
issueUrl: string;
|
||||
}
|
||||
|
||||
|
||||
export const newIssueUrl = `https://github.com/danielyxie/bitburner/issues/new`;
|
||||
|
||||
function getErrorMetadata(error: Error, errorInfo?: React.ErrorInfo, page?: Page): IErrorMetadata {
|
||||
const isElectron = navigator.userAgent.toLowerCase().indexOf(" electron/") > -1;
|
||||
const env = process.env.NODE_ENV === "development" ? GameEnv.Development : GameEnv.Production;
|
||||
const version: GameVersion = {
|
||||
version: CONSTANTS.VersionString,
|
||||
hash: hash(),
|
||||
toDisplay: () => `v${CONSTANTS.VersionString} (${hash()})`,
|
||||
}
|
||||
const features: BrowserFeatures = {
|
||||
userAgent: navigator.userAgent,
|
||||
|
||||
language: navigator.language,
|
||||
cookiesEnabled: navigator.cookieEnabled,
|
||||
doNotTrack: navigator.doNotTrack,
|
||||
indexedDb: (!!window.indexedDB),
|
||||
}
|
||||
const metadata: IErrorMetadata = {
|
||||
platform: isElectron ? Platform.Steam : Platform.Browser,
|
||||
environment: env,
|
||||
version, features,
|
||||
error, errorInfo, page,
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export function getErrorForDisplay(error: Error, errorInfo?: React.ErrorInfo, page?: Page): IErrorData {
|
||||
const metadata = getErrorMetadata(error, errorInfo, page);
|
||||
const fileName = (metadata.error as any).fileName;
|
||||
const features = `lang=${metadata.features.language} cookiesEnabled=${metadata.features.cookiesEnabled.toString()}` +
|
||||
` doNotTrack=${metadata.features.doNotTrack} indexedDb=${metadata.features.indexedDb.toString()}`;
|
||||
|
||||
const title = `${metadata.error.name}: ${metadata.error.message}${metadata.page && ` (at "${Page[metadata.page]}")`}`;
|
||||
const body = `
|
||||
## ${title}
|
||||
|
||||
### How did this happen?
|
||||
|
||||
Please fill this information with details if relevant.
|
||||
|
||||
- [ ] Save file
|
||||
- [ ] Minimal scripts to reproduce the issue
|
||||
- [ ] Steps to reproduce
|
||||
|
||||
### Environment
|
||||
|
||||
* Error: ${metadata.error?.toString() ?? 'n/a'}
|
||||
* Page: ${metadata.page ? Page[metadata.page] : 'n/a'}
|
||||
* Version: ${metadata.version.toDisplay()}
|
||||
* Environment: ${GameEnv[metadata.environment]}
|
||||
* Platform: ${Platform[metadata.platform]}
|
||||
* UserAgent: ${navigator.userAgent}
|
||||
* Features: ${features}
|
||||
* Source: ${fileName ?? 'n/a'}
|
||||
|
||||
${metadata.environment === GameEnv.Development ? `
|
||||
### Stack Trace
|
||||
\`\`\`
|
||||
${metadata.errorInfo?.componentStack.toString().trim()}
|
||||
\`\`\`
|
||||
` : ''}
|
||||
### Save
|
||||
\`\`\`
|
||||
Copy your save here if possible
|
||||
\`\`\`
|
||||
`.trim();
|
||||
|
||||
const issueUrl = `${newIssueUrl}?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
|
||||
|
||||
const data: IErrorData = {
|
||||
metadata,
|
||||
fileName,
|
||||
features,
|
||||
title,
|
||||
body,
|
||||
issueUrl,
|
||||
}
|
||||
return data;
|
||||
}
|
Loading…
Reference in New Issue
Block a user