diff options
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/head_telemetry.js')
-rw-r--r-- | toolkit/components/extensions/test/xpcshell/head_telemetry.js | 594 |
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, + }); + } + } +} |