[REF] web, website, website_forum, *: remove crash manager from frontend

*: test_website

In a previous commit, the new services and environment were made
available in the frontend. This now allows errors to be handled by the
new error service, and makes the legacy crash manager redundant. This
commit removes it.

Part of #72675

Related: odoo/enterprise#19258
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Samuel Degueldre
2021-07-14 07:34:28 +00:00
parent 95504aa0e1
commit 57ba855b66
10 changed files with 84 additions and 454 deletions
@@ -49,8 +49,8 @@ tour.register('test_error_website', {
trigger: 'a[href="/test_internal_error_json"]',
}, {
content: "rpc error 500 modal is an ErrorDialog",
extra_trigger: 'div.o_dialog_error.modal-body div.alert.alert-warning',
trigger: 'button.btn.btn-primary[type="button"]',
extra_trigger: 'div.o_dialog_error.modal-content div.alert.alert-warning',
trigger: '.modal-footer button.btn.btn-primary',
},
// HTTP ERROR
{
+3
View File
@@ -205,6 +205,7 @@ This module provides the core of the Odoo Web Client.
# TODO: split debug files correctly when converting frontend debug
# manager so we don't need action-service and stuff
('remove', 'web/static/src/core/debug/**/*'),
'web/static/src/public/error_notifications.js',
'web/static/src/legacy/scss/base_frontend.scss',
'web/static/src/legacy/scss/lazyloader.scss',
@@ -219,6 +220,8 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/js/public/public_root.js',
'web/static/src/legacy/js/public/public_root_instance.js',
'web/static/src/legacy/js/public/public_widget.js',
'web/static/src/legacy/legacy_promise_error_handler.js',
'web/static/src/legacy/legacy_rpc_error_handler.js',
('include', 'web.frontend_legacy'),
],
@@ -10,13 +10,19 @@ import { capitalize } from "../utils/strings";
const { hooks } = owl;
const { useState } = hooks;
const odooExceptionTitleMap = new Map();
odooExceptionTitleMap.set("odoo.exceptions.AccessDenied", _lt("Access Denied"));
odooExceptionTitleMap.set("odoo.exceptions.AccessError", _lt("Access Error"));
odooExceptionTitleMap.set("odoo.exceptions.MissingError", _lt("Missing Record"));
odooExceptionTitleMap.set("odoo.exceptions.UserError", _lt("User Error"));
odooExceptionTitleMap.set("odoo.exceptions.ValidationError", _lt("Validation Error"));
export const odooExceptionTitleMap = new Map(
Object.entries({
"odoo.addons.base.models.ir_mail_server.MailDeliveryException": _lt(
"MailDeliveryException"
),
"odoo.exceptions.AccessDenied": _lt("Access Denied"),
"odoo.exceptions.MissingError": _lt("Missing Record"),
"odoo.exceptions.UserError": _lt("User Error"),
"odoo.exceptions.ValidationError": _lt("Validation Error"),
"odoo.exceptions.AccessError": _lt("Access Error"),
"odoo.exceptions.Warning": _lt("Warning")
})
);
// -----------------------------------------------------------------------------
// Generic Error Dialog
@@ -40,13 +40,13 @@ Notification.props = {
return (
typeof m === "string" || (typeof m === "object" && typeof m.toString === "function")
);
},
}
},
title: { type: [String, Boolean], optional: true },
title: { type: [String, Boolean, { toString: Function }], optional: true },
type: {
type: String,
optional: true,
validate: (t) => ["warning", "danger", "success", "info"].includes(t),
validate: (t) => ["warning", "danger", "success", "info"].includes(t)
},
messageIsHtml: { type: Boolean, optional: true },
className: { type: String, optional: true },
@@ -58,15 +58,15 @@ Notification.props = {
name: { type: String },
icon: { type: String, optional: true },
primary: { type: Boolean, optional: true },
onClick: Function,
},
},
onClick: Function
}
}
},
onClose: { type: Function, optional: true },
onClose: { type: Function, optional: true }
};
Notification.defaultProps = {
buttons: [],
className: "",
messageIsHtml: false,
type: "warning",
type: "warning"
};
@@ -1,398 +0,0 @@
odoo.define('web.CrashManager', function (require) {
"use strict";
const AbstractService = require('web.AbstractService');
var ajax = require('web.ajax');
var core = require('web.core');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
var config = require('web.config');
var { formatTraceback, annotateTraceback } = require("@web/legacy/js/core/error_utils");
var _t = core._t;
var _lt = core._lt;
// Register this eventlistener before qunit does.
// Some errors needs to be negated by the crash_manager.
window.addEventListener('unhandledrejection', ev =>
core.bus.trigger('crash_manager_unhandledrejection', ev)
);
let active = true;
/**
* An extension of Dialog Widget to render the warnings and errors on the website.
* Extend it with your template of choice like ErrorDialog/WarningDialog
*/
var CrashManagerDialog = Dialog.extend({
xmlDependencies: (Dialog.prototype.xmlDependencies || []).concat(
['/web/static/src/legacy/frontend/public_crash_manager/public_crash_manager.xml']
),
/**
* @param {Object} error
* @param {string} error.message the message in Warning/Error Dialog
* @param {string} error.traceback the traceback in ErrorDialog
*
* @constructor
*/
init: function (parent, options, error) {
this._super.apply(this, [parent, options]);
this.error = error;
this.message = error.message;
this.traceback = error.traceback;
core.bus.off('close_dialogs', this);
},
willStart: async function () {
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);
}
}
},
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({
template: 'CrashManager.error',
});
var WarningDialog = CrashManagerDialog.extend({
template: 'CrashManager.warning',
/**
* Sets size to medium by default.
*
* @override
*/
init: function (parent, options, error) {
this._super(parent, _.extend({
size: 'medium',
}, options), error);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Focuses the ok button.
*
* @override
*/
open: function () {
this._super({shouldFocusButtons: true});
},
});
var CrashManager = AbstractService.extend({
init: function () {
var self = this;
active = true;
this.isConnected = true;
this.odooExceptionTitleMap = {
'odoo.addons.base.models.ir_mail_server.MailDeliveryException': _lt("MailDeliveryException"),
'odoo.exceptions.AccessDenied': _lt("Access Denied"),
'odoo.exceptions.AccessError': _lt("Access Error"),
'odoo.exceptions.MissingError': _lt("Missing Record"),
'odoo.exceptions.UserError': _lt("User Error"),
'odoo.exceptions.ValidationError': _lt("Validation Error"),
'odoo.exceptions.Warning': _lt("Warning"),
};
this._super.apply(this, arguments);
// crash manager integration
core.bus.on('rpc_error', this, this.rpc_error);
window.onerror = function (message, file, line, col, error) {
// Scripts injected in DOM (eg: google API's js files) won't return a clean error on window.onerror.
// The browser will just give you a 'Script error.' as message and nothing else for security issue.
// To enable onerror to work properly with CORS file, you should:
// 1. add crossorigin="anonymous" to your <script> tag loading the file
// 2. enabling 'Access-Control-Allow-Origin' on the server serving the file.
// Since in some case it wont be possible to to this, this handle should have the possibility to be
// handled by the script manipulating the injected file. For this, you will use window.onOriginError
// If it is not handled, we should display something clearer than the common crash_manager error dialog
// since it won't show anything except "Script error."
// This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html
if (!file && !line && !col) {
// Chrome and Opera set "Script error." on the `message` and hide the `error`
// Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error`
if (window.onOriginError) {
window.onOriginError();
delete window.onOriginError;
} else {
self.show_error({
type: _t("Odoo Client Error"),
message: _t("Unknown CORS error"),
data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")},
});
}
} else {
// ignore Chrome video internal error: https://crbug.com/809574
if (!error && message === 'ResizeObserver loop limit exceeded') {
return;
}
var traceback = error ? error.stack : '';
self.show_error({
type: _t("Odoo Client Error"),
message: message,
data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback, jsError: error},
});
}
};
// listen to unhandled rejected promises, and throw an error when the
// promise has been rejected due to a crash
core.bus.on('crash_manager_unhandledrejection', this, function (ev) {
if (ev.reason && ev.reason instanceof Error) {
let traceback = ev.reason.stack;
self.show_error({
type: _t("Odoo Client Error"),
message: '',
data: {debug: _t('Traceback:') + "\n" + traceback, jsError: ev.reason},
});
} else {
// the rejection is not due to an Error, so prevent the browser
// from displaying an 'unhandledrejection' error in the console
ev.stopPropagation();
ev.stopImmediatePropagation();
ev.preventDefault();
}
});
},
enable: function () {
active = true;
},
disable: function () {
active = false;
},
handleLostConnection: function () {
var self = this;
if (!this.isConnected) {
// already handled, nothing to do. This can happen when several
// rpcs are done in parallel and fail because of a lost connection.
return;
}
this.isConnected = false;
var delay = 2000;
core.bus.trigger('connection_lost');
setTimeout(function checkConnection() {
ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () {
core.bus.trigger('connection_restored');
self.isConnected = true;
}).guardedCatch(function () {
// exponential backoff, with some jitter
delay = (delay * 1.5) + 500*Math.random();
setTimeout(checkConnection, delay);
});
}, delay);
},
rpc_error: function(error) {
// Some qunit tests produces errors before the DOM is set.
// This produces an error loop as the modal/toast has no DOM to attach to.
if (!document.body || !active || this.connection_lost) return;
// Connection lost error
if (error.code === -32098) {
this.handleLostConnection();
return;
}
// Special exception handlers, see crash_registry bellow
var handler = core.crash_registry.get(error.data.name, true);
if (handler) {
new (handler)(this, error).display();
return;
}
// Odoo custom exception: UserError, AccessError, ...
if (_.has(this.odooExceptionTitleMap, error.data.name)) {
error = _.extend({}, error, {
data: _.extend({}, error.data, {
message: error.data.arguments[0],
title: this.odooExceptionTitleMap[error.data.name],
}),
});
this.show_warning(error);
return;
}
// Any other Python exception
this.show_error(error);
},
show_warning: function (error, options) {
if (!active) {
return;
}
var message = error.data ? error.data.message : error.message;
var title = _t("Something went wrong !");
if (error.type) {
title = _.str.capitalize(error.type);
} else if (error.data && error.data.title) {
title = _.str.capitalize(error.data.title);
}
return this.displayNotification(Object.assign({}, options, {
title,
message,
sticky: true,
}));
},
show_error: function (error) {
if (!active) {
return;
}
error.traceback = error.data.debug;
var dialog = new ErrorDialog(this, {
title: _.str.capitalize(error.type) || _t("Odoo Error"),
}, error);
return dialog.open();
},
show_message: function(exception) {
return this.show_error({
type: _t("Odoo Client Error"),
message: exception,
data: {debug: ""}
});
},
});
/**
* An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
*/
var ExceptionHandler = {
/**
* @param parent The parent.
* @param error The error object as returned by the JSON-RPC implementation.
*/
init: function(parent, error) {},
/**
* Called to inform to display the widget, if necessary. A typical way would be to implement
* this interface in a class extending instance.web.Dialog and simply display the dialog in this
* method.
*/
display: function() {},
};
/**
* Handle redirection warnings, which behave more or less like a regular
* warning, with an additional redirection button.
*/
var RedirectWarningHandler = Widget.extend(ExceptionHandler, {
init: function(parent, error) {
this._super(parent);
this.error = error;
},
display: function() {
var self = this;
var error = this.error;
var additional_context = _.extend({}, this.context, error.data.arguments[3]);
new WarningDialog(this, {
title: _.str.capitalize(error.type) || _t("Odoo Warning"),
buttons: [
{text: error.data.arguments[2], classes : "btn-primary", click: function() {
self.do_action(
error.data.arguments[1],
{
additional_context: additional_context,
});
self.destroy();
}, close: true},
{text: _t("Cancel"), click: function() { self.destroy(); }, close: true}
]
}, {
message: error.data.arguments[0],
}).open();
}
});
core.crash_registry.add('odoo.exceptions.RedirectWarning', RedirectWarningHandler);
function session_expired(cm) {
return {
display: function () {
const notif = {
type: _t("Odoo Session Expired"),
message: _t("Your Odoo session expired. The current page is about to be refreshed."),
};
const options = {
buttons: [{
text: _t("Ok"),
click: () => window.location.reload(true),
close: true
}],
};
cm.show_warning(notif, options);
}
};
}
core.crash_registry.add('odoo.http.SessionExpiredException', session_expired);
core.crash_registry.add('werkzeug.exceptions.Forbidden', session_expired);
core.crash_registry.add('504', function (cm) {
return {
display: function () {
cm.show_warning({
type: _t("Request timeout"),
message: _t("The operation was interrupted. This usually means that the current operation is taking too much time.")});
}
};
});
core.serviceRegistry.add('crash_manager', CrashManager);
return {
CrashManager: CrashManager,
ErrorDialog: ErrorDialog,
WarningDialog: WarningDialog,
disable: () => active = false,
};
});
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="CrashManager.warning" class="o_dialog_warning" role="alert">
<p t-esc="widget.message" style="white-space: pre-wrap;"/>
</div>
<div t-name="CrashManager.error" class="o_dialog_error">
<div class="alert alert-warning clearfix" role="alert">
<div class="float-right ml8 btn-group-vertical">
<button class="btn btn-primary o_clipboard_button">
<i class="fa fa-clipboard mr8"/>Copy the full error to clipboard
</button>
</div>
<p><b>An error occurred</b></p>
<p>Please use the copy button to report the error to your support service.</p>
</div>
<t t-set="errUID" t-value="_.uniqueId()"/>
<button class="btn btn-link" t-att-data-target="'#o_error_detail' + errUID" data-toggle="collapse">See details</button>
<div t-att-id="'o_error_detail' + errUID" class="collapse alert alert-danger o_error_detail" role="alert">
<pre t-esc="widget.message"/>
<pre t-esc="widget.traceback"/>
</div>
</div>
</templates>
@@ -17,6 +17,7 @@ import {
makeLegacyRpcService,
makeLegacySessionService,
makeLegacyDialogMappingService,
mapLegacyEnvToWowlEnv,
} from "../../utils";
import { ComponentAdapter } from "web.OwlCompatibility";
@@ -370,13 +371,13 @@ export async function createPublicRoot(RootWidget) {
serviceRegistry.add("legacy_rpc", makeLegacyRpcService(legacyEnv));
serviceRegistry.add("legacy_session", makeLegacySessionService(legacyEnv, session));
serviceRegistry.add("legacy_notification", makeLegacyNotificationService(legacyEnv));
serviceRegistry.add("legacy_crash_manager", makeLegacyCrashManagerService(legacyEnv));
serviceRegistry.add("legacy_dialog_mapping", makeLegacyDialogMappingService(legacyEnv));
await Promise.all([owl.utils.whenReady(), session.is_bound]);
const wowlEnv = makeEnv();
wowlEnv.qweb.addTemplates(await loadBundleTemplates("web.assets_frontend"));
await startServices(wowlEnv);
mapLegacyEnvToWowlEnv(legacyEnv, wowlEnv);
const adapter = new ComponentAdapter(null, { Component }); // Used for _trigger_up compat layer
const publicRoot = new RootWidget(adapter);
@@ -4,6 +4,7 @@ import { registry } from "@web/core/registry";
import { RPCErrorDialog } from "../core/errors/error_dialogs";
import { RPCError } from "../core/network/rpc_service";
const errorNotificationRegistry = registry.category("error_notifications");
const errorDialogRegistry = registry.category("error_dialogs");
const errorHandlerRegistry = registry.category("error_handlers");
@@ -39,8 +40,15 @@ function legacyRPCErrorHandler(env, error, originalError) {
event.preventDefault();
const exceptionName = originalError.exceptionName;
let ErrorComponent = originalError.Component;
if (!ErrorComponent && exceptionName && errorDialogRegistry.contains(exceptionName)) {
ErrorComponent = errorDialogRegistry.get(exceptionName);
if (!ErrorComponent && exceptionName) {
if (errorNotificationRegistry.contains(exceptionName)) {
const notif = errorNotificationRegistry.get(exceptionName);
env.services.notification.add(notif.message || originalError.data.message, notif);
return true;
}
if (errorDialogRegistry.contains(exceptionName)) {
ErrorComponent = errorDialogRegistry.get(exceptionName);
}
}
env.services.dialog.add(ErrorComponent || RPCErrorDialog, {
@@ -51,7 +59,7 @@ function legacyRPCErrorHandler(env, error, originalError) {
data: originalError.data,
subType: originalError.subType,
code: originalError.code,
type: originalError.type,
type: originalError.type
});
return true;
}
@@ -0,0 +1,37 @@
/** @odoo-module */
// This module makes it so that some errors only display a notification instead of an error dialog
import { registry } from "@web/core/registry";
import { odooExceptionTitleMap } from "@web/core/errors/error_dialogs";
import { _t } from "@web/core/l10n/translation";
odooExceptionTitleMap.forEach((title, exceptionName) => {
registry.category("error_notifications").add(exceptionName, {
title: title,
type: "warning",
sticky: true
});
});
const sessionExpired = {
title: _t("Odoo Session Expired"),
message: _t("Your Odoo session expired. The current page is about to be refreshed."),
buttons: [
{
text: _t("Ok"),
click: () => window.location.reload(true),
close: true
}
]
};
registry
.category("error_notifications")
.add("odoo.http.SessionExpiredException", sessionExpired)
.add("werkzeug.exceptions.Forbidden", sessionExpired)
.add("504", {
title: _t("Request timeout"),
message: _t(
"The operation was interrupted. This usually means that the current operation is taking too much time."
)
});
@@ -246,11 +246,11 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
msg = _t("Sorry you must be logged in to perform this action");
title = _t("Access Denied");
}
this.call('crash_manager', 'show_warning', {
this.displayNotification({
message: msg,
title: title,
}, {
sticky: false,
type: "warning",
});
},
/**
@@ -308,11 +308,11 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
} else if (data.error === 'post_non_flaggable') {
message = _t("This post can not be flagged");
}
self.call('crash_manager', 'show_warning', {
this.displayNotification({
message: message,
title: _t("Access Denied"),
}, {
sticky: false,
type: "warning",
});
} else if (data.success) {
var elem = $link;
@@ -348,11 +348,11 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
} else if (data.error === 'anonymous_user') {
message = _t('Sorry you must be logged to vote');
}
self.call('crash_manager', 'show_warning', {
this.displayNotification({
message: message,
title: _t("Access Denied"),
}, {
sticky: false,
type: "warning",
});
} else {
var $container = $btn.closest('.vote');
@@ -427,11 +427,11 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
if (data.error === 'anonymous_user') {
var message = _t("Sorry, anonymous users cannot choose correct answer.");
}
this.call('crash_manager', 'show_warning', {
this.displayNotification({
message: message,
title: _t("Access Denied"),
}, {
sticky: false,
type: "warning",
});
} else {
_.each(this.$('.forum_answer'), answer => {