diff --git a/app/assets/javascripts/discourse/app/instance-initializers/boot-client-error-handler.js b/app/assets/javascripts/discourse/app/instance-initializers/boot-services.js similarity index 62% rename from app/assets/javascripts/discourse/app/instance-initializers/boot-client-error-handler.js rename to app/assets/javascripts/discourse/app/instance-initializers/boot-services.js index d9f4480b9f4..44c7b39deb6 100644 --- a/app/assets/javascripts/discourse/app/instance-initializers/boot-client-error-handler.js +++ b/app/assets/javascripts/discourse/app/instance-initializers/boot-services.js @@ -1,5 +1,6 @@ export default { initialize(owner) { owner.lookup("service:client-error-handler"); + owner.lookup("service:deprecation-warning-handler"); }, }; diff --git a/app/assets/javascripts/discourse/app/services/deprecation-warning-handler.js b/app/assets/javascripts/discourse/app/services/deprecation-warning-handler.js new file mode 100644 index 00000000000..3483e60cc4c --- /dev/null +++ b/app/assets/javascripts/discourse/app/services/deprecation-warning-handler.js @@ -0,0 +1,111 @@ +import { registerDeprecationHandler } from "@ember/debug"; +import Service, { inject as service } from "@ember/service"; +import { addGlobalNotice } from "discourse/components/global-notice"; +import identifySource from "discourse/lib/source-identifier"; +import { escapeExpression } from "discourse/lib/utilities"; +import { registerDeprecationHandler as registerDiscourseDeprecationHandler } from "discourse-common/lib/deprecated"; +import { bind } from "discourse-common/utils/decorators"; +import I18n from "discourse-i18n"; + +// Deprecations matching patterns on this list will trigger warnings for admins. +// To avoid 'crying wolf', we should only add values here when we're sure they're +// not being triggered by core or official themes/plugins. +export const CRITICAL_DEPRECATIONS = [ + /^discourse.modal-controllers$/, + /^(?!discourse\.)/, // All unsilenced ember deprecations +]; + +// Deprecation handling APIs don't have any way to unregister handlers, so we set up permenant +// handlers and link them up to the application lifecycle using module-local state. +let handler; +registerDeprecationHandler((message, opts, next) => { + handler?.(message, opts); + return next(message, opts); +}); +registerDiscourseDeprecationHandler((message, opts) => + handler?.(message, opts) +); + +export default class DeprecationWarningHandler extends Service { + @service currentUser; + @service siteSettings; + + #adminWarned = false; + + constructor() { + super(...arguments); + handler = this.handle; + } + + willDestroy() { + handler = null; + } + + @bind + handle(message, opts) { + const workflowConfigs = window.deprecationWorkflow?.config?.workflow; + const matchingConfig = workflowConfigs.find( + (config) => config.matchId === opts.id + ); + + if (matchingConfig && matchingConfig.handler === "silence") { + return; + } + + const source = identifySource(); + if (source?.type === "browser-extension") { + return; + } + + this.maybeNotifyAdmin(opts.id, source); + } + + maybeNotifyAdmin(id, source) { + if (this.#adminWarned) { + return; + } + + if (!this.currentUser?.admin) { + return; + } + + if (!this.siteSettings.warn_critical_js_deprecations) { + return; + } + + if (CRITICAL_DEPRECATIONS.some((pattern) => pattern.test(id))) { + this.notifyAdmin(id, source); + } + } + + notifyAdmin(id, source) { + this.#adminWarned = true; + + let notice = I18n.t("critical_deprecation.notice"); + + if (this.siteSettings.warn_critical_js_deprecations_message) { + notice += " " + this.siteSettings.warn_critical_js_deprecations_message; + } + + if (source?.type === "theme") { + notice += + " " + + I18n.t("critical_deprecation.theme_source", { + name: escapeExpression(source.name), + path: source.path, + }); + } else if (source?.type === "plugin") { + notice += + " " + + I18n.t("critical_deprecation.plugin_source", { + name: escapeExpression(source.name), + }); + } + + addGlobalNotice(notice, "critical-deprecation", { + dismissable: true, + dismissDuration: moment.duration(1, "day"), + level: "warn", + }); + } +} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a2c57341b3e..544879944de 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -225,6 +225,11 @@ en: broken_plugin_alert: "Caused by plugin '%{name}'" + critical_deprecation: + notice: "[Admin Notice] One of your themes or plugins needs updating for compatibility with upcoming Discourse core changes (more info)." + theme_source: "Identified theme: '%{name}'." + plugin_source: "Identified plugin: '%{name}'" + s3: regions: ap_northeast_1: "Asia Pacific (Tokyo)" diff --git a/config/site_settings.yml b/config/site_settings.yml index 675f99ade1e..1f22b96a238 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -2308,6 +2308,14 @@ developer: default: false client: true hidden: true + warn_critical_js_deprecations: + default: true + client: true + hidden: true + warn_critical_js_deprecations_message: + default: "" + client: true + hidden: true navigation: navigation_menu: diff --git a/spec/system/ember_deprecation_spec.rb b/spec/system/ember_deprecation_spec.rb index 37779bece3f..738d4c642b7 100644 --- a/spec/system/ember_deprecation_spec.rb +++ b/spec/system/ember_deprecation_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -describe "Production mode debug shims", type: :system do - it "can successfully print a deprecation message after applying prod shims" do +describe "JS Deprecation Handling", type: :system do + it "can successfully print a deprecation message after applying production-mode shims" do visit("/latest") expect(find("#main-outlet-wrapper")).to be_visible @@ -31,4 +31,25 @@ describe "Production mode debug shims", type: :system do expect(call).to eq("DEPRECATION: Some message [deprecation id: some.id]") expect(backtrace).to include("shimLogDeprecationToConsole") end + + it "shows warnings to admins for critical deprecations" do + sign_in Fabricate(:admin) + + SiteSetting.warn_critical_js_deprecations = true + SiteSetting.warn_critical_js_deprecations_message = + "Discourse core changes will be applied to your site on Jan 15." + + visit("/latest") + + page.execute_script <<~JS + const deprecated = require("discourse-common/lib/deprecated").default; + deprecated("Fake deprecation message", { id: "fake-deprecation" }) + JS + + message = find("#global-notice-critical-deprecation") + expect(message).to have_text( + "One of your themes or plugins needs updating for compatibility with upcoming Discourse core changes", + ) + expect(message).to have_text(SiteSetting.warn_critical_js_deprecations_message) + end end