diff --git a/app/assets/javascripts/discourse/app/routes/application.js b/app/assets/javascripts/discourse/app/routes/application.js index 0187ace3700..ee27aca1e3c 100644 --- a/app/assets/javascripts/discourse/app/routes/application.js +++ b/app/assets/javascripts/discourse/app/routes/application.js @@ -8,6 +8,7 @@ import { setting } from "discourse/lib/computed"; import cookie from "discourse/lib/cookie"; import logout from "discourse/lib/logout"; import mobile from "discourse/lib/mobile"; +import identifySource, { consolePrefix } from "discourse/lib/source-identifier"; import DiscourseURL from "discourse/lib/url"; import Category from "discourse/models/category"; import Composer from "discourse/models/composer"; @@ -39,6 +40,7 @@ const ApplicationRoute = DiscourseRoute.extend({ loadingSlider: service(), router: service(), siteSettings: service(), + clientErrorHandler: service(), get includeExternalLoginMethods() { return ( @@ -117,15 +119,24 @@ const ApplicationRoute = DiscourseRoute.extend({ const xhrOrErr = err.jqXHR ? err.jqXHR : err; const exceptionController = this.controllerFor("exception"); - const c = window.console; - if (c && c.error) { - c.error(xhrOrErr); - } + const themeOrPluginSource = identifySource(err); + + // eslint-disable-next-line no-console + console.error( + ...[consolePrefix(err, themeOrPluginSource), xhrOrErr].filter(Boolean) + ); if (xhrOrErr && xhrOrErr.status === 404) { return this.router.transitionTo("exception-unknown"); } + if (themeOrPluginSource) { + this.clientErrorHandler.displayErrorNotice( + "Error loading route", + themeOrPluginSource + ); + } + exceptionController.setProperties({ lastTransition: transition, thrown: xhrOrErr, diff --git a/app/assets/javascripts/discourse/app/services/client-error-handler.js b/app/assets/javascripts/discourse/app/services/client-error-handler.js index 07bda6e0a4c..082b7cfb12a 100644 --- a/app/assets/javascripts/discourse/app/services/client-error-handler.js +++ b/app/assets/javascripts/discourse/app/services/client-error-handler.js @@ -83,11 +83,15 @@ export default class ClientErrorHandlerService extends Service { let html = `⚠️ ${escape(message)}`; - if (source && source.type === "theme") { + if (source?.type === "theme") { html += `
${I18n.t("themes.error_caused_by", { name: escape(source.name), path: source.path, })}`; + } else if (source?.type === "plugin") { + html += `
${I18n.t("broken_plugin_alert", { + name: escape(source.name), + })}`; } html += `
${I18n.t( diff --git a/app/assets/javascripts/discourse/tests/acceptance/client-error-handler-test.js b/app/assets/javascripts/discourse/tests/acceptance/client-error-handler-test.js new file mode 100644 index 00000000000..4e53b4d7a10 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/client-error-handler-test.js @@ -0,0 +1,30 @@ +import { getOwner } from "@ember/application"; +import { visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import Sinon from "sinon"; +import { acceptance } from "../helpers/qunit-helpers"; + +acceptance("client-error-handler service", function (needs) { + needs.user({ + admin: true, + }); + + test("displays route-loading errors caused by themes", async function (assert) { + const fakeError = new Error("Something bad happened"); + fakeError.stack = "assets/plugins/some-fake-plugin-name.js"; + + const topicRoute = getOwner(this).lookup("route:topic"); + Sinon.stub(topicRoute, "model").throws(fakeError); + + const consoleStub = Sinon.stub(console, "error"); + try { + await visit("/t/280"); + } catch {} + consoleStub.restore(); + + assert.dom(".broken-theme-alert-banner").exists(); + assert + .dom(".broken-theme-alert-banner") + .containsText("some-fake-plugin-name"); + }); +}); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index eb48bb84a51..43d643687f6 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -221,6 +221,8 @@ en: broken_decorator_alert: "Posts may not display correctly because one of the post content decorators on your site raised an error." + broken_plugin_alert: "Caused by plugin '%{name}'" + s3: regions: ap_northeast_1: "Asia Pacific (Tokyo)"