diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js index 3793e5dc239ab5e036326ecd5b4b594d767209a9..7bd3e58e8544eacccdc8df23c40a284daa6ef60d 100644 --- a/addons/web/static/src/js/services/crash_manager.js +++ b/addons/web/static/src/js/services/crash_manager.js @@ -29,6 +29,70 @@ window.addEventListener('unhandledrejection', ev => let active = true; +/** + * Format the traceback of an error. Basically, we just add the error message + * in the traceback if necessary (Chrome already does it by default, but not + * other browser. yay for non standard APIs) + * + * @param {Error} error + * @returns {string} + */ +function formatTraceback(error) { + const traceback = error.stack; + // Error.prototype.stack is non-standard. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + // However, most engines provide an implementation. + // In particular, Chrome formats the contents of Error.stack + // https://v8.dev/docs/stack-trace-api#compatibility + const browserDetection = new BrowserDetection(); + if (browserDetection.isBrowserChrome()) { + return error.stack; + } else { + return `${_t("Error:")} ${error.message}\n${error.stack}`; + } +} + +/** + * Returns an annotated traceback from an error. This is asynchronous because + * it needs to fetch the sourcemaps for each script involved in the error, + * then compute the correct file/line numbers and add the information to the + * correct line. + * + * @param {Error} error + * @returns {Promise<string>} + */ +async function annotateTraceback(error) { + const traceback = formatTraceback(error); + await ajax.loadJS('/web/static/lib/stacktracejs/stacktrace.js'); + const frames = await StackTrace.fromError(error); + const lines = traceback.split('\n'); + if (lines[lines.length-1].trim() === "") { + // firefox traceback have an empty line at the end + lines.splice(-1); + } + + // Chrome stacks contains some lines with (index 0) which apparently + // corresponds to some native functions (at least Promise.all). We need to + // ignore them because they will not correspond to a stackframe. + const skips = lines.filter(l => l.includes("(index 0")).length; + const offset = lines.length - frames.length - skips; + let lineIndex = offset; + let frameIndex = 0; + while (frameIndex < frames.length) { + const line = lines[lineIndex]; + if (line.includes("(index 0)")) { + lineIndex++; + continue; + } + const frame = frames[frameIndex]; + const info = ` (${frame.fileName}:${frame.lineNumber})`; + lines[lineIndex] = line + info; + lineIndex++; + frameIndex++; + } + return lines.join('\n'); +} + /** * An extension of Dialog Widget to render the warnings and errors on the website. * Extend it with your template of choice like ErrorDialog/WarningDialog @@ -37,7 +101,6 @@ var CrashManagerDialog = Dialog.extend({ xmlDependencies: (Dialog.prototype.xmlDependencies || []).concat( ['/web/static/src/xml/crash_manager.xml'] ), - jsLibs: ['/web/static/lib/stacktracejs/stacktrace.js'], /** * @param {Object} error @@ -53,24 +116,58 @@ var CrashManagerDialog = Dialog.extend({ this.traceback = error.traceback; core.bus.off('close_dialogs', this); }, + willStart: async function () { - await this._super(...arguments); - if (config.isDebug('assets') && this.error.data && this.error.data.jsError) { - // annotate the stacktrace with correct file/line number information - const frames = await StackTrace.fromError(this.error.data.jsError); - const lines = this.traceback.split('\n'); - if (lines[lines.length-1].trim() === "") { - // firefox traceback have an empty line at the end - lines.splice(-1); - } - const offset = lines.length - frames.length; - for (let i = 0; i < frames.length; i++) { - const info = ` (${frames[i].fileName}:${frames[i].lineNumber})`; - lines[offset + i] = lines[offset + i] + info; + await this._super() + const jsError = this.error.data && this.error.data.jsError; + if (jsError) { + if (config.isDebug('assets')) { + this.traceback = await annotateTraceback(jsError); + } else { + this.traceback = formatTraceback(jsError); } - this.traceback = lines.join('\n'); } }, + + start: async function () { + await this._super(); + const message = this.message; + const traceback = this.traceback; + + if (!traceback) { + return; + } + this.$(".o_error_detail").on("shown.bs.collapse", function (e) { + e.target.scrollTop = e.target.scrollHeight; + }); + + const $clipboardBtn = this.$(".o_clipboard_button"); + $clipboardBtn.tooltip({title: _t("Copied !"), trigger: "manual", placement: "left"}); + this.clipboard = new window.ClipboardJS($clipboardBtn[0], { + text: function () { + return (_t("Error") + ":\n" + message + "\n\n" + traceback).trim(); + }, + // Container added because of Bootstrap modal that give the focus to another element. + // We need to give to correct focus to ClipboardJS (see in ClipboardJS doc) + // https://github.com/zenorocha/clipboard.js/issues/155 + container: this.el, + }); + this.clipboard.on("success", function (e) { + _.defer(function () { + $clipboardBtn.tooltip("show"); + _.delay(function () { + $clipboardBtn.tooltip("hide"); + }, 800); + }); + }); + }, + destroy() { + if (this.clipboard) { + this.$(".o_clipboard_button").tooltip('dispose'); + this.clipboard.destroy(); + } + this._super(); + } }); var ErrorDialog = CrashManagerDialog.extend({ @@ -120,7 +217,6 @@ var CrashManager = AbstractService.extend({ 'odoo.exceptions.Warning': _lt("Warning"), }; - this.browserDetection = new BrowserDetection(); this._super.apply(this, arguments); // crash manager integration @@ -167,21 +263,11 @@ var CrashManager = AbstractService.extend({ // promise has been rejected due to a crash core.bus.on('crash_manager_unhandledrejection', this, function (ev) { if (ev.reason && ev.reason instanceof Error) { - // Error.prototype.stack is non-standard. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - // However, most engines provide an implementation. - // In particular, Chrome formats the contents of Error.stack - // https://v8.dev/docs/stack-trace-api#compatibility - let traceback; - if (self.browserDetection.isBrowserChrome()) { - traceback = ev.reason.stack; - } else { - traceback = `${_t("Error:")} ${ev.reason.message}\n${ev.reason.stack}`; - } + let traceback = ev.reason.stack; self.show_error({ type: _t("Odoo Client Error"), message: '', - data: {debug: _t('Traceback:') + "\n" + traceback}, + data: {debug: _t('Traceback:') + "\n" + traceback, jsError: ev.reason}, }); } else { // the rejection is not due to an Error, so prevent the browser @@ -276,41 +362,6 @@ var CrashManager = AbstractService.extend({ title: _.str.capitalize(error.type) || _t("Odoo Error"), }, error); - - // When the dialog opens, initialize the copy feature and destroy it when the dialog is closed - var $clipboardBtn; - var clipboard; - dialog.opened(function () { - // When the full traceback is shown, scroll it to the end (useful for better python error reporting) - dialog.$(".o_error_detail").on("shown.bs.collapse", function (e) { - e.target.scrollTop = e.target.scrollHeight; - }); - - $clipboardBtn = dialog.$(".o_clipboard_button"); - $clipboardBtn.tooltip({title: _t("Copied !"), trigger: "manual", placement: "left"}); - clipboard = new window.ClipboardJS($clipboardBtn[0], { - text: function () { - return (_t("Error") + ":\n" + error.message + "\n\n" + error.data.debug).trim(); - }, - // Container added because of Bootstrap modal that give the focus to another element. - // We need to give to correct focus to ClipboardJS (see in ClipboardJS doc) - // https://github.com/zenorocha/clipboard.js/issues/155 - container: dialog.el, - }); - clipboard.on("success", function (e) { - _.defer(function () { - $clipboardBtn.tooltip("show"); - _.delay(function () { - $clipboardBtn.tooltip("hide"); - }, 800); - }); - }); - }); - dialog.on("closed", this, function () { - $clipboardBtn.tooltip('dispose'); - clipboard.destroy(); - }); - return dialog.open(); }, show_message: function(exception) {