From be9b919b609c13019d45963c07a8f34234a83738 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Thu, 31 Oct 2024 15:43:20 +0100 Subject: [PATCH 1/2] decode minified stacktrace --- components/error-boundary.js | 7 +++--- lib/stacktrace.js | 47 ++++++++++++++++++++++++++++++++++++ package-lock.json | 7 +++++- package.json | 1 + 4 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 lib/stacktrace.js diff --git a/components/error-boundary.js b/components/error-boundary.js index 2b496ec7..0fe6554c 100644 --- a/components/error-boundary.js +++ b/components/error-boundary.js @@ -6,7 +6,7 @@ import copy from 'clipboard-copy' import { LoggerContext } from './logger' import Button from 'react-bootstrap/Button' import { useToast } from './toast' - +import { decodeMinifiedStackTrace } from '@/lib/stacktrace' class ErrorBoundary extends Component { constructor (props) { super(props) @@ -27,7 +27,7 @@ class ErrorBoundary extends Component { getErrorDetails () { let details = this.state.error.stack if (this.state.errorInfo?.componentStack) { - details += `\n\nComponent stack:${this.state.errorInfo.componentStack}` + details += `\n\nComponent stack:\n ${this.state.errorInfo.componentStack}` } return details } @@ -69,7 +69,8 @@ const CopyErrorButton = ({ errorDetails }) => { const toaster = useToast() const onClick = async () => { try { - await copy(errorDetails) + const decodedDetails = await decodeMinifiedStackTrace(errorDetails) + await copy(decodedDetails) toaster?.success?.('copied') } catch (err) { console.error(err) diff --git a/lib/stacktrace.js b/lib/stacktrace.js new file mode 100644 index 00000000..31032afa --- /dev/null +++ b/lib/stacktrace.js @@ -0,0 +1,47 @@ +import { SourceMapConsumer } from 'source-map' + +// FUN@FILE:LINE:COLUMN +const STACK_TRACE_LINE_REGEX = /^([A-Za-z0-9]*)@(.*):([0-9]+):([0-9]+)/ + +/** + * Decode a minified stack trace using source maps + * @param {string} stack - the minified stack trace + * @param {Object} [sourceMaps] - an object used to cache source maps + * @returns {Promise} Decoded stack trace + */ +export async function decodeMinifiedStackTrace (stack, sourceMaps = {}) { + let decodedStack = '' + for (const line of stack.split('\n')) { + try { + const stackLine = line.trim() + const stackLineParts = stackLine?.match(STACK_TRACE_LINE_REGEX) + if (stackLineParts) { + const [stackFile, stackLine, stackColumn] = stackLineParts.slice(2) + if (!stackFile || !stackLine || !stackColumn) throw new Error('Unsupported stack line ' + JSON.stringify(stackLineParts)) + if ( + ( + !stackFile.startsWith(process.env.NEXT_PUBLIC_ASSET_PREFIX) && + !stackFile.startsWith(process.env.NEXT_PUBLIC_URL) + ) || + !stackFile.endsWith('.js') + ) throw new Error('Unsupported file url ' + stackFile) + const sourceMapUrl = stackFile + '.map' + if (!sourceMaps[sourceMapUrl]) { + sourceMaps[sourceMapUrl] = await new SourceMapConsumer(await fetch(sourceMapUrl).then(res => res.text())) + } + const sourceMapper = sourceMaps[sourceMapUrl] + const map = sourceMapper.originalPositionFor({ + line: parseInt(stackLine), + column: parseInt(stackColumn) + }) + const { source, name, line, column } = map + decodedStack += `${name || ''}@${source}:${line}:${column}\n` + continue + } + } catch (e) { + console.error('Cannot decode stack line', e) + } + decodedStack += `${line}\n` + } + return decodedStack +} diff --git a/package-lock.json b/package-lock.json index 9d391222..51ae9bc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,7 @@ "remove-markdown": "^0.5.5", "sass": "^1.79.5", "serviceworker-storage": "^0.1.0", + "source-map": "^0.8.0-beta.0", "textarea-caret": "^3.1.0", "tldts": "^6.1.51", "tsx": "^4.19.1", @@ -18307,6 +18308,7 @@ "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "license": "BSD-3-Clause", "dependencies": { "whatwg-url": "^7.0.0" }, @@ -18343,6 +18345,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "license": "MIT", "dependencies": { "punycode": "^2.1.0" } @@ -18350,12 +18353,14 @@ "node_modules/source-map/node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "license": "BSD-2-Clause" }, "node_modules/source-map/node_modules/whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "license": "MIT", "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", diff --git a/package.json b/package.json index 6fb481e5..6b86ce63 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "remove-markdown": "^0.5.5", "sass": "^1.79.5", "serviceworker-storage": "^0.1.0", + "source-map": "^0.8.0-beta.0", "textarea-caret": "^3.1.0", "tldts": "^6.1.51", "tsx": "^4.19.1", From c3c3fe1ccb6632391abab0cbc27a39586a9dcaaf Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Thu, 31 Oct 2024 15:54:54 +0100 Subject: [PATCH 2/2] Always return original stack trace --- lib/stacktrace.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/stacktrace.js b/lib/stacktrace.js index 31032afa..e265ccda 100644 --- a/lib/stacktrace.js +++ b/lib/stacktrace.js @@ -11,13 +11,14 @@ const STACK_TRACE_LINE_REGEX = /^([A-Za-z0-9]*)@(.*):([0-9]+):([0-9]+)/ */ export async function decodeMinifiedStackTrace (stack, sourceMaps = {}) { let decodedStack = '' + let decoded = false for (const line of stack.split('\n')) { try { const stackLine = line.trim() const stackLineParts = stackLine?.match(STACK_TRACE_LINE_REGEX) if (stackLineParts) { const [stackFile, stackLine, stackColumn] = stackLineParts.slice(2) - if (!stackFile || !stackLine || !stackColumn) throw new Error('Unsupported stack line ' + JSON.stringify(stackLineParts)) + if (!stackFile || !stackLine || !stackColumn) throw new Error('Unsupported stack line') if ( ( !stackFile.startsWith(process.env.NEXT_PUBLIC_ASSET_PREFIX) && @@ -35,7 +36,9 @@ export async function decodeMinifiedStackTrace (stack, sourceMaps = {}) { column: parseInt(stackColumn) }) const { source, name, line, column } = map + if (!source || line === undefined) throw new Error('Unsupported stack line') decodedStack += `${name || ''}@${source}:${line}:${column}\n` + decoded = true continue } } catch (e) { @@ -43,5 +46,10 @@ export async function decodeMinifiedStackTrace (stack, sourceMaps = {}) { } decodedStack += `${line}\n` } + + if (decoded) { + decodedStack = `Decoded stacktrace:\n${decodedStack}\n\nOriginal stack trace:\n${stack}` + } + return decodedStack }