Bug 1768174 - Show Remote Settings health in about:support r=Gijs,fluent-reviewers,bolsson,acottner

Differential Revision: https://phabricator.services.mozilla.com/D210040
This commit is contained in:
Mathieu Leplatre 2024-06-04 16:55:26 +00:00
parent 498324f6a9
commit a9861f4887
8 changed files with 227 additions and 21 deletions

View file

@ -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"]

View file

@ -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();
});
});

View file

@ -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(),
};
};

View file

@ -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;

View file

@ -614,6 +614,52 @@
</table>
#endif
<!-- - - - - - - - - - - - - - - - - - - - - -->
<h2 class="major-section" id="remote-settings" data-l10n-id="support-remote-settings-title"/>
<table>
<tbody>
<tr>
<th class="column" data-l10n-id="support-remote-settings-status"/>
<td>
<span id="support-remote-settings-status-ok" data-l10n-id="support-remote-settings-status-ok"/>
<span id="support-remote-settings-status-broken" data-l10n-id="support-remote-settings-status-broken"/>
</td>
</tr>
<tr>
<th class="column" data-l10n-id="support-remote-settings-last-check"/>
<td id="support-remote-settings-last-check">
</td>
</tr>
<tr>
<th class="column" data-l10n-id="support-remote-settings-local-timestamp"/>
<td id="support-remote-settings-local-timestamp">
</td>
</tr>
<tr>
<th class="column" data-l10n-id="support-remote-settings-sync-history"/>
<td>
<table>
<thead>
<tr>
<th data-l10n-id="support-remote-settings-sync-history-status"/>
<th data-l10n-id="support-remote-settings-sync-history-datetime"/>
<th data-l10n-id="support-remote-settings-sync-history-infos"/>
</tr>
</thead>
<tbody id="support-remote-settings-sync-history-tbody">
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
#ifdef MOZ_NORMANDY
<!-- - - - - - - - - - - - - - - - - - - - - -->
<h2 class="major-section" id="remote-experiments" data-l10n-id="support-remote-experiments-title"/>

View file

@ -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

View file

@ -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) {

View file

@ -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
);
}
}
}
}