forked from mirrors/gecko-dev
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:
parent
498324f6a9
commit
a9861f4887
8 changed files with 227 additions and 21 deletions
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue