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