зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1254099 - Add Telemetry to settings update r=bsmedberg,glasserc,mgoodwin,rhelmer
MozReview-Commit-ID: 8vAuTImx7IH --HG-- extra : rebase_source : 15995e30bd1fdb697eb2374a0c28c68e0828e1e6
This commit is contained in:
Родитель
8e35a61454
Коммит
7d9c066f35
|
@ -27,6 +27,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "FirefoxAdapter",
|
|||
"resource://services-common/kinto-storage-adapter.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CanonicalJSON",
|
||||
"resource://gre/modules/CanonicalJSON.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "UptakeTelemetry",
|
||||
"resource://services-common/uptake-telemetry.js");
|
||||
|
||||
const KEY_APPDIR = "XCurProcD";
|
||||
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||
|
@ -203,6 +205,7 @@ class BlocklistClient {
|
|||
}
|
||||
|
||||
let sqliteHandle;
|
||||
let reportStatus = null;
|
||||
try {
|
||||
// Synchronize remote data into a local Sqlite DB.
|
||||
sqliteHandle = await FirefoxAdapter.openConnection({path: KINTO_STORAGE_PATH});
|
||||
|
@ -233,6 +236,7 @@ class BlocklistClient {
|
|||
// to record the fact that a check happened.
|
||||
if (lastModified <= collectionLastModified) {
|
||||
this.updateLastCheck(serverTime);
|
||||
reportStatus = UptakeTelemetry.STATUS.UP_TO_DATE;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -240,16 +244,25 @@ class BlocklistClient {
|
|||
try {
|
||||
const {ok} = await collection.sync({remote});
|
||||
if (!ok) {
|
||||
// Some synchronization conflicts occured.
|
||||
reportStatus = UptakeTelemetry.STATUS.CONFLICT_ERROR;
|
||||
throw new Error("Sync failed");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message == INVALID_SIGNATURE) {
|
||||
// Signature verification failed during synchronzation.
|
||||
reportStatus = UptakeTelemetry.STATUS.SIGNATURE_ERROR;
|
||||
// if sync fails with a signature error, it's likely that our
|
||||
// local data has been modified in some way.
|
||||
// We will attempt to fix this by retrieving the whole
|
||||
// remote collection.
|
||||
const payload = await fetchRemoteCollection(remote, collection);
|
||||
await this.validateCollectionSignature(remote, payload, collection, {ignoreLocal: true});
|
||||
try {
|
||||
await this.validateCollectionSignature(remote, payload, collection, {ignoreLocal: true});
|
||||
} catch (e) {
|
||||
reportStatus = UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR;
|
||||
throw e;
|
||||
}
|
||||
// if the signature is good (we haven't thrown), and the remote
|
||||
// last_modified is newer than the local last_modified, replace the
|
||||
// local data
|
||||
|
@ -259,18 +272,46 @@ class BlocklistClient {
|
|||
await collection.loadDump(payload.data);
|
||||
}
|
||||
} else {
|
||||
// The sync has thrown, it can be a network or a general error.
|
||||
if (/NetworkError/.test(e.message)) {
|
||||
reportStatus = UptakeTelemetry.STATUS.NETWORK_ERROR;
|
||||
} else if (/Backoff/.test(e.message)) {
|
||||
reportStatus = UptakeTelemetry.STATUS.BACKOFF;
|
||||
} else {
|
||||
reportStatus = UptakeTelemetry.STATUS.SYNC_ERROR;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
// Read local collection of records.
|
||||
const {data} = await collection.list();
|
||||
|
||||
await this.processCallback(data);
|
||||
// Handle the obtained records (ie. apply locally).
|
||||
try {
|
||||
await this.processCallback(data);
|
||||
} catch (e) {
|
||||
reportStatus = UptakeTelemetry.STATUS.APPLY_ERROR;
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Track last update.
|
||||
this.updateLastCheck(serverTime);
|
||||
} catch (e) {
|
||||
// No specific error was tracked, mark it as unknown.
|
||||
if (reportStatus === null) {
|
||||
reportStatus = UptakeTelemetry.STATUS.UNKNOWN_ERROR;
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
await sqliteHandle.close();
|
||||
if (sqliteHandle) {
|
||||
await sqliteHandle.close();
|
||||
}
|
||||
// No error was reported, this is a success!
|
||||
if (reportStatus === null) {
|
||||
reportStatus = UptakeTelemetry.STATUS.SUCCESS;
|
||||
}
|
||||
// Report success/error status to Telemetry.
|
||||
UptakeTelemetry.report(this.identifier, reportStatus);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.importGlobalProperties(["fetch"]);
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "UptakeTelemetry",
|
||||
"resource://services-common/uptake-telemetry.js");
|
||||
|
||||
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||
const PREF_SETTINGS_SERVER_BACKOFF = "services.settings.server.backoff";
|
||||
|
@ -17,6 +19,9 @@ const PREF_BLOCKLIST_LAST_UPDATE = "services.blocklist.last_update_second
|
|||
const PREF_BLOCKLIST_LAST_ETAG = "services.blocklist.last_etag";
|
||||
const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
|
||||
|
||||
// Telemetry update source identifier.
|
||||
const TELEMETRY_HISTOGRAM_KEY = "settings-changes-monitoring";
|
||||
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gBlocklistClients", function() {
|
||||
const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js", {});
|
||||
|
@ -33,6 +38,56 @@ XPCOMUtils.defineLazyGetter(this, "gBlocklistClients", function() {
|
|||
this.addTestBlocklistClient = (name, client) => { gBlocklistClients[name] = client; }
|
||||
|
||||
|
||||
async function pollChanges(url, lastEtag) {
|
||||
//
|
||||
// Fetch a versionInfo object from the server that looks like:
|
||||
// {"data":[
|
||||
// {
|
||||
// "host":"kinto-ota.dev.mozaws.net",
|
||||
// "last_modified":1450717104423,
|
||||
// "bucket":"blocklists",
|
||||
// "collection":"certificates"
|
||||
// }]}
|
||||
|
||||
// Use ETag to obtain a `304 Not modified` when no change occurred.
|
||||
const headers = {};
|
||||
if (lastEtag) {
|
||||
headers["If-None-Match"] = lastEtag;
|
||||
}
|
||||
const response = await fetch(url, {headers});
|
||||
|
||||
let versionInfo = [];
|
||||
// If no changes since last time, go on with empty list of changes.
|
||||
if (response.status != 304) {
|
||||
let payload;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (e) {}
|
||||
if (!payload.hasOwnProperty("data")) {
|
||||
// If the server is failing, the JSON response might not contain the
|
||||
// expected data (e.g. error response - Bug 1259145)
|
||||
throw new Error(`Server error response ${JSON.stringify(payload)}`);
|
||||
}
|
||||
versionInfo = payload.data;
|
||||
}
|
||||
// The server should always return ETag. But we've had situations where the CDN
|
||||
// was interfering.
|
||||
const currentEtag = response.headers.has("ETag") ? response.headers.get("ETag") : undefined;
|
||||
const serverTimeMillis = Date.parse(response.headers.get("Date"));
|
||||
|
||||
// Check if the server asked the clients to back off.
|
||||
let backoffSeconds;
|
||||
if (response.headers.has("Backoff")) {
|
||||
const value = parseInt(response.headers.get("Backoff"), 10);
|
||||
if (!isNaN(value)) {
|
||||
backoffSeconds = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {versionInfo, currentEtag, serverTimeMillis, backoffSeconds};
|
||||
}
|
||||
|
||||
|
||||
// This is called by the ping mechanism.
|
||||
// returns a promise that rejects if something goes wrong
|
||||
this.checkVersions = async function() {
|
||||
|
@ -41,69 +96,66 @@ this.checkVersions = async function() {
|
|||
const backoffReleaseTime = Services.prefs.getCharPref(PREF_SETTINGS_SERVER_BACKOFF);
|
||||
const remainingMilliseconds = parseInt(backoffReleaseTime, 10) - Date.now();
|
||||
if (remainingMilliseconds > 0) {
|
||||
// Backoff time has not elapsed yet.
|
||||
UptakeTelemetry.report(TELEMETRY_HISTOGRAM_KEY,
|
||||
UptakeTelemetry.STATUS.BACKOFF);
|
||||
throw new Error(`Server is asking clients to back off; retry in ${Math.ceil(remainingMilliseconds / 1000)}s.`);
|
||||
} else {
|
||||
Services.prefs.clearUserPref(PREF_SETTINGS_SERVER_BACKOFF);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch a versionInfo object that looks like:
|
||||
// {"data":[
|
||||
// {
|
||||
// "host":"kinto-ota.dev.mozaws.net",
|
||||
// "last_modified":1450717104423,
|
||||
// "bucket":"blocklists",
|
||||
// "collection":"certificates"
|
||||
// }]}
|
||||
// Right now, we only use the collection name and the last modified info
|
||||
const kintoBase = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
|
||||
const changesEndpoint = kintoBase + Services.prefs.getCharPref(PREF_BLOCKLIST_CHANGES_PATH);
|
||||
|
||||
// Use ETag to obtain a `304 Not modified` when no change occurred.
|
||||
const headers = {};
|
||||
let lastEtag;
|
||||
if (Services.prefs.prefHasUserValue(PREF_BLOCKLIST_LAST_ETAG)) {
|
||||
const lastEtag = Services.prefs.getCharPref(PREF_BLOCKLIST_LAST_ETAG);
|
||||
if (lastEtag) {
|
||||
headers["If-None-Match"] = lastEtag;
|
||||
lastEtag = Services.prefs.getCharPref(PREF_BLOCKLIST_LAST_ETAG);
|
||||
}
|
||||
|
||||
let pollResult;
|
||||
try {
|
||||
pollResult = await pollChanges(changesEndpoint, lastEtag);
|
||||
} catch (e) {
|
||||
// Report polling error to Uptake Telemetry.
|
||||
let report;
|
||||
if (/Server/.test(e.message)) {
|
||||
report = UptakeTelemetry.STATUS.SERVER_ERROR;
|
||||
} else if (/NetworkError/.test(e.message)) {
|
||||
report = UptakeTelemetry.STATUS.NETWORK_ERROR;
|
||||
} else {
|
||||
report = UptakeTelemetry.STATUS.UNKNOWN_ERROR;
|
||||
}
|
||||
UptakeTelemetry.report(TELEMETRY_HISTOGRAM_KEY, report);
|
||||
// No need to go further.
|
||||
throw new Error(`Polling for changes failed: ${e.message}.`);
|
||||
}
|
||||
|
||||
const response = await fetch(changesEndpoint, {headers});
|
||||
const {serverTimeMillis, versionInfo, currentEtag, backoffSeconds} = pollResult;
|
||||
|
||||
// Check if the server asked the clients to back off.
|
||||
if (response.headers.has("Backoff")) {
|
||||
const backoffSeconds = parseInt(response.headers.get("Backoff"), 10);
|
||||
if (!isNaN(backoffSeconds)) {
|
||||
const backoffReleaseTime = Date.now() + backoffSeconds * 1000;
|
||||
Services.prefs.setCharPref(PREF_SETTINGS_SERVER_BACKOFF, backoffReleaseTime);
|
||||
}
|
||||
// Report polling success to Uptake Telemetry.
|
||||
const report = versionInfo.length == 0 ? UptakeTelemetry.STATUS.UP_TO_DATE
|
||||
: UptakeTelemetry.STATUS.SUCCESS;
|
||||
UptakeTelemetry.report(TELEMETRY_HISTOGRAM_KEY, report);
|
||||
|
||||
// Check if the server asked the clients to back off (for next poll).
|
||||
if (backoffSeconds) {
|
||||
const backoffReleaseTime = Date.now() + backoffSeconds * 1000;
|
||||
Services.prefs.setCharPref(PREF_SETTINGS_SERVER_BACKOFF, backoffReleaseTime);
|
||||
}
|
||||
|
||||
let versionInfo;
|
||||
// No changes since last time. Go on with empty list of changes.
|
||||
if (response.status == 304) {
|
||||
versionInfo = {data: []};
|
||||
} else {
|
||||
versionInfo = await response.json();
|
||||
}
|
||||
|
||||
// If the server is failing, the JSON response might not contain the
|
||||
// expected data (e.g. error response - Bug 1259145)
|
||||
if (!versionInfo.hasOwnProperty("data")) {
|
||||
throw new Error("Polling for changes failed.");
|
||||
}
|
||||
|
||||
// Record new update time and the difference between local and server time
|
||||
const serverTimeMillis = Date.parse(response.headers.get("Date"));
|
||||
|
||||
// negative clockDifference means local time is behind server time
|
||||
// Record new update time and the difference between local and server time.
|
||||
// Negative clockDifference means local time is behind server time
|
||||
// by the absolute of that value in seconds (positive means it's ahead)
|
||||
const clockDifference = Math.floor((Date.now() - serverTimeMillis) / 1000);
|
||||
Services.prefs.setIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, clockDifference);
|
||||
Services.prefs.setIntPref(PREF_BLOCKLIST_LAST_UPDATE, serverTimeMillis / 1000);
|
||||
|
||||
// Iterate through the collections version info and initiate a synchronization
|
||||
// on the related blocklist client.
|
||||
let firstError;
|
||||
for (let collectionInfo of versionInfo.data) {
|
||||
for (const collectionInfo of versionInfo) {
|
||||
const {bucket, collection, last_modified: lastModified} = collectionInfo;
|
||||
const client = gBlocklistClients[collection];
|
||||
if (client && client.bucketName == bucket) {
|
||||
|
@ -122,8 +174,7 @@ this.checkVersions = async function() {
|
|||
}
|
||||
|
||||
// Save current Etag for next poll.
|
||||
if (response.headers.has("ETag")) {
|
||||
const currentEtag = response.headers.get("ETag");
|
||||
if (currentEtag) {
|
||||
Services.prefs.setCharPref(PREF_BLOCKLIST_LAST_ETAG, currentEtag);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ EXTRA_JS_MODULES['services-common'] += [
|
|||
'logmanager.js',
|
||||
'observers.js',
|
||||
'rest.js',
|
||||
'uptake-telemetry.js',
|
||||
'utils.js',
|
||||
]
|
||||
|
||||
|
|
|
@ -159,3 +159,22 @@ function uninstallFakePAC() {
|
|||
_("Uninstalling fake PAC.");
|
||||
MockRegistrar.unregister(fakePACCID);
|
||||
}
|
||||
|
||||
|
||||
function getUptakeTelemetrySnapshot(key) {
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
const TELEMETRY_HISTOGRAM_ID = "UPTAKE_REMOTE_CONTENT_RESULT_1";
|
||||
return Services.telemetry
|
||||
.getKeyedHistogramById(TELEMETRY_HISTOGRAM_ID)
|
||||
.snapshot(key);
|
||||
}
|
||||
|
||||
function checkUptakeTelemetry(snapshot1, snapshot2, expectedIncrements) {
|
||||
const LABELS = ["up_to_date", "success", "backoff", "pref_disabled", "parse_error", "content_error", "sign_error", "sign_retry_error", "conflict_error", "sync_error", "apply_error", "server_error", "certificate_error", "download_error", "timeout_error", "network_error", "offline_error", "cleanup_error", "unknown_error", "custom_1_error", "custom_2_error", "custom_3_error", "custom_4_error", "custom_5_error"];
|
||||
for (const label of LABELS) {
|
||||
const key = LABELS.indexOf(label);
|
||||
const expected = expectedIncrements[label] || 0;
|
||||
const actual = snapshot2.counts[key] - snapshot1.counts[key];
|
||||
equal(expected, actual, `check histogram count for ${label}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
|
|||
const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js", {});
|
||||
const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js", {});
|
||||
const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js", {});
|
||||
const { UptakeTelemetry } = Cu.import("resource://services-common/uptake-telemetry.js", {});
|
||||
|
||||
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
|
||||
"nsIBinaryInputStream", "setInputStream");
|
||||
|
@ -190,13 +191,14 @@ add_task(function* test_sends_reload_message_when_blocklist_has_changes() {
|
|||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(function* test_do_nothing_when_blocklist_is_up_to_date() {
|
||||
add_task(function* test_telemetry_reports_up_to_date() {
|
||||
for (let {client} of gBlocklistClients) {
|
||||
yield client.maybeSync(2000, Date.now() - 1000, {loadDump: false});
|
||||
const filePath = OS.Path.join(OS.Constants.Path.profileDir, client.filename);
|
||||
const profFile = new FileUtils.File(filePath);
|
||||
const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000;
|
||||
const serverTime = Date.now();
|
||||
const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
|
||||
|
||||
yield client.maybeSync(3000, serverTime);
|
||||
|
||||
|
@ -205,11 +207,86 @@ add_task(function* test_do_nothing_when_blocklist_is_up_to_date() {
|
|||
// Server time was updated.
|
||||
const after = Services.prefs.getIntPref(client.lastCheckTimePref);
|
||||
equal(after, Math.round(serverTime / 1000));
|
||||
// No Telemetry was sent.
|
||||
const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
|
||||
const expectedIncrements = {[UptakeTelemetry.STATUS.UP_TO_DATE]: 1};
|
||||
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
|
||||
}
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(function* test_telemetry_if_sync_succeeds() {
|
||||
// We test each client because Telemetry requires preleminary declarations.
|
||||
for (let {client} of gBlocklistClients) {
|
||||
const serverTime = Date.now();
|
||||
const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
|
||||
|
||||
yield client.maybeSync(2000, serverTime, {loadDump: false});
|
||||
|
||||
const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
|
||||
const expectedIncrements = {[UptakeTelemetry.STATUS.SUCCESS]: 1};
|
||||
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
|
||||
}
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(function* test_telemetry_reports_if_application_fails() {
|
||||
const {client} = gBlocklistClients[0];
|
||||
const serverTime = Date.now();
|
||||
const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
|
||||
const backup = client.processCallback;
|
||||
client.processCallback = () => { throw new Error("boom"); };
|
||||
|
||||
try {
|
||||
yield client.maybeSync(2000, serverTime, {loadDump: false});
|
||||
} catch (e) {}
|
||||
|
||||
client.processCallback = backup;
|
||||
|
||||
const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
|
||||
const expectedIncrements = {[UptakeTelemetry.STATUS.APPLY_ERROR]: 1};
|
||||
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(function* test_telemetry_reports_if_sync_fails() {
|
||||
const {client} = gBlocklistClients[0];
|
||||
const serverTime = Date.now();
|
||||
|
||||
const sqliteHandle = yield FirefoxAdapter.openConnection({path: kintoFilename});
|
||||
const collection = kintoCollection(client.collectionName, sqliteHandle);
|
||||
yield collection.db.saveLastModified(9999);
|
||||
yield sqliteHandle.close();
|
||||
|
||||
const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
|
||||
|
||||
try {
|
||||
yield client.maybeSync(10000, serverTime);
|
||||
} catch (e) {}
|
||||
|
||||
const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
|
||||
const expectedIncrements = {[UptakeTelemetry.STATUS.SYNC_ERROR]: 1};
|
||||
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(function* test_telemetry_reports_unknown_errors() {
|
||||
const {client} = gBlocklistClients[0];
|
||||
const serverTime = Date.now();
|
||||
const backup = FirefoxAdapter.openConnection;
|
||||
FirefoxAdapter.openConnection = () => { throw new Error("Internal"); };
|
||||
const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
|
||||
|
||||
try {
|
||||
yield client.maybeSync(2000, serverTime);
|
||||
} catch (e) {}
|
||||
|
||||
FirefoxAdapter.openConnection = backup;
|
||||
const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
|
||||
const expectedIncrements = {[UptakeTelemetry.STATUS.UNKNOWN_ERROR]: 1};
|
||||
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
// get a response for a given request from sample data
|
||||
function getSampleResponse(req, port) {
|
||||
|
@ -406,6 +483,20 @@ function getSampleResponse(req, port) {
|
|||
"os": "Darwin 11",
|
||||
"featureStatus": "BLOCKED_DEVICE"
|
||||
}]})
|
||||
},
|
||||
"GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified&_since=9999": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
],
|
||||
"status": {status: 503, statusText: "Service Unavailable"},
|
||||
"responseBody": JSON.stringify({
|
||||
code: 503,
|
||||
errno: 999,
|
||||
error: "Service Unavailable",
|
||||
})
|
||||
}
|
||||
};
|
||||
return responses[`${req.method}:${req.path}?${req.queryString}`] ||
|
||||
|
|
|
@ -7,6 +7,7 @@ const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js"
|
|||
const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js", {});
|
||||
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
|
||||
const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js", {});
|
||||
const { UptakeTelemetry } = Cu.import("resource://services-common/uptake-telemetry.js", {});
|
||||
|
||||
let server;
|
||||
|
||||
|
@ -16,6 +17,9 @@ const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection";
|
|||
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||
const PREF_SIGNATURE_ROOT = "security.content.signature.root_hash";
|
||||
|
||||
// Telemetry reports.
|
||||
const TELEMETRY_HISTOGRAM_KEY = OneCRLBlocklistClient.identifier;
|
||||
|
||||
const kintoFilename = "kinto.sqlite";
|
||||
|
||||
const CERT_DIR = "test_blocklist_signatures/";
|
||||
|
@ -301,11 +305,19 @@ add_task(function* test_check_signatures() {
|
|||
// .. and use this map to register handlers for each path
|
||||
registerHandlers(emptyCollectionResponses);
|
||||
|
||||
let startHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
|
||||
|
||||
// With all of this set up, we attempt a sync. This will resolve if all is
|
||||
// well and throw if something goes wrong.
|
||||
// We don't want to load initial json dumps in this test suite.
|
||||
yield OneCRLBlocklistClient.maybeSync(1000, startTime, {loadDump: false});
|
||||
|
||||
let endHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
|
||||
|
||||
// ensure that a success histogram is tracked when a succesful sync occurs.
|
||||
let expectedIncrements = {[UptakeTelemetry.STATUS.SUCCESS]: 1};
|
||||
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
|
||||
|
||||
// Check that some additions (2 records) to the collection have a valid
|
||||
// signature.
|
||||
|
||||
|
@ -442,8 +454,19 @@ add_task(function* test_check_signatures() {
|
|||
};
|
||||
|
||||
registerHandlers(badSigGoodSigResponses);
|
||||
|
||||
startHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
|
||||
|
||||
yield OneCRLBlocklistClient.maybeSync(5000, startTime);
|
||||
|
||||
endHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
|
||||
|
||||
// ensure that the failure count is incremented for a succesful sync with an
|
||||
// (initial) bad signature - only SERVICES_SETTINGS_SYNC_SIG_FAIL should
|
||||
// increment.
|
||||
expectedIncrements = {[UptakeTelemetry.STATUS.SIGNATURE_ERROR]: 1};
|
||||
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
|
||||
|
||||
const badSigGoodOldResponses = {
|
||||
// In this test, we deliberately serve a bad signature initially. The
|
||||
// subsequent sitnature returned is a valid one for the three item
|
||||
|
@ -483,6 +506,7 @@ add_task(function* test_check_signatures() {
|
|||
[RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID]
|
||||
};
|
||||
|
||||
startHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
|
||||
registerHandlers(allBadSigResponses);
|
||||
try {
|
||||
yield OneCRLBlocklistClient.maybeSync(6000, startTime);
|
||||
|
@ -490,6 +514,11 @@ add_task(function* test_check_signatures() {
|
|||
} catch (e) {
|
||||
yield checkRecordCount(2);
|
||||
}
|
||||
|
||||
// Ensure that the failure is reflected in the accumulated telemetry:
|
||||
endHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
|
||||
expectedIncrements = {[UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR]: 1};
|
||||
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
Cu.import("resource://testing-common/httpd.js");
|
||||
const { UptakeTelemetry } = Cu.import("resource://services-common/uptake-telemetry.js", {});
|
||||
|
||||
var server;
|
||||
|
||||
|
@ -8,6 +9,9 @@ const PREF_LAST_UPDATE = "services.blocklist.last_update_seconds";
|
|||
const PREF_LAST_ETAG = "services.blocklist.last_etag";
|
||||
const PREF_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
|
||||
|
||||
// Telemetry report result.
|
||||
const TELEMETRY_HISTOGRAM_KEY = "settings-changes-monitoring";
|
||||
|
||||
// Check to ensure maybeSync is called with correct values when a changes
|
||||
// document contains information on when a collection was last modified
|
||||
add_task(function* test_check_maybeSync() {
|
||||
|
@ -64,6 +68,9 @@ add_task(function* test_check_maybeSync() {
|
|||
do_check_eq(serverTime, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
const startHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
|
||||
|
||||
yield updater.checkVersions();
|
||||
|
||||
// check the last_update is updated
|
||||
|
@ -102,6 +109,7 @@ add_task(function* test_check_maybeSync() {
|
|||
response.setStatusLine(null, 503, "Service Unavailable");
|
||||
}
|
||||
server.registerPathHandler(changesPath, simulateErrorResponse);
|
||||
|
||||
// checkVersions() fails with adequate error.
|
||||
let error;
|
||||
try {
|
||||
|
@ -109,7 +117,7 @@ add_task(function* test_check_maybeSync() {
|
|||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
do_check_eq(error.message, "Polling for changes failed.");
|
||||
do_check_true(/Polling for changes failed/.test(error.message));
|
||||
// When an error occurs, last update was not overwritten (see Date header above).
|
||||
do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
|
||||
|
||||
|
@ -150,6 +158,25 @@ add_task(function* test_check_maybeSync() {
|
|||
yield updater.checkVersions();
|
||||
// Backoff tracking preference was cleared.
|
||||
do_check_false(Services.prefs.prefHasUserValue(PREF_SETTINGS_SERVER_BACKOFF));
|
||||
|
||||
|
||||
// Simulate a network error (to check telemetry report).
|
||||
Services.prefs.setCharPref(PREF_SETTINGS_SERVER, "http://localhost:42/v1");
|
||||
try {
|
||||
yield updater.checkVersions();
|
||||
} catch (e) {}
|
||||
|
||||
const endHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
|
||||
// ensure that we've accumulated the correct telemetry
|
||||
const expectedIncrements = {
|
||||
[UptakeTelemetry.STATUS.UP_TO_DATE]: 4,
|
||||
[UptakeTelemetry.STATUS.SUCCESS]: 1,
|
||||
[UptakeTelemetry.STATUS.BACKOFF]: 1,
|
||||
[UptakeTelemetry.STATUS.SERVER_ERROR]: 1,
|
||||
[UptakeTelemetry.STATUS.NETWORK_ERROR]: 1,
|
||||
[UptakeTelemetry.STATUS.UNKNOWN_ERROR]: 0,
|
||||
};
|
||||
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
const { UptakeTelemetry } = Cu.import("resource://services-common/uptake-telemetry.js", {});
|
||||
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_unknown_status_is_not_reported() {
|
||||
const source = "update-source";
|
||||
const startHistogram = getUptakeTelemetrySnapshot(source);
|
||||
|
||||
UptakeTelemetry.report(source, "unknown-status");
|
||||
|
||||
const endHistogram = getUptakeTelemetrySnapshot(source);
|
||||
const expectedIncrements = {};
|
||||
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
|
||||
});
|
||||
|
||||
add_task(function* test_each_status_can_be_caught_in_snapshot() {
|
||||
const source = "some-source";
|
||||
const startHistogram = getUptakeTelemetrySnapshot(source);
|
||||
|
||||
const expectedIncrements = {};
|
||||
for (const label of Object.keys(UptakeTelemetry.STATUS)) {
|
||||
const status = UptakeTelemetry.STATUS[label];
|
||||
UptakeTelemetry.report(source, status);
|
||||
expectedIncrements[status] = 1;
|
||||
}
|
||||
|
||||
const endHistogram = getUptakeTelemetrySnapshot(source);
|
||||
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
|
||||
});
|
|
@ -60,3 +60,5 @@ skip-if = os == "android"
|
|||
|
||||
[test_storage_server.js]
|
||||
skip-if = os == "android"
|
||||
|
||||
[test_uptake_telemetry.js]
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["UptakeTelemetry"];
|
||||
|
||||
const { utils: Cu } = Components;
|
||||
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
|
||||
|
||||
|
||||
// Telemetry report results.
|
||||
const TELEMETRY_HISTOGRAM_ID = "UPTAKE_REMOTE_CONTENT_RESULT_1";
|
||||
|
||||
/**
|
||||
* A Telemetry helper to report uptake of remote content.
|
||||
*/
|
||||
class UptakeTelemetry {
|
||||
|
||||
/**
|
||||
* Supported uptake statuses:
|
||||
*
|
||||
* - `UP_TO_DATE`: Local content was already up-to-date with remote content.
|
||||
* - `SUCCESS`: Local content was updated successfully.
|
||||
* - `BACKOFF`: Remote server asked clients to backoff.
|
||||
* - `PARSE_ERROR`: Parsing server response has failed.
|
||||
* - `CONTENT_ERROR`: Server response has unexpected content.
|
||||
* - `PREF_DISABLED`: Update is disabled in user preferences.
|
||||
* - `SIGNATURE_ERROR`: Signature verification after diff-based sync has failed.
|
||||
* - `SIGNATURE_RETRY_ERROR`: Signature verification after full fetch has failed.
|
||||
* - `CONFLICT_ERROR`: Some remote changes are in conflict with local changes.
|
||||
* - `SYNC_ERROR`: Synchronization of remote changes has failed.
|
||||
* - `APPLY_ERROR`: Application of changes locally has failed.
|
||||
* - `SERVER_ERROR`: Server failed to respond.
|
||||
* - `CERTIFICATE_ERROR`: Server certificate verification has failed.
|
||||
* - `DOWNLOAD_ERROR`: Data could not be fully retrieved.
|
||||
* - `TIMEOUT_ERROR`: Server response has timed out.
|
||||
* - `NETWORK_ERROR`: Communication with server has failed.
|
||||
* - `NETWORK_OFFLINE_ERROR`: Network not available.
|
||||
* - `UNKNOWN_ERROR`: Uncategorized error.
|
||||
* - `CLEANUP_ERROR`: Clean-up of temporary files has failed.
|
||||
* - `CUSTOM_1_ERROR`: Update source specific error #1.
|
||||
* - `CUSTOM_2_ERROR`: Update source specific error #2.
|
||||
* - `CUSTOM_3_ERROR`: Update source specific error #3.
|
||||
* - `CUSTOM_4_ERROR`: Update source specific error #4.
|
||||
* - `CUSTOM_5_ERROR`: Update source specific error #5.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
static get STATUS() {
|
||||
return {
|
||||
UP_TO_DATE: "up_to_date",
|
||||
SUCCESS: "success",
|
||||
BACKOFF: "backoff",
|
||||
PREF_DISABLED: "pref_disabled",
|
||||
PARSE_ERROR: "parse_error",
|
||||
CONTENT_ERROR: "content_error",
|
||||
SIGNATURE_ERROR: "sign_error",
|
||||
SIGNATURE_RETRY_ERROR: "sign_retry_error",
|
||||
CONFLICT_ERROR: "conflict_error",
|
||||
SYNC_ERROR: "sync_error",
|
||||
APPLY_ERROR: "apply_error",
|
||||
SERVER_ERROR: "server_error",
|
||||
CERTIFICATE_ERROR: "certificate_error",
|
||||
DOWNLOAD_ERROR: "download_error",
|
||||
TIMEOUT_ERROR: "timeout_error",
|
||||
NETWORK_ERROR: "network_error",
|
||||
NETWORK_OFFLINE_ERROR: "offline_error",
|
||||
CLEANUP_ERROR: "cleanup_error",
|
||||
UNKNOWN_ERROR: "unknown_error",
|
||||
CUSTOM_1_ERROR: "custom_1_error",
|
||||
CUSTOM_2_ERROR: "custom_2_error",
|
||||
CUSTOM_3_ERROR: "custom_3_error",
|
||||
CUSTOM_4_ERROR: "custom_4_error",
|
||||
CUSTOM_5_ERROR: "custom_5_error",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the uptake status for the specified source.
|
||||
*
|
||||
* @param {string} source the identifier of the update source.
|
||||
* @param {string} status the uptake status.
|
||||
*/
|
||||
static report(source, status) {
|
||||
Services.telemetry
|
||||
.getKeyedHistogramById(TELEMETRY_HISTOGRAM_ID)
|
||||
.add(source, status);
|
||||
}
|
||||
}
|
||||
|
||||
this.UptakeTelemetry = UptakeTelemetry;
|
|
@ -5803,6 +5803,17 @@
|
|||
"releaseChannelCollection": "opt-out",
|
||||
"description": "Update: the update wizard page displayed when the UI was closed (mapped in toolkit/mozapps/update/UpdateTelemetry.jsm)"
|
||||
},
|
||||
"UPTAKE_REMOTE_CONTENT_RESULT_1": {
|
||||
"record_in_processes": ["all"],
|
||||
"expires_in_version": "never",
|
||||
"kind": "categorical",
|
||||
"keyed": true,
|
||||
"labels": ["up_to_date", "success", "backoff", "pref_disabled", "parse_error", "content_error", "sign_error", "sign_retry_error", "conflict_error", "sync_error", "apply_error", "server_error", "certificate_error", "download_error", "timeout_error", "network_error", "offline_error", "cleanup_error", "unknown_error", "custom_1_error", "custom_2_error", "custom_3_error", "custom_4_error", "custom_5_error"],
|
||||
"releaseChannelCollection": "opt-out",
|
||||
"alert_emails": ["storage-team@mozilla.com"],
|
||||
"bug_numbers": [1254099],
|
||||
"description": "Generic histogram to track uptake of remote content like blocklists, settings or updates."
|
||||
},
|
||||
"UPDATE_NOTIFICATION_SHOWN": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
|
||||
|
|
|
@ -85,6 +85,8 @@ Flag histograms will ignore any changes after the flag is set, so once the flag
|
|||
|
||||
This histogram type is used when you want to record a count of something. It only stores a single value and defaults to `0`.
|
||||
|
||||
.. _histogram-type-keyed:
|
||||
|
||||
Keyed Histograms
|
||||
----------------
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ The current data collection possibilities include:
|
|||
* :doc:`stack capture <stack-capture>` allow recording application call stacks
|
||||
* :doc:`Use counters <use-counters>` measure the usage of web platform features
|
||||
* :doc:`Experiment annotations <experiments>`
|
||||
* :doc:`Remote content uptake <uptake>`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
@ -36,6 +37,7 @@ The current data collection possibilities include:
|
|||
custom-pings
|
||||
stack-capture
|
||||
experiments
|
||||
uptake
|
||||
*
|
||||
|
||||
Browser Usage Telemetry
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
================
|
||||
Uptake Telemetry
|
||||
================
|
||||
|
||||
Firefox continuously pulls data from different remote sources (eg. settings, system add-ons, …). In order to have consistent insights about the *uptake rate* of these *update sources*, our clients can use a unified Telemetry helper to report their *update status*.
|
||||
|
||||
The helper — described below — reports predefined update status, which eventually gives a unified way to obtain:
|
||||
|
||||
* the proportion of success among clients;
|
||||
* its evolution over time;
|
||||
* the distribution of error causes.
|
||||
|
||||
.. notes::
|
||||
|
||||
Examples of update sources: *remote settings, addons update, addons, gfx, and plugins blocklists, certificate revocation, certificate pinning, system addons delivery…*
|
||||
|
||||
Examples of update status: *up-to-date, success, network error, server error, signature error, server backoff, unknown error…*
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
const { UptakeTelemetry } = Cu.import("resource://services-common/uptake-telemetry.js", {});
|
||||
|
||||
UptakeTelemetry.report(source, status);
|
||||
|
||||
- ``source`` - a ``string`` that is an identifier for the update source (eg. ``addons-blocklist``)
|
||||
- ``status`` - one of the following status constants:
|
||||
- ``UptakeTelemetry.STATUS.UP_TO_DATE``: Local content was already up-to-date with remote content.
|
||||
- ``UptakeTelemetry.STATUS.SUCCESS``: Local content was updated successfully.
|
||||
- ``UptakeTelemetry.STATUS.BACKOFF``: Remote server asked clients to backoff.
|
||||
- ``UptakeTelemetry.STATUS.PREF_DISABLED``: Update is disabled in user preferences.
|
||||
- ``UptakeTelemetry.STATUS.PARSE_ERROR``: Parsing server response has failed.
|
||||
- ``UptakeTelemetry.STATUS.CONTENT_ERROR``: Server response has unexpected content.
|
||||
- ``UptakeTelemetry.STATUS.SIGNATURE_ERROR``: Signature verification after diff-based sync has failed.
|
||||
- ``UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR``: Signature verification after full fetch has failed.
|
||||
- ``UptakeTelemetry.STATUS.CONFLICT_ERROR``: Some remote changes are in conflict with local changes.
|
||||
- ``UptakeTelemetry.STATUS.SYNC_ERROR``: Synchronization of remote changes has failed.
|
||||
- ``UptakeTelemetry.STATUS.APPLY_ERROR``: Application of changes locally has failed.
|
||||
- ``UptakeTelemetry.STATUS.SERVER_ERROR``: Server failed to respond.
|
||||
- ``UptakeTelemetry.STATUS.CERTIFICATE_ERROR``: Server certificate verification has failed.
|
||||
- ``UptakeTelemetry.STATUS.DOWNLOAD_ERROR``: Data could not be fully retrieved.
|
||||
- ``UptakeTelemetry.STATUS.TIMEOUT_ERROR``: Server response has timed out.
|
||||
- ``UptakeTelemetry.STATUS.NETWORK_ERROR``: Communication with server has failed.
|
||||
- ``UptakeTelemetry.STATUS.NETWORK_OFFLINE_ERROR``: Network not available.
|
||||
- ``UptakeTelemetry.STATUS.CLEANUP_ERROR``: Clean-up of temporary files has failed.
|
||||
- ``UptakeTelemetry.STATUS.UNKNOWN_ERROR``: Uncategorized error.
|
||||
- ``UptakeTelemetry.STATUS.CUSTOM_1_ERROR``: Error #1 specific to this update source.
|
||||
- ``UptakeTelemetry.STATUS.CUSTOM_2_ERROR``: Error #2 specific to this update source.
|
||||
- ``UptakeTelemetry.STATUS.CUSTOM_3_ERROR``: Error #3 specific to this update source.
|
||||
- ``UptakeTelemetry.STATUS.CUSTOM_4_ERROR``: Error #4 specific to this update source.
|
||||
- ``UptakeTelemetry.STATUS.CUSTOM_5_ERROR``: Error #5 specific to this update source.
|
||||
|
||||
|
||||
The data is submitted to a single :ref:`keyed histogram <histogram-type-keyed>` whose id is ``UPTAKE_REMOTE_CONTENT_RESULT_1`` and the specified update ``source`` as the key.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
const UPDATE_SOURCE = "update-monitoring";
|
||||
|
||||
let status;
|
||||
try {
|
||||
const data = await fetch(uri);
|
||||
status = UptakeTelemetry.STATUS.SUCCESS;
|
||||
} catch (e) {
|
||||
status = /NetworkError/.test(e) ?
|
||||
UptakeTelemetry.STATUS.NETWORK_ERROR :
|
||||
UptakeTelemetry.STATUS.SERVER_ERROR ;
|
||||
}
|
||||
UptakeTelemetry.report(UPDATE_SOURCE, status);
|
||||
|
||||
|
||||
Use-cases
|
||||
---------
|
||||
|
||||
The following remote data sources are already using this unified histogram.
|
||||
|
||||
* remote settings changes monitoring
|
||||
* add-ons blocklist
|
||||
* gfx blocklist
|
||||
* plugins blocklist
|
||||
* certificate revocation
|
||||
* certificate pinning
|
||||
|
||||
Obviously, the goal is to eventually converge and avoid ad-hoc Telemetry probes for measuring uptake of remote content. Some notable potential use-cases are:
|
||||
|
||||
* nsUpdateService
|
||||
* mozapps extensions update
|
||||
* Shield recipe client
|
Загрузка…
Ссылка в новой задаче