From 7ab0ec2e7e8a6c1ee2e42c22987759dba22cdeeb Mon Sep 17 00:00:00 2001 From: Misode Date: Fri, 11 Mar 2022 00:05:24 +0100 Subject: [PATCH] Expose error stack traces (#211) --- package-lock.json | 34 +++++++++++++- package.json | 5 ++- src/app/components/ErrorPanel.tsx | 45 +++++++++++++++++-- src/app/components/generator/SourcePanel.tsx | 16 +++++-- .../previews/NoiseSettingsPreview.tsx | 20 ++++++--- src/app/hooks/useCanvas.ts | 6 ++- src/app/pages/Changelog.tsx | 2 +- src/app/pages/Generator.tsx | 11 ++--- src/app/pages/Sounds.tsx | 6 +-- src/app/pages/Versions.tsx | 2 +- src/app/previews/NoiseSettings.ts | 1 + src/app/services/Changelogs.ts | 1 + src/app/services/DataFetcher.ts | 8 ++-- src/styles/global.css | 5 +++ 14 files changed, 131 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index cdbd6e44..be3a828a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,8 @@ "howler": "^2.2.3", "js-yaml": "^3.14.1", "marked": "^4.0.10", - "rfdc": "^1.3.0" + "rfdc": "^1.3.0", + "sourcemapped-stacktrace": "^1.1.11" }, "devDependencies": { "@preact/preset-vite": "^2.1.0", @@ -3945,6 +3946,22 @@ "node": ">=0.10.0" } }, + "node_modules/sourcemapped-stacktrace": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/sourcemapped-stacktrace/-/sourcemapped-stacktrace-1.1.11.tgz", + "integrity": "sha512-O0pcWjJqzQFVsisPlPXuNawJHHg9N9UgpJ/aDmvi9+vnS3x1C0NhwkVFzzZ1VN0Xo+bekyweoqYvBw5ZBKiNnQ==", + "dependencies": { + "source-map": "0.5.6" + } + }, + "node_modules/sourcemapped-stacktrace/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7367,6 +7384,21 @@ "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", "dev": true }, + "sourcemapped-stacktrace": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/sourcemapped-stacktrace/-/sourcemapped-stacktrace-1.1.11.tgz", + "integrity": "sha512-O0pcWjJqzQFVsisPlPXuNawJHHg9N9UgpJ/aDmvi9+vnS3x1C0NhwkVFzzZ1VN0Xo+bekyweoqYvBw5ZBKiNnQ==", + "requires": { + "source-map": "0.5.6" + }, + "dependencies": { + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + } + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index 69b67351..76c7e281 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,14 @@ "brace": "^0.11.1", "buffer": "^6.0.3", "comment-json": "^4.1.1", - "deepslate-1.18": "npm:deepslate@^0.9.0-beta.9", "deepslate": "^0.9.0-beta.13", + "deepslate-1.18": "npm:deepslate@^0.9.0-beta.9", "deepslate-rs": "^0.1.6", "howler": "^2.2.3", "js-yaml": "^3.14.1", "marked": "^4.0.10", - "rfdc": "^1.3.0" + "rfdc": "^1.3.0", + "sourcemapped-stacktrace": "^1.1.11" }, "devDependencies": { "@preact/preset-vite": "^2.1.0", diff --git a/src/app/components/ErrorPanel.tsx b/src/app/components/ErrorPanel.tsx index 91a0d53d..7476766f 100644 --- a/src/app/components/ErrorPanel.tsx +++ b/src/app/components/ErrorPanel.tsx @@ -1,13 +1,52 @@ +import { useEffect, useMemo, useState } from 'preact/hooks' +import { mapStackTrace } from 'sourcemapped-stacktrace' import { Octicon } from './Octicon' type ErrorPanelProps = { - error: string, + error: string | Error, onDismiss?: () => unknown, } export function ErrorPanel({ error, onDismiss }: ErrorPanelProps) { + const [stackVisible, setStackVisible] = useState(false) + const [stack, setStack] = useState(undefined) + + useEffect(() => { + if (error instanceof Error) { + const stack = error.stack!.split('\n').map(line => { + return line.replace(/^(\s+)at (?:async )?(https?:.*)/, '$1at ($2)') + }) + setStack(stack.join('\n')) + mapStackTrace(stack.join('\n'), (mapped) => { + const mappedStack = mapped.map(line => { + return line.replace(/..\/..\/src\//, 'src/') + }).join('\n') + setStack(mappedStack) + }) + } + }, [error]) + + const url = useMemo(() => { + let url ='https://github.com/misode/misode.github.io/issues/new' + if (error instanceof Error) { + url += `?title=${encodeURIComponent(`${error.name}: ${error.message}`)}` + if (stack) { + url += `&body=${encodeURIComponent(`\`\`\`\n${error.name}: ${error.message}\n${stack}\n\`\`\`\n`)}` + } + } else { + url += `?title=${encodeURIComponent(error.toString())}` + } + return url + }, [error, stack]) + return
{onDismiss &&
{Octicon.x}
} -

{error}

-

If you think this is a bug, you can report it on GitHub

+

+ {error instanceof Error ? error.message : error} + {stack && setStackVisible(!stackVisible)}> + {Octicon.info} + } +

+ {stack && stackVisible &&
{stack}
} +

If you think this is a bug, you can report it on GitHub

} diff --git a/src/app/components/generator/SourcePanel.tsx b/src/app/components/generator/SourcePanel.tsx index fd0f1b9c..5e72a799 100644 --- a/src/app/components/generator/SourcePanel.tsx +++ b/src/app/components/generator/SourcePanel.tsx @@ -52,7 +52,7 @@ type SourcePanelProps = { doDownload?: number, doImport?: number, copySuccess: () => unknown, - onError: (message: string) => unknown, + onError: (message: string | Error) => unknown, } export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) { const { locale } = useLocale() @@ -78,7 +78,12 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm const output = getSerializedOutput(model, blockStates) editor.current.setValue(output) } catch (e) { - onError(`Error getting JSON output: ${message(e)}`) + if (e instanceof Error) { + e.message = `Error getting JSON output: ${e.message}` + onError(e) + } else { + onError(`Error getting JSON output: ${message(e)}`) + } console.error(e) editor.current.setValue('') } @@ -91,7 +96,12 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm const data = FORMATS[format].parse(value) model?.reset(DataModel.wrapLists(data), false) } catch (e) { - onError(`Error importing: ${message(e)}`) + if (e instanceof Error) { + e.message = `Error importing: ${e.message}` + onError(e) + } else { + onError(`Error importing: ${message(e)}`) + } console.error(e) } } diff --git a/src/app/components/previews/NoiseSettingsPreview.tsx b/src/app/components/previews/NoiseSettingsPreview.tsx index b4342817..8ac88a3d 100644 --- a/src/app/components/previews/NoiseSettingsPreview.tsx +++ b/src/app/components/previews/NoiseSettingsPreview.tsx @@ -48,13 +48,19 @@ export const NoiseSettingsPreview = ({ data, shown, version }: PreviewProps) => clearInterval(scrollInterval.current) } if (shown) { - redraw() - if (autoScroll) { - scrollInterval.current = setInterval(() => { - offset.current -= 8 - redraw() - }, 100) as any - } + (async () => { + try { + await redraw() + if (autoScroll) { + scrollInterval.current = setInterval(() => { + offset.current -= 8 + redraw() + }, 100) as any + } + } catch (e) { + throw e + } + })() } }, [state, seed, shown, biome, biomeScale, biomeDepth, autoScroll]) diff --git a/src/app/hooks/useCanvas.ts b/src/app/hooks/useCanvas.ts index 9b1f411a..4780012f 100644 --- a/src/app/hooks/useCanvas.ts +++ b/src/app/hooks/useCanvas.ts @@ -76,7 +76,11 @@ export function useCanvas({ size, draw, onDrag, onHover, onLeave }: { canvas.current.height = s[1] const img = ctx.getImageData(0, 0, s[0], s[1]) const ownCount = redrawCount.current += 1 - await draw(img) + try { + await draw(img) + } catch (e) { + throw e + } if (ownCount === redrawCount.current) { ctx.putImageData(img, 0, 0) } diff --git a/src/app/pages/Changelog.tsx b/src/app/pages/Changelog.tsx index 34e0c700..ce31aac4 100644 --- a/src/app/pages/Changelog.tsx +++ b/src/app/pages/Changelog.tsx @@ -9,7 +9,7 @@ interface Props { } export function Changelog({}: Props) { const { locale } = useLocale() - const [error, setError] = useState(null) + const [error, setError] = useState(null) useTitle(locale('title.changelog')) const [changelogs, setChangelogs] = useState([]) diff --git a/src/app/pages/Generator.tsx b/src/app/pages/Generator.tsx index a6739948..cd81c11a 100644 --- a/src/app/pages/Generator.tsx +++ b/src/app/pages/Generator.tsx @@ -18,10 +18,11 @@ export function Generator({}: Props) { const { locale } = useLocale() const { version, changeVersion } = useVersion() const { project, file, updateFile, openFile, closeFile } = useProject() - const [error, setError] = useState(null) + const [error, setError] = useState(null) const [errorBoundary, errorRetry] = useErrorBoundary() if (errorBoundary) { - return
+ errorBoundary.message = `Something went wrong rendering the generator: ${errorBoundary.message}` + return
} const gen = getGenerator(getCurrentUrl()) @@ -69,7 +70,7 @@ export function Generator({}: Props) { } setModel(m) }) - .catch(e => { console.error(e); setError(message(e)) }) + .catch(e => { console.error(e); setError(e) }) }, [version, gen.id]) const [dirty, setDirty] = useState(false) @@ -172,7 +173,7 @@ export function Generator({}: Props) { getCollections(version).then(collections => { setPresets(collections.get(gen.id).map(p => p.slice(10))) }) - .catch(e => { console.error(e); setError(e.message) }) + .catch(e => { console.error(e); setError(e) }) }, [version, gen.id]) const selectPreset = (id: string) => { @@ -192,7 +193,7 @@ export function Generator({}: Props) { } return preset } catch (e) { - setError(message(e)) + setError(e instanceof Error ? e : message(e)) } } diff --git a/src/app/pages/Sounds.tsx b/src/app/pages/Sounds.tsx index 6db86fee..4fea6491 100644 --- a/src/app/pages/Sounds.tsx +++ b/src/app/pages/Sounds.tsx @@ -4,7 +4,7 @@ import { Btn, BtnMenu, ErrorPanel, SoundConfig, TextInput } from '../components' import { useLocale, useTitle, useVersion } from '../contexts' import type { SoundEvents, VersionId } from '../services' import { fetchSounds } from '../services' -import { hexId, message } from '../Utils' +import { hexId } from '../Utils' interface Props { path?: string, @@ -12,7 +12,7 @@ interface Props { export function Sounds({}: Props) { const { locale } = useLocale() const { version, changeVersion } = useVersion() - const [error, setError] = useState(null) + const [error, setError] = useState(null) useTitle(locale('title.sounds')) const [sounds, setSounds] = useState({}) @@ -20,7 +20,7 @@ export function Sounds({}: Props) { useEffect(() => { fetchSounds(version) .then(setSounds) - .catch(e => { console.error(e); setError(message(e)) }) + .catch(e => { console.error(e); setError(e) }) }, [version]) const [search, setSearch] = useState('') diff --git a/src/app/pages/Versions.tsx b/src/app/pages/Versions.tsx index b27b2b38..6f90c93e 100644 --- a/src/app/pages/Versions.tsx +++ b/src/app/pages/Versions.tsx @@ -11,7 +11,7 @@ interface Props { } export function Versions({}: Props) { const { locale } = useLocale() - const [error, setError] = useState(null) + const [error, setError] = useState(null) useTitle(locale('title.versions')) const [versions, setVersions] = useState([]) diff --git a/src/app/previews/NoiseSettings.ts b/src/app/previews/NoiseSettings.ts index fa037af1..6c5be98b 100644 --- a/src/app/previews/NoiseSettings.ts +++ b/src/app/previews/NoiseSettings.ts @@ -224,6 +224,7 @@ class LevelSlice { public generate(generator: NoiseChunkGenerator, forcedBiome?: string) { this.chunks.forEach((chunk, i) => { if (!this.done[i]) { + throw new Error('Test') generator.fill(chunk, true) generator.buildSurface(chunk, forcedBiome) this.done[i] = true diff --git a/src/app/services/Changelogs.ts b/src/app/services/Changelogs.ts index 3ab0d5ea..b394809c 100644 --- a/src/app/services/Changelogs.ts +++ b/src/app/services/Changelogs.ts @@ -20,6 +20,7 @@ let Changelogs: Change[] | Promise | null = null export async function getChangelogs() { if (!Changelogs) { const index = await (await fetch(`${repo}/index.json`)).json() as string[] + throw new Error('Test') Changelogs = (await Promise.all( index.map((group, i) => fetchGroup(parseVersion(group), i)) )).flat().map(change => ({ diff --git a/src/app/services/DataFetcher.ts b/src/app/services/DataFetcher.ts index 7d737421..aa993c17 100644 --- a/src/app/services/DataFetcher.ts +++ b/src/app/services/DataFetcher.ts @@ -1,7 +1,7 @@ -import type { CollectionRegistry } from '@mcschema/core' -import config from '../../config.json' -import { message } from '../Utils' -import type { BlockStateRegistry, VersionId } from './Schemas' +import type { CollectionRegistry } from '@mcschema/core'; +import config from '../../config.json'; +import { message } from '../Utils'; +import type { BlockStateRegistry, VersionId } from './Schemas'; // Cleanup old caches ['1.15', '1.16', '1.17'].forEach(v => localStorage.removeItem(`cache_${v}`)); diff --git a/src/styles/global.css b/src/styles/global.css index b1b00730..fba97674 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -749,6 +749,11 @@ main.has-preview { margin: 10px 0; } +.error h3 span { + margin-left: 8px; + cursor: pointer; +} + .error .error-dismiss { float: right; cursor: pointer;