Created
June 21, 2022 18:20
-
-
Save fuweichin/02c4805d2932d263cfc52e9e713e23f2 to your computer and use it in GitHub Desktop.
Collect and show uncaught script error and unhandled promise rejection
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>Script Error Repoter</title> | |
</head> | |
<body> | |
<h1>Script Error Repoter</h1> | |
<button id="raiseError">Raise an error</button> | |
<button id="raiseAsyncError">Raise an async error</button> | |
<script src="script-error-reporter.js"></script> | |
<script> | |
function main() { | |
document.getElementById('raiseError').addEventListener('click', () => { | |
throw new Error('Something went wrong'); | |
}); | |
document.getElementById('raiseAsyncError').addEventListener('click', () => { | |
fetch('//0.0.0.0/null').then((res) => { | |
return res.text(); | |
}); | |
}); | |
} | |
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', main) : main; | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(() => { | |
let config = { | |
maxBufferItems: 10, | |
}; | |
let htmlTemplate = document.createElement('template'); | |
htmlTemplate.innerHTML = /*html*/`<style> | |
:host { | |
position: relative; | |
border: 2px solid #c77; | |
padding: 8px 16px; | |
margin: 16px; | |
background-color: #fdd; | |
color: #333; | |
color-scheme: light; | |
display: block; | |
} | |
:host(:not([open])){ | |
display: none; | |
} | |
.message{ | |
white-space: pre-wrap; | |
word-wrap: break-word; | |
font-family: monospace; | |
font-size: 12px; | |
} | |
button[value="close"]{ | |
position: absolute; | |
right: 1em; | |
top: 0.5em; | |
} | |
</style> | |
<h3>This page contains the following errors:</h3> | |
<div class="message"></div> | |
<h3>For more details, see DevTools Console</h3> | |
<button value="close">Close</button> | |
`; | |
let weakMap = new WeakMap(); | |
class ScriptErrorElement extends HTMLElement { | |
static get observedAttributes() { | |
return ['open']; | |
} | |
constructor() { | |
super(); | |
const shadowRoot = this.attachShadow({mode: 'closed'}); | |
shadowRoot.appendChild(htmlTemplate.content.cloneNode(true)); | |
shadowRoot.querySelector('button[value="close"]').addEventListener('click', () => { | |
this.open = false; | |
this.dispatchEvent(new Event('close')); | |
}); | |
weakMap.set(this, shadowRoot); | |
this.style.margin = '16px'; | |
this._onclose = null; | |
} | |
show() { | |
this.open = true; | |
} | |
get open() { | |
return this.getAttribute('open') !== null; | |
} | |
set open(v) { | |
let willOpen = !!v; | |
let isOpen = this.getAttribute('open') !== null; | |
if (isOpen && !willOpen) { | |
this.removeAttribute('open'); | |
} else if (!isOpen && willOpen) { | |
this.setAttribute('open', ''); | |
} | |
} | |
set value(value) { | |
let shadowRoot = weakMap.get(this); | |
shadowRoot.querySelector('.message').textContent = value; | |
} | |
get value() { | |
let shadowRoot = weakMap.get(this); | |
return shadowRoot.querySelector('.message').textContent; | |
} | |
set onclose(handler) { | |
let valid = typeof handler === 'function' || typeof handler === 'object' * handler.handleEvent; | |
this.removeEventListener('close', this._onclose); | |
if (valid) { | |
this.addEventListener('close', handler); | |
this._onclose = handler; | |
} else { | |
this._onclose = null; | |
} | |
} | |
get onclose() { | |
return this._onclose; | |
} | |
connectedCallback() { | |
// TODO | |
} | |
disconnectedCallback() { | |
// TODO | |
} | |
} | |
Object.defineProperty(ScriptErrorElement, Symbol.toStringTag, { | |
configurable: true, | |
value: 'ScriptErrorElement' | |
}); | |
customElements.define('script-error', ScriptErrorElement); | |
let errorContainer; | |
let errorBuffer = []; | |
let ensureErrorContainer = () => { | |
if (!errorContainer) { | |
errorContainer = document.body.insertAdjacentElement('afterbegin', new ScriptErrorElement()); | |
errorContainer.onclose = () => { | |
errorBuffer.splice(0, errorBuffer.length); | |
}; | |
} | |
return errorContainer; | |
}; | |
/** | |
* @param {Error} error | |
* @param {Promise} [promise] | |
*/ | |
/** | |
* @param {string} summary | |
* @param {string} filename | |
* @param {string} lineno | |
* @param {string} colno | |
* @param {Error} [error] | |
*/ | |
function __showError__(errorOrSummary/* , filename, lineno, colno, error*/) { | |
let summary, filename, lineno, colno, error; | |
if (typeof errorOrSummary === 'string' && arguments.length >= 5) { | |
summary = (errorOrSummary.startsWith('Uncaught ') ? '' : 'Uncaught ') + errorOrSummary; | |
filename = arguments[1]; | |
lineno = arguments[2]; | |
colno = arguments[3]; | |
error = arguments[4]; | |
if (filename === '' || lineno === 0 && colno === 0) { | |
return false; | |
} | |
if (error && filename === document.documentURI && error.name === 'SyntaxError' && lineno < 10) { | |
// Ignore errors caused by Console eager evaluation | |
return false; | |
} | |
} else if (Object.prototype.toString.call(errorOrSummary) === '[object Error]') { | |
error = errorOrSummary; | |
let inPromise = arguments[1] instanceof Promise ? true : false; | |
summary = 'Uncaught ' + (inPromise ? '(in promise) ' : '') + error.toString(); | |
if (error.sourceURL !== undefined) { | |
filename = error.sourceURL; | |
lineno = error.line; | |
colno = error.column; | |
} else if (error.fileName !== undefined) { | |
filename = error.fileName; | |
lineno = error.lineNumber; | |
colno = error.columnNumber; | |
} else { | |
let lines = error.stack.split(/\n {2,4}/, 2); | |
if (lines.length === 2) { | |
let cause = lines[1]; | |
cause = cause.endsWith(')') ? cause.replace(/^at \S+ \(/g, '').slice(0, -1) : | |
cause.replace(/^at /, ''); | |
filename = cause.replace(/:(\d+):(\d+)$/, ($0, $1, $2) => { | |
lineno = $1; | |
colno = $2; | |
return ''; | |
}); | |
} else { | |
filename = ''; | |
lineno = 0; | |
colno = 0; | |
} | |
} | |
} else { | |
return false; | |
} | |
let errorMessage = summary + '\n at ' + (filename ? filename : '<anonymous>') + ':' + lineno + ':' + colno + '\n'; | |
errorBuffer.push(errorMessage); | |
if (errorBuffer.length > config.maxBufferItems) { | |
errorBuffer.shift(); | |
} | |
let container = ensureErrorContainer(); | |
let host = container.shadowRoot || container; | |
host.value = errorBuffer.join(''); | |
container.open = true; | |
return true; | |
} | |
window.addEventListener('error', (e) => { | |
let {message: summary, filename, lineno, colno, error} = e; | |
window.__showError__(summary, filename, lineno, colno, error); | |
}); | |
window.addEventListener('unhandledrejection', (e) => { | |
window.__showError__(e.reason, e.promise); | |
}); | |
// Expose a method to show caught errors, e.g. errors which are caught by Vue.js / React.js error handler | |
window.__showError__ = __showError__; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment