summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/head_telemetry.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/head_telemetry.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/head_telemetry.js594
1 files changed, 594 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/head_telemetry.js b/toolkit/components/extensions/test/xpcshell/head_telemetry.js
new file mode 100644
index 0000000000..a05211d006
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/head_telemetry.js
@@ -0,0 +1,594 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported IS_OOP, valueSum, clearHistograms, getSnapshots, promiseTelemetryRecorded,
+ assertDNRTelemetryMetricsDefined, assertDNRTelemetryMetricsNoSamples, assertDNRTelemetryMetricsGetValueEq,
+ assertDNRTelemetryMetricsSamplesCount, resetTelemetryData, setupTelemetryForTests */
+
+ChromeUtils.defineESModuleGetters(this, {
+ ContentTaskUtils: "resource://testing-common/ContentTaskUtils.sys.mjs",
+});
+
+// Allows to run xpcshell telemetry test also on products (e.g. Thunderbird) where
+// that telemetry wouldn't be actually collected in practice (but to be sure
+// that it will work on those products as well by just adding the product in
+// the telemetry metric definitions if it turns out we want to).
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+const IS_OOP = Services.prefs.getBoolPref("extensions.webextensions.remote");
+
+const WEBEXT_EVENTPAGE_RUNNING_TIME_MS = "WEBEXT_EVENTPAGE_RUNNING_TIME_MS";
+const WEBEXT_EVENTPAGE_RUNNING_TIME_MS_BY_ADDONID =
+ "WEBEXT_EVENTPAGE_RUNNING_TIME_MS_BY_ADDONID";
+const WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT = "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT";
+const WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID =
+ "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID";
+
+// Keep this in sync with the order in Histograms.json for "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT":
+// the position of the category string determines the index of the values collected in the categorial
+// histogram and so the existing labels should be kept in the exact same order and any new category
+// to be added in the future should be appended to the existing ones.
+const HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES = [
+ "suspend",
+ "reset_other",
+ "reset_event",
+ "reset_listeners",
+ "reset_nativeapp",
+ "reset_streamfilter",
+ "reset_parentapicall",
+];
+
+const GLEAN_EVENTPAGE_IDLE_RESULT_CATEGORIES = [
+ ...HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ "__other__",
+];
+
+function valueSum(arr) {
+ return Object.values(arr).reduce((a, b) => a + b, 0);
+}
+
+function clearHistograms() {
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+ Services.telemetry.getSnapshotForKeyedHistograms("main", true /* clear */);
+}
+
+function clearScalars() {
+ Services.telemetry.getSnapshotForScalars("main", true /* clear */);
+ Services.telemetry.getSnapshotForKeyedScalars("main", true /* clear */);
+}
+
+function getSnapshots(process) {
+ return Services.telemetry.getSnapshotForHistograms("main", false /* clear */)[
+ process
+ ];
+}
+
+function getKeyedSnapshots(process) {
+ return Services.telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ )[process];
+}
+
+// TODO Bug 1357509: There is no good way to make sure that the parent received
+// the histogram entries from the extension and content processes. Let's stick
+// to the ugly, spinning the event loop until we have a good approach.
+function promiseTelemetryRecorded(id, process, expectedCount) {
+ let condition = () => {
+ let snapshot = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ )[process][id];
+ return snapshot && valueSum(snapshot.values) >= expectedCount;
+ };
+ return ContentTaskUtils.waitForCondition(condition);
+}
+
+function promiseKeyedTelemetryRecorded(
+ id,
+ process,
+ expectedKey,
+ expectedCount
+) {
+ let condition = () => {
+ let snapshot = Services.telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ )[process][id];
+ return (
+ snapshot &&
+ snapshot[expectedKey] &&
+ valueSum(snapshot[expectedKey].values) >= expectedCount
+ );
+ };
+ return ContentTaskUtils.waitForCondition(condition);
+}
+
+function assertHistogramSnapshot(
+ histogramId,
+ { keyed, processSnapshot, expectedValue },
+ msg
+) {
+ let histogram;
+
+ if (keyed) {
+ histogram = Services.telemetry.getKeyedHistogramById(histogramId);
+ } else {
+ histogram = Services.telemetry.getHistogramById(histogramId);
+ }
+
+ let res = processSnapshot(histogram.snapshot());
+ Assert.deepEqual(res, expectedValue, msg);
+ return res;
+}
+
+function assertHistogramEmpty(histogramId) {
+ assertHistogramSnapshot(
+ histogramId,
+ {
+ processSnapshot: snapshot => snapshot.sum,
+ expectedValue: 0,
+ },
+ `No data recorded for histogram: ${histogramId}.`
+ );
+}
+
+function assertKeyedHistogramEmpty(histogramId) {
+ assertHistogramSnapshot(
+ histogramId,
+ {
+ keyed: true,
+ processSnapshot: snapshot => Object.keys(snapshot).length,
+ expectedValue: 0,
+ },
+ `No data recorded for histogram: ${histogramId}.`
+ );
+}
+
+function assertHistogramCategoryNotEmpty(
+ histogramId,
+ { category, categories, keyed, key },
+ msg
+) {
+ let message = msg;
+
+ if (!msg) {
+ message = `Data recorded for histogram: ${histogramId}, category "${category}"`;
+ if (keyed) {
+ message += `, key "${key}"`;
+ }
+ }
+
+ assertHistogramSnapshot(
+ histogramId,
+ {
+ keyed,
+ processSnapshot: snapshot => {
+ const categoryIndex = categories.indexOf(category);
+ if (keyed) {
+ return {
+ [key]: snapshot[key]
+ ? snapshot[key].values[categoryIndex] > 0
+ : null,
+ };
+ }
+ return snapshot.values[categoryIndex] > 0;
+ },
+ expectedValue: keyed ? { [key]: true } : true,
+ },
+ message
+ );
+}
+
+function setupTelemetryForTests() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+ // FOG needs to be initialized in order for data to flow.
+ Services.fog.initializeFOG();
+}
+
+function resetTelemetryData() {
+ Services.fog.testResetFOG();
+
+ // Clear histograms data recorded in the unified telemetry
+ // (needed to make sure we can keep asserting that the same
+ // amount of samples collected by Glean should also be found
+ // in the related mirrored unified telemetry probe after we
+ // have reset Glean metrics data using testResetFOG).
+ clearHistograms();
+ clearScalars();
+}
+
+function assertValidGleanMetric({
+ metricId,
+ gleanMetric,
+ gleanMetricConstructor,
+ msg,
+}) {
+ const { GleanMetric } = globalThis;
+ if (!(gleanMetric instanceof GleanMetric)) {
+ throw new Error(
+ `gleanMetric "${metricId}" ${gleanMetric} should be an instance of GleanMetric ${msg}`
+ );
+ }
+
+ if (
+ gleanMetricConstructor &&
+ !(gleanMetric instanceof gleanMetricConstructor)
+ ) {
+ throw new Error(
+ `gleanMetric "${metricId}" should be an instance of the given GleanMetric constructor: ${gleanMetric} not an instance of ${gleanMetricConstructor} ${msg}`
+ );
+ }
+}
+
+// TODO reuse this helper inside the DNR specific test helper which would be doing
+// a similar assertion on DNR metrics.
+function assertGleanMetricsNoSamples({
+ metricId,
+ gleanMetric,
+ gleanMetricConstructor,
+ message,
+}) {
+ const msg = message ? `(${message})` : "";
+ assertValidGleanMetric({
+ metricId,
+ gleanMetric,
+ gleanMetricConstructor,
+ msg,
+ });
+ const gleanData = gleanMetric.testGetValue();
+ Assert.deepEqual(
+ gleanData,
+ undefined,
+ `Got no sample for Glean metric ${metricId} ${msg}`
+ );
+}
+
+// TODO reuse this helper inside the DNR specific test helper which would be doing
+// a similar assertion on DNR metrics.
+function assertGleanMetricsSamplesCount({
+ metricId,
+ gleanMetric,
+ gleanMetricConstructor,
+ expectedSamplesCount,
+ message,
+}) {
+ const msg = message ? `(${message})` : "";
+ assertValidGleanMetric({
+ metricId,
+ gleanMetric,
+ gleanMetricConstructor,
+ msg,
+ });
+ const gleanData = gleanMetric.testGetValue();
+ Assert.notEqual(
+ gleanData,
+ undefined,
+ `Got some sample for Glean metric ${metricId} ${msg}`
+ );
+ Assert.equal(
+ valueSum(gleanData.values),
+ expectedSamplesCount,
+ `Got the expected number of samples for Glean metric ${metricId} ${msg}`
+ );
+}
+
+function assertGleanLabeledCounter({
+ metricId,
+ gleanMetric,
+ gleanMetricLabels,
+ expectedLabelsValue,
+ ignoreNonExpectedLabels,
+ ignoreUnknownLabels,
+ message,
+}) {
+ const { GleanLabeled } = globalThis;
+ const msg = message ? `(${message})` : "";
+ if (!Array.isArray(gleanMetricLabels) || !gleanMetricLabels.length) {
+ throw new Error(
+ `Missing mandatory gleanMetricLabels property ${msg}: ${gleanMetricLabels}`
+ );
+ }
+
+ if (!(gleanMetric instanceof GleanLabeled)) {
+ throw new Error(
+ `Glean metric "${metricId}" should be an instance of GleanLabeled: ${gleanMetric} ${msg}`
+ );
+ }
+
+ for (const label of gleanMetricLabels) {
+ const expectedLabelValue = expectedLabelsValue[label];
+ if (ignoreNonExpectedLabels && !(label in expectedLabelsValue)) {
+ continue;
+ }
+ Assert.deepEqual(
+ gleanMetric[label].testGetValue(),
+ expectedLabelValue,
+ `Expect Glean "${metricId}" metric label "${label}" to be ${
+ expectedLabelValue > 0 ? expectedLabelValue : "empty"
+ }`
+ );
+ }
+
+ if (!ignoreUnknownLabels) {
+ Assert.deepEqual(
+ gleanMetric["__other__"].testGetValue(), // eslint-disable-line dot-notation
+ undefined,
+ `Expect Glean "${metricId}" metric label "__other__" to be empty.`
+ );
+ }
+}
+
+function assertGleanLabeledCounterEmpty({
+ metricId,
+ gleanMetric,
+ gleanMetricLabels,
+ message,
+}) {
+ // All empty labels passed to the other helpers to make it
+ // assert that all labels are empty.
+ assertGleanLabeledCounter({
+ metricId,
+ gleanMetric,
+ gleanMetricLabels,
+ expectedLabelsValue: {},
+ message,
+ });
+}
+
+function assertGleanLabeledCounterNotEmpty({
+ metricId,
+ gleanMetric,
+ expectedNotEmptyLabels,
+ ignoreUnknownLabels,
+ message,
+}) {
+ const { GleanLabeled } = globalThis;
+ const msg = message ? `(${message})` : "";
+ if (
+ !Array.isArray(expectedNotEmptyLabels) ||
+ !expectedNotEmptyLabels.length
+ ) {
+ throw new Error(
+ `Missing mandatory expectedNotEmptyLabels property ${msg}: ${expectedNotEmptyLabels}`
+ );
+ }
+
+ if (!(gleanMetric instanceof GleanLabeled)) {
+ throw new Error(
+ `Glean metric "${metricId}" should be an instance of GleanLabeled: ${gleanMetric} ${msg}`
+ );
+ }
+
+ for (const label of expectedNotEmptyLabels) {
+ Assert.notEqual(
+ gleanMetric[label].testGetValue(),
+ undefined,
+ `Expect Glean "${metricId}" metric label "${label}" to not be empty`
+ );
+ }
+
+ if (!ignoreUnknownLabels) {
+ Assert.deepEqual(
+ gleanMetric["__other__"].testGetValue(), // eslint-disable-line dot-notation
+ undefined,
+ `Expect Glean "${metricId}" metric label "__other__" to be empty.`
+ );
+ }
+}
+
+function assertDNRTelemetryMetricsDefined(metrics) {
+ const metricsNotFound = metrics.filter(metricDetails => {
+ const { metric, label } = metricDetails;
+ if (!Glean.extensionsApisDnr[metric]) {
+ return true;
+ }
+ if (label) {
+ return !Glean.extensionsApisDnr[metric][label];
+ }
+ return false;
+ });
+ Assert.deepEqual(
+ metricsNotFound,
+ [],
+ `All expected extensionsApisDnr Glean metrics should be found`
+ );
+}
+
+function assertDNRTelemetryMirrored({
+ gleanMetric,
+ gleanLabel,
+ unifiedName,
+ unifiedType,
+}) {
+ assertDNRTelemetryMetricsDefined([
+ { metric: gleanMetric, label: gleanLabel },
+ ]);
+ const gleanData = gleanLabel
+ ? Glean.extensionsApisDnr[gleanMetric][gleanLabel].testGetValue()
+ : Glean.extensionsApisDnr[gleanMetric].testGetValue();
+
+ if (!unifiedName) {
+ Assert.ok(
+ false,
+ `Unexpected missing unifiedName parameter on call to assertDNRTelemetryMirrored`
+ );
+ return;
+ }
+
+ let unifiedData;
+
+ switch (unifiedType) {
+ case "histogram": {
+ let found = false;
+ try {
+ const histogram = Services.telemetry.getHistogramById(unifiedName);
+ found = !!histogram;
+ } catch (err) {
+ Cu.reportError(err);
+ }
+ Assert.ok(found, `Expect an histogram named ${unifiedName} to be found`);
+ unifiedData = Services.telemetry.getSnapshotForHistograms("main", false)
+ .parent[unifiedName];
+ break;
+ }
+ case "keyedScalar": {
+ const snapshot = Services.telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false
+ );
+ if (unifiedName in (snapshot?.parent || {})) {
+ unifiedData = snapshot.parent[unifiedName][gleanLabel];
+ }
+ break;
+ }
+ case "scalar": {
+ const snapshot = Services.telemetry.getSnapshotForScalars("main", false);
+ if (unifiedName in (snapshot?.parent || {})) {
+ unifiedData = snapshot.parent[unifiedName];
+ }
+ break;
+ }
+ default:
+ Assert.ok(
+ false,
+ `Unexpected unifiedType ${unifiedType} on call to assertDNRTelemetryMirrored`
+ );
+ return;
+ }
+
+ if (gleanData == undefined) {
+ Assert.deepEqual(
+ unifiedData,
+ undefined,
+ `Expect mirrored unified telemetry ${unifiedType} ${unifiedName} has no samples as Glean ${gleanMetric}`
+ );
+ } else {
+ switch (unifiedType) {
+ case "histogram": {
+ Assert.deepEqual(
+ valueSum(unifiedData.values),
+ valueSum(gleanData.values),
+ `Expect mirrored unified telemetry ${unifiedType} ${unifiedName} has samples mirrored from Glean ${gleanMetric}`
+ );
+ break;
+ }
+ case "scalar":
+ case "keyedScalar": {
+ Assert.deepEqual(
+ unifiedData,
+ gleanData,
+ `Expect mirrored unified telemetry ${unifiedType} ${unifiedName} has samples mirrored from Glean ${gleanMetric}`
+ );
+ break;
+ }
+ }
+ }
+}
+
+function assertDNRTelemetryMetricsNoSamples(metrics, msg) {
+ assertDNRTelemetryMetricsDefined(metrics);
+ for (const metricDetails of metrics) {
+ const { metric, label } = metricDetails;
+
+ const gleanData = label
+ ? Glean.extensionsApisDnr[metric][label].testGetValue()
+ : Glean.extensionsApisDnr[metric].testGetValue();
+ Assert.deepEqual(
+ gleanData,
+ undefined,
+ `Expect no sample for Glean metric extensionApisDnr.${metric} (${msg}): ${gleanData}`
+ );
+
+ if (metricDetails.mirroredName) {
+ const { mirroredName, mirroredType } = metricDetails;
+ assertDNRTelemetryMirrored({
+ gleanMetric: metric,
+ gleanLabel: label,
+ unifiedName: mirroredName,
+ unifiedType: mirroredType,
+ });
+ }
+ }
+}
+
+function assertDNRTelemetryMetricsGetValueEq(metrics, msg) {
+ assertDNRTelemetryMetricsDefined(metrics);
+ for (const metricDetails of metrics) {
+ const { metric, label, expectedGetValue } = metricDetails;
+
+ const gleanData = label
+ ? Glean.extensionsApisDnr[metric][label].testGetValue()
+ : Glean.extensionsApisDnr[metric].testGetValue();
+ Assert.deepEqual(
+ gleanData,
+ expectedGetValue,
+ `Got expected value set on Glean metric extensionApisDnr.${metric}${
+ label ? `.${label}` : ""
+ } (${msg})`
+ );
+
+ if (metricDetails.mirroredName) {
+ const { mirroredName, mirroredType } = metricDetails;
+ assertDNRTelemetryMirrored({
+ gleanMetric: metric,
+ gleanLabel: label,
+ unifiedName: mirroredName,
+ unifiedType: mirroredType,
+ });
+ }
+ }
+}
+
+function assertDNRTelemetryMetricsSamplesCount(metrics, msg) {
+ assertDNRTelemetryMetricsDefined(metrics);
+
+ // This assertion helpers doesn't currently handle labeled metrics,
+ // raise an explicit error to catch if one is included by mistake.
+ const labeledMetricsFound = metrics.filter(metric => !!metric.label);
+ if (labeledMetricsFound.length) {
+ throw new Error(
+ `Unexpected labeled metrics in call to assertDNRTelemetryMetricsSamplesCount: ${labeledMetricsFound}`
+ );
+ }
+
+ for (const metricDetails of metrics) {
+ const { metric, expectedSamplesCount } = metricDetails;
+
+ const gleanData = Glean.extensionsApisDnr[metric].testGetValue();
+ Assert.notEqual(
+ gleanData,
+ undefined,
+ `Got some sample for Glean metric extensionApisDnr.${metric}: ${
+ gleanData && JSON.stringify(gleanData)
+ }`
+ );
+ Assert.equal(
+ valueSum(gleanData.values),
+ expectedSamplesCount,
+ `Got the expected number of samples for Glean metric extensionsApisDnr.${metric} (${msg})`
+ );
+ // Make sure we are accumulating meaningfull values in the sample,
+ // if we do have samples for the bucket "0" it likely means we have
+ // not been collecting the value correctly (e.g. typo in the property
+ // name being collected).
+ Assert.ok(
+ !gleanData.values["0"],
+ `No sample for Glean metric extensionsApisDnr.${metric} should be collected for the bucket "0"`
+ );
+
+ if (metricDetails.mirroredName) {
+ const { mirroredName, mirroredType } = metricDetails;
+ assertDNRTelemetryMirrored({
+ gleanMetric: metric,
+ unifiedName: mirroredName,
+ unifiedType: mirroredType,
+ });
+ }
+ }
+}