Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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, {
});
// 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];
}
// 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,
});
}
}
}