FEATURE: Catch decorateCooked errors from themes/plugins (#15450)
If a theme/plugin raises an error while decorating post content, the decorator will be skipped, and the error reported on the console. Additionally, administrators will be shown a red warning at the top of the screen. This commit refactors and re-uses some of the logic from the theme-initializer-error-reporting logic. In future, new error reports can be added by doing something like: ``` document.dispatchEvent( new CustomEvent("discourse-error", { detail: { messageKey: "some.translation.key", error }, }) ); ```
This commit is contained in:
parent
94560d2383
commit
1f1aa6a0d8
|
@ -3,7 +3,7 @@ import { buildResolver } from "discourse-common/resolver";
|
|||
import { isTesting } from "discourse-common/config/environment";
|
||||
|
||||
const _pluginCallbacks = [];
|
||||
let _themeErrors = [];
|
||||
let _unhandledThemeErrors = [];
|
||||
|
||||
const Discourse = Application.extend({
|
||||
rootElement: "#main",
|
||||
|
@ -24,12 +24,11 @@ const Discourse = Application.extend({
|
|||
if (!module) {
|
||||
throw new Error(moduleName + " must export an initializer.");
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
if (!themeId || isTesting()) {
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
_themeErrors.push([themeId, err]);
|
||||
fireThemeErrorEvent();
|
||||
fireThemeErrorEvent({ themeId, error });
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -38,12 +37,11 @@ const Discourse = Application.extend({
|
|||
init.initialize = (app) => {
|
||||
try {
|
||||
return oldInitialize.call(init, app.__container__, app);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
if (!themeId || isTesting()) {
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
_themeErrors.push([themeId, err]);
|
||||
fireThemeErrorEvent();
|
||||
fireThemeErrorEvent({ themeId, error });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -92,14 +90,22 @@ function moduleThemeId(moduleName) {
|
|||
}
|
||||
}
|
||||
|
||||
function fireThemeErrorEvent() {
|
||||
const event = new CustomEvent("discourse-theme-error");
|
||||
document.dispatchEvent(event);
|
||||
function fireThemeErrorEvent({ themeId, error }) {
|
||||
const event = new CustomEvent("discourse-error", {
|
||||
cancelable: true,
|
||||
detail: { themeId, error },
|
||||
});
|
||||
|
||||
const unhandled = document.dispatchEvent(event);
|
||||
|
||||
if (unhandled) {
|
||||
_unhandledThemeErrors.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAndClearThemeErrors() {
|
||||
const copy = _themeErrors;
|
||||
_themeErrors = [];
|
||||
export function getAndClearUnhandledThemeErrors() {
|
||||
const copy = _unhandledThemeErrors;
|
||||
_unhandledThemeErrors = [];
|
||||
return copy;
|
||||
}
|
||||
|
||||
|
|
|
@ -118,6 +118,21 @@ function canModify(klass, type, resolverName, changes) {
|
|||
}
|
||||
}
|
||||
|
||||
function wrapWithErrorHandler(func, messageKey) {
|
||||
return function () {
|
||||
try {
|
||||
return func.call(this, ...arguments);
|
||||
} catch (error) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("discourse-error", {
|
||||
detail: { messageKey, error },
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class PluginApi {
|
||||
constructor(version, container) {
|
||||
this.version = version;
|
||||
|
@ -309,6 +324,8 @@ class PluginApi {
|
|||
decorateCookedElement(callback, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
callback = wrapWithErrorHandler(callback, "broken_decorator_alert");
|
||||
|
||||
addDecorator(callback, { afterAdopt: !!opts.afterAdopt });
|
||||
|
||||
if (!opts.onlyStream) {
|
||||
|
|
|
@ -1,22 +1,43 @@
|
|||
import { isTesting } from "discourse-common/config/environment";
|
||||
import { getAndClearThemeErrors } from "discourse/app";
|
||||
import { getAndClearUnhandledThemeErrors } from "discourse/app";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import I18n from "I18n";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
const showingErrors = new Set();
|
||||
|
||||
export default {
|
||||
name: "theme-errors-handler",
|
||||
after: "inject-discourse-objects",
|
||||
|
||||
initialize(container) {
|
||||
const currentUser = container.lookup("current-user:main");
|
||||
if (isTesting()) {
|
||||
return;
|
||||
}
|
||||
renderErrorNotices(currentUser);
|
||||
document.addEventListener("discourse-theme-error", () =>
|
||||
renderErrorNotices(currentUser)
|
||||
);
|
||||
|
||||
this.currentUser = container.lookup("current-user:main");
|
||||
|
||||
getAndClearUnhandledThemeErrors().forEach((e) => {
|
||||
reportThemeError(this.currentUser, e);
|
||||
});
|
||||
|
||||
document.addEventListener("discourse-error", this.handleDiscourseError);
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
document.removeEventListener(this.handleDiscourseError);
|
||||
delete this.currentUser;
|
||||
},
|
||||
|
||||
@bind
|
||||
handleDiscourseError(e) {
|
||||
if (e.detail?.themeId) {
|
||||
reportThemeError(this.currentUser, e);
|
||||
} else {
|
||||
reportGenericError(this.currentUser, e);
|
||||
}
|
||||
e.preventDefault(); // Mark as handled
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -32,24 +53,42 @@ function reportToLogster(name, error) {
|
|||
});
|
||||
}
|
||||
|
||||
function renderErrorNotices(currentUser) {
|
||||
getAndClearThemeErrors().forEach(([themeId, error]) => {
|
||||
const name =
|
||||
PreloadStore.get("activatedThemes")[themeId] || `(theme-id: ${themeId})`;
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(`An error occurred in the "${name}" theme/component:`, error);
|
||||
reportToLogster(name, error);
|
||||
if (!currentUser || !currentUser.admin) {
|
||||
return;
|
||||
}
|
||||
const path = getURL("/admin/customize/themes");
|
||||
const message = I18n.t("themes.broken_theme_alert", {
|
||||
theme: name,
|
||||
path: `<a href="${path}">${path}</a>`,
|
||||
});
|
||||
const alertDiv = document.createElement("div");
|
||||
alertDiv.classList.add("broken-theme-alert");
|
||||
alertDiv.innerHTML = `⚠️ ${message}`;
|
||||
document.body.prepend(alertDiv);
|
||||
function reportThemeError(currentUser, e) {
|
||||
const { themeId, error } = e.detail;
|
||||
|
||||
const name =
|
||||
PreloadStore.get("activatedThemes")[themeId] || `(theme-id: ${themeId})`;
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(`An error occurred in the "${name}" theme/component:`, error);
|
||||
reportToLogster(name, error);
|
||||
|
||||
const path = getURL("/admin/customize/themes");
|
||||
const message = I18n.t("themes.broken_theme_alert", {
|
||||
theme: name,
|
||||
path: `<a href="${path}">${path}</a>`,
|
||||
});
|
||||
displayErrorNotice(currentUser, message);
|
||||
}
|
||||
|
||||
function reportGenericError(currentUser, e) {
|
||||
const { messageKey, error } = e.detail;
|
||||
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(error);
|
||||
|
||||
if (messageKey && !showingErrors.has(messageKey)) {
|
||||
showingErrors.add(messageKey);
|
||||
displayErrorNotice(currentUser, I18n.t(messageKey));
|
||||
}
|
||||
}
|
||||
|
||||
function displayErrorNotice(currentUser, message) {
|
||||
if (!currentUser?.admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const alertDiv = document.createElement("div");
|
||||
alertDiv.classList.add("broken-theme-alert");
|
||||
alertDiv.innerHTML = `⚠️ ${message}`;
|
||||
document.body.prepend(alertDiv);
|
||||
}
|
||||
|
|
|
@ -195,6 +195,8 @@ en:
|
|||
default_description: "Default"
|
||||
broken_theme_alert: "Your site may not work because theme / component %{theme} has errors. Disable it at %{path}."
|
||||
|
||||
broken_decorator_alert: "Posts may not display correctly because one of the post content decorators on your site raised an error. Check the browser developer tools for more information."
|
||||
|
||||
s3:
|
||||
regions:
|
||||
ap_northeast_1: "Asia Pacific (Tokyo)"
|
||||
|
|
Loading…
Reference in New Issue