586 lines
18 KiB
JavaScript
586 lines
18 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/
|
|
|
|
/**
|
|
* This test case populates the profile with some fake stored
|
|
* pings, and checks that pending pings are immediatlely sent
|
|
* after delayed init.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const PING_SAVE_FOLDER = "saved-telemetry-pings";
|
|
const OLD_FORMAT_PINGS = 4;
|
|
const RECENT_PINGS = 4;
|
|
|
|
var gSeenPings = 0;
|
|
|
|
/**
|
|
* Creates some Telemetry pings for the and saves them to disk. Each ping gets a
|
|
* unique ID based on an incrementor.
|
|
*
|
|
* @param {Array} aPingInfos An array of ping type objects. Each entry must be an
|
|
* object containing a "num" field for the number of pings to create and
|
|
* an "age" field. The latter representing the age in milliseconds to offset
|
|
* from now. A value of 10 would make the ping 10ms older than now, for
|
|
* example.
|
|
* @returns Promise
|
|
* @resolve an Array with the created pings ids.
|
|
*/
|
|
var createSavedPings = async function (aPingInfos) {
|
|
let pingIds = [];
|
|
let now = Date.now();
|
|
|
|
for (let type in aPingInfos) {
|
|
let num = aPingInfos[type].num;
|
|
let age = now - (aPingInfos[type].age || 0);
|
|
for (let i = 0; i < num; ++i) {
|
|
let pingId = await TelemetryController.addPendingPing(
|
|
"test-ping",
|
|
{},
|
|
{ overwrite: true }
|
|
);
|
|
if (aPingInfos[type].age) {
|
|
// savePing writes to the file synchronously, so we're good to
|
|
// modify the lastModifedTime now.
|
|
let filePath = getSavePathForPingId(pingId);
|
|
await IOUtils.setModificationTime(filePath, age);
|
|
}
|
|
pingIds.push(pingId);
|
|
}
|
|
}
|
|
|
|
return pingIds;
|
|
};
|
|
|
|
/**
|
|
* Fakes the pending pings storage quota.
|
|
* @param {Integer} aPendingQuota The new quota, in bytes.
|
|
*/
|
|
function fakePendingPingsQuota(aPendingQuota) {
|
|
let { Policy } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/TelemetryStorage.sys.mjs"
|
|
);
|
|
Policy.getPendingPingsQuota = () => aPendingQuota;
|
|
}
|
|
|
|
/**
|
|
* Returns a path for the file that a ping should be
|
|
* stored in locally.
|
|
*
|
|
* @returns path
|
|
*/
|
|
function getSavePathForPingId(aPingId) {
|
|
return PathUtils.join(PathUtils.profileDir, PING_SAVE_FOLDER, aPingId);
|
|
}
|
|
|
|
/**
|
|
* Check if the number of Telemetry pings received by the HttpServer is not equal
|
|
* to aExpectedNum.
|
|
*
|
|
* @param aExpectedNum the number of pings we expect to receive.
|
|
*/
|
|
function assertReceivedPings(aExpectedNum) {
|
|
Assert.equal(gSeenPings, aExpectedNum);
|
|
}
|
|
|
|
/**
|
|
* Our handler function for the HttpServer that simply
|
|
* increments the gSeenPings global when it successfully
|
|
* receives and decodes a Telemetry payload.
|
|
*
|
|
* @param aRequest the HTTP request sent from HttpServer.
|
|
*/
|
|
function pingHandler() {
|
|
gSeenPings++;
|
|
}
|
|
|
|
add_task(async function test_setup() {
|
|
PingServer.start();
|
|
PingServer.registerPingHandler(pingHandler);
|
|
do_get_profile();
|
|
await loadAddonManager(
|
|
"xpcshell@tests.mozilla.org",
|
|
"XPCShell",
|
|
"1",
|
|
"1.9.2"
|
|
);
|
|
finishAddonManagerStartup();
|
|
fakeIntlReady();
|
|
// Make sure we don't generate unexpected pings due to pref changes.
|
|
await setEmptyPrefWatchlist();
|
|
|
|
Services.prefs.setCharPref(
|
|
TelemetryUtils.Preferences.Server,
|
|
"http://localhost:" + PingServer.port
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Setup the tests by making sure the ping storage directory is available, otherwise
|
|
* |TelemetryController.testSaveDirectoryToFile| could fail.
|
|
*/
|
|
add_task(async function setupEnvironment() {
|
|
// The following tests assume this pref to be true by default.
|
|
Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
|
|
|
|
await TelemetryController.testSetup();
|
|
|
|
let directory = TelemetryStorage.pingDirectoryPath;
|
|
await IOUtils.makeDirectory(directory, {
|
|
ignoreExisting: true,
|
|
permissions: 0o700,
|
|
});
|
|
|
|
await TelemetryStorage.testClearPendingPings();
|
|
});
|
|
|
|
/**
|
|
* Test that really recent pings are sent on Telemetry initialization.
|
|
*/
|
|
add_task(async function test_recent_pings_sent() {
|
|
let pingTypes = [{ num: RECENT_PINGS }];
|
|
await createSavedPings(pingTypes);
|
|
|
|
await TelemetryController.testReset();
|
|
await TelemetrySend.testWaitOnOutgoingPings();
|
|
assertReceivedPings(RECENT_PINGS);
|
|
|
|
await TelemetryStorage.testClearPendingPings();
|
|
});
|
|
|
|
/**
|
|
* Create an overdue ping in the old format and try to send it.
|
|
*/
|
|
add_task(async function test_old_formats() {
|
|
// A test ping in the old, standard format.
|
|
const PING_OLD_FORMAT = {
|
|
slug: "1234567abcd",
|
|
reason: "test-ping",
|
|
payload: {
|
|
info: {
|
|
reason: "test-ping",
|
|
OS: "XPCShell",
|
|
appID: "SomeId",
|
|
appVersion: "1.0",
|
|
appName: "XPCShell",
|
|
appBuildID: "123456789",
|
|
appUpdateChannel: "Test",
|
|
platformBuildID: "987654321",
|
|
},
|
|
},
|
|
};
|
|
|
|
// A ping with no info section, but with a slug.
|
|
const PING_NO_INFO = {
|
|
slug: "1234-no-info-ping",
|
|
reason: "test-ping",
|
|
payload: {},
|
|
};
|
|
|
|
// A ping with no payload.
|
|
const PING_NO_PAYLOAD = {
|
|
slug: "5678-no-payload",
|
|
reason: "test-ping",
|
|
};
|
|
|
|
// A ping with no info and no slug.
|
|
const PING_NO_SLUG = {
|
|
reason: "test-ping",
|
|
payload: {},
|
|
};
|
|
|
|
const PING_FILES_PATHS = [
|
|
getSavePathForPingId(PING_OLD_FORMAT.slug),
|
|
getSavePathForPingId(PING_NO_INFO.slug),
|
|
getSavePathForPingId(PING_NO_PAYLOAD.slug),
|
|
getSavePathForPingId("no-slug-file"),
|
|
];
|
|
|
|
// Write the ping to file
|
|
await TelemetryStorage.savePing(PING_OLD_FORMAT, true);
|
|
await TelemetryStorage.savePing(PING_NO_INFO, true);
|
|
await TelemetryStorage.savePing(PING_NO_PAYLOAD, true);
|
|
await TelemetryStorage.savePingToFile(
|
|
PING_NO_SLUG,
|
|
PING_FILES_PATHS[3],
|
|
true
|
|
);
|
|
|
|
gSeenPings = 0;
|
|
await TelemetryController.testReset();
|
|
await TelemetrySend.testWaitOnOutgoingPings();
|
|
assertReceivedPings(OLD_FORMAT_PINGS);
|
|
|
|
// |TelemetryStorage.cleanup| doesn't know how to remove a ping with no slug or id,
|
|
// so remove it manually so that the next test doesn't fail.
|
|
await IOUtils.remove(PING_FILES_PATHS[3]);
|
|
|
|
await TelemetryStorage.testClearPendingPings();
|
|
});
|
|
|
|
add_task(async function test_corrupted_pending_pings() {
|
|
const TEST_TYPE = "test_corrupted";
|
|
|
|
Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").clear();
|
|
Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").clear();
|
|
|
|
// Save a pending ping and get its id.
|
|
let pendingPingId = await TelemetryController.addPendingPing(
|
|
TEST_TYPE,
|
|
{},
|
|
{}
|
|
);
|
|
|
|
// Try to load it: there should be no error.
|
|
await TelemetryStorage.loadPendingPing(pendingPingId);
|
|
|
|
let h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_LOAD_FAILURE_READ"
|
|
).snapshot();
|
|
Assert.equal(
|
|
h.sum,
|
|
0,
|
|
"Telemetry must not report a pending ping load failure"
|
|
);
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_LOAD_FAILURE_PARSE"
|
|
).snapshot();
|
|
Assert.equal(
|
|
h.sum,
|
|
0,
|
|
"Telemetry must not report a pending ping parse failure"
|
|
);
|
|
|
|
// Delete it from the disk, so that its id will be kept in the cache but it will
|
|
// fail loading the file.
|
|
await IOUtils.remove(getSavePathForPingId(pendingPingId));
|
|
|
|
// Try to load a pending ping which isn't there anymore.
|
|
await Assert.rejects(
|
|
TelemetryStorage.loadPendingPing(pendingPingId),
|
|
/PingReadError/,
|
|
"Telemetry must fail loading a ping which isn't there"
|
|
);
|
|
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_LOAD_FAILURE_READ"
|
|
).snapshot();
|
|
Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure");
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_LOAD_FAILURE_PARSE"
|
|
).snapshot();
|
|
Assert.equal(
|
|
h.sum,
|
|
0,
|
|
"Telemetry must not report a pending ping parse failure"
|
|
);
|
|
|
|
// Save a new ping, so that it gets in the pending pings cache.
|
|
pendingPingId = await TelemetryController.addPendingPing(TEST_TYPE, {}, {});
|
|
// Overwrite it with a corrupted JSON file and then try to load it.
|
|
const INVALID_JSON = "{ invalid,JSON { {1}";
|
|
await IOUtils.writeUTF8(getSavePathForPingId(pendingPingId), INVALID_JSON);
|
|
|
|
// Try to load the ping with the corrupted JSON content.
|
|
await Assert.rejects(
|
|
TelemetryStorage.loadPendingPing(pendingPingId),
|
|
/PingParseError/,
|
|
"Telemetry must fail loading a corrupted ping"
|
|
);
|
|
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_LOAD_FAILURE_READ"
|
|
).snapshot();
|
|
Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure");
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_LOAD_FAILURE_PARSE"
|
|
).snapshot();
|
|
Assert.equal(h.sum, 1, "Telemetry must report a pending ping parse failure");
|
|
|
|
let exists = await IOUtils.exists(getSavePathForPingId(pendingPingId));
|
|
Assert.ok(!exists, "The unparseable ping should have been removed");
|
|
|
|
await TelemetryStorage.testClearPendingPings();
|
|
});
|
|
|
|
/**
|
|
* Create a ping in the old format, send it, and make sure the request URL contains
|
|
* the correct version query parameter.
|
|
*/
|
|
add_task(async function test_overdue_old_format() {
|
|
// A test ping in the old, standard format.
|
|
const PING_OLD_FORMAT = {
|
|
slug: "1234567abcd",
|
|
reason: "test-ping",
|
|
payload: {
|
|
info: {
|
|
reason: "test-ping",
|
|
OS: "XPCShell",
|
|
appID: "SomeId",
|
|
appVersion: "1.0",
|
|
appName: "XPCShell",
|
|
appBuildID: "123456789",
|
|
appUpdateChannel: "Test",
|
|
platformBuildID: "987654321",
|
|
},
|
|
},
|
|
};
|
|
|
|
// Write the ping to file
|
|
await TelemetryStorage.savePing(PING_OLD_FORMAT, true);
|
|
|
|
let receivedPings = 0;
|
|
// Register a new prefix handler to validate the URL.
|
|
PingServer.registerPingHandler(request => {
|
|
// Check that we have a version query parameter in the URL.
|
|
Assert.notEqual(request.queryString, "");
|
|
|
|
// Make sure the version in the query string matches the old ping format version.
|
|
let params = request.queryString.split("&");
|
|
Assert.ok(params.find(p => p == "v=1"));
|
|
|
|
receivedPings++;
|
|
});
|
|
|
|
await TelemetryController.testReset();
|
|
await TelemetrySend.testWaitOnOutgoingPings();
|
|
Assert.equal(receivedPings, 1, "We must receive a ping in the old format.");
|
|
|
|
await TelemetryStorage.testClearPendingPings();
|
|
PingServer.resetPingHandler();
|
|
});
|
|
|
|
add_task(async function test_pendingPingsQuota() {
|
|
const PING_TYPE = "foo";
|
|
|
|
// Disable upload so pings don't get sent and removed from the pending pings directory.
|
|
Services.prefs.setBoolPref(
|
|
TelemetryUtils.Preferences.FhrUploadEnabled,
|
|
false
|
|
);
|
|
|
|
// Remove all the pending pings then startup and wait for the cleanup task to complete.
|
|
// There should be nothing to remove.
|
|
await TelemetryStorage.testClearPendingPings();
|
|
await TelemetryController.testReset();
|
|
await TelemetrySend.testWaitOnOutgoingPings();
|
|
await TelemetryStorage.testPendingQuotaTaskPromise();
|
|
|
|
// Remove the pending optout ping generated when flipping FHR upload off.
|
|
await TelemetryStorage.testClearPendingPings();
|
|
|
|
let expectedPrunedPings = [];
|
|
let expectedNotPrunedPings = [];
|
|
|
|
let checkPendingPings = async function () {
|
|
// Check that the pruned pings are not on disk anymore.
|
|
for (let prunedPingId of expectedPrunedPings) {
|
|
await Assert.rejects(
|
|
TelemetryStorage.loadPendingPing(prunedPingId),
|
|
/TelemetryStorage.loadPendingPing - no ping with id/,
|
|
"Ping " + prunedPingId + " should have been pruned."
|
|
);
|
|
const pingPath = getSavePathForPingId(prunedPingId);
|
|
Assert.ok(
|
|
!(await IOUtils.exists(pingPath)),
|
|
"The ping should not be on the disk anymore."
|
|
);
|
|
}
|
|
|
|
// Check that the expected pings are there.
|
|
for (let expectedPingId of expectedNotPrunedPings) {
|
|
Assert.ok(
|
|
await TelemetryStorage.loadPendingPing(expectedPingId),
|
|
"Ping" + expectedPingId + " should be among the pending pings."
|
|
);
|
|
}
|
|
};
|
|
|
|
let pendingPingsInfo = [];
|
|
let pingsSizeInBytes = 0;
|
|
|
|
// Create 10 pings to test the pending pings quota.
|
|
for (let days = 1; days < 11; days++) {
|
|
const date = fakeNow(2010, 1, days, 1, 1, 0);
|
|
const pingId = await TelemetryController.addPendingPing(PING_TYPE, {}, {});
|
|
|
|
// Find the size of the ping.
|
|
const pingFilePath = getSavePathForPingId(pingId);
|
|
const pingSize = (await IOUtils.stat(pingFilePath)).size;
|
|
// Add the info at the beginning of the array, so that most recent pings come first.
|
|
pendingPingsInfo.unshift({
|
|
id: pingId,
|
|
size: pingSize,
|
|
timestamp: date.getTime(),
|
|
});
|
|
|
|
// Set the last modification date.
|
|
await IOUtils.setModificationTime(pingFilePath, date.getTime());
|
|
|
|
// Add it to the pending ping directory size.
|
|
pingsSizeInBytes += pingSize;
|
|
}
|
|
|
|
// We need to test the pending pings size before we hit the quota, otherwise a special
|
|
// value is recorded.
|
|
Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").clear();
|
|
Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA"
|
|
).clear();
|
|
Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS"
|
|
).clear();
|
|
|
|
await TelemetryController.testReset();
|
|
await TelemetryStorage.testPendingQuotaTaskPromise();
|
|
|
|
// Check that the correct values for quota probes are reported when no quota is hit.
|
|
let h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_PINGS_SIZE_MB"
|
|
).snapshot();
|
|
Assert.equal(
|
|
h.sum,
|
|
Math.round(pingsSizeInBytes / 1024 / 1024),
|
|
"Telemetry must report the correct pending pings directory size."
|
|
);
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA"
|
|
).snapshot();
|
|
Assert.equal(
|
|
h.sum,
|
|
0,
|
|
"Telemetry must report 0 evictions if quota is not hit."
|
|
);
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS"
|
|
).snapshot();
|
|
Assert.equal(
|
|
h.sum,
|
|
0,
|
|
"Telemetry must report a null elapsed time if quota is not hit."
|
|
);
|
|
|
|
// Set the quota to 80% of the space.
|
|
const testQuotaInBytes = pingsSizeInBytes * 0.8;
|
|
fakePendingPingsQuota(testQuotaInBytes);
|
|
|
|
// The storage prunes pending pings until we reach 90% of the requested storage quota.
|
|
// Based on that, find how many pings should be kept.
|
|
const safeQuotaSize = Math.round(testQuotaInBytes * 0.9);
|
|
let sizeInBytes = 0;
|
|
let pingsWithinQuota = [];
|
|
let pingsOutsideQuota = [];
|
|
|
|
for (let pingInfo of pendingPingsInfo) {
|
|
sizeInBytes += pingInfo.size;
|
|
if (sizeInBytes >= safeQuotaSize) {
|
|
pingsOutsideQuota.push(pingInfo.id);
|
|
continue;
|
|
}
|
|
pingsWithinQuota.push(pingInfo.id);
|
|
}
|
|
|
|
expectedNotPrunedPings = pingsWithinQuota;
|
|
expectedPrunedPings = pingsOutsideQuota;
|
|
|
|
// Reset TelemetryController to start the pending pings cleanup.
|
|
await TelemetryController.testReset();
|
|
await TelemetryStorage.testPendingQuotaTaskPromise();
|
|
await checkPendingPings();
|
|
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA"
|
|
).snapshot();
|
|
Assert.equal(
|
|
h.sum,
|
|
pingsOutsideQuota.length,
|
|
"Telemetry must correctly report the over quota pings evicted from the pending pings directory."
|
|
);
|
|
h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").snapshot();
|
|
Assert.equal(
|
|
h.sum,
|
|
17,
|
|
"Pending pings quota was hit, a special size must be reported."
|
|
);
|
|
|
|
// Trigger a cleanup again and make sure we're not removing anything.
|
|
await TelemetryController.testReset();
|
|
await TelemetryStorage.testPendingQuotaTaskPromise();
|
|
await checkPendingPings();
|
|
|
|
const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";
|
|
// Create a pending oversized ping.
|
|
const OVERSIZED_PING = {
|
|
id: OVERSIZED_PING_ID,
|
|
type: PING_TYPE,
|
|
creationDate: new Date().toISOString(),
|
|
// Generate a 2MB string to use as the ping payload.
|
|
payload: generateRandomString(2 * 1024 * 1024),
|
|
};
|
|
await TelemetryStorage.savePendingPing(OVERSIZED_PING);
|
|
|
|
// Reset the histograms.
|
|
Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").clear();
|
|
Telemetry.getHistogramById(
|
|
"TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB"
|
|
).clear();
|
|
|
|
// Try to manually load the oversized ping.
|
|
await Assert.rejects(
|
|
TelemetryStorage.loadPendingPing(OVERSIZED_PING_ID),
|
|
/loadPendingPing - exceeded the maximum ping size/,
|
|
"The oversized ping should have been pruned."
|
|
);
|
|
Assert.ok(
|
|
!(await IOUtils.exists(getSavePathForPingId(OVERSIZED_PING_ID))),
|
|
"The ping should not be on the disk anymore."
|
|
);
|
|
|
|
// Make sure we're correctly updating the related histograms.
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PING_SIZE_EXCEEDED_PENDING"
|
|
).snapshot();
|
|
Assert.equal(
|
|
h.sum,
|
|
1,
|
|
"Telemetry must report 1 oversized ping in the pending pings directory."
|
|
);
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB"
|
|
).snapshot();
|
|
Assert.equal(h.values[2], 1, "Telemetry must report a 2MB, oversized, ping.");
|
|
|
|
// Save the ping again to check if it gets pruned when scanning the pings directory.
|
|
await TelemetryStorage.savePendingPing(OVERSIZED_PING);
|
|
expectedPrunedPings.push(OVERSIZED_PING_ID);
|
|
|
|
// Scan the pending pings directory.
|
|
await TelemetryController.testReset();
|
|
await TelemetryStorage.testPendingQuotaTaskPromise();
|
|
await checkPendingPings();
|
|
|
|
// Make sure we're correctly updating the related histograms.
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_PING_SIZE_EXCEEDED_PENDING"
|
|
).snapshot();
|
|
Assert.equal(
|
|
h.sum,
|
|
2,
|
|
"Telemetry must report 1 oversized ping in the pending pings directory."
|
|
);
|
|
h = Telemetry.getHistogramById(
|
|
"TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB"
|
|
).snapshot();
|
|
Assert.equal(
|
|
h.values[2],
|
|
2,
|
|
"Telemetry must report two 2MB, oversized, pings."
|
|
);
|
|
|
|
Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
|
|
});
|
|
|
|
add_task(async function teardown() {
|
|
await PingServer.stop();
|
|
});
|