Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test gets skipped with pattern: os == 'win' && socketprocess_networking && fission OR os == 'mac' && socketprocess_networking && fission OR os == 'mac' && debug OR os == 'linux' && socketprocess_networking
- Manifest: toolkit/components/extensions/test/xpcshell/xpcshell-remote.toml includes toolkit/components/extensions/test/xpcshell/xpcshell-common.toml
- Manifest: toolkit/components/extensions/test/xpcshell/xpcshell.toml includes toolkit/components/extensions/test/xpcshell/xpcshell-common.toml
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const { ExtensionStorageIDB } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionStorageIDB.sys.mjs"
);
const { getTrimmedString } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionTelemetry.sys.mjs"
);
const { TelemetryController } = ChromeUtils.importESModule(
"resource://gre/modules/TelemetryController.sys.mjs"
);
const HISTOGRAM_IDS = [
"WEBEXT_STORAGE_LOCAL_IDB_SET_MS",
"WEBEXT_STORAGE_LOCAL_IDB_GET_MS",
];
const KEYED_HISTOGRAM_IDS = [
"WEBEXT_STORAGE_LOCAL_IDB_SET_MS_BY_ADDONID",
"WEBEXT_STORAGE_LOCAL_IDB_GET_MS_BY_ADDONID",
];
const EXTENSION_ID1 = "@test-extension1";
const EXTENSION_ID2 = "@test-extension2";
async function test_telemetry_background() {
const { GleanTimingDistribution } = globalThis;
// NOTE: we do not collect telemetry for the legacy JSON backend anymore
// and so if the IDB backend is not enabled we expect the related telemetry
// histograms and timing distributions to be empty.
const expectedEmptyGleanMetrics = ExtensionStorageIDB.isBackendEnabled
? []
: ["storageLocalGetIdb", "storageLocalSetIdb"];
const expectedNonEmptyGleanMetrics = ExtensionStorageIDB.isBackendEnabled
? ["storageLocalGetIdb", "storageLocalSetIdb"]
: [];
const expectedEmptyHistograms = ExtensionStorageIDB.isBackendEnabled
? []
: HISTOGRAM_IDS;
const expectedEmptyKeyedHistograms = ExtensionStorageIDB.isBackendEnabled
? []
: KEYED_HISTOGRAM_IDS;
const expectedNonEmptyHistograms = ExtensionStorageIDB.isBackendEnabled
? HISTOGRAM_IDS
: [];
const expectedNonEmptyKeyedHistograms = ExtensionStorageIDB.isBackendEnabled
? KEYED_HISTOGRAM_IDS
: [];
const server = createHttpServer();
server.registerDirectory("/data/", do_get_file("data"));
async function contentScript() {
await browser.storage.local.set({ a: "b" });
await browser.storage.local.get("a");
browser.test.sendMessage("contentDone");
}
let baseManifest = {
permissions: ["storage"],
content_scripts: [
{
js: ["content_script.js"],
},
],
};
let baseExtInfo = {
async background() {
await browser.storage.local.set({ a: "b" });
await browser.storage.local.get("a");
browser.test.sendMessage("backgroundDone");
},
files: {
"content_script.js": contentScript,
},
};
let extension1 = ExtensionTestUtils.loadExtension({
...baseExtInfo,
manifest: {
...baseManifest,
browser_specific_settings: {
gecko: { id: EXTENSION_ID1 },
},
},
});
let extension2 = ExtensionTestUtils.loadExtension({
...baseExtInfo,
manifest: {
...baseManifest,
browser_specific_settings: {
gecko: { id: EXTENSION_ID2 },
},
},
});
// Make sure to force flushing glean fog data from child processes before
// resetting the already collected data.
await Services.fog.testFlushAllChildren();
resetTelemetryData();
// Verify the telemetry data has been cleared.
// Assert glean telemetry data.
for (let metricId of expectedNonEmptyGleanMetrics) {
assertGleanMetricsNoSamples({
metricId,
gleanMetric: Glean.extensionsTiming[metricId],
gleanMetricConstructor: GleanTimingDistribution,
});
}
// Assert unified telemetry data.
let process = IS_OOP ? "extension" : "parent";
let snapshots = getSnapshots(process);
let keyedSnapshots = getKeyedSnapshots(process);
for (let id of HISTOGRAM_IDS) {
ok(!(id in snapshots), `No data recorded for histogram: ${id}.`);
}
for (let id of KEYED_HISTOGRAM_IDS) {
Assert.deepEqual(
Object.keys(keyedSnapshots[id] || {}),
[],
`No data recorded for histogram: ${id}.`
);
}
await extension1.startup();
await extension1.awaitMessage("backgroundDone");
// Assert glean telemetry data.
await Services.fog.testFlushAllChildren();
for (let metricId of expectedNonEmptyGleanMetrics) {
assertGleanMetricsSamplesCount({
metricId,
gleanMetric: Glean.extensionsTiming[metricId],
gleanMetricConstructor: GleanTimingDistribution,
expectedSamplesCount: 1,
});
}
// Assert unified telemetry data.
if (AppConstants.platform != "android") {
for (let id of expectedNonEmptyHistograms) {
await promiseTelemetryRecorded(id, process, 1);
}
for (let id of expectedNonEmptyKeyedHistograms) {
await promiseKeyedTelemetryRecorded(id, process, EXTENSION_ID1, 1);
}
// Telemetry from extension1's background page should be recorded.
snapshots = getSnapshots(process);
keyedSnapshots = getKeyedSnapshots(process);
for (let id of expectedNonEmptyHistograms) {
equal(
valueSum(snapshots[id].values),
1,
`Data recorded for histogram: ${id}.`
);
}
for (let id of expectedNonEmptyKeyedHistograms) {
Assert.deepEqual(
Object.keys(keyedSnapshots[id]),
[EXTENSION_ID1],
`Data recorded for histogram: ${id}.`
);
equal(
valueSum(keyedSnapshots[id][EXTENSION_ID1].values),
1,
`Data recorded for histogram: ${id}.`
);
}
}
await extension2.startup();
await extension2.awaitMessage("backgroundDone");
// Assert glean telemetry data.
await Services.fog.testFlushAllChildren();
for (let metricId of expectedNonEmptyGleanMetrics) {
assertGleanMetricsSamplesCount({
metricId,
gleanMetric: Glean.extensionsTiming[metricId],
gleanMetricConstructor: GleanTimingDistribution,
expectedSamplesCount: 2,
});
}
// Assert unified telemetry data.
if (AppConstants.platform != "android") {
for (let id of expectedNonEmptyHistograms) {
await promiseTelemetryRecorded(id, process, 2);
}
for (let id of expectedNonEmptyKeyedHistograms) {
await promiseKeyedTelemetryRecorded(id, process, EXTENSION_ID2, 1);
}
// Telemetry from extension2's background page should be recorded.
snapshots = getSnapshots(process);
keyedSnapshots = getKeyedSnapshots(process);
for (let id of expectedNonEmptyHistograms) {
equal(
valueSum(snapshots[id].values),
2,
`Additional data recorded for histogram: ${id}.`
);
}
for (let id of expectedNonEmptyKeyedHistograms) {
Assert.deepEqual(
Object.keys(keyedSnapshots[id]).sort(),
[EXTENSION_ID1, EXTENSION_ID2],
`Additional data recorded for histogram: ${id}.`
);
equal(
valueSum(keyedSnapshots[id][EXTENSION_ID2].values),
1,
`Additional data recorded for histogram: ${id}.`
);
}
}
await extension2.unload();
await Services.fog.testFlushAllChildren();
resetTelemetryData();
// Run a content script.
process = "content";
// Expect only telemetry for the single extension content script
// that should be executed when loading the test webpage.
let expectedCount = 1;
let expectedKeyedCount = 1;
let contentPage = await ExtensionTestUtils.loadContentPage(
`${BASE_URL}/file_sample.html`
);
await extension1.awaitMessage("contentDone");
// Assert glean telemetry data.
await Services.fog.testFlushAllChildren();
for (let metricId of expectedNonEmptyGleanMetrics) {
assertGleanMetricsSamplesCount({
metricId,
gleanMetric: Glean.extensionsTiming[metricId],
gleanMetricConstructor: GleanTimingDistribution,
expectedSamplesCount: expectedCount,
});
}
// Assert unified telemetry data.
if (AppConstants.platform != "android") {
for (let id of expectedNonEmptyHistograms) {
await promiseTelemetryRecorded(id, process, expectedCount);
}
for (let id of expectedNonEmptyKeyedHistograms) {
await promiseKeyedTelemetryRecorded(
id,
process,
EXTENSION_ID1,
expectedKeyedCount
);
}
// Telemetry from extension1's content script should be recorded.
snapshots = getSnapshots(process);
keyedSnapshots = getKeyedSnapshots(process);
for (let id of expectedNonEmptyHistograms) {
equal(
valueSum(snapshots[id].values),
expectedCount,
`Data recorded in content script for histogram: ${id}.`
);
}
for (let id of expectedNonEmptyKeyedHistograms) {
Assert.deepEqual(
Object.keys(keyedSnapshots[id]).sort(),
[EXTENSION_ID1],
`Additional data recorded for histogram: ${id}.`
);
equal(
valueSum(keyedSnapshots[id][EXTENSION_ID1].values),
expectedKeyedCount,
`Additional data recorded for histogram: ${id}.`
);
}
}
await extension1.unload();
// Telemetry that we expect to be empty.
// Assert glean telemetry data.
await Services.fog.testFlushAllChildren();
for (let metricId of expectedEmptyGleanMetrics) {
assertGleanMetricsNoSamples({
metricId,
gleanMetric: Glean.extensionsTiming[metricId],
gleanMetricConstructor: GleanTimingDistribution,
});
}
// Assert unified telemetry data.
if (AppConstants.platform != "android") {
for (let id of expectedEmptyHistograms) {
ok(!(id in snapshots), `No data recorded for histogram: ${id}.`);
}
for (let id of expectedEmptyKeyedHistograms) {
Assert.deepEqual(
Object.keys(keyedSnapshots[id] || {}),
[],
`No data recorded for histogram: ${id}.`
);
}
}
await contentPage.close();
}
add_task(async function setup() {
// Telemetry test setup needed to ensure that the builtin events are defined
// and they can be collected and verified.
await TelemetryController.testSetup();
// This is actually only needed on Android, because it does not properly support unified telemetry
// and so, if not enabled explicitly here, it would make these tests to fail when running on a
// non-Nightly build.
const oldCanRecordBase = Services.telemetry.canRecordBase;
Services.telemetry.canRecordBase = true;
registerCleanupFunction(() => {
Services.telemetry.canRecordBase = oldCanRecordBase;
});
});
add_task(function test_telemetry_background_file_backend() {
return runWithPrefs(
[[ExtensionStorageIDB.BACKEND_ENABLED_PREF, false]],
test_telemetry_background
);
});
add_task(function test_telemetry_background_idb_backend() {
return runWithPrefs(
[
[ExtensionStorageIDB.BACKEND_ENABLED_PREF, true],
// Set the migrated preference for the two test extension, because the
// first storage.local call fallbacks to run in the parent process when we
// don't know which is the selected backend during the extension startup
// and so we can't choose the telemetry histogram to use.
[
`${ExtensionStorageIDB.IDB_MIGRATED_PREF_BRANCH}.${EXTENSION_ID1}`,
true,
],
[
`${ExtensionStorageIDB.IDB_MIGRATED_PREF_BRANCH}.${EXTENSION_ID2}`,
true,
],
],
test_telemetry_background
);
});
// This test verifies that we do record the expected telemetry event when we
// normalize the error message for an unexpected error (an error raised internally
// by the QuotaManager and/or IndexedDB, which it is being normalized into the generic
// "An unexpected error occurred" error message).
add_task(async function test_telemetry_storage_local_unexpected_error() {
// Clear any telemetry events collected so far.
Services.telemetry.clearEvents();
const methods = ["clear", "get", "remove", "set"];
const veryLongErrorName = `VeryLongErrorName${Array(200).fill(0).join("")}`;
const otherError = new Error("an error recorded as OtherError");
const recordedErrors = [
new DOMException("error message", "UnexpectedDOMException"),
new DOMException("error message", veryLongErrorName),
otherError,
];
// We expect the following errors to not be recorded in telemetry (because they
// are raised on scenarios that we already expect).
const nonRecordedErrors = [
new DOMException("error message", "QuotaExceededError"),
new DOMException("error message", "DataCloneError"),
];
const expectedEvents = [];
const errors = [].concat(recordedErrors, nonRecordedErrors);
for (let i = 0; i < errors.length; i++) {
const error = errors[i];
const storageMethod = methods[i] || "set";
ExtensionStorageIDB.normalizeStorageError({
error: errors[i],
extensionId: EXTENSION_ID1,
storageMethod,
});
if (recordedErrors.includes(error)) {
let error_name =
error === otherError ? "OtherError" : getTrimmedString(error.name);
expectedEvents.push({
value: EXTENSION_ID1,
object: storageMethod,
extra: { error_name },
});
}
}
let glean = Glean.extensionsData.storageLocalError.testGetValue() ?? [];
equal(glean.length, expectedEvents.length, "Correct number of events.");
for (let i = 0; i < expectedEvents.length; i++) {
let event = expectedEvents[i];
equal(glean[i].extra.addon_id, event.value, "Correct addon_id.");
equal(glean[i].extra.method, event.object, "Correct method.");
equal(glean[i].extra.error_name, event.extra.error_name, "Correct error.");
}
Services.fog.testResetFOG();
});