From a9861f48878328cae95b625dc3800e7cddd63a11 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 4 Jun 2024 16:55:26 +0000 Subject: [PATCH] Bug 1768174 - Show Remote Settings health in about:support r=Gijs,fluent-reviewers,bolsson,acottner Differential Revision: https://phabricator.services.mozilla.com/D210040 --- browser/base/content/test/about/browser.toml | 1 + .../test/about/browser_aboutSupport.js | 46 ++++++++++++ services/settings/remote-settings.sys.mjs | 22 ++++-- toolkit/content/aboutSupport.js | 25 +++++++ toolkit/content/aboutSupport.xhtml | 46 ++++++++++++ .../en-US/toolkit/about/aboutSupport.ftl | 14 ++++ toolkit/modules/Troubleshoot.sys.mjs | 19 +++++ .../tests/browser/browser_Troubleshoot.js | 75 +++++++++++++++---- 8 files changed, 227 insertions(+), 21 deletions(-) diff --git a/browser/base/content/test/about/browser.toml b/browser/base/content/test/about/browser.toml index 2c6dafb4dd2a..e07bc92d3a9d 100644 --- a/browser/base/content/test/about/browser.toml +++ b/browser/base/content/test/about/browser.toml @@ -76,6 +76,7 @@ skip-if = ["tsan"] # Bug 1676326, highly frequent on TSan ["browser_aboutSupport.js"] skip-if = ["os == 'linux' && os_version == '18.04' && asan"] # Bug 1713368 +tags = "remote-settings" ["browser_aboutSupport_newtab_security_state.js"] diff --git a/browser/base/content/test/about/browser_aboutSupport.js b/browser/base/content/test/about/browser_aboutSupport.js index 62f470c812f5..e9036c1cd6ff 100644 --- a/browser/base/content/test/about/browser_aboutSupport.js +++ b/browser/base/content/test/about/browser_aboutSupport.js @@ -9,6 +9,12 @@ const { ExperimentAPI } = ChromeUtils.importESModule( const { ExperimentFakes } = ChromeUtils.importESModule( "resource://testing-common/NimbusTestUtils.sys.mjs" ); +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); +ChromeUtils.defineESModuleGetters(this, { + sinon: "resource://testing-common/Sinon.sys.mjs", +}); add_task(async function () { await BrowserTestUtils.withNewTab( @@ -147,3 +153,43 @@ add_task(async function test_remote_configuration() { await doCleanup(); }); + +add_task(async function test_remote_settings() { + const sandbox = sinon.createSandbox(); + sandbox.stub(RemoteSettings, "inspect").resolves({ + isSynchronizationBroken: false, + lastCheck: 1715698289, + localTimestamp: '"1715698176626"', + history: { + "settings-sync": [ + { status: "SUCCESS", datetime: "2024-05-14T14:49:36.626Z", infos: {} }, + ], + }, + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:support" }, + async browser => { + const localTimestamp = await SpecialPowers.spawn( + browser, + [], + async () => { + const sel = "#support-remote-settings-local-timestamp"; + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(sel)?.innerText + ); + return content.document.querySelector(sel).innerText; + } + ); + Assert.equal( + localTimestamp, + '"1715698176626"', + "Rendered the local timestamp" + ); + } + ); + + registerCleanupFunction(() => { + sandbox.restore(); + }); +}); diff --git a/services/settings/remote-settings.sys.mjs b/services/settings/remote-settings.sys.mjs index c06e99eb2af5..ac4d76614d10 100644 --- a/services/settings/remote-settings.sys.mjs +++ b/services/settings/remote-settings.sys.mjs @@ -490,15 +490,22 @@ function remoteSettingsFunction() { /** * Returns an object with polling status information and the list of * known remote settings collections. + * @param {Object} options + * @param {boolean?} options.localOnly (optional) If set to `true`, do not contact the server. */ - remoteSettings.inspect = async () => { - // Make sure we fetch the latest server info, use a random cache bust value. - const randomCacheBust = 99990000 + Math.floor(Math.random() * 9999); - const { changes, currentEtag: serverTimestamp } = - await lazy.Utils.fetchLatestChanges(lazy.Utils.SERVER_URL, { - expected: randomCacheBust, - }); + remoteSettings.inspect = async (options = {}) => { + const { localOnly = false } = options; + let changes = []; + let serverTimestamp = null; + if (!localOnly) { + // Make sure we fetch the latest server info, use a random cache bust value. + const randomCacheBust = 99990000 + Math.floor(Math.random() * 9999); + ({ changes, currentEtag: serverTimestamp } = + await lazy.Utils.fetchLatestChanges(lazy.Utils.SERVER_URL, { + expected: randomCacheBust, + })); + } const collections = await Promise.all( changes.map(async change => { const { bucket, collection, last_modified: serverTimestamp } = change; @@ -537,6 +544,7 @@ function remoteSettingsFunction() { history: { [TELEMETRY_SOURCE_SYNC]: await lazy.gSyncHistory.list(), }, + isSynchronizationBroken: await isSynchronizationBroken(), }; }; diff --git a/toolkit/content/aboutSupport.js b/toolkit/content/aboutSupport.js index 282ab5b921e8..9941ada33be3 100644 --- a/toolkit/content/aboutSupport.js +++ b/toolkit/content/aboutSupport.js @@ -1504,6 +1504,31 @@ var snapshotFormatters = { ); }, + remoteSettings(data) { + if (!data) { + return; + } + const { isSynchronizationBroken, lastCheck, localTimestamp, history } = + data; + + $("support-remote-settings-status-ok").style.display = + isSynchronizationBroken ? "none" : "block"; + $("support-remote-settings-status-broken").style.display = + isSynchronizationBroken ? "block" : "none"; + $("support-remote-settings-last-check").textContent = lastCheck; + $("support-remote-settings-local-timestamp").textContent = localTimestamp; + $.append( + $("support-remote-settings-sync-history-tbody"), + history["settings-sync"].map(({ status, datetime, infos }) => + $.new("tr", [ + $.new("td", [document.createTextNode(status)]), + $.new("td", [document.createTextNode(datetime)]), + $.new("td", [document.createTextNode(JSON.stringify(infos))]), + ]) + ) + ); + }, + normandy(data) { if (!data) { return; diff --git a/toolkit/content/aboutSupport.xhtml b/toolkit/content/aboutSupport.xhtml index f815ab77f47d..9456c4bfd139 100644 --- a/toolkit/content/aboutSupport.xhtml +++ b/toolkit/content/aboutSupport.xhtml @@ -614,6 +614,52 @@ #endif + + +

+ + + + + + + + + + + + + + + + +
+ + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + +
+
+ #ifdef MOZ_NORMANDY

diff --git a/toolkit/locales/en-US/toolkit/about/aboutSupport.ftl b/toolkit/locales/en-US/toolkit/about/aboutSupport.ftl index f54f5e401468..c5997646ac3a 100644 --- a/toolkit/locales/en-US/toolkit/about/aboutSupport.ftl +++ b/toolkit/locales/en-US/toolkit/about/aboutSupport.ftl @@ -435,6 +435,20 @@ support-printing-modified-settings = Modified print settings support-printing-prefs-name = Name support-printing-prefs-value = Value +## Remote Settings sections + +support-remote-settings-title = Remote Settings +support-remote-settings-status = Status +support-remote-settings-status-ok = OK +# Status when synchronization is not working. +support-remote-settings-status-broken = Not working +support-remote-settings-last-check = Last check +support-remote-settings-local-timestamp = Local timestamp +support-remote-settings-sync-history = History +support-remote-settings-sync-history-status = Status +support-remote-settings-sync-history-datetime = Date +support-remote-settings-sync-history-infos = Infos + ## Normandy sections support-remote-experiments-title = Remote Experiments diff --git a/toolkit/modules/Troubleshoot.sys.mjs b/toolkit/modules/Troubleshoot.sys.mjs index abb18977af63..eda11eaf5a08 100644 --- a/toolkit/modules/Troubleshoot.sys.mjs +++ b/toolkit/modules/Troubleshoot.sys.mjs @@ -1079,6 +1079,25 @@ var dataProviders = { nimbusRollouts, }); }, + + async remoteSettings(done) { + const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" + ); + + const inspected = await RemoteSettings.inspect({ localOnly: true }); + + // Show last check in standard format. + inspected.lastCheck = inspected.lastCheck + ? new Date(inspected.lastCheck * 1000).toISOString() + : ""; + // Trim history entries. + for (let h of Object.values(inspected.history)) { + h.splice(10, Infinity); + } + + done(inspected); + }, }; if (AppConstants.MOZ_CRASHREPORTER) { diff --git a/toolkit/modules/tests/browser/browser_Troubleshoot.js b/toolkit/modules/tests/browser/browser_Troubleshoot.js index dbfbe69909a0..632d350b2d97 100644 --- a/toolkit/modules/tests/browser/browser_Troubleshoot.js +++ b/toolkit/modules/tests/browser/browser_Troubleshoot.js @@ -1275,6 +1275,41 @@ const SNAPSHOT_SCHEMA = { }, }, }, + remoteSettings: { + type: "object", + additionalProperties: true, + properties: { + isSynchronizationBroken: { + required: true, + type: "boolean", + }, + lastCheck: { + required: true, + type: "string", + }, + localTimestamp: { + required: false, + type: ["number", "null"], + }, + history: { + required: true, + type: "object", + properties: { + "settings-sync": { + type: "array", + items: { + type: "object", + properties: { + status: { type: "string", required: true }, + datetime: { type: "string", required: true }, + infos: { type: "object", required: true }, + }, + }, + }, + }, + }, + }, + }, legacyUserStylesheets: { type: "object", properties: { @@ -1325,17 +1360,27 @@ function validateObject(obj, schema) { if (obj === undefined && !schema.required) { return; } - if (typeof schema.type != "string") { - throw schemaErr("'type' must be a string", schema); + let types = Array.isArray(schema.type) ? schema.type : [schema.type]; + if (!types.every(elt => typeof elt == "string")) { + throw schemaErr("'type' must be a string or array of strings", schema); } - if (objType(obj) != schema.type) { + if (!types.includes(objType(obj))) { throw validationErr("Object is not of the expected type", obj, schema); } - let validatorFnName = "validateObject_" + schema.type; - if (!(validatorFnName in this)) { - throw schemaErr("Validator function not defined for type", schema); + let lastError; + for (let type of types) { + let validatorFnName = "validateObject_" + type; + if (!(validatorFnName in this)) { + throw schemaErr("Validator function not defined for type", schema); + } + try { + this[validatorFnName](obj, schema); + return; + } catch (e) { + lastError = e; + } } - this[validatorFnName](obj, schema); + throw lastError; } function validateObject_object(obj, schema) { @@ -1348,13 +1393,15 @@ function validateObject_object(obj, schema) { validateObject(obj[prop], schema.properties[prop]); } // Now check that the object doesn't have any properties not in the schema. - for (let prop in obj) { - if (!(prop in schema.properties)) { - throw validationErr( - "Object has property " + prop + " not in schema", - obj, - schema - ); + if (!schema.additionalProperties) { + for (let prop in obj) { + if (!(prop in schema.properties)) { + throw validationErr( + "Object has property " + prop + " not in schema", + obj, + schema + ); + } } } }