summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/tests/unit')
-rw-r--r--toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.sys.mjs76
-rw-r--r--toolkit/components/telemetry/tests/unit/TelemetryEnvironmentTesting.sys.mjs859
-rw-r--r--toolkit/components/telemetry/tests/unit/data/search-extensions/engines.json14
-rw-r--r--toolkit/components/telemetry/tests/unit/data/search-extensions/telemetrySearchIdentifier/manifest.json29
-rw-r--r--toolkit/components/telemetry/tests/unit/engine.xml7
-rw-r--r--toolkit/components/telemetry/tests/unit/file_UninstallPing.worker.js37
-rw-r--r--toolkit/components/telemetry/tests/unit/head.js587
-rw-r--r--toolkit/components/telemetry/tests/unit/testNoPDB32.dllbin0 -> 8704 bytes
-rw-r--r--toolkit/components/telemetry/tests/unit/testNoPDB64.dllbin0 -> 10240 bytes
-rwxr-xr-xtoolkit/components/telemetry/tests/unit/testNoPDBAArch64.dllbin0 -> 1536 bytes
-rw-r--r--toolkit/components/telemetry/tests/unit/testUnicodePDB32.dllbin0 -> 8704 bytes
-rw-r--r--toolkit/components/telemetry/tests/unit/testUnicodePDB64.dllbin0 -> 10752 bytes
-rwxr-xr-xtoolkit/components/telemetry/tests/unit/testUnicodePDBAArch64.dllbin0 -> 7168 bytes
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ChildEvents.js222
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ChildHistograms.js333
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ChildScalars.js241
-rw-r--r--toolkit/components/telemetry/tests/unit/test_CoveragePing.js114
-rw-r--r--toolkit/components/telemetry/tests/unit/test_EventPing.js280
-rw-r--r--toolkit/components/telemetry/tests/unit/test_HealthPing.js279
-rw-r--r--toolkit/components/telemetry/tests/unit/test_MigratePendingPings.js153
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ModulesPing.js300
-rw-r--r--toolkit/components/telemetry/tests/unit/test_PingAPI.js709
-rw-r--r--toolkit/components/telemetry/tests/unit/test_PingSender.js284
-rw-r--r--toolkit/components/telemetry/tests/unit/test_RDDScalars.js53
-rw-r--r--toolkit/components/telemetry/tests/unit/test_SocketScalars.js49
-rw-r--r--toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js289
-rw-r--r--toolkit/components/telemetry/tests/unit/test_SyncPingIntegration.js65
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryAndroidEnvironment.js64
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryChildEvents_buildFaster.js129
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryClientID_reset.js185
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryController.js1218
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js74
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js82
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js71
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js1427
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment_search.js410
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js1109
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEvents_buildFaster.js463
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js29
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryHistograms.js2073
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js143
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js56
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js350
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js1088
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryScalars_buildFaster.js226
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryScalars_impressionId.js48
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryScalars_multistore.js415
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySend.js1110
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js586
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySession.js2357
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js197
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySession_activeTicks.js126
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js196
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js79
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryUtils.js39
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ThirdPartyModulesPing.js282
-rw-r--r--toolkit/components/telemetry/tests/unit/test_UninstallPing.js126
-rw-r--r--toolkit/components/telemetry/tests/unit/test_UserInteraction.js134
-rw-r--r--toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js470
-rw-r--r--toolkit/components/telemetry/tests/unit/test_UtilityScalars.js65
-rw-r--r--toolkit/components/telemetry/tests/unit/test_bug1555798.js48
-rw-r--r--toolkit/components/telemetry/tests/unit/test_client_id.js163
-rw-r--r--toolkit/components/telemetry/tests/unit/test_failover_retry.js261
-rw-r--r--toolkit/components/telemetry/tests/unit/xpcshell.ini134
64 files changed, 21013 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.sys.mjs b/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.sys.mjs
new file mode 100644
index 0000000000..0ec25213a4
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.sys.mjs
@@ -0,0 +1,76 @@
+import { TelemetryArchive } from "resource://gre/modules/TelemetryArchive.sys.mjs";
+
+function checkForProperties(ping, expected) {
+ for (let [props, val] of expected) {
+ let test = ping;
+ for (let prop of props) {
+ test = test[prop];
+ if (test === undefined) {
+ return false;
+ }
+ }
+ if (test !== val) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * A helper object that allows test code to check whether a telemetry ping
+ * was properly saved. To use, first initialize to collect the starting pings
+ * and then check for new ping data.
+ */
+function Checker() {}
+Checker.prototype = {
+ promiseInit() {
+ this._pingMap = new Map();
+ return TelemetryArchive.promiseArchivedPingList().then(plist => {
+ for (let ping of plist) {
+ this._pingMap.set(ping.id, ping);
+ }
+ });
+ },
+
+ /**
+ * Find and return a new ping with certain properties.
+ *
+ * @param expected: an array of [['prop'...], 'value'] to check
+ * For example:
+ * [
+ * [['environment', 'build', 'applicationId'], '20150101010101'],
+ * [['version'], 1],
+ * [['metadata', 'OOMAllocationSize'], 123456789],
+ * ]
+ * @returns a matching ping if found, or null
+ */
+ async promiseFindPing(type, expected) {
+ let candidates = [];
+ let plist = await TelemetryArchive.promiseArchivedPingList();
+ for (let ping of plist) {
+ if (this._pingMap.has(ping.id)) {
+ continue;
+ }
+ if (ping.type == type) {
+ candidates.push(ping);
+ }
+ }
+
+ for (let candidate of candidates) {
+ let ping = await TelemetryArchive.promiseArchivedPingById(candidate.id);
+ if (checkForProperties(ping, expected)) {
+ return ping;
+ }
+ }
+ return null;
+ },
+};
+
+export const TelemetryArchiveTesting = {
+ setup() {
+ Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
+ Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true);
+ },
+
+ Checker,
+};
diff --git a/toolkit/components/telemetry/tests/unit/TelemetryEnvironmentTesting.sys.mjs b/toolkit/components/telemetry/tests/unit/TelemetryEnvironmentTesting.sys.mjs
new file mode 100644
index 0000000000..da12fb74f5
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/TelemetryEnvironmentTesting.sys.mjs
@@ -0,0 +1,859 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ Assert: "resource://testing-common/Assert.sys.mjs",
+ // AttributionCode is only needed for Firefox
+ AttributionCode: "resource:///modules/AttributionCode.sys.mjs",
+
+ MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs",
+});
+
+const gIsWindows = AppConstants.platform == "win";
+const gIsMac = AppConstants.platform == "macosx";
+const gIsAndroid = AppConstants.platform == "android";
+const gIsLinux = AppConstants.platform == "linux";
+
+const MILLISECONDS_PER_MINUTE = 60 * 1000;
+const MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE;
+const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR;
+
+const PLATFORM_VERSION = "1.9.2";
+const APP_VERSION = "1";
+const APP_ID = "xpcshell@tests.mozilla.org";
+const APP_NAME = "XPCShell";
+
+const DISTRIBUTION_ID = "distributor-id";
+const DISTRIBUTION_VERSION = "4.5.6b";
+const DISTRIBUTOR_NAME = "Some Distributor";
+const DISTRIBUTOR_CHANNEL = "A Channel";
+const PARTNER_NAME = "test";
+const PARTNER_ID = "NicePartner-ID-3785";
+
+// The profile reset date, in milliseconds (Today)
+const PROFILE_RESET_DATE_MS = Date.now();
+// The profile creation date, in milliseconds (Yesterday).
+const PROFILE_FIRST_USE_MS = PROFILE_RESET_DATE_MS - MILLISECONDS_PER_DAY;
+const PROFILE_CREATION_DATE_MS = PROFILE_FIRST_USE_MS - MILLISECONDS_PER_DAY;
+
+const GFX_VENDOR_ID = "0xabcd";
+const GFX_DEVICE_ID = "0x1234";
+
+const EXPECTED_HDD_FIELDS = ["profile", "binary", "system"];
+
+// Valid attribution code to write so that settings.attribution can be tested.
+const ATTRIBUTION_CODE = "source%3Dgoogle.com%26dlsource%3Dunittest";
+
+function truncateToDays(aMsec) {
+ return Math.floor(aMsec / MILLISECONDS_PER_DAY);
+}
+
+var SysInfo = {
+ overrides: {},
+
+ getProperty(name) {
+ // Assert.ok(false, "Mock SysInfo: " + name + ", " + JSON.stringify(this.overrides));
+ if (name in this.overrides) {
+ return this.overrides[name];
+ }
+
+ return this._genuine.QueryInterface(Ci.nsIPropertyBag).getProperty(name);
+ },
+
+ getPropertyAsACString(name) {
+ return this.get(name);
+ },
+
+ getPropertyAsUint32(name) {
+ return this.get(name);
+ },
+
+ get(name) {
+ return this._genuine.QueryInterface(Ci.nsIPropertyBag2).get(name);
+ },
+
+ get diskInfo() {
+ return this._genuine.QueryInterface(Ci.nsISystemInfo).diskInfo;
+ },
+
+ get osInfo() {
+ return this._genuine.QueryInterface(Ci.nsISystemInfo).osInfo;
+ },
+
+ get processInfo() {
+ return this._genuine.QueryInterface(Ci.nsISystemInfo).processInfo;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIPropertyBag2", "nsISystemInfo"]),
+};
+
+/**
+ * TelemetryEnvironmentTesting - tools for testing the telemetry environment
+ * reporting.
+ */
+export var TelemetryEnvironmentTesting = {
+ EXPECTED_HDD_FIELDS,
+
+ init(appInfo) {
+ this.appInfo = appInfo;
+ },
+
+ setSysInfoOverrides(overrides) {
+ SysInfo.overrides = overrides;
+ },
+
+ spoofGfxAdapter() {
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
+ Ci.nsIGfxInfoDebug
+ );
+ gfxInfo.spoofVendorID(GFX_VENDOR_ID);
+ gfxInfo.spoofDeviceID(GFX_DEVICE_ID);
+ } catch (x) {
+ // If we can't test gfxInfo, that's fine, we'll note it later.
+ }
+ },
+
+ spoofProfileReset() {
+ return IOUtils.writeJSON(
+ PathUtils.join(PathUtils.profileDir, "times.json"),
+ {
+ created: PROFILE_CREATION_DATE_MS,
+ reset: PROFILE_RESET_DATE_MS,
+ firstUse: PROFILE_FIRST_USE_MS,
+ }
+ );
+ },
+
+ spoofPartnerInfo() {
+ let prefsToSpoof = {};
+ prefsToSpoof["distribution.id"] = DISTRIBUTION_ID;
+ prefsToSpoof["distribution.version"] = DISTRIBUTION_VERSION;
+ prefsToSpoof["app.distributor"] = DISTRIBUTOR_NAME;
+ prefsToSpoof["app.distributor.channel"] = DISTRIBUTOR_CHANNEL;
+ prefsToSpoof["app.partner.test"] = PARTNER_NAME;
+ prefsToSpoof["mozilla.partner.id"] = PARTNER_ID;
+
+ // Spoof the preferences.
+ for (let pref in prefsToSpoof) {
+ Services.prefs
+ .getDefaultBranch(null)
+ .setStringPref(pref, prefsToSpoof[pref]);
+ }
+ },
+
+ async spoofAttributionData() {
+ if (gIsWindows || gIsMac) {
+ lazy.AttributionCode._clearCache();
+ await lazy.AttributionCode.writeAttributionFile(ATTRIBUTION_CODE);
+ }
+ },
+
+ cleanupAttributionData() {
+ if (gIsWindows || gIsMac) {
+ lazy.AttributionCode.attributionFile.remove(false);
+ lazy.AttributionCode._clearCache();
+ }
+ },
+
+ registerFakeSysInfo() {
+ lazy.MockRegistrar.register("@mozilla.org/system-info;1", SysInfo);
+ },
+
+ /**
+ * Check that a value is a string and not empty.
+ *
+ * @param aValue The variable to check.
+ * @return True if |aValue| has type "string" and is not empty, False otherwise.
+ */
+ checkString(aValue) {
+ return typeof aValue == "string" && aValue != "";
+ },
+
+ /**
+ * If value is non-null, check if it's a valid string.
+ *
+ * @param aValue The variable to check.
+ * @return True if it's null or a valid string, false if it's non-null and an invalid
+ * string.
+ */
+ checkNullOrString(aValue) {
+ if (aValue) {
+ return this.checkString(aValue);
+ } else if (aValue === null) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * If value is non-null, check if it's a boolean.
+ *
+ * @param aValue The variable to check.
+ * @return True if it's null or a valid boolean, false if it's non-null and an invalid
+ * boolean.
+ */
+ checkNullOrBool(aValue) {
+ return aValue === null || typeof aValue == "boolean";
+ },
+
+ checkBuildSection(data) {
+ const expectedInfo = {
+ applicationId: APP_ID,
+ applicationName: APP_NAME,
+ buildId: this.appInfo.appBuildID,
+ version: APP_VERSION,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ lazy.Assert.ok(
+ "build" in data,
+ "There must be a build section in Environment."
+ );
+
+ for (let f in expectedInfo) {
+ lazy.Assert.ok(
+ this.checkString(data.build[f]),
+ f + " must be a valid string."
+ );
+ lazy.Assert.equal(
+ data.build[f],
+ expectedInfo[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // Make sure architecture is in the environment.
+ lazy.Assert.ok(this.checkString(data.build.architecture));
+
+ lazy.Assert.equal(
+ data.build.updaterAvailable,
+ AppConstants.MOZ_UPDATER,
+ "build.updaterAvailable must equal AppConstants.MOZ_UPDATER"
+ );
+ },
+
+ checkSettingsSection(data) {
+ const EXPECTED_FIELDS_TYPES = {
+ blocklistEnabled: "boolean",
+ e10sEnabled: "boolean",
+ e10sMultiProcesses: "number",
+ fissionEnabled: "boolean",
+ intl: "object",
+ locale: "string",
+ telemetryEnabled: "boolean",
+ update: "object",
+ userPrefs: "object",
+ };
+
+ lazy.Assert.ok(
+ "settings" in data,
+ "There must be a settings section in Environment."
+ );
+
+ for (let f in EXPECTED_FIELDS_TYPES) {
+ lazy.Assert.equal(
+ typeof data.settings[f],
+ EXPECTED_FIELDS_TYPES[f],
+ f + " must have the correct type."
+ );
+ }
+
+ // This property is not always present, but when it is, it must be a number.
+ if ("launcherProcessState" in data.settings) {
+ lazy.Assert.equal(typeof data.settings.launcherProcessState, "number");
+ }
+
+ // Check "addonCompatibilityCheckEnabled" separately.
+ lazy.Assert.equal(
+ data.settings.addonCompatibilityCheckEnabled,
+ lazy.AddonManager.checkCompatibility
+ );
+
+ // Check "isDefaultBrowser" separately, as it is not available on Android an can either be
+ // null or boolean on other platforms.
+ if (gIsAndroid) {
+ lazy.Assert.ok(
+ !("isDefaultBrowser" in data.settings),
+ "Must not be available on Android."
+ );
+ } else if ("isDefaultBrowser" in data.settings) {
+ // isDefaultBrowser might not be available in the payload, since it's
+ // gathered after the session was restored.
+ lazy.Assert.ok(this.checkNullOrBool(data.settings.isDefaultBrowser));
+ }
+
+ // Check "channel" separately, as it can either be null or string.
+ let update = data.settings.update;
+ lazy.Assert.ok(this.checkNullOrString(update.channel));
+ lazy.Assert.equal(typeof update.enabled, "boolean");
+ lazy.Assert.equal(typeof update.autoDownload, "boolean");
+ lazy.Assert.equal(typeof update.background, "boolean");
+
+ // Check sandbox settings exist and make sense
+ if (data.settings.sandbox.effectiveContentProcessLevel !== null) {
+ lazy.Assert.equal(
+ typeof data.settings.sandbox.effectiveContentProcessLevel,
+ "number",
+ "sandbox.effectiveContentProcessLevel must have the correct type"
+ );
+ }
+
+ if (data.settings.sandbox.contentWin32kLockdownState !== null) {
+ lazy.Assert.equal(
+ typeof data.settings.sandbox.contentWin32kLockdownState,
+ "number",
+ "sandbox.contentWin32kLockdownState must have the correct type"
+ );
+
+ let win32kLockdownState =
+ data.settings.sandbox.contentWin32kLockdownState;
+ lazy.Assert.ok(win32kLockdownState >= 1 && win32kLockdownState <= 17);
+ }
+
+ // Check "defaultSearchEngine" separately, as it can either be undefined or string.
+ if ("defaultSearchEngine" in data.settings) {
+ this.checkString(data.settings.defaultSearchEngine);
+ lazy.Assert.equal(typeof data.settings.defaultSearchEngineData, "object");
+ }
+
+ if ("defaultPrivateSearchEngineData" in data.settings) {
+ lazy.Assert.equal(
+ typeof data.settings.defaultPrivateSearchEngineData,
+ "object"
+ );
+ }
+
+ if ((gIsWindows || gIsMac) && AppConstants.MOZ_BUILD_APP == "browser") {
+ lazy.Assert.equal(typeof data.settings.attribution, "object");
+ lazy.Assert.equal(data.settings.attribution.source, "google.com");
+ lazy.Assert.equal(data.settings.attribution.dlsource, "unittest");
+ }
+
+ this.checkIntlSettings(data.settings);
+ },
+
+ checkIntlSettings({ intl }) {
+ let fields = [
+ "requestedLocales",
+ "availableLocales",
+ "appLocales",
+ "acceptLanguages",
+ ];
+
+ for (let field of fields) {
+ lazy.Assert.ok(Array.isArray(intl[field]), `${field} is an array`);
+ }
+
+ // These fields may be null if they aren't ready yet. This is mostly to deal
+ // with test failures on Android, but they aren't guaranteed to exist.
+ let optionalFields = ["systemLocales", "regionalPrefsLocales"];
+
+ for (let field of optionalFields) {
+ let isArray = Array.isArray(intl[field]);
+ let isNull = intl[field] === null;
+ lazy.Assert.ok(isArray || isNull, `${field} is an array or null`);
+ }
+ },
+
+ checkProfileSection(data) {
+ lazy.Assert.ok(
+ "profile" in data,
+ "There must be a profile section in Environment."
+ );
+ lazy.Assert.equal(
+ data.profile.creationDate,
+ truncateToDays(PROFILE_CREATION_DATE_MS)
+ );
+ lazy.Assert.equal(
+ data.profile.resetDate,
+ truncateToDays(PROFILE_RESET_DATE_MS)
+ );
+ lazy.Assert.equal(
+ data.profile.firstUseDate,
+ truncateToDays(PROFILE_FIRST_USE_MS)
+ );
+ },
+
+ checkPartnerSection(data, isInitial) {
+ const EXPECTED_FIELDS = {
+ distributionId: DISTRIBUTION_ID,
+ distributionVersion: DISTRIBUTION_VERSION,
+ partnerId: PARTNER_ID,
+ distributor: DISTRIBUTOR_NAME,
+ distributorChannel: DISTRIBUTOR_CHANNEL,
+ };
+
+ lazy.Assert.ok(
+ "partner" in data,
+ "There must be a partner section in Environment."
+ );
+
+ for (let f in EXPECTED_FIELDS) {
+ let expected = isInitial ? null : EXPECTED_FIELDS[f];
+ lazy.Assert.strictEqual(
+ data.partner[f],
+ expected,
+ f + " must have the correct value."
+ );
+ }
+
+ // Check that "partnerNames" exists and contains the correct element.
+ lazy.Assert.ok(Array.isArray(data.partner.partnerNames));
+ if (isInitial) {
+ lazy.Assert.equal(data.partner.partnerNames.length, 0);
+ } else {
+ lazy.Assert.ok(data.partner.partnerNames.includes(PARTNER_NAME));
+ }
+ },
+
+ checkGfxAdapter(data) {
+ const EXPECTED_ADAPTER_FIELDS_TYPES = {
+ description: "string",
+ vendorID: "string",
+ deviceID: "string",
+ subsysID: "string",
+ RAM: "number",
+ driver: "string",
+ driverVendor: "string",
+ driverVersion: "string",
+ driverDate: "string",
+ GPUActive: "boolean",
+ };
+
+ for (let f in EXPECTED_ADAPTER_FIELDS_TYPES) {
+ lazy.Assert.ok(f in data, f + " must be available.");
+
+ if (data[f]) {
+ // Since we have a non-null value, check if it has the correct type.
+ lazy.Assert.equal(
+ typeof data[f],
+ EXPECTED_ADAPTER_FIELDS_TYPES[f],
+ f + " must have the correct type."
+ );
+ }
+ }
+ },
+
+ checkSystemSection(data, assertProcessData) {
+ const EXPECTED_FIELDS = [
+ "memoryMB",
+ "cpu",
+ "os",
+ "hdd",
+ "gfx",
+ "appleModelId",
+ ];
+
+ lazy.Assert.ok(
+ "system" in data,
+ "There must be a system section in Environment."
+ );
+
+ // Make sure we have all the top level sections and fields.
+ for (let f of EXPECTED_FIELDS) {
+ lazy.Assert.ok(f in data.system, f + " must be available.");
+ }
+
+ lazy.Assert.ok(
+ Number.isFinite(data.system.memoryMB),
+ "MemoryMB must be a number."
+ );
+
+ if (assertProcessData) {
+ if (gIsWindows || gIsMac || gIsLinux) {
+ let EXTRA_CPU_FIELDS = [
+ "cores",
+ "model",
+ "family",
+ "stepping",
+ "l2cacheKB",
+ "l3cacheKB",
+ "speedMHz",
+ "vendor",
+ "name",
+ ];
+
+ for (let f of EXTRA_CPU_FIELDS) {
+ // Note this is testing TelemetryEnvironment.js only, not that the
+ // values are valid - null is the fallback.
+ lazy.Assert.ok(
+ f in data.system.cpu,
+ f + " must be available under cpu."
+ );
+ }
+
+ if (gIsWindows) {
+ lazy.Assert.equal(
+ typeof data.system.isWow64,
+ "boolean",
+ "isWow64 must be available on Windows and have the correct type."
+ );
+ lazy.Assert.equal(
+ typeof data.system.isWowARM64,
+ "boolean",
+ "isWowARM64 must be available on Windows and have the correct type."
+ );
+ lazy.Assert.equal(
+ typeof data.system.hasWinPackageId,
+ "boolean",
+ "hasWinPackageId must be available on Windows and have the correct type."
+ );
+ // This is only sent for Mozilla produced MSIX packages
+ lazy.Assert.ok(
+ !("winPackageFamilyName" in data.system) ||
+ data.system.winPackageFamilyName === null ||
+ typeof data.system.winPackageFamilyName === "string",
+ "winPackageFamilyName must be a string if non null"
+ );
+ lazy.Assert.ok(
+ "virtualMaxMB" in data.system,
+ "virtualMaxMB must be available."
+ );
+ lazy.Assert.ok(
+ Number.isFinite(data.system.virtualMaxMB),
+ "virtualMaxMB must be a number."
+ );
+
+ for (let f of [
+ "count",
+ "model",
+ "family",
+ "stepping",
+ "l2cacheKB",
+ "l3cacheKB",
+ "speedMHz",
+ ]) {
+ lazy.Assert.ok(
+ Number.isFinite(data.system.cpu[f]),
+ f + " must be a number if non null."
+ );
+ }
+ }
+
+ // These should be numbers if they are not null
+ for (let f of [
+ "count",
+ "model",
+ "family",
+ "stepping",
+ "l2cacheKB",
+ "l3cacheKB",
+ "speedMHz",
+ ]) {
+ lazy.Assert.ok(
+ !(f in data.system.cpu) ||
+ data.system.cpu[f] === null ||
+ Number.isFinite(data.system.cpu[f]),
+ f + " must be a number if non null."
+ );
+ }
+
+ // We insist these are available
+ for (let f of ["cores"]) {
+ lazy.Assert.ok(
+ !(f in data.system.cpu) || Number.isFinite(data.system.cpu[f]),
+ f + " must be a number if non null."
+ );
+ }
+ }
+ }
+
+ let cpuData = data.system.cpu;
+
+ lazy.Assert.ok(
+ Array.isArray(cpuData.extensions),
+ "CPU extensions must be available."
+ );
+
+ let osData = data.system.os;
+ lazy.Assert.ok(this.checkNullOrString(osData.name));
+ lazy.Assert.ok(this.checkNullOrString(osData.version));
+ lazy.Assert.ok(this.checkNullOrString(osData.locale));
+
+ // Service pack is only available on Windows.
+ if (gIsWindows) {
+ lazy.Assert.ok(
+ Number.isFinite(osData.servicePackMajor),
+ "ServicePackMajor must be a number."
+ );
+ lazy.Assert.ok(
+ Number.isFinite(osData.servicePackMinor),
+ "ServicePackMinor must be a number."
+ );
+ if ("windowsBuildNumber" in osData) {
+ // This might not be available on all Windows platforms.
+ lazy.Assert.ok(
+ Number.isFinite(osData.windowsBuildNumber),
+ "windowsBuildNumber must be a number."
+ );
+ }
+ if ("windowsUBR" in osData) {
+ // This might not be available on all Windows platforms.
+ lazy.Assert.ok(
+ osData.windowsUBR === null || Number.isFinite(osData.windowsUBR),
+ "windowsUBR must be null or a number."
+ );
+ }
+ } else if (gIsAndroid) {
+ lazy.Assert.ok(this.checkNullOrString(osData.kernelVersion));
+ }
+
+ for (let disk of EXPECTED_HDD_FIELDS) {
+ lazy.Assert.ok(this.checkNullOrString(data.system.hdd[disk].model));
+ lazy.Assert.ok(this.checkNullOrString(data.system.hdd[disk].revision));
+ lazy.Assert.ok(this.checkNullOrString(data.system.hdd[disk].type));
+ }
+
+ let gfxData = data.system.gfx;
+ lazy.Assert.ok("D2DEnabled" in gfxData);
+ lazy.Assert.ok("DWriteEnabled" in gfxData);
+ lazy.Assert.ok("Headless" in gfxData);
+ lazy.Assert.ok("EmbeddedInFirefoxReality" in gfxData);
+ // DWriteVersion is disabled due to main thread jank and will be enabled
+ // again as part of bug 1154500.
+ // Assert.ok("DWriteVersion" in gfxData);
+ if (gIsWindows) {
+ lazy.Assert.equal(typeof gfxData.D2DEnabled, "boolean");
+ lazy.Assert.equal(typeof gfxData.DWriteEnabled, "boolean");
+ lazy.Assert.equal(typeof gfxData.EmbeddedInFirefoxReality, "boolean");
+ // As above, will be enabled again as part of bug 1154500.
+ // Assert.ok(this.checkString(gfxData.DWriteVersion));
+ }
+
+ lazy.Assert.ok("adapters" in gfxData);
+ lazy.Assert.ok(
+ !!gfxData.adapters.length,
+ "There must be at least one GFX adapter."
+ );
+ for (let adapter of gfxData.adapters) {
+ this.checkGfxAdapter(adapter);
+ }
+ lazy.Assert.equal(typeof gfxData.adapters[0].GPUActive, "boolean");
+ lazy.Assert.ok(
+ gfxData.adapters[0].GPUActive,
+ "The first GFX adapter must be active."
+ );
+
+ lazy.Assert.ok(Array.isArray(gfxData.monitors));
+ if (gIsWindows || gIsMac || gIsLinux) {
+ lazy.Assert.ok(
+ gfxData.monitors.length >= 1,
+ "There is at least one monitor."
+ );
+ lazy.Assert.equal(typeof gfxData.monitors[0].screenWidth, "number");
+ lazy.Assert.equal(typeof gfxData.monitors[0].screenHeight, "number");
+ if (gIsWindows) {
+ lazy.Assert.equal(typeof gfxData.monitors[0].refreshRate, "number");
+ lazy.Assert.equal(typeof gfxData.monitors[0].pseudoDisplay, "boolean");
+ }
+ if (gIsMac) {
+ lazy.Assert.equal(typeof gfxData.monitors[0].scale, "number");
+ }
+ }
+
+ lazy.Assert.equal(typeof gfxData.features, "object");
+ lazy.Assert.equal(typeof gfxData.features.compositor, "string");
+
+ lazy.Assert.equal(typeof gfxData.features.gpuProcess, "object");
+ lazy.Assert.equal(typeof gfxData.features.gpuProcess.status, "string");
+
+ try {
+ // If we've not got nsIGfxInfoDebug, then this will throw and stop us doing
+ // this test.
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
+ Ci.nsIGfxInfoDebug
+ );
+
+ if (gIsWindows || gIsMac) {
+ lazy.Assert.equal(GFX_VENDOR_ID, gfxData.adapters[0].vendorID);
+ lazy.Assert.equal(GFX_DEVICE_ID, gfxData.adapters[0].deviceID);
+ }
+
+ let features = gfxInfo.getFeatures();
+ lazy.Assert.equal(features.compositor, gfxData.features.compositor);
+ lazy.Assert.equal(
+ features.gpuProcess.status,
+ gfxData.features.gpuProcess.status
+ );
+ lazy.Assert.equal(features.opengl, gfxData.features.opengl);
+ lazy.Assert.equal(features.webgl, gfxData.features.webgl);
+ } catch (e) {}
+
+ if (gIsMac) {
+ lazy.Assert.ok(this.checkString(data.system.appleModelId));
+ } else {
+ lazy.Assert.ok(this.checkNullOrString(data.system.appleModelId));
+ }
+
+ // This feature is only available on Windows 8+
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
+ lazy.Assert.ok(
+ "sec" in data.system,
+ "sec must be available under data.system"
+ );
+
+ let SEC_FIELDS = ["antivirus", "antispyware", "firewall"];
+ for (let f of SEC_FIELDS) {
+ lazy.Assert.ok(
+ f in data.system.sec,
+ f + " must be available under data.system.sec"
+ );
+
+ let value = data.system.sec[f];
+ // value is null on Windows Server
+ lazy.Assert.ok(
+ value === null || Array.isArray(value),
+ f + " must be either null or an array"
+ );
+ if (Array.isArray(value)) {
+ for (let product of value) {
+ lazy.Assert.equal(
+ typeof product,
+ "string",
+ "Each element of " + f + " must be a string"
+ );
+ }
+ }
+ }
+ }
+ },
+
+ checkActiveAddon(data, partialRecord) {
+ let signedState = "number";
+ // system add-ons have an undefined signState
+ if (data.isSystem) {
+ signedState = "undefined";
+ }
+
+ const EXPECTED_ADDON_FIELDS_TYPES = {
+ version: "string",
+ scope: "number",
+ type: "string",
+ updateDay: "number",
+ isSystem: "boolean",
+ isWebExtension: "boolean",
+ multiprocessCompatible: "boolean",
+ };
+
+ const FULL_ADDON_FIELD_TYPES = {
+ blocklisted: "boolean",
+ name: "string",
+ userDisabled: "boolean",
+ appDisabled: "boolean",
+ foreignInstall: "boolean",
+ hasBinaryComponents: "boolean",
+ installDay: "number",
+ signedState,
+ };
+
+ let fields = EXPECTED_ADDON_FIELDS_TYPES;
+ if (!partialRecord) {
+ fields = Object.assign({}, fields, FULL_ADDON_FIELD_TYPES);
+ }
+
+ for (let [name, type] of Object.entries(fields)) {
+ lazy.Assert.ok(name in data, name + " must be available.");
+ lazy.Assert.equal(
+ typeof data[name],
+ type,
+ name + " must have the correct type."
+ );
+ }
+
+ if (!partialRecord) {
+ // We check "description" separately, as it can be null.
+ lazy.Assert.ok(this.checkNullOrString(data.description));
+ }
+ },
+
+ checkTheme(data) {
+ const EXPECTED_THEME_FIELDS_TYPES = {
+ id: "string",
+ blocklisted: "boolean",
+ name: "string",
+ userDisabled: "boolean",
+ appDisabled: "boolean",
+ version: "string",
+ scope: "number",
+ foreignInstall: "boolean",
+ installDay: "number",
+ updateDay: "number",
+ };
+
+ for (let f in EXPECTED_THEME_FIELDS_TYPES) {
+ lazy.Assert.ok(f in data, f + " must be available.");
+ lazy.Assert.equal(
+ typeof data[f],
+ EXPECTED_THEME_FIELDS_TYPES[f],
+ f + " must have the correct type."
+ );
+ }
+
+ // We check "description" separately, as it can be null.
+ lazy.Assert.ok(this.checkNullOrString(data.description));
+ },
+
+ checkActiveGMPlugin(data) {
+ // GMP plugin version defaults to null until GMPDownloader runs to update it.
+ if (data.version) {
+ lazy.Assert.equal(typeof data.version, "string");
+ }
+ lazy.Assert.equal(typeof data.userDisabled, "boolean");
+ lazy.Assert.equal(typeof data.applyBackgroundUpdates, "number");
+ },
+
+ checkAddonsSection(data, expectBrokenAddons, partialAddonsRecords) {
+ const EXPECTED_FIELDS = ["activeAddons", "theme", "activeGMPlugins"];
+
+ lazy.Assert.ok(
+ "addons" in data,
+ "There must be an addons section in Environment."
+ );
+ for (let f of EXPECTED_FIELDS) {
+ lazy.Assert.ok(f in data.addons, f + " must be available.");
+ }
+
+ // Check the active addons, if available.
+ if (!expectBrokenAddons) {
+ let activeAddons = data.addons.activeAddons;
+ for (let addon in activeAddons) {
+ this.checkActiveAddon(activeAddons[addon], partialAddonsRecords);
+ }
+ }
+
+ // Check "theme" structure.
+ if (Object.keys(data.addons.theme).length !== 0) {
+ this.checkTheme(data.addons.theme);
+ }
+
+ // Check active GMPlugins
+ let activeGMPlugins = data.addons.activeGMPlugins;
+ for (let gmPlugin in activeGMPlugins) {
+ this.checkActiveGMPlugin(activeGMPlugins[gmPlugin]);
+ }
+ },
+
+ checkEnvironmentData(data, options = {}) {
+ const {
+ isInitial = false,
+ expectBrokenAddons = false,
+ assertProcessData = false,
+ } = options;
+
+ this.checkBuildSection(data);
+ this.checkSettingsSection(data);
+ this.checkProfileSection(data);
+ this.checkPartnerSection(data, isInitial);
+ this.checkSystemSection(data, assertProcessData);
+ this.checkAddonsSection(data, expectBrokenAddons);
+ },
+};
diff --git a/toolkit/components/telemetry/tests/unit/data/search-extensions/engines.json b/toolkit/components/telemetry/tests/unit/data/search-extensions/engines.json
new file mode 100644
index 0000000000..d745558b73
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/data/search-extensions/engines.json
@@ -0,0 +1,14 @@
+{
+ "data": [
+ {
+ "webExtension": {
+ "id": "telemetrySearchIdentifier@search.mozilla.org"
+ },
+ "appliesTo": [
+ {
+ "included": { "everywhere": true }
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/telemetry/tests/unit/data/search-extensions/telemetrySearchIdentifier/manifest.json b/toolkit/components/telemetry/tests/unit/data/search-extensions/telemetrySearchIdentifier/manifest.json
new file mode 100644
index 0000000000..b0b949b635
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/data/search-extensions/telemetrySearchIdentifier/manifest.json
@@ -0,0 +1,29 @@
+{
+ "name": "telemetrySearchIdentifier",
+ "manifest_version": 2,
+ "version": "1.0",
+ "description": "telemetrySearchIdentifier",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "telemetrySearchIdentifier@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "telemetrySearchIdentifier",
+ "search_url": "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB",
+ "params": [
+ {
+ "name": "search",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "sourceId",
+ "value": "Mozilla-search"
+ }
+ ],
+ "suggest_url": "https://ar.wikipedia.org/w/api.php?action=opensearch&search={searchTerms}"
+ }
+ }
+}
diff --git a/toolkit/components/telemetry/tests/unit/engine.xml b/toolkit/components/telemetry/tests/unit/engine.xml
new file mode 100644
index 0000000000..2304fcdd7b
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/engine.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-telemetry</ShortName>
+<Url type="text/html" method="GET" template="http://www.example.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/telemetry/tests/unit/file_UninstallPing.worker.js b/toolkit/components/telemetry/tests/unit/file_UninstallPing.worker.js
new file mode 100644
index 0000000000..fcc8007a2f
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/file_UninstallPing.worker.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/. */
+
+/* eslint-env mozilla/chrome-worker */
+
+/* import-globals-from /toolkit/components/workerloader/require.js */
+importScripts("resource://gre/modules/workers/require.js");
+
+const PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
+
+const Agent = {
+ _file: null,
+ open(path) {
+ this._file = IOUtils.openFileForSyncReading(path);
+ },
+ close() {
+ this._file.close();
+ },
+};
+
+// This boilerplate connects the PromiseWorker to the Agent so
+// that messages from the main thread map to methods on the
+// Agent.
+const worker = new PromiseWorker.AbstractWorker();
+worker.dispatch = function (method, args = []) {
+ return Agent[method](...args);
+};
+worker.postMessage = function (result, ...transfers) {
+ self.postMessage(result, ...transfers);
+};
+worker.close = function () {
+ self.close();
+};
+self.addEventListener("message", msg => worker.handleMessage(msg));
+self.addEventListener("unhandledrejection", function (error) {
+ throw error.reason;
+});
diff --git a/toolkit/components/telemetry/tests/unit/head.js b/toolkit/components/telemetry/tests/unit/head.js
new file mode 100644
index 0000000000..7088dd2227
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -0,0 +1,587 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ Log: "resource://gre/modules/Log.sys.mjs",
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+ TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
+ TelemetryScheduler: "resource://gre/modules/TelemetryScheduler.sys.mjs",
+ TelemetrySend: "resource://gre/modules/TelemetrySend.sys.mjs",
+ TelemetryStorage: "resource://gre/modules/TelemetryStorage.sys.mjs",
+ TelemetryUtils: "resource://gre/modules/TelemetryUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HttpServer: "resource://testing-common/httpd.js",
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+const gIsWindows = AppConstants.platform == "win";
+const gIsMac = AppConstants.platform == "macosx";
+const gIsAndroid = AppConstants.platform == "android";
+const gIsLinux = AppConstants.platform == "linux";
+
+// Desktop Firefox, ie. not mobile Firefox or Thunderbird.
+const gIsFirefox = AppConstants.MOZ_APP_NAME == "firefox";
+
+const Telemetry = Services.telemetry;
+
+const MILLISECONDS_PER_MINUTE = 60 * 1000;
+const MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE;
+const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR;
+
+const UUID_REGEX =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+
+var gGlobalScope = this;
+
+const PingServer = {
+ _httpServer: null,
+ _started: false,
+ _defers: [PromiseUtils.defer()],
+ _currentDeferred: 0,
+ _logger: null,
+
+ get port() {
+ return this._httpServer.identity.primaryPort;
+ },
+
+ get host() {
+ return this._httpServer.identity.primaryHost;
+ },
+
+ get started() {
+ return this._started;
+ },
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(
+ "Toolkit.Telemetry",
+ "PingServer::"
+ );
+ }
+
+ return this._logger;
+ },
+
+ registerPingHandler(handler) {
+ const wrapped = wrapWithExceptionHandler(handler);
+ this._httpServer.registerPrefixHandler("/submit/telemetry/", wrapped);
+ },
+
+ resetPingHandler() {
+ this.registerPingHandler((request, response) => {
+ let r = request;
+ this._log.trace(
+ `defaultPingHandler() - ${r.method} ${r.scheme}://${r.host}:${r.port}${r.path}`
+ );
+ let deferred = this._defers[this._defers.length - 1];
+ this._defers.push(PromiseUtils.defer());
+ deferred.resolve(request);
+ });
+ },
+
+ start() {
+ this._httpServer = new HttpServer();
+ this._httpServer.start(-1);
+ this._started = true;
+ this.clearRequests();
+ this.resetPingHandler();
+ },
+
+ stop() {
+ return new Promise(resolve => {
+ this._httpServer.stop(resolve);
+ this._started = false;
+ });
+ },
+
+ clearRequests() {
+ this._defers = [PromiseUtils.defer()];
+ this._currentDeferred = 0;
+ },
+
+ promiseNextRequest() {
+ const deferred = this._defers[this._currentDeferred++];
+ // Send the ping to the consumer on the next tick, so that the completion gets
+ // signaled to Telemetry.
+ return new Promise(r =>
+ Services.tm.dispatchToMainThread(() => r(deferred.promise))
+ );
+ },
+
+ promiseNextPing() {
+ return this.promiseNextRequest().then(request =>
+ decodeRequestPayload(request)
+ );
+ },
+
+ async promiseNextRequests(count) {
+ let results = [];
+ for (let i = 0; i < count; ++i) {
+ results.push(await this.promiseNextRequest());
+ }
+
+ return results;
+ },
+
+ promiseNextPings(count) {
+ return this.promiseNextRequests(count).then(requests => {
+ return Array.from(requests, decodeRequestPayload);
+ });
+ },
+};
+
+/**
+ * Decode the payload of an HTTP request into a ping.
+ * @param {Object} request The data representing an HTTP request (nsIHttpRequest).
+ * @return {Object} The decoded ping payload.
+ */
+function decodeRequestPayload(request) {
+ let s = request.bodyInputStream;
+ let payload = null;
+
+ if (
+ request.hasHeader("content-encoding") &&
+ request.getHeader("content-encoding") == "gzip"
+ ) {
+ let observer = {
+ buffer: "",
+ onStreamComplete(loader, context, status, length, result) {
+ // String.fromCharCode can only deal with 500,000 characters
+ // at a time, so chunk the result into parts of that size.
+ const chunkSize = 500000;
+ for (let offset = 0; offset < result.length; offset += chunkSize) {
+ this.buffer += String.fromCharCode.apply(
+ String,
+ result.slice(offset, offset + chunkSize)
+ );
+ }
+ },
+ };
+
+ let scs = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
+ Ci.nsIStreamLoader
+ );
+ listener.init(observer);
+ let converter = scs.asyncConvertData(
+ "gzip",
+ "uncompressed",
+ listener,
+ null
+ );
+ converter.onStartRequest(null, null);
+ converter.onDataAvailable(null, s, 0, s.available());
+ converter.onStopRequest(null, null, null);
+ let unicodeConverter = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ unicodeConverter.charset = "UTF-8";
+ let utf8string = unicodeConverter.ConvertToUnicode(observer.buffer);
+ utf8string += unicodeConverter.Finish();
+ payload = JSON.parse(utf8string);
+ } else {
+ let bytes = NetUtil.readInputStream(s, s.available());
+ payload = JSON.parse(new TextDecoder().decode(bytes));
+ }
+
+ if (payload && "clientId" in payload) {
+ // Check for canary value
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ payload.clientId,
+ `Known clientId shouldn't appear in a "${payload.type}" ping on the server.`
+ );
+ }
+
+ return payload;
+}
+
+function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {
+ const PING_FORMAT_VERSION = 4;
+ const MANDATORY_PING_FIELDS = [
+ "type",
+ "id",
+ "creationDate",
+ "version",
+ "application",
+ "payload",
+ ];
+
+ const APPLICATION_TEST_DATA = {
+ buildId: gAppInfo.appBuildID,
+ name: APP_NAME,
+ version: APP_VERSION,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ // Check that the ping contains all the mandatory fields.
+ for (let f of MANDATORY_PING_FIELDS) {
+ Assert.ok(f in aPing, f + " must be available.");
+ }
+
+ Assert.equal(aPing.type, aType, "The ping must have the correct type.");
+ Assert.equal(
+ aPing.version,
+ PING_FORMAT_VERSION,
+ "The ping must have the correct version."
+ );
+
+ // Test the application section.
+ for (let f in APPLICATION_TEST_DATA) {
+ Assert.equal(
+ aPing.application[f],
+ APPLICATION_TEST_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // We can't check the values for channel and architecture. Just make
+ // sure they are in.
+ Assert.ok(
+ "architecture" in aPing.application,
+ "The application section must have an architecture field."
+ );
+ Assert.ok(
+ "channel" in aPing.application,
+ "The application section must have a channel field."
+ );
+
+ // Check the clientId and environment fields, as needed.
+ Assert.equal("clientId" in aPing, aHasClientId);
+ Assert.equal("environment" in aPing, aHasEnvironment);
+}
+
+function wrapWithExceptionHandler(f) {
+ function wrapper(...args) {
+ try {
+ f(...args);
+ } catch (ex) {
+ if (typeof ex != "object") {
+ throw ex;
+ }
+ dump("Caught exception: " + ex.message + "\n");
+ dump(ex.stack);
+ do_test_finished();
+ }
+ }
+ return wrapper;
+}
+
+async function loadAddonManager(...args) {
+ AddonTestUtils.init(gGlobalScope);
+ AddonTestUtils.overrideCertDB();
+ createAppInfo(...args);
+
+ // As we're not running in application, we need to setup the features directory
+ // used by system add-ons.
+ const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true);
+ AddonTestUtils.registerDirectory("XREAppFeat", distroDir);
+ await AddonTestUtils.overrideBuiltIns({
+ system: ["tel-system-xpi@tests.mozilla.org"],
+ });
+ return AddonTestUtils.promiseStartupManager();
+}
+
+function finishAddonManagerStartup() {
+ Services.obs.notifyObservers(null, "test-load-xpi-database");
+}
+
+var gAppInfo = null;
+
+function createAppInfo(
+ ID = APP_ID,
+ name = APP_NAME,
+ version = APP_VERSION,
+ platformVersion = PLATFORM_VERSION
+) {
+ AddonTestUtils.createAppInfo(ID, name, version, platformVersion);
+ gAppInfo = AddonTestUtils.appInfo;
+}
+
+// Fake the timeout functions for the TelemetryScheduler.
+function fakeSchedulerTimer(set, clear) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryScheduler.sys.mjs"
+ );
+ Policy.setSchedulerTickTimeout = set;
+ Policy.clearSchedulerTickTimeout = clear;
+}
+
+/* global TelemetrySession:false, TelemetryEnvironment:false, TelemetryController:false,
+ TelemetryStorage:false, TelemetrySend:false, TelemetryReportingPolicy:false
+ */
+
+/**
+ * Fake the current date.
+ * This passes all received arguments to a new Date constructor and
+ * uses the resulting date to fake the time in Telemetry modules.
+ *
+ * @return Date The new faked date.
+ */
+function fakeNow(...args) {
+ const date = new Date(...args);
+ const modules = [
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+ ),
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+ ),
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryControllerParent.sys.mjs"
+ ),
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+ ),
+ ChromeUtils.importESModule("resource://gre/modules/TelemetrySend.sys.mjs"),
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+ ),
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryScheduler.sys.mjs"
+ ),
+ ];
+
+ for (let m of modules) {
+ m.Policy.now = () => date;
+ }
+
+ return new Date(date);
+}
+
+function fakeMonotonicNow(ms) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+ );
+ Policy.monotonicNow = () => ms;
+ return ms;
+}
+
+// Fake the timeout functions for TelemetryController sending.
+function fakePingSendTimer(set, clear) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+ );
+ let obj = Cu.cloneInto({ set, clear }, TelemetrySend, {
+ cloneFunctions: true,
+ });
+ Policy.setSchedulerTickTimeout = obj.set;
+ Policy.clearSchedulerTickTimeout = obj.clear;
+}
+
+function fakeMidnightPingFuzzingDelay(delayMs) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+ );
+ Policy.midnightPingFuzzingDelay = () => delayMs;
+}
+
+function fakeGeneratePingId(func) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryControllerParent.sys.mjs"
+ );
+ Policy.generatePingId = func;
+}
+
+function fakeCachedClientId(uuid) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryControllerParent.sys.mjs"
+ );
+ Policy.getCachedClientID = () => uuid;
+}
+
+// Fake the gzip compression for the next ping to be sent out
+// and immediately reset to the original function.
+function fakeGzipCompressStringForNextPing(length) {
+ const { Policy, gzipCompressString } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+ );
+ let largePayload = generateString(length);
+ Policy.gzipCompressString = data => {
+ Policy.gzipCompressString = gzipCompressString;
+ return largePayload;
+ };
+}
+
+function fakeIntlReady() {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+ );
+ Policy._intlLoaded = true;
+ // Dispatch the observer event in case the promise has been registered already.
+ Services.obs.notifyObservers(null, "browser-delayed-startup-finished");
+}
+
+// Override the uninstall ping file names
+function fakeUninstallPingPath(aPathFcn) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+ );
+ Policy.getUninstallPingPath =
+ aPathFcn ||
+ (id => ({
+ directory: new FileUtils.File(PathUtils.profileDir),
+ file: `uninstall_ping_0123456789ABCDEF_${id}.json`,
+ }));
+}
+
+// Return a date that is |offset| ms in the future from |date|.
+function futureDate(date, offset) {
+ return new Date(date.getTime() + offset);
+}
+
+function truncateToDays(aMsec) {
+ return Math.floor(aMsec / MILLISECONDS_PER_DAY);
+}
+
+// Returns a promise that resolves to true when the passed promise rejects,
+// false otherwise.
+function promiseRejects(promise) {
+ return promise.then(
+ () => false,
+ () => true
+ );
+}
+
+// Generates a random string of at least a specific length.
+function generateRandomString(length) {
+ let string = "";
+
+ while (string.length < length) {
+ string += Math.random().toString(36);
+ }
+
+ return string.substring(0, length);
+}
+
+function generateString(length) {
+ return new Array(length + 1).join("a");
+}
+
+// Short-hand for retrieving the histogram with that id.
+function getHistogram(histogramId) {
+ return Telemetry.getHistogramById(histogramId);
+}
+
+// Short-hand for retrieving the snapshot of the Histogram with that id.
+function getSnapshot(histogramId) {
+ return Telemetry.getHistogramById(histogramId).snapshot();
+}
+
+// Helper for setting an empty list of Environment preferences to watch.
+function setEmptyPrefWatchlist() {
+ const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+ );
+ return TelemetryEnvironment.onInitialized().then(() =>
+ TelemetryEnvironment.testWatchPreferences(new Map())
+ );
+}
+
+if (runningInParent) {
+ // Set logging preferences for all the tests.
+ Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
+ // Telemetry archiving should be on.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.ArchiveEnabled, true);
+ // Telemetry xpcshell tests cannot show the infobar.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ true
+ );
+ // FHR uploads should be enabled.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+ // Many tests expect the shutdown and the new-profile to not be sent on shutdown
+ // and will fail if receive an unexpected ping. Let's globally disable these features:
+ // the relevant tests will enable these prefs when needed.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ false
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ );
+ Services.prefs.setBoolPref("toolkit.telemetry.newProfilePing.enabled", false);
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FirstShutdownPingEnabled,
+ false
+ );
+ // Turn off Health Ping submission.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.HealthPingEnabled,
+ false
+ );
+
+ // Speed up child process accumulations
+ Services.prefs.setIntPref(TelemetryUtils.Preferences.IPCBatchTimeout, 10);
+
+ // Non-unified Telemetry (e.g. Fennec on Android) needs the preference to be set
+ // in order to enable Telemetry.
+ if (Services.prefs.getBoolPref(TelemetryUtils.Preferences.Unified, false)) {
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.OverridePreRelease,
+ true
+ );
+ } else {
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.TelemetryEnabled,
+ true
+ );
+ }
+
+ fakePingSendTimer(
+ (callback, timeout) => {
+ Services.tm.dispatchToMainThread(() => callback());
+ },
+ () => {}
+ );
+
+ // This gets imported via fakeNow();
+ registerCleanupFunction(() => TelemetrySend.shutdown());
+}
+
+TelemetryController.testInitLogging();
+
+// Avoid timers interrupting test behavior.
+fakeSchedulerTimer(
+ () => {},
+ () => {}
+);
+// Make pind sending predictable.
+fakeMidnightPingFuzzingDelay(0);
+
+// Avoid using the directory service, which is not registered in some tests.
+fakeUninstallPingPath();
+
+const PLATFORM_VERSION = "1.9.2";
+const APP_VERSION = "1";
+const APP_ID = "xpcshell@tests.mozilla.org";
+const APP_NAME = "XPCShell";
+
+const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC =
+ "distribution-customization-complete";
+
+const PLUGIN2_NAME = "Quicktime";
+const PLUGIN2_DESC = "A mock Quicktime plugin";
+const PLUGIN2_VERSION = "2.3";
+//
+// system add-ons are enabled at startup, so record date when the test starts
+const SYSTEM_ADDON_INSTALL_DATE = Date.now();
diff --git a/toolkit/components/telemetry/tests/unit/testNoPDB32.dll b/toolkit/components/telemetry/tests/unit/testNoPDB32.dll
new file mode 100644
index 0000000000..e7f9febc4b
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testNoPDB32.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/testNoPDB64.dll b/toolkit/components/telemetry/tests/unit/testNoPDB64.dll
new file mode 100644
index 0000000000..19f95c98ed
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testNoPDB64.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/testNoPDBAArch64.dll b/toolkit/components/telemetry/tests/unit/testNoPDBAArch64.dll
new file mode 100755
index 0000000000..ecfff07036
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testNoPDBAArch64.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/testUnicodePDB32.dll b/toolkit/components/telemetry/tests/unit/testUnicodePDB32.dll
new file mode 100644
index 0000000000..d3eec65ea5
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testUnicodePDB32.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/testUnicodePDB64.dll b/toolkit/components/telemetry/tests/unit/testUnicodePDB64.dll
new file mode 100644
index 0000000000..c11f8453de
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testUnicodePDB64.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/testUnicodePDBAArch64.dll b/toolkit/components/telemetry/tests/unit/testUnicodePDBAArch64.dll
new file mode 100755
index 0000000000..a892a84315
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testUnicodePDBAArch64.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/test_ChildEvents.js b/toolkit/components/telemetry/tests/unit/test_ChildEvents.js
new file mode 100644
index 0000000000..392febd5dc
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ChildEvents.js
@@ -0,0 +1,222 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+const RECORDED_CONTENT_EVENTS = [
+ ["telemetry.test", "content_only", "object1"],
+ ["telemetry.test", "main_and_content", "object1"],
+ ["telemetry.test", "content_only", "object1", "some value"],
+ ["telemetry.test", "content_only", "object1", null, { foo: "x", bar: "y" }],
+ [
+ "telemetry.test",
+ "content_only",
+ "object1",
+ "some value",
+ { foo: "x", bar: "y" },
+ ],
+];
+
+const UNRECORDED_CONTENT_EVENTS = [["telemetry.test", "main_only", "object1"]];
+
+const RECORDED_PARENT_EVENTS = [
+ ["telemetry.test", "main_and_content", "object1"],
+ ["telemetry.test", "main_only", "object1"],
+];
+
+const UNRECORDED_PARENT_EVENTS = [
+ ["telemetry.test", "content_only", "object1"],
+];
+
+const RECORDED_DYNAMIC_EVENTS = [
+ ["telemetry.test.dynamic", "test1", "object1"],
+ ["telemetry.test.dynamic", "test2", "object1"],
+];
+
+function run_child_test() {
+ // Record some events in the "content" process.
+ RECORDED_CONTENT_EVENTS.forEach(e => Telemetry.recordEvent(...e));
+ // These events should not be recorded for the content process.
+ UNRECORDED_CONTENT_EVENTS.forEach(e => Telemetry.recordEvent(...e));
+ // Record some dynamic events from the content process.
+ RECORDED_DYNAMIC_EVENTS.forEach(e => Telemetry.recordEvent(...e));
+}
+
+/**
+ * This function waits until content events are reported into the
+ * events snapshot.
+ */
+async function waitForContentEvents() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ return (
+ Object.keys(snapshot).includes("content") &&
+ Object.keys(snapshot).includes("dynamic")
+ );
+ });
+}
+
+add_task(async function () {
+ if (!runningInParent) {
+ TelemetryController.testSetupContent();
+ run_child_test();
+ do_send_remote_message(MESSAGE_CHILD_TEST_DONE);
+ return;
+ }
+
+ // Setup.
+ do_get_profile(true);
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ await TelemetryController.testSetup();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ // Enable recording for the test event category.
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+
+ // Register dynamic test events.
+ Telemetry.registerEvents("telemetry.test.dynamic", {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ // Event with extra_keys.
+ test2: {
+ methods: ["test2", "test2b"],
+ objects: ["object1"],
+ extra_keys: ["key1", "key2"],
+ },
+ });
+
+ // Run test in child, don't wait for it to finish: just wait for the
+ // MESSAGE_CHILD_TEST_DONE.
+ const timestampBeforeChildEvents = Telemetry.msSinceProcessStart();
+ run_test_in_child("test_ChildEvents.js");
+ await do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
+
+ // Once events are set by the content process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ await waitForContentEvents();
+ const timestampAfterChildEvents = Telemetry.msSinceProcessStart();
+
+ // Also record some events in the parent.
+ RECORDED_PARENT_EVENTS.forEach(e => Telemetry.recordEvent(...e));
+ UNRECORDED_PARENT_EVENTS.forEach(e => Telemetry.recordEvent(...e));
+
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+
+ Assert.ok("parent" in snapshot, "Should have main process section");
+ Assert.ok(
+ !!snapshot.parent.length,
+ "Main process section should have events."
+ );
+ Assert.ok("content" in snapshot, "Should have child process section");
+ Assert.ok(
+ !!snapshot.content.length,
+ "Child process section should have events."
+ );
+ Assert.ok("dynamic" in snapshot, "Should have dynamic process section");
+ Assert.ok(
+ !!snapshot.dynamic.length,
+ "Dynamic process section should have events."
+ );
+
+ // Check that the expected events are present from the content process.
+ let contentEvents = snapshot.content.map(e => e.slice(1));
+ Assert.equal(
+ contentEvents.length,
+ RECORDED_CONTENT_EVENTS.length,
+ "Should match expected event count."
+ );
+ for (let i = 0; i < RECORDED_CONTENT_EVENTS.length; ++i) {
+ Assert.deepEqual(
+ contentEvents[i],
+ RECORDED_CONTENT_EVENTS[i],
+ "Should have recorded expected event."
+ );
+ }
+
+ // Check that the expected events are present from the parent process.
+ let parentEvents = snapshot.parent.map(e => e.slice(1));
+ Assert.equal(
+ parentEvents.length,
+ RECORDED_PARENT_EVENTS.length,
+ "Should match expected event count."
+ );
+ for (let i = 0; i < RECORDED_PARENT_EVENTS.length; ++i) {
+ Assert.deepEqual(
+ parentEvents[i],
+ RECORDED_PARENT_EVENTS[i],
+ "Should have recorded expected event."
+ );
+ }
+
+ // Check that the expected dynamic events are present.
+ let dynamicEvents = snapshot.dynamic.map(e => e.slice(1));
+ Assert.equal(
+ dynamicEvents.length,
+ RECORDED_DYNAMIC_EVENTS.length,
+ "Should match expected event count."
+ );
+ for (let i = 0; i < RECORDED_DYNAMIC_EVENTS.length; ++i) {
+ Assert.deepEqual(
+ dynamicEvents[i],
+ RECORDED_DYNAMIC_EVENTS[i],
+ "Should have recorded expected event."
+ );
+ }
+
+ // Check that the event timestamps are in the expected ranges.
+ let contentTimestamps = snapshot.content.map(e => e[0]);
+ let parentTimestamps = snapshot.parent.map(e => e[0]);
+
+ Assert.ok(
+ contentTimestamps.every(
+ ts =>
+ ts > Math.floor(timestampBeforeChildEvents) &&
+ ts < timestampAfterChildEvents
+ ),
+ "All content event timestamps should be in the expected time range."
+ );
+ Assert.ok(
+ parentTimestamps.every(ts => ts >= Math.floor(timestampAfterChildEvents)),
+ "All parent event timestamps should be in the expected time range."
+ );
+
+ // Make sure all events are cleared from storage properly.
+ snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ );
+ Assert.greaterOrEqual(
+ Object.keys(snapshot).length,
+ 2,
+ "Should have events from at least two processes."
+ );
+ snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ );
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should have cleared all events from storage."
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
new file mode 100644
index 0000000000..5da3fc6647
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
@@ -0,0 +1,333 @@
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+function run_child_test() {
+ // Setup histograms with some fixed values.
+ let flagHist = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
+ flagHist.add(1);
+ let countHist = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", false);
+ countHist.add();
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", true);
+ countHist.add();
+ countHist.add();
+ let categHist = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL");
+ categHist.add("Label2");
+ categHist.add("Label3");
+
+ let flagKeyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG");
+ flagKeyed.add("a", 1);
+ flagKeyed.add("b", 1);
+ let countKeyed = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_COUNT"
+ );
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT", false);
+ countKeyed.add("a");
+ countKeyed.add("b");
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT", true);
+ countKeyed.add("a");
+ countKeyed.add("b");
+ countKeyed.add("b");
+
+ // Test record_in_processes
+ let contentLinear = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_CONTENT_PROCESS"
+ );
+ contentLinear.add(10);
+ let contentKeyed = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_CONTENT_PROCESS"
+ );
+ contentKeyed.add("content", 1);
+ let contentFlag = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_FLAG_CONTENT_PROCESS"
+ );
+ contentFlag.add(true);
+ let mainFlag = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG_MAIN_PROCESS");
+ mainFlag.add(true);
+ let allLinear = Telemetry.getHistogramById("TELEMETRY_TEST_ALL_PROCESSES");
+ allLinear.add(10);
+ let allChildLinear = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_ALL_CHILD_PROCESSES"
+ );
+ allChildLinear.add(10);
+
+ // Test snapshot APIs.
+ // Should be forbidden in content processes.
+ Assert.throws(
+ () => Telemetry.getHistogramById("TELEMETRY_TEST_COUNT").snapshot(),
+ /Histograms can only be snapshotted in the parent process/,
+ "Snapshotting should be forbidden in the content process"
+ );
+
+ Assert.throws(
+ () =>
+ Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT").snapshot(),
+ /Keyed histograms can only be snapshotted in the parent process/,
+ "Snapshotting should be forbidden in the content process"
+ );
+
+ Assert.throws(
+ () => Telemetry.getHistogramById("TELEMETRY_TEST_COUNT").clear(),
+ /Histograms can only be cleared in the parent process/,
+ "Clearing should be forbidden in the content process"
+ );
+
+ Assert.throws(
+ () => Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT").clear(),
+ /Keyed histograms can only be cleared in the parent process/,
+ "Clearing should be forbidden in the content process"
+ );
+
+ Assert.throws(
+ () => Telemetry.getSnapshotForHistograms(),
+ /NS_ERROR_FAILURE/,
+ "Snapshotting should be forbidden in the content process"
+ );
+
+ Assert.throws(
+ () => Telemetry.getSnapshotForKeyedHistograms(),
+ /NS_ERROR_FAILURE/,
+ "Snapshotting should be forbidden in the content process"
+ );
+}
+
+function check_histogram_values(payload) {
+ const hs = payload.histograms;
+ Assert.ok("TELEMETRY_TEST_COUNT" in hs, "Should have count test histogram.");
+ Assert.ok("TELEMETRY_TEST_FLAG" in hs, "Should have flag test histogram.");
+ Assert.ok(
+ "TELEMETRY_TEST_CATEGORICAL" in hs,
+ "Should have categorical test histogram."
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_COUNT.sum,
+ 2,
+ "Count test histogram should have the right value."
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_FLAG.sum,
+ 1,
+ "Flag test histogram should have the right value."
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_CATEGORICAL.sum,
+ 3,
+ "Categorical test histogram should have the right sum."
+ );
+
+ const kh = payload.keyedHistograms;
+ Assert.ok(
+ "TELEMETRY_TEST_KEYED_COUNT" in kh,
+ "Should have keyed count test histogram."
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_KEYED_FLAG" in kh,
+ "Should have keyed flag test histogram."
+ );
+ Assert.equal(
+ kh.TELEMETRY_TEST_KEYED_COUNT.a.sum,
+ 1,
+ "Keyed count test histogram should have the right value."
+ );
+ Assert.equal(
+ kh.TELEMETRY_TEST_KEYED_COUNT.b.sum,
+ 2,
+ "Keyed count test histogram should have the right value."
+ );
+ Assert.equal(
+ kh.TELEMETRY_TEST_KEYED_FLAG.a.sum,
+ 1,
+ "Keyed flag test histogram should have the right value."
+ );
+ Assert.equal(
+ kh.TELEMETRY_TEST_KEYED_FLAG.b.sum,
+ 1,
+ "Keyed flag test histogram should have the right value."
+ );
+}
+
+add_task(async function () {
+ if (!runningInParent) {
+ TelemetryController.testSetupContent();
+ run_child_test();
+ dump("... done with child test\n");
+ do_send_remote_message(MESSAGE_CHILD_TEST_DONE);
+ return;
+ }
+
+ // Setup.
+ do_get_profile(true);
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ await TelemetryController.testSetup();
+ if (runningInParent) {
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ }
+
+ // Run test in child, don't wait for it to finish.
+ run_test_in_child("test_ChildHistograms.js");
+ await do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
+
+ await ContentTaskUtils.waitForCondition(() => {
+ let payload = TelemetrySession.getPayload("test-ping");
+ return (
+ payload &&
+ "processes" in payload &&
+ "content" in payload.processes &&
+ "histograms" in payload.processes.content &&
+ "TELEMETRY_TEST_COUNT" in payload.processes.content.histograms
+ );
+ });
+
+ // Test record_in_processes in main process, too
+ let contentLinear = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_CONTENT_PROCESS"
+ );
+ contentLinear.add(20);
+ let contentKeyed = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_CONTENT_PROCESS"
+ );
+ contentKeyed.add("parent", 1);
+ let contentFlag = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_FLAG_CONTENT_PROCESS"
+ );
+ contentFlag.add(true);
+ let mainFlag = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG_MAIN_PROCESS");
+ mainFlag.add(true);
+ let allLinear = Telemetry.getHistogramById("TELEMETRY_TEST_ALL_PROCESSES");
+ allLinear.add(20);
+ let allChildLinear = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_ALL_CHILD_PROCESSES"
+ );
+ allChildLinear.add(20);
+ let countKeyed = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_COUNT"
+ );
+ countKeyed.add("a");
+
+ const payload = TelemetrySession.getPayload("test-ping");
+ Assert.ok("processes" in payload, "Should have processes section");
+ Assert.ok(
+ "content" in payload.processes,
+ "Should have child process section"
+ );
+ Assert.ok(
+ "histograms" in payload.processes.content,
+ "Child process section should have histograms."
+ );
+ Assert.ok(
+ "keyedHistograms" in payload.processes.content,
+ "Child process section should have keyed histograms."
+ );
+ check_histogram_values(payload.processes.content);
+
+ // Check record_in_processes
+ // Content Process
+ let hs = payload.processes.content.histograms;
+ let khs = payload.processes.content.keyedHistograms;
+ Assert.ok(
+ "TELEMETRY_TEST_CONTENT_PROCESS" in hs,
+ "Should have content process histogram"
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_CONTENT_PROCESS.sum,
+ 10,
+ "Should have correct value"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_KEYED_CONTENT_PROCESS" in khs,
+ "Should have keyed content process histogram"
+ );
+ Assert.equal(
+ khs.TELEMETRY_TEST_KEYED_CONTENT_PROCESS.content.sum,
+ 1,
+ "Should have correct value"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_FLAG_CONTENT_PROCESS" in hs,
+ "Should have content process histogram"
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_FLAG_CONTENT_PROCESS.sum,
+ 1,
+ "Should have correct value"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_ALL_PROCESSES" in hs,
+ "Should have content process histogram"
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_ALL_PROCESSES.sum,
+ 10,
+ "Should have correct value"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_ALL_CHILD_PROCESSES" in hs,
+ "Should have content process histogram"
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_ALL_CHILD_PROCESSES.sum,
+ 10,
+ "Should have correct value"
+ );
+ Assert.ok(
+ !("TELEMETRY_TEST_FLAG_MAIN_PROCESS" in hs),
+ "Should not have main process histogram in child process payload"
+ );
+
+ // Main Process
+ let mainHs = payload.histograms;
+ let mainKhs = payload.keyedHistograms;
+ Assert.ok(
+ !("TELEMETRY_TEST_CONTENT_PROCESS" in mainHs),
+ "Should not have content process histogram in main process payload"
+ );
+ Assert.ok(
+ !("TELEMETRY_TEST_KEYED_CONTENT_PROCESS" in mainKhs),
+ "Should not have keyed content process histogram in main process payload"
+ );
+ Assert.ok(
+ !("TELEMETRY_TEST_FLAG_CONTENT_PROCESS" in mainHs),
+ "Should not have content process histogram in main process payload"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_ALL_PROCESSES" in mainHs,
+ "Should have all-process histogram in main process payload"
+ );
+ Assert.equal(
+ mainHs.TELEMETRY_TEST_ALL_PROCESSES.sum,
+ 20,
+ "Should have correct value"
+ );
+ Assert.ok(
+ !("TELEMETRY_TEST_ALL_CHILD_PROCESSES" in mainHs),
+ "Should not have all-child process histogram in main process payload"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_FLAG_MAIN_PROCESS" in mainHs,
+ "Should have main process histogram in main process payload"
+ );
+ Assert.equal(
+ mainHs.TELEMETRY_TEST_FLAG_MAIN_PROCESS.sum,
+ 1,
+ "Should have correct value"
+ );
+ Assert.equal(
+ mainKhs.TELEMETRY_TEST_KEYED_COUNT.a.sum,
+ 1,
+ "Should have correct value in parent"
+ );
+
+ do_test_finished();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_ChildScalars.js b/toolkit/components/telemetry/tests/unit/test_ChildScalars.js
new file mode 100644
index 0000000000..775288fed3
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ChildScalars.js
@@ -0,0 +1,241 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+const UINT_SCALAR = "telemetry.test.unsigned_int_kind";
+const KEYED_UINT_SCALAR = "telemetry.test.keyed_unsigned_int";
+const KEYED_BOOL_SCALAR = "telemetry.test.keyed_boolean_kind";
+const CONTENT_ONLY_UINT_SCALAR = "telemetry.test.content_only_uint";
+const ALL_PROCESSES_UINT_SCALAR = "telemetry.test.all_processes_uint";
+const ALL_CHILD_PROCESSES_STRING_SCALAR =
+ "telemetry.test.all_child_processes_string";
+
+function run_child_test() {
+ // Attempt to set some scalar values from the "content" process.
+ // The next scalars are not allowed to be recorded in the content process.
+ Telemetry.scalarSet(UINT_SCALAR, 1);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "should-not-be-recorded", 1);
+
+ // The next scalars shou be recorded in only the content process.
+ Telemetry.scalarSet(CONTENT_ONLY_UINT_SCALAR, 37);
+ Telemetry.scalarSet(ALL_CHILD_PROCESSES_STRING_SCALAR, "all-child-processes");
+
+ // The next scalar will be recorded in the parent and content processes.
+ Telemetry.keyedScalarSet(KEYED_BOOL_SCALAR, "content-key", true);
+ Telemetry.keyedScalarSet(KEYED_BOOL_SCALAR, "content-key2", false);
+ Telemetry.scalarSet(ALL_PROCESSES_UINT_SCALAR, 37);
+}
+
+function setParentScalars() {
+ // The following scalars are not allowed to be recorded in the parent process.
+ Telemetry.scalarSet(CONTENT_ONLY_UINT_SCALAR, 15);
+ Telemetry.scalarSet(ALL_CHILD_PROCESSES_STRING_SCALAR, "all-child-processes");
+
+ // The next ones will be recorded only in the parent.
+ Telemetry.scalarSet(UINT_SCALAR, 15);
+
+ // This last batch will be available both in the parent and child processes.
+ Telemetry.keyedScalarSet(KEYED_BOOL_SCALAR, "parent-key", false);
+ Telemetry.scalarSet(ALL_PROCESSES_UINT_SCALAR, 37);
+}
+
+function checkParentScalars(processData) {
+ const scalars = processData.scalars;
+ const keyedScalars = processData.keyedScalars;
+
+ // Check the plain scalars, make sure we're only recording what we expect.
+ Assert.ok(
+ !(CONTENT_ONLY_UINT_SCALAR in scalars),
+ "Scalars must not be recorded in other processes unless allowed."
+ );
+ Assert.ok(
+ !(ALL_CHILD_PROCESSES_STRING_SCALAR in scalars),
+ "Scalars must not be recorded in other processes unless allowed."
+ );
+ Assert.ok(
+ UINT_SCALAR in scalars,
+ `${UINT_SCALAR} must be recorded in the parent process.`
+ );
+ Assert.equal(
+ scalars[UINT_SCALAR],
+ 15,
+ `${UINT_SCALAR} must have the correct value (parent process).`
+ );
+ Assert.ok(
+ ALL_PROCESSES_UINT_SCALAR in scalars,
+ `${ALL_PROCESSES_UINT_SCALAR} must be recorded in the parent process.`
+ );
+ Assert.equal(
+ scalars[ALL_PROCESSES_UINT_SCALAR],
+ 37,
+ `${ALL_PROCESSES_UINT_SCALAR} must have the correct value (parent process).`
+ );
+
+ // Now check the keyed scalars.
+ Assert.ok(
+ KEYED_BOOL_SCALAR in keyedScalars,
+ `${KEYED_BOOL_SCALAR} must be recorded in the parent process.`
+ );
+ Assert.ok(
+ "parent-key" in keyedScalars[KEYED_BOOL_SCALAR],
+ `${KEYED_BOOL_SCALAR} must be recorded in the parent process.`
+ );
+ Assert.equal(
+ Object.keys(keyedScalars[KEYED_BOOL_SCALAR]).length,
+ 1,
+ `${KEYED_BOOL_SCALAR} must only contain the expected key in parent process.`
+ );
+ Assert.equal(
+ keyedScalars[KEYED_BOOL_SCALAR]["parent-key"],
+ false,
+ `${KEYED_BOOL_SCALAR} must have the correct value (parent process).`
+ );
+}
+
+function checkContentScalars(processData) {
+ const scalars = processData.scalars;
+ const keyedScalars = processData.keyedScalars;
+
+ // Check the plain scalars for the content process.
+ Assert.ok(
+ !(UINT_SCALAR in scalars),
+ "Scalars must not be recorded in other processes unless allowed."
+ );
+ Assert.ok(
+ !(KEYED_UINT_SCALAR in keyedScalars),
+ "Keyed scalars must not be recorded in other processes unless allowed."
+ );
+ Assert.ok(
+ CONTENT_ONLY_UINT_SCALAR in scalars,
+ `${CONTENT_ONLY_UINT_SCALAR} must be recorded in the content process.`
+ );
+ Assert.equal(
+ scalars[CONTENT_ONLY_UINT_SCALAR],
+ 37,
+ `${CONTENT_ONLY_UINT_SCALAR} must have the correct value (content process).`
+ );
+ Assert.ok(
+ ALL_CHILD_PROCESSES_STRING_SCALAR in scalars,
+ `${ALL_CHILD_PROCESSES_STRING_SCALAR} must be recorded in the content process.`
+ );
+ Assert.equal(
+ scalars[ALL_CHILD_PROCESSES_STRING_SCALAR],
+ "all-child-processes",
+ `${ALL_CHILD_PROCESSES_STRING_SCALAR} must have the correct value (content process).`
+ );
+ Assert.ok(
+ ALL_PROCESSES_UINT_SCALAR in scalars,
+ `${ALL_PROCESSES_UINT_SCALAR} must be recorded in the content process.`
+ );
+ Assert.equal(
+ scalars[ALL_PROCESSES_UINT_SCALAR],
+ 37,
+ `${ALL_PROCESSES_UINT_SCALAR} must have the correct value (content process).`
+ );
+
+ // Check the keyed scalars.
+ Assert.ok(
+ KEYED_BOOL_SCALAR in keyedScalars,
+ `${KEYED_BOOL_SCALAR} must be recorded in the content process.`
+ );
+ Assert.ok(
+ "content-key" in keyedScalars[KEYED_BOOL_SCALAR],
+ `${KEYED_BOOL_SCALAR} must be recorded in the content process.`
+ );
+ Assert.ok(
+ "content-key2" in keyedScalars[KEYED_BOOL_SCALAR],
+ `${KEYED_BOOL_SCALAR} must be recorded in the content process.`
+ );
+ Assert.equal(
+ keyedScalars[KEYED_BOOL_SCALAR]["content-key"],
+ true,
+ `${KEYED_BOOL_SCALAR} must have the correct value (content process).`
+ );
+ Assert.equal(
+ keyedScalars[KEYED_BOOL_SCALAR]["content-key2"],
+ false,
+ `${KEYED_BOOL_SCALAR} must have the correct value (content process).`
+ );
+ Assert.equal(
+ Object.keys(keyedScalars[KEYED_BOOL_SCALAR]).length,
+ 2,
+ `${KEYED_BOOL_SCALAR} must contain the expected keys in content process.`
+ );
+}
+
+/**
+ * This function waits until content scalars are reported into the
+ * scalar snapshot.
+ */
+async function waitForContentScalars() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const scalars = Telemetry.getSnapshotForScalars("main", false);
+ return Object.keys(scalars).includes("content");
+ });
+}
+
+add_task(async function () {
+ if (!runningInParent) {
+ TelemetryController.testSetupContent();
+ run_child_test();
+ do_send_remote_message(MESSAGE_CHILD_TEST_DONE);
+ return;
+ }
+
+ // Setup.
+ do_get_profile(true);
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ await TelemetryController.testSetup();
+ if (runningInParent) {
+ setParentScalars();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ }
+
+ // Run test in child, don't wait for it to finish: just wait for the
+ // MESSAGE_CHILD_TEST_DONE.
+ run_test_in_child("test_ChildScalars.js");
+ await do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
+
+ // Once scalars are set by the content process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ await waitForContentScalars();
+
+ // Get an "environment-changed" ping rather than a "test-ping", as
+ // scalar measurements are only supported in subsession pings.
+ const payload = TelemetrySession.getPayload("environment-change");
+
+ // Validate the scalar data.
+ Assert.ok("processes" in payload, "Should have processes section");
+ Assert.ok(
+ "content" in payload.processes,
+ "Should have child process section"
+ );
+ Assert.ok(
+ "scalars" in payload.processes.content,
+ "Child process section should have scalars."
+ );
+ Assert.ok(
+ "keyedScalars" in payload.processes.content,
+ "Child process section should have keyed scalars."
+ );
+ checkParentScalars(payload.processes.parent);
+ checkContentScalars(payload.processes.content);
+
+ do_test_finished();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_CoveragePing.js b/toolkit/components/telemetry/tests/unit/test_CoveragePing.js
new file mode 100644
index 0000000000..7fd7da0baa
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_CoveragePing.js
@@ -0,0 +1,114 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const COVERAGE_VERSION = "2";
+
+const COVERAGE_ENABLED_PREF = "toolkit.coverage.enabled";
+const OPT_OUT_PREF = "toolkit.coverage.opt-out";
+const ALREADY_RUN_PREF = `toolkit.coverage.already-run.v${COVERAGE_VERSION}`;
+const COVERAGE_UUID_PREF = `toolkit.coverage.uuid.v${COVERAGE_VERSION}`;
+const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
+const REPORTING_ENDPOINT_BASE_PREF = "toolkit.coverage.endpoint.base";
+const REPORTING_ENDPOINT = "submit/coverage/coverage";
+
+Services.prefs.setIntPref("toolkit.coverage.log-level", 20);
+
+add_task(async function setup() {
+ let uuid = "test123";
+ Services.prefs.setCharPref(COVERAGE_UUID_PREF, uuid);
+
+ const server = new HttpServer();
+ server.start(-1);
+ const serverPort = server.identity.primaryPort;
+
+ Services.prefs.setCharPref(
+ REPORTING_ENDPOINT_BASE_PREF,
+ `http://localhost:${serverPort}`
+ );
+
+ server.registerPathHandler(
+ `/${REPORTING_ENDPOINT}/${COVERAGE_VERSION}/${uuid}`,
+ (request, response) => {
+ equal(request.method, "PUT");
+ let telemetryEnabled = Services.prefs.getBoolPref(
+ TELEMETRY_ENABLED_PREF,
+ false
+ );
+
+ let requestBody = NetUtil.readInputStreamToString(
+ request.bodyInputStream,
+ request.bodyInputStream.available()
+ );
+
+ let resultObj = JSON.parse(requestBody);
+
+ deepEqual(Object.keys(resultObj), [
+ "appUpdateChannel",
+ "osName",
+ "osVersion",
+ "telemetryEnabled",
+ ]);
+
+ if (telemetryEnabled) {
+ ok(resultObj.telemetryEnabled);
+ } else {
+ ok(!resultObj.telemetryEnabled);
+ }
+
+ const response_body = "OK";
+ response.bodyOutputStream.write(response_body, response_body.length);
+ server.stop();
+ }
+ );
+
+ // Trigger a proper telemetry init.
+ do_get_profile(true);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ await TelemetryController.testSetup();
+});
+
+add_task(async function test_prefs() {
+ // Telemetry reporting setting does not control this ping, but it
+ // reported by this ping.
+ Services.prefs.setBoolPref(TELEMETRY_ENABLED_PREF, false);
+
+ // should not run if enabled pref is false
+ Services.prefs.setBoolPref(COVERAGE_ENABLED_PREF, false);
+ Services.prefs.setBoolPref(ALREADY_RUN_PREF, false);
+ Services.prefs.setBoolPref(OPT_OUT_PREF, false);
+
+ await TelemetryController.testReset();
+
+ let alreadyRun = Services.prefs.getBoolPref(ALREADY_RUN_PREF, false);
+ ok(!alreadyRun, "should not have run with enabled pref false");
+
+ // should not run if opt-out pref is true
+ Services.prefs.setBoolPref(COVERAGE_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(ALREADY_RUN_PREF, false);
+ Services.prefs.setBoolPref(OPT_OUT_PREF, true);
+
+ await TelemetryController.testReset();
+
+ // should run if opt-out pref is false and coverage is enabled
+ Services.prefs.setBoolPref(COVERAGE_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(ALREADY_RUN_PREF, false);
+ Services.prefs.setBoolPref(OPT_OUT_PREF, false);
+
+ await TelemetryController.testReset();
+
+ // the telemetry setting should be set correctly
+ Services.prefs.setBoolPref(TELEMETRY_ENABLED_PREF, true);
+
+ await TelemetryController.testReset();
+
+ alreadyRun = Services.prefs.getBoolPref(ALREADY_RUN_PREF, false);
+
+ ok(alreadyRun, "should run if no opt-out and enabled");
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_EventPing.js b/toolkit/components/telemetry/tests/unit/test_EventPing.js
new file mode 100644
index 0000000000..450e88a846
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_EventPing.js
@@ -0,0 +1,280 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryEventPing: "resource://gre/modules/EventPing.sys.mjs",
+});
+
+function checkPingStructure(type, payload, options) {
+ Assert.equal(
+ type,
+ TelemetryEventPing.EVENT_PING_TYPE,
+ "Should be an event ping."
+ );
+ // Check the payload for required fields.
+ Assert.ok("reason" in payload, "Payload must have reason.");
+ Assert.ok(
+ "processStartTimestamp" in payload,
+ "Payload must have processStartTimestamp."
+ );
+ Assert.ok("sessionId" in payload, "Payload must have sessionId.");
+ Assert.ok("subsessionId" in payload, "Payload must have subsessionId.");
+ Assert.ok("lostEventsCount" in payload, "Payload must have lostEventsCount.");
+ Assert.ok("events" in payload, "Payload must have events.");
+}
+
+function fakePolicy(set, clear, send) {
+ let { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/EventPing.sys.mjs"
+ );
+ Policy.setTimeout = set;
+ Policy.clearTimeout = clear;
+ Policy.sendPing = send;
+}
+
+function pass() {
+ /* intentionally empty */
+}
+function fail() {
+ Assert.ok(false, "Not allowed");
+}
+
+function recordEvents(howMany) {
+ for (let i = 0; i < howMany; i++) {
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ }
+}
+
+add_task(async function setup() {
+ // Trigger a proper telemetry init.
+ do_get_profile(true);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ await TelemetryController.testSetup();
+ TelemetryEventPing.testReset();
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+});
+
+// Tests often take the form of faking policy within faked policy.
+// This is to allow us to record events in addition to any that were
+// recorded to trigger the submit in the first place.
+// This works because we start the timer at the top of _submitPing, giving us
+// this opportunity.
+// This results in things looking this way:
+/*
+fakePolicy((callback, delay) => {
+ // Code that runs at the top of _submitPing
+ fakePolicy(pass, pass, (type, payload, options) => {
+ // Code that runs at the bottom of _submitPing
+ });
+}, pass, fail);
+// Code that triggers _submitPing to run
+*/
+
+add_task(async function test_eventLimitReached() {
+ Telemetry.clearEvents();
+ TelemetryEventPing.testReset();
+
+ let pingCount = 0;
+
+ fakePolicy(pass, pass, fail);
+ recordEvents(999);
+ fakePolicy(
+ (callback, delay) => {
+ Telemetry.recordEvent("telemetry.test", "test2", "object1");
+ fakePolicy(pass, pass, (type, payload, options) => {
+ checkPingStructure(type, payload, options);
+ Assert.ok(options.addClientId, "Adds the client id.");
+ Assert.ok(options.addEnvironment, "Adds the environment.");
+ Assert.ok(!options.usePingSender, "Doesn't require pingsender.");
+ Assert.equal(
+ payload.reason,
+ TelemetryEventPing.Reason.MAX,
+ "Sending because we hit max"
+ );
+ Assert.equal(
+ payload.events.parent.length,
+ 1000,
+ "Has one thousand events"
+ );
+ Assert.equal(payload.lostEventsCount, 0, "Lost no events");
+ Assert.ok(
+ !payload.events.parent.some(ev => ev[1] === "test2"),
+ "Should not have included the final event (yet)."
+ );
+ pingCount++;
+ });
+ },
+ pass,
+ fail
+ );
+ // Now trigger the submit.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ Assert.equal(pingCount, 1, "Should have sent a ping");
+
+ // With a recent MAX ping sent, record another max amount of events (and then two extras).
+ fakePolicy(fail, fail, fail);
+ recordEvents(998);
+ fakePolicy(
+ (callback, delay) => {
+ Telemetry.recordEvent("telemetry.test", "test2", "object2");
+ Telemetry.recordEvent("telemetry.test", "test2", "object2");
+ fakePolicy(pass, pass, (type, payload, options) => {
+ checkPingStructure(type, payload, options);
+ Assert.ok(options.addClientId, "Adds the client id.");
+ Assert.ok(options.addEnvironment, "Adds the environment.");
+ Assert.ok(!options.usePingSender, "Doesn't require pingsender.");
+ Assert.equal(
+ payload.reason,
+ TelemetryEventPing.Reason.MAX,
+ "Sending because we hit max"
+ );
+ Assert.equal(
+ payload.events.parent.length,
+ 1000,
+ "Has one thousand events"
+ );
+ Assert.equal(payload.lostEventsCount, 2, "Lost two events");
+ Assert.equal(
+ payload.events.parent[0][2],
+ "test2",
+ "The first event of the second bunch should be the leftover event of the first bunch."
+ );
+ Assert.ok(
+ !payload.events.parent.some(ev => ev[3] === "object2"),
+ "Should not have included any of the lost two events."
+ );
+ pingCount++;
+ });
+ callback(); // Trigger the send immediately.
+ },
+ pass,
+ fail
+ );
+ recordEvents(1);
+ Assert.equal(pingCount, 2, "Should have sent a second ping");
+
+ // Ensure we send a subsequent MAX ping exactly on 1000 events, and without
+ // the two events we lost.
+ fakePolicy(fail, fail, fail);
+ recordEvents(999);
+ fakePolicy((callback, delay) => {
+ fakePolicy(pass, pass, (type, payload, options) => {
+ checkPingStructure(type, payload, options);
+ Assert.ok(options.addClientId, "Adds the client id.");
+ Assert.ok(options.addEnvironment, "Adds the environment.");
+ Assert.ok(!options.usePingSender, "Doesn't require pingsender.");
+ Assert.equal(
+ payload.reason,
+ TelemetryEventPing.Reason.MAX,
+ "Sending because we hit max"
+ );
+ Assert.equal(
+ payload.events.parent.length,
+ 1000,
+ "Has one thousand events"
+ );
+ Assert.equal(payload.lostEventsCount, 0, "Lost no events");
+ Assert.ok(
+ !payload.events.parent.some(ev => ev[3] === "object2"),
+ "Should not have included any of the lost two events from the previous bunch."
+ );
+ pingCount++;
+ });
+ callback(); // Trigger the send immediately
+ });
+ recordEvents(1);
+ Assert.equal(pingCount, 3, "Should have sent a third ping");
+});
+
+add_task(async function test_timers() {
+ Telemetry.clearEvents();
+ TelemetryEventPing.testReset();
+
+ // Immediately after submitting a MAX ping, we should set the timer for the
+ // next interval.
+ recordEvents(999);
+ fakePolicy(
+ (callback, delay) => {
+ Assert.equal(
+ delay,
+ TelemetryEventPing.minFrequency,
+ "Timer should be started with the min frequency"
+ );
+ },
+ pass,
+ pass
+ );
+ recordEvents(1);
+
+ fakePolicy(
+ (callback, delay) => {
+ Assert.ok(
+ delay <= TelemetryEventPing.maxFrequency,
+ "Timer should be at most the max frequency for a subsequent MAX ping."
+ );
+ },
+ pass,
+ pass
+ );
+ recordEvents(1000);
+});
+
+add_task(async function test_periodic() {
+ Telemetry.clearEvents();
+ TelemetryEventPing.testReset();
+
+ fakePolicy(
+ (callback, delay) => {
+ Assert.equal(
+ delay,
+ TelemetryEventPing.minFrequency,
+ "Timer should default to the min frequency"
+ );
+ fakePolicy(pass, pass, (type, payload, options) => {
+ checkPingStructure(type, payload, options);
+ Assert.ok(options.addClientId, "Adds the client id.");
+ Assert.ok(options.addEnvironment, "Adds the environment.");
+ Assert.ok(!options.usePingSender, "Doesn't require pingsender.");
+ Assert.equal(
+ payload.reason,
+ TelemetryEventPing.Reason.PERIODIC,
+ "Sending because we hit a timer"
+ );
+ Assert.equal(payload.events.parent.length, 1, "Has one event");
+ Assert.equal(payload.lostEventsCount, 0, "Lost no events");
+ });
+ callback();
+ },
+ pass,
+ fail
+ );
+
+ recordEvents(1);
+ TelemetryEventPing._startTimer();
+});
+
+// Ensure this is the final test in the suite, as it shuts things down.
+add_task(async function test_shutdown() {
+ Telemetry.clearEvents();
+ TelemetryEventPing.testReset();
+
+ recordEvents(999);
+ fakePolicy(pass, pass, (type, payload, options) => {
+ Assert.ok(options.addClientId, "Adds the client id.");
+ Assert.ok(options.addEnvironment, "Adds the environment.");
+ Assert.ok(options.usePingSender, "Asks for pingsender.");
+ Assert.equal(
+ payload.reason,
+ TelemetryEventPing.Reason.SHUTDOWN,
+ "Sending because we are shutting down"
+ );
+ Assert.equal(payload.events.parent.length, 999, "Has 999 events");
+ Assert.equal(payload.lostEventsCount, 0, "No lost events");
+ });
+ TelemetryEventPing.shutdown();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_HealthPing.js b/toolkit/components/telemetry/tests/unit/test_HealthPing.js
new file mode 100644
index 0000000000..b922a07f47
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_HealthPing.js
@@ -0,0 +1,279 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This tests the public Telemetry API for submitting Health pings.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryHealthPing: "resource://gre/modules/HealthPing.sys.mjs",
+});
+
+function checkHealthPingStructure(ping, expectedFailuresDict) {
+ let payload = ping.payload;
+ Assert.equal(
+ ping.type,
+ TelemetryHealthPing.HEALTH_PING_TYPE,
+ "Should have recorded a health ping."
+ );
+
+ for (let [key, value] of Object.entries(expectedFailuresDict)) {
+ Assert.deepEqual(
+ payload[key],
+ value,
+ "Should have recorded correct entry with key: " + key
+ );
+ }
+}
+
+function fakeHealthSchedulerTimer(set, clear) {
+ let { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/HealthPing.sys.mjs"
+ );
+ Policy.setSchedulerTickTimeout = set;
+ Policy.clearSchedulerTickTimeout = clear;
+}
+
+add_setup(async function setup() {
+ // Trigger a proper telemetry init.
+ do_get_profile(true);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ Preferences.set(TelemetryUtils.Preferences.HealthPingEnabled, true);
+
+ await TelemetryController.testSetup();
+ PingServer.start();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ Preferences.set(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+});
+
+registerCleanupFunction(async function cleanup() {
+ await PingServer.stop();
+});
+
+add_task(async function test_sendImmediately() {
+ PingServer.clearRequests();
+ TelemetryHealthPing.testReset();
+
+ await TelemetryHealthPing.recordSendFailure("testProblem");
+ let ping = await PingServer.promiseNextPing();
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.SEND_FAILURE]: {
+ testProblem: 1,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.IMMEDIATE,
+ });
+});
+
+add_task(async function test_sendOnDelay() {
+ PingServer.clearRequests();
+ TelemetryHealthPing.testReset();
+
+ // This first failure should immediately trigger a ping. After this, subsequent failures should be throttled.
+ await TelemetryHealthPing.recordSendFailure("testFailure");
+ let testPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ testPing.type,
+ TelemetryHealthPing.HEALTH_PING_TYPE,
+ "Should have recorded a health ping."
+ );
+
+ // Retrieve delayed call back.
+ let pingSubmissionCallBack = null;
+ fakeHealthSchedulerTimer(
+ callBack => (pingSubmissionCallBack = callBack),
+ () => {}
+ );
+
+ // Record two failures, health ping must not be send now.
+ await TelemetryHealthPing.recordSendFailure("testFailure");
+ await TelemetryHealthPing.recordSendFailure("testFailure");
+
+ // Wait for sending delayed health ping.
+ await pingSubmissionCallBack();
+
+ let ping = await PingServer.promiseNextPing();
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.SEND_FAILURE]: {
+ testFailure: 2,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.DELAYED,
+ });
+});
+
+add_task(async function test_sendOverSizedPing() {
+ TelemetryHealthPing.testReset();
+ PingServer.clearRequests();
+ let OVER_SIZED_PING_TYPE = "over-sized-ping";
+ let overSizedData = generateRandomString(2 * 1024 * 1024);
+
+ await TelemetryController.submitExternalPing(OVER_SIZED_PING_TYPE, {
+ data: overSizedData,
+ });
+ let ping = await PingServer.promiseNextPing();
+
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
+ [OVER_SIZED_PING_TYPE]: 1,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.IMMEDIATE,
+ });
+});
+
+add_task(async function test_sendOnlyTopTenDiscardedPings() {
+ TelemetryHealthPing.testReset();
+ await TelemetrySend.reset();
+ PingServer.clearRequests();
+ let PING_TYPE = "sort-discarded";
+
+ // This first failure should immediately trigger a ping. After this, subsequent failures should be throttled.
+ await TelemetryHealthPing.recordSendFailure("testFailure");
+ let testPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ testPing.type,
+ TelemetryHealthPing.HEALTH_PING_TYPE,
+ "Should have recorded a health ping."
+ );
+
+ // Retrieve delayed call back.
+ let pingSubmissionCallBack = null;
+ fakeHealthSchedulerTimer(
+ callBack => (pingSubmissionCallBack = callBack),
+ () => {}
+ );
+
+ // Add failures
+ for (let i = 1; i < 12; i++) {
+ for (let j = 1; j < i; j++) {
+ TelemetryHealthPing.recordDiscardedPing(PING_TYPE + i);
+ }
+ }
+
+ await TelemetrySend.reset();
+ await pingSubmissionCallBack();
+ let ping = await PingServer.promiseNextPing();
+
+ checkHealthPingStructure(ping, {
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.DELAYED,
+ [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
+ [PING_TYPE + 11]: 10,
+ [PING_TYPE + 10]: 9,
+ [PING_TYPE + 9]: 8,
+ [PING_TYPE + 8]: 7,
+ [PING_TYPE + 7]: 6,
+ [PING_TYPE + 6]: 5,
+ [PING_TYPE + 5]: 4,
+ [PING_TYPE + 4]: 3,
+ [PING_TYPE + 3]: 2,
+ [PING_TYPE + 2]: 1,
+ },
+ });
+});
+
+add_task(async function test_discardedForSizePending() {
+ TelemetryHealthPing.testReset();
+ PingServer.clearRequests();
+
+ const PING_TYPE = "discarded-for-size-pending";
+
+ const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";
+ // Create a pending oversized ping.
+ let overSizedPayload = generateRandomString(2 * 1024 * 1024);
+ 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: overSizedPayload,
+ };
+
+ // Test loadPendingPing.
+ await TelemetryStorage.savePendingPing(OVERSIZED_PING);
+ // 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."
+ );
+
+ let ping = await PingServer.promiseNextPing();
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
+ "<unknown>": 1,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.IMMEDIATE,
+ });
+
+ // Test _scanPendingPings.
+ TelemetryHealthPing.testReset();
+ await TelemetryStorage.savePendingPing(OVERSIZED_PING);
+ await TelemetryStorage.loadPendingPingList();
+
+ ping = await PingServer.promiseNextPing();
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
+ "<unknown>": 1,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.IMMEDIATE,
+ });
+});
+
+add_task(async function test_usePingSenderOnShutdown() {
+ if (
+ gIsAndroid ||
+ (AppConstants.platform == "linux" && !Services.appinfo.is64Bit)
+ ) {
+ // We don't support the pingsender on Android, yet, see bug 1335917.
+ // We also don't support the pingsender testing on Treeherder for
+ // Linux 32 bit (due to missing libraries). So skip it there too.
+ // See bug 1310703 comment 78.
+ return;
+ }
+
+ TelemetryHealthPing.testReset();
+ await TelemetrySend.reset();
+ PingServer.clearRequests();
+
+ // This first failure should immediately trigger a ping.
+ // After this, subsequent failures should be throttled.
+ await TelemetryHealthPing.recordSendFailure("testFailure");
+ await PingServer.promiseNextPing();
+
+ TelemetryHealthPing.recordSendFailure("testFailure");
+ let nextRequest = PingServer.promiseNextRequest();
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ let request = await nextRequest;
+ let ping = decodeRequestPayload(request);
+
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.SEND_FAILURE]: {
+ testFailure: 1,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.SHUT_DOWN,
+ });
+
+ // Check that the health ping is sent at shutdown using the pingsender.
+ Assert.equal(
+ request.getHeader("User-Agent"),
+ "pingsender/1.0",
+ "Should have received the correct user agent string."
+ );
+ Assert.equal(
+ request.getHeader("X-PingSender-Version"),
+ "1.0",
+ "Should have received the correct PingSender version string."
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_MigratePendingPings.js b/toolkit/components/telemetry/tests/unit/test_MigratePendingPings.js
new file mode 100644
index 0000000000..bb618bb8da
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_MigratePendingPings.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+"use strict";
+
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+const { makeFakeAppDir } = ChromeUtils.importESModule(
+ "resource://testing-common/AppData.sys.mjs"
+);
+
+// The name of the pending pings directory outside of the user profile,
+// in the user app data directory.
+const PENDING_PING_DIR_NAME = "Pending Pings";
+
+// Create a directory inside the profile and register it as UAppData, so
+// we can stick fake crash pings inside there. We put it inside the profile
+// just because we know that will get cleaned up after the test runs.
+async function createFakeAppDir() {
+ // Create "<profile>/UAppData/Pending Pings".
+ const pendingPingsPath = PathUtils.join(
+ PathUtils.profileDir,
+ "UAppData",
+ PENDING_PING_DIR_NAME
+ );
+ await IOUtils.makeDirectory(pendingPingsPath, {
+ ignoreExisting: true,
+ createAncestors: true,
+ });
+
+ await makeFakeAppDir();
+}
+
+add_task(async function setup() {
+ // Init the profile.
+ do_get_profile();
+ await createFakeAppDir();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+});
+
+add_task(async function test_migrateUnsentPings() {
+ const PINGS = [
+ {
+ type: "crash",
+ id: TelemetryUtils.generateUUID(),
+ payload: { foo: "bar" },
+ dateCreated: new Date(2010, 1, 1, 10, 0, 0),
+ },
+ {
+ type: "other",
+ id: TelemetryUtils.generateUUID(),
+ payload: { moo: "meh" },
+ dateCreated: new Date(2010, 2, 1, 10, 2, 0),
+ },
+ ];
+ const APP_DATA_DIR = Services.dirsvc.get("UAppData", Ci.nsIFile).path;
+ const APPDATA_PINGS_DIR = PathUtils.join(APP_DATA_DIR, PENDING_PING_DIR_NAME);
+
+ // Create some pending pings outside of the user profile.
+ for (let ping of PINGS) {
+ const pingPath = PathUtils.join(APPDATA_PINGS_DIR, ping.id + ".json");
+ await TelemetryStorage.savePingToFile(ping, pingPath, true);
+ }
+
+ // Make sure the pending ping list is empty.
+ await TelemetryStorage.testClearPendingPings();
+
+ // Start the migration from TelemetryStorage.
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ Assert.equal(
+ pendingPings.length,
+ 2,
+ "TelemetryStorage must have migrated 2 pings."
+ );
+
+ for (let ping of PINGS) {
+ // Verify that the pings were migrated and are among the pending pings.
+ Assert.ok(
+ pendingPings.find(p => p.id == ping.id),
+ "The ping must have been migrated."
+ );
+
+ // Try to load the migrated ping from the user profile.
+ let migratedPing = await TelemetryStorage.loadPendingPing(ping.id);
+ Assert.equal(
+ ping.id,
+ migratedPing.id,
+ "Should have loaded the correct ping id."
+ );
+ Assert.equal(
+ ping.type,
+ migratedPing.type,
+ "Should have loaded the correct ping type."
+ );
+ Assert.deepEqual(
+ ping.payload,
+ migratedPing.payload,
+ "Should have loaded the correct payload."
+ );
+
+ // Verify that the pings are no longer outside of the user profile.
+ const pingPath = PathUtils.join(APPDATA_PINGS_DIR, ping.id + ".json");
+ Assert.ok(
+ !(await IOUtils.exists(pingPath)),
+ "The ping should not be in the Pending Pings directory anymore."
+ );
+ }
+});
+
+add_task(async function test_migrateIncompatiblePing() {
+ const APP_DATA_DIR = Services.dirsvc.get("UAppData", Ci.nsIFile).path;
+ const APPDATA_PINGS_DIR = PathUtils.join(APP_DATA_DIR, PENDING_PING_DIR_NAME);
+
+ // Create a ping incompatible with migration outside of the user profile.
+ const pingPath = PathUtils.join(APPDATA_PINGS_DIR, "incompatible.json");
+ await TelemetryStorage.savePingToFile({ incom: "patible" }, pingPath, true);
+
+ // Ensure the pending ping list is empty.
+ await TelemetryStorage.testClearPendingPings();
+ TelemetryStorage.reset();
+
+ // Start the migration from TelemetryStorage.
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ Assert.equal(
+ pendingPings.length,
+ 0,
+ "TelemetryStorage must have migrated no pings." +
+ JSON.stringify(pendingPings)
+ );
+
+ Assert.ok(
+ !(await IOUtils.exists(pingPath)),
+ "The incompatible ping must have been deleted by the migration"
+ );
+});
+
+add_task(async function teardown() {
+ // Delete the UAppData directory and make sure nothing breaks.
+ const APP_DATA_DIR = Services.dirsvc.get("UAppData", Ci.nsIFile).path;
+ await IOUtils.remove(APP_DATA_DIR, { recursive: true });
+ Assert.ok(
+ !(await IOUtils.exists(APP_DATA_DIR)),
+ "The UAppData directory must not exist anymore."
+ );
+ TelemetryStorage.reset();
+ await TelemetryStorage.loadPendingPingList();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_ModulesPing.js b/toolkit/components/telemetry/tests/unit/test_ModulesPing.js
new file mode 100644
index 0000000000..8196d67dba
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ModulesPing.js
@@ -0,0 +1,300 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+
+const MAX_NAME_LENGTH = 64;
+
+// The following libraries (except libxul) are all built from the
+// toolkit/components/telemetry/tests/modules-test.cpp file, which contains
+// instructions on how to build them.
+const libModules = ctypes.libraryName("modules-test");
+const libUnicode = ctypes.libraryName("modμles-test");
+const libLongName =
+ "lorem_ipsum_dolor_sit_amet_consectetur_adipiscing_elit_Fusce_sit_amet_tellus_non_magna_euismod_vestibulum_Vivamus_turpis_duis.dll";
+
+function chooseDLL(x86, x64, aarch64) {
+ let xpcomabi = Services.appinfo.XPCOMABI;
+ let cpu = xpcomabi.split("-")[0];
+ switch (cpu) {
+ case "aarch64":
+ return aarch64;
+ case "x86_64":
+ return x64;
+ case "x86":
+ return x86;
+ // This case only happens on Android, which gets skipped below. The previous
+ // code was returning the x86 version when testing for arm.
+ case "arm":
+ return x86;
+ default:
+ Assert.ok(false, "unexpected CPU type: " + cpu);
+ return x86;
+ }
+}
+
+const libUnicodePDB = chooseDLL(
+ "testUnicodePDB32.dll",
+ "testUnicodePDB64.dll",
+ "testUnicodePDBAArch64.dll"
+);
+const libNoPDB = chooseDLL(
+ "testNoPDB32.dll",
+ "testNoPDB64.dll",
+ "testNoPDBAArch64.dll"
+);
+const libxul = PathUtils.filename(PathUtils.xulLibraryPath);
+
+const libModulesFile = do_get_file(libModules).path;
+const libUnicodeFile = PathUtils.join(
+ PathUtils.parent(libModulesFile),
+ libUnicode
+);
+const libLongNameFile = PathUtils.join(
+ PathUtils.parent(libModulesFile),
+ libLongName
+);
+const libUnicodePDBFile = do_get_file(libUnicodePDB).path;
+const libNoPDBFile = do_get_file(libNoPDB).path;
+
+let libModulesHandle,
+ libUnicodeHandle,
+ libLongNameHandle,
+ libUnicodePDBHandle,
+ libNoPDBHandle;
+
+let expectedLibs;
+if (AppConstants.platform === "win") {
+ const version = AppConstants.MOZ_APP_VERSION.substring(
+ 0,
+ AppConstants.MOZ_APP_VERSION.indexOf(".") + 2
+ );
+
+ expectedLibs = [
+ {
+ name: libxul,
+ debugName: libxul.replace(".dll", ".pdb"),
+ version,
+ },
+ {
+ name: libModules,
+ debugName: libModules.replace(".dll", ".pdb"),
+ version,
+ },
+ {
+ name: libUnicode,
+ debugName: libModules.replace(".dll", ".pdb"),
+ version,
+ },
+ {
+ name: libLongName.substring(0, MAX_NAME_LENGTH - 1) + "…",
+ debugName: libModules.replace(".dll", ".pdb"),
+ version,
+ },
+ {
+ name: libUnicodePDB,
+ debugName: "libmodμles.pdb",
+ version: null,
+ },
+ {
+ name: libNoPDB,
+ debugName: null,
+ version: null,
+ },
+ {
+ // We choose this DLL because it's guaranteed to exist in our process and
+ // be signed on all Windows versions that we support.
+ name: "ntdll.dll",
+ // debugName changes depending on OS version and is irrelevant to this test
+ // version changes depending on OS version and is irrelevant to this test
+ certSubject: "Microsoft Windows",
+ },
+ ];
+} else {
+ expectedLibs = [
+ {
+ name: libxul,
+ debugName: libxul,
+ version: null,
+ },
+ {
+ name: libModules,
+ debugName: libModules,
+ version: null,
+ },
+ {
+ name: libUnicode,
+ debugName: libUnicode,
+ version: null,
+ },
+ {
+ name: libLongName.substring(0, MAX_NAME_LENGTH - 1) + "…",
+ debugName: libLongName.substring(0, MAX_NAME_LENGTH - 1) + "…",
+ version: null,
+ },
+ ];
+}
+
+add_task(async function setup() {
+ do_get_profile();
+
+ await IOUtils.copy(libModulesFile, libUnicodeFile);
+ await IOUtils.copy(libModulesFile, libLongNameFile);
+
+ libModulesHandle = ctypes.open(libModulesFile);
+ libUnicodeHandle = ctypes.open(libUnicodeFile);
+ libLongNameHandle = ctypes.open(libLongNameFile);
+ if (AppConstants.platform === "win") {
+ libUnicodePDBHandle = ctypes.open(libUnicodePDBFile);
+ libNoPDBHandle = ctypes.open(libNoPDBFile);
+ }
+
+ // Pretend the untrustedmodules ping has already been sent now to get it out
+ // of the way and avoid confusing the test with our PingServer receiving two
+ // pings during our test.
+ Services.prefs.setIntPref(
+ "app.update.lastUpdateTime.telemetry_untrustedmodules_ping",
+ Math.round(Date.now() / 1000)
+ );
+
+ // Force the timer to fire (using a small interval).
+ Cc["@mozilla.org/updates/timer-manager;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "utm-test-init", "");
+ Preferences.set("toolkit.telemetry.modulesPing.interval", 0);
+ Preferences.set("app.update.url", "http://localhost");
+
+ // Start the local ping server and setup Telemetry to use it during the tests.
+ PingServer.start();
+ Preferences.set(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+});
+
+registerCleanupFunction(function () {
+ if (libModulesHandle) {
+ libModulesHandle.close();
+ }
+ if (libUnicodeHandle) {
+ libUnicodeHandle.close();
+ }
+ if (libLongNameHandle) {
+ libLongNameHandle.close();
+ }
+ if (libUnicodePDBHandle) {
+ libUnicodePDBHandle.close();
+ }
+ if (libNoPDBHandle) {
+ libNoPDBHandle.close();
+ }
+
+ return IOUtils.remove(libUnicodeFile)
+ .then(() => IOUtils.remove(libLongNameFile))
+ .then(() => PingServer.stop());
+});
+
+add_task(
+ {
+ skip_if: () => !AppConstants.MOZ_GECKO_PROFILER,
+ },
+ async function test_send_ping() {
+ await TelemetryController.testSetup();
+
+ let found = await PingServer.promiseNextPing();
+ Assert.ok(!!found, "Telemetry ping submitted.");
+ Assert.strictEqual(found.type, "modules", "Ping type is 'modules'");
+ Assert.ok(found.environment, "'modules' ping has an environment.");
+ Assert.ok(!!found.clientId, "'modules' ping has a client ID.");
+ Assert.ok(
+ !!found.payload.modules,
+ "Telemetry ping payload contains the 'modules' array."
+ );
+
+ let nameComparator;
+ if (AppConstants.platform === "win") {
+ // Do case-insensitive checking of file/module names on Windows
+ nameComparator = function (a, b) {
+ if (typeof a === "string" && typeof b === "string") {
+ return a.toLowerCase() === b.toLowerCase();
+ }
+
+ return a === b;
+ };
+ } else {
+ nameComparator = function (a, b) {
+ return a === b;
+ };
+ }
+
+ for (let lib of expectedLibs) {
+ let test_lib = found.payload.modules.find(module =>
+ nameComparator(module.name, lib.name)
+ );
+
+ Assert.ok(!!test_lib, "There is a '" + lib.name + "' module.");
+
+ if ("version" in lib) {
+ if (lib.version !== null) {
+ Assert.ok(
+ test_lib.version.startsWith(lib.version),
+ "The version of the " +
+ lib.name +
+ " module (" +
+ test_lib.version +
+ ") is correct (it starts with '" +
+ lib.version +
+ "')."
+ );
+ } else {
+ Assert.strictEqual(
+ test_lib.version,
+ null,
+ "The version of the " + lib.name + " module is null."
+ );
+ }
+ }
+
+ if ("debugName" in lib) {
+ Assert.ok(
+ nameComparator(test_lib.debugName, lib.debugName),
+ "The " + lib.name + " module has the correct debug name."
+ );
+ }
+
+ if (lib.debugName === null) {
+ Assert.strictEqual(
+ test_lib.debugID,
+ null,
+ "The " + lib.name + " module doesn't have a debug ID."
+ );
+ } else {
+ Assert.greater(
+ test_lib.debugID.length,
+ 0,
+ "The " + lib.name + " module has a debug ID."
+ );
+ }
+
+ if ("certSubject" in lib) {
+ Assert.strictEqual(
+ test_lib.certSubject,
+ lib.certSubject,
+ "The " + lib.name + " module has the expected cert subject."
+ );
+ }
+ }
+
+ let test_lib = found.payload.modules.find(
+ module => module.name === libLongName
+ );
+ Assert.ok(!test_lib, "There isn't a '" + libLongName + "' module.");
+ }
+);
diff --git a/toolkit/components/telemetry/tests/unit/test_PingAPI.js b/toolkit/components/telemetry/tests/unit/test_PingAPI.js
new file mode 100644
index 0000000000..b03f870846
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_PingAPI.js
@@ -0,0 +1,709 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// This tests the public Telemetry API for submitting pings.
+
+"use strict";
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+const { TelemetryArchive } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryArchive.sys.mjs"
+);
+
+XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function () {
+ return PathUtils.join(PathUtils.profileDir, "datareporting", "archived");
+});
+
+/**
+ * Fakes the archive storage quota.
+ * @param {Integer} aArchiveQuota The new quota, in bytes.
+ */
+function fakeStorageQuota(aArchiveQuota) {
+ let { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+ );
+ Policy.getArchiveQuota = () => aArchiveQuota;
+}
+
+/**
+ * Lists all the valid archived pings and their metadata, sorted by creation date.
+ *
+ * @return {Object[]} A list of objects with the extracted data in the form:
+ * { timestamp: <number>,
+ * id: <string>,
+ * type: <string>,
+ * size: <integer> }
+ */
+var getArchivedPingsInfo = async function () {
+ let archivedPings = [];
+
+ // Iterate through the subdirs of |gPingsArchivePath|.
+ for (const dir of await IOUtils.getChildren(gPingsArchivePath)) {
+ const { type } = await IOUtils.stat(dir);
+ if (type != "directory") {
+ continue;
+ }
+
+ // Then get a list of the files for the current subdir.
+ for (const filePath of await IOUtils.getChildren(dir)) {
+ const fileInfo = await IOUtils.stat(filePath);
+ if (fileInfo.type == "directory") {
+ continue;
+ }
+ let pingInfo = TelemetryStorage._testGetArchivedPingDataFromFileName(
+ PathUtils.filename(filePath)
+ );
+ if (!pingInfo) {
+ // This is not a valid archived ping, skip it.
+ continue;
+ }
+ // Find the size of the ping and then add the info to the array.
+ pingInfo.size = fileInfo.size;
+ archivedPings.push(pingInfo);
+ }
+ }
+
+ // Sort the list by creation date and then return it.
+ archivedPings.sort((a, b) => b.timestamp - a.timestamp);
+ return archivedPings;
+};
+
+add_task(async function test_setup() {
+ do_get_profile(true);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+});
+
+add_task(async function test_archivedPings() {
+ // TelemetryController should not be fully initialized at this point.
+ // Submitting pings should still work fine.
+
+ const PINGS = [
+ {
+ type: "test-ping-api-1",
+ payload: { foo: "bar" },
+ dateCreated: new Date(2010, 1, 1, 10, 0, 0),
+ },
+ {
+ type: "test-ping-api-2",
+ payload: { moo: "meh" },
+ dateCreated: new Date(2010, 2, 1, 10, 0, 0),
+ },
+ ];
+
+ // Submit pings and check the ping list.
+ let expectedPingList = [];
+
+ for (let data of PINGS) {
+ fakeNow(data.dateCreated);
+ data.id = await TelemetryController.submitExternalPing(
+ data.type,
+ data.payload
+ );
+ let list = await TelemetryArchive.promiseArchivedPingList();
+
+ expectedPingList.push({
+ id: data.id,
+ type: data.type,
+ timestampCreated: data.dateCreated.getTime(),
+ });
+ Assert.deepEqual(
+ list,
+ expectedPingList,
+ "Archived ping list should contain submitted pings"
+ );
+ }
+
+ // Check loading the archived pings.
+ let checkLoadingPings = async function () {
+ for (let data of PINGS) {
+ let ping = await TelemetryArchive.promiseArchivedPingById(data.id);
+ Assert.equal(ping.id, data.id, "Archived ping should have matching id");
+ Assert.equal(
+ ping.type,
+ data.type,
+ "Archived ping should have matching type"
+ );
+ Assert.equal(
+ ping.creationDate,
+ data.dateCreated.toISOString(),
+ "Archived ping should have matching creation date"
+ );
+ }
+ };
+
+ await checkLoadingPings();
+
+ // Check that we find the archived pings again by scanning after a restart.
+ await TelemetryController.testReset();
+
+ let pingList = await TelemetryArchive.promiseArchivedPingList();
+ Assert.deepEqual(
+ expectedPingList,
+ pingList,
+ "Should have submitted pings in archive list after restart"
+ );
+ await checkLoadingPings();
+
+ // Write invalid pings into the archive with both valid and invalid names.
+ let writeToArchivedDir = async function (
+ dirname,
+ filename,
+ content,
+ compressed
+ ) {
+ const dirPath = PathUtils.join(gPingsArchivePath, dirname);
+ await IOUtils.makeDirectory(dirPath, { ignoreExisting: true });
+ const filePath = PathUtils.join(dirPath, filename);
+ const options = { tmpPath: filePath + ".tmp", mode: "overwrite" };
+ if (compressed) {
+ options.compress = true;
+ }
+ await IOUtils.writeUTF8(filePath, content, options);
+ };
+
+ const FAKE_ID1 = "10000000-0123-0123-0123-0123456789a1";
+ const FAKE_ID2 = "20000000-0123-0123-0123-0123456789a2";
+ const FAKE_ID3 = "20000000-0123-0123-0123-0123456789a3";
+ const FAKE_TYPE = "foo";
+
+ // These should get rejected.
+ await writeToArchivedDir("xx", "foo.json", "{}");
+ await writeToArchivedDir("2010-02", "xx.xx.xx.json", "{}");
+ // This one should get picked up...
+ await writeToArchivedDir(
+ "2010-02",
+ "1." + FAKE_ID1 + "." + FAKE_TYPE + ".json",
+ "{}"
+ );
+ // ... but get overwritten by this one.
+ await writeToArchivedDir(
+ "2010-02",
+ "2." + FAKE_ID1 + "." + FAKE_TYPE + ".json",
+ ""
+ );
+ // This should get picked up fine.
+ await writeToArchivedDir(
+ "2010-02",
+ "3." + FAKE_ID2 + "." + FAKE_TYPE + ".json",
+ ""
+ );
+ // This compressed ping should get picked up fine as well.
+ await writeToArchivedDir(
+ "2010-02",
+ "4." + FAKE_ID3 + "." + FAKE_TYPE + ".jsonlz4",
+ ""
+ );
+
+ expectedPingList.push({
+ id: FAKE_ID1,
+ type: "foo",
+ timestampCreated: 2,
+ });
+ expectedPingList.push({
+ id: FAKE_ID2,
+ type: "foo",
+ timestampCreated: 3,
+ });
+ expectedPingList.push({
+ id: FAKE_ID3,
+ type: "foo",
+ timestampCreated: 4,
+ });
+ expectedPingList.sort((a, b) => a.timestampCreated - b.timestampCreated);
+
+ // Reset the TelemetryArchive so we scan the archived dir again.
+ await TelemetryController.testReset();
+
+ // Check that we are still picking up the valid archived pings on disk,
+ // plus the valid ones above.
+ pingList = await TelemetryArchive.promiseArchivedPingList();
+ Assert.deepEqual(
+ expectedPingList,
+ pingList,
+ "Should have picked up valid archived pings"
+ );
+ await checkLoadingPings();
+
+ // Now check that we fail to load the two invalid pings from above.
+ Assert.ok(
+ await promiseRejects(TelemetryArchive.promiseArchivedPingById(FAKE_ID1)),
+ "Should have rejected invalid ping"
+ );
+ Assert.ok(
+ await promiseRejects(TelemetryArchive.promiseArchivedPingById(FAKE_ID2)),
+ "Should have rejected invalid ping"
+ );
+});
+
+add_task(async function test_archiveCleanup() {
+ const PING_TYPE = "foo";
+
+ // Empty the archive.
+ await IOUtils.remove(gPingsArchivePath, { recursive: true });
+
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").clear();
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT").clear();
+ // Also reset these histograms to make sure normal sized pings don't get counted.
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").clear();
+ Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB"
+ ).clear();
+
+ // Build the cache. Nothing should be evicted as there's no ping directory.
+ await TelemetryController.testReset();
+ await TelemetryStorage.testCleanupTaskPromise();
+ await TelemetryArchive.promiseArchivedPingList();
+
+ let h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_SCAN_PING_COUNT"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must report 0 pings scanned if no archive dir exists."
+ );
+ // One directory out of four was removed as well.
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must report 0 evicted dirs if no archive dir exists."
+ );
+
+ let expectedPrunedInfo = [];
+ let expectedNotPrunedInfo = [];
+
+ let checkArchive = async function () {
+ // Check that the pruned pings are not on disk anymore.
+ for (let prunedInfo of expectedPrunedInfo) {
+ await Assert.rejects(
+ TelemetryArchive.promiseArchivedPingById(prunedInfo.id),
+ /TelemetryStorage.loadArchivedPing - no ping with id/,
+ "Ping " + prunedInfo.id + " should have been pruned."
+ );
+ const pingPath = TelemetryStorage._testGetArchivedPingPath(
+ prunedInfo.id,
+ prunedInfo.creationDate,
+ PING_TYPE
+ );
+ Assert.ok(
+ !(await IOUtils.exists(pingPath)),
+ "The ping should not be on the disk anymore."
+ );
+ }
+
+ // Check that the expected pings are there.
+ for (let expectedInfo of expectedNotPrunedInfo) {
+ Assert.ok(
+ await TelemetryArchive.promiseArchivedPingById(expectedInfo.id),
+ "Ping" + expectedInfo.id + " should be in the archive."
+ );
+ }
+ };
+
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT").clear();
+
+ // Create a ping which should be pruned because it is past the retention period.
+ let date = fakeNow(2010, 1, 1, 1, 0, 0);
+ let firstDate = date;
+ let pingId = await TelemetryController.submitExternalPing(PING_TYPE, {}, {});
+ expectedPrunedInfo.push({ id: pingId, creationDate: date });
+
+ // Create a ping which should be kept because it is within the retention period.
+ const oldestDirectoryDate = fakeNow(2010, 2, 1, 1, 0, 0);
+ pingId = await TelemetryController.submitExternalPing(PING_TYPE, {}, {});
+ expectedNotPrunedInfo.push({ id: pingId, creationDate: oldestDirectoryDate });
+
+ // Create 20 other pings which are within the retention period, but would be affected
+ // by the disk quota.
+ for (let month of [3, 4]) {
+ for (let minute = 0; minute < 10; minute++) {
+ date = fakeNow(2010, month, 1, 1, minute, 0);
+ pingId = await TelemetryController.submitExternalPing(PING_TYPE, {}, {});
+ expectedNotPrunedInfo.push({ id: pingId, creationDate: date });
+ }
+ }
+
+ // We expect all the pings we archived to be in this histogram.
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT");
+ Assert.equal(
+ h.snapshot().sum,
+ 22,
+ "All the pings must be live-accumulated in the histogram."
+ );
+ // Reset the histogram that will be populated by the archive scan.
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").clear();
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE").clear();
+
+ // Move the current date 60 days ahead of the first ping.
+ fakeNow(futureDate(firstDate, 60 * MILLISECONDS_PER_DAY));
+ // Reset TelemetryArchive and TelemetryController to start the startup cleanup.
+ await TelemetryController.testReset();
+ // Wait for the cleanup to finish.
+ await TelemetryStorage.testCleanupTaskPromise();
+ // Then scan the archived dir.
+ await TelemetryArchive.promiseArchivedPingList();
+
+ // Check that the archive is in the correct state.
+ await checkArchive();
+
+ // Make sure the ping count is correct after the scan (one ping was removed).
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_SCAN_PING_COUNT"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 21,
+ "The histogram must count all the pings in the archive."
+ );
+ // One directory out of four was removed as well.
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 1,
+ "Telemetry must correctly report removed archive directories."
+ );
+ // Check that the remaining directories are correctly counted.
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_DIRECTORIES_COUNT"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 3,
+ "Telemetry must correctly report the remaining archive directories."
+ );
+ // Check that the remaining directories are correctly counted.
+ const oldestAgeInMonths = 1;
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ oldestAgeInMonths,
+ "Telemetry must correctly report age of the oldest directory in the archive."
+ );
+
+ // We need to test the archive size before we hit the quota, otherwise a special
+ // value is recorded.
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").clear();
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").clear();
+ Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS"
+ ).clear();
+
+ // Move the current date 60 days ahead of the second ping.
+ fakeNow(futureDate(oldestDirectoryDate, 60 * MILLISECONDS_PER_DAY));
+ // Reset TelemetryController and TelemetryArchive.
+ await TelemetryController.testReset();
+ // Wait for the cleanup to finish.
+ await TelemetryStorage.testCleanupTaskPromise();
+ // Then scan the archived dir again.
+ await TelemetryArchive.promiseArchivedPingList();
+
+ // Move the oldest ping to the unexpected pings list.
+ expectedPrunedInfo.push(expectedNotPrunedInfo.shift());
+ // Check that the archive is in the correct state.
+ await checkArchive();
+
+ // Find how much disk space the archive takes.
+ const archivedPingsInfo = await getArchivedPingsInfo();
+ let archiveSizeInBytes = archivedPingsInfo.reduce(
+ (lastResult, element) => lastResult + element.size,
+ 0
+ );
+
+ // Check that the correct values for quota probes are reported when no quota is hit.
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").snapshot();
+ Assert.equal(
+ h.sum,
+ Math.round(archiveSizeInBytes / 1024 / 1024),
+ "Telemetry must report the correct archive size."
+ );
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must report 0 evictions if quota is not hit."
+ );
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_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 = archiveSizeInBytes * 0.8;
+ fakeStorageQuota(testQuotaInBytes);
+
+ // The storage prunes archived pings until we reach 90% of the requested storage quota.
+ // Based on that, find how many pings should be kept.
+ const safeQuotaSize = testQuotaInBytes * 0.9;
+ let sizeInBytes = 0;
+ let pingsWithinQuota = [];
+ let pingsOutsideQuota = [];
+
+ for (let pingInfo of archivedPingsInfo) {
+ sizeInBytes += pingInfo.size;
+ if (sizeInBytes >= safeQuotaSize) {
+ pingsOutsideQuota.push({
+ id: pingInfo.id,
+ creationDate: new Date(pingInfo.timestamp),
+ });
+ continue;
+ }
+ pingsWithinQuota.push({
+ id: pingInfo.id,
+ creationDate: new Date(pingInfo.timestamp),
+ });
+ }
+
+ expectedNotPrunedInfo = pingsWithinQuota;
+ expectedPrunedInfo = expectedPrunedInfo.concat(pingsOutsideQuota);
+
+ // Reset TelemetryArchive and TelemetryController to start the startup cleanup.
+ await TelemetryController.testReset();
+ await TelemetryStorage.testCleanupTaskPromise();
+ await TelemetryArchive.promiseArchivedPingList();
+ // Check that the archive is in the correct state.
+ await checkArchive();
+
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ pingsOutsideQuota.length,
+ "Telemetry must correctly report the over quota pings evicted from the archive."
+ );
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").snapshot();
+ Assert.equal(
+ h.sum,
+ 300,
+ "Archive 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.testCleanupTaskPromise();
+ await TelemetryArchive.promiseArchivedPingList();
+ await checkArchive();
+
+ const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";
+ // Create and archive an oversized, uncompressed, ping.
+ const OVERSIZED_PING = {
+ id: OVERSIZED_PING_ID,
+ type: PING_TYPE,
+ creationDate: new Date().toISOString(),
+ // Generate a ~2MB string to use as the payload.
+ payload: generateRandomString(2 * 1024 * 1024),
+ };
+ await TelemetryArchive.promiseArchivePing(OVERSIZED_PING);
+
+ // Get the size of the archived ping.
+ const oversizedPingPath =
+ TelemetryStorage._testGetArchivedPingPath(
+ OVERSIZED_PING.id,
+ new Date(OVERSIZED_PING.creationDate),
+ PING_TYPE
+ ) + "lz4";
+ const archivedPingSizeMB = Math.floor(
+ (await IOUtils.stat(oversizedPingPath)).size / 1024 / 1024
+ );
+
+ // We expect the oversized ping to be pruned when scanning the archive.
+ expectedPrunedInfo.push({
+ id: OVERSIZED_PING_ID,
+ creationDate: new Date(OVERSIZED_PING.creationDate),
+ });
+
+ // Scan the archive.
+ await TelemetryController.testReset();
+ await TelemetryStorage.testCleanupTaskPromise();
+ await TelemetryArchive.promiseArchivedPingList();
+ // The following also checks that non oversized pings are not removed.
+ await checkArchive();
+
+ // Make sure we're correctly updating the related histograms.
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 1,
+ "Telemetry must report 1 oversized ping in the archive."
+ );
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB"
+ ).snapshot();
+ Assert.equal(
+ h.values[archivedPingSizeMB],
+ 1,
+ "Telemetry must report the correct size for the oversized ping."
+ );
+});
+
+add_task(async function test_clientId() {
+ // Check that a ping submitted after the delayed telemetry initialization completed
+ // should get a valid client id.
+ await TelemetryController.testReset();
+ const clientId = await ClientID.getClientID();
+
+ let id = await TelemetryController.submitExternalPing(
+ "test-type",
+ {},
+ { addClientId: true }
+ );
+ let ping = await TelemetryArchive.promiseArchivedPingById(id);
+
+ Assert.ok(!!ping, "Should have loaded the ping.");
+ Assert.ok("clientId" in ping, "Ping should have a client id.");
+ Assert.ok(UUID_REGEX.test(ping.clientId), "Client id is in UUID format.");
+ Assert.equal(
+ ping.clientId,
+ clientId,
+ "Ping client id should match the global client id."
+ );
+
+ // We should have cached the client id now. Lets confirm that by
+ // checking the client id on a ping submitted before the async
+ // controller setup is finished.
+ let promiseSetup = TelemetryController.testReset();
+ id = await TelemetryController.submitExternalPing(
+ "test-type",
+ {},
+ { addClientId: true }
+ );
+ ping = await TelemetryArchive.promiseArchivedPingById(id);
+ Assert.equal(ping.clientId, clientId);
+
+ // Finish setup.
+ await promiseSetup;
+});
+
+add_task(async function test_InvalidPingType() {
+ const TYPES = [
+ "a",
+ "-",
+ "¿€€€?",
+ "-foo-",
+ "-moo",
+ "zoo-",
+ ".bar",
+ "asfd.asdf",
+ ];
+
+ for (let type of TYPES) {
+ let histogram = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_INVALID_PING_TYPE_SUBMITTED"
+ );
+ Assert.ok(
+ !(type in histogram.snapshot()),
+ "Should not have counted this invalid ping yet: " + type
+ );
+ Assert.ok(
+ promiseRejects(TelemetryController.submitExternalPing(type, {})),
+ "Ping type should have been rejected."
+ );
+ Assert.equal(
+ histogram.snapshot()[type].sum,
+ 1,
+ "Should have counted this as an invalid ping type."
+ );
+ }
+});
+
+add_task(async function test_InvalidPayloadType() {
+ const PAYLOAD_TYPES = [19, "string", [1, 2, 3, 4], null, undefined];
+
+ let histogram = Telemetry.getHistogramById(
+ "TELEMETRY_INVALID_PAYLOAD_SUBMITTED"
+ );
+ for (let i = 0; i < PAYLOAD_TYPES.length; i++) {
+ histogram.clear();
+ Assert.equal(
+ histogram.snapshot().sum,
+ 0,
+ "Should not have counted this invalid payload yet: " +
+ JSON.stringify(PAYLOAD_TYPES[i])
+ );
+ Assert.ok(
+ await promiseRejects(
+ TelemetryController.submitExternalPing("payload-test", PAYLOAD_TYPES[i])
+ ),
+ "Payload type should have been rejected."
+ );
+ Assert.equal(
+ histogram.snapshot().sum,
+ 1,
+ "Should have counted this as an invalid payload type."
+ );
+ }
+});
+
+add_task(async function test_currentPingData() {
+ await TelemetryController.testSetup();
+
+ // Setup test data.
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+ h.clear();
+ h.add(1);
+ let k = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"
+ );
+ k.clear();
+ k.add("a", 1);
+
+ // Get current ping data objects and check that their data is sane.
+ for (let subsession of [true, false]) {
+ let ping = TelemetryController.getCurrentPingData(subsession);
+
+ Assert.ok(!!ping, "Should have gotten a ping.");
+ Assert.equal(ping.type, "main", "Ping should have correct type.");
+ const expectedReason = subsession
+ ? "gather-subsession-payload"
+ : "gather-payload";
+ Assert.equal(
+ ping.payload.info.reason,
+ expectedReason,
+ "Ping should have the correct reason."
+ );
+
+ let id = "TELEMETRY_TEST_RELEASE_OPTOUT";
+ Assert.ok(
+ id in ping.payload.histograms,
+ "Payload should have test count histogram."
+ );
+ Assert.equal(
+ ping.payload.histograms[id].sum,
+ 1,
+ "Test count value should match."
+ );
+ id = "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT";
+ Assert.ok(
+ id in ping.payload.keyedHistograms,
+ "Payload should have keyed test histogram."
+ );
+ Assert.equal(
+ ping.payload.keyedHistograms[id].a.sum,
+ 1,
+ "Keyed test value should match."
+ );
+ }
+});
+
+add_task(async function test_shutdown() {
+ await TelemetryController.testShutdown();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_PingSender.js b/toolkit/components/telemetry/tests/unit/test_PingSender.js
new file mode 100644
index 0000000000..7c9c920025
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_PingSender.js
@@ -0,0 +1,284 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+// This tests submitting a ping using the stand-alone pingsender program.
+
+"use strict";
+
+const { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+const { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+);
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function generateTestPingData() {
+ return {
+ type: "test-pingsender-type",
+ id: TelemetryUtils.generateUUID(),
+ creationDate: new Date().toISOString(),
+ version: 4,
+ payload: {
+ dummy: "stuff",
+ },
+ };
+}
+
+function testSendingPings(pingPaths) {
+ const url = "http://localhost:" + PingServer.port + "/submit/telemetry/";
+ const pings = pingPaths.map(path => {
+ return {
+ url,
+ path,
+ };
+ });
+ TelemetrySend.testRunPingSender(pings, (_, topic, __) => {
+ switch (topic) {
+ case "process-finished": // finished indicates an exit code of 0
+ Assert.ok(true, "Pingsender should be able to post to localhost");
+ break;
+ case "process-failed": // failed indicates an exit code != 0
+ Assert.ok(false, "Pingsender should be able to post to localhost");
+ break;
+ }
+ });
+}
+
+/**
+ * Wait for a ping file to be deleted from the pending pings directory.
+ */
+function waitForPingDeletion(pingId) {
+ const path = PathUtils.join(TelemetryStorage.pingDirectoryPath, pingId);
+
+ let checkFn = (resolve, reject) =>
+ setTimeout(() => {
+ IOUtils.exists(path).then(exists => {
+ if (!exists) {
+ Assert.ok(true, `${pingId} was deleted`);
+ resolve();
+ } else {
+ checkFn(resolve, reject);
+ }
+ }, reject);
+ }, 250);
+
+ return new Promise((resolve, reject) => checkFn(resolve, reject));
+}
+
+add_task(async function setup() {
+ // Init the profile.
+ do_get_profile(true);
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Start the ping server and let Telemetry know about it.
+ PingServer.start();
+});
+
+async function test_pingSender(version = "1.0") {
+ // Generate a new ping and save it among the pending pings.
+ const data = generateTestPingData();
+ await TelemetryStorage.savePing(data, true);
+
+ // Get the local path of the saved ping.
+ const pingPath = PathUtils.join(TelemetryStorage.pingDirectoryPath, data.id);
+
+ // Spawn an HTTP server that returns an error. We will be running the
+ // PingSender twice, trying to send the ping to this server. After the
+ // second time, we will resolve |deferred404Hit|.
+ let failingServer = new HttpServer();
+ let deferred404Hit = PromiseUtils.defer();
+ let hitCount = 0;
+ failingServer.registerPathHandler("/lookup_fail", (metadata, response) => {
+ response.setStatusLine("1.1", 404, "Not Found");
+ hitCount++;
+
+ if (hitCount >= 2) {
+ // Resolve the promise on the next tick.
+ Services.tm.dispatchToMainThread(() => deferred404Hit.resolve());
+ }
+ });
+ failingServer.start(-1);
+
+ // Try to send the ping twice using the pingsender (we expect 404 both times).
+ const errorUrl =
+ "http://localhost:" + failingServer.identity.primaryPort + "/lookup_fail";
+ TelemetrySend.testRunPingSender([{ url: errorUrl, path: pingPath }]);
+ TelemetrySend.testRunPingSender([{ url: errorUrl, path: pingPath }]);
+
+ // Wait until we hit the 404 server twice. After that, make sure that the ping
+ // still exists locally.
+ await deferred404Hit.promise;
+ Assert.ok(
+ await IOUtils.exists(pingPath),
+ "The pending ping must not be deleted if we fail to send using the PingSender"
+ );
+
+ // Try to send it using the pingsender.
+ testSendingPings([pingPath]);
+
+ let req = await PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+
+ Assert.equal(
+ req.getHeader("User-Agent"),
+ `pingsender/${version}`,
+ "Should have received the correct user agent string."
+ );
+ Assert.equal(
+ req.getHeader("X-PingSender-Version"),
+ version,
+ "Should have received the correct PingSender version string."
+ );
+ Assert.equal(
+ req.getHeader("Content-Encoding"),
+ "gzip",
+ "Should have a gzip encoded ping."
+ );
+ Assert.ok(req.getHeader("Date"), "Should have received a Date header.");
+ Assert.equal(ping.id, data.id, "Should have received the correct ping id.");
+ Assert.equal(
+ ping.type,
+ data.type,
+ "Should have received the correct ping type."
+ );
+ Assert.deepEqual(
+ ping.payload,
+ data.payload,
+ "Should have received the correct payload."
+ );
+
+ // Check that the PingSender removed the pending ping.
+ await waitForPingDeletion(data.id);
+
+ // Shut down the failing server.
+ await new Promise(r => failingServer.stop(r));
+}
+
+add_task(async function test_pingsender1() {
+ let orig = Services.prefs.getBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ false
+ );
+ try {
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ false
+ );
+ await test_pingSender("1.0");
+ } finally {
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ orig
+ );
+ }
+});
+
+add_task(async function test_pingsender2() {
+ let orig = Services.prefs.getBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ false
+ );
+ try {
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ true
+ );
+ await test_pingSender("2.0");
+ } finally {
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ orig
+ );
+ }
+});
+
+add_task(async function test_bannedDomains() {
+ // Generate a new ping and save it among the pending pings.
+ const data = generateTestPingData();
+ await TelemetryStorage.savePing(data, true);
+
+ // Get the local path of the saved ping.
+ const pingPath = PathUtils.join(TelemetryStorage.pingDirectoryPath, data.id);
+
+ // Confirm we can't send a ping to another destination url
+ let bannedUris = [
+ "https://example.com",
+ "http://localhost.com",
+ "http://localHOST.com",
+ "http://localhost@example.com",
+ "http://localhost:bob@example.com",
+ "http://localhost:localhost@localhost.example.com",
+ ];
+ for (let url of bannedUris) {
+ let result = await new Promise(resolve =>
+ TelemetrySend.testRunPingSender(
+ [{ url, path: pingPath }],
+ (_, topic, __) => {
+ switch (topic) {
+ case "process-finished": // finished indicates an exit code of 0
+ case "process-failed": // failed indicates an exit code != 0
+ resolve(topic);
+ }
+ }
+ )
+ );
+ Assert.equal(
+ result,
+ "process-failed",
+ `Pingsender should not be able to post to ${url}`
+ );
+ }
+});
+
+add_task(async function test_pingSender_multiple_pings() {
+ // Generate two new pings and save them among the pending pings.
+ const data = [generateTestPingData(), generateTestPingData()];
+
+ for (const d of data) {
+ await TelemetryStorage.savePing(d, true);
+ }
+
+ // Get the local path of the saved pings.
+ const pingPaths = data.map(d =>
+ PathUtils.join(TelemetryStorage.pingDirectoryPath, d.id)
+ );
+
+ // Try to send them using the pingsender.
+ testSendingPings(pingPaths);
+
+ // Check the pings. We don't have an ordering guarantee, so we move the
+ // elements to a new array when we find them.
+ let data2 = [];
+ while (data.length) {
+ let req = await PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+ let idx = data.findIndex(d => d.id == ping.id);
+ Assert.ok(
+ idx >= 0,
+ `Should have received the correct ping id: ${data[idx].id}`
+ );
+ data2.push(data[idx]);
+ data.splice(idx, 1);
+ }
+
+ // Check that the PingSender removed the pending pings.
+ for (const d of data2) {
+ await waitForPingDeletion(d.id);
+ }
+});
+
+add_task(async function cleanup() {
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_RDDScalars.js b/toolkit/components/telemetry/tests/unit/test_RDDScalars.js
new file mode 100644
index 0000000000..e7078012d2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_RDDScalars.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const RDD_ONLY_UINT_SCALAR = "telemetry.test.rdd_only_uint";
+
+const rddProcessTest = () => {
+ return Cc["@mozilla.org/rdd-process-test;1"].createInstance(
+ Ci.nsIRddProcessTest
+ );
+};
+
+/**
+ * This function waits until rdd scalars are reported into the
+ * scalar snapshot.
+ */
+async function waitForRddScalars() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const scalars = Telemetry.getSnapshotForScalars("main", false);
+ return Object.keys(scalars).includes("rdd");
+ }, "Waiting for rdd scalars to have been set");
+}
+
+add_setup(async function setup_telemetry_rdd() {
+ do_get_profile(true);
+ await TelemetryController.testSetup();
+});
+
+add_task(async function test_scalars_in_rdd_process() {
+ Telemetry.clearScalars();
+ const pid = await rddProcessTest().testTelemetryProbes();
+ info(`Started some RDD: ${pid}`);
+
+ // Once scalars are set by the rdd process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ await waitForRddScalars();
+
+ Assert.equal(
+ Telemetry.getSnapshotForScalars("main", false).rdd[RDD_ONLY_UINT_SCALAR],
+ 42,
+ `${RDD_ONLY_UINT_SCALAR} must have the correct value (rdd process).`
+ );
+
+ await rddProcessTest().stopProcess();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_SocketScalars.js b/toolkit/components/telemetry/tests/unit/test_SocketScalars.js
new file mode 100644
index 0000000000..a685a611b2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_SocketScalars.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const SOCKET_ONLY_UINT_SCALAR = "telemetry.test.socket_only_uint";
+
+/**
+ * This function waits until socket scalars are reported into the
+ * scalar snapshot.
+ */
+async function waitForSocketScalars() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const scalars = Telemetry.getSnapshotForScalars("main", false);
+ return Object.keys(scalars).includes("socket");
+ }, "Waiting for socket scalars to have been set");
+}
+
+add_task(async function test_scalars_in_socket_process() {
+ Assert.ok(
+ Services.prefs.getBoolPref("network.process.enabled"),
+ "Socket process should be enabled"
+ );
+
+ do_get_profile(true);
+ await TelemetryController.testSetup();
+
+ Services.io.socketProcessTelemetryPing();
+
+ // Once scalars are set by the socket process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ // Note: this requires the socket process to be enabled (see bug 1716307).
+ await waitForSocketScalars();
+
+ Assert.equal(
+ Telemetry.getSnapshotForScalars("main", false).socket[
+ SOCKET_ONLY_UINT_SCALAR
+ ],
+ 42,
+ `${SOCKET_ONLY_UINT_SCALAR} must have the correct value (socket process).`
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
new file mode 100644
index 0000000000..4ffa10f879
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
@@ -0,0 +1,289 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+const { TelemetryArchive } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryArchive.sys.mjs"
+);
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+
+const MS_IN_ONE_HOUR = 60 * 60 * 1000;
+const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR;
+
+const PREF_BRANCH = "toolkit.telemetry.";
+
+const REASON_ABORTED_SESSION = "aborted-session";
+const REASON_DAILY = "daily";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+const REASON_SHUTDOWN = "shutdown";
+
+var promiseValidateArchivedPings = async function (aExpectedReasons) {
+ // The list of ping reasons which mark the session end (and must reset the subsession
+ // count).
+ const SESSION_END_PING_REASONS = new Set([
+ REASON_ABORTED_SESSION,
+ REASON_SHUTDOWN,
+ ]);
+
+ let list = await TelemetryArchive.promiseArchivedPingList();
+
+ // We're just interested in the "main" pings.
+ list = list.filter(p => p.type == "main");
+
+ Assert.equal(
+ aExpectedReasons.length,
+ list.length,
+ "All the expected pings must be received."
+ );
+
+ let previousPing = await TelemetryArchive.promiseArchivedPingById(list[0].id);
+ Assert.equal(
+ aExpectedReasons.shift(),
+ previousPing.payload.info.reason,
+ "Telemetry should only get pings with expected reasons."
+ );
+ Assert.equal(
+ previousPing.payload.info.previousSessionId,
+ null,
+ "The first session must report a null previous session id."
+ );
+ Assert.equal(
+ previousPing.payload.info.previousSubsessionId,
+ null,
+ "The first subsession must report a null previous subsession id."
+ );
+ Assert.equal(
+ previousPing.payload.info.profileSubsessionCounter,
+ 1,
+ "profileSubsessionCounter must be 1 the first time."
+ );
+ Assert.equal(
+ previousPing.payload.info.subsessionCounter,
+ 1,
+ "subsessionCounter must be 1 the first time."
+ );
+
+ let expectedSubsessionCounter = 1;
+ let expectedPreviousSessionId = previousPing.payload.info.sessionId;
+
+ for (let i = 1; i < list.length; i++) {
+ let currentPing = await TelemetryArchive.promiseArchivedPingById(
+ list[i].id
+ );
+ let currentInfo = currentPing.payload.info;
+ let previousInfo = previousPing.payload.info;
+ info(
+ "Archive entry " +
+ i +
+ " - id: " +
+ currentPing.id +
+ ", reason: " +
+ currentInfo.reason
+ );
+
+ Assert.equal(
+ aExpectedReasons.shift(),
+ currentInfo.reason,
+ "Telemetry should only get pings with expected reasons."
+ );
+ Assert.equal(
+ currentInfo.previousSessionId,
+ expectedPreviousSessionId,
+ "Telemetry must correctly chain session identifiers."
+ );
+ Assert.equal(
+ currentInfo.previousSubsessionId,
+ previousInfo.subsessionId,
+ "Telemetry must correctly chain subsession identifiers."
+ );
+ Assert.equal(
+ currentInfo.profileSubsessionCounter,
+ previousInfo.profileSubsessionCounter + 1,
+ "Telemetry must correctly track the profile subsessions count."
+ );
+ Assert.equal(
+ currentInfo.subsessionCounter,
+ expectedSubsessionCounter,
+ "The subsession counter should be monotonically increasing."
+ );
+
+ // Store the current ping as previous.
+ previousPing = currentPing;
+ // Reset the expected subsession counter, if required. Otherwise increment the expected
+ // subsession counter.
+ // If this is the final subsession of a session we need to update expected values accordingly.
+ if (SESSION_END_PING_REASONS.has(currentInfo.reason)) {
+ expectedSubsessionCounter = 1;
+ expectedPreviousSessionId = currentInfo.sessionId;
+ } else {
+ expectedSubsessionCounter++;
+ }
+ }
+};
+
+add_task(async function test_setup() {
+ do_test_pending();
+
+ // Addon manager needs a profile directory
+ 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();
+});
+
+add_task(async function test_subsessionsChaining() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android, so skip the next checks.
+ return;
+ }
+
+ const PREF_TEST = PREF_BRANCH + "test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ ]);
+ Preferences.reset(PREF_TEST);
+
+ // Fake the clock data to manually trigger an aborted-session ping and a daily ping.
+ // This is also helpful to make sure we get the archived pings in an expected order.
+ let now = fakeNow(2009, 9, 18, 0, 0, 0);
+ let monotonicNow = fakeMonotonicNow(1000);
+
+ let moveClockForward = minutes => {
+ let ms = minutes * MILLISECONDS_PER_MINUTE;
+ now = fakeNow(futureDate(now, ms));
+ monotonicNow = fakeMonotonicNow(monotonicNow + ms);
+ };
+
+ // Keep track of the ping reasons we're expecting in this test.
+ let expectedReasons = [];
+
+ // Start and shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 1,
+ // subsessionCounter: 1, subsessionId: A, and previousSubsessionId: null to be archived.
+ await TelemetryController.testSetup();
+ await TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry but don't wait for it to initialise before shutting down. We expect a
+ // shutdown ping with profileSubsessionCounter: 2, subsessionCounter: 1, subsessionId: B
+ // and previousSubsessionId: A to be archived.
+ moveClockForward(30);
+ TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry and simulate an aborted-session ping. We expect an aborted-session ping
+ // with profileSubsessionCounter: 3, subsessionCounter: 1, subsessionId: C and
+ // previousSubsessionId: B to be archived.
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+ moveClockForward(6);
+ // Trigger the an aborted session ping save. When testing,we are not saving the aborted-session
+ // ping as soon as Telemetry starts, otherwise we would end up with unexpected pings being
+ // sent when calling |TelemetryController.testReset()|, thus breaking some tests.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+ expectedReasons.push(REASON_ABORTED_SESSION);
+
+ // Start Telemetry and trigger an environment change through a pref modification. We expect
+ // an environment-change ping with profileSubsessionCounter: 4, subsessionCounter: 1,
+ // subsessionId: D and previousSubsessionId: C to be archived.
+ moveClockForward(30);
+ await TelemetryController.testReset();
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ moveClockForward(30);
+ Preferences.set(PREF_TEST, 1);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // Shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 5,
+ // subsessionCounter: 2, subsessionId: E and previousSubsessionId: D to be archived.
+ moveClockForward(30);
+ await TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry and trigger a daily ping. We expect a daily ping with
+ // profileSubsessionCounter: 6, subsessionCounter: 1, subsessionId: F and
+ // previousSubsessionId: E to be archived.
+ moveClockForward(30);
+ await TelemetryController.testReset();
+
+ // Delay the callback around midnight.
+ now = fakeNow(futureDate(now, MS_IN_ONE_DAY));
+ // Trigger the daily ping.
+ await schedulerTickCallback();
+ expectedReasons.push(REASON_DAILY);
+
+ // Trigger an environment change ping. We expect an environment-changed ping with
+ // profileSubsessionCounter: 7, subsessionCounter: 2, subsessionId: G and
+ // previousSubsessionId: F to be archived.
+ moveClockForward(30);
+ Preferences.set(PREF_TEST, 0);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // Shut down Telemetry and trigger a shutdown ping.
+ moveClockForward(30);
+ await TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry and trigger an environment change.
+ await TelemetryController.testReset();
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ moveClockForward(30);
+ Preferences.set(PREF_TEST, 1);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // Don't shut down, instead trigger an aborted-session ping.
+ moveClockForward(6);
+ // Trigger the an aborted session ping save.
+ await schedulerTickCallback();
+ expectedReasons.push(REASON_ABORTED_SESSION);
+
+ // Start Telemetry and trigger a daily ping.
+ moveClockForward(30);
+ await TelemetryController.testReset();
+ // Delay the callback around midnight.
+ now = futureDate(now, MS_IN_ONE_DAY);
+ fakeNow(now);
+ // Trigger the daily ping.
+ await schedulerTickCallback();
+ expectedReasons.push(REASON_DAILY);
+
+ // Trigger an environment change.
+ moveClockForward(30);
+ Preferences.set(PREF_TEST, 0);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // And an aborted-session ping again.
+ moveClockForward(6);
+ // Trigger the an aborted session ping save.
+ await schedulerTickCallback();
+ expectedReasons.push(REASON_ABORTED_SESSION);
+
+ // Make sure the aborted-session ping gets archived.
+ await TelemetryController.testReset();
+
+ await promiseValidateArchivedPings(expectedReasons);
+});
+
+add_task(async function () {
+ await TelemetryController.testShutdown();
+ do_test_finished();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_SyncPingIntegration.js b/toolkit/components/telemetry/tests/unit/test_SyncPingIntegration.js
new file mode 100644
index 0000000000..16a18644e9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_SyncPingIntegration.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// Enable the collection (during test) for all products so even products
+// that don't collect the data will be able to run the test without failure.
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+add_setup(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+});
+
+add_task(async function test_register_twice_fails() {
+ TelemetryController.registerSyncPingShutdown(() => {});
+ Assert.throws(
+ () => TelemetryController.registerSyncPingShutdown(() => {}),
+ /The sync ping shutdown handler is already registered./
+ );
+ await TelemetryController.testReset();
+});
+
+add_task(async function test_reset_clears_handler() {
+ await TelemetryController.testSetup();
+ TelemetryController.registerSyncPingShutdown(() => {});
+ await TelemetryController.testReset();
+ // If this works the reset must have cleared it.
+ TelemetryController.registerSyncPingShutdown(() => {});
+ await TelemetryController.testReset();
+});
+
+add_task(async function test_shutdown_handler_submits() {
+ let handlerCalled = false;
+ await TelemetryController.testSetup();
+ TelemetryController.registerSyncPingShutdown(() => {
+ handlerCalled = true;
+ // and submit a ping.
+ let ping = {
+ why: "shutdown",
+ };
+ TelemetryController.submitExternalPing("sync", ping);
+ });
+
+ await TelemetryController.testShutdown();
+ Assert.ok(handlerCalled);
+ await TelemetryController.testReset();
+});
+
+add_task(async function test_shutdown_handler_no_submit() {
+ let handlerCalled = false;
+ await TelemetryController.testSetup();
+ TelemetryController.registerSyncPingShutdown(() => {
+ handlerCalled = true;
+ // but don't submit a ping.
+ });
+
+ await TelemetryController.testShutdown();
+ Assert.ok(handlerCalled);
+});
+
+// NB: The last test in this file *must not* restart TelemetryController, or it
+// will cause intermittent timeouts for this test.
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryAndroidEnvironment.js b/toolkit/components/telemetry/tests/unit/test_TelemetryAndroidEnvironment.js
new file mode 100644
index 0000000000..b5035ff56c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryAndroidEnvironment.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+/* Android-only TelemetryEnvironment xpcshell test that ensures that the device data is stored in the Environment.
+ */
+
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+
+/**
+ * Check that a value is a string and not empty.
+ *
+ * @param aValue The variable to check.
+ * @return True if |aValue| has type "string" and is not empty, False otherwise.
+ */
+function checkString(aValue) {
+ return typeof aValue == "string" && aValue != "";
+}
+
+/**
+ * If value is non-null, check if it's a valid string.
+ *
+ * @param aValue The variable to check.
+ * @return True if it's null or a valid string, false if it's non-null and an invalid
+ * string.
+ */
+function checkNullOrString(aValue) {
+ if (aValue) {
+ return checkString(aValue);
+ } else if (aValue === null) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * If value is non-null, check if it's a boolean.
+ *
+ * @param aValue The variable to check.
+ * @return True if it's null or a valid boolean, false if it's non-null and an invalid
+ * boolean.
+ */
+function checkNullOrBool(aValue) {
+ return aValue === null || typeof aValue == "boolean";
+}
+
+function checkSystemSection(data) {
+ Assert.ok("system" in data, "There must be a system section in Environment.");
+ // Device data is only available on Android.
+ if (gIsAndroid) {
+ let deviceData = data.system.device;
+ Assert.ok(checkNullOrString(deviceData.model));
+ Assert.ok(checkNullOrString(deviceData.manufacturer));
+ Assert.ok(checkNullOrString(deviceData.hardware));
+ Assert.ok(checkNullOrBool(deviceData.isTablet));
+ }
+}
+
+add_task(async function test_systemEnvironment() {
+ let environmentData = TelemetryEnvironment.currentEnvironment;
+ checkSystemSection(environmentData);
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryChildEvents_buildFaster.js b/toolkit/components/telemetry/tests/unit/test_TelemetryChildEvents_buildFaster.js
new file mode 100644
index 0000000000..00a45c8b12
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryChildEvents_buildFaster.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+const TEST_STATIC_EVENT_NAME = "telemetry.test";
+const TEST_EVENT_NAME = "telemetry.test.child";
+
+function run_child_test() {
+ Telemetry.recordEvent(TEST_EVENT_NAME, "child", "builtin");
+ Telemetry.recordEvent(TEST_STATIC_EVENT_NAME, "main_and_content", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "child", "anotherone");
+}
+
+/**
+ * This function waits until content events are reported into the
+ * events snapshot.
+ */
+async function waitForContentEvents() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ return Object.keys(snapshot).includes("content");
+ });
+}
+
+add_task(async function test_setup() {
+ if (!runningInParent) {
+ TelemetryController.testSetupContent();
+ run_child_test();
+ do_send_remote_message(MESSAGE_CHILD_TEST_DONE);
+ return;
+ }
+
+ // Setup.
+ do_get_profile(true);
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ await TelemetryController.testSetup();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ // Enable recording for the test event category.
+
+ // Register some dynamic builtin test events.
+ Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
+ dynamic: {
+ methods: ["dynamic", "child"],
+ objects: ["builtin", "anotherone"],
+ },
+ dynamic_expired: {
+ methods: ["check"],
+ objects: ["expiry"],
+ expired: true,
+ },
+ });
+ Telemetry.setEventRecordingEnabled(TEST_STATIC_EVENT_NAME, true);
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+
+ Telemetry.recordEvent(TEST_EVENT_NAME, "dynamic", "builtin");
+ Telemetry.recordEvent(TEST_STATIC_EVENT_NAME, "main_and_content", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "dynamic", "anotherone");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "check", "expiry");
+
+ // Run test in child, don't wait for it to finish: just wait for the
+ // MESSAGE_CHILD_TEST_DONE.
+ run_test_in_child("test_TelemetryChildEvents_buildFaster.js");
+ await do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
+
+ // Once events are set by the content process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ await waitForContentEvents();
+
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+ Assert.ok(
+ "content" in snapshot,
+ "Should have content events in the snapshot."
+ );
+
+ // All events should now be recorded in the right order
+ let expectedParent = [
+ [TEST_EVENT_NAME, "dynamic", "builtin"],
+ [TEST_STATIC_EVENT_NAME, "main_and_content", "object1"],
+ [TEST_EVENT_NAME, "dynamic", "anotherone"],
+ ];
+ let expectedContent = [
+ [TEST_EVENT_NAME, "child", "builtin"],
+ [TEST_STATIC_EVENT_NAME, "main_and_content", "object1"],
+ [TEST_EVENT_NAME, "child", "anotherone"],
+ ];
+
+ Assert.equal(
+ snapshot.parent.length,
+ expectedParent.length,
+ "Should have recorded the right amount of events in parent."
+ );
+ for (let i = 0; i < expectedParent.length; ++i) {
+ Assert.deepEqual(
+ snapshot.parent[i].slice(1),
+ expectedParent[i],
+ "Should have recorded the expected event data in parent."
+ );
+ }
+
+ Assert.equal(
+ snapshot.content.length,
+ expectedContent.length,
+ "Should have recorded the right amount of events in content."
+ );
+ for (let i = 0; i < expectedContent.length; ++i) {
+ Assert.deepEqual(
+ snapshot.content[i].slice(1),
+ expectedContent[i],
+ "Should have recorded the expected event data in content."
+ );
+ }
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryClientID_reset.js b/toolkit/components/telemetry/tests/unit/test_TelemetryClientID_reset.js
new file mode 100644
index 0000000000..6e59f65633
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryClientID_reset.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+const DELETION_REQUEST_PING_TYPE = "deletion-request";
+const TEST_PING_TYPE = "test-ping-type";
+
+function sendPing(addEnvironment = false) {
+ let options = {
+ addClientId: true,
+ addEnvironment,
+ };
+ return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ await new Promise(resolve =>
+ Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))
+ );
+
+ PingServer.start();
+ Preferences.set(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+ await TelemetryController.testSetup();
+});
+
+/**
+ * Testing the following scenario:
+ *
+ * 1. Telemetry upload gets disabled
+ * 2. Canary client ID is set
+ * 3. Instance is shut down
+ * 4. Telemetry upload flag is toggled
+ * 5. Instance is started again
+ * 6. Detect that upload is enabled and reset client ID
+ *
+ * This scenario e.g. happens when switching between channels
+ * with and without the deletion-request ping reset included.
+ */
+add_task(async function test_clientid_reset_after_reenabling() {
+ await sendPing();
+ let ping = await PingServer.promiseNextPing();
+ Assert.equal(ping.type, TEST_PING_TYPE, "The ping must be a test ping");
+ Assert.ok("clientId" in ping);
+
+ let firstClientId = ping.clientId;
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ firstClientId,
+ "Client ID should be valid and random"
+ );
+
+ // Disable FHR upload: this should trigger a deletion-request ping.
+ Preferences.set(TelemetryUtils.Preferences.FhrUploadEnabled, false);
+
+ ping = await PingServer.promiseNextPing();
+ Assert.equal(
+ ping.type,
+ DELETION_REQUEST_PING_TYPE,
+ "The ping must be a deletion-request ping"
+ );
+ Assert.equal(ping.clientId, firstClientId);
+ let clientId = await ClientID.getClientID();
+ Assert.equal(TelemetryUtils.knownClientID, clientId);
+
+ // Now shutdown the instance
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+
+ // Flip the pref again
+ Preferences.set(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Start the instance
+ await TelemetryController.testReset();
+
+ let newClientId = await ClientID.getClientID();
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ newClientId,
+ "Client ID should be valid and random"
+ );
+ Assert.notEqual(
+ firstClientId,
+ newClientId,
+ "Client ID should be newly generated"
+ );
+});
+
+/**
+ * Testing the following scenario:
+ * (Reverse of the first test)
+ *
+ * 1. Telemetry upload gets disabled, canary client ID is set
+ * 2. Telemetry upload is enabled
+ * 3. New client ID is generated.
+ * 3. Instance is shut down
+ * 4. Telemetry upload flag is toggled
+ * 5. Instance is started again
+ * 6. Detect that upload is disabled and sets canary client ID
+ *
+ * This scenario e.g. happens when switching between channels
+ * with and without the deletion-request ping reset included.
+ */
+add_task(async function test_clientid_canary_after_disabling() {
+ await sendPing();
+ let ping = await PingServer.promiseNextPing();
+ Assert.equal(ping.type, TEST_PING_TYPE, "The ping must be a test ping");
+ Assert.ok("clientId" in ping);
+
+ let firstClientId = ping.clientId;
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ firstClientId,
+ "Client ID should be valid and random"
+ );
+
+ // Disable FHR upload: this should trigger a deletion-request ping.
+ Preferences.set(TelemetryUtils.Preferences.FhrUploadEnabled, false);
+
+ ping = await PingServer.promiseNextPing();
+ Assert.equal(
+ ping.type,
+ DELETION_REQUEST_PING_TYPE,
+ "The ping must be a deletion-request ping"
+ );
+ Assert.equal(ping.clientId, firstClientId);
+ let clientId = await ClientID.getClientID();
+ Assert.equal(TelemetryUtils.knownClientID, clientId);
+
+ Preferences.set(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+ await sendPing();
+ ping = await PingServer.promiseNextPing();
+ Assert.equal(ping.type, TEST_PING_TYPE, "The ping must be a test ping");
+ Assert.notEqual(
+ firstClientId,
+ ping.clientId,
+ "Client ID should be newly generated"
+ );
+
+ // Now shutdown the instance
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+
+ // Flip the pref again
+ Preferences.set(TelemetryUtils.Preferences.FhrUploadEnabled, false);
+
+ // Start the instance
+ await TelemetryController.testReset();
+
+ let newClientId = await ClientID.getClientID();
+ Assert.equal(
+ TelemetryUtils.knownClientID,
+ newClientId,
+ "Client ID should be a canary when upload disabled"
+ );
+});
+
+add_task(async function stopServer() {
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
new file mode 100644
index 0000000000..41fcd2a9bc
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -0,0 +1,1218 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* This testcase triggers two telemetry pings.
+ *
+ * Telemetry code keeps histograms of past telemetry pings. The first
+ * ping populates these histograms. One of those histograms is then
+ * checked in the second request.
+ */
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+);
+const { TelemetryArchive } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryArchive.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const { TelemetryArchiveTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryArchiveTesting.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ JsonSchemaValidator:
+ "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
+ jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs",
+});
+
+const PING_FORMAT_VERSION = 4;
+const DELETION_REQUEST_PING_TYPE = "deletion-request";
+const TEST_PING_TYPE = "test-ping-type";
+
+var gClientID = null;
+
+XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", async function () {
+ return PathUtils.join(PathUtils.profileDir, "datareporting");
+});
+
+function sendPing(aSendClientId, aSendEnvironment) {
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ } else {
+ TelemetrySend.setServer("http://doesnotexist");
+ }
+
+ let options = {
+ addClientId: aSendClientId,
+ addEnvironment: aSendEnvironment,
+ };
+ return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
+}
+
+function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {
+ const MANDATORY_PING_FIELDS = [
+ "type",
+ "id",
+ "creationDate",
+ "version",
+ "application",
+ "payload",
+ ];
+
+ const APPLICATION_TEST_DATA = {
+ buildId: gAppInfo.appBuildID,
+ name: APP_NAME,
+ version: APP_VERSION,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ // Check that the ping contains all the mandatory fields.
+ for (let f of MANDATORY_PING_FIELDS) {
+ Assert.ok(f in aPing, f + " must be available.");
+ }
+
+ Assert.equal(aPing.type, aType, "The ping must have the correct type.");
+ Assert.equal(
+ aPing.version,
+ PING_FORMAT_VERSION,
+ "The ping must have the correct version."
+ );
+
+ // Test the application section.
+ for (let f in APPLICATION_TEST_DATA) {
+ Assert.equal(
+ aPing.application[f],
+ APPLICATION_TEST_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // We can't check the values for channel and architecture. Just make
+ // sure they are in.
+ Assert.ok(
+ "architecture" in aPing.application,
+ "The application section must have an architecture field."
+ );
+ Assert.ok(
+ "channel" in aPing.application,
+ "The application section must have a channel field."
+ );
+
+ // Check the clientId and environment fields, as needed.
+ Assert.equal("clientId" in aPing, aHasClientId);
+ Assert.equal("environment" in aPing, aHasEnvironment);
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ 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.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ await new Promise(resolve =>
+ Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))
+ );
+});
+
+add_task(async function asyncSetup() {
+ await TelemetryController.testSetup();
+});
+
+// Ensure that not overwriting an existing file fails silently
+add_task(async function test_overwritePing() {
+ let ping = { id: "foo" };
+ await TelemetryStorage.savePing(ping, true);
+ await TelemetryStorage.savePing(ping, false);
+ await TelemetryStorage.cleanupPingFile(ping);
+});
+
+// Checks that a sent ping is correctly received by a dummy http server.
+add_task(async function test_simplePing() {
+ PingServer.start();
+ // Update the Telemetry Server preference with the address of the local server.
+ // Otherwise we might end up sending stuff to a non-existing server after
+ // |TelemetryController.testReset| is called.
+ Preferences.set(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ await sendPing(false, false);
+ let request = await PingServer.promiseNextRequest();
+
+ let ping = decodeRequestPayload(request);
+ checkPingFormat(ping, TEST_PING_TYPE, false, false);
+});
+
+add_task(async function test_disableDataUpload() {
+ const OPTIN_PROBE = "telemetry.data_upload_optin";
+ const isUnified = Preferences.get(TelemetryUtils.Preferences.Unified, false);
+ if (!isUnified) {
+ // Skipping the test if unified telemetry is off, as no deletion-request ping will be generated.
+ return;
+ }
+
+ // Check that the optin probe is not set.
+ // (If there are no recorded scalars, "parent" will be undefined).
+ let snapshot = Telemetry.getSnapshotForScalars("main", false).parent || {};
+ Assert.ok(
+ !(OPTIN_PROBE in snapshot),
+ "Data optin scalar should not be set at start"
+ );
+
+ // Send a first ping to get the current used client id
+ await sendPing(true, false);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ let firstClientId = ping.clientId;
+
+ Assert.ok(firstClientId, "Test ping needs a client ID");
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ firstClientId,
+ "Client ID should be valid and random"
+ );
+
+ // The next step should trigger an event, watch for it.
+ let disableObserved = TestUtils.topicObserved(
+ TelemetryUtils.TELEMETRY_UPLOAD_DISABLED_TOPIC
+ );
+
+ // Disable FHR upload: this should trigger a deletion-request ping.
+ Preferences.set(TelemetryUtils.Preferences.FhrUploadEnabled, false);
+
+ // Wait for the disable event
+ await disableObserved;
+
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_REQUEST_PING_TYPE, true, false);
+ // Wait on ping activity to settle.
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ snapshot = Telemetry.getSnapshotForScalars("main", false).parent || {};
+ Assert.ok(
+ !(OPTIN_PROBE in snapshot),
+ "Data optin scalar should not be set after opt out"
+ );
+
+ // Restore FHR Upload.
+ Preferences.set(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // We need to wait until the scalar is set
+ await ContentTaskUtils.waitForCondition(() => {
+ const scalarSnapshot = Telemetry.getSnapshotForScalars("main", false);
+ return (
+ Object.keys(scalarSnapshot).includes("parent") &&
+ OPTIN_PROBE in scalarSnapshot.parent
+ );
+ });
+
+ snapshot = Telemetry.getSnapshotForScalars("main", false).parent || {};
+ Assert.ok(
+ snapshot[OPTIN_PROBE],
+ "Enabling data upload should set optin probe"
+ );
+
+ // The clientId should've been reset when we restored FHR Upload.
+ let secondClientId = TelemetryController.getCurrentPingData().clientId;
+ Assert.notEqual(
+ firstClientId,
+ secondClientId,
+ "The client id must have changed"
+ );
+ // Simulate a failure in sending the deletion-request ping by disabling the HTTP server.
+ await PingServer.stop();
+
+ // Try to send a ping. It will be saved as pending and get deleted when disabling upload.
+ TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Disable FHR upload to send a deletion-request ping again.
+ Preferences.set(TelemetryUtils.Preferences.FhrUploadEnabled, false);
+ // Wait for the deletion-request ping to be submitted.
+ await TelemetryController.testPromiseDeletionRequestPingSubmitted();
+
+ // Wait on sending activity to settle, as |TelemetryController.testReset()| doesn't do that.
+ await TelemetrySend.testWaitOnOutgoingPings();
+ // Wait for the pending pings to be deleted. Resetting TelemetryController doesn't
+ // trigger the shutdown, so we need to call it ourselves.
+ await TelemetryStorage.shutdown();
+ // Simulate a restart, and spin the send task.
+ await TelemetryController.testReset();
+
+ // Disabling Telemetry upload must clear out all the pending pings.
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ Assert.equal(
+ pendingPings.length,
+ 1,
+ "All the pending pings should have been deleted, except the deletion-request ping"
+ );
+
+ // Enable the ping server again.
+ PingServer.start();
+ // We set the new server using the pref, otherwise it would get reset with
+ // |TelemetryController.testReset|.
+ Preferences.set(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ // Stop the sending task and then start it again.
+ await TelemetrySend.shutdown();
+ // Reset the controller to spin the ping sending task.
+ await TelemetryController.testReset();
+
+ // Re-enable Telemetry
+ Preferences.set(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Send a test ping
+ await sendPing(true, false);
+
+ // We should have received the test ping first.
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+
+ // The data in the test ping should be different than before
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ ping.clientId,
+ "Client ID should be reset to a random value"
+ );
+ Assert.notEqual(
+ firstClientId,
+ ping.clientId,
+ "Client ID should be different from the previous value"
+ );
+
+ // The "deletion-request" ping should come next, as it was pending.
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_REQUEST_PING_TYPE, true, false);
+ Assert.equal(
+ secondClientId,
+ ping.clientId,
+ "Deletion must be requested for correct client id"
+ );
+
+ // Wait on ping activity to settle before moving on to the next test. If we were
+ // to shut down telemetry, even though the PingServer caught the expected pings,
+ // TelemetrySend could still be processing them (clearing pings would happen in
+ // a couple of ticks). Shutting down would cancel the request and save them as
+ // pending pings.
+ await TelemetrySend.testWaitOnOutgoingPings();
+});
+
+add_task(async function test_pingHasClientId() {
+ // Make sure we have no cached client ID for this test: we'll try to send
+ // a ping with it while Telemetry is being initialized.
+ Preferences.reset(TelemetryUtils.Preferences.CachedClientId);
+ await TelemetryController.testShutdown();
+ await ClientID._reset();
+ await TelemetryStorage.testClearPendingPings();
+ // And also clear the counter histogram since we're here.
+ let h = Telemetry.getHistogramById(
+ "TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID"
+ );
+ h.clear();
+
+ // Init telemetry and try to send a ping with a client ID.
+ let promisePingSetup = TelemetryController.testReset();
+ await sendPing(true, false);
+ Assert.equal(
+ h.snapshot().sum,
+ 1,
+ "We must have a ping waiting for the clientId early during startup."
+ );
+ // Wait until we are fully initialized. Pings will be assembled but won't get
+ // sent before then.
+ await promisePingSetup;
+
+ let ping = await PingServer.promiseNextPing();
+ // Fetch the client ID after initializing and fetching the the ping, so we
+ // don't unintentionally trigger its loading. We'll still need the client ID
+ // to see if the ping looks sane.
+ gClientID = await ClientID.getClientID();
+
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "The correct clientId must be reported."
+ );
+
+ // Shutdown Telemetry so we can safely restart it.
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+
+ // We should have cached the client ID now. Lets confirm that by checking it before
+ // the async ping setup is finished.
+ h.clear();
+ promisePingSetup = TelemetryController.testReset();
+ await sendPing(true, false);
+ await promisePingSetup;
+
+ // Check that we received the cached client id.
+ Assert.equal(h.snapshot().sum, 0, "We must have used the cached clientId.");
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "Telemetry should report the correct cached clientId."
+ );
+
+ // Check that sending a ping without relying on the cache, after the
+ // initialization, still works.
+ Preferences.reset(TelemetryUtils.Preferences.CachedClientId);
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ await sendPing(true, false);
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "The correct clientId must be reported."
+ );
+ Assert.equal(
+ h.snapshot().sum,
+ 0,
+ "No ping should have been waiting for a clientId."
+ );
+});
+
+add_task(async function test_pingHasEnvironment() {
+ // Send a ping with the environment data.
+ await sendPing(false, true);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, false, true);
+
+ // Test a field in the environment build section.
+ Assert.equal(ping.application.buildId, ping.environment.build.buildId);
+});
+
+add_task(async function test_pingHasEnvironmentAndClientId() {
+ // Send a ping with the environment data and client id.
+ await sendPing(true, true);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+
+ // Test a field in the environment build section.
+ Assert.equal(ping.application.buildId, ping.environment.build.buildId);
+ // Test that we have the correct clientId.
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "The correct clientId must be reported."
+ );
+});
+
+add_task(async function test_archivePings() {
+ let now = new Date(2009, 10, 18, 12, 0, 0);
+ fakeNow(now);
+
+ // Disable ping upload so that pings don't get sent.
+ // With unified telemetry the FHR upload pref controls this,
+ // with non-unified telemetry the Telemetry enabled pref.
+ const isUnified = Preferences.get(TelemetryUtils.Preferences.Unified, false);
+ const uploadPref = isUnified
+ ? TelemetryUtils.Preferences.FhrUploadEnabled
+ : TelemetryUtils.Preferences.TelemetryEnabled;
+ Preferences.set(uploadPref, false);
+
+ // If we're using unified telemetry, disabling ping upload will generate a "deletion-request" ping. Catch it.
+ if (isUnified) {
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_REQUEST_PING_TYPE, true, false);
+ }
+
+ // Register a new Ping Handler that asserts if a ping is received, then send a ping.
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Telemetry must not send pings if not allowed to.")
+ );
+ let pingId = await sendPing(true, true);
+
+ // Check that the ping was archived, even with upload disabled.
+ let ping = await TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(
+ ping.id,
+ pingId,
+ "TelemetryController should still archive pings."
+ );
+
+ // Check that pings don't get archived if not allowed to.
+ now = new Date(2010, 10, 18, 12, 0, 0);
+ fakeNow(now);
+ Preferences.set(TelemetryUtils.Preferences.ArchiveEnabled, false);
+ pingId = await sendPing(true, true);
+ let promise = TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.ok(
+ await promiseRejects(promise),
+ "TelemetryController should not archive pings if the archive pref is disabled."
+ );
+
+ // Enable archiving and the upload so that pings get sent and archived again.
+ Preferences.set(uploadPref, true);
+ Preferences.set(TelemetryUtils.Preferences.ArchiveEnabled, true);
+
+ now = new Date(2014, 6, 18, 22, 0, 0);
+ fakeNow(now);
+ // Restore the non asserting ping handler.
+ PingServer.resetPingHandler();
+ pingId = await sendPing(true, true);
+
+ // Check that we archive pings when successfully sending them.
+ await PingServer.promiseNextPing();
+ ping = await TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(
+ ping.id,
+ pingId,
+ "TelemetryController should still archive pings if ping upload is enabled."
+ );
+});
+
+// Test that we fuzz the submission time around midnight properly
+// to avoid overloading the telemetry servers.
+add_task(async function test_midnightPingSendFuzzing() {
+ const fuzzingDelay = 60 * 60 * 1000;
+ fakeMidnightPingFuzzingDelay(fuzzingDelay);
+ let now = new Date(2030, 5, 1, 11, 0, 0);
+ fakeNow(now);
+
+ let waitForTimer = () =>
+ new Promise(resolve => {
+ fakePingSendTimer(
+ (callback, timeout) => {
+ resolve([callback, timeout]);
+ },
+ () => {}
+ );
+ });
+
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ // A ping after midnight within the fuzzing delay should not get sent.
+ now = new Date(2030, 5, 2, 0, 40, 0);
+ fakeNow(now);
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(false, "No ping should be received yet.");
+ });
+ let timerPromise = waitForTimer();
+ await sendPing(true, true);
+ let [timerCallback, timerTimeout] = await timerPromise;
+ Assert.ok(!!timerCallback);
+ Assert.deepEqual(
+ futureDate(now, timerTimeout),
+ new Date(2030, 5, 2, 1, 0, 0)
+ );
+
+ // A ping just before the end of the fuzzing delay should not get sent.
+ now = new Date(2030, 5, 2, 0, 59, 59);
+ fakeNow(now);
+ timerPromise = waitForTimer();
+ await sendPing(true, true);
+ [timerCallback, timerTimeout] = await timerPromise;
+ Assert.deepEqual(timerTimeout, 1 * 1000);
+
+ // Restore the previous ping handler.
+ PingServer.resetPingHandler();
+
+ // Setting the clock to after the fuzzing delay, we should trigger the two ping sends
+ // with the timer callback.
+ now = futureDate(now, timerTimeout);
+ fakeNow(now);
+ await timerCallback();
+ const pings = await PingServer.promiseNextPings(2);
+ for (let ping of pings) {
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ }
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Moving the clock further we should still send pings immediately.
+ now = futureDate(now, 5 * 60 * 1000);
+ await sendPing(true, true);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Check that pings shortly before midnight are immediately sent.
+ now = fakeNow(2030, 5, 3, 23, 59, 0);
+ await sendPing(true, true);
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Clean-up.
+ fakeMidnightPingFuzzingDelay(0);
+ fakePingSendTimer(
+ () => {},
+ () => {}
+ );
+});
+
+add_task(async function test_changePingAfterSubmission() {
+ // Submit a ping with a custom payload.
+ let payload = { canary: "test" };
+ let pingPromise = TelemetryController.submitExternalPing(
+ TEST_PING_TYPE,
+ payload
+ );
+
+ // Change the payload with a predefined value.
+ payload.canary = "changed";
+
+ // Wait for the ping to be archived.
+ const pingId = await pingPromise;
+
+ // Make sure our changes didn't affect the submitted payload.
+ let archivedCopy = await TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(
+ archivedCopy.payload.canary,
+ "test",
+ "The payload must not be changed after being submitted."
+ );
+});
+
+add_task(async function test_telemetryCleanFHRDatabase() {
+ const FHR_DBNAME_PREF = "datareporting.healthreport.dbName";
+ const CUSTOM_DB_NAME = "unlikely.to.be.used.sqlite";
+ const DEFAULT_DB_NAME = "healthreport.sqlite";
+
+ // Check that we're able to remove a FHR DB with a custom name.
+ const profileDir = PathUtils.profileDir;
+ const CUSTOM_DB_PATHS = [
+ PathUtils.join(profileDir, CUSTOM_DB_NAME),
+ PathUtils.join(profileDir, CUSTOM_DB_NAME + "-wal"),
+ PathUtils.join(profileDir, CUSTOM_DB_NAME + "-shm"),
+ ];
+ Preferences.set(FHR_DBNAME_PREF, CUSTOM_DB_NAME);
+
+ // Write fake DB files to the profile directory.
+ for (let dbFilePath of CUSTOM_DB_PATHS) {
+ await IOUtils.writeUTF8(dbFilePath, "some data");
+ }
+
+ // Trigger the cleanup and check that the files were removed.
+ await TelemetryStorage.removeFHRDatabase();
+ for (let dbFilePath of CUSTOM_DB_PATHS) {
+ try {
+ await IOUtils.read(dbFilePath);
+ } catch (e) {
+ Assert.ok(DOMException.isInstance(e));
+ Assert.equal(
+ e.name,
+ "NotFoundError",
+ "The DB must not be on the disk anymore: " + dbFilePath
+ );
+ }
+ }
+
+ // We should not break anything if there's no DB file.
+ await TelemetryStorage.removeFHRDatabase();
+
+ // Check that we're able to remove a FHR DB with the default name.
+ Preferences.reset(FHR_DBNAME_PREF);
+
+ const DEFAULT_DB_PATHS = [
+ PathUtils.join(profileDir, DEFAULT_DB_NAME),
+ PathUtils.join(profileDir, DEFAULT_DB_NAME + "-wal"),
+ PathUtils.join(profileDir, DEFAULT_DB_NAME + "-shm"),
+ ];
+
+ // Write fake DB files to the profile directory.
+ for (let dbFilePath of DEFAULT_DB_PATHS) {
+ await IOUtils.writeUTF8(dbFilePath, "some data");
+ }
+
+ // Trigger the cleanup and check that the files were removed.
+ await TelemetryStorage.removeFHRDatabase();
+ for (let dbFilePath of DEFAULT_DB_PATHS) {
+ try {
+ await IOUtils.read(dbFilePath);
+ } catch (e) {
+ Assert.ok(DOMException.isInstance(e));
+ Assert.equal(
+ e.name,
+ "NotFoundError",
+ "The DB must not be on the disk anymore: " + dbFilePath
+ );
+ }
+ }
+});
+
+add_task(async function test_sendNewProfile() {
+ if (
+ gIsAndroid ||
+ (AppConstants.platform == "linux" && !Services.appinfo.is64Bit)
+ ) {
+ // We don't support the pingsender on Android, yet, see bug 1335917.
+ // We also don't suppor the pingsender testing on Treeherder for
+ // Linux 32 bit (due to missing libraries). So skip it there too.
+ // See bug 1310703 comment 78.
+ return;
+ }
+
+ const NEWPROFILE_PING_TYPE = "new-profile";
+ const PREF_NEWPROFILE_ENABLED = "toolkit.telemetry.newProfilePing.enabled";
+ const PREF_NEWPROFILE_DELAY = "toolkit.telemetry.newProfilePing.delay";
+
+ // Make sure Telemetry is shut down before beginning and that we have
+ // no pending pings.
+ let resetTest = async function () {
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ };
+ await resetTest();
+
+ // Make sure to reset all the new-profile ping prefs.
+ const stateFilePath = PathUtils.join(
+ await DATAREPORTING_PATH,
+ "session-state.json"
+ );
+ await IOUtils.remove(stateFilePath);
+ Preferences.set(PREF_NEWPROFILE_DELAY, 1);
+ Preferences.set(PREF_NEWPROFILE_ENABLED, true);
+
+ // Check that a new-profile ping is sent on the first session.
+ let nextReq = PingServer.promiseNextRequest();
+ await TelemetryController.testReset();
+ let req = await nextReq;
+ let ping = decodeRequestPayload(req);
+ checkPingFormat(ping, NEWPROFILE_PING_TYPE, true, true);
+ Assert.equal(
+ ping.payload.reason,
+ "startup",
+ "The new-profile ping generated after startup must have the correct reason"
+ );
+ Assert.ok(
+ "parent" in ping.payload.processes,
+ "The new-profile ping generated after startup must have processes.parent data"
+ );
+
+ // Check that is not sent with the pingsender during startup.
+ Assert.throws(
+ () => req.getHeader("X-PingSender-Version"),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Should not have used the pingsender."
+ );
+
+ // Make sure that the new-profile ping is sent at shutdown if it wasn't sent before.
+ await resetTest();
+ await IOUtils.remove(stateFilePath);
+ Preferences.reset(PREF_NEWPROFILE_DELAY);
+
+ nextReq = PingServer.promiseNextRequest();
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ req = await nextReq;
+ ping = decodeRequestPayload(req);
+ checkPingFormat(ping, NEWPROFILE_PING_TYPE, true, true);
+ Assert.equal(
+ ping.payload.reason,
+ "shutdown",
+ "The new-profile ping generated at shutdown must have the correct reason"
+ );
+ Assert.ok(
+ "parent" in ping.payload.processes,
+ "The new-profile ping generated at shutdown must have processes.parent data"
+ );
+
+ // Check that the new-profile ping is sent at shutdown using the pingsender.
+ Assert.equal(
+ req.getHeader("User-Agent"),
+ "pingsender/1.0",
+ "Should have received the correct user agent string."
+ );
+ Assert.equal(
+ req.getHeader("X-PingSender-Version"),
+ "1.0",
+ "Should have received the correct PingSender version string."
+ );
+
+ // Check that no new-profile ping is sent on second sessions, not at startup
+ // nor at shutdown.
+ await resetTest();
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "The new-profile ping must be sent only on new profiles.")
+ );
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Check that we don't send the new-profile ping if the profile already contains
+ // a state file (but no "newProfilePingSent" property).
+ await resetTest();
+ await IOUtils.remove(stateFilePath);
+ const sessionState = {
+ sessionId: null,
+ subsessionId: null,
+ profileSubsessionCounter: 3785,
+ };
+ await IOUtils.writeJSON(stateFilePath, sessionState);
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Reset the pref and restart Telemetry.
+ Preferences.reset(PREF_NEWPROFILE_ENABLED);
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_encryptedPing() {
+ if (gIsAndroid) {
+ // The underlying jwcrypto module being used here is not currently available on Android.
+ return;
+ }
+ Cu.importGlobalProperties(["crypto"]);
+
+ const ECDH_PARAMS = {
+ name: "ECDH",
+ namedCurve: "P-256",
+ };
+
+ const privateKey = {
+ crv: "P-256",
+ d: "rcs093UlGDG6piwHenmSDoAxbzMIXT43JkQbkt3xEmI",
+ ext: true,
+ key_ops: ["deriveKey"],
+ kty: "EC",
+ x: "h12feyTYBZ__wO_AnM1a5-KTDlko3-YyQ_en19jyrs0",
+ y: "6GSfzo14ehDyH5E-xCOedJDAYlN0AGPMCtIgFbheLko",
+ };
+
+ const publicKey = {
+ crv: "P-256",
+ ext: true,
+ kty: "EC",
+ x: "h12feyTYBZ__wO_AnM1a5-KTDlko3-YyQ_en19jyrs0",
+ y: "6GSfzo14ehDyH5E-xCOedJDAYlN0AGPMCtIgFbheLko",
+ };
+
+ const pioneerId = "12345";
+ const schemaName = "abc";
+ const schemaNamespace = "def";
+ const schemaVersion = 2;
+
+ Services.prefs.setStringPref("toolkit.telemetry.pioneerId", pioneerId);
+
+ // Stop the sending task and then start it again.
+ await TelemetrySend.shutdown();
+ // Reset the controller to spin the ping sending task.
+ await TelemetryController.testReset();
+
+ // Submit a ping with a custom payload, which will be encrypted.
+ let payload = { canary: "test" };
+ let pingPromise = TelemetryController.submitExternalPing(
+ "pioneer-study",
+ payload,
+ {
+ studyName: "pioneer-dev-1@allizom.org",
+ addPioneerId: true,
+ useEncryption: true,
+ encryptionKeyId: "pioneer-dev-20200423",
+ publicKey,
+ schemaName,
+ schemaNamespace,
+ schemaVersion,
+ }
+ );
+
+ // Wait for the ping to be archived.
+ const pingId = await pingPromise;
+
+ let archivedCopy = await TelemetryArchive.promiseArchivedPingById(pingId);
+
+ Assert.notEqual(
+ archivedCopy.payload.encryptedData,
+ payload,
+ "The encrypted payload must not match the plaintext."
+ );
+
+ Assert.equal(
+ archivedCopy.payload.pioneerId,
+ pioneerId,
+ "Pioneer ID in ping must match the pref."
+ );
+
+ // Validate ping against schema.
+ const schema = {
+ $schema: "http://json-schema.org/draft-04/schema#",
+ properties: {
+ application: {
+ additionalProperties: false,
+ properties: {
+ architecture: {
+ type: "string",
+ },
+ buildId: {
+ pattern: "^[0-9]{10}",
+ type: "string",
+ },
+ channel: {
+ type: "string",
+ },
+ displayVersion: {
+ pattern: "^[0-9]{2,3}\\.",
+ type: "string",
+ },
+ name: {
+ type: "string",
+ },
+ platformVersion: {
+ pattern: "^[0-9]{2,3}\\.",
+ type: "string",
+ },
+ vendor: {
+ type: "string",
+ },
+ version: {
+ pattern: "^[0-9]{2,3}\\.",
+ type: "string",
+ },
+ xpcomAbi: {
+ type: "string",
+ },
+ },
+ required: [
+ "architecture",
+ "buildId",
+ "channel",
+ "name",
+ "platformVersion",
+ "version",
+ "vendor",
+ "xpcomAbi",
+ ],
+ type: "object",
+ },
+ creationDate: {
+ pattern:
+ "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$",
+ type: "string",
+ },
+ id: {
+ pattern:
+ "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
+ type: "string",
+ },
+ payload: {
+ description: "",
+ properties: {
+ encryptedData: {
+ description: "JOSE/JWE encrypted payload.",
+ type: "string",
+ },
+ encryptionKeyId: {
+ description: "JOSE/JWK key id, e.g. pioneer-20170520.",
+ type: "string",
+ },
+ pioneerId: {
+ description: "Custom pioneer id, must not be Telemetry clientId",
+ pattern:
+ "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
+ type: "string",
+ },
+ schemaName: {
+ description:
+ "Name of a schema used for validation of the encryptedData",
+ maxLength: 100,
+ minLength: 1,
+ pattern: "^\\S+$",
+ type: "string",
+ },
+ schemaNamespace: {
+ description:
+ "The namespace of the schema used for validation and routing to a dataset.",
+ maxLength: 100,
+ minLength: 1,
+ pattern: "^\\S+$",
+ type: "string",
+ },
+ schemaVersion: {
+ description: "Integer version number of the schema",
+ minimum: 1,
+ type: "integer",
+ },
+ studyName: {
+ description: "Name of a particular study. Usually the addon_id.",
+ maxLength: 100,
+ minLength: 1,
+ pattern: "^\\S+$",
+ type: "string",
+ },
+ },
+ required: [
+ "encryptedData",
+ "encryptionKeyId",
+ "pioneerId",
+ "studyName",
+ "schemaName",
+ "schemaNamespace",
+ "schemaVersion",
+ ],
+ title: "pioneer-study",
+ type: "object",
+ },
+ type: {
+ description: "doc_type, restated",
+ enum: ["pioneer-study"],
+ type: "string",
+ },
+ version: {
+ maximum: 4,
+ minimum: 4,
+ type: "integer",
+ },
+ },
+ required: [
+ "application",
+ "creationDate",
+ "id",
+ "payload",
+ "type",
+ "version",
+ ],
+ title: "pioneer-study",
+ type: "object",
+ };
+
+ const result = JsonSchemaValidator.validate(archivedCopy, schema);
+
+ Assert.ok(
+ result.valid,
+ `Archived ping should validate against schema: ${result.error}`
+ );
+
+ // check that payload can be decrypted.
+ const privateJWK = await crypto.subtle.importKey(
+ "jwk",
+ privateKey,
+ ECDH_PARAMS,
+ false,
+ ["deriveKey"]
+ );
+
+ const decryptedJWE = await jwcrypto.decryptJWE(
+ archivedCopy.payload.encryptedData,
+ privateJWK
+ );
+
+ Assert.deepEqual(
+ JSON.parse(new TextDecoder("utf-8").decode(decryptedJWE)),
+ payload,
+ "decrypted payload should match"
+ );
+});
+
+add_task(async function test_encryptedPing_overrideId() {
+ if (gIsAndroid) {
+ // The underlying jwcrypto module being used here is not currently available on Android.
+ return;
+ }
+ Cu.importGlobalProperties(["crypto"]);
+
+ const publicKey = {
+ crv: "P-256",
+ ext: true,
+ kty: "EC",
+ x: "h12feyTYBZ__wO_AnM1a5-KTDlko3-YyQ_en19jyrs0",
+ y: "6GSfzo14ehDyH5E-xCOedJDAYlN0AGPMCtIgFbheLko",
+ };
+
+ const prefPioneerId = "12345";
+ const overriddenPioneerId = "c0ffeeaa-bbbb-abab-baba-eeff0ceeff0c";
+ const schemaName = "abc";
+ const schemaNamespace = "def";
+ const schemaVersion = 2;
+
+ Services.prefs.setStringPref("toolkit.telemetry.pioneerId", prefPioneerId);
+
+ let archiveTester = new TelemetryArchiveTesting.Checker();
+ await archiveTester.promiseInit();
+
+ // Submit a ping with a custom payload, which will be encrypted.
+ let payload = { canary: "test" };
+ let pingPromise = TelemetryController.submitExternalPing(
+ "test-pioneer-study-override",
+ payload,
+ {
+ studyName: "pioneer-dev-1@allizom.org",
+ addPioneerId: true,
+ overridePioneerId: overriddenPioneerId,
+ useEncryption: true,
+ encryptionKeyId: "pioneer-dev-20200423",
+ publicKey,
+ schemaName,
+ schemaNamespace,
+ schemaVersion,
+ }
+ );
+
+ // Wait for the ping to be submitted, to have the ping id to scan the
+ // archive for.
+ const pingId = await pingPromise;
+
+ // And then wait for the ping to be available in the archive.
+ await TestUtils.waitForCondition(
+ () => archiveTester.promiseFindPing("test-pioneer-study-override", []),
+ "Failed to find the pioneer ping"
+ );
+
+ let archivedCopy = await TelemetryArchive.promiseArchivedPingById(pingId);
+
+ Assert.notEqual(
+ archivedCopy.payload.encryptedData,
+ payload,
+ "The encrypted payload must not match the plaintext."
+ );
+
+ Assert.equal(
+ archivedCopy.payload.pioneerId,
+ overriddenPioneerId,
+ "Pioneer ID in ping must match the provided override."
+ );
+});
+
+// Testing shutdown and checking that pings sent afterwards are rejected.
+add_task(async function test_pingRejection() {
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ await sendPing(false, false).then(
+ () => Assert.ok(false, "Pings submitted after shutdown must be rejected."),
+ () => Assert.ok(true, "Ping submitted after shutdown correctly rejected.")
+ );
+});
+
+add_task(async function test_newCanRecordsMatchTheOld() {
+ Assert.equal(
+ Telemetry.canRecordBase,
+ Telemetry.canRecordReleaseData,
+ "Release Data is the new way to say Base Collection"
+ );
+ Assert.equal(
+ Telemetry.canRecordExtended,
+ Telemetry.canRecordPrereleaseData,
+ "Prerelease Data is the new way to say Extended Collection"
+ );
+});
+
+add_task(function test_histogram_filtering() {
+ const COUNT_ID = "TELEMETRY_TEST_COUNT";
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const count = Telemetry.getHistogramById(COUNT_ID);
+ const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ count.add(1);
+ keyed.add("a", 1);
+
+ let snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ let keyedSnapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ Assert.ok(COUNT_ID in snapshot, "test histogram should be snapshotted");
+ Assert.ok(
+ KEYED_ID in keyedSnapshot,
+ "test keyed histogram should be snapshotted"
+ );
+
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ keyedSnapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ Assert.ok(
+ !(COUNT_ID in snapshot),
+ "test histogram should not be snapshotted"
+ );
+ Assert.ok(
+ !(KEYED_ID in keyedSnapshot),
+ "test keyed histogram should not be snapshotted"
+ );
+});
+
+add_task(function test_scalar_filtering() {
+ const COUNT_ID = "telemetry.test.unsigned_int_kind";
+ const KEYED_ID = "telemetry.test.keyed_unsigned_int";
+
+ Telemetry.scalarSet(COUNT_ID, 2);
+ Telemetry.keyedScalarSet(KEYED_ID, "a", 2);
+
+ let snapshot = Telemetry.getSnapshotForScalars(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ let keyedSnapshot = Telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ Assert.ok(COUNT_ID in snapshot, "test scalars should be snapshotted");
+ Assert.ok(
+ KEYED_ID in keyedSnapshot,
+ "test keyed scalars should be snapshotted"
+ );
+
+ snapshot = Telemetry.getSnapshotForScalars(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ keyedSnapshot = Telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ Assert.ok(!(COUNT_ID in snapshot), "test scalars should not be snapshotted");
+ Assert.ok(
+ !(KEYED_ID in keyedSnapshot),
+ "test keyed scalars should not be snapshotted"
+ );
+});
+
+add_task(async function stopServer() {
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js
new file mode 100644
index 0000000000..126684fe82
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* Test inclusion of previous build ID in telemetry pings when build ID changes.
+ * bug 841028
+ *
+ * Cases to cover:
+ * 1) Run with no "previousBuildID" stored in prefs:
+ * -> no previousBuildID in telemetry system info, new value set in prefs.
+ * 2) previousBuildID in prefs, equal to current build ID:
+ * -> no previousBuildID in telemetry, prefs not updated.
+ * 3) previousBuildID in prefs, not equal to current build ID:
+ * -> previousBuildID in telemetry, new value set in prefs.
+ */
+
+"use strict";
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+
+// Set up our dummy AppInfo object so we can control the appBuildID.
+const { getAppInfo, updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+// Check that when run with no previous build ID stored, we update the pref but do not
+// put anything into the metadata.
+add_task(async function test_firstRun() {
+ await TelemetryController.testReset();
+ let metadata = TelemetrySession.getMetadata();
+ Assert.equal(false, "previousBuildID" in metadata);
+ let appBuildID = getAppInfo().appBuildID;
+ let buildIDPref = Services.prefs.getCharPref(
+ TelemetryUtils.Preferences.PreviousBuildID
+ );
+ Assert.equal(appBuildID, buildIDPref);
+});
+
+// Check that a subsequent run with the same build ID does not put prev build ID in
+// metadata. Assumes testFirstRun() has already been called to set the previousBuildID pref.
+add_task(async function test_secondRun() {
+ await TelemetryController.testReset();
+ let metadata = TelemetrySession.getMetadata();
+ Assert.equal(false, "previousBuildID" in metadata);
+});
+
+// Set up telemetry with a different app build ID and check that the old build ID
+// is returned in the metadata and the pref is updated to the new build ID.
+// Assumes testFirstRun() has been called to set the previousBuildID pref.
+const NEW_BUILD_ID = "20130314";
+add_task(async function test_newBuild() {
+ let info = getAppInfo();
+ let oldBuildID = info.appBuildID;
+ info.appBuildID = NEW_BUILD_ID;
+ await TelemetryController.testReset();
+ let metadata = TelemetrySession.getMetadata();
+ Assert.equal(metadata.previousBuildId, oldBuildID);
+ let buildIDPref = Services.prefs.getCharPref(
+ TelemetryUtils.Preferences.PreviousBuildID
+ );
+ Assert.equal(NEW_BUILD_ID, buildIDPref);
+});
+
+function run_test() {
+ // Make sure we have a profile directory.
+ do_get_profile();
+
+ run_next_test();
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
new file mode 100644
index 0000000000..95ef3789d5
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that TelemetryController sends close to shutdown don't lead
+// to AsyncShutdown timeouts.
+
+"use strict";
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+);
+const { AsyncShutdown } = ChromeUtils.importESModule(
+ "resource://gre/modules/AsyncShutdown.sys.mjs"
+);
+
+function contentHandler(metadata, response) {
+ dump("contentHandler called for path: " + metadata._path + "\n");
+ // We intentionally don't finish writing the response here to let the
+ // client time out.
+ response.processAsync();
+ response.setHeader("Content-Type", "text/plain");
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ 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.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+});
+
+/**
+ * Ensures that TelemetryController does not hang processing shutdown
+ * phases. Assumes that Telemetry shutdown routines do not take longer than
+ * CRASH_TIMEOUT_MS to complete.
+ */
+add_task(async function test_sendTelemetryShutsDownWithinReasonableTimeout() {
+ const CRASH_TIMEOUT_MS = 10 * 1000;
+ // Enable testing mode for AsyncShutdown, otherwise some testing-only functionality
+ // is not available.
+ Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+ // Reducing the max delay for waitiing on phases to complete from 1 minute
+ // (standard) to 20 seconds to avoid blocking the tests in case of misbehavior.
+ Services.prefs.setIntPref(
+ "toolkit.asyncshutdown.crash_timeout",
+ CRASH_TIMEOUT_MS
+ );
+
+ let httpServer = new HttpServer();
+ httpServer.registerPrefixHandler("/", contentHandler);
+ httpServer.start(-1);
+
+ await TelemetryController.testSetup();
+ TelemetrySend.setServer(
+ "http://localhost:" + httpServer.identity.primaryPort
+ );
+ let submissionPromise = TelemetryController.submitExternalPing(
+ "test-ping-type",
+ {}
+ );
+
+ // Trigger the AsyncShutdown phase TelemetryController hangs off.
+ AsyncShutdown.profileBeforeChange._trigger();
+ AsyncShutdown.sendTelemetry._trigger();
+ // Now wait for the ping submission.
+ await submissionPromise;
+
+ // If we get here, we didn't time out in the shutdown routines.
+ Assert.ok(true, "Didn't time out on shutdown.");
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
new file mode 100644
index 0000000000..99e6c05319
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check that TelemetrySession notifies correctly on idle-daily.
+
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+
+var gHttpServer = null;
+
+add_task(async function test_setup() {
+ do_get_profile();
+
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Start the webserver to check if the pending ping correctly arrives.
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+});
+
+add_task(async function testSendPendingOnIdleDaily() {
+ // Create a valid pending ping.
+ const PENDING_PING = {
+ id: "2133234d-4ea1-44f4-909e-ce8c6c41e0fc",
+ type: "test-ping",
+ version: 4,
+ application: {},
+ payload: {},
+ };
+ await TelemetryStorage.savePing(PENDING_PING, true);
+
+ // Telemetry will not send this ping at startup, because it's not overdue.
+ await TelemetryController.testSetup();
+ TelemetrySend.setServer(
+ "http://localhost:" + gHttpServer.identity.primaryPort
+ );
+
+ let pendingPromise = new Promise(resolve =>
+ gHttpServer.registerPrefixHandler("/submit/telemetry/", request =>
+ resolve(request)
+ )
+ );
+
+ let gatherPromise = PromiseUtils.defer();
+ Services.obs.addObserver(gatherPromise.resolve, "gather-telemetry");
+
+ // Check that we are correctly receiving the gather-telemetry notification.
+ TelemetrySession.observe(null, "idle-daily", null);
+ await gatherPromise.promise;
+ Assert.ok(true, "Received gather-telemetry notification.");
+
+ Services.obs.removeObserver(gatherPromise.resolve, "gather-telemetry");
+
+ // Check that the pending ping is correctly received.
+ let { TelemetrySendImpl } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+ );
+ TelemetrySendImpl.observe(null, "idle-daily", null);
+ let request = await pendingPromise;
+ let ping = decodeRequestPayload(request);
+
+ // Validate the ping data.
+ Assert.equal(ping.id, PENDING_PING.id);
+ Assert.equal(ping.type, PENDING_PING.type);
+
+ await new Promise(resolve => gHttpServer.stop(resolve));
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
new file mode 100644
index 0000000000..6bb5b8c6bd
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -0,0 +1,1427 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { AddonManager, AddonManagerPrivate } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+const { TelemetryEnvironmentTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryEnvironmentTesting.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionTestUtils:
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs",
+});
+
+async function installXPIFromURL(url) {
+ let install = await AddonManager.getInstallForURL(url);
+ return install.install();
+}
+
+// The webserver hosting the addons.
+var gHttpServer = null;
+// The URL of the webserver root.
+var gHttpRoot = null;
+// The URL of the data directory, on the webserver.
+var gDataRoot = null;
+
+function MockAddonWrapper(aAddon) {
+ this.addon = aAddon;
+}
+MockAddonWrapper.prototype = {
+ get id() {
+ return this.addon.id;
+ },
+
+ get type() {
+ return this.addon.type;
+ },
+
+ get appDisabled() {
+ return false;
+ },
+
+ get isCompatible() {
+ return true;
+ },
+
+ get isPlatformCompatible() {
+ return true;
+ },
+
+ get scope() {
+ return AddonManager.SCOPE_PROFILE;
+ },
+
+ get foreignInstall() {
+ return false;
+ },
+
+ get providesUpdatesSecurely() {
+ return true;
+ },
+
+ get blocklistState() {
+ return 0; // Not blocked.
+ },
+
+ get pendingOperations() {
+ return AddonManager.PENDING_NONE;
+ },
+
+ get permissions() {
+ return AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_DISABLE;
+ },
+
+ get isActive() {
+ return true;
+ },
+
+ get name() {
+ return this.addon.name;
+ },
+
+ get version() {
+ return this.addon.version;
+ },
+
+ get creator() {
+ return new AddonManagerPrivate.AddonAuthor(this.addon.author);
+ },
+
+ get userDisabled() {
+ return this.appDisabled;
+ },
+};
+
+function createMockAddonProvider(aName) {
+ let mockProvider = {
+ _addons: [],
+
+ get name() {
+ return aName;
+ },
+
+ addAddon(aAddon) {
+ this._addons.push(aAddon);
+ AddonManagerPrivate.callAddonListeners(
+ "onInstalled",
+ new MockAddonWrapper(aAddon)
+ );
+ },
+
+ async getAddonsByTypes(aTypes) {
+ return this._addons
+ .filter(a => !aTypes || aTypes.includes(a.type))
+ .map(a => new MockAddonWrapper(a));
+ },
+
+ shutdown() {
+ return Promise.resolve();
+ },
+ };
+
+ return mockProvider;
+}
+
+add_task(async function setup() {
+ TelemetryEnvironmentTesting.registerFakeSysInfo();
+ TelemetryEnvironmentTesting.spoofGfxAdapter();
+ do_get_profile();
+
+ // We need to ensure FOG is initialized, otherwise we will panic trying to get test values.
+ Services.fog.initializeFOG();
+
+ // The system add-on must be installed before AddonManager is started.
+ const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true);
+ do_get_file("system.xpi").copyTo(
+ distroDir,
+ "tel-system-xpi@tests.mozilla.org.xpi"
+ );
+ let system_addon = FileUtils.File(distroDir.path);
+ system_addon.append("tel-system-xpi@tests.mozilla.org.xpi");
+ system_addon.lastModifiedTime = SYSTEM_ADDON_INSTALL_DATE;
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+
+ TelemetryEnvironmentTesting.init(gAppInfo);
+
+ // The test runs in a fresh profile so starting the AddonManager causes
+ // the addons database to be created (as does setting new theme).
+ // For test_addonsStartup below, we want to test a "warm" startup where
+ // there is already a database on disk. Simulate that here by just
+ // restarting the AddonManager.
+ await AddonTestUtils.promiseShutdownManager();
+ await AddonTestUtils.overrideBuiltIns({ system: [] });
+ AddonTestUtils.addonStartup.remove(true);
+ await AddonTestUtils.promiseStartupManager();
+
+ // Setup a webserver to serve Addons, etc.
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+ let port = gHttpServer.identity.primaryPort;
+ gHttpRoot = "http://localhost:" + port + "/";
+ gDataRoot = gHttpRoot + "data/";
+ gHttpServer.registerDirectory("/data/", do_get_cwd());
+ registerCleanupFunction(() => gHttpServer.stop(() => {}));
+
+ // Create the attribution data file, so that settings.attribution will exist.
+ // The attribution functionality only exists in Firefox.
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ TelemetryEnvironmentTesting.spoofAttributionData();
+ registerCleanupFunction(TelemetryEnvironmentTesting.cleanupAttributionData);
+ }
+
+ await TelemetryEnvironmentTesting.spoofProfileReset();
+ await TelemetryEnvironment.delayedInit();
+ await SearchTestUtils.useTestEngines("data", "search-extensions");
+});
+
+add_task(async function test_checkEnvironment() {
+ // During startup we have partial addon records.
+ // First make sure we haven't yet read the addons DB, then test that
+ // we have some partial addons data.
+ Assert.equal(
+ AddonManagerPrivate.isDBLoaded(),
+ false,
+ "addons database is not loaded"
+ );
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkAddonsSection(data, false, true);
+
+ // Check that settings.intl is lazily loaded.
+ Assert.equal(
+ typeof data.settings.intl,
+ "object",
+ "intl is initially an object"
+ );
+ Assert.equal(
+ Object.keys(data.settings.intl).length,
+ 0,
+ "intl is initially empty"
+ );
+
+ // Now continue with startup.
+ let initPromise = TelemetryEnvironment.onInitialized();
+ finishAddonManagerStartup();
+
+ // Fake the delayed startup event for intl data to load.
+ fakeIntlReady();
+
+ let environmentData = await initPromise;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData, {
+ isInitial: true,
+ });
+
+ TelemetryEnvironmentTesting.spoofPartnerInfo();
+ Services.obs.notifyObservers(null, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);
+
+ environmentData = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData, {
+ assertProcessData: true,
+ });
+});
+
+add_task(async function test_prefWatchPolicies() {
+ const PREF_TEST_1 = "toolkit.telemetry.test.pref_new";
+ const PREF_TEST_2 = "toolkit.telemetry.test.pref1";
+ const PREF_TEST_3 = "toolkit.telemetry.test.pref2";
+ const PREF_TEST_4 = "toolkit.telemetry.test.pref_old";
+ const PREF_TEST_5 = "toolkit.telemetry.test.requiresRestart";
+
+ const expectedValue = "some-test-value";
+ const unexpectedValue = "unexpected-test-value";
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST_1, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ [PREF_TEST_2, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ [PREF_TEST_3, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ [PREF_TEST_4, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ [
+ PREF_TEST_5,
+ { what: TelemetryEnvironment.RECORD_PREF_VALUE, requiresRestart: true },
+ ],
+ ]);
+
+ Preferences.set(PREF_TEST_4, expectedValue);
+ Preferences.set(PREF_TEST_5, expectedValue);
+
+ // Set the Environment preferences to watch.
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let deferred = PromiseUtils.defer();
+
+ // Check that the pref values are missing or present as expected
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1],
+ undefined
+ );
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_4],
+ expectedValue
+ );
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_5],
+ expectedValue
+ );
+
+ TelemetryEnvironment.registerChangeListener(
+ "testWatchPrefs",
+ (reason, data) => deferred.resolve(data)
+ );
+ let oldEnvironmentData = TelemetryEnvironment.currentEnvironment;
+
+ // Trigger a change in the watched preferences.
+ Preferences.set(PREF_TEST_1, expectedValue);
+ Preferences.set(PREF_TEST_2, false);
+ Preferences.set(PREF_TEST_5, unexpectedValue);
+ let eventEnvironmentData = await deferred.promise;
+
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener("testWatchPrefs");
+
+ // Check environment contains the correct data.
+ Assert.deepEqual(oldEnvironmentData, eventEnvironmentData);
+ let userPrefs = TelemetryEnvironment.currentEnvironment.settings.userPrefs;
+
+ Assert.equal(
+ userPrefs[PREF_TEST_1],
+ expectedValue,
+ "Environment contains the correct preference value."
+ );
+ Assert.equal(
+ userPrefs[PREF_TEST_2],
+ "<user-set>",
+ "Report that the pref was user set but the value is not shown."
+ );
+ Assert.ok(
+ !(PREF_TEST_3 in userPrefs),
+ "Do not report if preference not user set."
+ );
+ Assert.equal(
+ userPrefs[PREF_TEST_5],
+ expectedValue,
+ "The pref value in the environment data should still be the same"
+ );
+});
+
+add_task(async function test_prefWatch_prefReset() {
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ ]);
+
+ // Set the preference to a non-default value.
+ Preferences.set(PREF_TEST, false);
+
+ // Set the Environment preferences to watch.
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener(
+ "testWatchPrefs_reset",
+ deferred.resolve
+ );
+
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
+ "<user-set>"
+ );
+
+ // Trigger a change in the watched preferences.
+ Preferences.reset(PREF_TEST);
+ await deferred.promise;
+
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
+ undefined
+ );
+
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_reset");
+});
+
+add_task(async function test_prefDefault() {
+ const PREF_TEST = "toolkit.telemetry.test.defaultpref1";
+ const expectedValue = "some-test-value";
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE }],
+ ]);
+
+ // Set the preference to a default value.
+ Services.prefs.getDefaultBranch(null).setCharPref(PREF_TEST, expectedValue);
+
+ // Set the Environment preferences to watch.
+ // We're not watching, but this function does the setup we need.
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
+ expectedValue
+ );
+});
+
+add_task(async function test_prefDefaultState() {
+ const PREF_TEST = "toolkit.telemetry.test.defaultpref2";
+ const expectedValue = "some-test-value";
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_STATE }],
+ ]);
+
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ Assert.equal(
+ PREF_TEST in TelemetryEnvironment.currentEnvironment.settings.userPrefs,
+ false
+ );
+
+ // Set the preference to a default value.
+ Services.prefs.getDefaultBranch(null).setCharPref(PREF_TEST, expectedValue);
+
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
+ "<set>"
+ );
+});
+
+add_task(async function test_prefInvalid() {
+ const PREF_TEST_1 = "toolkit.telemetry.test.invalid1";
+ const PREF_TEST_2 = "toolkit.telemetry.test.invalid2";
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST_1, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE }],
+ [PREF_TEST_2, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_STATE }],
+ ]);
+
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1],
+ undefined
+ );
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_2],
+ undefined
+ );
+});
+
+add_task(async function test_addonsWatch_InterestingChange() {
+ const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
+ const ADDON_ID = "tel-restartless-webext@tests.mozilla.org";
+ // We only expect a single notification for each install, uninstall, enable, disable.
+ const EXPECTED_NOTIFICATIONS = 4;
+
+ let receivedNotifications = 0;
+
+ let registerCheckpointPromise = aExpected => {
+ return new Promise(resolve =>
+ TelemetryEnvironment.registerChangeListener(
+ "testWatchAddons_Changes" + aExpected,
+ (reason, data) => {
+ Assert.equal(reason, "addons-changed");
+ receivedNotifications++;
+ resolve();
+ }
+ )
+ );
+ };
+
+ let assertCheckpoint = aExpected => {
+ Assert.equal(receivedNotifications, aExpected);
+ TelemetryEnvironment.unregisterChangeListener(
+ "testWatchAddons_Changes" + aExpected
+ );
+ };
+
+ // Test for receiving one notification after each change.
+ let checkpointPromise = registerCheckpointPromise(1);
+ await installXPIFromURL(ADDON_INSTALL_URL);
+ await checkpointPromise;
+ assertCheckpoint(1);
+ Assert.ok(
+ ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons
+ );
+
+ checkpointPromise = registerCheckpointPromise(2);
+ let addon = await AddonManager.getAddonByID(ADDON_ID);
+ await addon.disable();
+ await checkpointPromise;
+ assertCheckpoint(2);
+ Assert.ok(
+ !(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons)
+ );
+
+ checkpointPromise = registerCheckpointPromise(3);
+ let startupPromise = AddonTestUtils.promiseWebExtensionStartup(ADDON_ID);
+ await addon.enable();
+ await checkpointPromise;
+ assertCheckpoint(3);
+ Assert.ok(
+ ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons
+ );
+ await startupPromise;
+
+ checkpointPromise = registerCheckpointPromise(4);
+ (await AddonManager.getAddonByID(ADDON_ID)).uninstall();
+ await checkpointPromise;
+ assertCheckpoint(4);
+ Assert.ok(
+ !(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons)
+ );
+
+ Assert.equal(
+ receivedNotifications,
+ EXPECTED_NOTIFICATIONS,
+ "We must only receive the notifications we expect."
+ );
+});
+
+add_task(async function test_addonsWatch_NotInterestingChange() {
+ // Plugins from GMPProvider are listed separately in addons.activeGMPlugins.
+ // We simulate the "plugin" type in this test and verify that it is excluded.
+ const PLUGIN_ID = "tel-fake-gmp-plugin@tests.mozilla.org";
+ // "theme" type is already covered by addons.theme, so we aren't interested.
+ const THEME_ID = "tel-theme@tests.mozilla.org";
+ // "dictionary" type should be in addon.activeAddons.
+ const DICT_ID = "tel-dict@tests.mozilla.org";
+
+ let receivedNotification = false;
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener("testNotInteresting", () => {
+ Assert.ok(
+ !receivedNotification,
+ "Should not receive multiple notifications"
+ );
+ receivedNotification = true;
+ deferred.resolve();
+ });
+
+ // "plugin" type, to verify that non-XPIProvider types such as the "plugin"
+ // type from GMPProvider are not included in activeAddons.
+ let fakePluginProvider = createMockAddonProvider("Fake GMPProvider");
+ AddonManagerPrivate.registerProvider(fakePluginProvider);
+ fakePluginProvider.addAddon({
+ id: PLUGIN_ID,
+ name: "Fake plugin",
+ version: "1",
+ type: "plugin",
+ });
+
+ // "theme" type.
+ let themeXpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ theme: {},
+ browser_specific_settings: { gecko: { id: THEME_ID } },
+ },
+ });
+ let themeAddon = (await AddonTestUtils.promiseInstallFile(themeXpi)).addon;
+
+ let dictXpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ dictionaries: {},
+ browser_specific_settings: { gecko: { id: DICT_ID } },
+ },
+ });
+ let dictAddon = (await AddonTestUtils.promiseInstallFile(dictXpi)).addon;
+
+ await deferred.promise;
+ Assert.ok(
+ !(PLUGIN_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons),
+ "GMP plugins should not appear in active addons."
+ );
+ Assert.ok(
+ !(THEME_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons),
+ "Themes should not appear in active addons."
+ );
+ Assert.ok(
+ DICT_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons,
+ "Dictionaries should appear in active addons."
+ );
+
+ TelemetryEnvironment.unregisterChangeListener("testNotInteresting");
+
+ AddonManagerPrivate.unregisterProvider(fakePluginProvider);
+ await themeAddon.uninstall();
+ await dictAddon.uninstall();
+});
+
+add_task(async function test_addons() {
+ const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
+ const ADDON_ID = "tel-restartless-webext@tests.mozilla.org";
+ const ADDON_INSTALL_DATE = truncateToDays(Date.now());
+ const EXPECTED_ADDON_DATA = {
+ blocklisted: false,
+ description: "A restartless addon which gets enabled without a reboot.",
+ name: "XPI Telemetry Restartless Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: ADDON_INSTALL_DATE,
+ updateDay: ADDON_INSTALL_DATE,
+ signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
+ isSystem: false,
+ isWebExtension: true,
+ multiprocessCompatible: true,
+ };
+ const SYSTEM_ADDON_ID = "tel-system-xpi@tests.mozilla.org";
+ const EXPECTED_SYSTEM_ADDON_DATA = {
+ blocklisted: false,
+ description: "A system addon which is shipped with Firefox.",
+ name: "XPI Telemetry System Add-on Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE),
+ updateDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE),
+ signedState: undefined,
+ isSystem: true,
+ isWebExtension: true,
+ multiprocessCompatible: true,
+ };
+
+ const WEBEXTENSION_ADDON_ID = "tel-webextension-xpi@tests.mozilla.org";
+ const WEBEXTENSION_ADDON_INSTALL_DATE = truncateToDays(Date.now());
+ const EXPECTED_WEBEXTENSION_ADDON_DATA = {
+ blocklisted: false,
+ description: "A webextension addon.",
+ name: "XPI Telemetry WebExtension Add-on Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: WEBEXTENSION_ADDON_INSTALL_DATE,
+ updateDay: WEBEXTENSION_ADDON_INSTALL_DATE,
+ signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
+ isSystem: false,
+ isWebExtension: true,
+ multiprocessCompatible: true,
+ };
+
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener(
+ "test_WebExtension",
+ (reason, data) => {
+ Assert.equal(reason, "addons-changed");
+ deferred.resolve();
+ }
+ );
+
+ // Install an add-on so we have some data.
+ let addon = await installXPIFromURL(ADDON_INSTALL_URL);
+
+ // Install a webextension as well.
+ // Note: all addons are webextensions, so doing this again is redundant...
+ ExtensionTestUtils.init(this);
+
+ let webextension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ name: "XPI Telemetry WebExtension Add-on Test",
+ description: "A webextension addon.",
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: WEBEXTENSION_ADDON_ID,
+ },
+ },
+ },
+ });
+
+ await webextension.startup();
+ await deferred.promise;
+ TelemetryEnvironment.unregisterChangeListener("test_WebExtension");
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ // Check addon data.
+ Assert.ok(
+ ADDON_ID in data.addons.activeAddons,
+ "We must have one active addon."
+ );
+ let targetAddon = data.addons.activeAddons[ADDON_ID];
+ for (let f in EXPECTED_ADDON_DATA) {
+ Assert.equal(
+ targetAddon[f],
+ EXPECTED_ADDON_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // Check system add-on data.
+ Assert.ok(
+ SYSTEM_ADDON_ID in data.addons.activeAddons,
+ "We must have one active system addon."
+ );
+ let targetSystemAddon = data.addons.activeAddons[SYSTEM_ADDON_ID];
+ for (let f in EXPECTED_SYSTEM_ADDON_DATA) {
+ Assert.equal(
+ targetSystemAddon[f],
+ EXPECTED_SYSTEM_ADDON_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // Check webextension add-on data.
+ Assert.ok(
+ WEBEXTENSION_ADDON_ID in data.addons.activeAddons,
+ "We must have one active webextension addon."
+ );
+ let targetWebExtensionAddon = data.addons.activeAddons[WEBEXTENSION_ADDON_ID];
+ for (let f in EXPECTED_WEBEXTENSION_ADDON_DATA) {
+ Assert.equal(
+ targetWebExtensionAddon[f],
+ EXPECTED_WEBEXTENSION_ADDON_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ await webextension.unload();
+
+ // Uninstall the addon.
+ await addon.startupPromise;
+ await addon.uninstall();
+});
+
+add_task(async function test_signedAddon() {
+ AddonTestUtils.useRealCertChecks = true;
+
+ const ADDON_INSTALL_URL = gDataRoot + "signed-webext.xpi";
+ const ADDON_ID = "tel-signed-webext@tests.mozilla.org";
+ const ADDON_INSTALL_DATE = truncateToDays(Date.now());
+ const EXPECTED_ADDON_DATA = {
+ blocklisted: false,
+ description: "A signed webextension",
+ name: "XPI Telemetry Signed Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: ADDON_INSTALL_DATE,
+ updateDay: ADDON_INSTALL_DATE,
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ };
+
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener(
+ "test_signedAddon",
+ deferred.resolve
+ );
+
+ // Install the addon.
+ let addon = await installXPIFromURL(ADDON_INSTALL_URL);
+
+ await deferred.promise;
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener("test_signedAddon");
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ // Check addon data.
+ Assert.ok(
+ ADDON_ID in data.addons.activeAddons,
+ "Add-on should be in the environment."
+ );
+ let targetAddon = data.addons.activeAddons[ADDON_ID];
+ for (let f in EXPECTED_ADDON_DATA) {
+ Assert.equal(
+ targetAddon[f],
+ EXPECTED_ADDON_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ AddonTestUtils.useRealCertChecks = false;
+ await addon.startupPromise;
+ await addon.uninstall();
+});
+
+add_task(async function test_addonsFieldsLimit() {
+ const ADDON_INSTALL_URL = gDataRoot + "long-fields.xpi";
+ const ADDON_ID = "tel-longfields-webext@tests.mozilla.org";
+
+ // Install the addon and wait for the TelemetryEnvironment to pick it up.
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener(
+ "test_longFieldsAddon",
+ deferred.resolve
+ );
+ let addon = await installXPIFromURL(ADDON_INSTALL_URL);
+ await deferred.promise;
+ TelemetryEnvironment.unregisterChangeListener("test_longFieldsAddon");
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ // Check that the addon is available and that the string fields are limited.
+ Assert.ok(
+ ADDON_ID in data.addons.activeAddons,
+ "Add-on should be in the environment."
+ );
+ let targetAddon = data.addons.activeAddons[ADDON_ID];
+
+ // TelemetryEnvironment limits the length of string fields for activeAddons to 100 chars,
+ // to mitigate misbehaving addons.
+ Assert.lessOrEqual(
+ targetAddon.version.length,
+ 100,
+ "The version string must have been limited"
+ );
+ Assert.lessOrEqual(
+ targetAddon.name.length,
+ 100,
+ "The name string must have been limited"
+ );
+ Assert.lessOrEqual(
+ targetAddon.description.length,
+ 100,
+ "The description string must have been limited"
+ );
+
+ await addon.startupPromise;
+ await addon.uninstall();
+});
+
+add_task(async function test_collectionWithbrokenAddonData() {
+ const BROKEN_ADDON_ID = "telemetry-test2.example.com@services.mozilla.org";
+ const BROKEN_MANIFEST = {
+ id: "telemetry-test2.example.com@services.mozilla.org",
+ name: "telemetry broken addon",
+ origin: "https://telemetry-test2.example.com",
+ version: 1, // This is intentionally not a string.
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ type: "extension",
+ };
+
+ const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
+ const ADDON_ID = "tel-restartless-webext@tests.mozilla.org";
+ const ADDON_INSTALL_DATE = truncateToDays(Date.now());
+ const EXPECTED_ADDON_DATA = {
+ blocklisted: false,
+ description: "A restartless addon which gets enabled without a reboot.",
+ name: "XPI Telemetry Restartless Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: ADDON_INSTALL_DATE,
+ updateDay: ADDON_INSTALL_DATE,
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ };
+
+ let receivedNotifications = 0;
+
+ let registerCheckpointPromise = aExpected => {
+ return new Promise(resolve =>
+ TelemetryEnvironment.registerChangeListener(
+ "testBrokenAddon_collection" + aExpected,
+ (reason, data) => {
+ Assert.equal(reason, "addons-changed");
+ receivedNotifications++;
+ resolve();
+ }
+ )
+ );
+ };
+
+ let assertCheckpoint = aExpected => {
+ Assert.equal(receivedNotifications, aExpected);
+ TelemetryEnvironment.unregisterChangeListener(
+ "testBrokenAddon_collection" + aExpected
+ );
+ };
+
+ // Register the broken provider and install the broken addon.
+ let checkpointPromise = registerCheckpointPromise(1);
+ let brokenAddonProvider = createMockAddonProvider(
+ "Broken Extensions Provider"
+ );
+ AddonManagerPrivate.registerProvider(brokenAddonProvider);
+ brokenAddonProvider.addAddon(BROKEN_MANIFEST);
+ await checkpointPromise;
+ assertCheckpoint(1);
+
+ // Now install an addon which returns the correct information.
+ checkpointPromise = registerCheckpointPromise(2);
+ let addon = await installXPIFromURL(ADDON_INSTALL_URL);
+ await checkpointPromise;
+ assertCheckpoint(2);
+
+ // Check that the new environment contains the info from the broken provider,
+ // despite the addon missing some details.
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data, {
+ expectBrokenAddons: true,
+ });
+
+ let activeAddons = data.addons.activeAddons;
+ Assert.ok(
+ BROKEN_ADDON_ID in activeAddons,
+ "The addon with the broken manifest must be reported."
+ );
+ Assert.equal(
+ activeAddons[BROKEN_ADDON_ID].version,
+ null,
+ "null should be reported for invalid data."
+ );
+ Assert.ok(ADDON_ID in activeAddons, "The valid addon must be reported.");
+ Assert.equal(
+ activeAddons[ADDON_ID].description,
+ EXPECTED_ADDON_DATA.description,
+ "The description for the valid addon should be correct."
+ );
+
+ // Unregister the broken provider so we don't mess with other tests.
+ AddonManagerPrivate.unregisterProvider(brokenAddonProvider);
+
+ // Uninstall the valid addon.
+ await addon.startupPromise;
+ await addon.uninstall();
+});
+
+add_task(
+ { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
+ async function test_delayed_defaultBrowser() {
+ // Skip this test on Thunderbird since it is not a browser, so it cannot
+ // be the default browser.
+
+ // Make sure we don't have anything already cached for this test.
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+
+ let environmentData = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData);
+ Assert.equal(
+ environmentData.settings.isDefaultBrowser,
+ null,
+ "isDefaultBrowser must be null before the session is restored."
+ );
+
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored");
+
+ environmentData = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData);
+ Assert.ok(
+ "isDefaultBrowser" in environmentData.settings,
+ "isDefaultBrowser must be available after the session is restored."
+ );
+ Assert.equal(
+ typeof environmentData.settings.isDefaultBrowser,
+ "boolean",
+ "isDefaultBrowser must be of the right type."
+ );
+
+ // Make sure pref-flipping doesn't overwrite the browser default state.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ ]);
+ Preferences.reset(PREF_TEST);
+
+ // Watch the test preference.
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener(
+ "testDefaultBrowser_pref",
+ deferred.resolve
+ );
+ // Trigger an environment change.
+ Preferences.set(PREF_TEST, 1);
+ await deferred.promise;
+ TelemetryEnvironment.unregisterChangeListener("testDefaultBrowser_pref");
+
+ // Check that the data is still available.
+ environmentData = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData);
+ Assert.ok(
+ "isDefaultBrowser" in environmentData.settings,
+ "isDefaultBrowser must still be available after a pref is flipped."
+ );
+ }
+);
+
+add_task(async function test_osstrings() {
+ // First test that numbers in sysinfo properties are converted to string fields
+ // in system.os.
+ TelemetryEnvironmentTesting.setSysInfoOverrides({
+ version: 1,
+ name: 2,
+ kernel_version: 3,
+ });
+
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ Assert.equal(data.system.os.version, "1");
+ Assert.equal(data.system.os.name, "2");
+ if (AppConstants.platform == "android") {
+ Assert.equal(data.system.os.kernelVersion, "3");
+ }
+
+ // Check that null values are also handled.
+ TelemetryEnvironmentTesting.setSysInfoOverrides({
+ version: null,
+ name: null,
+ kernel_version: null,
+ });
+
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ Assert.equal(data.system.os.version, null);
+ Assert.equal(data.system.os.name, null);
+ if (AppConstants.platform == "android") {
+ Assert.equal(data.system.os.kernelVersion, null);
+ }
+
+ // Clean up.
+ TelemetryEnvironmentTesting.setSysInfoOverrides({});
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+});
+
+add_task(async function test_experimentsAPI() {
+ const EXPERIMENT1 = "experiment-1";
+ const EXPERIMENT1_BRANCH = "nice-branch";
+ const EXPERIMENT2 = "experiment-2";
+ const EXPERIMENT2_BRANCH = "other-branch";
+
+ let checkExperiment = (environmentData, id, branch, type = null) => {
+ Assert.ok(
+ "experiments" in environmentData,
+ "The current environment must report the experiment annotations."
+ );
+ Assert.ok(
+ id in environmentData.experiments,
+ "The experiments section must contain the expected experiment id."
+ );
+ Assert.equal(
+ environmentData.experiments[id].branch,
+ branch,
+ "The experiment branch must be correct."
+ );
+ };
+
+ // Clean the environment and check that it's reporting the correct info.
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ // We don't expect the experiments section to be there if no annotation
+ // happened.
+ Assert.ok(
+ !("experiments" in data),
+ "No experiments section must be reported if nothing was annotated."
+ );
+
+ // Add a change listener and add an experiment annotation.
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener(
+ "test_experimentsAPI",
+ (reason, env) => {
+ deferred.resolve(env);
+ }
+ );
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT1, EXPERIMENT1_BRANCH);
+ let eventEnvironmentData = await deferred.promise;
+
+ // Check that the old environment does not contain the experiments.
+ TelemetryEnvironmentTesting.checkEnvironmentData(eventEnvironmentData);
+ Assert.ok(
+ !("experiments" in eventEnvironmentData),
+ "No experiments section must be reported in the old environment."
+ );
+
+ // Check that the current environment contains the right experiment.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ checkExperiment(data, EXPERIMENT1, EXPERIMENT1_BRANCH);
+
+ TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI");
+
+ // Add a second annotation and check that both experiments are there.
+ deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener(
+ "test_experimentsAPI2",
+ (reason, env) => {
+ deferred.resolve(env);
+ }
+ );
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT2, EXPERIMENT2_BRANCH);
+ eventEnvironmentData = await deferred.promise;
+
+ // Check that the current environment contains both the experiment.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ checkExperiment(data, EXPERIMENT1, EXPERIMENT1_BRANCH);
+ checkExperiment(data, EXPERIMENT2, EXPERIMENT2_BRANCH);
+
+ // The previous environment should only contain the first experiment.
+ checkExperiment(eventEnvironmentData, EXPERIMENT1, EXPERIMENT1_BRANCH);
+ Assert.ok(
+ !(EXPERIMENT2 in eventEnvironmentData),
+ "The old environment must not contain the new experiment annotation."
+ );
+
+ TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI2");
+
+ // Check that removing an unknown experiment annotation does not trigger
+ // a notification.
+ TelemetryEnvironment.registerChangeListener("test_experimentsAPI3", () => {
+ Assert.ok(
+ false,
+ "Removing an unknown experiment annotation must not trigger a change."
+ );
+ });
+ TelemetryEnvironment.setExperimentInactive("unknown-experiment-id");
+ // Also make sure that passing non-string parameters arguments doesn't throw nor
+ // trigger a notification.
+ TelemetryEnvironment.setExperimentActive({}, "some-branch");
+ TelemetryEnvironment.setExperimentActive("some-id", {});
+ TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI3");
+
+ // Check that removing a known experiment leaves the other in place and triggers
+ // a change.
+ deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener(
+ "test_experimentsAPI4",
+ (reason, env) => {
+ deferred.resolve(env);
+ }
+ );
+ TelemetryEnvironment.setExperimentInactive(EXPERIMENT1);
+ eventEnvironmentData = await deferred.promise;
+
+ // Check that the current environment contains just the second experiment.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.ok(
+ !(EXPERIMENT1 in data),
+ "The current environment must not contain the removed experiment annotation."
+ );
+ checkExperiment(data, EXPERIMENT2, EXPERIMENT2_BRANCH);
+
+ // The previous environment should contain both annotations.
+ checkExperiment(eventEnvironmentData, EXPERIMENT1, EXPERIMENT1_BRANCH);
+ checkExperiment(eventEnvironmentData, EXPERIMENT2, EXPERIMENT2_BRANCH);
+
+ // Set an experiment with a type and check that it correctly shows up.
+ TelemetryEnvironment.setExperimentActive(
+ "typed-experiment",
+ "random-branch",
+ { type: "ab-test" }
+ );
+ data = TelemetryEnvironment.currentEnvironment;
+ checkExperiment(data, "typed-experiment", "random-branch", "ab-test");
+});
+
+add_task(async function test_experimentsAPI_limits() {
+ const EXPERIMENT =
+ "experiment-2-experiment-2-experiment-2-experiment-2-experiment-2" +
+ "-experiment-2-experiment-2-experiment-2-experiment-2";
+ const EXPERIMENT_BRANCH =
+ "other-branch-other-branch-other-branch-other-branch-other" +
+ "-branch-other-branch-other-branch-other-branch-other-branch";
+ const EXPERIMENT_TRUNCATED = EXPERIMENT.substring(0, 100);
+ const EXPERIMENT_BRANCH_TRUNCATED = EXPERIMENT_BRANCH.substring(0, 100);
+
+ // Clean the environment and check that it's reporting the correct info.
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ // We don't expect the experiments section to be there if no annotation
+ // happened.
+ Assert.ok(
+ !("experiments" in data),
+ "No experiments section must be reported if nothing was annotated."
+ );
+
+ // Add a change listener and wait for the annotation to happen.
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener("test_experimentsAPI", () =>
+ deferred.resolve()
+ );
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT, EXPERIMENT_BRANCH);
+ await deferred.promise;
+
+ // Check that the current environment contains the truncated values
+ // for the experiment data.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.ok(
+ "experiments" in data,
+ "The environment must contain an experiments section."
+ );
+ Assert.ok(
+ EXPERIMENT_TRUNCATED in data.experiments,
+ "The experiments must be reporting the truncated id."
+ );
+ Assert.ok(
+ !(EXPERIMENT in data.experiments),
+ "The experiments must not be reporting the full id."
+ );
+ Assert.equal(
+ EXPERIMENT_BRANCH_TRUNCATED,
+ data.experiments[EXPERIMENT_TRUNCATED].branch,
+ "The experiments must be reporting the truncated branch."
+ );
+
+ TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI");
+
+ // Check that an overly long type is truncated.
+ const longType = "a0123456678901234567890123456789";
+ TelemetryEnvironment.setExperimentActive("exp", "some-branch", {
+ type: longType,
+ });
+ data = TelemetryEnvironment.currentEnvironment;
+ Assert.equal(data.experiments.exp.type, longType.substring(0, 20));
+});
+
+if (gIsWindows) {
+ add_task(async function test_environmentHDDInfo() {
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ let empty = { model: null, revision: null, type: null };
+ Assert.deepEqual(
+ data.system.hdd,
+ { binary: empty, profile: empty, system: empty },
+ "Should have no data yet."
+ );
+ await TelemetryEnvironment.delayedInit();
+ data = TelemetryEnvironment.currentEnvironment;
+ for (let k of TelemetryEnvironmentTesting.EXPECTED_HDD_FIELDS) {
+ TelemetryEnvironmentTesting.checkString(data.system.hdd[k].model);
+ TelemetryEnvironmentTesting.checkString(data.system.hdd[k].revision);
+ TelemetryEnvironmentTesting.checkString(data.system.hdd[k].type);
+ }
+ });
+
+ add_task(async function test_environmentProcessInfo() {
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ Assert.deepEqual(data.system.isWow64, null, "Should have no data yet.");
+ await TelemetryEnvironment.delayedInit();
+ data = TelemetryEnvironment.currentEnvironment;
+ Assert.equal(
+ typeof data.system.isWow64,
+ "boolean",
+ "isWow64 must be a boolean."
+ );
+ Assert.equal(
+ typeof data.system.isWowARM64,
+ "boolean",
+ "isWowARM64 must be a boolean."
+ );
+ Assert.equal(
+ typeof data.system.hasWinPackageId,
+ "boolean",
+ "hasWinPackageId must be a boolean."
+ );
+ // This is only sent for Mozilla produced MSIX packages
+ Assert.ok(
+ !("winPackageFamilyName" in data.system) ||
+ data.system.winPackageFamilyName === null ||
+ typeof data.system.winPackageFamilyName === "string",
+ "winPackageFamilyName must be a string if non null"
+ );
+ // These should be numbers if they are not null
+ for (let f of [
+ "count",
+ "model",
+ "family",
+ "stepping",
+ "l2cacheKB",
+ "l3cacheKB",
+ "speedMHz",
+ "cores",
+ ]) {
+ Assert.ok(
+ !(f in data.system.cpu) ||
+ data.system.cpu[f] === null ||
+ Number.isFinite(data.system.cpu[f]),
+ f + " must be a number if non null."
+ );
+ }
+ Assert.ok(
+ TelemetryEnvironmentTesting.checkString(data.system.cpu.vendor),
+ "vendor must be a valid string."
+ );
+ });
+
+ add_task(async function test_environmentOSInfo() {
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ Assert.deepEqual(
+ data.system.os.installYear,
+ null,
+ "Should have no data yet."
+ );
+ await TelemetryEnvironment.delayedInit();
+ data = TelemetryEnvironment.currentEnvironment;
+ Assert.ok(
+ Number.isFinite(data.system.os.installYear),
+ "Install year must be a number."
+ );
+ });
+}
+
+add_task(
+ { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
+ async function test_environmentServicesInfo() {
+ let cache = TelemetryEnvironment.testCleanRestart();
+ await cache.onInitialized();
+ let oldGetFxaSignedInUser = cache._getFxaSignedInUser;
+ try {
+ // Test the 'yes to both' case.
+
+ // This makes the weave service return that the usere is definitely a sync user
+ Preferences.set("services.sync.username", "c00lperson123@example.com");
+ let calledFxa = false;
+ cache._getFxaSignedInUser = () => {
+ calledFxa = true;
+ return null;
+ };
+
+ await cache._updateServicesInfo();
+ ok(
+ !calledFxa,
+ "Shouldn't need to ask FxA if they're definitely signed in"
+ );
+ deepEqual(cache.currentEnvironment.services, {
+ accountEnabled: true,
+ syncEnabled: true,
+ });
+
+ // Test the fxa-but-not-sync case.
+ Preferences.reset("services.sync.username");
+ // We don't actually inspect the returned object, just t
+ cache._getFxaSignedInUser = async () => {
+ return {};
+ };
+ await cache._updateServicesInfo();
+ deepEqual(cache.currentEnvironment.services, {
+ accountEnabled: true,
+ syncEnabled: false,
+ });
+ // Test the "no to both" case.
+ cache._getFxaSignedInUser = async () => {
+ return null;
+ };
+ await cache._updateServicesInfo();
+ deepEqual(cache.currentEnvironment.services, {
+ accountEnabled: false,
+ syncEnabled: false,
+ });
+ // And finally, the 'fxa is in an error state' case.
+ cache._getFxaSignedInUser = () => {
+ throw new Error("You'll never know");
+ };
+ await cache._updateServicesInfo();
+ equal(cache.currentEnvironment.services, null);
+ } finally {
+ cache._getFxaSignedInUser = oldGetFxaSignedInUser;
+ Preferences.reset("services.sync.username");
+ }
+ }
+);
+
+add_task(async function test_normandyTestPrefsGoneAfter91() {
+ const testPrefBool = "app.normandy.test-prefs.bool";
+ const testPrefInteger = "app.normandy.test-prefs.integer";
+ const testPrefString = "app.normandy.test-prefs.string";
+
+ Services.prefs.setBoolPref(testPrefBool, true);
+ Services.prefs.setIntPref(testPrefInteger, 10);
+ Services.prefs.setCharPref(testPrefString, "test-string");
+
+ const data = TelemetryEnvironment.currentEnvironment;
+
+ if (Services.vc.compare(data.build.version, "91") > 0) {
+ Assert.equal(
+ data.settings.userPrefs["app.normandy.test-prefs.bool"],
+ null,
+ "This probe should expire in FX91. bug 1686105 "
+ );
+ Assert.equal(
+ data.settings.userPrefs["app.normandy.test-prefs.integer"],
+ null,
+ "This probe should expire in FX91. bug 1686105 "
+ );
+ Assert.equal(
+ data.settings.userPrefs["app.normandy.test-prefs.string"],
+ null,
+ "This probe should expire in FX91. bug 1686105 "
+ );
+ }
+});
+
+add_task(async function test_environmentShutdown() {
+ // Define and reset the test preference.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ ]);
+ Preferences.reset(PREF_TEST);
+
+ // Set up the preferences and listener, then the trigger shutdown
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ TelemetryEnvironment.registerChangeListener(
+ "test_environmentShutdownChange",
+ () => {
+ // Register a new change listener that asserts if change is propogated
+ Assert.ok(false, "No change should be propagated after shutdown.");
+ }
+ );
+ TelemetryEnvironment.shutdown();
+
+ // Flipping the test preference after shutdown should not trigger the listener
+ Preferences.set(PREF_TEST, 1);
+
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener(
+ "test_environmentShutdownChange"
+ );
+});
+
+add_task(async function test_environmentDidntChange() {
+ // Clean the environment and check that it's reporting the correct info.
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ const LISTENER_NAME = "test_environmentDidntChange";
+ TelemetryEnvironment.registerChangeListener(LISTENER_NAME, () => {
+ Assert.ok(false, "The environment didn't actually change.");
+ });
+
+ // Don't actually change the environment, but notify of a compositor abort.
+ const COMPOSITOR_ABORTED_TOPIC = "compositor:process-aborted";
+ Services.obs.notifyObservers(null, COMPOSITOR_ABORTED_TOPIC);
+
+ TelemetryEnvironment.unregisterChangeListener(LISTENER_NAME);
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment_search.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment_search.js
new file mode 100644
index 0000000000..5b3a572ef1
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment_search.js
@@ -0,0 +1,410 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+const { TelemetryEnvironmentTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryEnvironmentTesting.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+
+function promiseNextTick() {
+ return new Promise(resolve => executeSoon(resolve));
+}
+
+// The webserver hosting the addons.
+var gHttpServer = null;
+// The URL of the webserver root.
+var gHttpRoot = null;
+// The URL of the data directory, on the webserver.
+var gDataRoot = null;
+
+add_task(async function setup() {
+ TelemetryEnvironmentTesting.registerFakeSysInfo();
+ TelemetryEnvironmentTesting.spoofGfxAdapter();
+ do_get_profile();
+
+ // We need to ensure FOG is initialized, otherwise we will panic trying to get test values.
+ Services.fog.initializeFOG();
+
+ // The system add-on must be installed before AddonManager is started.
+ const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true);
+ do_get_file("system.xpi").copyTo(
+ distroDir,
+ "tel-system-xpi@tests.mozilla.org.xpi"
+ );
+ let system_addon = FileUtils.File(distroDir.path);
+ system_addon.append("tel-system-xpi@tests.mozilla.org.xpi");
+ system_addon.lastModifiedTime = SYSTEM_ADDON_INSTALL_DATE;
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+
+ TelemetryEnvironmentTesting.init(gAppInfo);
+
+ // The test runs in a fresh profile so starting the AddonManager causes
+ // the addons database to be created (as does setting new theme).
+ // For test_addonsStartup below, we want to test a "warm" startup where
+ // there is already a database on disk. Simulate that here by just
+ // restarting the AddonManager.
+ await AddonTestUtils.promiseShutdownManager();
+ await AddonTestUtils.overrideBuiltIns({ system: [] });
+ AddonTestUtils.addonStartup.remove(true);
+ await AddonTestUtils.promiseStartupManager();
+
+ // Setup a webserver to serve Addons, etc.
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+ let port = gHttpServer.identity.primaryPort;
+ gHttpRoot = "http://localhost:" + port + "/";
+ gDataRoot = gHttpRoot + "data/";
+ gHttpServer.registerDirectory("/data/", do_get_cwd());
+ registerCleanupFunction(() => gHttpServer.stop(() => {}));
+
+ // Create the attribution data file, so that settings.attribution will exist.
+ // The attribution functionality only exists in Firefox.
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ TelemetryEnvironmentTesting.spoofAttributionData();
+ registerCleanupFunction(TelemetryEnvironmentTesting.cleanupAttributionData);
+ }
+
+ await TelemetryEnvironmentTesting.spoofProfileReset();
+ await TelemetryEnvironment.delayedInit();
+ await SearchTestUtils.useTestEngines("data", "search-extensions");
+
+ // Now continue with startup.
+ let initPromise = TelemetryEnvironment.onInitialized();
+ finishAddonManagerStartup();
+
+ // Fake the delayed startup event for intl data to load.
+ fakeIntlReady();
+
+ let environmentData = await initPromise;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData, {
+ isInitial: true,
+ });
+
+ TelemetryEnvironmentTesting.spoofPartnerInfo();
+ Services.obs.notifyObservers(null, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);
+
+ environmentData = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData, {
+ assertProcessData: true,
+ });
+});
+
+async function checkDefaultSearch(privateOn, reInitSearchService) {
+ // Start off with separate default engine for private browsing turned off.
+ Preferences.set(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ privateOn
+ );
+ Preferences.set("browser.search.separatePrivateDefault", privateOn);
+
+ let data;
+ if (privateOn) {
+ data = await TelemetryEnvironment.testCleanRestart().onInitialized();
+ } else {
+ data = TelemetryEnvironment.currentEnvironment;
+ }
+
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.ok(!("defaultSearchEngine" in data.settings));
+ Assert.ok(!("defaultSearchEngineData" in data.settings));
+ Assert.ok(!("defaultPrivateSearchEngine" in data.settings));
+ Assert.ok(!("defaultPrivateSearchEngineData" in data.settings));
+
+ // Load the engines definitions from a xpcshell data: that's needed so that
+ // the search provider reports an engine identifier.
+
+ // Initialize the search service.
+ if (reInitSearchService) {
+ Services.search.wrappedJSObject.reset();
+ }
+ await Services.search.init();
+ await promiseNextTick();
+
+ // Our default engine from the JAR file has an identifier. Check if it is correctly
+ // reported.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.equal(data.settings.defaultSearchEngine, "telemetrySearchIdentifier");
+ let expectedSearchEngineData = {
+ name: "telemetrySearchIdentifier",
+ loadPath: "[addon]telemetrySearchIdentifier@search.mozilla.org",
+ origin: "default",
+ submissionURL:
+ "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB?search=&sourceId=Mozilla-search",
+ };
+ Assert.deepEqual(
+ data.settings.defaultSearchEngineData,
+ expectedSearchEngineData
+ );
+ if (privateOn) {
+ Assert.equal(
+ data.settings.defaultPrivateSearchEngine,
+ "telemetrySearchIdentifier"
+ );
+ Assert.deepEqual(
+ data.settings.defaultPrivateSearchEngineData,
+ expectedSearchEngineData,
+ "Should have the correct data for the private search engine"
+ );
+ } else {
+ Assert.ok(
+ !("defaultPrivateSearchEngine" in data.settings),
+ "Should not have private name recorded as the pref for separate is off"
+ );
+ Assert.ok(
+ !("defaultPrivateSearchEngineData" in data.settings),
+ "Should not have private data recorded as the pref for separate is off"
+ );
+ }
+
+ // Add a new search engine (this will have no engine identifier).
+ const SEARCH_ENGINE_ID = privateOn
+ ? "telemetry_private"
+ : "telemetry_default";
+ const SEARCH_ENGINE_URL = `https://www.example.org/${
+ privateOn ? "private" : ""
+ }`;
+ await SearchTestUtils.installSearchExtension({
+ id: `${SEARCH_ENGINE_ID}@test.engine`,
+ name: SEARCH_ENGINE_ID,
+ search_url: SEARCH_ENGINE_URL,
+ });
+
+ // Register a new change listener and then wait for the search engine change to be notified.
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener(
+ "testWatch_SearchDefault",
+ deferred.resolve
+ );
+ if (privateOn) {
+ // As we had no default and no search engines, the normal mode engine will
+ // assume the same as the added engine. To ensure the telemetry is different
+ // we enforce a different default here.
+ const engine = await Services.search.getEngineByName(
+ "telemetrySearchIdentifier"
+ );
+ engine.hidden = false;
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.setDefaultPrivate(
+ Services.search.getEngineByName(SEARCH_ENGINE_ID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ } else {
+ await Services.search.setDefault(
+ Services.search.getEngineByName(SEARCH_ENGINE_ID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ await deferred.promise;
+
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID;
+ const EXPECTED_SEARCH_ENGINE_DATA = {
+ name: SEARCH_ENGINE_ID,
+ loadPath: `[addon]${SEARCH_ENGINE_ID}@test.engine`,
+ origin: "verified",
+ };
+ if (privateOn) {
+ Assert.equal(
+ data.settings.defaultSearchEngine,
+ "telemetrySearchIdentifier"
+ );
+ Assert.deepEqual(
+ data.settings.defaultSearchEngineData,
+ expectedSearchEngineData
+ );
+ Assert.equal(
+ data.settings.defaultPrivateSearchEngine,
+ EXPECTED_SEARCH_ENGINE
+ );
+ Assert.deepEqual(
+ data.settings.defaultPrivateSearchEngineData,
+ EXPECTED_SEARCH_ENGINE_DATA
+ );
+ } else {
+ Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);
+ Assert.deepEqual(
+ data.settings.defaultSearchEngineData,
+ EXPECTED_SEARCH_ENGINE_DATA
+ );
+ }
+ TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
+}
+
+add_task(async function test_defaultSearchEngine() {
+ await checkDefaultSearch(false);
+
+ // Cleanly install an engine from an xml file, and check if origin is
+ // recorded as "verified".
+ let promise = new Promise(resolve => {
+ TelemetryEnvironment.registerChangeListener(
+ "testWatch_SearchDefault",
+ resolve
+ );
+ });
+ let engine = await new Promise((resolve, reject) => {
+ Services.obs.addObserver(function obs(obsSubject, obsTopic, obsData) {
+ try {
+ let searchEngine = obsSubject.QueryInterface(Ci.nsISearchEngine);
+ info("Observed " + obsData + " for " + searchEngine.name);
+ if (
+ obsData != "engine-added" ||
+ searchEngine.name != "engine-telemetry"
+ ) {
+ return;
+ }
+
+ Services.obs.removeObserver(obs, "browser-search-engine-modified");
+ resolve(searchEngine);
+ } catch (ex) {
+ reject(ex);
+ }
+ }, "browser-search-engine-modified");
+ Services.search.addOpenSearchEngine(gDataRoot + "/engine.xml", null);
+ });
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await promise;
+ TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.deepEqual(data.settings.defaultSearchEngineData, {
+ name: "engine-telemetry",
+ loadPath: "[http]localhost/engine-telemetry.xml",
+ origin: "verified",
+ });
+
+ // Now break this engine's load path hash.
+ promise = new Promise(resolve => {
+ TelemetryEnvironment.registerChangeListener(
+ "testWatch_SearchDefault",
+ resolve
+ );
+ });
+ engine.wrappedJSObject.setAttr("loadPathHash", "broken");
+ Services.obs.notifyObservers(
+ null,
+ "browser-search-engine-modified",
+ "engine-default"
+ );
+ await promise;
+ TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
+ data = TelemetryEnvironment.currentEnvironment;
+ Assert.equal(data.settings.defaultSearchEngineData.origin, "invalid");
+ await Services.search.removeEngine(engine);
+
+ const SEARCH_ENGINE_ID = "telemetry_default";
+ const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID;
+ // Work around bug 1165341: Intentionally set the default engine.
+ await Services.search.setDefault(
+ Services.search.getEngineByName(SEARCH_ENGINE_ID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ // Double-check the default for the next part of the test.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);
+
+ // Define and reset the test preference.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ ]);
+ Preferences.reset(PREF_TEST);
+
+ // Watch the test preference.
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener(
+ "testSearchEngine_pref",
+ deferred.resolve
+ );
+ // Trigger an environment change.
+ Preferences.set(PREF_TEST, 1);
+ await deferred.promise;
+ TelemetryEnvironment.unregisterChangeListener("testSearchEngine_pref");
+
+ // Check that the search engine information is correctly retained when prefs change.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);
+});
+
+add_task(async function test_defaultPrivateSearchEngine() {
+ await checkDefaultSearch(true, true);
+});
+
+add_task(async function test_defaultSearchEngine_paramsChanged() {
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestEngine",
+ search_url: "https://www.google.com/fake1",
+ },
+ { skipUnload: true }
+ );
+
+ let promise = new Promise(resolve => {
+ TelemetryEnvironment.registerChangeListener(
+ "testWatch_SearchDefault",
+ resolve
+ );
+ });
+ let engine = Services.search.getEngineByName("TestEngine");
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await promise;
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.deepEqual(data.settings.defaultSearchEngineData, {
+ name: "TestEngine",
+ loadPath: "[addon]testengine@tests.mozilla.org",
+ origin: "verified",
+ submissionURL: "https://www.google.com/fake1?q=",
+ });
+
+ promise = new Promise(resolve => {
+ TelemetryEnvironment.registerChangeListener(
+ "testWatch_SearchDefault",
+ resolve
+ );
+ });
+
+ engine.wrappedJSObject.update({
+ manifest: SearchTestUtils.createEngineManifest({
+ name: "TestEngine",
+ version: "1.2",
+ search_url: "https://www.google.com/fake2",
+ }),
+ });
+
+ await promise;
+
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.deepEqual(data.settings.defaultSearchEngineData, {
+ name: "TestEngine",
+ loadPath: "[addon]testengine@tests.mozilla.org",
+ origin: "verified",
+ submissionURL: "https://www.google.com/fake2?q=",
+ });
+
+ await extension.unload();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js
new file mode 100644
index 0000000000..4369c5a608
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js
@@ -0,0 +1,1109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+ChromeUtils.defineESModuleGetters(this, {
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+});
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const PRERELEASE_CHANNELS = Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS;
+const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS;
+
+function checkEventFormat(events) {
+ Assert.ok(Array.isArray(events), "Events should be serialized to an array.");
+ for (let e of events) {
+ Assert.ok(Array.isArray(e), "Event should be an array.");
+ Assert.greaterOrEqual(
+ e.length,
+ 4,
+ "Event should have at least 4 elements."
+ );
+ Assert.lessOrEqual(e.length, 6, "Event should have at most 6 elements.");
+
+ Assert.equal(typeof e[0], "number", "Element 0 should be a number.");
+ Assert.equal(typeof e[1], "string", "Element 1 should be a string.");
+ Assert.equal(typeof e[2], "string", "Element 2 should be a string.");
+ Assert.equal(typeof e[3], "string", "Element 3 should be a string.");
+
+ if (e.length > 4) {
+ Assert.ok(
+ e[4] === null || typeof e[4] == "string",
+ "Event element 4 should be null or a string."
+ );
+ }
+ if (e.length > 5) {
+ Assert.ok(
+ e[5] === null || typeof e[5] == "object",
+ "Event element 5 should be null or an object."
+ );
+ }
+
+ let extra = e[5];
+ if (extra) {
+ Assert.ok(
+ Object.keys(extra).every(k => typeof k == "string"),
+ "All extra keys should be strings."
+ );
+ Assert.ok(
+ Object.values(extra).every(v => typeof v == "string"),
+ "All extra values should be strings."
+ );
+ }
+ }
+}
+
+/**
+ * @param summaries is of the form
+ * [{process, [event category, event object, event method], count}]
+ * @param clearScalars - true if you want to clear the scalars
+ */
+function checkEventSummary(summaries, clearScalars) {
+ let scalars = Telemetry.getSnapshotForKeyedScalars("main", clearScalars);
+
+ for (let [process, [category, eObject, method], count] of summaries) {
+ let uniqueEventName = `${category}#${eObject}#${method}`;
+ let summaryCount;
+ if (process === "dynamic") {
+ summaryCount =
+ scalars.dynamic["telemetry.dynamic_event_counts"][uniqueEventName];
+ } else {
+ summaryCount =
+ scalars[process]["telemetry.event_counts"][uniqueEventName];
+ }
+ Assert.equal(
+ summaryCount,
+ count,
+ `${uniqueEventName} had wrong summary count`
+ );
+ }
+}
+
+function checkRegistrationFailure(failureType) {
+ let snapshot = Telemetry.getSnapshotForHistograms("main", true);
+ Assert.ok(
+ "parent" in snapshot,
+ "There should be at least one parent histogram when checking for registration failures."
+ );
+ Assert.ok(
+ "TELEMETRY_EVENT_REGISTRATION_ERROR" in snapshot.parent,
+ "TELEMETRY_EVENT_REGISTRATION_ERROR should exist when checking for registration failures."
+ );
+ let values = snapshot.parent.TELEMETRY_EVENT_REGISTRATION_ERROR.values;
+ Assert.ok(
+ !!values,
+ "TELEMETRY_EVENT_REGISTRATION_ERROR's values should exist when checking for registration failures."
+ );
+ Assert.equal(
+ values[failureType],
+ 1,
+ `Event registration ought to have failed due to type ${failureType}`
+ );
+}
+
+function checkRecordingFailure(failureType) {
+ let snapshot = Telemetry.getSnapshotForHistograms("main", true);
+ Assert.ok(
+ "parent" in snapshot,
+ "There should be at least one parent histogram when checking for recording failures."
+ );
+ Assert.ok(
+ "TELEMETRY_EVENT_RECORDING_ERROR" in snapshot.parent,
+ "TELEMETRY_EVENT_RECORDING_ERROR should exist when checking for recording failures."
+ );
+ let values = snapshot.parent.TELEMETRY_EVENT_RECORDING_ERROR.values;
+ Assert.ok(
+ !!values,
+ "TELEMETRY_EVENT_RECORDING_ERROR's values should exist when checking for recording failures."
+ );
+ Assert.equal(
+ values[failureType],
+ 1,
+ `Event recording ought to have failed due to type ${failureType}`
+ );
+}
+
+add_task(async function test_event_summary_limit() {
+ Telemetry.clearEvents();
+ Telemetry.clearScalars();
+
+ const limit = 500; // matches kMaxEventSummaryKeys in TelemetryScalar.cpp.
+ let objects = [];
+ for (let i = 0; i < limit + 1; i++) {
+ objects.push("object" + i);
+ }
+ // Using "telemetry.test.dynamic" as using "telemetry.test" will enable
+ // the "telemetry.test" category.
+ Telemetry.registerEvents("telemetry.test.dynamic", {
+ test_method: {
+ methods: ["testMethod"],
+ objects,
+ record_on_release: true,
+ },
+ });
+ for (let object of objects) {
+ Telemetry.recordEvent("telemetry.test.dynamic", "testMethod", object);
+ }
+
+ TelemetryTestUtils.assertNumberOfEvents(
+ limit + 1,
+ {},
+ { process: "dynamic" }
+ );
+ let scalarSnapshot = Telemetry.getSnapshotForKeyedScalars("main", true);
+ Assert.equal(
+ Object.keys(scalarSnapshot.dynamic["telemetry.dynamic_event_counts"])
+ .length,
+ limit,
+ "Should not have recorded more than `limit` events"
+ );
+});
+
+add_task(async function test_recording_state() {
+ Telemetry.clearEvents();
+ Telemetry.clearScalars();
+
+ const events = [
+ ["telemetry.test", "test1", "object1"],
+ ["telemetry.test.second", "test", "object1"],
+ ];
+
+ // Both test categories should be off by default.
+ events.forEach(e => Telemetry.recordEvent(...e));
+ TelemetryTestUtils.assertEvents([]);
+ checkEventSummary(
+ events.map(e => ["parent", e, 1]),
+ true
+ );
+
+ // Enable one test category and see that we record correctly.
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+ events.forEach(e => Telemetry.recordEvent(...e));
+ TelemetryTestUtils.assertEvents([events[0]]);
+ checkEventSummary(
+ events.map(e => ["parent", e, 1]),
+ true
+ );
+
+ // Also enable the other test category and see that we record correctly.
+ Telemetry.setEventRecordingEnabled("telemetry.test.second", true);
+ events.forEach(e => Telemetry.recordEvent(...e));
+ TelemetryTestUtils.assertEvents(events);
+ checkEventSummary(
+ events.map(e => ["parent", e, 1]),
+ true
+ );
+
+ // Now turn of one category again and check that this works as expected.
+ Telemetry.setEventRecordingEnabled("telemetry.test", false);
+ events.forEach(e => Telemetry.recordEvent(...e));
+ TelemetryTestUtils.assertEvents([events[1]]);
+ checkEventSummary(
+ events.map(e => ["parent", e, 1]),
+ true
+ );
+});
+
+add_task(async function recording_setup() {
+ // Make sure both test categories are enabled for the remaining tests.
+ // Otherwise their event recording won't work.
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+ Telemetry.setEventRecordingEnabled("telemetry.test.second", true);
+});
+
+add_task(async function test_recording() {
+ Telemetry.clearScalars();
+ Telemetry.clearEvents();
+
+ // Record some events.
+ let expected = [
+ { optout: false, event: ["telemetry.test", "test1", "object1"] },
+ { optout: false, event: ["telemetry.test", "test2", "object2"] },
+
+ { optout: false, event: ["telemetry.test", "test1", "object1", "value"] },
+ {
+ optout: false,
+ event: ["telemetry.test", "test1", "object1", "value", null],
+ },
+ {
+ optout: false,
+ event: ["telemetry.test", "test1", "object1", null, { key1: "value1" }],
+ },
+ {
+ optout: false,
+ event: [
+ "telemetry.test",
+ "test1",
+ "object1",
+ "value",
+ { key1: "value1", key2: "value2" },
+ ],
+ },
+
+ { optout: true, event: ["telemetry.test", "optout", "object1"] },
+ { optout: false, event: ["telemetry.test.second", "test", "object1"] },
+ {
+ optout: false,
+ event: [
+ "telemetry.test.second",
+ "test",
+ "object1",
+ null,
+ { key1: "value1" },
+ ],
+ },
+ ];
+
+ for (let entry of expected) {
+ entry.tsBefore = Math.floor(Telemetry.msSinceProcessStart());
+ try {
+ Telemetry.recordEvent(...entry.event);
+ } catch (ex) {
+ Assert.ok(
+ false,
+ `Failed to record event ${JSON.stringify(entry.event)}: ${ex}`
+ );
+ }
+ entry.tsAfter = Math.floor(Telemetry.msSinceProcessStart());
+ }
+
+ // Strip off trailing null values to match the serialized events.
+ for (let entry of expected) {
+ let e = entry.event;
+ while (e.length >= 3 && e[e.length - 1] === null) {
+ e.pop();
+ }
+ }
+
+ // Check that the events were summarized properly.
+ let summaries = {};
+ expected.forEach(({ optout, event }) => {
+ let [category, eObject, method] = event;
+ let uniqueEventName = `${category}#${eObject}#${method}`;
+ if (!(uniqueEventName in summaries)) {
+ summaries[uniqueEventName] = ["parent", event, 1];
+ } else {
+ summaries[uniqueEventName][2]++;
+ }
+ });
+ checkEventSummary(Object.values(summaries), true);
+
+ // The following should not result in any recorded events.
+ Telemetry.recordEvent("unknown.category", "test1", "object1");
+ checkRecordingFailure(0 /* UnknownEvent */);
+ Telemetry.recordEvent("telemetry.test", "unknown", "object1");
+ checkRecordingFailure(0 /* UnknownEvent */);
+ Telemetry.recordEvent("telemetry.test", "test1", "unknown");
+ checkRecordingFailure(0 /* UnknownEvent */);
+
+ let checkEvents = (events, expectedEvents) => {
+ checkEventFormat(events);
+ Assert.equal(
+ events.length,
+ expectedEvents.length,
+ "Snapshot should have the right number of events."
+ );
+
+ for (let i = 0; i < events.length; ++i) {
+ let { tsBefore, tsAfter } = expectedEvents[i];
+ let ts = events[i][0];
+ Assert.greaterOrEqual(
+ ts,
+ tsBefore,
+ "The recorded timestamp should be greater than the one before recording."
+ );
+ Assert.lessOrEqual(
+ ts,
+ tsAfter,
+ "The recorded timestamp should be less than the one after recording."
+ );
+
+ let recordedData = events[i].slice(1);
+ let expectedData = expectedEvents[i].event.slice();
+ Assert.deepEqual(
+ recordedData,
+ expectedData,
+ "The recorded event data should match."
+ );
+ }
+ };
+
+ // Check that the expected events were recorded.
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ checkEvents(snapshot.parent, expected);
+
+ // Check serializing only opt-out events.
+ snapshot = Telemetry.snapshotEvents(ALL_CHANNELS, false);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ let filtered = expected.filter(e => !!e.optout);
+ checkEvents(snapshot.parent, filtered);
+});
+
+add_task(async function test_clear() {
+ Telemetry.clearEvents();
+
+ const COUNT = 10;
+ for (let i = 0; i < COUNT; ++i) {
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ Telemetry.recordEvent("telemetry.test.second", "test", "object1");
+ }
+
+ // Check that events were recorded.
+ // The events are cleared by passing the respective flag.
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ Assert.equal(
+ snapshot.parent.length,
+ 2 * COUNT,
+ `Should have recorded ${2 * COUNT} events.`
+ );
+
+ // Now the events should be cleared.
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ `Should have cleared the events.`
+ );
+
+ for (let i = 0; i < COUNT; ++i) {
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ Telemetry.recordEvent("telemetry.test.second", "test", "object1");
+ }
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true, 5);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ Assert.equal(snapshot.parent.length, 5, "Should have returned 5 events");
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ Assert.equal(
+ snapshot.parent.length,
+ 2 * COUNT - 5,
+ `Should have returned ${2 * COUNT - 5} events`
+ );
+
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false, 5);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ Assert.equal(snapshot.parent.length, 5, "Should have returned 5 events");
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ Assert.equal(
+ snapshot.parent.length,
+ 2 * COUNT - 5 + 1,
+ `Should have returned ${2 * COUNT - 5 + 1} events`
+ );
+});
+
+add_task(async function test_expiry() {
+ Telemetry.clearEvents();
+
+ // Recording call with event that is expired by version.
+ Telemetry.recordEvent("telemetry.test", "expired_version", "object1");
+ checkRecordingFailure(1 /* Expired */);
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should not record event with expired version."
+ );
+
+ // Recording call with event that has expiry_version set into the future.
+ Telemetry.recordEvent("telemetry.test", "not_expired_optout", "object1");
+ TelemetryTestUtils.assertNumberOfEvents(1);
+});
+
+add_task(async function test_invalidParams() {
+ Telemetry.clearEvents();
+
+ // Recording call with wrong type for value argument.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", 1);
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should not record event when value argument with invalid type is passed."
+ );
+ checkRecordingFailure(3 /* Value */);
+
+ // Recording call with wrong type for extra argument.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, "invalid");
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should not record event when extra argument with invalid type is passed."
+ );
+ checkRecordingFailure(4 /* Extra */);
+
+ // Recording call with unknown extra key.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {
+ key3: "x",
+ });
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should not record event when extra argument with invalid key is passed."
+ );
+ checkRecordingFailure(2 /* ExtraKey */);
+
+ // Recording call with invalid value type.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {
+ key3: 1,
+ });
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should not record event when extra argument with invalid value type is passed."
+ );
+ checkRecordingFailure(4 /* Extra */);
+});
+
+add_task(async function test_storageLimit() {
+ Telemetry.clearEvents();
+
+ let limitReached = TestUtils.topicObserved(
+ "event-telemetry-storage-limit-reached"
+ );
+ // Record more events than the storage limit allows.
+ let LIMIT = 1000;
+ let COUNT = LIMIT + 10;
+ for (let i = 0; i < COUNT; ++i) {
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", String(i));
+ }
+
+ await limitReached;
+ Assert.ok(true, "Topic was notified when event limit was reached");
+
+ // Check that the right events were recorded.
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ COUNT,
+ `Should have only recorded all ${COUNT} events`
+ );
+ Assert.ok(
+ events.every((e, idx) => e[4] === String(idx)),
+ "Should have recorded all events."
+ );
+});
+
+add_task(async function test_valueLimits() {
+ Telemetry.clearEvents();
+
+ // Record values that are at or over the limits for string lengths.
+ let LIMIT = 80;
+ let expected = [
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT - 10), null],
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT), null],
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 1), null],
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 10), null],
+
+ [
+ "telemetry.test",
+ "test1",
+ "object1",
+ null,
+ { key1: "a".repeat(LIMIT - 10) },
+ ],
+ ["telemetry.test", "test1", "object1", null, { key1: "a".repeat(LIMIT) }],
+ [
+ "telemetry.test",
+ "test1",
+ "object1",
+ null,
+ { key1: "a".repeat(LIMIT + 1) },
+ ],
+ [
+ "telemetry.test",
+ "test1",
+ "object1",
+ null,
+ { key1: "a".repeat(LIMIT + 10) },
+ ],
+ ];
+
+ for (let event of expected) {
+ Telemetry.recordEvent(...event);
+ if (event[3]) {
+ event[3] = event[3].substr(0, LIMIT);
+ } else {
+ event[3] = undefined;
+ }
+ if (event[4]) {
+ event[4].key1 = event[4].key1.substr(0, LIMIT);
+ }
+ }
+
+ // Strip off trailing null values to match the serialized events.
+ for (let e of expected) {
+ while (e.length >= 3 && e[e.length - 1] === null) {
+ e.pop();
+ }
+ }
+
+ // Check that the right events were recorded.
+ TelemetryTestUtils.assertEvents(expected);
+});
+
+add_task(async function test_unicodeValues() {
+ Telemetry.clearEvents();
+
+ // Record string values containing unicode characters.
+ let value = "漢語";
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", value);
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {
+ key1: value,
+ });
+
+ // Check that the values were correctly recorded.
+ TelemetryTestUtils.assertEvents([{ value }, { extra: { key1: value } }]);
+});
+
+add_task(async function test_dynamicEvents() {
+ Telemetry.clearEvents();
+ Telemetry.clearScalars();
+ Telemetry.canRecordExtended = true;
+
+ // Register some test events.
+ Telemetry.registerEvents("telemetry.test.dynamic", {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ // Event with extra_keys.
+ test2: {
+ methods: ["test2", "test2b"],
+ objects: ["object1"],
+ extra_keys: ["key1", "key2"],
+ },
+ // Expired event.
+ test3: {
+ methods: ["test3"],
+ objects: ["object1"],
+ expired: true,
+ },
+ // A release-channel recording event.
+ test4: {
+ methods: ["test4"],
+ objects: ["object1"],
+ record_on_release: true,
+ },
+ });
+
+ // Record some valid events.
+ Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1");
+ Telemetry.recordEvent("telemetry.test.dynamic", "test2", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ Telemetry.recordEvent("telemetry.test.dynamic", "test2b", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ Telemetry.recordEvent(
+ "telemetry.test.dynamic",
+ "test3",
+ "object1",
+ "some value"
+ );
+ Telemetry.recordEvent("telemetry.test.dynamic", "test4", "object1", null);
+
+ // Test recording an unknown event.
+ Telemetry.recordEvent("telemetry.test.dynamic", "unknown", "unknown");
+ checkRecordingFailure(0 /* UnknownEvent */);
+
+ // Now check that the snapshot contains the expected data.
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
+ Assert.ok(
+ "dynamic" in snapshot,
+ "Should have dynamic events in the snapshot."
+ );
+
+ let expected = [
+ ["telemetry.test.dynamic", "test1", "object1"],
+ [
+ "telemetry.test.dynamic",
+ "test2",
+ "object1",
+ null,
+ { key1: "foo", key2: "bar" },
+ ],
+ [
+ "telemetry.test.dynamic",
+ "test2b",
+ "object1",
+ null,
+ { key1: "foo", key2: "bar" },
+ ],
+ // "test3" is epxired, so it should not be recorded.
+ ["telemetry.test.dynamic", "test4", "object1"],
+ ];
+ let events = snapshot.dynamic;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+
+ // Check that we've summarized the recorded events
+ checkEventSummary(
+ expected.map(ev => ["dynamic", ev, 1]),
+ true
+ );
+
+ // Check that the opt-out snapshot contains only the one expected event.
+ snapshot = Telemetry.snapshotEvents(ALL_CHANNELS, false);
+ Assert.ok(
+ "dynamic" in snapshot,
+ "Should have dynamic events in the snapshot."
+ );
+ Assert.equal(
+ snapshot.dynamic.length,
+ 1,
+ "Should have one opt-out event in the snapshot."
+ );
+ expected = ["telemetry.test.dynamic", "test4", "object1"];
+ Assert.deepEqual(snapshot.dynamic[0].slice(1), expected);
+
+ // Recording with unknown extra keys should be ignored and print an error.
+ Telemetry.clearEvents();
+ Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1", null, {
+ key1: "foo",
+ });
+ Telemetry.recordEvent("telemetry.test.dynamic", "test2", "object1", null, {
+ key1: "foo",
+ unknown: "bar",
+ });
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.ok(
+ !("dynamic" in snapshot),
+ "Should have not recorded dynamic events with unknown extra keys."
+ );
+
+ // Other built-in events should not show up in the "dynamic" bucket of the snapshot.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.ok(
+ !("dynamic" in snapshot),
+ "Should have not recorded built-in event into dynamic bucket."
+ );
+
+ // Test that recording opt-in and opt-out events works as expected.
+ Telemetry.clearEvents();
+ Telemetry.canRecordExtended = false;
+
+ Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1");
+ Telemetry.recordEvent("telemetry.test.dynamic", "test4", "object1");
+
+ expected = [
+ // Only "test4" should have been recorded.
+ ["telemetry.test.dynamic", "test4", "object1"],
+ ];
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ 1,
+ "Should have one opt-out event in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+});
+
+add_task(async function test_dynamicEventRegistrationValidation() {
+ Telemetry.canRecordExtended = true;
+ Telemetry.clearEvents();
+
+ // Test registration of invalid categories.
+ Telemetry.getSnapshotForHistograms("main", true); // Clear histograms before we begin.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry+test+dynamic", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ }),
+ /Category parameter should match the identifier pattern\./,
+ "Should throw when registering category names with invalid characters."
+ );
+ checkRegistrationFailure(2 /* Category */);
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents(
+ "telemetry.test.test.test.test.test.test.test.test",
+ {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ }
+ ),
+ /Category parameter should match the identifier pattern\./,
+ "Should throw when registering overly long category names."
+ );
+ checkRegistrationFailure(2 /* Category */);
+
+ // Test registration of invalid event names.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic1", {
+ "test?1": {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ }),
+ /Event names should match the identifier pattern\./,
+ "Should throw when registering event names with invalid characters."
+ );
+ checkRegistrationFailure(1 /* Name */);
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic2", {
+ test1test1test1test1test1test1test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ }),
+ /Event names should match the identifier pattern\./,
+ "Should throw when registering overly long event names."
+ );
+ checkRegistrationFailure(1 /* Name */);
+
+ // Test registration of invalid method names.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic3", {
+ test1: {
+ methods: ["test?1"],
+ objects: ["object1"],
+ },
+ }),
+ /Method names should match the identifier pattern\./,
+ "Should throw when registering method names with invalid characters."
+ );
+ checkRegistrationFailure(3 /* Method */);
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic", {
+ test1: {
+ methods: ["test1test1test1test1test1test1test1"],
+ objects: ["object1"],
+ },
+ }),
+ /Method names should match the identifier pattern\./,
+ "Should throw when registering overly long method names."
+ );
+ checkRegistrationFailure(3 /* Method */);
+
+ // Test registration of invalid object names.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic4", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object?1"],
+ },
+ }),
+ /Object names should match the identifier pattern\./,
+ "Should throw when registering object names with invalid characters."
+ );
+ checkRegistrationFailure(4 /* Object */);
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic5", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1object1object1object1object1object1"],
+ },
+ }),
+ /Object names should match the identifier pattern\./,
+ "Should throw when registering overly long object names."
+ );
+ checkRegistrationFailure(4 /* Object */);
+
+ // Test validation of invalid key names.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic6", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ extra_keys: ["a?1"],
+ },
+ }),
+ /Extra key names should match the identifier pattern\./,
+ "Should throw when registering extra key names with invalid characters."
+ );
+ checkRegistrationFailure(5 /* ExtraKeys */);
+
+ // Test validation of key names that are too long - we allow a maximum of 15 characters.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic7", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ extra_keys: ["a012345678901234"],
+ },
+ }),
+ /Extra key names should match the identifier pattern\./,
+ "Should throw when registering extra key names which are too long."
+ );
+ checkRegistrationFailure(5 /* ExtraKeys */);
+ Telemetry.registerEvents("telemetry.test.dynamic8", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ extra_keys: ["a01234567890123"],
+ },
+ });
+
+ // Test validation of extra key count - we only allow 10.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic9", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ extra_keys: [
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "a6",
+ "a7",
+ "a8",
+ "a9",
+ "a10",
+ "a11",
+ ],
+ },
+ }),
+ /No more than 10 extra keys can be registered\./,
+ "Should throw when registering too many extra keys."
+ );
+ checkRegistrationFailure(5 /* ExtraKeys */);
+ Telemetry.registerEvents("telemetry.test.dynamic10", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ extra_keys: ["a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10"],
+ },
+ });
+});
+
+// When add-ons update, they may re-register some of the dynamic events.
+// Test through some possible scenarios.
+add_task(async function test_dynamicEventRegisterAgain() {
+ Telemetry.canRecordExtended = true;
+ Telemetry.clearEvents();
+
+ const category = "telemetry.test.register.again";
+ let events = {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ };
+
+ // First register the initial event and make sure it can be recorded.
+ Telemetry.registerEvents(category, events);
+ let expected = [[category, "test1", "object1"]];
+ expected.forEach(e => Telemetry.recordEvent(...e));
+
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ expected.length,
+ "Should have right number of events in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+
+ // Register the same event again and make sure it can still be recorded.
+ Telemetry.registerEvents(category, events);
+ Telemetry.recordEvent(category, "test1", "object1");
+
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ expected.length,
+ "Should have right number of events in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+
+ // Now register another event in the same category and make sure both events can be recorded.
+ events.test2 = {
+ methods: ["test2"],
+ objects: ["object2"],
+ };
+ Telemetry.registerEvents(category, events);
+
+ expected = [
+ [category, "test1", "object1"],
+ [category, "test2", "object2"],
+ ];
+ expected.forEach(e => Telemetry.recordEvent(...e));
+
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ expected.length,
+ "Should have right number of events in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+
+ // Check that adding a new object to an event entry works.
+ events.test1.methods = ["test1a"];
+ events.test2.objects = ["object2", "object2a"];
+ Telemetry.registerEvents(category, events);
+
+ expected = [
+ [category, "test1", "object1"],
+ [category, "test2", "object2"],
+ [category, "test1a", "object1"],
+ [category, "test2", "object2a"],
+ ];
+ expected.forEach(e => Telemetry.recordEvent(...e));
+
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ expected.length,
+ "Should have right number of events in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+
+ // Make sure that we can expire events that are already registered.
+ events.test2.expired = true;
+ Telemetry.registerEvents(category, events);
+
+ expected = [[category, "test1", "object1"]];
+ expected.forEach(e => Telemetry.recordEvent(...e));
+
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ expected.length,
+ "Should have right number of events in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+});
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_productSpecificEvents() {
+ const EVENT_CATEGORY = "telemetry.test";
+ const DEFAULT_PRODUCTS_EVENT = "default_products";
+ const DESKTOP_ONLY_EVENT = "desktop_only";
+ const MULTIPRODUCT_EVENT = "multiproduct";
+ const MOBILE_ONLY_EVENT = "mobile_only";
+
+ Telemetry.clearEvents();
+
+ // Try to record the desktop and multiproduct event
+ Telemetry.recordEvent(EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1");
+ Telemetry.recordEvent(EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1");
+ Telemetry.recordEvent(EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1");
+
+ // Try to record the mobile-only event
+ Telemetry.recordEvent(EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1");
+
+ let events = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true).parent;
+
+ let expected = [
+ [EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1"],
+ [EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1"],
+ [EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1"],
+ ];
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+ }
+);
+
+add_task(
+ {
+ skip_if: () => !gIsAndroid,
+ },
+ async function test_mobileSpecificEvents() {
+ const EVENT_CATEGORY = "telemetry.test";
+ const DEFAULT_PRODUCTS_EVENT = "default_products";
+ const DESKTOP_ONLY_EVENT = "desktop_only";
+ const MULTIPRODUCT_EVENT = "multiproduct";
+ const MOBILE_ONLY_EVENT = "mobile_only";
+
+ Telemetry.clearEvents();
+
+ // Try to record the mobile-only and multiproduct event
+ Telemetry.recordEvent(EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1");
+ Telemetry.recordEvent(EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1");
+ Telemetry.recordEvent(EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1");
+
+ // Try to record the mobile-only event
+ Telemetry.recordEvent(EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1");
+
+ let events = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true).parent;
+
+ let expected = [
+ [EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1"],
+ [EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1"],
+ [EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1"],
+ ];
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+ }
+);
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEvents_buildFaster.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents_buildFaster.js
new file mode 100644
index 0000000000..c7e9e5aaba
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents_buildFaster.js
@@ -0,0 +1,463 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+/**
+ * Return the path to the definitions file for the events.
+ */
+function getDefinitionsPath() {
+ // Write the event definition to the spec file in the binary directory.
+ let definitionFile = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ definitionFile = Services.dirsvc.get("GreD", Ci.nsIFile);
+ definitionFile.append("EventArtifactDefinitions.json");
+ return definitionFile.path;
+}
+
+add_task(async function test_setup() {
+ do_get_profile();
+});
+
+add_task(
+ {
+ // The test needs to write a file, and that fails in tests on Android.
+ // We don't really need the Android coverage, so skip on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_invalidJSON() {
+ const INVALID_JSON = "{ invalid,JSON { {1}";
+ const FILE_PATH = getDefinitionsPath();
+
+ // Write a corrupted JSON file.
+ await IOUtils.writeUTF8(FILE_PATH, INVALID_JSON, {
+ mode: "overwrite",
+ });
+
+ // Simulate Firefox startup. This should not throw!
+ await TelemetryController.testSetup();
+ await TelemetryController.testPromiseJsProbeRegistration();
+
+ // Cleanup.
+ await TelemetryController.testShutdown();
+ await IOUtils.remove(FILE_PATH);
+ }
+);
+
+add_task(
+ {
+ // The test needs to write a file, and that fails in tests on Android.
+ // We don't really need the Android coverage, so skip on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_dynamicBuiltin() {
+ const DYNAMIC_EVENT_SPEC = {
+ "telemetry.test.builtin": {
+ test: {
+ objects: ["object1", "object2"],
+ expires: "never",
+ methods: ["test1", "test2"],
+ extra_keys: ["key2", "key1"],
+ record_on_release: false,
+ },
+ },
+ // Test a new, expired event
+ "telemetry.test.expired": {
+ expired: {
+ objects: ["object1"],
+ methods: ["method1"],
+ expires: AppConstants.MOZ_APP_VERSION,
+ record_on_release: false,
+ },
+ },
+ // Test overwriting static expiries
+ "telemetry.test": {
+ expired_version: {
+ objects: ["object1"],
+ methods: ["expired_version"],
+ expires: "never",
+ record_on_release: false,
+ },
+ not_expired_optout: {
+ objects: ["object1"],
+ methods: ["not_expired_optout"],
+ expires: AppConstants.MOZ_APP_VERSION,
+ record_on_release: true,
+ },
+ },
+ };
+
+ Telemetry.clearEvents();
+
+ // Let's write to the definition file to also cover the file
+ // loading part.
+ const FILE_PATH = getDefinitionsPath();
+ await IOUtils.writeJSON(FILE_PATH, DYNAMIC_EVENT_SPEC);
+
+ // Start TelemetryController to trigger loading the specs.
+ await TelemetryController.testReset();
+ await TelemetryController.testPromiseJsProbeRegistration();
+
+ // Record the events
+ const TEST_EVENT_NAME = "telemetry.test.builtin";
+ const DYNAMIC_EVENT_CATEGORY = "telemetry.test.expired";
+ const STATIC_EVENT_CATEGORY = "telemetry.test";
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+ Telemetry.setEventRecordingEnabled(DYNAMIC_EVENT_CATEGORY, true);
+ Telemetry.setEventRecordingEnabled(STATIC_EVENT_CATEGORY, true);
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object2", null, {
+ key2: "bar",
+ });
+ Telemetry.recordEvent(DYNAMIC_EVENT_CATEGORY, "method1", "object1");
+ Telemetry.recordEvent(STATIC_EVENT_CATEGORY, "expired_version", "object1");
+ Telemetry.recordEvent(
+ STATIC_EVENT_CATEGORY,
+ "not_expired_optout",
+ "object1"
+ );
+
+ // Check the values we tried to store.
+ const snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok(
+ "parent" in snapshot,
+ "Should have parent events in the snapshot."
+ );
+
+ let expected = [
+ [TEST_EVENT_NAME, "test1", "object1"],
+ [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
+ [TEST_EVENT_NAME, "test2", "object2", null, { key2: "bar" }],
+ [STATIC_EVENT_CATEGORY, "expired_version", "object1"],
+ ];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+
+ // Clean up.
+ await TelemetryController.testShutdown();
+ await IOUtils.remove(FILE_PATH);
+ }
+);
+
+add_task(async function test_dynamicBuiltinEvents() {
+ Telemetry.clearEvents();
+ Telemetry.clearScalars();
+ Telemetry.canRecordExtended = true;
+
+ const TEST_EVENT_NAME = "telemetry.test.dynamicbuiltin";
+
+ // Register some dynamic builtin test events.
+ Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ // Event with extra_keys.
+ test2: {
+ methods: ["test2", "test2b"],
+ objects: ["object1", "object2"],
+ extra_keys: ["key1", "key2"],
+ },
+ });
+
+ // Record some events.
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2b", "object2", null, {
+ key2: "bar",
+ });
+ // Now check that the snapshot contains the expected data.
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+
+ // For checking event summaries
+ const scalars = Telemetry.getSnapshotForKeyedScalars("main", true);
+ Assert.ok(
+ "parent" in scalars,
+ "Should have parent scalars in the main snapshot."
+ );
+
+ let expected = [
+ [TEST_EVENT_NAME, "test1", "object1"],
+ [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
+ [TEST_EVENT_NAME, "test2b", "object2", null, { key2: "bar" }],
+ ];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+
+ const uniqueEventName = `${expected[i][0]}#${expected[i][1]}#${expected[i][2]}`;
+ const summaryCount =
+ scalars.parent["telemetry.event_counts"][uniqueEventName];
+ Assert.equal(1, summaryCount, `${uniqueEventName} had wrong summary count`);
+ }
+});
+
+add_task(async function test_dynamicBuiltinEventsDisabledByDefault() {
+ Telemetry.clearEvents();
+ Telemetry.canRecordExtended = true;
+
+ const TEST_EVENT_NAME = "telemetry.test.offbydefault";
+
+ // Register some dynamic builtin test events.
+ Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ });
+
+ // Record some events.
+ // Explicitely _don't_ enable the category
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+
+ // Now check that the snapshot contains the expected data.
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok(
+ !("parent" in snapshot),
+ "Should not have parent events in the snapshot."
+ );
+
+ // Now enable the category and record again
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+
+ snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+
+ let expected = [[TEST_EVENT_NAME, "test1", "object1"]];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+});
+
+add_task(async function test_dynamicBuiltinDontOverwriteStaticData() {
+ Telemetry.clearEvents();
+ Telemetry.canRecordExtended = true;
+
+ const TEST_STATIC_EVENT_NAME = "telemetry.test";
+ const TEST_EVENT_NAME = "telemetry.test.nooverwrite";
+
+ // Register some dynamic builtin test events.
+ Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
+ dynamic: {
+ methods: ["dynamic"],
+ objects: ["builtin", "anotherone"],
+ },
+ });
+
+ // First enable the categories we're using
+ Telemetry.setEventRecordingEnabled(TEST_STATIC_EVENT_NAME, true);
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+
+ // Now record some dynamic-builtin and static events
+ Telemetry.recordEvent(TEST_EVENT_NAME, "dynamic", "builtin");
+ Telemetry.recordEvent(TEST_STATIC_EVENT_NAME, "test1", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "dynamic", "anotherone");
+
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+
+ // All events should now be recorded in the right order
+ let expected = [
+ [TEST_EVENT_NAME, "dynamic", "builtin"],
+ [TEST_STATIC_EVENT_NAME, "test1", "object1"],
+ [TEST_EVENT_NAME, "dynamic", "anotherone"],
+ ];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+});
+
+add_task(async function test_dynamicBuiltinEventsOverridingStatic() {
+ Telemetry.clearEvents();
+ Telemetry.canRecordExtended = true;
+
+ const TEST_EVENT_NAME = "telemetry.test";
+
+ // Register dynamic builtin test events, overwriting existing one.
+ Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1", "object2"],
+ },
+ // Event with extra_keys.
+ test2: {
+ methods: ["test2"],
+ objects: ["object1", "object2", "object3"],
+ extra_keys: ["key1", "key2", "newdynamickey"],
+ },
+ });
+
+ // Record some events that should be available in the static event already .
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ // Record events with newly added objects and keys.
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object2", null, {
+ newdynamickey: "foo",
+ });
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object3", null, {
+ key1: "foo",
+ });
+ // Now check that the snapshot contains the expected data.
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+
+ let expected = [
+ [TEST_EVENT_NAME, "test1", "object1"],
+ [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
+ [TEST_EVENT_NAME, "test2", "object2", null, { newdynamickey: "foo" }],
+ [TEST_EVENT_NAME, "test2", "object3", null, { key1: "foo" }],
+ ];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+});
+
+add_task(async function test_realDynamicDontOverwrite() {
+ // Real dynamic events follow similar code paths internally.
+ // Let's ensure they trigger the right code path and don't overwrite.
+
+ Telemetry.clearEvents();
+ Telemetry.canRecordExtended = true;
+
+ const TEST_EVENT_NAME = "telemetry.test";
+
+ // Register dynamic test events, this should not overwrite existing ones.
+ Telemetry.registerEvents(TEST_EVENT_NAME, {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1", "object2"],
+ },
+ // Event with extra_keys.
+ test2: {
+ methods: ["test2"],
+ objects: ["object1", "object2", "object3"],
+ extra_keys: ["key1", "key2", "realdynamic"],
+ },
+ });
+
+ // Record some events that should be available in the static event already .
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ // Record events with newly added objects and keys.
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object2", null, {
+ realdynamic: "foo",
+ });
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object3", null, {
+ key1: "foo",
+ });
+ // Now check that the snapshot contains the expected data.
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+
+ let expected = [
+ [TEST_EVENT_NAME, "test1", "object1"],
+ [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
+ [TEST_EVENT_NAME, "test2", "object3", null, { key1: "foo" }],
+ ];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js b/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js
new file mode 100644
index 0000000000..29ea4c0a1e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ let testFlag = Services.telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
+ deepEqual(
+ testFlag.snapshot().values,
+ { 0: 1, 1: 0 },
+ "Original value is correct"
+ );
+ testFlag.add(1);
+ deepEqual(
+ testFlag.snapshot().values,
+ { 0: 0, 1: 1, 2: 0 },
+ "Value is correct after ping"
+ );
+ testFlag.clear();
+ deepEqual(
+ testFlag.snapshot().values,
+ { 0: 1, 1: 0 },
+ "Value is correct after calling clear()"
+ );
+ testFlag.add(1);
+ deepEqual(
+ testFlag.snapshot().values,
+ { 0: 0, 1: 1, 2: 0 },
+ "Value is correct after ping"
+ );
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryHistograms.js b/toolkit/components/telemetry/tests/unit/test_TelemetryHistograms.js
new file mode 100644
index 0000000000..1ac0c76351
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryHistograms.js
@@ -0,0 +1,2073 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const INT_MAX = 0x7fffffff;
+
+// Return an array of numbers from lower up to, excluding, upper
+function numberRange(lower, upper) {
+ let a = [];
+ for (let i = lower; i < upper; ++i) {
+ a.push(i);
+ }
+ return a;
+}
+
+function check_histogram(histogram_type, name, min, max, bucket_count) {
+ var h = Telemetry.getHistogramById(name);
+ h.add(0);
+ var s = h.snapshot();
+ Assert.equal(0, s.sum);
+
+ var hgrams = Telemetry.getSnapshotForHistograms("main", false).parent;
+ let gh = hgrams[name];
+ Assert.equal(gh.histogram_type, histogram_type);
+
+ Assert.deepEqual(gh.range, [min, max]);
+
+ // Check that booleans work with nonboolean histograms
+ h.add(false);
+ h.add(true);
+ s = Object.values(h.snapshot().values);
+ Assert.deepEqual(s, [2, 1, 0]);
+
+ // Check that clearing works.
+ h.clear();
+ s = h.snapshot();
+ Assert.deepEqual(s.values, {});
+ Assert.equal(s.sum, 0);
+
+ h.add(0);
+ h.add(1);
+ var c = Object.values(h.snapshot().values);
+ Assert.deepEqual(c, [1, 1, 0]);
+}
+
+// This MUST be the very first test of this file.
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ function test_instantiate() {
+ const ID = "TELEMETRY_TEST_COUNT";
+ let h = Telemetry.getHistogramById(ID);
+
+ // Instantiate the subsession histogram through |add| and make sure they match.
+ // This MUST be the first use of "TELEMETRY_TEST_COUNT" in this file, otherwise
+ // |add| will not instantiate the histogram.
+ h.add(1);
+ let snapshot = h.snapshot();
+ let subsession = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(ID in subsession);
+ Assert.equal(
+ snapshot.sum,
+ subsession[ID].sum,
+ "Histogram and subsession histogram sum must match."
+ );
+ // Clear the histogram, so we don't void the assumptions from the other tests.
+ h.clear();
+ }
+);
+
+add_task(async function test_parameterChecks() {
+ let kinds = [Telemetry.HISTOGRAM_EXPONENTIAL, Telemetry.HISTOGRAM_LINEAR];
+ let testNames = ["TELEMETRY_TEST_EXPONENTIAL", "TELEMETRY_TEST_LINEAR"];
+ for (let i = 0; i < kinds.length; i++) {
+ let histogram_type = kinds[i];
+ let test_type = testNames[i];
+ let [min, max, bucket_count] = [1, INT_MAX - 1, 10];
+ check_histogram(histogram_type, test_type, min, max, bucket_count);
+ }
+});
+
+add_task(async function test_parameterCounts() {
+ let histogramIds = [
+ "TELEMETRY_TEST_EXPONENTIAL",
+ "TELEMETRY_TEST_LINEAR",
+ "TELEMETRY_TEST_FLAG",
+ "TELEMETRY_TEST_CATEGORICAL",
+ "TELEMETRY_TEST_BOOLEAN",
+ ];
+
+ for (let id of histogramIds) {
+ let h = Telemetry.getHistogramById(id);
+ h.clear();
+ h.add();
+ Assert.equal(
+ h.snapshot().sum,
+ 0,
+ "Calling add() without a value should only log an error."
+ );
+ h.clear();
+ }
+});
+
+add_task(async function test_parameterCountsKeyed() {
+ let histogramIds = [
+ "TELEMETRY_TEST_KEYED_FLAG",
+ "TELEMETRY_TEST_KEYED_BOOLEAN",
+ "TELEMETRY_TEST_KEYED_EXPONENTIAL",
+ "TELEMETRY_TEST_KEYED_LINEAR",
+ ];
+
+ for (let id of histogramIds) {
+ let h = Telemetry.getKeyedHistogramById(id);
+ h.clear();
+ h.add("key");
+ Assert.deepEqual(
+ h.snapshot(),
+ {},
+ "Calling add('key') without a value should only log an error."
+ );
+ h.clear();
+ }
+});
+
+add_task(async function test_noSerialization() {
+ // Instantiate the storage for this histogram and make sure it doesn't
+ // get reflected into JS, as it has no interesting data in it.
+ Telemetry.getHistogramById("NEWTAB_PAGE_PINNED_SITES_COUNT");
+ let histograms = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.equal(false, "NEWTAB_PAGE_PINNED_SITES_COUNT" in histograms);
+});
+
+add_task(async function test_boolean_histogram() {
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN");
+ var r = h.snapshot().range;
+ // boolean histograms ignore numeric parameters
+ Assert.deepEqual(r, [1, 2]);
+ h.add(0);
+ h.add(1);
+ h.add(2);
+
+ h.add(true);
+ h.add(false);
+ var s = h.snapshot();
+ Assert.equal(s.histogram_type, Telemetry.HISTOGRAM_BOOLEAN);
+ // last bucket should always be 0 since .add parameters are normalized to either 0 or 1
+ Assert.deepEqual(s.values, { 0: 2, 1: 3, 2: 0 });
+ Assert.equal(s.sum, 3);
+});
+
+add_task(async function test_flag_histogram() {
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
+ var r = h.snapshot().range;
+ // Flag histograms ignore numeric parameters.
+ Assert.deepEqual(r, [1, 2]);
+ // Should already have a 0 counted.
+ var v = h.snapshot().values;
+ var s = h.snapshot().sum;
+ Assert.deepEqual(v, { 0: 1, 1: 0 });
+ Assert.equal(s, 0);
+ // Should switch counts.
+ h.add(1);
+ var v2 = h.snapshot().values;
+ var s2 = h.snapshot().sum;
+ Assert.deepEqual(v2, { 0: 0, 1: 1, 2: 0 });
+ Assert.equal(s2, 1);
+ // Should only switch counts once.
+ h.add(1);
+ var v3 = h.snapshot().values;
+ var s3 = h.snapshot().sum;
+ Assert.deepEqual(v3, { 0: 0, 1: 1, 2: 0 });
+ Assert.equal(s3, 1);
+ Assert.equal(h.snapshot().histogram_type, Telemetry.HISTOGRAM_FLAG);
+});
+
+add_task(async function test_count_histogram() {
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT2");
+ let s = h.snapshot();
+ Assert.deepEqual(s.range, [1, 2]);
+ Assert.deepEqual(s.values, {});
+ Assert.equal(s.sum, 0);
+ h.add();
+ s = h.snapshot();
+ Assert.deepEqual(s.values, { 0: 1, 1: 0 });
+ Assert.equal(s.sum, 1);
+ h.add();
+ s = h.snapshot();
+ Assert.deepEqual(s.values, { 0: 2, 1: 0 });
+ Assert.equal(s.sum, 2);
+});
+
+add_task(async function test_categorical_histogram() {
+ let h1 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL");
+ for (let v of ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1]) {
+ h1.add(v);
+ }
+ for (let s of ["", "Label4", "1234"]) {
+ // The |add| method should not throw for unexpected values, but rather
+ // print an error message in the console.
+ h1.add(s);
+ }
+
+ let snapshot = h1.snapshot();
+ Assert.equal(snapshot.sum, 6);
+ Assert.deepEqual(snapshot.range, [1, 50]);
+ Assert.deepEqual(snapshot.values, { 0: 3, 1: 2, 2: 2, 3: 0 });
+
+ let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL_OPTOUT");
+ for (let v of [
+ "CommonLabel",
+ "CommonLabel",
+ "Label4",
+ "Label5",
+ "Label6",
+ 0,
+ 1,
+ ]) {
+ h2.add(v);
+ }
+ for (let s of ["", "Label3", "1234"]) {
+ // The |add| method should not throw for unexpected values, but rather
+ // print an error message in the console.
+ h2.add(s);
+ }
+
+ snapshot = h2.snapshot();
+ Assert.equal(snapshot.sum, 7);
+ Assert.deepEqual(snapshot.range, [1, 50]);
+ Assert.deepEqual(snapshot.values, { 0: 3, 1: 2, 2: 1, 3: 1, 4: 0 });
+
+ // This histogram overrides the default of 50 values to 70.
+ let h3 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL_NVALUES");
+ for (let v of ["CommonLabel", "Label7", "Label8"]) {
+ h3.add(v);
+ }
+
+ snapshot = h3.snapshot();
+ Assert.equal(snapshot.sum, 3);
+ Assert.deepEqual(snapshot.range, [1, 70]);
+ Assert.deepEqual(snapshot.values, { 0: 1, 1: 1, 2: 1, 3: 0 });
+});
+
+add_task(async function test_getCategoricalLabels() {
+ let h = Telemetry.getCategoricalLabels();
+
+ Assert.deepEqual(h.TELEMETRY_TEST_CATEGORICAL, [
+ "CommonLabel",
+ "Label2",
+ "Label3",
+ ]);
+ Assert.deepEqual(h.TELEMETRY_TEST_CATEGORICAL_OPTOUT, [
+ "CommonLabel",
+ "Label4",
+ "Label5",
+ "Label6",
+ ]);
+ Assert.deepEqual(h.TELEMETRY_TEST_CATEGORICAL_NVALUES, [
+ "CommonLabel",
+ "Label7",
+ "Label8",
+ ]);
+ Assert.deepEqual(h.TELEMETRY_TEST_KEYED_CATEGORICAL, [
+ "CommonLabel",
+ "Label2",
+ "Label3",
+ ]);
+});
+
+add_task(async function test_add_error_behaviour() {
+ const PLAIN_HISTOGRAMS_TO_TEST = [
+ "TELEMETRY_TEST_FLAG",
+ "TELEMETRY_TEST_EXPONENTIAL",
+ "TELEMETRY_TEST_LINEAR",
+ "TELEMETRY_TEST_BOOLEAN",
+ ];
+
+ const KEYED_HISTOGRAMS_TO_TEST = [
+ "TELEMETRY_TEST_KEYED_FLAG",
+ "TELEMETRY_TEST_KEYED_COUNT",
+ "TELEMETRY_TEST_KEYED_BOOLEAN",
+ ];
+
+ // Check that |add| doesn't throw for plain histograms.
+ for (let hist of PLAIN_HISTOGRAMS_TO_TEST) {
+ const returnValue =
+ Telemetry.getHistogramById(hist).add("unexpected-value");
+ Assert.strictEqual(
+ returnValue,
+ undefined,
+ "Adding to an histogram must return 'undefined'."
+ );
+ }
+
+ // And for keyed histograms.
+ for (let hist of KEYED_HISTOGRAMS_TO_TEST) {
+ const returnValue = Telemetry.getKeyedHistogramById(hist).add(
+ "some-key",
+ "unexpected-value"
+ );
+ Assert.strictEqual(
+ returnValue,
+ undefined,
+ "Adding to a keyed histogram must return 'undefined'."
+ );
+ }
+});
+
+add_task(async function test_API_return_values() {
+ // Check that the plain scalar functions don't allow to crash the browser.
+ // We expect 'undefined' to be returned so that .add(1).add() can't be called.
+ // See bug 1321349 for context.
+ let hist = Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR");
+ let keyedHist = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
+
+ const RETURN_VALUES = [
+ hist.clear(),
+ hist.add(1),
+ keyedHist.clear(),
+ keyedHist.add("some-key", 1),
+ ];
+
+ for (let returnValue of RETURN_VALUES) {
+ Assert.strictEqual(
+ returnValue,
+ undefined,
+ "The function must return undefined"
+ );
+ }
+});
+
+add_task(async function test_getHistogramById() {
+ try {
+ Telemetry.getHistogramById("nonexistent");
+ do_throw("This can't happen");
+ } catch (e) {}
+ var h = Telemetry.getHistogramById("CYCLE_COLLECTOR");
+ var s = h.snapshot();
+ Assert.equal(s.histogram_type, Telemetry.HISTOGRAM_EXPONENTIAL);
+ Assert.deepEqual(s.range, [1, 10000]);
+});
+
+add_task(async function test_getSlowSQL() {
+ var slow = Telemetry.slowSQL;
+ Assert.ok("mainThread" in slow && "otherThreads" in slow);
+});
+
+// Check that telemetry doesn't record in private mode
+add_task(async function test_privateMode() {
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN");
+ var orig = h.snapshot();
+ Telemetry.canRecordExtended = false;
+ h.add(1);
+ Assert.deepEqual(orig, h.snapshot());
+ Telemetry.canRecordExtended = true;
+ h.add(1);
+ Assert.notDeepEqual(orig, h.snapshot());
+});
+
+// Check that telemetry records only when it is suppose to.
+add_task(async function test_histogramRecording() {
+ // Check that no histogram is recorded if both base and extended recording are off.
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+ h.clear();
+ let orig = h.snapshot();
+ h.add(1);
+ Assert.equal(orig.sum, h.snapshot().sum);
+
+ // Check that only base histograms are recorded.
+ Telemetry.canRecordBase = true;
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "Histogram value should have incremented by 1 due to recording."
+ );
+
+ // Extended histograms should not be recorded.
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN");
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(
+ orig.sum,
+ h.snapshot().sum,
+ "Histograms should be equal after recording."
+ );
+
+ // Runtime created histograms should not be recorded.
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN");
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(
+ orig.sum,
+ h.snapshot().sum,
+ "Histograms should be equal after recording."
+ );
+
+ // Check that extended histograms are recorded when required.
+ Telemetry.canRecordExtended = true;
+
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "Runtime histogram value should have incremented by 1 due to recording."
+ );
+
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN");
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "Histogram value should have incremented by 1 due to recording."
+ );
+
+ // Check that base histograms are still being recorded.
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+ h.clear();
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "Histogram value should have incremented by 1 due to recording."
+ );
+});
+
+add_task(async function test_expired_histogram() {
+ var test_expired_id = "TELEMETRY_TEST_EXPIRED";
+ var dummy = Telemetry.getHistogramById(test_expired_id);
+
+ dummy.add(1);
+
+ for (let process of ["main", "content", "gpu", "extension"]) {
+ let histograms = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ );
+ if (!(process in histograms)) {
+ info("Nothing present for process " + process);
+ continue;
+ }
+ Assert.equal(histograms[process].__expired__, undefined);
+ }
+ let parentHgrams = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.equal(parentHgrams[test_expired_id], undefined);
+});
+
+add_task(async function test_keyed_expired_histogram() {
+ var test_expired_id = "TELEMETRY_TEST_EXPIRED_KEYED";
+ var dummy = Telemetry.getKeyedHistogramById(test_expired_id);
+ dummy.add("someKey", 1);
+
+ const histograms = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ );
+ for (let process of ["parent", "content", "gpu", "extension"]) {
+ if (!(process in histograms)) {
+ info("Nothing present for process " + process);
+ continue;
+ }
+ Assert.ok(
+ !(test_expired_id in histograms[process]),
+ "The expired keyed histogram must not be reported"
+ );
+ }
+});
+
+add_task(async function test_keyed_histogram() {
+ // Check that invalid names get rejected.
+
+ let threw = false;
+ try {
+ Telemetry.getKeyedHistogramById(
+ "test::unknown histogram",
+ "never",
+ Telemetry.HISTOGRAM_BOOLEAN
+ );
+ } catch (e) {
+ // This should throw as it is an unknown ID
+ threw = true;
+ }
+ Assert.ok(threw, "getKeyedHistogramById should have thrown");
+});
+
+add_task(async function test_keyed_boolean_histogram() {
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_BOOLEAN";
+ let KEYS = numberRange(0, 2).map(i => "key" + (i + 1));
+ KEYS.push("漢語");
+ let histogramBase = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 2,
+ sum: 1,
+ values: { 0: 0, 1: 1, 2: 0 },
+ };
+ let testHistograms = numberRange(0, 3).map(i =>
+ JSON.parse(JSON.stringify(histogramBase))
+ );
+ let testKeys = [];
+ let testSnapShot = {};
+
+ let h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ for (let i = 0; i < 2; ++i) {
+ let key = KEYS[i];
+ h.add(key, true);
+ testSnapShot[key] = testHistograms[i];
+ testKeys.push(key);
+
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+ }
+
+ h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let key = KEYS[2];
+ h.add(key, false);
+ testKeys.push(key);
+ testSnapShot[key] = testHistograms[2];
+ testSnapShot[key].sum = 0;
+ testSnapShot[key].values = { 0: 1, 1: 0 };
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let parentHgrams = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.deepEqual(parentHgrams[KEYED_ID], testSnapShot);
+
+ h.clear();
+ Assert.deepEqual(h.keys(), []);
+ Assert.deepEqual(h.snapshot(), {});
+});
+
+add_task(async function test_keyed_count_histogram() {
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const KEYS = numberRange(0, 5).map(i => "key" + (i + 1));
+ let histogramBase = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ sum: 0,
+ values: { 0: 1, 1: 0 },
+ };
+ let testHistograms = numberRange(0, 5).map(i =>
+ JSON.parse(JSON.stringify(histogramBase))
+ );
+ let testKeys = [];
+ let testSnapShot = {};
+
+ let h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ h.clear();
+ for (let i = 0; i < 4; ++i) {
+ let key = KEYS[i];
+ let value = i * 2 + 1;
+
+ for (let k = 0; k < value; ++k) {
+ h.add(key);
+ }
+ testHistograms[i].values[0] = value;
+ testHistograms[i].sum = value;
+ testSnapShot[key] = testHistograms[i];
+ testKeys.push(key);
+
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot()[key], testHistograms[i]);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+ }
+
+ h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let key = KEYS[4];
+ h.add(key);
+ testKeys.push(key);
+ testHistograms[4].values[0] = 1;
+ testHistograms[4].sum = 1;
+ testSnapShot[key] = testHistograms[4];
+
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let parentHgrams = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.deepEqual(parentHgrams[KEYED_ID], testSnapShot);
+
+ // Test clearing categorical histogram.
+ h.clear();
+ Assert.deepEqual(h.keys(), []);
+ Assert.deepEqual(h.snapshot(), {});
+
+ // Test leaving out the value argument. That should increment by 1.
+ h.add("key");
+ Assert.equal(h.snapshot().key.sum, 1);
+});
+
+add_task(async function test_keyed_categorical_histogram() {
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_CATEGORICAL";
+ const KEYS = numberRange(0, 5).map(i => "key" + (i + 1));
+
+ let h = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ for (let k of KEYS) {
+ // Test adding both per label and index.
+ for (let v of ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1]) {
+ h.add(k, v);
+ }
+
+ // The |add| method should not throw for unexpected values, but rather
+ // print an error message in the console.
+ for (let s of ["", "Label4", "1234"]) {
+ h.add(k, s);
+ }
+ }
+
+ // Check that the set of keys in the snapshot is what we expect.
+ let snapshot = h.snapshot();
+ let snapshotKeys = Object.keys(snapshot);
+ Assert.equal(KEYS.length, snapshotKeys.length);
+ Assert.ok(KEYS.every(k => snapshotKeys.includes(k)));
+
+ // Check the snapshot values.
+ for (let k of KEYS) {
+ Assert.ok(k in snapshot);
+ Assert.equal(snapshot[k].sum, 6);
+ Assert.deepEqual(snapshot[k].range, [1, 50]);
+ Assert.deepEqual(snapshot[k].values, { 0: 3, 1: 2, 2: 2, 3: 0 });
+ }
+});
+
+add_task(async function test_keyed_flag_histogram() {
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_FLAG";
+ let h = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ const KEY = "default";
+ h.add(KEY, true);
+
+ let testSnapshot = {};
+ testSnapshot[KEY] = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 3,
+ sum: 1,
+ values: { 0: 0, 1: 1, 2: 0 },
+ };
+
+ Assert.deepEqual(h.keys().sort(), [KEY]);
+ Assert.deepEqual(h.snapshot(), testSnapshot);
+
+ let parentHgrams = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.deepEqual(parentHgrams[KEYED_ID], testSnapshot);
+
+ h.clear();
+ Assert.deepEqual(h.keys(), []);
+ Assert.deepEqual(h.snapshot(), {});
+});
+
+add_task(async function test_keyed_histogram_recording() {
+ // Check that no histogram is recorded if both base and extended recording are off.
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+
+ const TEST_KEY = "record_foo";
+ let h = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"
+ );
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.ok(!(TEST_KEY in h.snapshot()));
+
+ // Check that only base histograms are recorded.
+ Telemetry.canRecordBase = true;
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "The keyed histogram should record the correct value."
+ );
+
+ // Extended set keyed histograms should not be recorded.
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN");
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.ok(
+ !(TEST_KEY in h.snapshot()),
+ "The keyed histograms should not record any data."
+ );
+
+ // Check that extended histograms are recorded when required.
+ Telemetry.canRecordExtended = true;
+
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "The runtime keyed histogram should record the correct value."
+ );
+
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN");
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "The keyed histogram should record the correct value."
+ );
+
+ // Check that base histograms are still being recorded.
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot()[TEST_KEY].sum, 1);
+});
+
+add_task(async function test_histogram_recording_enabled() {
+ Telemetry.canRecordBase = true;
+ Telemetry.canRecordExtended = true;
+
+ // Check that a "normal" histogram respects recording-enabled on/off
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ var orig = h.snapshot();
+
+ h.add(1);
+ Assert.equal(orig.sum + 1, h.snapshot().sum, "add should record by default.");
+
+ // Check that when recording is disabled - add is ignored
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", false);
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "When recording is disabled add should not record."
+ );
+
+ // Check that we're back to normal after recording is enabled
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", true);
+ h.add(1);
+ Assert.equal(
+ orig.sum + 2,
+ h.snapshot().sum,
+ "When recording is re-enabled add should record."
+ );
+
+ // Check that we're correctly accumulating values other than 1.
+ h.clear();
+ h.add(3);
+ Assert.equal(
+ 3,
+ h.snapshot().sum,
+ "Recording counts greater than 1 should work."
+ );
+
+ // Check that a histogram with recording disabled by default behaves correctly
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT_INIT_NO_RECORD");
+ orig = h.snapshot();
+
+ h.add(1);
+ Assert.equal(
+ orig.sum,
+ h.snapshot().sum,
+ "When recording is disabled by default, add should not record by default."
+ );
+
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_COUNT_INIT_NO_RECORD",
+ true
+ );
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "When recording is enabled add should record."
+ );
+
+ // Restore to disabled
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_COUNT_INIT_NO_RECORD",
+ false
+ );
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "When recording is disabled add should not record."
+ );
+});
+
+add_task(async function test_keyed_histogram_recording_enabled() {
+ Telemetry.canRecordBase = true;
+ Telemetry.canRecordExtended = true;
+
+ // Check RecordingEnabled for keyed histograms which are recording by default
+ const TEST_KEY = "record_foo";
+ let h = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"
+ );
+
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "Keyed histogram add should record by default"
+ );
+
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT",
+ false
+ );
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "Keyed histogram add should not record when recording is disabled"
+ );
+
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT",
+ true
+ );
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "Keyed histogram add should record when recording is re-enabled"
+ );
+
+ // Check that a histogram with recording disabled by default behaves correctly
+ h = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD"
+ );
+ h.clear();
+
+ h.add(TEST_KEY, 1);
+ Assert.ok(
+ !(TEST_KEY in h.snapshot()),
+ "Keyed histogram add should not record by default for histograms which don't record by default"
+ );
+
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD",
+ true
+ );
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "Keyed histogram add should record when recording is enabled"
+ );
+
+ // Restore to disabled
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD",
+ false
+ );
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "Keyed histogram add should not record when recording is disabled"
+ );
+});
+
+add_task(async function test_histogramSnapshots() {
+ let keyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
+ keyed.add("a", 1);
+
+ // Check that keyed histograms are not returned
+ let parentHgrams = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(!("TELEMETRY_TEST_KEYED_COUNT" in parentHgrams));
+});
+
+add_task(async function test_datasets() {
+ // Check that datasets work as expected.
+
+ const currentRecordExtended = Telemetry.canRecordExtended;
+
+ // Clear everything out
+ Telemetry.getSnapshotForHistograms("main", true /* clear */);
+ Telemetry.getSnapshotForKeyedHistograms("main", true /* clear */);
+
+ // Empty histograms are filtered. Let's record what we check below.
+ Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN").add(1);
+ Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT").add(1);
+ // Keyed flag histograms are skipped if empty, let's add data
+ Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG").add("a", 1);
+ Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN").add(
+ "a",
+ 1
+ );
+ Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT").add(
+ "a",
+ 1
+ );
+
+ // Check that registeredHistogram works properly
+ Telemetry.canRecordExtended = true;
+ let registered = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ );
+ registered = new Set(Object.keys(registered.parent));
+ Assert.ok(registered.has("TELEMETRY_TEST_FLAG"));
+ Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTIN"));
+ Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTOUT"));
+ Telemetry.canRecordExtended = false;
+ registered = Telemetry.getSnapshotForHistograms("main", false /* clear */);
+ registered = new Set(Object.keys(registered.parent));
+ Assert.ok(!registered.has("TELEMETRY_TEST_FLAG"));
+ Assert.ok(!registered.has("TELEMETRY_TEST_RELEASE_OPTIN"));
+ Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTOUT"));
+
+ // Check that registeredKeyedHistograms works properly
+ Telemetry.canRecordExtended = true;
+ registered = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ );
+ registered = new Set(Object.keys(registered.parent));
+ Assert.ok(registered.has("TELEMETRY_TEST_KEYED_FLAG"));
+ Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"));
+ Telemetry.canRecordExtended = false;
+ registered = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ );
+ registered = new Set(Object.keys(registered.parent));
+ Assert.ok(!registered.has("TELEMETRY_TEST_KEYED_FLAG"));
+ Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"));
+
+ Telemetry.canRecordExtended = currentRecordExtended;
+});
+
+add_task(async function test_keyed_keys() {
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_KEYS");
+ h.clear();
+ Telemetry.clearScalars();
+
+ // The |add| method should not throw for keys that are not allowed.
+ h.add("testkey", true);
+ h.add("thirdKey", false);
+ h.add("not-allowed", true);
+
+ // Check that we have the expected keys.
+ let snap = h.snapshot();
+ Assert.equal(Object.keys(snap).length, 2, "Only 2 keys must be recorded.");
+ Assert.ok("testkey" in snap, "'testkey' must be recorded.");
+ Assert.ok("thirdKey" in snap, "'thirdKey' must be recorded.");
+ Assert.deepEqual(
+ snap.testkey.values,
+ { 0: 0, 1: 1, 2: 0 },
+ "'testkey' must contain the correct value."
+ );
+ Assert.deepEqual(
+ snap.thirdKey.values,
+ { 0: 1, 1: 0 },
+ "'thirdKey' must contain the correct value."
+ );
+
+ // Keys that are not allowed must not be recorded.
+ Assert.ok(!("not-allowed" in snap), "'not-allowed' must not be recorded.");
+
+ // Check that these failures were correctly tracked.
+ const parentScalars = Telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false
+ ).parent;
+ const scalarName = "telemetry.accumulate_unknown_histogram_keys";
+ Assert.ok(
+ scalarName in parentScalars,
+ "Accumulation to unallowed keys must be reported."
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_KEYED_KEYS" in parentScalars[scalarName],
+ "Accumulation to unallowed keys must be recorded with the correct key."
+ );
+ Assert.equal(
+ parentScalars[scalarName].TELEMETRY_TEST_KEYED_KEYS,
+ 1,
+ "Accumulation to unallowed keys must report the correct value."
+ );
+});
+
+add_task(async function test_count_multiple_samples() {
+ let valid = [1, 1, 3, 0];
+ let invalid = ["1", "0", "", "random"];
+
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ h.clear();
+
+ // If the array contains even a single invalid value, no accumulation should take place
+ // Keep the valid values in front of invalid to check if it is simply accumulating as
+ // it's traversing the array and throwing upon first invalid value. That should not happen.
+ h.add(valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.equal(s1.sum, 0);
+ // Ensure that no accumulations of 0-like values took place.
+ // These accumulations won't increase the sum.
+ Assert.deepEqual({}, s1.values);
+
+ h.add(valid);
+ let s2 = h.snapshot();
+ Assert.deepEqual(s2.values, { 0: 4, 1: 0 });
+ Assert.equal(s2.sum, 5);
+});
+
+add_task(async function test_categorical_multiple_samples() {
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL");
+ h.clear();
+ let valid = ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1];
+ let invalid = ["", "Label4", "1234", "0", "1", 5000];
+
+ // At least one invalid parameter, so no accumulation should happen here
+ // Valid values in front of invalid.
+ h.add(valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.equal(s1.sum, 0);
+ Assert.deepEqual({}, s1.values);
+
+ h.add(valid);
+ let snapshot = h.snapshot();
+ Assert.equal(snapshot.sum, 6);
+ Assert.deepEqual(snapshot.values, { 0: 3, 1: 2, 2: 2, 3: 0 });
+});
+
+add_task(async function test_boolean_multiple_samples() {
+ let valid = [true, false, 0, 1, 2];
+ let invalid = ["", "0", "1", ",2", "true", "false", "random"];
+
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN");
+ h.clear();
+
+ // At least one invalid parameter, so no accumulation should happen here
+ // Valid values in front of invalid.
+ h.add(valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.equal(s1.sum, 0);
+ Assert.deepEqual({}, s1.values);
+
+ h.add(valid);
+ let s = h.snapshot();
+ Assert.deepEqual(s.values, { 0: 2, 1: 3, 2: 0 });
+ Assert.equal(s.sum, 3);
+});
+
+add_task(async function test_linear_multiple_samples() {
+ // According to telemetry.mozilla.org/histogram-simulator, bucket at
+ // index 1 of TELEMETRY_TEST_LINEAR has max value of 268.44M
+ let valid = [0, 1, 5, 10, 268450000, 268450001, Math.pow(2, 31) + 1];
+ let invalid = ["", "0", "1", "random"];
+
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR");
+ h.clear();
+
+ // At least one invalid paramater, so no accumulations.
+ // Valid values in front of invalid.
+ h.add(valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.equal(s1.sum, 0);
+ Assert.deepEqual({}, s1.values);
+
+ h.add(valid);
+ let s2 = h.snapshot();
+ // Values >= INT32_MAX are accumulated as INT32_MAX - 1
+ Assert.equal(s2.sum, valid.reduce((acc, cur) => acc + cur) - 3);
+ Assert.deepEqual(Object.values(s2.values), [1, 3, 2, 1]);
+});
+
+add_task(async function test_keyed_no_arguments() {
+ // Test for no accumulation when add is called with no arguments
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_LINEAR");
+ h.clear();
+
+ h.add();
+
+ // No keys should be added due to no accumulation.
+ Assert.equal(h.keys().length, 0);
+});
+
+add_task(async function test_keyed_categorical_invalid_string() {
+ // Test for no accumulation when add is called on a
+ // keyed categorical histogram with an invalid string label.
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_CATEGORICAL");
+ h.clear();
+
+ h.add("someKey", "#notALabel");
+
+ // No keys should be added due to no accumulation.
+ Assert.equal(h.keys().length, 0);
+});
+
+add_task(async function test_keyed_count_multiple_samples() {
+ let valid = [1, 1, 3, 0];
+ let invalid = ["1", "0", "", "random"];
+ let key = "somekeystring";
+
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
+ h.clear();
+
+ // If the array contains even a single invalid value, no accumulation should take place
+ // Keep the valid values in front of invalid to check if it is simply accumulating as
+ // it's traversing the array and throwing upon first invalid value. That should not happen.
+ h.add(key, valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.ok(!(key in s1));
+
+ h.add(key, valid);
+ let s2 = h.snapshot()[key];
+ Assert.deepEqual(s2.values, { 0: 4, 1: 0 });
+ Assert.equal(s2.sum, 5);
+});
+
+add_task(async function test_keyed_categorical_multiple_samples() {
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_CATEGORICAL");
+ h.clear();
+ let valid = ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1];
+ let invalid = ["", "Label4", "1234", "0", "1", 5000];
+ let key = "somekeystring";
+
+ // At least one invalid parameter, so no accumulation should happen here
+ // Valid values in front of invalid.
+ h.add(key, valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.ok(!(key in s1));
+
+ h.add(key, valid);
+ let snapshot = h.snapshot()[key];
+ Assert.equal(snapshot.sum, 6);
+ Assert.deepEqual(Object.values(snapshot.values), [3, 2, 2, 0]);
+});
+
+add_task(async function test_keyed_boolean_multiple_samples() {
+ let valid = [true, false, 0, 1, 2];
+ let invalid = ["", "0", "1", ",2", "true", "false", "random"];
+ let key = "somekey";
+
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_BOOLEAN");
+ h.clear();
+
+ // At least one invalid parameter, so no accumulation should happen here
+ // Valid values in front of invalid.
+ h.add(key, valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.ok(!(key in s1));
+
+ h.add(key, valid);
+ let s = h.snapshot()[key];
+ Assert.deepEqual(s.values, { 0: 2, 1: 3, 2: 0 });
+ Assert.equal(s.sum, 3);
+});
+
+add_task(async function test_keyed_linear_multiple_samples() {
+ // According to telemetry.mozilla.org/histogram-simulator, bucket at
+ // index 1 of TELEMETRY_TEST_LINEAR has max value of 3.13K
+ let valid = [0, 1, 5, 10, 268450000, 268450001, Math.pow(2, 31) + 1];
+ let invalid = ["", "0", "1", "random"];
+ let key = "somestring";
+
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_LINEAR");
+ h.clear();
+
+ // At least one invalid paramater, so no accumulations.
+ // Valid values in front of invalid.
+ h.add(key, valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.ok(!(key in s1));
+
+ h.add(key, valid);
+ let s2 = h.snapshot()[key];
+ // Values >= INT32_MAX are accumulated as INT32_MAX - 1
+ Assert.equal(s2.sum, valid.reduce((acc, cur) => acc + cur) - 3);
+ Assert.deepEqual(s2.range, [1, 250000]);
+ Assert.deepEqual(s2.values, { 0: 1, 1: 3, 250000: 3 });
+});
+
+add_task(async function test_non_array_non_string_obj() {
+ let invalid_obj = {
+ prop1: "someValue",
+ prop2: "someOtherValue",
+ };
+ let key = "someString";
+
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_LINEAR");
+ h.clear();
+
+ h.add(key, invalid_obj);
+ Assert.equal(h.keys().length, 0);
+});
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_productSpecificHistograms() {
+ const DEFAULT_PRODUCTS_HISTOGRAM = "TELEMETRY_TEST_DEFAULT_PRODUCTS";
+ const DESKTOP_ONLY_HISTOGRAM = "TELEMETRY_TEST_DESKTOP_ONLY";
+ const MULTIPRODUCT_HISTOGRAM = "TELEMETRY_TEST_MULTIPRODUCT";
+ const MOBILE_ONLY_HISTOGRAM = "TELEMETRY_TEST_MOBILE_ONLY";
+
+ var default_histo = Telemetry.getHistogramById(DEFAULT_PRODUCTS_HISTOGRAM);
+ var desktop_histo = Telemetry.getHistogramById(DESKTOP_ONLY_HISTOGRAM);
+ var multiproduct_histo = Telemetry.getHistogramById(MULTIPRODUCT_HISTOGRAM);
+ var mobile_histo = Telemetry.getHistogramById(MOBILE_ONLY_HISTOGRAM);
+ default_histo.clear();
+ desktop_histo.clear();
+ multiproduct_histo.clear();
+ mobile_histo.clear();
+
+ default_histo.add(42);
+ desktop_histo.add(42);
+ multiproduct_histo.add(42);
+ mobile_histo.add(42);
+
+ let histograms = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+
+ Assert.ok(
+ DEFAULT_PRODUCTS_HISTOGRAM in histograms,
+ "Should have recorded default products histogram"
+ );
+ Assert.ok(
+ DESKTOP_ONLY_HISTOGRAM in histograms,
+ "Should have recorded desktop-only histogram"
+ );
+ Assert.ok(
+ MULTIPRODUCT_HISTOGRAM in histograms,
+ "Should have recorded multiproduct histogram"
+ );
+
+ Assert.ok(
+ !(MOBILE_ONLY_HISTOGRAM in histograms),
+ "Should not have recorded mobile-only histogram"
+ );
+ }
+);
+
+add_task(
+ {
+ skip_if: () => !gIsAndroid,
+ },
+ async function test_mobileSpecificHistograms() {
+ const DEFAULT_PRODUCTS_HISTOGRAM = "TELEMETRY_TEST_DEFAULT_PRODUCTS";
+ const DESKTOP_ONLY_HISTOGRAM = "TELEMETRY_TEST_DESKTOP_ONLY";
+ const MULTIPRODUCT_HISTOGRAM = "TELEMETRY_TEST_MULTIPRODUCT";
+ const MOBILE_ONLY_HISTOGRAM = "TELEMETRY_TEST_MOBILE_ONLY";
+
+ var default_histo = Telemetry.getHistogramById(DEFAULT_PRODUCTS_HISTOGRAM);
+ var desktop_histo = Telemetry.getHistogramById(DESKTOP_ONLY_HISTOGRAM);
+ var multiproduct_histo = Telemetry.getHistogramById(MULTIPRODUCT_HISTOGRAM);
+ var mobile_histo = Telemetry.getHistogramById(MOBILE_ONLY_HISTOGRAM);
+ default_histo.clear();
+ desktop_histo.clear();
+ multiproduct_histo.clear();
+ mobile_histo.clear();
+
+ default_histo.add(1);
+ desktop_histo.add(1);
+ multiproduct_histo.add(1);
+ mobile_histo.add(1);
+
+ let histograms = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+
+ Assert.ok(
+ DEFAULT_PRODUCTS_HISTOGRAM in histograms,
+ "Should have recorded default products histogram"
+ );
+ Assert.ok(
+ MOBILE_ONLY_HISTOGRAM in histograms,
+ "Should have recorded mobile-only histogram"
+ );
+ Assert.ok(
+ MULTIPRODUCT_HISTOGRAM in histograms,
+ "Should have recorded multiproduct histogram"
+ );
+
+ Assert.ok(
+ !(DESKTOP_ONLY_HISTOGRAM in histograms),
+ "Should not have recorded desktop-only histogram"
+ );
+ }
+);
+
+add_task(async function test_productsOverride() {
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+ const DEFAULT_PRODUCTS_HISTOGRAM = "TELEMETRY_TEST_DEFAULT_PRODUCTS";
+ const DESKTOP_ONLY_HISTOGRAM = "TELEMETRY_TEST_DESKTOP_ONLY";
+ const MULTIPRODUCT_HISTOGRAM = "TELEMETRY_TEST_MULTIPRODUCT";
+ const MOBILE_ONLY_HISTOGRAM = "TELEMETRY_TEST_MOBILE_ONLY";
+
+ var default_histo = Telemetry.getHistogramById(DEFAULT_PRODUCTS_HISTOGRAM);
+ var desktop_histo = Telemetry.getHistogramById(DESKTOP_ONLY_HISTOGRAM);
+ var multiproduct_histo = Telemetry.getHistogramById(MULTIPRODUCT_HISTOGRAM);
+ var mobile_histo = Telemetry.getHistogramById(MOBILE_ONLY_HISTOGRAM);
+ default_histo.clear();
+ desktop_histo.clear();
+ multiproduct_histo.clear();
+ mobile_histo.clear();
+
+ default_histo.add(1);
+ desktop_histo.add(1);
+ multiproduct_histo.add(1);
+ mobile_histo.add(1);
+
+ let histograms = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+
+ Assert.ok(
+ DEFAULT_PRODUCTS_HISTOGRAM in histograms,
+ "Should have recorded default products histogram"
+ );
+ Assert.ok(
+ MOBILE_ONLY_HISTOGRAM in histograms,
+ "Should have recorded mobile-only histogram"
+ );
+ Assert.ok(
+ MULTIPRODUCT_HISTOGRAM in histograms,
+ "Should have recorded multiproduct histogram"
+ );
+
+ Assert.ok(
+ DESKTOP_ONLY_HISTOGRAM in histograms,
+ "Should not have recorded desktop-only histogram"
+ );
+ Services.prefs.clearUserPref(
+ "toolkit.telemetry.testing.overrideProductsCheck"
+ );
+});
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_clearHistogramsOnSnapshot() {
+ const COUNT = "TELEMETRY_TEST_COUNT";
+ let h = Telemetry.getHistogramById(COUNT);
+ h.clear();
+ let snapshot;
+
+ // The first snapshot should be empty, nothing recorded.
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(!(COUNT in snapshot));
+
+ // After recording into a histogram, the data should be in the snapshot. Don't delete it.
+ h.add(1);
+
+ Assert.equal(h.snapshot().sum, 1);
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(COUNT in snapshot);
+ Assert.equal(snapshot[COUNT].sum, 1);
+
+ // After recording into a histogram again, the data should be updated and in the snapshot.
+ // Clean up after.
+ h.add(41);
+
+ Assert.equal(h.snapshot().sum, 42);
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ true /* clear */
+ ).parent;
+ Assert.ok(COUNT in snapshot);
+ Assert.equal(snapshot[COUNT].sum, 42);
+
+ // Finally, no data should be in the snapshot.
+ Assert.equal(h.snapshot().sum, 0);
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(!(COUNT in snapshot));
+ }
+);
+
+add_task(async function test_valid_os_smoketest() {
+ let nonExistingProbe;
+ let existingProbe;
+
+ switch (AppConstants.platform) {
+ case "linux":
+ nonExistingProbe = "TELEMETRY_TEST_OS_ANDROID_ONLY";
+ existingProbe = "TELEMETRY_TEST_OS_LINUX_ONLY";
+ break;
+ case "macosx":
+ nonExistingProbe = "TELEMETRY_TEST_OS_ANDROID_ONLY";
+ existingProbe = "TELEMETRY_TEST_OS_MAC_ONLY";
+ break;
+ case "win":
+ nonExistingProbe = "TELEMETRY_TEST_OS_ANDROID_ONLY";
+ existingProbe = "TELEMETRY_TEST_OS_WIN_ONLY";
+ break;
+ case "android":
+ nonExistingProbe = "TELEMETRY_TEST_OS_LINUX_ONLY";
+ existingProbe = "TELEMETRY_TEST_OS_ANDROID_ONLY";
+ break;
+ default:
+ /* Unknown OS. Let's not test OS-specific probes */
+ return;
+ }
+
+ Assert.throws(
+ () => Telemetry.getHistogramById(nonExistingProbe),
+ /NS_ERROR_FAILURE/,
+ `Should throw on ${nonExistingProbe} probe that's not available on ${AppConstants.platform}`
+ );
+
+ let h = Telemetry.getHistogramById(existingProbe);
+ h.clear();
+ h.add(1);
+ let snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(
+ existingProbe in snapshot,
+ `${existingProbe} should be recorded on ${AppConstants.platform}`
+ );
+ Assert.equal(snapshot[existingProbe].sum, 1);
+});
+
+add_task(async function test_multistore_individual_histogram() {
+ Telemetry.canRecordExtended = true;
+
+ let id;
+ let hist;
+ let snapshot;
+
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ snapshot = hist.snapshot();
+ Assert.equal(0, snapshot.sum, `Histogram ${id} should be empty.`);
+ hist.add(1);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ 1,
+ snapshot.sum,
+ `Histogram ${id} should have recorded one value.`
+ );
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.equal(0, snapshot.sum, `Histogram ${id} should be cleared.`);
+
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ hist = Telemetry.getHistogramById(id);
+ snapshot = hist.snapshot();
+ Assert.equal(0, snapshot.sum, `Histogram ${id} should be empty.`);
+ hist.add(1);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ 1,
+ snapshot.sum,
+ `Histogram ${id} should have recorded one value.`
+ );
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.equal(0, snapshot.sum, `Histogram ${id} should be cleared.`);
+
+ // When sync only, then the snapshot will be empty on the main store
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+ hist.add(1);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ hist = Telemetry.getKeyedHistogramById(id);
+ snapshot = hist.snapshot();
+ Assert.deepEqual({}, snapshot, `Histogram ${id} should be empty.`);
+ hist.add("key-a", 1);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ 1,
+ snapshot["key-a"].sum,
+ `Histogram ${id} should have recorded one value.`
+ );
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.deepEqual({}, snapshot, `Histogram ${id} should be cleared.`);
+
+ // When sync only, then the snapshot will be empty on the main store
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ hist = Telemetry.getKeyedHistogramById(id);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+ hist.add("key-a", 1);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+});
+
+add_task(async function test_multistore_main_snapshot() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForHistograms("main", true);
+ Telemetry.getSnapshotForKeyedHistograms("main", true);
+
+ let id;
+ let hist;
+ let snapshot;
+
+ // Plain histograms
+
+ // Fill with data
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ // Getting snapshot and NOT clearing (using default values for optional parameters)
+ snapshot = Telemetry.getSnapshotForHistograms().parent;
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ // Data should still be in, getting snapshot and clearing
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ // Should be empty after clearing
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ /* clear */ false
+ ).parent;
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ // Keyed histograms
+
+ // Fill with data
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-a", 1);
+
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-b", 1);
+
+ // Getting snapshot and NOT clearing (using default values for optional parameters)
+ snapshot = Telemetry.getSnapshotForKeyedHistograms().parent;
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ // Data should still be in, getting snapshot and clearing
+ snapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ // Should be empty after clearing
+ snapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ /* clear */ false
+ ).parent;
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+});
+
+add_task(async function test_multistore_argument_handling() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForHistograms("main", true);
+ Telemetry.getSnapshotForHistograms("sync", true);
+ Telemetry.getSnapshotForKeyedHistograms("main", true);
+ Telemetry.getSnapshotForKeyedHistograms("sync", true);
+
+ let id;
+ let hist;
+ let snapshot;
+
+ // Plain Histograms
+
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(37);
+
+ // No argument
+ snapshot = hist.snapshot();
+ Assert.equal(37, snapshot.sum, `${id} should be in a default store snapshot`);
+
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.equal(0, snapshot.sum, `${id} should be cleared in the default store`);
+
+ snapshot = hist.snapshot({ store: "sync" });
+ Assert.equal(
+ 37,
+ snapshot.sum,
+ `${id} should not have been cleared in the sync store`
+ );
+
+ Assert.throws(
+ () => hist.snapshot(2, "or", "more", "arguments"),
+ /one argument/,
+ "snapshot should check argument count"
+ );
+ Assert.throws(
+ () => hist.snapshot(2),
+ /object argument/,
+ "snapshot should check argument type"
+ );
+ Assert.throws(
+ () => hist.snapshot({}),
+ /property/,
+ "snapshot should check for object property"
+ );
+ Assert.throws(
+ () => hist.snapshot({ store: 1 }),
+ /string/,
+ "snapshot should check object property's type"
+ );
+
+ Assert.throws(
+ () => hist.clear(2, "or", "more", "arguments"),
+ /one argument/,
+ "clear should check argument count"
+ );
+ Assert.throws(
+ () => hist.clear(2),
+ /object argument/,
+ "clear should check argument type"
+ );
+ Assert.throws(
+ () => hist.clear({}),
+ /property/,
+ "clear should check for object property"
+ );
+ Assert.throws(
+ () => hist.clear({ store: 1 }),
+ /string/,
+ "clear should check object property's type"
+ );
+
+ // Keyed Histogram
+
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-1", 37);
+
+ // No argument
+ snapshot = hist.snapshot();
+ Assert.equal(
+ 37,
+ snapshot["key-1"].sum,
+ `${id} should be in a default store snapshot`
+ );
+
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.ok(
+ !("key-1" in snapshot),
+ `${id} should be cleared in the default store`
+ );
+
+ snapshot = hist.snapshot({ store: "sync" });
+ Assert.equal(
+ 37,
+ snapshot["key-1"].sum,
+ `${id} should not have been cleared in the sync store`
+ );
+
+ Assert.throws(
+ () => hist.snapshot(2, "or", "more", "arguments"),
+ /one argument/,
+ "snapshot should check argument count"
+ );
+ Assert.throws(
+ () => hist.snapshot(2),
+ /object argument/,
+ "snapshot should check argument type"
+ );
+ Assert.throws(
+ () => hist.snapshot({}),
+ /property/,
+ "snapshot should check for object property"
+ );
+ Assert.throws(
+ () => hist.snapshot({ store: 1 }),
+ /string/,
+ "snapshot should check object property's type"
+ );
+
+ Assert.throws(
+ () => hist.clear(2, "or", "more", "arguments"),
+ /one argument/,
+ "clear should check argument count"
+ );
+ Assert.throws(
+ () => hist.clear(2),
+ /object argument/,
+ "clear should check argument type"
+ );
+ Assert.throws(
+ () => hist.clear({}),
+ /property/,
+ "clear should check for object property"
+ );
+ Assert.throws(
+ () => hist.clear({ store: 1 }),
+ /string/,
+ "clear should check object property's type"
+ );
+});
+
+add_task(async function test_multistore_sync_snapshot() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForHistograms("main", true);
+ Telemetry.getSnapshotForHistograms("sync", true);
+
+ let id;
+ let hist;
+ let snapshot;
+
+ // Plain histograms
+
+ // Fill with data
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ // Getting snapshot and clearing
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "sync",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a sync store snapshot`);
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a sync store snapshot`);
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ Assert.ok(id in snapshot, `${id} should be in a sync store snapshot`);
+});
+
+add_task(async function test_multistore_keyed_sync_snapshot() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForKeyedHistograms("main", true);
+ Telemetry.getSnapshotForKeyedHistograms("sync", true);
+
+ let id;
+ let hist;
+ let snapshot;
+
+ // Plain histograms
+
+ // Fill with data
+ id = "TELEMETRY_TEST_KEYED_LINEAR";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-1", 1);
+
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-1", 1);
+
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-1", 1);
+
+ // Getting snapshot and clearing
+ snapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_KEYED_LINEAR";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ snapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "sync",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_KEYED_LINEAR";
+ Assert.ok(!(id in snapshot), `${id} should not be in a sync store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a sync store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ Assert.ok(id in snapshot, `${id} should be in a sync store snapshot`);
+});
+
+add_task(async function test_multistore_plain_individual_snapshot() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForHistograms("main", true);
+ Telemetry.getSnapshotForHistograms("sync", true);
+
+ let id;
+ let hist;
+
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ hist = Telemetry.getHistogramById(id);
+
+ hist.add(37);
+ Assert.deepEqual(37, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "sync" }));
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual(0, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "sync" }));
+
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ hist = Telemetry.getHistogramById(id);
+
+ hist.add(37);
+ Assert.deepEqual(37, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" }).sum);
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual(0, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" }).sum);
+
+ hist.add(3);
+ Assert.deepEqual(3, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(40, hist.snapshot({ store: "sync" }).sum);
+
+ hist.clear({ store: "sync" });
+ Assert.deepEqual(3, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(0, hist.snapshot({ store: "sync" }).sum);
+
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ hist = Telemetry.getHistogramById(id);
+
+ hist.add(37);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" }).sum);
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" }).sum);
+
+ hist.add(3);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(40, hist.snapshot({ store: "sync" }).sum);
+
+ hist.clear({ store: "sync" });
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(0, hist.snapshot({ store: "sync" }).sum);
+});
+
+add_task(async function test_multistore_keyed_individual_snapshot() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForKeyedHistograms("main", true);
+ Telemetry.getSnapshotForKeyedHistograms("sync", true);
+
+ let id;
+ let hist;
+
+ id = "TELEMETRY_TEST_KEYED_LINEAR";
+ hist = Telemetry.getKeyedHistogramById(id);
+
+ hist.add("key-1", 37);
+ Assert.deepEqual(37, hist.snapshot({ store: "main" })["key-1"].sum);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "sync" }));
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual({}, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(undefined, hist.snapshot({ store: "sync" }));
+
+ hist.add("key-1", 4);
+ hist.clear({ store: "sync" });
+ Assert.deepEqual(4, hist.snapshot({ store: "main" })["key-1"].sum);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "sync" }));
+
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ hist = Telemetry.getKeyedHistogramById(id);
+
+ hist.add("key-1", 37);
+ Assert.deepEqual(37, hist.snapshot({ store: "main" })["key-1"].sum);
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual({}, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.add("key-1", 3);
+ Assert.deepEqual(3, hist.snapshot({ store: "main" })["key-1"].sum);
+ Assert.deepEqual(40, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.clear({ store: "sync" });
+ Assert.deepEqual(3, hist.snapshot({ store: "main" })["key-1"].sum);
+ Assert.deepEqual({}, hist.snapshot({ store: "sync" }));
+
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ hist = Telemetry.getKeyedHistogramById(id);
+
+ hist.add("key-1", 37);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.add("key-1", 3);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(40, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.clear({ store: "sync" });
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual({}, hist.snapshot({ store: "sync" }));
+});
+
+add_task(async function test_can_record_in_process_regression_bug_1530361() {
+ Telemetry.getSnapshotForHistograms("main", true);
+
+ // The socket and gpu processes should not have any histograms.
+ // Flag and count histograms have defaults, so if we're accidentally recording them
+ // in these processes they'd show up even immediately after being cleared.
+ let snapshot = Telemetry.getSnapshotForHistograms("main", true);
+
+ Assert.deepEqual(
+ snapshot.gpu,
+ {},
+ "No histograms should have been recorded for the gpu process"
+ );
+ Assert.deepEqual(
+ snapshot.socket,
+ {},
+ "No histograms should have been recorded for the socket process"
+ );
+});
+
+add_task(function test_knows_its_name() {
+ let h;
+
+ // Plain histograms
+ const histNames = [
+ "TELEMETRY_TEST_FLAG",
+ "TELEMETRY_TEST_COUNT",
+ "TELEMETRY_TEST_CATEGORICAL",
+ "TELEMETRY_TEST_EXPIRED",
+ ];
+
+ for (let name of histNames) {
+ h = Telemetry.getHistogramById(name);
+ Assert.equal(name, h.name());
+ }
+
+ // Keyed histograms
+ const keyedHistNames = [
+ "TELEMETRY_TEST_KEYED_EXPONENTIAL",
+ "TELEMETRY_TEST_KEYED_BOOLEAN",
+ "TELEMETRY_TEST_EXPIRED_KEYED",
+ ];
+
+ for (let name of keyedHistNames) {
+ h = Telemetry.getKeyedHistogramById(name);
+ Assert.equal(name, h.name());
+ }
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js b/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js
new file mode 100644
index 0000000000..00891c36e8
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* A testcase to make sure reading late writes stacks works. */
+
+// Constants from prio.h for nsIFileOutputStream.init
+const PR_WRONLY = 0x2;
+const PR_CREATE_FILE = 0x8;
+const PR_TRUNCATE = 0x20;
+const RW_OWNER = parseInt("0600", 8);
+
+const STACK_SUFFIX1 = "stack1.txt";
+const STACK_SUFFIX2 = "stack2.txt";
+const STACK_BOGUS_SUFFIX = "bogus.txt";
+const LATE_WRITE_PREFIX = "Telemetry.LateWriteFinal-";
+
+// The names and IDs don't matter, but the format of the IDs does.
+const LOADED_MODULES = {
+ "4759A7E6993548C89CAF716A67EC242D00": "libtest.so",
+ F77AF15BB8D6419FA875954B4A3506CA00: "libxul.so",
+ "1E2F7FB590424E8F93D60BB88D66B8C500": "libc.so",
+ E4D6D70CC09A63EF8B88D532F867858800: "libmodμles.so",
+};
+const N_MODULES = Object.keys(LOADED_MODULES).length;
+
+// Format of individual items is [index, offset-in-library].
+const STACK1 = [
+ [0, 0],
+ [1, 1],
+ [2, 2],
+ [3, 3],
+];
+const STACK2 = [
+ [0, 0],
+ [1, 5],
+ [2, 10],
+ [3, 15],
+];
+// XXX The only error checking is for a zero-sized stack.
+const STACK_BOGUS = [];
+
+function write_string_to_file(file, contents) {
+ let ostream = Cc[
+ "@mozilla.org/network/safe-file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(
+ file,
+ PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ RW_OWNER,
+ ostream.DEFER_OPEN
+ );
+
+ var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bos.setOutputStream(ostream);
+
+ let utf8 = new TextEncoder().encode(contents);
+ bos.writeByteArray(utf8);
+ ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ ostream.close();
+}
+
+function construct_file(suffix) {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append(LATE_WRITE_PREFIX + suffix);
+ return file;
+}
+
+function write_late_writes_file(stack, suffix) {
+ let file = construct_file(suffix);
+ let contents = N_MODULES + "\n";
+ for (let id in LOADED_MODULES) {
+ contents += id + " " + LOADED_MODULES[id] + "\n";
+ }
+
+ contents += stack.length + "\n";
+ for (let element of stack) {
+ contents += element[0] + " " + element[1].toString(16) + "\n";
+ }
+
+ write_string_to_file(file, contents);
+}
+
+function run_test() {
+ do_get_profile();
+
+ write_late_writes_file(STACK1, STACK_SUFFIX1);
+ write_late_writes_file(STACK2, STACK_SUFFIX2);
+ write_late_writes_file(STACK_BOGUS, STACK_BOGUS_SUFFIX);
+
+ let lateWrites = Telemetry.lateWrites;
+ Assert.ok("memoryMap" in lateWrites);
+ Assert.equal(lateWrites.memoryMap.length, 0);
+ Assert.ok("stacks" in lateWrites);
+ Assert.equal(lateWrites.stacks.length, 0);
+
+ do_test_pending();
+ Telemetry.asyncFetchTelemetryData(function () {
+ actual_test();
+ });
+}
+
+function actual_test() {
+ Assert.ok(!construct_file(STACK_SUFFIX1).exists());
+ Assert.ok(!construct_file(STACK_SUFFIX2).exists());
+ Assert.ok(!construct_file(STACK_BOGUS_SUFFIX).exists());
+
+ let lateWrites = Telemetry.lateWrites;
+
+ Assert.ok("memoryMap" in lateWrites);
+ Assert.equal(lateWrites.memoryMap.length, N_MODULES);
+ for (let id in LOADED_MODULES) {
+ let matchingLibrary = lateWrites.memoryMap.filter(function (
+ library,
+ idx,
+ array
+ ) {
+ return library[1] == id;
+ });
+ Assert.equal(matchingLibrary.length, 1);
+ let library = matchingLibrary[0];
+ let name = library[0];
+ Assert.equal(LOADED_MODULES[id], name);
+ }
+
+ Assert.ok("stacks" in lateWrites);
+ Assert.equal(lateWrites.stacks.length, 2);
+ let uneval_STACKS = [uneval(STACK1), uneval(STACK2)];
+ let first_stack = lateWrites.stacks[0];
+ let second_stack = lateWrites.stacks[1];
+ function stackChecker(canonicalStack) {
+ let unevalCanonicalStack = uneval(canonicalStack);
+ return function (obj, idx, array) {
+ return unevalCanonicalStack == obj;
+ };
+ }
+ Assert.equal(uneval_STACKS.filter(stackChecker(first_stack)).length, 1);
+ Assert.equal(uneval_STACKS.filter(stackChecker(second_stack)).length, 1);
+
+ do_test_finished();
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js b/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js
new file mode 100644
index 0000000000..7f9322e37c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* A testcase to make sure reading the failed profile lock count works. */
+
+const LOCK_FILE_NAME = "Telemetry.FailedProfileLocks.txt";
+const N_FAILED_LOCKS = 10;
+
+// Constants from prio.h for nsIFileOutputStream.init
+const PR_WRONLY = 0x2;
+const PR_CREATE_FILE = 0x8;
+const PR_TRUNCATE = 0x20;
+const RW_OWNER = parseInt("0600", 8);
+
+function write_string_to_file(file, contents) {
+ let ostream = Cc[
+ "@mozilla.org/network/safe-file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(
+ file,
+ PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ RW_OWNER,
+ ostream.DEFER_OPEN
+ );
+ ostream.write(contents, contents.length);
+ ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ ostream.close();
+}
+
+function construct_file() {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append(LOCK_FILE_NAME);
+ return file;
+}
+
+function run_test() {
+ do_get_profile();
+
+ Assert.equal(Telemetry.failedProfileLockCount, 0);
+
+ write_string_to_file(construct_file(), N_FAILED_LOCKS.toString());
+
+ // Make sure that we're not eagerly reading the count now that the
+ // file exists.
+ Assert.equal(Telemetry.failedProfileLockCount, 0);
+
+ do_test_pending();
+ Telemetry.asyncFetchTelemetryData(actual_test);
+}
+
+function actual_test() {
+ Assert.equal(Telemetry.failedProfileLockCount, N_FAILED_LOCKS);
+ Assert.ok(!construct_file().exists());
+ do_test_finished();
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
new file mode 100644
index 0000000000..76343be200
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
@@ -0,0 +1,350 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that TelemetryController sends close to shutdown don't lead
+// to AsyncShutdown timeouts.
+
+"use strict";
+
+const { TelemetryReportingPolicy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+);
+const { UpdateUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/UpdateUtils.sys.mjs"
+);
+
+const TEST_CHANNEL = "TestChannelABC";
+
+const PREF_MINIMUM_CHANNEL_POLICY_VERSION =
+ TelemetryUtils.Preferences.MinimumPolicyVersion + ".channel-" + TEST_CHANNEL;
+
+function fakeShowPolicyTimeout(set, clear) {
+ let { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+ );
+ Policy.setShowInfobarTimeout = set;
+ Policy.clearShowInfobarTimeout = clear;
+}
+
+function fakeResetAcceptedPolicy() {
+ Preferences.reset(TelemetryUtils.Preferences.AcceptedPolicyDate);
+ Preferences.reset(TelemetryUtils.Preferences.AcceptedPolicyVersion);
+}
+
+function setMinimumPolicyVersion(aNewPolicyVersion) {
+ const CHANNEL_NAME = UpdateUtils.getUpdateChannel(false);
+ // We might have channel-dependent minimum policy versions.
+ const CHANNEL_DEPENDENT_PREF =
+ TelemetryUtils.Preferences.MinimumPolicyVersion +
+ ".channel-" +
+ CHANNEL_NAME;
+
+ // Does the channel-dependent pref exist? If so, set its value.
+ if (Preferences.get(CHANNEL_DEPENDENT_PREF, undefined)) {
+ Preferences.set(CHANNEL_DEPENDENT_PREF, aNewPolicyVersion);
+ return;
+ }
+
+ // We don't have a channel specific minimu, so set the common one.
+ Preferences.set(
+ TelemetryUtils.Preferences.MinimumPolicyVersion,
+ aNewPolicyVersion
+ );
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile(true);
+ 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();
+
+ // Don't bypass the notifications in this test, we'll fake it.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ false
+ );
+
+ TelemetryReportingPolicy.setup();
+});
+
+add_task(
+ {
+ // This tests initialises the search service, but that doesn't currently
+ // work on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_firstRun() {
+ await Services.search.init();
+
+ const FIRST_RUN_TIMEOUT_MSEC = 60 * 1000; // 60s
+ const OTHER_RUNS_TIMEOUT_MSEC = 10 * 1000; // 10s
+
+ Preferences.reset(TelemetryUtils.Preferences.FirstRun);
+
+ let startupTimeout = 0;
+ fakeShowPolicyTimeout(
+ (callback, timeout) => (startupTimeout = timeout),
+ () => {}
+ );
+ TelemetryReportingPolicy.reset();
+
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored");
+ Assert.equal(
+ startupTimeout,
+ FIRST_RUN_TIMEOUT_MSEC,
+ "The infobar display timeout should be 60s on the first run."
+ );
+
+ // Run again, and check that we actually wait only 10 seconds.
+ TelemetryReportingPolicy.reset();
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored");
+ Assert.equal(
+ startupTimeout,
+ OTHER_RUNS_TIMEOUT_MSEC,
+ "The infobar display timeout should be 10s on other runs."
+ );
+ }
+);
+
+add_task(async function test_prefs() {
+ TelemetryReportingPolicy.reset();
+
+ let now = fakeNow(2009, 11, 18);
+
+ // If the date is not valid (earlier than 2012), we don't regard the policy as accepted.
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(!TelemetryReportingPolicy.testIsUserNotified());
+ Assert.equal(
+ Preferences.get(TelemetryUtils.Preferences.AcceptedPolicyDate, null),
+ 0,
+ "Invalid dates should not make the policy accepted."
+ );
+
+ // Check that the notification date and version are correctly saved to the prefs.
+ now = fakeNow(2012, 11, 18);
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.equal(
+ Preferences.get(TelemetryUtils.Preferences.AcceptedPolicyDate, null),
+ now.getTime(),
+ "A valid date must correctly be saved."
+ );
+
+ // Now that user is notified, check if we are allowed to upload.
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "We must be able to upload after the policy is accepted."
+ );
+
+ // Disable submission and check that we're no longer allowed to upload.
+ Preferences.set(TelemetryUtils.Preferences.DataSubmissionEnabled, false);
+ Assert.ok(
+ !TelemetryReportingPolicy.canUpload(),
+ "We must not be able to upload if data submission is disabled."
+ );
+
+ // Turn the submission back on.
+ Preferences.set(TelemetryUtils.Preferences.DataSubmissionEnabled, true);
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "We must be able to upload if data submission is enabled and the policy was accepted."
+ );
+
+ // Set a new minimum policy version and check that user is no longer notified.
+ let newMinimum =
+ Preferences.get(TelemetryUtils.Preferences.CurrentPolicyVersion, 1) + 1;
+ setMinimumPolicyVersion(newMinimum);
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "A greater minimum policy version must invalidate the policy and disable upload."
+ );
+
+ // Eventually accept the policy and make sure user is notified.
+ Preferences.set(TelemetryUtils.Preferences.CurrentPolicyVersion, newMinimum);
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ "Accepting the policy again should show the user as notified."
+ );
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "Accepting the policy again should let us upload data."
+ );
+
+ // Set a new, per channel, minimum policy version. Start by setting a test current channel.
+ let defaultPrefs = new Preferences({ defaultBranch: true });
+ defaultPrefs.set("app.update.channel", TEST_CHANNEL);
+
+ // Increase and set the new minimum version, then check that we're not notified anymore.
+ newMinimum++;
+ Preferences.set(PREF_MINIMUM_CHANNEL_POLICY_VERSION, newMinimum);
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "Increasing the minimum policy version should invalidate the policy."
+ );
+
+ // Eventually accept the policy and make sure user is notified.
+ Preferences.set(TelemetryUtils.Preferences.CurrentPolicyVersion, newMinimum);
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ "Accepting the policy again should show the user as notified."
+ );
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "Accepting the policy again should let us upload data."
+ );
+});
+
+add_task(async function test_migratePrefs() {
+ const DEPRECATED_FHR_PREFS = {
+ "datareporting.policy.dataSubmissionPolicyAccepted": true,
+ "datareporting.policy.dataSubmissionPolicyBypassAcceptance": true,
+ "datareporting.policy.dataSubmissionPolicyResponseType": "foxyeah",
+ "datareporting.policy.dataSubmissionPolicyResponseTime":
+ Date.now().toString(),
+ };
+
+ // Make sure the preferences are set before setting up the policy.
+ for (let name in DEPRECATED_FHR_PREFS) {
+ Preferences.set(name, DEPRECATED_FHR_PREFS[name]);
+ }
+ // Set up the policy.
+ TelemetryReportingPolicy.reset();
+ // They should have been removed by now.
+ for (let name in DEPRECATED_FHR_PREFS) {
+ Assert.ok(!Preferences.has(name), name + " should have been removed.");
+ }
+});
+
+add_task(async function test_userNotifiedOfCurrentPolicy() {
+ fakeResetAcceptedPolicy();
+ TelemetryReportingPolicy.reset();
+
+ // User should be reported as not notified by default.
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "The initial state should be unnotified."
+ );
+
+ // Forcing a policy version should not automatically make the user notified.
+ Preferences.set(
+ TelemetryUtils.Preferences.AcceptedPolicyVersion,
+ TelemetryReportingPolicy.DEFAULT_DATAREPORTING_POLICY_VERSION
+ );
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "The default state of the date should have a time of 0 and it should therefore fail"
+ );
+
+ // Showing the notification bar should make the user notified.
+ fakeNow(2012, 11, 11);
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ "Using the proper API causes user notification to report as true."
+ );
+
+ // It is assumed that later versions of the policy will incorporate previous
+ // ones, therefore this should also return true.
+ let newVersion =
+ Preferences.get(TelemetryUtils.Preferences.CurrentPolicyVersion, 1) + 1;
+ Preferences.set(TelemetryUtils.Preferences.AcceptedPolicyVersion, newVersion);
+ Assert.ok(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ "A future version of the policy should pass."
+ );
+
+ newVersion =
+ Preferences.get(TelemetryUtils.Preferences.CurrentPolicyVersion, 1) - 1;
+ Preferences.set(TelemetryUtils.Preferences.AcceptedPolicyVersion, newVersion);
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "A previous version of the policy should fail."
+ );
+});
+
+add_task(async function test_canSend() {
+ const TEST_PING_TYPE = "test-ping";
+
+ PingServer.start();
+ Preferences.set(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ await TelemetryController.testReset();
+ TelemetryReportingPolicy.reset();
+
+ // User should be reported as not notified by default.
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "The initial state should be unnotified."
+ );
+
+ // Assert if we receive any ping before the policy is accepted.
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Reset the ping handler.
+ PingServer.resetPingHandler();
+
+ // Fake the infobar: this should also trigger the ping send task.
+ TelemetryReportingPolicy.testInfobarShown();
+ let ping = await PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(
+ ping[0].type,
+ TEST_PING_TYPE,
+ "We should have received the previous ping."
+ );
+
+ // Submit another ping, to make sure it gets sent.
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Get the ping and check its type.
+ ping = await PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(
+ ping[0].type,
+ TEST_PING_TYPE,
+ "We should have received the new ping."
+ );
+
+ // Fake a restart with a pending ping.
+ await TelemetryController.addPendingPing(TEST_PING_TYPE, {});
+ await TelemetryController.testReset();
+
+ // We should be immediately sending the ping out.
+ ping = await PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(
+ ping[0].type,
+ TEST_PING_TYPE,
+ "We should have received the pending ping."
+ );
+
+ // Submit another ping, to make sure it gets sent.
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Get the ping and check its type.
+ ping = await PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(
+ ping[0].type,
+ TEST_PING_TYPE,
+ "We should have received the new ping."
+ );
+
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js
new file mode 100644
index 0000000000..46f1ca9058
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js
@@ -0,0 +1,1088 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+const UINT_SCALAR = "telemetry.test.unsigned_int_kind";
+const STRING_SCALAR = "telemetry.test.string_kind";
+const BOOLEAN_SCALAR = "telemetry.test.boolean_kind";
+const KEYED_UINT_SCALAR = "telemetry.test.keyed_unsigned_int";
+const KEYED_EXCEED_SCALAR = "telemetry.keyed_scalars_exceed_limit";
+
+function getProcessScalars(aProcessName, aKeyed = false, aClear = false) {
+ const scalars = aKeyed
+ ? Telemetry.getSnapshotForKeyedScalars("main", aClear)[aProcessName]
+ : Telemetry.getSnapshotForScalars("main", aClear)[aProcessName];
+ return scalars || {};
+}
+
+add_task(async function test_serializationFormat() {
+ Telemetry.clearScalars();
+
+ // Set the scalars to a known value.
+ const expectedUint = 3785;
+ const expectedString = "some value";
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.scalarSet(STRING_SCALAR, expectedString);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, true);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "first_key", 1234);
+
+ // Get a snapshot of the scalars for the main process (internally called "default").
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+
+ // Check that they are serialized to the correct format.
+ Assert.equal(
+ typeof scalars[UINT_SCALAR],
+ "number",
+ UINT_SCALAR + " must be serialized to the correct format."
+ );
+ Assert.ok(
+ Number.isInteger(scalars[UINT_SCALAR]),
+ UINT_SCALAR + " must be a finite integer."
+ );
+ Assert.equal(
+ scalars[UINT_SCALAR],
+ expectedUint,
+ UINT_SCALAR + " must have the correct value."
+ );
+ Assert.equal(
+ typeof scalars[STRING_SCALAR],
+ "string",
+ STRING_SCALAR + " must be serialized to the correct format."
+ );
+ Assert.equal(
+ scalars[STRING_SCALAR],
+ expectedString,
+ STRING_SCALAR + " must have the correct value."
+ );
+ Assert.equal(
+ typeof scalars[BOOLEAN_SCALAR],
+ "boolean",
+ BOOLEAN_SCALAR + " must be serialized to the correct format."
+ );
+ Assert.equal(
+ scalars[BOOLEAN_SCALAR],
+ true,
+ BOOLEAN_SCALAR + " must have the correct value."
+ );
+ Assert.ok(
+ !(KEYED_UINT_SCALAR in scalars),
+ "Keyed scalars must be reported in a separate section."
+ );
+});
+
+add_task(async function test_keyedSerializationFormat() {
+ Telemetry.clearScalars();
+
+ const expectedKey = "first_key";
+ const expectedOtherKey = "漢語";
+ const expectedUint = 3785;
+ const expectedOtherValue = 1107;
+
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, expectedKey, expectedUint);
+ Telemetry.keyedScalarSet(
+ KEYED_UINT_SCALAR,
+ expectedOtherKey,
+ expectedOtherValue
+ );
+
+ // Get a snapshot of the scalars.
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ Assert.ok(
+ !(UINT_SCALAR in keyedScalars),
+ UINT_SCALAR + " must not be serialized with the keyed scalars."
+ );
+ Assert.ok(
+ KEYED_UINT_SCALAR in keyedScalars,
+ KEYED_UINT_SCALAR + " must be serialized with the keyed scalars."
+ );
+ Assert.equal(
+ Object.keys(keyedScalars[KEYED_UINT_SCALAR]).length,
+ 2,
+ "The keyed scalar must contain exactly 2 keys."
+ );
+ Assert.ok(
+ expectedKey in keyedScalars[KEYED_UINT_SCALAR],
+ KEYED_UINT_SCALAR + " must contain the expected keys."
+ );
+ Assert.ok(
+ expectedOtherKey in keyedScalars[KEYED_UINT_SCALAR],
+ KEYED_UINT_SCALAR + " must contain the expected keys."
+ );
+ Assert.ok(
+ Number.isInteger(keyedScalars[KEYED_UINT_SCALAR][expectedKey]),
+ KEYED_UINT_SCALAR + "." + expectedKey + " must be a finite integer."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR][expectedKey],
+ expectedUint,
+ KEYED_UINT_SCALAR + "." + expectedKey + " must have the correct value."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR][expectedOtherKey],
+ expectedOtherValue,
+ KEYED_UINT_SCALAR + "." + expectedOtherKey + " must have the correct value."
+ );
+});
+
+add_task(async function test_nonexistingScalar() {
+ const NON_EXISTING_SCALAR = "telemetry.test.non_existing";
+
+ Telemetry.clearScalars();
+
+ // The JS API must not throw when used incorrectly but rather print
+ // a message to the console.
+ Telemetry.scalarAdd(NON_EXISTING_SCALAR, 11715);
+ Telemetry.scalarSet(NON_EXISTING_SCALAR, 11715);
+ Telemetry.scalarSetMaximum(NON_EXISTING_SCALAR, 11715);
+
+ // Make sure we do not throw on any operation for non-existing scalars.
+ Telemetry.keyedScalarAdd(NON_EXISTING_SCALAR, "some_key", 11715);
+ Telemetry.keyedScalarSet(NON_EXISTING_SCALAR, "some_key", 11715);
+ Telemetry.keyedScalarSetMaximum(NON_EXISTING_SCALAR, "some_key", 11715);
+
+ // Get a snapshot of the scalars.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+
+ Assert.ok(
+ !(NON_EXISTING_SCALAR in scalars),
+ "The non existing scalar must not be persisted."
+ );
+
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ Assert.ok(
+ !(NON_EXISTING_SCALAR in keyedScalars),
+ "The non existing keyed scalar must not be persisted."
+ );
+});
+
+add_task(async function test_expiredScalar() {
+ const EXPIRED_SCALAR = "telemetry.test.expired";
+ const EXPIRED_KEYED_SCALAR = "telemetry.test.keyed_expired";
+ const UNEXPIRED_SCALAR = "telemetry.test.unexpired";
+
+ Telemetry.clearScalars();
+
+ // Try to set the expired scalar to some value. We will not be recording the value,
+ // but we shouldn't throw.
+ Telemetry.scalarAdd(EXPIRED_SCALAR, 11715);
+ Telemetry.scalarSet(EXPIRED_SCALAR, 11715);
+ Telemetry.scalarSetMaximum(EXPIRED_SCALAR, 11715);
+ Telemetry.keyedScalarAdd(EXPIRED_KEYED_SCALAR, "some_key", 11715);
+ Telemetry.keyedScalarSet(EXPIRED_KEYED_SCALAR, "some_key", 11715);
+ Telemetry.keyedScalarSetMaximum(EXPIRED_KEYED_SCALAR, "some_key", 11715);
+
+ // The unexpired scalar has an expiration version, but far away in the future.
+ const expectedValue = 11716;
+ Telemetry.scalarSet(UNEXPIRED_SCALAR, expectedValue);
+
+ // Get a snapshot of the scalars.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent");
+
+ Assert.ok(
+ !(EXPIRED_SCALAR in scalars),
+ "The expired scalar must not be persisted."
+ );
+ Assert.equal(
+ scalars[UNEXPIRED_SCALAR],
+ expectedValue,
+ "The unexpired scalar must be persisted with the correct value."
+ );
+ Assert.ok(
+ !(EXPIRED_KEYED_SCALAR in keyedScalars),
+ "The expired keyed scalar must not be persisted."
+ );
+});
+
+add_task(async function test_unsignedIntScalar() {
+ let checkScalar = expectedValue => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars[UINT_SCALAR],
+ expectedValue,
+ UINT_SCALAR + " must contain the expected value."
+ );
+ };
+
+ Telemetry.clearScalars();
+
+ // Let's start with an accumulation without a prior set.
+ Telemetry.scalarAdd(UINT_SCALAR, 1);
+ Telemetry.scalarAdd(UINT_SCALAR, 2);
+ // Do we get what we expect?
+ checkScalar(3);
+
+ // Let's test setting the scalar to a value.
+ Telemetry.scalarSet(UINT_SCALAR, 3785);
+ checkScalar(3785);
+ Telemetry.scalarAdd(UINT_SCALAR, 1);
+ checkScalar(3786);
+
+ // Does setMaximum work?
+ Telemetry.scalarSet(UINT_SCALAR, 2);
+ checkScalar(2);
+ Telemetry.scalarSetMaximum(UINT_SCALAR, 5);
+ checkScalar(5);
+ // The value of the probe should still be 5, as the previous value
+ // is greater than the one we want to set.
+ Telemetry.scalarSetMaximum(UINT_SCALAR, 3);
+ checkScalar(5);
+
+ // Check that non-integer numbers get truncated and set.
+ Telemetry.scalarSet(UINT_SCALAR, 3.785);
+ checkScalar(3);
+
+ // Setting or adding a negative number must report an error through
+ // the console and drop the change (shouldn't throw).
+ Telemetry.scalarAdd(UINT_SCALAR, -5);
+ Telemetry.scalarSet(UINT_SCALAR, -5);
+ Telemetry.scalarSetMaximum(UINT_SCALAR, -1);
+ checkScalar(3);
+
+ // If we try to set a value of a different type, the JS API should not
+ // throw but rather print a console message.
+ Telemetry.scalarSet(UINT_SCALAR, 1);
+ Telemetry.scalarSet(UINT_SCALAR, "unexpected value");
+ Telemetry.scalarAdd(UINT_SCALAR, "unexpected value");
+ Telemetry.scalarSetMaximum(UINT_SCALAR, "unexpected value");
+ // The stored value must not be compromised.
+ checkScalar(1);
+});
+
+add_task(async function test_stringScalar() {
+ let checkExpectedString = expectedString => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars[STRING_SCALAR],
+ expectedString,
+ STRING_SCALAR + " must contain the expected string value."
+ );
+ };
+
+ Telemetry.clearScalars();
+
+ // Let's check simple strings...
+ let expected = "test string";
+ Telemetry.scalarSet(STRING_SCALAR, expected);
+ checkExpectedString(expected);
+ expected = "漢語";
+ Telemetry.scalarSet(STRING_SCALAR, expected);
+ checkExpectedString(expected);
+
+ // We have some unsupported operations for strings.
+ Telemetry.scalarAdd(STRING_SCALAR, 1);
+ Telemetry.scalarAdd(STRING_SCALAR, "string value");
+ Telemetry.scalarSetMaximum(STRING_SCALAR, 1);
+ Telemetry.scalarSetMaximum(STRING_SCALAR, "string value");
+ Telemetry.scalarSet(STRING_SCALAR, 1);
+
+ // Try to set the scalar to a string longer than the maximum length limit.
+ const LONG_STRING =
+ "browser.qaxfiuosnzmhlg.rpvxicawolhtvmbkpnludhedobxvkjwqyeyvmv";
+ Telemetry.scalarSet(STRING_SCALAR, LONG_STRING);
+ checkExpectedString(LONG_STRING.substr(0, 50));
+});
+
+add_task(async function test_booleanScalar() {
+ let checkExpectedBool = expectedBoolean => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars[BOOLEAN_SCALAR],
+ expectedBoolean,
+ BOOLEAN_SCALAR + " must contain the expected boolean value."
+ );
+ };
+
+ Telemetry.clearScalars();
+
+ // Set a test boolean value.
+ let expected = false;
+ Telemetry.scalarSet(BOOLEAN_SCALAR, expected);
+ checkExpectedBool(expected);
+ expected = true;
+ Telemetry.scalarSet(BOOLEAN_SCALAR, expected);
+ checkExpectedBool(expected);
+
+ // Check that setting a numeric value implicitly converts to boolean.
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 1);
+ checkExpectedBool(true);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 0);
+ checkExpectedBool(false);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 1.0);
+ checkExpectedBool(true);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 0.0);
+ checkExpectedBool(false);
+
+ // Check that unsupported operations for booleans do not throw.
+ Telemetry.scalarAdd(BOOLEAN_SCALAR, 1);
+ Telemetry.scalarAdd(BOOLEAN_SCALAR, "string value");
+ Telemetry.scalarSetMaximum(BOOLEAN_SCALAR, 1);
+ Telemetry.scalarSetMaximum(BOOLEAN_SCALAR, "string value");
+ Telemetry.scalarSet(BOOLEAN_SCALAR, "true");
+});
+
+add_task(async function test_scalarRecording() {
+ const OPTIN_SCALAR = "telemetry.test.release_optin";
+ const OPTOUT_SCALAR = "telemetry.test.release_optout";
+
+ let checkValue = (scalarName, expectedValue) => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars[scalarName],
+ expectedValue,
+ scalarName + " must contain the expected value."
+ );
+ };
+
+ let checkNotSerialized = scalarName => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.ok(!(scalarName in scalars), scalarName + " was not recorded.");
+ };
+
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+ Telemetry.clearScalars();
+
+ // Check that no scalar is recorded if both base and extended recording are off.
+ Telemetry.scalarSet(OPTOUT_SCALAR, 3);
+ Telemetry.scalarSet(OPTIN_SCALAR, 3);
+ checkNotSerialized(OPTOUT_SCALAR);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that opt-out scalars are recorded, while opt-in are not.
+ Telemetry.canRecordBase = true;
+ Telemetry.scalarSet(OPTOUT_SCALAR, 3);
+ Telemetry.scalarSet(OPTIN_SCALAR, 3);
+ checkValue(OPTOUT_SCALAR, 3);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that both opt-out and opt-in scalars are recorded.
+ Telemetry.canRecordExtended = true;
+ Telemetry.scalarSet(OPTOUT_SCALAR, 5);
+ Telemetry.scalarSet(OPTIN_SCALAR, 6);
+ checkValue(OPTOUT_SCALAR, 5);
+ checkValue(OPTIN_SCALAR, 6);
+});
+
+add_task(async function test_keyedScalarRecording() {
+ const OPTIN_SCALAR = "telemetry.test.keyed_release_optin";
+ const OPTOUT_SCALAR = "telemetry.test.keyed_release_optout";
+ const testKey = "policy_key";
+
+ let checkValue = (scalarName, expectedValue) => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.equal(
+ scalars[scalarName][testKey],
+ expectedValue,
+ scalarName + " must contain the expected value."
+ );
+ };
+
+ let checkNotSerialized = scalarName => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.ok(!(scalarName in scalars), scalarName + " was not recorded.");
+ };
+
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+ Telemetry.clearScalars();
+
+ // Check that no scalar is recorded if both base and extended recording are off.
+ Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 3);
+ Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 3);
+ checkNotSerialized(OPTOUT_SCALAR);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that opt-out scalars are recorded, while opt-in are not.
+ Telemetry.canRecordBase = true;
+ Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 3);
+ Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 3);
+ checkValue(OPTOUT_SCALAR, 3);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that both opt-out and opt-in scalars are recorded.
+ Telemetry.canRecordExtended = true;
+ Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 5);
+ Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 6);
+ checkValue(OPTOUT_SCALAR, 5);
+ checkValue(OPTIN_SCALAR, 6);
+});
+
+add_task(async function test_subsession() {
+ Telemetry.clearScalars();
+
+ // Set the scalars to a known value.
+ Telemetry.scalarSet(UINT_SCALAR, 3785);
+ Telemetry.scalarSet(STRING_SCALAR, "some value");
+ Telemetry.scalarSet(BOOLEAN_SCALAR, false);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "some_random_key", 12);
+
+ // Get a snapshot and reset the subsession. The value we set must be there.
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ let keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+
+ Assert.equal(
+ scalars[UINT_SCALAR],
+ 3785,
+ UINT_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ scalars[STRING_SCALAR],
+ "some value",
+ STRING_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ scalars[BOOLEAN_SCALAR],
+ false,
+ BOOLEAN_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR].some_random_key,
+ 12,
+ KEYED_UINT_SCALAR + " must contain the expected value."
+ );
+
+ // Get a new snapshot and reset the subsession again. Since no new value
+ // was set, the scalars should not be reported.
+ scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+
+ Assert.ok(
+ !(UINT_SCALAR in scalars),
+ UINT_SCALAR + " must be empty and not reported."
+ );
+ Assert.ok(
+ !(STRING_SCALAR in scalars),
+ STRING_SCALAR + " must be empty and not reported."
+ );
+ Assert.ok(
+ !(BOOLEAN_SCALAR in scalars),
+ BOOLEAN_SCALAR + " must be empty and not reported."
+ );
+ Assert.ok(
+ !(KEYED_UINT_SCALAR in keyedScalars),
+ KEYED_UINT_SCALAR + " must be empty and not reported."
+ );
+});
+
+add_task(async function test_keyed_uint() {
+ Telemetry.clearScalars();
+
+ const KEYS = ["a_key", "another_key", "third_key"];
+ let expectedValues = [1, 1, 1];
+
+ // Set all the keys to a baseline value.
+ for (let key of KEYS) {
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, key, 1);
+ }
+
+ // Increment only one key.
+ Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, KEYS[1], 1);
+ expectedValues[1]++;
+
+ // Use SetMaximum on the third key.
+ Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, KEYS[2], 37);
+ expectedValues[2] = 37;
+
+ // Get a snapshot of the scalars and make sure the keys contain
+ // the correct values.
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ for (let k = 0; k < 3; k++) {
+ const keyName = KEYS[k];
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR][keyName],
+ expectedValues[k],
+ KEYED_UINT_SCALAR + "." + keyName + " must contain the correct value."
+ );
+ }
+
+ // Do not throw when doing unsupported things on uint keyed scalars.
+ // Just test one single unsupported operation, the other are covered in the plain
+ // unsigned scalar test.
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "new_key", "unexpected value");
+});
+
+add_task(async function test_keyed_boolean() {
+ Telemetry.clearScalars();
+
+ const KEYED_BOOLEAN_TYPE = "telemetry.test.keyed_boolean_kind";
+ const first_key = "first_key";
+ const second_key = "second_key";
+
+ // Set the initial values.
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, first_key, true);
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, second_key, false);
+
+ // Get a snapshot of the scalars and make sure the keys contain
+ // the correct values.
+ let keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.equal(
+ keyedScalars[KEYED_BOOLEAN_TYPE][first_key],
+ true,
+ "The key must contain the expected value."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_BOOLEAN_TYPE][second_key],
+ false,
+ "The key must contain the expected value."
+ );
+
+ // Now flip the values and make sure we get the expected values back.
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, first_key, false);
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, second_key, true);
+
+ keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.equal(
+ keyedScalars[KEYED_BOOLEAN_TYPE][first_key],
+ false,
+ "The key must contain the expected value."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_BOOLEAN_TYPE][second_key],
+ true,
+ "The key must contain the expected value."
+ );
+
+ // Do not throw when doing unsupported things on a boolean keyed scalars.
+ // Just test one single unsupported operation, the other are covered in the plain
+ // boolean scalar test.
+ Telemetry.keyedScalarAdd(KEYED_BOOLEAN_TYPE, "somehey", 1);
+});
+
+add_task(async function test_keyed_keys_length() {
+ Telemetry.clearScalars();
+
+ const LONG_KEY_STRING =
+ "browser.qaxfiuosnzmhlg.rpvxicawolhtvmbkpnludhedobxvkjwqyeyvmv.somemoresowereach70chars";
+ const NORMAL_KEY = "a_key";
+
+ // Set the value for a key within the length limits.
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, NORMAL_KEY, 1);
+
+ // Now try to set and modify the value for a very long key (must not throw).
+ Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, LONG_KEY_STRING, 10);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, LONG_KEY_STRING, 1);
+ Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, LONG_KEY_STRING, 10);
+
+ // Also attempt to set the value for an empty key.
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "", 1);
+
+ // Make sure the key with the right length contains the expected value.
+ let keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.equal(
+ Object.keys(keyedScalars[KEYED_UINT_SCALAR]).length,
+ 1,
+ "The keyed scalar must contain exactly 1 key."
+ );
+ Assert.ok(
+ NORMAL_KEY in keyedScalars[KEYED_UINT_SCALAR],
+ "The keyed scalar must contain the expected key."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR][NORMAL_KEY],
+ 1,
+ "The key must contain the expected value."
+ );
+ Assert.ok(
+ !(LONG_KEY_STRING in keyedScalars[KEYED_UINT_SCALAR]),
+ "The data for the long key should not have been recorded."
+ );
+ Assert.ok(
+ !("" in keyedScalars[KEYED_UINT_SCALAR]),
+ "The data for the empty key should not have been recorded."
+ );
+});
+
+add_task(async function test_keyed_max_keys() {
+ Telemetry.clearScalars();
+
+ // Generate the names for the first 100 keys.
+ let keyNamesSet = new Set();
+ for (let k = 0; k < 100; k++) {
+ keyNamesSet.add("key_" + k);
+ }
+
+ // Add 100 keys to an histogram and set their initial value.
+ let valueToSet = 0;
+ keyNamesSet.forEach(keyName => {
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, keyName, valueToSet++);
+ });
+
+ // Perform some operations on the 101th key. This should throw, as
+ // we're not allowed to have more than 100 keys.
+ const LAST_KEY_NAME = "overflowing_key";
+ Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, LAST_KEY_NAME, 10);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, LAST_KEY_NAME, 1);
+ Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, LAST_KEY_NAME, 10);
+
+ // Make sure all the keys except the last one are available and have the correct
+ // values.
+ let keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ // Check that the keyed scalar only contain the first 100 keys.
+ const reportedKeysSet = new Set(Object.keys(keyedScalars[KEYED_UINT_SCALAR]));
+ Assert.ok(
+ [...keyNamesSet].filter(x => reportedKeysSet.has(x)) &&
+ [...reportedKeysSet].filter(x => keyNamesSet.has(x)),
+ "The keyed scalar must contain all the 100 keys, and drop the others."
+ );
+
+ // Check that all the keys recorded the expected values.
+ let expectedValue = 0;
+ keyNamesSet.forEach(keyName => {
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR][keyName],
+ expectedValue++,
+ "The key must contain the expected value."
+ );
+ });
+
+ // Check that KEYED_EXCEED_SCALAR is in keyedScalars
+ Assert.ok(
+ KEYED_EXCEED_SCALAR in keyedScalars,
+ "We have exceeded maximum number of Keys."
+ );
+
+ // Generate the names for the exceeded keys
+ let keyNamesSet2 = new Set();
+ for (let k = 0; k < 100; k++) {
+ keyNamesSet2.add("key2_" + k);
+ }
+
+ // Add 100 keys to the keyed exceed scalar and set their initial value.
+ valueToSet = 0;
+ keyNamesSet2.forEach(keyName2 => {
+ Telemetry.keyedScalarSet(KEYED_EXCEED_SCALAR, keyName2, valueToSet++);
+ });
+
+ // Check that there are exactly 100 keys in KEYED_EXCEED_SCALAR
+ let snapshot = Telemetry.getSnapshotForKeyedScalars("main", false);
+ Assert.equal(
+ 100,
+ Object.keys(snapshot.parent[KEYED_UINT_SCALAR]).length,
+ "The keyed scalar must contain all the 100 keys."
+ );
+
+ // Check that KEYED_UINT_SCALAR is in keyedScalars and its value equals 3
+ Assert.ok(
+ KEYED_UINT_SCALAR in keyedScalars[KEYED_EXCEED_SCALAR],
+ "The keyed Scalar is in the keyed exceeded scalar"
+ );
+ Assert.equal(
+ keyedScalars[KEYED_EXCEED_SCALAR][KEYED_UINT_SCALAR],
+ 3,
+ "We have exactly 3 keys over the limit"
+ );
+});
+
+add_task(async function test_dynamicScalars_registration() {
+ Telemetry.clearScalars();
+
+ const TEST_CASES = [
+ {
+ category: "telemetry.test",
+ data: {
+ missing_kind: {
+ keyed: false,
+ record_on_release: true,
+ },
+ },
+ evaluation: /missing 'kind'/,
+ description: "Registration must fail if required fields are missing",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_collection: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: "opt-in",
+ },
+ },
+ evaluation: /Invalid 'record_on_release'/,
+ description:
+ "Registration must fail if 'record_on_release' is of the wrong type",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_kind: {
+ kind: "12",
+ },
+ },
+ evaluation: /Invalid or missing 'kind'/,
+ description: "Registration must fail if 'kind' is of the wrong type",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_expired: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ expired: "never",
+ },
+ },
+ evaluation: /Invalid 'expired'/,
+ description: "Registration must fail if 'expired' is of the wrong type",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ valid_scalar: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ record_on_release: true,
+ },
+ invalid_scalar: {
+ expired: false,
+ },
+ },
+ evaluation: /Invalid or missing 'kind'/,
+ description:
+ "No scalar must be registered if the batch contains an invalid one",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_stores: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ stores: true,
+ },
+ },
+ evaluation: /Invalid 'stores'/,
+ description: "Registration must fail if 'stores' is of the wrong type",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_stores: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ stores: {},
+ },
+ },
+ evaluation: /Invalid 'stores'/,
+ description: "Registration must fail if 'stores' is of the wrong type",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_stores: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ stores: [{}],
+ },
+ },
+ evaluation: /'stores' array isn't a string./,
+ description:
+ "Registration must fail if element in 'stores' is of the wrong type",
+ },
+ ];
+
+ for (let testCase of TEST_CASES) {
+ Assert.throws(
+ () => Telemetry.registerScalars(testCase.category, testCase.data),
+ testCase.evaluation,
+ testCase.description
+ );
+ }
+});
+
+add_task(async function test_dynamicScalars_doubleRegistration() {
+ Telemetry.clearScalars();
+
+ // Register a test scalar.
+ Telemetry.registerScalars("telemetry.test.dynamic", {
+ double_registration_1: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ },
+ });
+
+ // Verify that we can record the scalar.
+ Telemetry.scalarSet("telemetry.test.dynamic.double_registration_1", 1);
+
+ // Register the same scalar again, along with a second scalar.
+ // This must not throw.
+ Telemetry.registerScalars("telemetry.test.dynamic", {
+ double_registration_1: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ },
+ double_registration_2: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ },
+ });
+
+ // Set the dynamic scalars to some test values.
+ Telemetry.scalarAdd("telemetry.test.dynamic.double_registration_1", 1);
+ Telemetry.scalarSet("telemetry.test.dynamic.double_registration_2", 3);
+
+ // Get a snapshot of the scalars and check that the dynamic ones were correctly set.
+ let scalars = getProcessScalars("dynamic", false, false);
+
+ Assert.equal(
+ scalars["telemetry.test.dynamic.double_registration_1"],
+ 2,
+ "The recorded scalar must contain the right value."
+ );
+ Assert.equal(
+ scalars["telemetry.test.dynamic.double_registration_2"],
+ 3,
+ "The recorded scalar must contain the right value."
+ );
+
+ // Register an existing scalar again, only change the definition
+ // to make it expire.
+ Telemetry.registerScalars("telemetry.test.dynamic", {
+ double_registration_2: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ expired: true,
+ },
+ });
+
+ // Attempt to record and make sure that no recording happens.
+ Telemetry.scalarAdd("telemetry.test.dynamic.double_registration_2", 1);
+ scalars = getProcessScalars("dynamic", false, false);
+ Assert.equal(
+ scalars["telemetry.test.dynamic.double_registration_2"],
+ 3,
+ "The recorded scalar must contain the right value."
+ );
+});
+
+add_task(async function test_dynamicScalars_recording() {
+ Telemetry.clearScalars();
+
+ // Disable extended recording so that we will just record opt-out.
+ Telemetry.canRecordExtended = false;
+
+ // Register some test scalars.
+ Telemetry.registerScalars("telemetry.test.dynamic", {
+ record_optout: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ },
+ record_keyed: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: true,
+ record_on_release: true,
+ },
+ record_optin: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_BOOLEAN,
+ record_on_release: false,
+ },
+ record_expired: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_STRING,
+ expired: true,
+ record_on_release: true,
+ },
+ });
+
+ // Set the dynamic scalars to some test values.
+ Telemetry.scalarSet("telemetry.test.dynamic.record_optout", 1);
+ Telemetry.keyedScalarSet("telemetry.test.dynamic.record_keyed", "someKey", 5);
+ Telemetry.scalarSet("telemetry.test.dynamic.record_optin", false);
+ Telemetry.scalarSet("telemetry.test.dynamic.record_expired", "test");
+
+ // Get a snapshot of the scalars and check that the dynamic ones were correctly set.
+ let scalars = getProcessScalars("dynamic", false, false);
+ let keyedScalars = getProcessScalars("dynamic", true, true);
+
+ Assert.ok(
+ !("telemetry.test.dynamic.record_optin" in scalars),
+ "Dynamic opt-in scalars must not be recorded."
+ );
+ Assert.ok(
+ "telemetry.test.dynamic.record_keyed" in keyedScalars,
+ "Dynamic opt-out keyed scalars must be recorded."
+ );
+ Assert.ok(
+ !("telemetry.test.dynamic.record_expired" in scalars),
+ "Dynamic expired scalars must not be recorded."
+ );
+ Assert.ok(
+ "telemetry.test.dynamic.record_optout" in scalars,
+ "Dynamic opt-out scalars must be recorded."
+ );
+ Assert.equal(
+ scalars["telemetry.test.dynamic.record_optout"],
+ 1,
+ "The recorded scalar must contain the right value."
+ );
+ Assert.equal(
+ keyedScalars["telemetry.test.dynamic.record_keyed"].someKey,
+ 5,
+ "The recorded keyed scalar must contain the right value."
+ );
+
+ // Enable extended recording.
+ Telemetry.canRecordExtended = true;
+
+ // Set the dynamic scalars to some test values.
+ Telemetry.scalarSet("telemetry.test.dynamic.record_optin", true);
+ Telemetry.scalarSet("telemetry.test.dynamic.record_expired", "test");
+
+ // Get a snapshot of the scalars and check that the dynamic ones were correctly set.
+ scalars = getProcessScalars("dynamic", false, true);
+
+ Assert.ok(
+ !("telemetry.test.dynamic.record_expired" in scalars),
+ "Dynamic expired scalars must not be recorded."
+ );
+ Assert.ok(
+ "telemetry.test.dynamic.record_optin" in scalars,
+ "Dynamic opt-in scalars must be recorded."
+ );
+ Assert.equal(
+ scalars["telemetry.test.dynamic.record_optin"],
+ true,
+ "The recorded scalar must contain the right value."
+ );
+});
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_productSpecificScalar() {
+ const DEFAULT_PRODUCT_SCALAR = "telemetry.test.default_products";
+ const DESKTOP_ONLY_SCALAR = "telemetry.test.desktop_only";
+ const MULTIPRODUCT_SCALAR = "telemetry.test.multiproduct";
+ const MOBILE_ONLY_SCALAR = "telemetry.test.mobile_only";
+ const MOBILE_ONLY_KEYED_SCALAR = "telemetry.test.keyed_mobile_only";
+
+ Telemetry.clearScalars();
+
+ // Try to set the desktop scalars
+ let expectedValue = 11714;
+ Telemetry.scalarAdd(DEFAULT_PRODUCT_SCALAR, expectedValue);
+ Telemetry.scalarAdd(DESKTOP_ONLY_SCALAR, expectedValue);
+ Telemetry.scalarAdd(MULTIPRODUCT_SCALAR, expectedValue);
+
+ // Try to set the mobile-only scalar to some value. We will not be recording the value,
+ // but we shouldn't throw.
+ let expectedKey = "some_key";
+ Telemetry.scalarSet(MOBILE_ONLY_SCALAR, 11715);
+ Telemetry.scalarSetMaximum(MOBILE_ONLY_SCALAR, 11715);
+ Telemetry.keyedScalarAdd(MOBILE_ONLY_KEYED_SCALAR, expectedKey, 11715);
+ Telemetry.keyedScalarSet(MOBILE_ONLY_KEYED_SCALAR, expectedKey, 11715);
+ Telemetry.keyedScalarSetMaximum(
+ MOBILE_ONLY_KEYED_SCALAR,
+ expectedKey,
+ 11715
+ );
+
+ // Get a snapshot of the scalars.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent");
+
+ Assert.equal(
+ scalars[DEFAULT_PRODUCT_SCALAR],
+ expectedValue,
+ "The default platfomrs scalar must contain the right value"
+ );
+ Assert.equal(
+ scalars[DESKTOP_ONLY_SCALAR],
+ expectedValue,
+ "The desktop-only scalar must contain the right value"
+ );
+ Assert.equal(
+ scalars[MULTIPRODUCT_SCALAR],
+ expectedValue,
+ "The multiproduct scalar must contain the right value"
+ );
+
+ Assert.ok(
+ !(MOBILE_ONLY_SCALAR in scalars),
+ "The mobile-only scalar must not be persisted."
+ );
+ Assert.ok(
+ !(MOBILE_ONLY_KEYED_SCALAR in keyedScalars),
+ "The mobile-only keyed scalar must not be persisted."
+ );
+ }
+);
+
+add_task(
+ {
+ skip_if: () => !gIsAndroid,
+ },
+ async function test_mobileSpecificScalar() {
+ const DEFAULT_PRODUCT_SCALAR = "telemetry.test.default_products";
+ const DESKTOP_ONLY_SCALAR = "telemetry.test.desktop_only";
+ const DESKTOP_ONLY_KEYED_SCALAR = "telemetry.test.keyed_desktop_only";
+ const MULTIPRODUCT_SCALAR = "telemetry.test.multiproduct";
+ const MOBILE_ONLY_SCALAR = "telemetry.test.mobile_only";
+ const MOBILE_ONLY_KEYED_SCALAR = "telemetry.test.keyed_mobile_only";
+
+ Telemetry.clearScalars();
+
+ // Try to set the mobile and multiproduct scalars
+ let expectedValue = 11714;
+ let expectedKey = "some_key";
+ Telemetry.scalarAdd(DEFAULT_PRODUCT_SCALAR, expectedValue);
+ Telemetry.scalarAdd(MOBILE_ONLY_SCALAR, expectedValue);
+ Telemetry.keyedScalarSet(
+ MOBILE_ONLY_KEYED_SCALAR,
+ expectedKey,
+ expectedValue
+ );
+ Telemetry.scalarAdd(MULTIPRODUCT_SCALAR, expectedValue);
+
+ // Try to set the desktop-only scalar to some value. We will not be recording the value,
+ // but we shouldn't throw.
+ Telemetry.scalarSet(DESKTOP_ONLY_SCALAR, 11715);
+ Telemetry.scalarSetMaximum(DESKTOP_ONLY_SCALAR, 11715);
+ Telemetry.keyedScalarAdd(DESKTOP_ONLY_KEYED_SCALAR, expectedKey, 11715);
+ Telemetry.keyedScalarSet(DESKTOP_ONLY_KEYED_SCALAR, expectedKey, 11715);
+ Telemetry.keyedScalarSetMaximum(
+ DESKTOP_ONLY_KEYED_SCALAR,
+ expectedKey,
+ 11715
+ );
+
+ // Get a snapshot of the scalars.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ Assert.equal(
+ scalars[DEFAULT_PRODUCT_SCALAR],
+ expectedValue,
+ "The default products scalar must contain the right value"
+ );
+ Assert.equal(
+ scalars[MOBILE_ONLY_SCALAR],
+ expectedValue,
+ "The mobile-only scalar must contain the right value"
+ );
+ Assert.equal(
+ keyedScalars[MOBILE_ONLY_KEYED_SCALAR][expectedKey],
+ expectedValue,
+ "The mobile-only keyed scalar must contain the right value"
+ );
+ Assert.equal(
+ scalars[MULTIPRODUCT_SCALAR],
+ expectedValue,
+ "The multiproduct scalar must contain the right value"
+ );
+
+ Assert.ok(
+ !(DESKTOP_ONLY_SCALAR in scalars),
+ "The desktop-only scalar must not be persisted."
+ );
+ Assert.ok(
+ !(DESKTOP_ONLY_KEYED_SCALAR in keyedScalars),
+ "The desktop-only keyed scalar must not be persisted."
+ );
+ }
+);
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_buildFaster.js b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_buildFaster.js
new file mode 100644
index 0000000000..909025b91a
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_buildFaster.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+/**
+ * Return the path to the definitions file for the scalars.
+ */
+function getDefinitionsPath() {
+ // Write the scalar definition to the spec file in the binary directory.
+ let definitionFile = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ definitionFile = Services.dirsvc.get("GreD", Ci.nsIFile);
+ definitionFile.append("ScalarArtifactDefinitions.json");
+ return definitionFile.path;
+}
+
+add_task(async function test_setup() {
+ do_get_profile();
+});
+
+add_task(
+ {
+ // The test needs to write a file, and that fails in tests on Android.
+ // We don't really need the Android coverage, so skip on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_invalidJSON() {
+ const INVALID_JSON = "{ invalid,JSON { {1}";
+ const FILE_PATH = getDefinitionsPath();
+
+ // Write a corrupted JSON file.
+ await IOUtils.writeUTF8(FILE_PATH, INVALID_JSON, {
+ mode: "overwrite",
+ });
+
+ // Simulate Firefox startup. This should not throw!
+ await TelemetryController.testSetup();
+ await TelemetryController.testPromiseJsProbeRegistration();
+
+ // Cleanup.
+ await TelemetryController.testShutdown();
+ await IOUtils.remove(FILE_PATH);
+ }
+);
+
+add_task(
+ {
+ // The test needs to write a file, and that fails in tests on Android.
+ // We don't really need the Android coverage, so skip on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_dynamicBuiltin() {
+ const DYNAMIC_SCALAR_SPEC = {
+ "telemetry.test": {
+ builtin_dynamic: {
+ kind: "nsITelemetry::SCALAR_TYPE_COUNT",
+ expires: "never",
+ record_on_release: false,
+ keyed: false,
+ },
+ builtin_dynamic_other: {
+ kind: "nsITelemetry::SCALAR_TYPE_BOOLEAN",
+ expires: "never",
+ record_on_release: false,
+ keyed: false,
+ },
+ builtin_dynamic_expired: {
+ kind: "nsITelemetry::SCALAR_TYPE_BOOLEAN",
+ expires: AppConstants.MOZ_APP_VERSION,
+ record_on_release: false,
+ keyed: false,
+ },
+ builtin_dynamic_multi: {
+ kind: "nsITelemetry::SCALAR_TYPE_COUNT",
+ expired: false,
+ record_on_release: false,
+ keyed: false,
+ stores: ["main", "sync"],
+ },
+ builtin_dynamic_sync_only: {
+ kind: "nsITelemetry::SCALAR_TYPE_COUNT",
+ expired: false,
+ record_on_release: false,
+ keyed: false,
+ stores: ["sync"],
+ },
+ },
+ };
+
+ Telemetry.clearScalars();
+
+ // Let's write to the definition file to also cover the file
+ // loading part.
+ const FILE_PATH = getDefinitionsPath();
+ await IOUtils.writeJSON(FILE_PATH, DYNAMIC_SCALAR_SPEC);
+
+ // Start TelemetryController to trigger loading the specs.
+ await TelemetryController.testReset();
+ await TelemetryController.testPromiseJsProbeRegistration();
+
+ // Store to that scalar.
+ const TEST_SCALAR1 = "telemetry.test.builtin_dynamic";
+ const TEST_SCALAR2 = "telemetry.test.builtin_dynamic_other";
+ const TEST_SCALAR3 = "telemetry.test.builtin_dynamic_multi";
+ const TEST_SCALAR4 = "telemetry.test.builtin_dynamic_sync_only";
+ const TEST_SCALAR5 = "telemetry.test.builtin_dynamic_expired";
+ Telemetry.scalarSet(TEST_SCALAR1, 3785);
+ Telemetry.scalarSet(TEST_SCALAR2, true);
+ Telemetry.scalarSet(TEST_SCALAR3, 1337);
+ Telemetry.scalarSet(TEST_SCALAR4, 31337);
+ Telemetry.scalarSet(TEST_SCALAR5, true);
+
+ // Check the values we tried to store.
+ const scalars = Telemetry.getSnapshotForScalars("main", false).parent;
+ const syncScalars = Telemetry.getSnapshotForScalars("sync", false).parent;
+
+ // Check that they are serialized to the correct format.
+ Assert.equal(
+ typeof scalars[TEST_SCALAR1],
+ "number",
+ TEST_SCALAR1 + " must be serialized to the correct format."
+ );
+ Assert.ok(
+ Number.isInteger(scalars[TEST_SCALAR1]),
+ TEST_SCALAR1 + " must be a finite integer."
+ );
+ Assert.equal(
+ scalars[TEST_SCALAR1],
+ 3785,
+ TEST_SCALAR1 + " must have the correct value."
+ );
+ Assert.equal(
+ typeof scalars[TEST_SCALAR2],
+ "boolean",
+ TEST_SCALAR2 + " must be serialized to the correct format."
+ );
+ Assert.equal(
+ scalars[TEST_SCALAR2],
+ true,
+ TEST_SCALAR2 + " must have the correct value."
+ );
+
+ Assert.equal(
+ typeof scalars[TEST_SCALAR3],
+ "number",
+ `${TEST_SCALAR3} must be serialized to the correct format.`
+ );
+ Assert.equal(
+ scalars[TEST_SCALAR3],
+ 1337,
+ `${TEST_SCALAR3} must have the correct value.`
+ );
+ Assert.equal(
+ typeof syncScalars[TEST_SCALAR3],
+ "number",
+ `${TEST_SCALAR3} must be serialized in the sync store to the correct format.`
+ );
+ Assert.equal(
+ syncScalars[TEST_SCALAR3],
+ 1337,
+ `${TEST_SCALAR3} must have the correct value in the sync snapshot.`
+ );
+
+ Assert.ok(
+ !(TEST_SCALAR4 in scalars),
+ `${TEST_SCALAR4} must not be in the main store.`
+ );
+ Assert.equal(
+ typeof syncScalars[TEST_SCALAR4],
+ "number",
+ `${TEST_SCALAR4} must be in the sync snapshot.`
+ );
+ Assert.equal(
+ syncScalars[TEST_SCALAR4],
+ 31337,
+ `${TEST_SCALAR4} must have the correct value.`
+ );
+
+ // Clean up.
+ await TelemetryController.testShutdown();
+ await IOUtils.remove(FILE_PATH);
+ }
+);
+
+add_task(async function test_keyedDynamicBuiltin() {
+ Telemetry.clearScalars();
+
+ // Register the built-in scalars (let's not take the I/O hit).
+ Telemetry.registerBuiltinScalars("telemetry.test", {
+ builtin_dynamic_keyed: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ expired: false,
+ record_on_release: false,
+ keyed: true,
+ },
+ });
+
+ // Store to that scalar.
+ const TEST_SCALAR1 = "telemetry.test.builtin_dynamic_keyed";
+ Telemetry.keyedScalarSet(TEST_SCALAR1, "test-key", 3785);
+
+ // Check the values we tried to store.
+ const scalars = Telemetry.getSnapshotForKeyedScalars("main", false).parent;
+
+ // Check that they are serialized to the correct format.
+ Assert.equal(
+ typeof scalars[TEST_SCALAR1],
+ "object",
+ TEST_SCALAR1 + " must be a keyed scalar."
+ );
+ Assert.equal(
+ typeof scalars[TEST_SCALAR1]["test-key"],
+ "number",
+ TEST_SCALAR1 + " must be serialized to the correct format."
+ );
+ Assert.ok(
+ Number.isInteger(scalars[TEST_SCALAR1]["test-key"]),
+ TEST_SCALAR1 + " must be a finite integer."
+ );
+ Assert.equal(
+ scalars[TEST_SCALAR1]["test-key"],
+ 3785,
+ TEST_SCALAR1 + " must have the correct value."
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_impressionId.js b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_impressionId.js
new file mode 100644
index 0000000000..8717d34501
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_impressionId.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const CATEGORY = "telemetry.test";
+const MAIN_ONLY = `${CATEGORY}.main_only`;
+const IMPRESSION_ID_ONLY = `${CATEGORY}.impression_id_only`;
+
+add_task(async function test_multistore_basics() {
+ Telemetry.clearScalars();
+
+ const expectedUint = 3785;
+ const expectedString = "{some_impression_id}";
+ Telemetry.scalarSet(MAIN_ONLY, expectedUint);
+ Telemetry.scalarSet(IMPRESSION_ID_ONLY, expectedString);
+
+ const mainScalars = Telemetry.getSnapshotForScalars("main").parent;
+ const impressionIdScalars =
+ Telemetry.getSnapshotForScalars("deletion-request").parent;
+
+ Assert.ok(
+ MAIN_ONLY in mainScalars,
+ `Main-store scalar ${MAIN_ONLY} must be in main snapshot.`
+ );
+ Assert.ok(
+ !(MAIN_ONLY in impressionIdScalars),
+ `Main-store scalar ${MAIN_ONLY} must not be in deletion-request snapshot.`
+ );
+ Assert.equal(
+ mainScalars[MAIN_ONLY],
+ expectedUint,
+ `Main-store scalar ${MAIN_ONLY} must have correct value.`
+ );
+
+ Assert.ok(
+ IMPRESSION_ID_ONLY in impressionIdScalars,
+ `Deletion-request store scalar ${IMPRESSION_ID_ONLY} must be in deletion-request snapshot.`
+ );
+ Assert.ok(
+ !(IMPRESSION_ID_ONLY in mainScalars),
+ `Deletion-request scalar ${IMPRESSION_ID_ONLY} must not be in main snapshot.`
+ );
+ Assert.equal(
+ impressionIdScalars[IMPRESSION_ID_ONLY],
+ expectedString,
+ `Deletion-request store scalar ${IMPRESSION_ID_ONLY} must have correct value.`
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_multistore.js b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_multistore.js
new file mode 100644
index 0000000000..841caa4f1d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_multistore.js
@@ -0,0 +1,415 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const CATEGORY = "telemetry.test";
+const MAIN_ONLY = `${CATEGORY}.main_only`;
+const SYNC_ONLY = `${CATEGORY}.sync_only`;
+const MULTIPLE_STORES = `${CATEGORY}.multiple_stores`;
+const MULTIPLE_STORES_STRING = `${CATEGORY}.multiple_stores_string`;
+const MULTIPLE_STORES_BOOL = `${CATEGORY}.multiple_stores_bool`;
+const MULTIPLE_STORES_KEYED = `${CATEGORY}.multiple_stores_keyed`;
+
+function getParentSnapshot(store, keyed = false, clear = false) {
+ return keyed
+ ? Telemetry.getSnapshotForKeyedScalars(store, clear).parent
+ : Telemetry.getSnapshotForScalars(store, clear).parent;
+}
+
+add_task(async function test_multistore_basics() {
+ Telemetry.clearScalars();
+
+ const expectedUint = 3785;
+ const expectedBool = true;
+ const expectedString = "some value";
+ const expectedKey = "some key";
+ Telemetry.scalarSet(MAIN_ONLY, expectedUint);
+ Telemetry.scalarSet(SYNC_ONLY, expectedUint);
+ Telemetry.scalarSet(MULTIPLE_STORES, expectedUint);
+ Telemetry.scalarSet(MULTIPLE_STORES_STRING, expectedString);
+ Telemetry.scalarSet(MULTIPLE_STORES_BOOL, expectedBool);
+ Telemetry.keyedScalarSet(MULTIPLE_STORES_KEYED, expectedKey, expectedUint);
+
+ const mainScalars = getParentSnapshot("main");
+ const syncScalars = getParentSnapshot("sync");
+ const mainKeyedScalars = getParentSnapshot("main", true /* keyed */);
+ const syncKeyedScalars = getParentSnapshot("sync", true /* keyed */);
+
+ Assert.ok(
+ MAIN_ONLY in mainScalars,
+ `Main-store scalar ${MAIN_ONLY} must be in main snapshot.`
+ );
+ Assert.ok(
+ !(MAIN_ONLY in syncScalars),
+ `Main-store scalar ${MAIN_ONLY} must not be in sync snapshot.`
+ );
+ Assert.equal(
+ mainScalars[MAIN_ONLY],
+ expectedUint,
+ `Main-store scalar ${MAIN_ONLY} must have correct value.`
+ );
+
+ Assert.ok(
+ SYNC_ONLY in syncScalars,
+ `Sync-store scalar ${SYNC_ONLY} must be in sync snapshot.`
+ );
+ Assert.ok(
+ !(SYNC_ONLY in mainScalars),
+ `Sync-store scalar ${SYNC_ONLY} must not be in main snapshot.`
+ );
+ Assert.equal(
+ syncScalars[SYNC_ONLY],
+ expectedUint,
+ `Sync-store scalar ${SYNC_ONLY} must have correct value.`
+ );
+
+ Assert.ok(
+ MULTIPLE_STORES in mainScalars && MULTIPLE_STORES in syncScalars,
+ `Multi-store scalar ${MULTIPLE_STORES} must be in both main and sync snapshots.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES} must have correct value in main store.`
+ );
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES} must have correct value in sync store.`
+ );
+
+ Assert.ok(
+ MULTIPLE_STORES_STRING in mainScalars &&
+ MULTIPLE_STORES_STRING in syncScalars,
+ `Multi-store scalar ${MULTIPLE_STORES_STRING} must be in both main and sync snapshots.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES_STRING],
+ expectedString,
+ `Multi-store scalar ${MULTIPLE_STORES_STRING} must have correct value in main store.`
+ );
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES_STRING],
+ expectedString,
+ `Multi-store scalar ${MULTIPLE_STORES_STRING} must have correct value in sync store.`
+ );
+
+ Assert.ok(
+ MULTIPLE_STORES_BOOL in mainScalars && MULTIPLE_STORES_BOOL in syncScalars,
+ `Multi-store scalar ${MULTIPLE_STORES_BOOL} must be in both main and sync snapshots.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES_BOOL],
+ expectedBool,
+ `Multi-store scalar ${MULTIPLE_STORES_BOOL} must have correct value in main store.`
+ );
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES_BOOL],
+ expectedBool,
+ `Multi-store scalar ${MULTIPLE_STORES_BOOL} must have correct value in sync store.`
+ );
+
+ Assert.ok(
+ MULTIPLE_STORES_KEYED in mainKeyedScalars &&
+ MULTIPLE_STORES_KEYED in syncKeyedScalars,
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must be in both main and sync snapshots.`
+ );
+ Assert.ok(
+ expectedKey in mainKeyedScalars[MULTIPLE_STORES_KEYED] &&
+ expectedKey in syncKeyedScalars[MULTIPLE_STORES_KEYED],
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must have key ${expectedKey} in both snapshots.`
+ );
+ Assert.equal(
+ mainKeyedScalars[MULTIPLE_STORES_KEYED][expectedKey],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must have correct value in main store.`
+ );
+ Assert.equal(
+ syncKeyedScalars[MULTIPLE_STORES_KEYED][expectedKey],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must have correct value in sync store.`
+ );
+});
+
+add_task(async function test_multistore_uint() {
+ Telemetry.clearScalars();
+
+ // Uint scalars are the only kind with an implicit default value of 0.
+ // They shouldn't report any value until set, but if you Add or SetMaximum
+ // they pretend that they have been set to 0 for the purposes of that operation.
+
+ function assertNotIn() {
+ let mainScalars = getParentSnapshot("main");
+ let syncScalars = getParentSnapshot("sync");
+
+ if (!mainScalars && !syncScalars) {
+ Assert.ok(true, "No scalars at all");
+ } else {
+ Assert.ok(
+ !(MULTIPLE_STORES in mainScalars) && !(MULTIPLE_STORES in syncScalars),
+ `Multi-store scalar ${MULTIPLE_STORES} must not have an initial value in either store.`
+ );
+ }
+ }
+ assertNotIn();
+
+ // Test that Add operates on implicit 0.
+ Telemetry.scalarAdd(MULTIPLE_STORES, 1);
+
+ function assertBothEqual(val, clear = false) {
+ let mainScalars = getParentSnapshot("main", false, clear);
+ let syncScalars = getParentSnapshot("sync", false, clear);
+
+ Assert.ok(
+ MULTIPLE_STORES in mainScalars && MULTIPLE_STORES in syncScalars,
+ `Multi-store scalar ${MULTIPLE_STORES} must be in both main and sync snapshots.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ val,
+ `Multi-store scalar ${MULTIPLE_STORES} must have the correct value in main store.`
+ );
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES],
+ val,
+ `Multi-store scalar ${MULTIPLE_STORES} must have the correct value in sync store.`
+ );
+ }
+
+ assertBothEqual(1, true /* clear */);
+
+ assertNotIn();
+
+ // Test that SetMaximum operates on implicit 0.
+ Telemetry.scalarSetMaximum(MULTIPLE_STORES, 1337);
+ assertBothEqual(1337);
+
+ // Test that Add works, since we're in the neighbourhood.
+ Telemetry.scalarAdd(MULTIPLE_STORES, 1);
+ assertBothEqual(1338, true /* clear */);
+
+ assertNotIn();
+
+ // Test that clearing individual stores works
+ // and that afterwards the values are managed independently.
+ Telemetry.scalarAdd(MULTIPLE_STORES, 1234);
+ assertBothEqual(1234);
+ let syncScalars = getParentSnapshot(
+ "sync",
+ false /* keyed */,
+ true /* clear */
+ );
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES],
+ 1234,
+ `Multi-store scalar ${MULTIPLE_STORES} must be present in a second snapshot.`
+ );
+ syncScalars = getParentSnapshot("sync");
+ Assert.equal(
+ syncScalars,
+ undefined,
+ `Multi-store scalar ${MULTIPLE_STORES} must not be present after clearing.`
+ );
+ let mainScalars = getParentSnapshot("main");
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ 1234,
+ `Multi-store scalar ${MULTIPLE_STORES} must maintain value in main store after sync store is cleared.`
+ );
+
+ Telemetry.scalarSetMaximum(MULTIPLE_STORES, 1);
+ syncScalars = getParentSnapshot("sync");
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES],
+ 1,
+ `Multi-store scalar ${MULTIPLE_STORES} must return to using implicit 0 for setMax operation.`
+ );
+ mainScalars = getParentSnapshot("main");
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ 1234,
+ `Multi-store scalar ${MULTIPLE_STORES} must retain old value.`
+ );
+
+ Telemetry.scalarAdd(MULTIPLE_STORES, 1);
+ syncScalars = getParentSnapshot("sync");
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES],
+ 2,
+ `Multi-store scalar ${MULTIPLE_STORES} must manage independently for add operations.`
+ );
+ mainScalars = getParentSnapshot("main");
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ 1235,
+ `Multi-store scalar ${MULTIPLE_STORES} must add properly.`
+ );
+
+ Telemetry.scalarSet(MULTIPLE_STORES, 9876);
+ assertBothEqual(9876);
+});
+
+add_task(async function test_empty_absence() {
+ // Current semantics are we don't snapshot empty things.
+ // So no {parent: {}, ...}. Instead {...}.
+
+ Telemetry.clearScalars();
+
+ Telemetry.scalarSet(MULTIPLE_STORES, 1);
+ let snapshot = getParentSnapshot("main", false /* keyed */, true /* clear */);
+
+ Assert.ok(
+ MULTIPLE_STORES in snapshot,
+ `${MULTIPLE_STORES} must be in the snapshot.`
+ );
+ Assert.equal(
+ snapshot[MULTIPLE_STORES],
+ 1,
+ `${MULTIPLE_STORES} must have the correct value.`
+ );
+
+ snapshot = getParentSnapshot("main", false /* keyed */, true /* clear */);
+ Assert.equal(
+ snapshot,
+ undefined,
+ `Parent snapshot must be empty if no data.`
+ );
+
+ snapshot = getParentSnapshot("sync", false /* keyed */, true /* clear */);
+ Assert.ok(
+ MULTIPLE_STORES in snapshot,
+ `${MULTIPLE_STORES} must be in the sync snapshot.`
+ );
+ Assert.equal(
+ snapshot[MULTIPLE_STORES],
+ 1,
+ `${MULTIPLE_STORES} must have the correct value in the sync snapshot.`
+ );
+});
+
+add_task(async function test_empty_absence_keyed() {
+ // Current semantics are we don't snapshot empty things.
+ // So no {parent: {}, ...}. Instead {...}.
+ // And for Keyed Scalars, no {parent: { keyed_scalar: {} }, ...}. Just {...}.
+
+ Telemetry.clearScalars();
+
+ const key = "just a key, y'know";
+ Telemetry.keyedScalarSet(MULTIPLE_STORES_KEYED, key, 1);
+ let snapshot = getParentSnapshot("main", true /* keyed */, true /* clear */);
+
+ Assert.ok(
+ MULTIPLE_STORES_KEYED in snapshot,
+ `${MULTIPLE_STORES_KEYED} must be in the snapshot.`
+ );
+ Assert.ok(
+ key in snapshot[MULTIPLE_STORES_KEYED],
+ `${MULTIPLE_STORES_KEYED} must have the stored key.`
+ );
+ Assert.equal(
+ snapshot[MULTIPLE_STORES_KEYED][key],
+ 1,
+ `${MULTIPLE_STORES_KEYED}[${key}] should have the correct value.`
+ );
+
+ snapshot = getParentSnapshot("main", true /* keyed */);
+ Assert.equal(
+ snapshot,
+ undefined,
+ `Parent snapshot should be empty if no data.`
+ );
+ snapshot = getParentSnapshot("sync", true /* keyed */);
+
+ Assert.ok(
+ MULTIPLE_STORES_KEYED in snapshot,
+ `${MULTIPLE_STORES_KEYED} must be in the sync snapshot.`
+ );
+ Assert.ok(
+ key in snapshot[MULTIPLE_STORES_KEYED],
+ `${MULTIPLE_STORES_KEYED} must have the stored key.`
+ );
+ Assert.equal(
+ snapshot[MULTIPLE_STORES_KEYED][key],
+ 1,
+ `${MULTIPLE_STORES_KEYED}[${key}] should have the correct value.`
+ );
+});
+
+add_task(async function test_multistore_default_values() {
+ Telemetry.clearScalars();
+
+ const expectedUint = 3785;
+ const expectedKey = "some key";
+ Telemetry.scalarSet(MAIN_ONLY, expectedUint);
+ Telemetry.scalarSet(SYNC_ONLY, expectedUint);
+ Telemetry.scalarSet(MULTIPLE_STORES, expectedUint);
+ Telemetry.keyedScalarSet(MULTIPLE_STORES_KEYED, expectedKey, expectedUint);
+
+ let mainScalars;
+ let mainKeyedScalars;
+
+ // Getting snapshot and NOT clearing (using default values for optional parameters)
+ mainScalars = Telemetry.getSnapshotForScalars().parent;
+ mainKeyedScalars = Telemetry.getSnapshotForKeyedScalars().parent;
+
+ Assert.equal(
+ mainScalars[MAIN_ONLY],
+ expectedUint,
+ `Main-store scalar ${MAIN_ONLY} must have correct value.`
+ );
+ Assert.ok(
+ !(SYNC_ONLY in mainScalars),
+ `Sync-store scalar ${SYNC_ONLY} must not be in main snapshot.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES} must have correct value in main store.`
+ );
+ Assert.equal(
+ mainKeyedScalars[MULTIPLE_STORES_KEYED][expectedKey],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must have correct value in main store.`
+ );
+
+ // Getting snapshot and clearing
+ mainScalars = Telemetry.getSnapshotForScalars("main", true).parent;
+ mainKeyedScalars = Telemetry.getSnapshotForKeyedScalars("main", true).parent;
+
+ Assert.equal(
+ mainScalars[MAIN_ONLY],
+ expectedUint,
+ `Main-store scalar ${MAIN_ONLY} must have correct value.`
+ );
+ Assert.ok(
+ !(SYNC_ONLY in mainScalars),
+ `Sync-store scalar ${SYNC_ONLY} must not be in main snapshot.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES} must have correct value in main store.`
+ );
+ Assert.equal(
+ mainKeyedScalars[MULTIPLE_STORES_KEYED][expectedKey],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must have correct value in main store.`
+ );
+
+ // Getting snapshot (with default values), should be empty now
+ mainScalars = Telemetry.getSnapshotForScalars().parent || {};
+ mainKeyedScalars = Telemetry.getSnapshotForKeyedScalars().parent || {};
+
+ Assert.ok(
+ !(MAIN_ONLY in mainScalars),
+ `Main-store scalar ${MAIN_ONLY} must not be in main snapshot.`
+ );
+ Assert.ok(
+ !(MULTIPLE_STORES in mainScalars),
+ `Multi-store scalar ${MULTIPLE_STORES} must not be in main snapshot.`
+ );
+ Assert.ok(
+ !(MULTIPLE_STORES_KEYED in mainKeyedScalars),
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must not be in main snapshot.`
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
new file mode 100644
index 0000000000..5bcb69d5a0
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
@@ -0,0 +1,1110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// This tests the public Telemetry API for submitting pings.
+
+"use strict";
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+const { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+);
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryHealthPing: "resource://gre/modules/HealthPing.sys.mjs",
+});
+
+const MS_IN_A_MINUTE = 60 * 1000;
+
+function countPingTypes(pings) {
+ let countByType = new Map();
+ for (let p of pings) {
+ countByType.set(p.type, 1 + (countByType.get(p.type) || 0));
+ }
+ return countByType;
+}
+
+function setPingLastModified(id, timestamp) {
+ const path = PathUtils.join(TelemetryStorage.pingDirectoryPath, id);
+ return IOUtils.setModificationTime(path, timestamp);
+}
+
+// Mock out the send timer activity.
+function waitForTimer() {
+ return new Promise(resolve => {
+ fakePingSendTimer(
+ (callback, timeout) => {
+ resolve([callback, timeout]);
+ },
+ () => {}
+ );
+ });
+}
+
+function sendPing(aSendClientId, aSendEnvironment) {
+ const TEST_PING_TYPE = "test-ping-type";
+
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ } else {
+ TelemetrySend.setServer("http://doesnotexist");
+ }
+
+ let options = {
+ addClientId: aSendClientId,
+ addEnvironment: aSendEnvironment,
+ };
+ return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
+}
+
+// Allow easy faking of readable ping ids.
+// This helps with debugging issues with e.g. ordering in the send logic.
+function fakePingId(type, number) {
+ const HEAD = "93bd0011-2c8f-4e1c-bee0-";
+ const TAIL = "000000000000";
+ const N = String(number);
+ const id = HEAD + type + TAIL.slice(type.length, -N.length) + N;
+ fakeGeneratePingId(() => id);
+ return id;
+}
+
+var checkPingsSaved = async function (pingIds) {
+ let allFound = true;
+ for (let id of pingIds) {
+ const path = PathUtils.join(TelemetryStorage.pingDirectoryPath, id);
+ let exists = false;
+ try {
+ exists = await IOUtils.exists(path);
+ } catch (ex) {}
+
+ if (!exists) {
+ dump("checkPingsSaved - failed to find ping: " + path + "\n");
+ allFound = false;
+ }
+ }
+
+ return allFound;
+};
+
+function histogramValueCount(h) {
+ return Object.values(h.values).reduce((a, b) => a + b, 0);
+}
+
+add_task(async function test_setup() {
+ // Trigger a proper telemetry init.
+ do_get_profile(true);
+
+ // Addon manager needs a profile directory.
+ 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.setBoolPref(
+ TelemetryUtils.Preferences.HealthPingEnabled,
+ true
+ );
+ TelemetryStopwatch.setTestModeEnabled(true);
+});
+
+// Test the ping sending logic.
+add_task(async function test_sendPendingPings() {
+ const TYPE_PREFIX = "test-sendPendingPings-";
+ const TEST_TYPE_A = TYPE_PREFIX + "A";
+ const TEST_TYPE_B = TYPE_PREFIX + "B";
+
+ const TYPE_A_COUNT = 20;
+ const TYPE_B_COUNT = 5;
+
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById(
+ "TELEMETRY_SEND_SUCCESS"
+ );
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ histSuccess.clear();
+ histSendTimeSuccess.clear();
+ histSendTimeFail.clear();
+
+ // Fake a current date.
+ let now = TelemetryUtils.truncateToDays(new Date());
+ now = fakeNow(futureDate(now, 10 * 60 * MS_IN_A_MINUTE));
+
+ // Enable test-mode for TelemetrySend, otherwise we won't store pending pings
+ // before the module is fully initialized later.
+ TelemetrySend.setTestModeEnabled(true);
+
+ // Submit some pings without the server and telemetry started yet.
+ for (let i = 0; i < TYPE_A_COUNT; ++i) {
+ fakePingId("a", i);
+ const id = await TelemetryController.submitExternalPing(TEST_TYPE_A, {});
+ await setPingLastModified(id, now.getTime() + i * 1000);
+ }
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ TYPE_A_COUNT,
+ "Should have correct pending ping count"
+ );
+
+ // Submit some more pings of a different type.
+ now = fakeNow(futureDate(now, 5 * MS_IN_A_MINUTE));
+ for (let i = 0; i < TYPE_B_COUNT; ++i) {
+ fakePingId("b", i);
+ const id = await TelemetryController.submitExternalPing(TEST_TYPE_B, {});
+ await setPingLastModified(id, now.getTime() + i * 1000);
+ }
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ TYPE_A_COUNT + TYPE_B_COUNT,
+ "Should have correct pending ping count"
+ );
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ {},
+ "Should not have recorded any sending in histograms yet."
+ );
+ Assert.equal(
+ histSendTimeSuccess.snapshot().sum,
+ 0,
+ "Should not have recorded any sending in histograms yet."
+ );
+ Assert.equal(
+ histSendTimeFail.snapshot().sum,
+ 0,
+ "Should not have recorded any sending in histograms yet."
+ );
+
+ // Now enable sending to the ping server.
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ PingServer.start();
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ let timerPromise = waitForTimer();
+ await TelemetryController.testReset();
+ let [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+ Assert.ok(!!pingSendTimerCallback, "Should have a timer callback");
+
+ // We should have received 10 pings from the first send batch:
+ // 5 of type B and 5 of type A, as sending is newest-first.
+ // The other pings should be delayed by the 10-pings-per-minute limit.
+ let pings = await PingServer.promiseNextPings(10);
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ TYPE_A_COUNT - 5,
+ "Should have correct pending ping count"
+ );
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+ let countByType = countPingTypes(pings);
+
+ Assert.equal(
+ countByType.get(TEST_TYPE_B),
+ TYPE_B_COUNT,
+ "Should have received the correct amount of type B pings"
+ );
+ Assert.equal(
+ countByType.get(TEST_TYPE_A),
+ 10 - TYPE_B_COUNT,
+ "Should have received the correct amount of type A pings"
+ );
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: 0, 1: 10, 2: 0 },
+ "Should have recorded sending success in histograms."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeSuccess.snapshot()),
+ 10,
+ "Should have recorded successful send times in histograms."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeFail.snapshot()),
+ 0,
+ "Should not have recorded any failed sending in histograms yet."
+ );
+
+ // As we hit the ping send limit and still have pending pings, a send tick should
+ // be scheduled in a minute.
+ Assert.ok(!!pingSendTimerCallback, "Timer callback should be set");
+ Assert.equal(
+ pingSendTimeout,
+ MS_IN_A_MINUTE,
+ "Send tick timeout should be correct"
+ );
+
+ // Trigger the next tick - we should receive the next 10 type A pings.
+ PingServer.resetPingHandler();
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ timerPromise = waitForTimer();
+ pingSendTimerCallback();
+ [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+
+ pings = await PingServer.promiseNextPings(10);
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+ countByType = countPingTypes(pings);
+
+ Assert.equal(
+ countByType.get(TEST_TYPE_A),
+ 10,
+ "Should have received the correct amount of type A pings"
+ );
+
+ // We hit the ping send limit again and still have pending pings, a send tick should
+ // be scheduled in a minute.
+ Assert.equal(
+ pingSendTimeout,
+ MS_IN_A_MINUTE,
+ "Send tick timeout should be correct"
+ );
+
+ // Trigger the next tick - we should receive the remaining type A pings.
+ PingServer.resetPingHandler();
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ await pingSendTimerCallback();
+
+ pings = await PingServer.promiseNextPings(5);
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+ countByType = countPingTypes(pings);
+
+ Assert.equal(
+ countByType.get(TEST_TYPE_A),
+ 5,
+ "Should have received the correct amount of type A pings"
+ );
+
+ await TelemetrySend.testWaitOnOutgoingPings();
+ PingServer.resetPingHandler();
+ // Restore the default ping id generator.
+ fakeGeneratePingId(() => TelemetryUtils.generateUUID());
+});
+
+add_task(async function test_sendDateHeader() {
+ fakeNow(new Date(Date.UTC(2011, 1, 1, 11, 0, 0)));
+ await TelemetrySend.reset();
+
+ let pingId = await TelemetryController.submitExternalPing(
+ "test-send-date-header",
+ {}
+ );
+ let req = await PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+ Assert.equal(
+ req.getHeader("Date"),
+ "Tue, 01 Feb 2011 11:00:00 GMT",
+ "Telemetry should send the correct Date header with requests."
+ );
+ Assert.equal(ping.id, pingId, "Should have received the correct ping id.");
+});
+
+// Test the backoff timeout behavior after send failures.
+add_task(async function test_backoffTimeout() {
+ const TYPE_PREFIX = "test-backoffTimeout-";
+ const TEST_TYPE_C = TYPE_PREFIX + "C";
+ const TEST_TYPE_D = TYPE_PREFIX + "D";
+ const TEST_TYPE_E = TYPE_PREFIX + "E";
+
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById(
+ "TELEMETRY_SEND_SUCCESS"
+ );
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+
+ // Failing a ping send now should trigger backoff behavior.
+ let now = fakeNow(2010, 1, 1, 11, 0, 0);
+ await TelemetrySend.reset();
+ PingServer.stop();
+
+ histSuccess.clear();
+ histSendTimeSuccess.clear();
+ histSendTimeFail.clear();
+
+ fakePingId("c", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ let sendAttempts = 0;
+ let timerPromise = waitForTimer();
+ await TelemetryController.submitExternalPing(TEST_TYPE_C, {});
+ let [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 1,
+ "Should have one pending ping."
+ );
+ ++sendAttempts;
+
+ const MAX_BACKOFF_TIMEOUT = 120 * MS_IN_A_MINUTE;
+ for (
+ let timeout = 2 * MS_IN_A_MINUTE;
+ timeout <= MAX_BACKOFF_TIMEOUT;
+ timeout *= 2
+ ) {
+ Assert.ok(!!pingSendTimerCallback, "Should have received a timer callback");
+ Assert.equal(
+ pingSendTimeout,
+ timeout,
+ "Send tick timeout should be correct"
+ );
+
+ let callback = pingSendTimerCallback;
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ timerPromise = waitForTimer();
+ await callback();
+ [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+ ++sendAttempts;
+ }
+
+ timerPromise = waitForTimer();
+ await pingSendTimerCallback();
+ [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+ Assert.equal(
+ pingSendTimeout,
+ MAX_BACKOFF_TIMEOUT,
+ "Tick timeout should be capped"
+ );
+ ++sendAttempts;
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: sendAttempts, 1: 0 },
+ "Should have recorded sending failure in histograms."
+ );
+ Assert.equal(
+ histSendTimeSuccess.snapshot().sum,
+ 0,
+ "Should not have recorded any sending success in histograms yet."
+ );
+ Assert.greaterOrEqual(
+ histSendTimeFail.snapshot().sum,
+ 0,
+ "Should have recorded send failure times in histograms."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeFail.snapshot()),
+ sendAttempts,
+ "Should have recorded send failure times in histograms."
+ );
+
+ // Submitting a new ping should reset the backoff behavior.
+ fakePingId("d", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ timerPromise = waitForTimer();
+ await TelemetryController.submitExternalPing(TEST_TYPE_D, {});
+ [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+ Assert.equal(
+ pingSendTimeout,
+ 2 * MS_IN_A_MINUTE,
+ "Send tick timeout should be correct"
+ );
+ sendAttempts += 2;
+
+ // With the server running again, we should send out the pending pings immediately
+ // when a new ping is submitted.
+ PingServer.start();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ fakePingId("e", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ timerPromise = waitForTimer();
+ await TelemetryController.submitExternalPing(TEST_TYPE_E, {});
+
+ let pings = await PingServer.promiseNextPings(3);
+ let countByType = countPingTypes(pings);
+
+ Assert.equal(
+ countByType.get(TEST_TYPE_C),
+ 1,
+ "Should have received the correct amount of type C pings"
+ );
+ Assert.equal(
+ countByType.get(TEST_TYPE_D),
+ 1,
+ "Should have received the correct amount of type D pings"
+ );
+ Assert.equal(
+ countByType.get(TEST_TYPE_E),
+ 1,
+ "Should have received the correct amount of type E pings"
+ );
+
+ await TelemetrySend.testWaitOnOutgoingPings();
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should have no pending pings left"
+ );
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: sendAttempts, 1: 3, 2: 0 },
+ "Should have recorded sending failure in histograms."
+ );
+ Assert.greaterOrEqual(
+ histSendTimeSuccess.snapshot().sum,
+ 0,
+ "Should have recorded sending success in histograms."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeSuccess.snapshot()),
+ 3,
+ "Should have recorded sending success in histograms."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeFail.snapshot()),
+ sendAttempts,
+ "Should have recorded send failure times in histograms."
+ );
+
+ // Restore the default ping id generator.
+ fakeGeneratePingId(() => TelemetryUtils.generateUUID());
+});
+
+add_task(async function test_discardBigPings() {
+ const TEST_PING_TYPE = "test-ping-type";
+
+ let histSizeExceeded = Telemetry.getHistogramById(
+ "TELEMETRY_PING_SIZE_EXCEEDED_SEND"
+ );
+ let histDiscardedSize = Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB"
+ );
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById(
+ "TELEMETRY_SEND_SUCCESS"
+ );
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ for (let h of [
+ histSizeExceeded,
+ histDiscardedSize,
+ histSuccess,
+ histSendTimeSuccess,
+ histSendTimeFail,
+ ]) {
+ h.clear();
+ }
+
+ // Submit a ping of a normal size and check that we don't count it in the histogram.
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {
+ test: "test",
+ });
+ await TelemetrySend.testWaitOnOutgoingPings();
+ await PingServer.promiseNextPing();
+
+ Assert.equal(
+ histSizeExceeded.snapshot().sum,
+ 0,
+ "Telemetry must report no oversized ping submitted."
+ );
+ Assert.equal(
+ histDiscardedSize.snapshot().sum,
+ 0,
+ "Telemetry must report no oversized pings."
+ );
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: 0, 1: 1, 2: 0 },
+ "Should have recorded sending success."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeSuccess.snapshot()),
+ 1,
+ "Should have recorded send success time."
+ );
+ Assert.greaterOrEqual(
+ histSendTimeSuccess.snapshot().sum,
+ 0,
+ "Should have recorded send success time."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeFail.snapshot()),
+ 0,
+ "Should not have recorded send failure time."
+ );
+
+ // Submit an oversized ping and check that it gets discarded.
+ TelemetryHealthPing.testReset();
+ // Ensure next ping has a 2 MB gzipped payload.
+ fakeGzipCompressStringForNextPing(2 * 1024 * 1024);
+ const OVERSIZED_PAYLOAD = {
+ data: "empty on purpose - policy takes care of size",
+ };
+ await TelemetryController.submitExternalPing(
+ TEST_PING_TYPE,
+ OVERSIZED_PAYLOAD
+ );
+ await TelemetrySend.testWaitOnOutgoingPings();
+ let ping = await PingServer.promiseNextPing();
+
+ Assert.equal(
+ ping.type,
+ TelemetryHealthPing.HEALTH_PING_TYPE,
+ "Should have received a health ping."
+ );
+ Assert.equal(
+ ping.payload.reason,
+ TelemetryHealthPing.Reason.IMMEDIATE,
+ "Health ping should have the right reason."
+ );
+ Assert.deepEqual(
+ ping.payload[TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE],
+ { [TEST_PING_TYPE]: 1 },
+ "Should have recorded correct type of oversized ping."
+ );
+ Assert.deepEqual(
+ ping.payload.os,
+ TelemetryHealthPing.OsInfo,
+ "Should have correct os info."
+ );
+
+ Assert.equal(
+ histSizeExceeded.snapshot().sum,
+ 1,
+ "Telemetry must report 1 oversized ping submitted."
+ );
+ Assert.equal(
+ histDiscardedSize.snapshot().values[2],
+ 1,
+ "Telemetry must report a 2MB, oversized, ping submitted."
+ );
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: 0, 1: 2, 2: 0 },
+ "Should have recorded sending success."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeSuccess.snapshot()),
+ 2,
+ "Should have recorded send success time."
+ );
+ Assert.greaterOrEqual(
+ histSendTimeSuccess.snapshot().sum,
+ 0,
+ "Should have recorded send success time."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeFail.snapshot()),
+ 0,
+ "Should not have recorded send failure time."
+ );
+});
+
+add_task(async function test_largeButWithinLimit() {
+ const TEST_PING_TYPE = "test-ping-type";
+
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ histSuccess.clear();
+
+ // Next ping will have a 900KB gzip payload.
+ fakeGzipCompressStringForNextPing(900 * 1024);
+ const LARGE_PAYLOAD = {
+ data: "empty on purpose - policy takes care of size",
+ };
+
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, LARGE_PAYLOAD);
+ await TelemetrySend.testWaitOnOutgoingPings();
+ await PingServer.promiseNextRequest();
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: 0, 1: 1, 2: 0 },
+ "Should have sent large ping."
+ );
+});
+
+add_task(async function test_evictedOnServerErrors() {
+ const TEST_TYPE = "test-evicted";
+
+ await TelemetrySend.reset();
+
+ let histEvicted = Telemetry.getHistogramById(
+ "TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS"
+ );
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById(
+ "TELEMETRY_SEND_SUCCESS"
+ );
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ for (let h of [
+ histEvicted,
+ histSuccess,
+ histSendTimeSuccess,
+ histSendTimeFail,
+ ]) {
+ h.clear();
+ }
+
+ // Write a custom ping handler which will return 403. This will trigger ping eviction
+ // on client side.
+ PingServer.registerPingHandler((req, res) => {
+ res.setStatusLine(null, 403, "Forbidden");
+ res.processAsync();
+ res.finish();
+ });
+
+ // Clear the histogram and submit a ping.
+ let pingId = await TelemetryController.submitExternalPing(TEST_TYPE, {});
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ Assert.equal(
+ histEvicted.snapshot().sum,
+ 1,
+ "Telemetry must report a ping evicted due to server errors"
+ );
+ Assert.deepEqual(histSuccess.snapshot().values, { 0: 0, 1: 1, 2: 0 });
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1);
+ Assert.greaterOrEqual(histSendTimeSuccess.snapshot().sum, 0);
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0);
+
+ // The ping should not be persisted.
+ await Assert.rejects(
+ TelemetryStorage.loadPendingPing(pingId),
+ /TelemetryStorage.loadPendingPing - no ping with id/,
+ "The ping must not be persisted."
+ );
+
+ // Reset the ping handler and submit a new ping.
+ PingServer.resetPingHandler();
+ pingId = await TelemetryController.submitExternalPing(TEST_TYPE, {});
+
+ let ping = await PingServer.promiseNextPings(1);
+ Assert.equal(ping[0].id, pingId, "The correct ping must be received");
+
+ // We should not have updated the error histogram.
+ await TelemetrySend.testWaitOnOutgoingPings();
+ Assert.equal(
+ histEvicted.snapshot().sum,
+ 1,
+ "Telemetry must report only one ping evicted due to server errors"
+ );
+ Assert.deepEqual(histSuccess.snapshot().values, { 0: 0, 1: 2, 2: 0 });
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 2);
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0);
+});
+
+add_task(async function test_tooLateToSend() {
+ Assert.ok(true, "TEST BEGIN");
+ const TEST_TYPE = "test-too-late-to-send";
+
+ await TelemetrySend.reset();
+ PingServer.start();
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should have no pending pings yet"
+ );
+
+ TelemetrySend.testTooLateToSend(true);
+
+ const id = await TelemetryController.submitExternalPing(TEST_TYPE, {});
+
+ // Triggering a shutdown should persist the pings
+ await TelemetrySend.shutdown();
+ const pendingPings = TelemetryStorage.getPendingPingList();
+ Assert.equal(pendingPings.length, 1, "Should have a pending ping in storage");
+ Assert.equal(pendingPings[0].id, id, "Should have pended our test's ping");
+
+ Assert.equal(
+ Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE_TYPE").snapshot()
+ .values[7],
+ 1,
+ "Should have registered the failed attempt to send"
+ );
+ Assert.equal(
+ Telemetry.getKeyedHistogramById(
+ "TELEMETRY_SEND_FAILURE_TYPE_PER_PING"
+ ).snapshot()[TEST_TYPE].values[7],
+ 1,
+ "Should have registered the failed attempt to send TEST_TYPE ping"
+ );
+ await TelemetryStorage.reset();
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should clean up after yourself"
+ );
+});
+
+add_task(
+ { skip_if: () => gIsAndroid },
+ async function test_pingSenderShutdownBatch() {
+ const TEST_TYPE = "test-ping-sender-batch";
+
+ await TelemetrySend.reset();
+ PingServer.start();
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings at this time.")
+ );
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should have no pending pings yet"
+ );
+
+ TelemetrySend.testTooLateToSend(true);
+
+ const id = await TelemetryController.submitExternalPing(
+ TEST_TYPE,
+ { payload: false },
+ { usePingSender: true }
+ );
+ const id2 = await TelemetryController.submitExternalPing(
+ TEST_TYPE,
+ { payload: false },
+ { usePingSender: true }
+ );
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 2,
+ "Should have stored these two pings in pending land."
+ );
+
+ // Permit pings to be received.
+ PingServer.resetPingHandler();
+
+ TelemetrySend.flushPingSenderBatch();
+
+ // Pings don't have to be sent in the order they're submitted.
+ const ping = await PingServer.promiseNextPing();
+ const ping2 = await PingServer.promiseNextPing();
+ Assert.ok(
+ (ping.id == id && ping2.id == id2) || (ping.id == id2 && ping2.id == id)
+ );
+ Assert.equal(ping.type, TEST_TYPE);
+ Assert.equal(ping2.type, TEST_TYPE);
+
+ await TelemetryStorage.reset();
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should clean up after yourself"
+ );
+ }
+);
+
+// Test that the current, non-persisted pending pings are properly saved on shutdown.
+add_task(async function test_persistCurrentPingsOnShutdown() {
+ const TEST_TYPE = "test-persistCurrentPingsOnShutdown";
+ const PING_COUNT = 5;
+ await TelemetrySend.reset();
+ PingServer.stop();
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should have no pending pings yet"
+ );
+
+ // Submit new pings that shouldn't be persisted yet.
+ let ids = [];
+ for (let i = 0; i < 5; ++i) {
+ ids.push(fakePingId("f", i));
+ TelemetryController.submitExternalPing(TEST_TYPE, {});
+ }
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ PING_COUNT,
+ "Should have the correct pending ping count"
+ );
+
+ // Triggering a shutdown should persist the pings.
+ await TelemetrySend.shutdown();
+ Assert.ok(
+ await checkPingsSaved(ids),
+ "All pending pings should have been persisted"
+ );
+
+ // After a restart the pings should have been found when scanning.
+ await TelemetrySend.reset();
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ PING_COUNT,
+ "Should have the correct pending ping count"
+ );
+
+ // Restore the default ping id generator.
+ fakeGeneratePingId(() => TelemetryUtils.generateUUID());
+});
+
+add_task(async function test_sendCheckOverride() {
+ const TEST_PING_TYPE = "test-sendCheckOverride";
+
+ // Disable "health" ping. It can sneak into the test.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.HealthPingEnabled,
+ false
+ );
+
+ // Clear any pending pings.
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+
+ // Enable the ping server.
+ PingServer.start();
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ // Start Telemetry and disable the test-mode so pings don't get
+ // sent unless we enable the override.
+ await TelemetryController.testReset();
+
+ // Submit a test ping and make sure it doesn't get sent. We only do
+ // that if we're on unofficial builds: pings will always get sent otherwise.
+ if (!Services.telemetry.isOfficialTelemetry) {
+ TelemetrySend.setTestModeEnabled(false);
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {
+ test: "test",
+ });
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should have no pending pings"
+ );
+ }
+
+ // Enable the override and try to send again.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.OverrideOfficialCheck,
+ true
+ );
+ PingServer.resetPingHandler();
+ await TelemetrySend.reset();
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {
+ test: "test",
+ });
+
+ // Make sure we received the ping.
+ const ping = await PingServer.promiseNextPing();
+ Assert.equal(
+ ping.type,
+ TEST_PING_TYPE,
+ "Must receive a ping of the expected type"
+ );
+
+ // Restore the test mode and disable the override.
+ TelemetrySend.setTestModeEnabled(true);
+ Services.prefs.clearUserPref(
+ TelemetryUtils.Preferences.OverrideOfficialCheck
+ );
+});
+
+add_task(async function test_submissionPath() {
+ const PING_FORMAT_VERSION = 4;
+ const TEST_PING_TYPE = "test-ping-type";
+
+ await TelemetrySend.reset();
+ PingServer.clearRequests();
+
+ await sendPing(false, false);
+
+ // Fetch the request from the server.
+ let request = await PingServer.promiseNextRequest();
+
+ // Get the payload.
+ let ping = decodeRequestPayload(request);
+ checkPingFormat(ping, TEST_PING_TYPE, false, false);
+
+ let app = ping.application;
+ let pathComponents = [
+ ping.id,
+ ping.type,
+ app.name,
+ app.version,
+ app.channel,
+ app.buildId,
+ ];
+
+ let urlComponents = request.path.split("/");
+
+ for (let i = 0; i < pathComponents.length; i++) {
+ Assert.ok(
+ urlComponents.includes(pathComponents[i]),
+ `Path should include ${pathComponents[i]}`
+ );
+ }
+
+ // 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 new ping format version.
+ let params = request.queryString.split("&");
+ Assert.ok(params.find(p => p == "v=" + PING_FORMAT_VERSION));
+});
+
+add_task(async function testCookies() {
+ const TEST_TYPE = "test-cookies";
+
+ await TelemetrySend.reset();
+ PingServer.clearRequests();
+
+ let uri = Services.io.newURI("http://localhost:" + PingServer.port);
+ let channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+ Services.cookies.QueryInterface(Ci.nsICookieService);
+ Services.cookies.setCookieStringFromHttp(uri, "cookie-time=yes", channel);
+
+ const id = await TelemetryController.submitExternalPing(TEST_TYPE, {});
+ let foundit = false;
+ while (!foundit) {
+ var request = await PingServer.promiseNextRequest();
+ var ping = decodeRequestPayload(request);
+ foundit = id === ping.id;
+ }
+ Assert.equal(id, ping.id, "We're testing the right ping's request, right?");
+ Assert.equal(
+ false,
+ request.hasHeader("Cookie"),
+ "Request should not have Cookie header"
+ );
+});
+
+add_task(async function test_pref_observer() {
+ // This test requires the presence of the crash reporter component.
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ if (
+ !registrar.isContractIDRegistered("@mozilla.org/toolkit/crash-reporter;1")
+ ) {
+ return;
+ }
+
+ await TelemetrySend.setup(true);
+
+ const IS_UNIFIED_TELEMETRY = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.Unified,
+ false
+ );
+
+ let origTelemetryEnabled = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.TelemetryEnabled
+ );
+ let origFhrUploadEnabled = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled
+ );
+
+ if (!IS_UNIFIED_TELEMETRY) {
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.TelemetryEnabled,
+ true
+ );
+ }
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ function waitAnnotateCrashReport(expectedValue, trigger) {
+ return new Promise(function (resolve, reject) {
+ let keys = new Set(["TelemetryClientId", "TelemetryServerURL"]);
+
+ let crs = {
+ QueryInterface: ChromeUtils.generateQI(["nsICrashReporter"]),
+ annotateCrashReport(key, value) {
+ if (!keys.delete(key)) {
+ MockRegistrar.unregister(gMockCrs);
+ reject(
+ Error(`Crash report annotation with unexpected key: "${key}".`)
+ );
+ }
+
+ if (expectedValue && value == "") {
+ MockRegistrar.unregister(gMockCrs);
+ reject(Error("Crash report annotation without expected value."));
+ }
+
+ if (keys.size == 0) {
+ MockRegistrar.unregister(gMockCrs);
+ resolve();
+ }
+ },
+ removeCrashReportAnnotation(key) {
+ if (!keys.delete(key)) {
+ MockRegistrar.unregister(gMockCrs);
+ }
+
+ if (keys.size == 0) {
+ MockRegistrar.unregister(gMockCrs);
+ resolve();
+ }
+ },
+ UpdateCrashEventsDir() {},
+ };
+
+ let gMockCrs = MockRegistrar.register(
+ "@mozilla.org/toolkit/crash-reporter;1",
+ crs
+ );
+ registerCleanupFunction(function () {
+ MockRegistrar.unregister(gMockCrs);
+ });
+
+ trigger();
+ });
+ }
+
+ await waitAnnotateCrashReport(!IS_UNIFIED_TELEMETRY, () =>
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ )
+ );
+
+ await waitAnnotateCrashReport(true, () =>
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ true
+ )
+ );
+
+ if (!IS_UNIFIED_TELEMETRY) {
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.TelemetryEnabled,
+ origTelemetryEnabled
+ );
+ }
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ origFhrUploadEnabled
+ );
+});
+
+add_task(async function cleanup() {
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
new file mode 100644
index 0000000000..7b7b0f674d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
@@ -0,0 +1,586 @@
+/* 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(aRequest) {
+ 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();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
new file mode 100644
index 0000000000..bae3d63942
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -0,0 +1,2357 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* This testcase triggers two telemetry pings.
+ *
+ * Telemetry code keeps histograms of past telemetry pings. The first
+ * ping populates these histograms. One of those histograms is then
+ * checked in the second request.
+ */
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+const { TelemetryReportingPolicy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+);
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+const PING_FORMAT_VERSION = 4;
+const PING_TYPE_MAIN = "main";
+const PING_TYPE_SAVED_SESSION = "saved-session";
+
+const REASON_ABORTED_SESSION = "aborted-session";
+const REASON_SAVED_SESSION = "saved-session";
+const REASON_SHUTDOWN = "shutdown";
+const REASON_TEST_PING = "test-ping";
+const REASON_DAILY = "daily";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+
+const IGNORE_CLONED_HISTOGRAM = "test::ignore_me_also";
+// Add some unicode characters here to ensure that sending them works correctly.
+const SHUTDOWN_TIME = 10000;
+const FAILED_PROFILE_LOCK_ATTEMPTS = 2;
+
+// Constants from prio.h for nsIFileOutputStream.init
+const PR_WRONLY = 0x2;
+const PR_CREATE_FILE = 0x8;
+const PR_TRUNCATE = 0x20;
+const RW_OWNER = parseInt("0600", 8);
+
+const MS_IN_ONE_HOUR = 60 * 60 * 1000;
+const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR;
+
+const DATAREPORTING_DIR = "datareporting";
+const ABORTED_PING_FILE_NAME = "aborted-session-ping";
+const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
+
+XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function () {
+ return PathUtils.join(PathUtils.profileDir, DATAREPORTING_DIR);
+});
+
+var gClientID = null;
+var gMonotonicNow = 0;
+
+function sendPing() {
+ TelemetrySession.gatherStartup();
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ return TelemetrySession.testPing();
+ }
+ TelemetrySend.setServer("http://doesnotexist");
+ return TelemetrySession.testPing();
+}
+
+function fakeGenerateUUID(sessionFunc, subsessionFunc) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+ );
+ Policy.generateSessionUUID = sessionFunc;
+ Policy.generateSubsessionUUID = subsessionFunc;
+}
+
+function fakeIdleNotification(topic) {
+ return TelemetryScheduler.observe(null, topic, null);
+}
+
+function setupTestData() {
+ Services.startup.interrupted = true;
+ let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ h2.add();
+
+ let k1 = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
+ k1.add("a");
+ k1.add("a");
+ k1.add("b");
+}
+
+function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {
+ const MANDATORY_PING_FIELDS = [
+ "type",
+ "id",
+ "creationDate",
+ "version",
+ "application",
+ "payload",
+ ];
+
+ const APPLICATION_TEST_DATA = {
+ buildId: gAppInfo.appBuildID,
+ name: APP_NAME,
+ version: APP_VERSION,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ // Check that the ping contains all the mandatory fields.
+ for (let f of MANDATORY_PING_FIELDS) {
+ Assert.ok(f in aPing, f + " must be available.");
+ }
+
+ Assert.equal(aPing.type, aType, "The ping must have the correct type.");
+ Assert.equal(
+ aPing.version,
+ PING_FORMAT_VERSION,
+ "The ping must have the correct version."
+ );
+
+ // Test the application section.
+ for (let f in APPLICATION_TEST_DATA) {
+ Assert.equal(
+ aPing.application[f],
+ APPLICATION_TEST_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // We can't check the values for channel and architecture. Just make
+ // sure they are in.
+ Assert.ok(
+ "architecture" in aPing.application,
+ "The application section must have an architecture field."
+ );
+ Assert.ok(
+ "channel" in aPing.application,
+ "The application section must have a channel field."
+ );
+
+ // Check the clientId and environment fields, as needed.
+ Assert.equal("clientId" in aPing, aHasClientId);
+ Assert.equal("environment" in aPing, aHasEnvironment);
+}
+
+function checkPayloadInfo(data, reason) {
+ const ALLOWED_REASONS = [
+ "environment-change",
+ "shutdown",
+ "daily",
+ "saved-session",
+ "test-ping",
+ ];
+ let numberCheck = arg => {
+ return typeof arg == "number";
+ };
+ let positiveNumberCheck = arg => {
+ return numberCheck(arg) && arg >= 0;
+ };
+ let stringCheck = arg => {
+ return typeof arg == "string" && arg != "";
+ };
+ let revisionCheck = arg => {
+ return AppConstants.MOZILLA_OFFICIAL
+ ? stringCheck(arg)
+ : typeof arg == "string";
+ };
+ let uuidCheck = arg => {
+ return UUID_REGEX.test(arg);
+ };
+ let isoDateCheck = arg => {
+ // We expect use of this version of the ISO format:
+ // 2015-04-12T18:51:19.1+00:00
+ const isoDateRegEx =
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{2}:\d{2}$/;
+ return (
+ stringCheck(arg) &&
+ !Number.isNaN(Date.parse(arg)) &&
+ isoDateRegEx.test(arg)
+ );
+ };
+
+ const EXPECTED_INFO_FIELDS_TYPES = {
+ reason: stringCheck,
+ revision: revisionCheck,
+ timezoneOffset: numberCheck,
+ sessionId: uuidCheck,
+ subsessionId: uuidCheck,
+ // Special cases: previousSessionId and previousSubsessionId are null on first run.
+ previousSessionId: arg => {
+ return arg ? uuidCheck(arg) : true;
+ },
+ previousSubsessionId: arg => {
+ return arg ? uuidCheck(arg) : true;
+ },
+ subsessionCounter: positiveNumberCheck,
+ profileSubsessionCounter: positiveNumberCheck,
+ sessionStartDate: isoDateCheck,
+ subsessionStartDate: isoDateCheck,
+ subsessionLength: positiveNumberCheck,
+ };
+
+ for (let f in EXPECTED_INFO_FIELDS_TYPES) {
+ Assert.ok(f in data, f + " must be available.");
+
+ let checkFunc = EXPECTED_INFO_FIELDS_TYPES[f];
+ Assert.ok(
+ checkFunc(data[f]),
+ f + " must have the correct type and valid data " + data[f]
+ );
+ }
+
+ // Check for a valid revision.
+ if (data.revision != "") {
+ const revisionUrlRegEx =
+ /^http[s]?:\/\/hg.mozilla.org(\/[a-z\S]+)+(\/rev\/[0-9a-z]+)$/g;
+ Assert.ok(revisionUrlRegEx.test(data.revision));
+ }
+
+ // Previous buildId is not mandatory.
+ if (data.previousBuildId) {
+ Assert.ok(stringCheck(data.previousBuildId));
+ }
+
+ Assert.ok(
+ ALLOWED_REASONS.find(r => r == data.reason),
+ "Payload must contain an allowed reason."
+ );
+ Assert.equal(data.reason, reason, "Payload reason must match expected.");
+
+ Assert.ok(
+ Date.parse(data.subsessionStartDate) >= Date.parse(data.sessionStartDate)
+ );
+ Assert.ok(data.profileSubsessionCounter >= data.subsessionCounter);
+
+ // According to https://en.wikipedia.org/wiki/List_of_UTC_time_offsets,
+ // UTC offsets range from -12 to +14 hours.
+ // Don't think the extremes of the range are affected by further
+ // daylight-savings adjustments, but it is possible.
+ Assert.ok(
+ data.timezoneOffset >= -12 * 60,
+ "The timezone must be in a valid range."
+ );
+ Assert.ok(
+ data.timezoneOffset <= 14 * 60,
+ "The timezone must be in a valid range."
+ );
+}
+
+function checkScalars(processes) {
+ // Check that the scalars section is available in the ping payload.
+ const parentProcess = processes.parent;
+ Assert.ok(
+ "scalars" in parentProcess,
+ "The scalars section must be available in the parent process."
+ );
+ Assert.ok(
+ "keyedScalars" in parentProcess,
+ "The keyedScalars section must be available in the parent process."
+ );
+ Assert.equal(
+ typeof parentProcess.scalars,
+ "object",
+ "The scalars entry must be an object."
+ );
+ Assert.equal(
+ typeof parentProcess.keyedScalars,
+ "object",
+ "The keyedScalars entry must be an object."
+ );
+
+ let checkScalar = function (scalar, name) {
+ // Check if the value is of a supported type.
+ const valueType = typeof scalar;
+ switch (valueType) {
+ case "string":
+ Assert.ok(
+ scalar.length <= 50,
+ "String values can't have more than 50 characters"
+ );
+ break;
+ case "number":
+ Assert.ok(
+ scalar >= 0,
+ "We only support unsigned integer values in scalars."
+ );
+ break;
+ case "boolean":
+ Assert.ok(true, "Boolean scalar found.");
+ break;
+ default:
+ Assert.ok(
+ false,
+ name + " contains an unsupported value type (" + valueType + ")"
+ );
+ }
+ };
+
+ // Check that we have valid scalar entries.
+ const scalars = parentProcess.scalars;
+ for (let name in scalars) {
+ Assert.equal(typeof name, "string", "Scalar names must be strings.");
+ checkScalar(scalars[name], name);
+ }
+
+ // Check that we have valid keyed scalar entries.
+ const keyedScalars = parentProcess.keyedScalars;
+ for (let name in keyedScalars) {
+ Assert.equal(typeof name, "string", "Scalar names must be strings.");
+ Assert.ok(
+ Object.keys(keyedScalars[name]).length,
+ "The reported keyed scalars must contain at least 1 key."
+ );
+ for (let key in keyedScalars[name]) {
+ Assert.equal(typeof key, "string", "Keyed scalar keys must be strings.");
+ Assert.ok(
+ key.length <= 70,
+ "Keyed scalar keys can't have more than 70 characters."
+ );
+ checkScalar(scalars[name][key], name);
+ }
+ }
+}
+
+function checkPayload(payload, reason, successfulPings) {
+ Assert.ok("info" in payload, "Payload must contain an info section.");
+ checkPayloadInfo(payload.info, reason);
+
+ Assert.ok(payload.simpleMeasurements.totalTime >= 0);
+ Assert.equal(payload.simpleMeasurements.startupInterrupted, 1);
+ Assert.equal(payload.simpleMeasurements.shutdownDuration, SHUTDOWN_TIME);
+
+ let activeTicks = payload.simpleMeasurements.activeTicks;
+ Assert.ok(activeTicks >= 0);
+
+ if ("browser.timings.last_shutdown" in payload.processes.parent.scalars) {
+ Assert.equal(
+ payload.processes.parent.scalars["browser.timings.last_shutdown"],
+ SHUTDOWN_TIME
+ );
+ }
+
+ Assert.equal(
+ payload.simpleMeasurements.failedProfileLockCount,
+ FAILED_PROFILE_LOCK_ATTEMPTS
+ );
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let failedProfileLocksFile = profileDirectory.clone();
+ failedProfileLocksFile.append("Telemetry.FailedProfileLocks.txt");
+ Assert.ok(!failedProfileLocksFile.exists());
+
+ let isWindows = "@mozilla.org/windows-registry-key;1" in Cc;
+ if (isWindows) {
+ Assert.ok(payload.simpleMeasurements.startupSessionRestoreReadBytes > 0);
+ Assert.ok(payload.simpleMeasurements.startupSessionRestoreWriteBytes > 0);
+ }
+
+ const TELEMETRY_SEND_SUCCESS = "TELEMETRY_SEND_SUCCESS";
+ const TELEMETRY_SUCCESS = "TELEMETRY_SUCCESS";
+ const TELEMETRY_TEST_FLAG = "TELEMETRY_TEST_FLAG";
+ const TELEMETRY_TEST_COUNT = "TELEMETRY_TEST_COUNT";
+ const TELEMETRY_TEST_KEYED_FLAG = "TELEMETRY_TEST_KEYED_FLAG";
+ const TELEMETRY_TEST_KEYED_COUNT = "TELEMETRY_TEST_KEYED_COUNT";
+
+ if (successfulPings > 0) {
+ Assert.ok(TELEMETRY_SEND_SUCCESS in payload.histograms);
+ }
+ Assert.ok(TELEMETRY_TEST_FLAG in payload.histograms);
+ Assert.ok(TELEMETRY_TEST_COUNT in payload.histograms);
+
+ Assert.ok(!(IGNORE_CLONED_HISTOGRAM in payload.histograms));
+
+ // Flag histograms should automagically spring to life.
+ const expected_flag = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 3,
+ values: { 0: 1, 1: 0 },
+ sum: 0,
+ };
+ let flag = payload.histograms[TELEMETRY_TEST_FLAG];
+ Assert.deepEqual(flag, expected_flag);
+
+ // We should have a test count.
+ const expected_count = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: { 0: 1, 1: 0 },
+ sum: 1,
+ };
+ let count = payload.histograms[TELEMETRY_TEST_COUNT];
+ Assert.deepEqual(count, expected_count);
+
+ // There should be one successful report from the previous telemetry ping.
+ if (successfulPings > 0) {
+ const expected_tc = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 2,
+ values: { 0: 2, 1: successfulPings, 2: 0 },
+ sum: successfulPings,
+ };
+ let tc = payload.histograms[TELEMETRY_SUCCESS];
+ Assert.deepEqual(tc, expected_tc);
+ }
+
+ // The ping should include data from memory reporters. We can't check that
+ // this data is correct, because we can't control the values returned by the
+ // memory reporters. But we can at least check that the data is there.
+ //
+ // It's important to check for the presence of reporters with a mix of units,
+ // because MemoryTelemetry has separate logic for each one. But we can't
+ // currently check UNITS_COUNT_CUMULATIVE or UNITS_PERCENTAGE because
+ // Telemetry doesn't touch a memory reporter with these units that's
+ // available on all platforms.
+
+ Assert.ok("MEMORY_TOTAL" in payload.histograms); // UNITS_BYTES
+ Assert.ok("MEMORY_JS_GC_HEAP" in payload.histograms); // UNITS_BYTES
+ Assert.ok("MEMORY_JS_COMPARTMENTS_SYSTEM" in payload.histograms); // UNITS_COUNT
+
+ Assert.ok(
+ "mainThread" in payload.slowSQL && "otherThreads" in payload.slowSQL
+ );
+
+ // Check keyed histogram payload.
+
+ Assert.ok("keyedHistograms" in payload);
+ let keyedHistograms = payload.keyedHistograms;
+ Assert.ok(!(TELEMETRY_TEST_KEYED_FLAG in keyedHistograms));
+ Assert.ok(TELEMETRY_TEST_KEYED_COUNT in keyedHistograms);
+
+ const expected_keyed_count = {
+ a: {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: { 0: 2, 1: 0 },
+ sum: 2,
+ },
+ b: {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: { 0: 1, 1: 0 },
+ sum: 1,
+ },
+ };
+ Assert.deepEqual(
+ expected_keyed_count,
+ keyedHistograms[TELEMETRY_TEST_KEYED_COUNT]
+ );
+
+ Assert.ok(
+ "processes" in payload,
+ "The payload must have a processes section."
+ );
+ Assert.ok(
+ "parent" in payload.processes,
+ "There must be at least a parent process."
+ );
+
+ checkScalars(payload.processes);
+}
+
+function writeStringToFile(file, contents) {
+ let ostream = Cc[
+ "@mozilla.org/network/safe-file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(
+ file,
+ PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ RW_OWNER,
+ ostream.DEFER_OPEN
+ );
+ ostream.write(contents, contents.length);
+ ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ ostream.close();
+}
+
+function write_fake_shutdown_file() {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append("Telemetry.ShutdownTime.txt");
+ let contents = "" + SHUTDOWN_TIME;
+ writeStringToFile(file, contents);
+}
+
+function write_fake_failedprofilelocks_file() {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append("Telemetry.FailedProfileLocks.txt");
+ let contents = "" + FAILED_PROFILE_LOCK_ATTEMPTS;
+ writeStringToFile(file, contents);
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Make it look like we've previously failed to lock a profile a couple times.
+ write_fake_failedprofilelocks_file();
+
+ // Make it look like we've shutdown before.
+ write_fake_shutdown_file();
+
+ await new Promise(resolve =>
+ Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))
+ );
+});
+
+add_task(async function asyncSetup() {
+ await TelemetryController.testSetup();
+ // Load the client ID from the client ID provider to check for pings sanity.
+ gClientID = await ClientID.getClientID();
+});
+
+// Ensures that expired histograms are not part of the payload.
+add_task(async function test_expiredHistogram() {
+ let dummy = Telemetry.getHistogramById("TELEMETRY_TEST_EXPIRED");
+
+ dummy.add(1);
+
+ Assert.equal(
+ TelemetrySession.getPayload().histograms.TELEMETRY_TEST_EXPIRED,
+ undefined
+ );
+});
+
+add_task(async function sessionTimeExcludingAndIncludingSuspend() {
+ if (gIsAndroid) {
+ // We don't support this new probe on android at the moment.
+ return;
+ }
+ Preferences.set("toolkit.telemetry.testing.overrideProductsCheck", true);
+ await TelemetryController.testReset();
+ let subsession = TelemetrySession.getPayload("environment-change", true);
+ let parentScalars = subsession.processes.parent.scalars;
+
+ let withSuspend =
+ parentScalars["browser.engagement.session_time_including_suspend"];
+ let withoutSuspend =
+ parentScalars["browser.engagement.session_time_excluding_suspend"];
+
+ Assert.ok(
+ withSuspend > 0,
+ "The session time including suspend should be positive"
+ );
+
+ Assert.ok(
+ withoutSuspend > 0,
+ "The session time excluding suspend should be positive"
+ );
+
+ // Two things about the next assertion:
+ // 1. The two calls to get the two different uptime values are made
+ // separately, so we can't guarantee equality, even if we know the machine
+ // has not been suspended (for example because it's running in infra and
+ // was just booted). In this case the value should be close to each other.
+ // 2. This test will fail if the device running this has been suspended in
+ // between booting the Firefox process running this test, and doing the
+ // following assertion test, but that's unlikely in practice.
+ const max_delta_ms = 100;
+
+ Assert.ok(
+ withSuspend - withoutSuspend <= max_delta_ms,
+ "In test condition, the two uptimes should be close to each other"
+ );
+
+ // This however should always hold, except on Windows < 10, where the two
+ // clocks are from different system calls, and it can fail in test condition
+ // because the machine has not been suspended.
+ if (
+ AppConstants.platform != "win" ||
+ AppConstants.isPlatformAndVersionAtLeast("win", "10.0")
+ ) {
+ Assert.greaterOrEqual(
+ withSuspend,
+ withoutSuspend,
+ `The uptime with suspend must always been greater or equal to the uptime
+ without suspend`
+ );
+ }
+
+ Preferences.set("toolkit.telemetry.testing.overrideProductsCheck", false);
+});
+
+// Sends a ping to a non existing server. If we remove this test, we won't get
+// all the histograms we need in the main ping.
+add_task(async function test_noServerPing() {
+ await sendPing();
+ // We need two pings in order to make sure STARTUP_MEMORY_STORAGE_SQLIE histograms
+ // are initialised. See bug 1131585.
+ await sendPing();
+ // Allowing Telemetry to persist unsent pings as pending. If omitted may cause
+ // problems to the consequent tests.
+ await TelemetryController.testShutdown();
+});
+
+// Checks that a sent ping is correctly received by a dummy http server.
+add_task(async function test_simplePing() {
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.start();
+ Preferences.set(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ let now = new Date(2020, 1, 1, 12, 5, 6);
+ let expectedDate = new Date(2020, 1, 1, 12, 0, 0);
+ fakeNow(now);
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 5000);
+
+ const expectedSessionUUID = "bd314d15-95bf-4356-b682-b6c4a8942202";
+ const expectedSubsessionUUID = "3e2e5f6c-74ba-4e4d-a93f-a48af238a8c7";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+ await TelemetryController.testReset();
+
+ // Session and subsession start dates are faked during TelemetrySession setup. We can
+ // now fake the session duration.
+ const SESSION_DURATION_IN_MINUTES = 15;
+ fakeNow(new Date(2020, 1, 1, 12, SESSION_DURATION_IN_MINUTES, 0));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + SESSION_DURATION_IN_MINUTES * 60 * 1000
+ );
+
+ await sendPing();
+ let ping = await PingServer.promiseNextPing();
+
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that we get the data we expect.
+ let payload = ping.payload;
+ Assert.equal(payload.info.sessionId, expectedSessionUUID);
+ Assert.equal(payload.info.subsessionId, expectedSubsessionUUID);
+ let sessionStartDate = new Date(payload.info.sessionStartDate);
+ Assert.equal(sessionStartDate.toISOString(), expectedDate.toISOString());
+ let subsessionStartDate = new Date(payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+ Assert.equal(payload.info.subsessionLength, SESSION_DURATION_IN_MINUTES * 60);
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ await TelemetryController.testShutdown();
+});
+
+// Saves the current session histograms, reloads them, performs a ping
+// and checks that the dummy http server received both the previously
+// saved ping and the new one.
+add_task(async function test_saveLoadPing() {
+ // Let's start out with a defined state.
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ PingServer.clearRequests();
+
+ // Setup test data and trigger pings.
+ setupTestData();
+ await TelemetrySession.testSavePendingPing();
+ await sendPing();
+
+ // Get requests received by dummy server.
+ const requests = await PingServer.promiseNextRequests(2);
+
+ for (let req of requests) {
+ Assert.equal(
+ req.getHeader("content-type"),
+ "application/json; charset=UTF-8",
+ "The request must have the correct content-type."
+ );
+ }
+
+ // We decode both requests to check for the |reason|.
+ let pings = Array.from(requests, decodeRequestPayload);
+
+ // Check we have the correct two requests. Ordering is not guaranteed. The ping type
+ // is encoded in the URL.
+ if (pings[0].type != PING_TYPE_MAIN) {
+ pings.reverse();
+ }
+
+ checkPingFormat(pings[0], PING_TYPE_MAIN, true, true);
+ checkPayload(pings[0].payload, REASON_TEST_PING, 0);
+ checkPingFormat(pings[1], PING_TYPE_SAVED_SESSION, true, true);
+ checkPayload(pings[1].payload, REASON_SAVED_SESSION, 0);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_checkSubsessionScalars() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android.
+ return;
+ }
+
+ // Clear the scalars.
+ Telemetry.clearScalars();
+ await TelemetryController.testReset();
+
+ // Set some scalars.
+ const UINT_SCALAR = "telemetry.test.unsigned_int_kind";
+ const STRING_SCALAR = "telemetry.test.string_kind";
+ let expectedUint = 37;
+ let expectedString = "Test value. Yay.";
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.scalarSet(STRING_SCALAR, expectedString);
+
+ // Check that scalars are not available in classic pings but are in subsession
+ // pings. Also clear the subsession.
+ let classic = TelemetrySession.getPayload();
+ let subsession = TelemetrySession.getPayload("environment-change", true);
+
+ const TEST_SCALARS = [UINT_SCALAR, STRING_SCALAR];
+ for (let name of TEST_SCALARS) {
+ // Scalar must be reported in subsession pings (e.g. main).
+ Assert.ok(
+ name in subsession.processes.parent.scalars,
+ name + " must be reported in a subsession ping."
+ );
+ }
+ // No scalar must be reported in classic pings (e.g. saved-session).
+ Assert.ok(
+ !Object.keys(classic.processes.parent.scalars).length,
+ "Scalars must not be reported in a classic ping."
+ );
+
+ // And make sure that we're getting the right values in the
+ // subsession ping.
+ Assert.equal(
+ subsession.processes.parent.scalars[UINT_SCALAR],
+ expectedUint,
+ UINT_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ subsession.processes.parent.scalars[STRING_SCALAR],
+ expectedString,
+ STRING_SCALAR + " must contain the expected value."
+ );
+
+ // Since we cleared the subsession in the last getPayload(), check that
+ // breaking subsessions clears the scalars.
+ subsession = TelemetrySession.getPayload("environment-change");
+ for (let name of TEST_SCALARS) {
+ Assert.ok(
+ !(name in subsession.processes.parent.scalars),
+ name + " must be cleared with the new subsession."
+ );
+ }
+
+ // Check if setting the scalars again works as expected.
+ expectedUint = 85;
+ expectedString = "A creative different value";
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.scalarSet(STRING_SCALAR, expectedString);
+ subsession = TelemetrySession.getPayload("environment-change");
+ Assert.equal(
+ subsession.processes.parent.scalars[UINT_SCALAR],
+ expectedUint,
+ UINT_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ subsession.processes.parent.scalars[STRING_SCALAR],
+ expectedString,
+ STRING_SCALAR + " must contain the expected value."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_dailyCollection() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ let now = new Date(2030, 1, 1, 12, 0, 0);
+ let nowHour = new Date(2030, 1, 1, 12, 0, 0);
+ let schedulerTickCallback = null;
+
+ PingServer.clearRequests();
+
+ fakeNow(now);
+
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+
+ // Init and check timer.
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+
+ // Set histograms to expected state.
+ const COUNT_ID = "TELEMETRY_TEST_COUNT";
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const count = Telemetry.getHistogramById(COUNT_ID);
+ const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ count.clear();
+ keyed.clear();
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+ keyed.add("b", 1);
+
+ // Make sure the daily ping gets triggered.
+ let expectedDate = nowHour;
+ now = futureDate(nowHour, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ Assert.ok(!!schedulerTickCallback);
+ // Run a scheduler tick: it should trigger the daily ping.
+ await schedulerTickCallback();
+
+ // Collect the daily ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].b.sum, 2);
+
+ // The daily ping is rescheduled for "tomorrow".
+ expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
+ now = futureDate(now, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ // Run a scheduler tick. Trigger and collect another ping. The histograms should be reset.
+ await schedulerTickCallback();
+
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.ok(!(COUNT_ID in ping.payload.histograms));
+ Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms));
+
+ // Trigger and collect another daily ping, with the histograms being set again.
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+
+ // The daily ping is rescheduled for "tomorrow".
+ expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
+ now = futureDate(now, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ await schedulerTickCallback();
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].b.sum, 1);
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_dailyDuplication() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ await TelemetrySend.reset();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Make sure the daily ping gets triggered at midnight.
+ // We need to make sure that we trigger this after the period where we wait for
+ // the user to become idle.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Get the first daily ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+
+ // We don't expect to receive any other daily ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(
+ false,
+ "No more daily pings should be sent/received in this test."
+ );
+ });
+
+ // Set the current time to a bit after midnight.
+ let secondDailyDue = new Date(firstDailyDue);
+ secondDailyDue.setHours(0);
+ secondDailyDue.setMinutes(15);
+ fakeNow(secondDailyDue);
+
+ // Run a scheduler tick: it should NOT trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ PingServer.resetPingHandler();
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_dailyOverdue() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 11, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+
+ // Skip one hour ahead: nothing should be due.
+ now.setHours(now.getHours() + 1);
+ fakeNow(now);
+
+ // Assert if we receive something!
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(false, "No daily ping should be received if not overdue!.");
+ });
+
+ // This tick should not trigger any daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Restore the non asserting ping handler.
+ PingServer.resetPingHandler();
+ PingServer.clearRequests();
+
+ // Simulate an overdue ping: we're not close to midnight, but the last daily ping
+ // time is too long ago.
+ let dailyOverdue = new Date(2030, 1, 2, 13, 0, 0);
+ fakeNow(dailyOverdue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Get the first daily ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_environmentChange() {
+ if (gIsAndroid) {
+ // We don't split subsessions on environment changes yet on Android.
+ return;
+ }
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let now = fakeNow(2040, 1, 1, 12, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Preferences.reset(PREF_TEST);
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ ]);
+
+ // Setup.
+ await TelemetryController.testReset();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ // Set histograms to expected state.
+ const COUNT_ID = "TELEMETRY_TEST_COUNT";
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const count = Telemetry.getHistogramById(COUNT_ID);
+ const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ count.clear();
+ keyed.clear();
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+
+ // Trigger and collect environment-change ping.
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ let startHour = TelemetryUtils.truncateToHours(now);
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+
+ Preferences.set(PREF_TEST, 1);
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], undefined);
+ Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
+ let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), startHour.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);
+
+ // Trigger and collect another ping. The histograms should be reset.
+ startHour = TelemetryUtils.truncateToHours(now);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+
+ Preferences.set(PREF_TEST, 2);
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], 1);
+ Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), startHour.toISOString());
+
+ Assert.ok(!(COUNT_ID in ping.payload.histograms));
+ Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms));
+
+ // Trigger and collect another ping. The histograms should be reset.
+ startHour = TelemetryUtils.truncateToHours(now);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_experimentAnnotations_subsession() {
+ if (gIsAndroid) {
+ // We don't split subsessions on environment changes yet on Android.
+ return;
+ }
+
+ const EXPERIMENT1 = "experiment-1";
+ const EXPERIMENT1_BRANCH = "nice-branch";
+ const EXPERIMENT2 = "experiment-2";
+ const EXPERIMENT2_BRANCH = "other-branch";
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let now = fakeNow(2040, 1, 1, 12, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+
+ // Setup.
+ await TelemetryController.testReset();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ Assert.equal(TelemetrySession.getPayload().info.subsessionCounter, 1);
+
+ // Trigger a subsession split with a telemetry annotation.
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ let futureTestDate = futureDate(now, 10 * MILLISECONDS_PER_MINUTE);
+ now = fakeNow(futureTestDate);
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT1, EXPERIMENT1_BRANCH);
+
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping, "A ping must be received.");
+
+ Assert.equal(
+ ping.type,
+ PING_TYPE_MAIN,
+ "The received ping must be a 'main' ping."
+ );
+ Assert.equal(
+ ping.payload.info.reason,
+ REASON_ENVIRONMENT_CHANGE,
+ "The 'main' ping must be triggered by a change in the environment."
+ );
+ // We expect the current experiments to be reported in the next ping, not this
+ // one.
+ Assert.ok(
+ !("experiments" in ping.environment),
+ "The old environment must contain no active experiments."
+ );
+ // Since this change wasn't throttled, the subsession counter must increase.
+ Assert.equal(
+ TelemetrySession.getPayload().info.subsessionCounter,
+ 2,
+ "The experiment annotation must trigger a new subsession."
+ );
+
+ // Add another annotation to the environment. We're not advancing the fake
+ // timer, so no subsession split should happen due to throttling.
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT2, EXPERIMENT2_BRANCH);
+ Assert.equal(
+ TelemetrySession.getPayload().info.subsessionCounter,
+ 2,
+ "The experiment annotation must not trigger a new subsession " +
+ "if throttling happens."
+ );
+ let oldExperiments = TelemetryEnvironment.getActiveExperiments();
+
+ // Fake the timer and remove an annotation, we expect a new subsession split.
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+ TelemetryEnvironment.setExperimentInactive(EXPERIMENT1, EXPERIMENT1_BRANCH);
+
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping, "A ping must be received.");
+
+ Assert.equal(
+ ping.type,
+ PING_TYPE_MAIN,
+ "The received ping must be a 'main' ping."
+ );
+ Assert.equal(
+ ping.payload.info.reason,
+ REASON_ENVIRONMENT_CHANGE,
+ "The 'main' ping must be triggered by a change in the environment."
+ );
+ // We expect both experiments to be in this environment.
+ Assert.deepEqual(
+ ping.environment.experiments,
+ oldExperiments,
+ "The environment must contain both the experiments."
+ );
+ Assert.equal(
+ TelemetrySession.getPayload().info.subsessionCounter,
+ 3,
+ "The removing an experiment annotation must trigger a new subsession."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_savedPingsOnShutdown() {
+ await TelemetryController.testReset();
+
+ // Assure that we store the ping properly when saving sessions on shutdown.
+ // We make the TelemetryController shutdown to trigger a session save.
+ const dir = TelemetryStorage.pingDirectoryPath;
+ await IOUtils.remove(dir, { ignoreAbsent: true, recursive: true });
+ await IOUtils.makeDirectory(dir);
+ await TelemetryController.testShutdown();
+
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ const ping = await PingServer.promiseNextPing();
+
+ let expectedType = gIsAndroid ? PING_TYPE_SAVED_SESSION : PING_TYPE_MAIN;
+ let expectedReason = gIsAndroid ? REASON_SAVED_SESSION : REASON_SHUTDOWN;
+
+ checkPingFormat(ping, expectedType, true, true);
+ Assert.equal(ping.payload.info.reason, expectedReason);
+ Assert.equal(ping.clientId, gClientID);
+});
+
+add_task(async function test_sendShutdownPing() {
+ if (
+ gIsAndroid ||
+ (AppConstants.platform == "linux" && !Services.appinfo.is64Bit)
+ ) {
+ // We don't support the pingsender on Android, yet, see bug 1335917.
+ // We also don't suppor the pingsender testing on Treeherder for
+ // Linux 32 bit (due to missing libraries). So skip it there too.
+ // See bug 1310703 comment 78.
+ return;
+ }
+
+ let checkPendingShutdownPing = async function () {
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ Assert.equal(pendingPings.length, 1, "We expect 1 pending ping: shutdown.");
+ // Load the pings off the disk.
+ const shutdownPing = await TelemetryStorage.loadPendingPing(
+ pendingPings[0].id
+ );
+ Assert.ok(shutdownPing, "The 'shutdown' ping must be saved to disk.");
+ Assert.equal(
+ "shutdown",
+ shutdownPing.payload.info.reason,
+ "The 'shutdown' ping must be saved to disk."
+ );
+ };
+
+ Preferences.set(TelemetryUtils.Preferences.ShutdownPingSender, true);
+ Preferences.set(TelemetryUtils.Preferences.FirstRun, false);
+ // Make sure the reporting policy picks up the updated pref.
+ TelemetryReportingPolicy.testUpdateFirstRun();
+ PingServer.clearRequests();
+ Telemetry.clearScalars();
+
+ // Shutdown telemetry and wait for an incoming ping.
+ let nextPing = PingServer.promiseNextPing();
+ await TelemetryController.testShutdown();
+ let ping = await nextPing;
+
+ // Check that we received a shutdown ping.
+ checkPingFormat(ping, ping.type, true, true);
+ Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
+ Assert.equal(ping.clientId, gClientID);
+ // Try again, this time disable ping upload. The PingSender
+ // should not be sending any ping!
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Telemetry must not send pings if not allowed to.")
+ );
+ Preferences.set(TelemetryUtils.Preferences.FhrUploadEnabled, false);
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+
+ // Enable ping upload and signal an OS shutdown. The pingsender
+ // will not be spawned and no ping will be sent.
+ Preferences.set(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(null, "quit-application-forced");
+ await TelemetryController.testShutdown();
+ // After re-enabling FHR, wait for the new client ID
+ gClientID = await ClientID.getClientID();
+
+ // Check that the "shutdown" ping was correctly saved to disk.
+ await checkPendingShutdownPing();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+ Telemetry.clearScalars();
+
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(
+ null,
+ "quit-application-granted",
+ "syncShutdown"
+ );
+ await TelemetryController.testShutdown();
+ await checkPendingShutdownPing();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+
+ // Disable the "submission policy". The shutdown ping must not be sent.
+ Preferences.set(TelemetryUtils.Preferences.BypassNotification, false);
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+
+ // We cannot reset the BypassNotification pref, as we need it to be
+ // |true| in tests.
+ Preferences.set(TelemetryUtils.Preferences.BypassNotification, true);
+
+ // With both upload enabled and the policy shown, make sure we don't
+ // send the shutdown ping using the pingsender on the first
+ // subsession.
+ Preferences.set(TelemetryUtils.Preferences.FirstRun, true);
+ // Make sure the reporting policy picks up the updated pref.
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Clear the state and prepare for the next test.
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ PingServer.resetPingHandler();
+
+ // Check that we're able to send the shutdown ping using the pingsender
+ // from the first session if the related pref is on.
+ Preferences.set(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ true
+ );
+ Preferences.set(TelemetryUtils.Preferences.FirstRun, true);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ // Restart/shutdown telemetry and wait for an incoming ping.
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ ping = await PingServer.promiseNextPing();
+
+ // Check that we received a shutdown ping.
+ checkPingFormat(ping, ping.type, true, true);
+ Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
+ Assert.equal(ping.clientId, gClientID);
+
+ // Reset the pref and restart Telemetry.
+ Preferences.set(TelemetryUtils.Preferences.ShutdownPingSender, false);
+ Preferences.set(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ );
+ Preferences.reset(TelemetryUtils.Preferences.FirstRun);
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_sendFirstShutdownPing() {
+ if (
+ gIsAndroid ||
+ (AppConstants.platform == "linux" && !Services.appinfo.is64Bit)
+ ) {
+ // We don't support the pingsender on Android, yet, see bug 1335917.
+ // We also don't suppor the pingsender testing on Treeherder for
+ // Linux 32 bit (due to missing libraries). So skip it there too.
+ // See bug 1310703 comment 78.
+ return;
+ }
+
+ let storageContainsFirstShutdown = async function () {
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ let pings = await Promise.all(
+ pendingPings.map(async p => {
+ return TelemetryStorage.loadPendingPing(p.id);
+ })
+ );
+ return pings.find(p => p.type == "first-shutdown");
+ };
+
+ let checkShutdownNotSent = async function () {
+ // The failure-mode of the ping-sender is used to check that a ping was
+ // *not* sent. This can be combined with the state of the storage to infer
+ // the appropriate behavior from the preference flags.
+
+ // Assert failure if we recive a ping.
+ PingServer.registerPingHandler((req, res) => {
+ const receivedPing = decodeRequestPayload(req);
+ Assert.ok(
+ false,
+ `No ping should be received in this test (got ${receivedPing.id}).`
+ );
+ });
+
+ // Assert that pings are sent on first run, forcing a forced application
+ // quit. This should be equivalent to the first test in this suite.
+ Preferences.set(TelemetryUtils.Preferences.FirstRun, true);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(null, "quit-application-forced");
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ await storageContainsFirstShutdown(),
+ "The 'first-shutdown' ping must be saved to disk."
+ );
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that it's not sent during subsequent runs
+ Preferences.set(TelemetryUtils.Preferences.FirstRun, false);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(null, "quit-application-forced");
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await storageContainsFirstShutdown()),
+ "The 'first-shutdown' ping should only be written during first run."
+ );
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that the the ping is only sent if the flag is enabled.
+ Preferences.set(TelemetryUtils.Preferences.FirstRun, true);
+ Preferences.set(TelemetryUtils.Preferences.FirstShutdownPingEnabled, false);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await storageContainsFirstShutdown()),
+ "The 'first-shutdown' ping should only be written if enabled"
+ );
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that the the ping is not collected when the ping-sender is disabled.
+ // The information would be made irrelevant by the main-ping in the second session.
+ Preferences.set(TelemetryUtils.Preferences.FirstShutdownPingEnabled, true);
+ Preferences.set(TelemetryUtils.Preferences.ShutdownPingSender, false);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await storageContainsFirstShutdown()),
+ "The 'first-shutdown' ping should only be written if ping-sender is enabled"
+ );
+
+ // Clear the state and prepare for the next test.
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ PingServer.resetPingHandler();
+ };
+
+ // Remove leftover pending pings from other tests
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ Telemetry.clearScalars();
+
+ // Set testing invariants for FirstShutdownPingEnabled
+ Preferences.set(TelemetryUtils.Preferences.ShutdownPingSender, true);
+ Preferences.set(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ );
+
+ // Set primary conditions of the 'first-shutdown' ping
+ Preferences.set(TelemetryUtils.Preferences.FirstShutdownPingEnabled, true);
+ Preferences.set(TelemetryUtils.Preferences.FirstRun, true);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ // Assert general 'first-shutdown' use-case.
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, "first-shutdown", true, true);
+ Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
+ Assert.equal(ping.clientId, gClientID);
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that the shutdown is not sent under various conditions
+ await checkShutdownNotSent();
+
+ // Reset the pref and restart Telemetry.
+ Preferences.set(TelemetryUtils.Preferences.ShutdownPingSender, false);
+ Preferences.set(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ );
+ Preferences.set(TelemetryUtils.Preferences.FirstShutdownPingEnabled, false);
+ Preferences.reset(TelemetryUtils.Preferences.FirstRun);
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_savedSessionData() {
+ // Create the directory which will contain the data file, if it doesn't already
+ // exist.
+ await IOUtils.makeDirectory(DATAREPORTING_PATH);
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ // Write test data to the session data file.
+ const dataFilePath = PathUtils.join(DATAREPORTING_PATH, "session-state.json");
+ const sessionState = {
+ sessionId: null,
+ subsessionId: null,
+ profileSubsessionCounter: 3785,
+ };
+ await IOUtils.writeJSON(dataFilePath, sessionState);
+
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Preferences.reset(PREF_TEST);
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ ]);
+
+ // We expect one new subsession when starting TelemetrySession and one after triggering
+ // an environment change.
+ const expectedSubsessions = sessionState.profileSubsessionCounter + 2;
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android, so skip the next checks.
+ return;
+ }
+
+ // Start TelemetrySession so that it loads the session data file.
+ await TelemetryController.testReset();
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Watch a test preference, trigger and environment change and wait for it to propagate.
+ // _watchPreferences triggers a subsession notification
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ fakeNow(new Date(2050, 1, 1, 12, 0, 0));
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let changePromise = new Promise(resolve =>
+ TelemetryEnvironment.registerChangeListener("test_fake_change", resolve)
+ );
+ Preferences.set(PREF_TEST, 1);
+ await changePromise;
+ TelemetryEnvironment.unregisterChangeListener("test_fake_change");
+
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
+ await TelemetryController.testShutdown();
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ // Load back the serialised session data.
+ let data = await IOUtils.readJSON(dataFilePath);
+ Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(data.sessionId, expectedSessionUUID);
+ Assert.equal(data.subsessionId, expectedSubsessionUUID);
+});
+
+add_task(async function test_sessionData_ShortSession() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android, so skip the next checks.
+ return;
+ }
+
+ const SESSION_STATE_PATH = PathUtils.join(
+ DATAREPORTING_PATH,
+ "session-state.json"
+ );
+
+ // Remove the session state file.
+ await IOUtils.remove(SESSION_STATE_PATH, { ignoreAbsent: true });
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+
+ // We intentionally don't wait for the setup to complete and shut down to simulate
+ // short sessions. We expect the profile subsession counter to be 1.
+ TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Restore the UUID generation functions.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ // Start TelemetryController so that it loads the session data file. We expect the profile
+ // subsession counter to be incremented by 1 again.
+ await TelemetryController.testReset();
+
+ // We expect 2 profile subsession counter updates.
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, 2);
+ Assert.equal(payload.info.previousSessionId, expectedSessionUUID);
+ Assert.equal(payload.info.previousSubsessionId, expectedSubsessionUUID);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_invalidSessionData() {
+ // Create the directory which will contain the data file, if it doesn't already
+ // exist.
+ await IOUtils.makeDirectory(DATAREPORTING_PATH);
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ // Write test data to the session data file. This should fail to parse.
+ const dataFilePath = PathUtils.join(DATAREPORTING_PATH, "session-state.json");
+ const unparseableData = "{asdf:@äü";
+ await IOUtils.writeUTF8(dataFilePath, unparseableData, {
+ tmpPath: `${dataFilePath}.tmp`,
+ });
+
+ // Start TelemetryController so that it loads the session data file.
+ await TelemetryController.testReset();
+
+ // The session data file should not load. Only expect the current subsession.
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Write test data to the session data file. This should fail validation.
+ const sessionState = {
+ profileSubsessionCounter: "not-a-number?",
+ someOtherField: 12,
+ };
+ await IOUtils.writeJSON(dataFilePath, sessionState);
+
+ // The session data file should not load. Only expect the current subsession.
+ const expectedSubsessions = 1;
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+
+ // Start TelemetryController so that it loads the session data file.
+ await TelemetryController.testShutdown();
+ await TelemetryController.testReset();
+
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ await TelemetryController.testShutdown();
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ // Load back the serialised session data.
+ let data = await IOUtils.readJSON(dataFilePath);
+ Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(data.sessionId, expectedSessionUUID);
+ Assert.equal(data.subsessionId, expectedSubsessionUUID);
+});
+
+add_task(async function test_abortedSession() {
+ if (gIsAndroid) {
+ // We don't have the aborted session ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2040, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // This ping is not yet in the pending pings folder, so we can't access it using
+ // TelemetryStorage.popPendingPings().
+ let abortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);
+
+ // Validate the ping.
+ checkPingFormat(abortedSessionPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(abortedSessionPing.payload.info.reason, REASON_ABORTED_SESSION);
+
+ // Trigger a another aborted-session ping and check that it overwrites the previous one.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ await schedulerTickCallback();
+
+ let updatedAbortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);
+ checkPingFormat(updatedAbortedSessionPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(
+ updatedAbortedSessionPing.payload.info.reason,
+ REASON_ABORTED_SESSION
+ );
+ Assert.notEqual(abortedSessionPing.id, updatedAbortedSessionPing.id);
+ Assert.notEqual(
+ abortedSessionPing.creationDate,
+ updatedAbortedSessionPing.creationDate
+ );
+
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await IOUtils.exists(ABORTED_FILE)),
+ "No aborted session ping must be available after a shutdown."
+ );
+});
+
+add_task(async function test_abortedSession_Shutdown() {
+ if (gIsAndroid) {
+ // We don't have the aborted session ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ let schedulerTickCallback = null;
+ let now = fakeNow(2040, 1, 1, 0, 0, 0);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ fakeNow(futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS));
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // Remove the aborted session file and then shut down to make sure exceptions (e.g file
+ // not found) do not compromise the shutdown.
+ await IOUtils.remove(ABORTED_FILE);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_abortedDailyCoalescing() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ let schedulerTickCallback = null;
+ PingServer.clearRequests();
+
+ let nowDate = new Date(2009, 10, 18, 0, 0, 0);
+ fakeNow(nowDate);
+
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Delay the callback around midnight so that the aborted-session ping gets merged with the
+ // daily ping.
+ let dailyDueDate = futureDate(nowDate, MS_IN_ONE_DAY);
+ fakeNow(dailyDueDate);
+ // Trigger both the daily ping and the saved-session.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+
+ // Wait for the daily ping.
+ let dailyPing = await PingServer.promiseNextPing();
+ Assert.equal(dailyPing.payload.info.reason, REASON_DAILY);
+
+ // Check that an aborted session ping was also written to disk.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // Read aborted session ping and check that the session/subsession ids equal the
+ // ones in the daily ping.
+ let abortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);
+ Assert.equal(
+ abortedSessionPing.payload.info.sessionId,
+ dailyPing.payload.info.sessionId
+ );
+ Assert.equal(
+ abortedSessionPing.payload.info.subsessionId,
+ dailyPing.payload.info.subsessionId
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerComputerSleep() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ // Remove any aborted-session ping from the previous tests.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ // Set a fake current date and start Telemetry.
+ let nowDate = fakeNow(2009, 10, 18, 0, 0, 0);
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Set the current time 3 days in the future at midnight, before running the callback.
+ nowDate = fakeNow(futureDate(nowDate, 3 * MS_IN_ONE_DAY));
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+
+ let dailyPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ dailyPing.payload.info.reason,
+ REASON_DAILY,
+ "The wake notification should have triggered a daily ping."
+ );
+ Assert.equal(
+ dailyPing.creationDate,
+ nowDate.toISOString(),
+ "The daily ping date should be correct."
+ );
+
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // Now also test if we are sending a daily ping if we wake up on the next
+ // day even when the timer doesn't trigger.
+ // This can happen due to timeouts not running out during sleep times,
+ // see bug 1262386, bug 1204823 et al.
+ // Note that we don't get wake notifications on Linux due to bug 758848.
+ nowDate = fakeNow(futureDate(nowDate, 1 * MS_IN_ONE_DAY));
+
+ // We emulate the mentioned timeout behavior by sending the wake notification
+ // instead of triggering the timeout callback.
+ // This should trigger a daily ping, because we passed midnight.
+ Services.obs.notifyObservers(null, "wake_notification");
+
+ dailyPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ dailyPing.payload.info.reason,
+ REASON_DAILY,
+ "The wake notification should have triggered a daily ping."
+ );
+ Assert.equal(
+ dailyPing.creationDate,
+ nowDate.toISOString(),
+ "The daily ping date should be correct."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerEnvironmentReschedules() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ // Reset the test preference.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Preferences.reset(PREF_TEST);
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ ]);
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ // bug 1829855 - Sometimes the idle dispatch from a previous test interferes.
+ TelemetryScheduler.testReset();
+ PingServer.clearRequests();
+
+ // Set a fake current date and start Telemetry.
+ let nowDate = fakeNow(2060, 10, 18, 0, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ // Set the current time at midnight.
+ fakeNow(futureDate(nowDate, MS_IN_ONE_DAY));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+
+ // Trigger the environment change.
+ Preferences.set(PREF_TEST, 1);
+
+ // Wait for the environment-changed ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.equal(ping.type, "main", `Expected 'main' ping on ${ping.id}`);
+ Assert.equal(
+ ping.payload.info.reason,
+ "environment-change",
+ `Expected 'environment-change' reason on ${ping.id}`
+ );
+
+ // We don't expect to receive any daily ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ const receivedPing = decodeRequestPayload(req);
+ Assert.ok(
+ false,
+ `No ping should be received in this test (got ${receivedPing.id} type: ${receivedPing.type} reason: ${receivedPing.payload.info.reason}).`
+ );
+ });
+
+ // Execute one scheduler tick. It should not trigger a daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerNothingDue() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Remove any aborted-session ping from the previous tests.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+
+ // We don't expect to receive any ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ const receivedPing = decodeRequestPayload(req);
+ Assert.ok(
+ false,
+ `No ping should be received in this test (got ${receivedPing.id}).`
+ );
+ });
+
+ // Set a current date/time away from midnight, so that the daily ping doesn't get
+ // sent.
+ let nowDate = new Date(2009, 10, 18, 11, 0, 0);
+ fakeNow(nowDate);
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Delay the callback execution to a time when no ping should be due.
+ let nothingDueDate = futureDate(
+ nowDate,
+ ABORTED_SESSION_UPDATE_INTERVAL_MS / 2
+ );
+ fakeNow(nothingDueDate);
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+
+ // Check that no aborted session ping was written to disk.
+ Assert.ok(!(await IOUtils.exists(ABORTED_FILE)));
+
+ await TelemetryController.testShutdown();
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_pingExtendedStats() {
+ const EXTENDED_PAYLOAD_FIELDS = [
+ "log",
+ "slowSQL",
+ "fileIOReports",
+ "lateWrites",
+ "addonDetails",
+ ];
+
+ // Reset telemetry and disable sending extended statistics.
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+ Telemetry.canRecordExtended = false;
+
+ await sendPing();
+
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that the payload does not contain extended statistics fields.
+ for (let f in EXTENDED_PAYLOAD_FIELDS) {
+ Assert.ok(
+ !(EXTENDED_PAYLOAD_FIELDS[f] in ping.payload),
+ EXTENDED_PAYLOAD_FIELDS[f] +
+ " must not be in the payload if the extended set is off."
+ );
+ }
+
+ // We check this one separately so that we can reuse EXTENDED_PAYLOAD_FIELDS below, since
+ // slowSQLStartup might not be there.
+ Assert.ok(
+ !("slowSQLStartup" in ping.payload),
+ "slowSQLStartup must not be sent if the extended set is off"
+ );
+
+ Assert.ok(
+ !("addonManager" in ping.payload.simpleMeasurements),
+ "addonManager must not be sent if the extended set is off."
+ );
+ Assert.ok(
+ !("UITelemetry" in ping.payload.simpleMeasurements),
+ "UITelemetry must not be sent."
+ );
+
+ // Restore the preference.
+ Telemetry.canRecordExtended = true;
+
+ // Send a new ping that should contain the extended data.
+ await sendPing();
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that the payload now contains extended statistics fields.
+ for (let f in EXTENDED_PAYLOAD_FIELDS) {
+ Assert.ok(
+ EXTENDED_PAYLOAD_FIELDS[f] in ping.payload,
+ EXTENDED_PAYLOAD_FIELDS[f] +
+ " must be in the payload if the extended set is on."
+ );
+ }
+
+ Assert.ok(
+ "addonManager" in ping.payload.simpleMeasurements,
+ "addonManager must be sent if the extended set is on."
+ );
+ Assert.ok(
+ !("UITelemetry" in ping.payload.simpleMeasurements),
+ "UITelemetry must not be sent."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerUserIdle() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const SCHEDULER_TICK_INTERVAL_MS = 5 * 60 * 1000;
+ const SCHEDULER_TICK_IDLE_INTERVAL_MS = 60 * 60 * 1000;
+
+ let now = new Date(2010, 1, 1, 11, 0, 0);
+ fakeNow(now);
+
+ let schedulerTimeout = 0;
+ fakeSchedulerTimer(
+ (callback, timeout) => {
+ schedulerTimeout = timeout;
+ },
+ () => {}
+ );
+ await TelemetryController.testReset();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ // When not idle, the scheduler should have a 5 minutes tick interval.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);
+
+ // Send an "idle" notification to the scheduler.
+ fakeIdleNotification("idle");
+
+ // When idle, the scheduler should have a 1hr tick interval.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_IDLE_INTERVAL_MS);
+
+ // Send an "active" notification to the scheduler.
+ await fakeIdleNotification("active");
+
+ // When user is back active, the scheduler tick should be 5 minutes again.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);
+
+ // We should not miss midnight when going to idle.
+ now.setHours(23);
+ now.setMinutes(50);
+ fakeNow(now);
+ fakeIdleNotification("idle");
+ Assert.equal(schedulerTimeout, 10 * 60 * 1000);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_DailyDueAndIdle() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let receivedPingRequest = null;
+ // Register a ping handler that will assert when receiving multiple daily pings.
+ PingServer.registerPingHandler(req => {
+ Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
+ receivedPingRequest = req;
+ });
+
+ // Faking scheduler timer has to happen before resetting TelemetryController
+ // to be effective.
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Trigger the daily ping.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ let tickPromise = schedulerTickCallback();
+
+ // Send an idle and then an active user notification.
+ fakeIdleNotification("idle");
+ fakeIdleNotification("active");
+
+ // Wait on the tick promise.
+ await tickPromise;
+
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Decode the ping contained in the request and check that's a daily ping.
+ Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
+ const receivedPing = decodeRequestPayload(receivedPingRequest);
+ checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_userIdleAndSchedlerTick() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ let receivedPingRequest = null;
+ // Register a ping handler that will assert when receiving multiple daily pings.
+ PingServer.registerPingHandler(req => {
+ Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
+ receivedPingRequest = req;
+ });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ PingServer.clearRequests();
+
+ // Move the current date/time to midnight.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // The active notification should trigger a scheduler tick. The latter will send the
+ // due daily ping.
+ fakeIdleNotification("active");
+
+ // Immediately running another tick should not send a daily ping again.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // A new "idle" notification should not send a new daily ping.
+ fakeIdleNotification("idle");
+
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Decode the ping contained in the request and check that's a daily ping.
+ Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
+ const receivedPing = decodeRequestPayload(receivedPingRequest);
+ checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);
+
+ PingServer.resetPingHandler();
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_changeThrottling() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android.
+ return;
+ }
+
+ let getSubsessionCount = () => {
+ return TelemetrySession.getPayload().info.subsessionCounter;
+ };
+
+ let now = fakeNow(2050, 1, 2, 0, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ await TelemetryController.testReset();
+ Assert.equal(getSubsessionCount(), 1);
+
+ // The first pref change should not trigger a notification.
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 1);
+
+ // We should get a change notification after the 5min throttling interval.
+ fakeNow(futureDate(now, 5 * MILLISECONDS_PER_MINUTE + 1));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 5 * MILLISECONDS_PER_MINUTE + 1
+ );
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 2);
+
+ // After that, changes should be throttled again.
+ now = fakeNow(futureDate(now, 1 * MILLISECONDS_PER_MINUTE));
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 1 * MILLISECONDS_PER_MINUTE);
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 2);
+
+ // ... for 5min.
+ now = fakeNow(futureDate(now, 4 * MILLISECONDS_PER_MINUTE + 1));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 4 * MILLISECONDS_PER_MINUTE + 1
+ );
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 3);
+});
+
+add_task(async function stopServer() {
+ // It is important to shut down the TelemetryController first as, due to test
+ // environment changes, failure to upload pings here during shutdown results
+ // in an infinite loop of send failures and retries.
+ await TelemetryController.testShutdown();
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js
new file mode 100644
index 0000000000..a248dd7b55
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+/**
+ * This file only contains the |test_abortedSessionQueued| test. This needs
+ * to be in a separate, stand-alone file since we're initializing Telemetry
+ * twice, in a non-standard way to simulate incorrect shutdowns. Doing this
+ * in other files might interfere with the other tests.
+ */
+
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+
+const DATAREPORTING_DIR = "datareporting";
+const ABORTED_PING_FILE_NAME = "aborted-session-ping";
+const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
+
+const PING_TYPE_MAIN = "main";
+const REASON_ABORTED_SESSION = "aborted-session";
+const TEST_PING_TYPE = "test-ping-type";
+
+XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function () {
+ return PathUtils.join(PathUtils.profileDir, DATAREPORTING_DIR);
+});
+
+function sendPing() {
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ } else {
+ TelemetrySend.setServer("http://doesnotexist");
+ }
+
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ };
+ return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
+}
+
+add_task(async function test_setup() {
+ do_get_profile();
+ PingServer.start();
+ Services.prefs.setCharPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+});
+
+add_task(async function test_abortedSessionQueued() {
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2040, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ !(await IOUtils.exists(ABORTED_FILE)),
+ "The aborted session ping must be removed from the aborted session ping directory."
+ );
+
+ // Restarting Telemetry again to trigger sending pings in TelemetrySend.
+ await TelemetryController.testReset();
+
+ // We should have received an aborted-session ping.
+ const receivedPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ receivedPing.type,
+ PING_TYPE_MAIN,
+ "Should have the correct type"
+ );
+ Assert.equal(
+ receivedPing.payload.info.reason,
+ REASON_ABORTED_SESSION,
+ "Ping should have the correct reason"
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+/*
+ * An aborted-session ping might have been written when Telemetry upload was disabled and
+ * the profile had a canary client ID.
+ * These pings should not be sent out at a later point when Telemetry is enabled again.
+ */
+add_task(async function test_abortedSession_canary_clientid() {
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2040, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // Set clientID in aborted-session ping to canary value
+ let abortedPing = await IOUtils.readJSON(ABORTED_FILE);
+ abortedPing.clientId = TelemetryUtils.knownClientID;
+ await IOUtils.writeJSON(ABORTED_FILE, abortedPing);
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ !(await IOUtils.exists(ABORTED_FILE)),
+ "The aborted session ping must be removed from the aborted session ping directory."
+ );
+
+ // Restarting Telemetry again to trigger sending pings in TelemetrySend.
+ await TelemetryController.testReset();
+
+ // Trigger a test ping, so we can verify the server received something.
+ sendPing();
+
+ // We should have received an aborted-session ping.
+ const receivedPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ receivedPing.type,
+ TEST_PING_TYPE,
+ "Should have received test ping"
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function stopServer() {
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySession_activeTicks.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySession_activeTicks.js
new file mode 100644
index 0000000000..23e476a028
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession_activeTicks.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+
+function tick(aHowMany) {
+ for (let i = 0; i < aHowMany; i++) {
+ Services.obs.notifyObservers(null, "user-interaction-active");
+ }
+}
+
+function checkSessionTicks(aExpected) {
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(
+ payload.simpleMeasurements.activeTicks,
+ aExpected,
+ "Should record the expected number of active ticks for the session."
+ );
+}
+
+function checkSubsessionTicks(aExpected, aClearSubsession) {
+ let payload = TelemetrySession.getPayload("main", aClearSubsession);
+ Assert.equal(
+ payload.simpleMeasurements.activeTicks,
+ aExpected,
+ "Should record the expected number of active ticks for the subsession."
+ );
+ if (aExpected > 0) {
+ Assert.equal(
+ payload.processes.parent.scalars["browser.engagement.active_ticks"],
+ aExpected,
+ "Should record the expected number of active ticks for the subsession, in a scalar."
+ );
+ }
+}
+
+add_task(async function test_setup() {
+ do_get_profile();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ // Ensure FOG's init
+ Services.fog.initializeFOG();
+});
+
+add_task(async function test_record_activeTicks() {
+ await TelemetryController.testSetup();
+
+ let checkActiveTicks = expected => {
+ // Scalars are only present in subsession payloads.
+ let payload = TelemetrySession.getPayload("main");
+ Assert.equal(
+ payload.simpleMeasurements.activeTicks,
+ expected,
+ "TelemetrySession must record the expected number of active ticks (in simpleMeasurements)."
+ );
+ // Subsessions are not yet supported on Android.
+ if (!gIsAndroid) {
+ Assert.equal(
+ payload.processes.parent.scalars["browser.engagement.active_ticks"],
+ expected,
+ "TelemetrySession must record the expected number of active ticks (in scalars)."
+ );
+ }
+ Assert.equal(Glean.browserEngagement.activeTicks.testGetValue(), expected);
+ };
+
+ for (let i = 0; i < 3; i++) {
+ Services.obs.notifyObservers(null, "user-interaction-active");
+ }
+ checkActiveTicks(3);
+
+ // Now send inactive. This must not increment the active ticks.
+ Services.obs.notifyObservers(null, "user-interaction-inactive");
+ checkActiveTicks(3);
+
+ // If we send active again, this should be counted as inactive.
+ Services.obs.notifyObservers(null, "user-interaction-active");
+ checkActiveTicks(3);
+
+ // If we send active again, this should be counted as active.
+ Services.obs.notifyObservers(null, "user-interaction-active");
+ checkActiveTicks(4);
+
+ Services.obs.notifyObservers(null, "user-interaction-active");
+ checkActiveTicks(5);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_subsession_activeTicks() {
+ await TelemetryController.testReset();
+ Telemetry.clearScalars();
+
+ tick(5);
+ checkSessionTicks(5);
+ checkSubsessionTicks(5, true);
+
+ // After clearing the subsession, subsession ticks should be 0 but session
+ // ticks should still be 5.
+ checkSubsessionTicks(0);
+ checkSessionTicks(5);
+
+ tick(1);
+ checkSessionTicks(6);
+ checkSubsessionTicks(1, true);
+
+ checkSubsessionTicks(0);
+ checkSessionTicks(6);
+
+ tick(2);
+ checkSessionTicks(8);
+ checkSubsessionTicks(2);
+
+ await TelemetryController.testShutdown();
+ }
+);
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js b/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js
new file mode 100644
index 0000000000..d9e5e08625
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const HIST_NAME = "TELEMETRY_SEND_SUCCESS";
+const HIST_NAME2 = "RANGE_CHECKSUM_ERRORS";
+const KEYED_HIST = { id: "TELEMETRY_INVALID_PING_TYPE_SUBMITTED", key: "TEST" };
+
+var refObj = {},
+ refObj2 = {};
+
+var originalCount1, originalCount2, originalCount3;
+
+function run_test() {
+ let histogram = Telemetry.getHistogramById(HIST_NAME);
+ let snapshot = histogram.snapshot();
+ originalCount1 = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ histogram = Telemetry.getHistogramById(HIST_NAME2);
+ snapshot = histogram.snapshot();
+ originalCount2 = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ histogram = Telemetry.getKeyedHistogramById(KEYED_HIST.id);
+ snapshot = histogram.snapshot()[KEYED_HIST.key] || { values: [] };
+ originalCount3 = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ Assert.ok(TelemetryStopwatch.start("mark1"));
+ Assert.ok(TelemetryStopwatch.start("mark2"));
+
+ Assert.ok(TelemetryStopwatch.start("mark1", refObj));
+ Assert.ok(TelemetryStopwatch.start("mark2", refObj));
+
+ // Same timer can't be re-started before being stopped
+ Assert.ok(!TelemetryStopwatch.start("mark1"));
+ Assert.ok(!TelemetryStopwatch.start("mark1", refObj));
+
+ // Can't stop a timer that was accidentaly started twice
+ Assert.ok(!TelemetryStopwatch.finish("mark1"));
+ Assert.ok(!TelemetryStopwatch.finish("mark1", refObj));
+
+ Assert.ok(TelemetryStopwatch.start("NON-EXISTENT_HISTOGRAM"));
+ Assert.ok(!TelemetryStopwatch.finish("NON-EXISTENT_HISTOGRAM"));
+
+ Assert.ok(TelemetryStopwatch.start("NON-EXISTENT_HISTOGRAM", refObj));
+ Assert.ok(!TelemetryStopwatch.finish("NON-EXISTENT_HISTOGRAM", refObj));
+
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2, refObj));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj2));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2, refObj2));
+
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME2));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME2, refObj));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME, refObj2));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME2, refObj2));
+
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME2));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME2, refObj));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME, refObj2));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME2, refObj2));
+
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME2));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME2, refObj));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME, refObj2));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME2, refObj2));
+
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2, refObj));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj2));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2, refObj2));
+
+ // Verify that TS.finish deleted the timers
+ Assert.ok(!TelemetryStopwatch.finish(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.finish(HIST_NAME, refObj));
+
+ // Verify that they can be used again
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME, refObj));
+
+ Assert.ok(!TelemetryStopwatch.finish("unknown-mark")); // Unknown marker
+ Assert.ok(!TelemetryStopwatch.finish("unknown-mark", {})); // Unknown object
+ Assert.ok(!TelemetryStopwatch.finish(HIST_NAME, {})); // Known mark on unknown object
+
+ // Test cancel
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.cancel(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.cancel(HIST_NAME, refObj));
+
+ // Verify that can not cancel twice
+ Assert.ok(!TelemetryStopwatch.cancel(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.cancel(HIST_NAME, refObj));
+
+ // Verify that cancel removes the timers
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(!TelemetryStopwatch.finish(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.finish(HIST_NAME, refObj));
+
+ // Verify that keyed histograms can be started.
+ Assert.ok(!TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY1"));
+ Assert.ok(!TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY2"));
+ Assert.ok(!TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY1", refObj));
+ Assert.ok(!TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY2", refObj));
+
+ Assert.ok(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1"));
+ Assert.ok(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY2"));
+ Assert.ok(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1", refObj));
+ Assert.ok(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY2", refObj));
+
+ Assert.ok(TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY1"));
+ Assert.ok(TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY2"));
+ Assert.ok(TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY1", refObj));
+ Assert.ok(TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY2", refObj));
+
+ // Restarting keyed histograms should fail.
+ Assert.ok(!TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1"));
+ Assert.ok(!TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1", refObj));
+
+ // Finishing a stopwatch of a non existing histogram should return false.
+ Assert.ok(!TelemetryStopwatch.finishKeyed("HISTOGRAM", "KEY2"));
+ Assert.ok(!TelemetryStopwatch.finishKeyed("HISTOGRAM", "KEY2", refObj));
+
+ // Starting & finishing a keyed stopwatch for an existing histogram should work.
+ Assert.ok(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ Assert.ok(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ // Verify that TS.finish deleted the timers
+ Assert.ok(!TelemetryStopwatch.runningKeyed(KEYED_HIST.id, KEYED_HIST.key));
+
+ // Verify that they can be used again
+ Assert.ok(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ Assert.ok(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, KEYED_HIST.key));
+
+ Assert.ok(!TelemetryStopwatch.finishKeyed("unknown-mark", "unknown-key"));
+ Assert.ok(!TelemetryStopwatch.finishKeyed(KEYED_HIST.id, "unknown-key"));
+
+ // Verify that keyed histograms can only be canceled through "keyed" API.
+ Assert.ok(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ Assert.throws(
+ () => TelemetryStopwatch.cancel(KEYED_HIST.id, KEYED_HIST.key),
+ /is not an object/
+ );
+ Assert.ok(TelemetryStopwatch.cancelKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ Assert.ok(!TelemetryStopwatch.cancelKeyed(KEYED_HIST.id, KEYED_HIST.key));
+
+ finishTest();
+}
+
+function finishTest() {
+ let histogram = Telemetry.getHistogramById(HIST_NAME);
+ let snapshot = histogram.snapshot();
+ let newCount = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ Assert.equal(
+ newCount - originalCount1,
+ 5,
+ "The correct number of histograms were added for histogram 1."
+ );
+
+ histogram = Telemetry.getHistogramById(HIST_NAME2);
+ snapshot = histogram.snapshot();
+ newCount = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ Assert.equal(
+ newCount - originalCount2,
+ 3,
+ "The correct number of histograms were added for histogram 2."
+ );
+
+ histogram = Telemetry.getKeyedHistogramById(KEYED_HIST.id);
+ snapshot = histogram.snapshot()[KEYED_HIST.key];
+ newCount = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ Assert.equal(
+ newCount - originalCount3,
+ 2,
+ "The correct number of histograms were added for histogram 3."
+ );
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js b/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
new file mode 100644
index 0000000000..37524fbb91
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+
+// The @mozilla/xre/app-info;1 XPCOM object provided by the xpcshell test harness doesn't
+// implement the nsIXULAppInfo interface, which is needed by Services and
+// TelemetrySession.sys.mjs. updateAppInfo() creates and registers a minimal mock app-info.
+const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+function getSimpleMeasurementsFromTelemetryController() {
+ return TelemetrySession.getPayload().simpleMeasurements;
+}
+
+add_task(async function test_setup() {
+ // Telemetry needs the AddonManager.
+ await loadAddonManager();
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ // Make profile available for |TelemetryController.testShutdown()|.
+ do_get_profile();
+
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ await new Promise(resolve =>
+ Services.telemetry.asyncFetchTelemetryData(resolve)
+ );
+});
+
+add_task(async function actualTest() {
+ await TelemetryController.testSetup();
+
+ // Test the module logic
+ let { TelemetryTimestamps } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryTimestamps.sys.mjs"
+ );
+ let now = Date.now();
+ TelemetryTimestamps.add("foo");
+ Assert.ok(TelemetryTimestamps.get().foo != null); // foo was added
+ Assert.ok(TelemetryTimestamps.get().foo >= now); // foo has a reasonable value
+
+ // Add timestamp with value
+ // Use a value far in the future since TelemetryController substracts the time of
+ // process initialization.
+ const YEAR_4000_IN_MS = 64060588800000;
+ TelemetryTimestamps.add("bar", YEAR_4000_IN_MS);
+ Assert.equal(TelemetryTimestamps.get().bar, YEAR_4000_IN_MS); // bar has the right value
+
+ // Can't add the same timestamp twice
+ TelemetryTimestamps.add("bar", 2);
+ Assert.equal(TelemetryTimestamps.get().bar, YEAR_4000_IN_MS); // bar wasn't overwritten
+
+ let threw = false;
+ try {
+ TelemetryTimestamps.add("baz", "this isn't a number");
+ } catch (ex) {
+ threw = true;
+ }
+ Assert.ok(threw); // adding non-number threw
+ Assert.equal(null, TelemetryTimestamps.get().baz); // no baz was added
+
+ // Test that the data gets added to the telemetry ping properly
+ let simpleMeasurements = getSimpleMeasurementsFromTelemetryController();
+ Assert.ok(simpleMeasurements != null); // got simple measurements from ping data
+ Assert.ok(simpleMeasurements.foo > 1); // foo was included
+ Assert.ok(simpleMeasurements.bar > 1); // bar was included
+ Assert.equal(undefined, simpleMeasurements.baz); // baz wasn't included since it wasn't added
+
+ await TelemetryController.testShutdown();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryUtils.js b/toolkit/components/telemetry/tests/unit/test_TelemetryUtils.js
new file mode 100644
index 0000000000..fd4cf5304f
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryUtils.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+const { UpdateUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/UpdateUtils.sys.mjs"
+);
+
+add_task(async function testUpdateChannelOverride() {
+ if (Preferences.has(TelemetryUtils.Preferences.OverrideUpdateChannel)) {
+ // If the pref is already set at this point, the test is running in a build
+ // that makes use of the override pref. For testing purposes, unset the pref.
+ Preferences.set(TelemetryUtils.Preferences.OverrideUpdateChannel, "");
+ }
+
+ // Check that we return the same channel as UpdateUtils, by default
+ Assert.equal(
+ TelemetryUtils.getUpdateChannel(),
+ UpdateUtils.getUpdateChannel(false),
+ "The telemetry reported channel must match the one from UpdateChannel, by default."
+ );
+
+ // Now set the override pref and check that we return the correct channel
+ const OVERRIDE_TEST_CHANNEL = "nightly-test";
+ Preferences.set(
+ TelemetryUtils.Preferences.OverrideUpdateChannel,
+ OVERRIDE_TEST_CHANNEL
+ );
+ Assert.equal(
+ TelemetryUtils.getUpdateChannel(),
+ OVERRIDE_TEST_CHANNEL,
+ "The telemetry reported channel must match the override pref when pref is set."
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_ThirdPartyModulesPing.js b/toolkit/components/telemetry/tests/unit/test_ThirdPartyModulesPing.js
new file mode 100644
index 0000000000..2da3a3bc5e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ThirdPartyModulesPing.js
@@ -0,0 +1,282 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const kDllName = "modules-test.dll";
+
+let gCurrentPidStr;
+
+async function load_and_free(name) {
+ // Dynamically load a DLL which we have hard-coded as untrusted; this should
+ // appear in the payload.
+ let dllHandle = ctypes.open(do_get_file(name).path);
+ if (dllHandle) {
+ dllHandle.close();
+ dllHandle = null;
+ }
+ // Give the thread some cycles to process a loading event.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 50));
+}
+
+add_task(async function setup() {
+ do_get_profile();
+
+ // Dynamically load a DLL which we have hard-coded as untrusted; this should
+ // appear in the payload.
+ await load_and_free(kDllName);
+
+ // Force the timer to fire (using a small interval).
+ Cc["@mozilla.org/updates/timer-manager;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "utm-test-init", "");
+ Preferences.set("toolkit.telemetry.untrustedModulesPing.frequency", 0);
+ Preferences.set("app.update.url", "http://localhost");
+
+ let currentPid = Services.appinfo.processID;
+ gCurrentPidStr = "browser.0x" + currentPid.toString(16);
+
+ // Start the local ping server and setup Telemetry to use it during the tests.
+ PingServer.start();
+ Preferences.set(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ return TelemetryController.testSetup();
+});
+
+registerCleanupFunction(function () {
+ return PingServer.stop();
+});
+
+// This tests basic end-to-end functionality of the untrusted modules
+// telemetry ping. We force the ping to fire, capture the result, and test for:
+// - Basic payload structure validity.
+// - Expected results for a few specific DLLs
+add_task(async function test_send_ping() {
+ let expectedModules = [
+ // This checks that a DLL loaded during runtime is evaluated properly.
+ // This is hard-coded as untrusted in toolkit/xre/UntrustedModules.cpp for
+ // testing purposes.
+ {
+ nameMatch: new RegExp(kDllName, "i"),
+ expectedTrusted: false,
+ wasFound: false,
+ },
+ {
+ nameMatch: /kernelbase.dll/i,
+ expectedTrusted: true,
+ wasFound: false,
+ },
+ ];
+
+ // There is a tiny chance some other ping is being sent legitimately before
+ // the one we care about. Spin until we find the correct ping type.
+ let found;
+ while (true) {
+ found = await PingServer.promiseNextPing();
+ if (found.type == "third-party-modules") {
+ break;
+ }
+ }
+
+ // Test the ping payload's validity.
+ Assert.ok(found, "Untrusted modules ping submitted");
+ Assert.ok(found.environment, "Ping has an environment");
+ Assert.ok(typeof found.clientId != "undefined", "Ping has a client ID");
+
+ Assert.equal(found.payload.structVersion, 1, "Version is correct");
+ Assert.ok(found.payload.modules, "'modules' object exists");
+ Assert.ok(Array.isArray(found.payload.modules), "'modules' is an array");
+ Assert.ok(found.payload.blockedModules, "'blockedModules' object exists");
+ Assert.ok(
+ Array.isArray(found.payload.blockedModules),
+ "'blockedModules' is an array"
+ );
+ // Unfortunately, the way this test is run it doesn't usually get a launcher
+ // process, so the blockedModules member doesn't get populated. This is the
+ // same structure that's used in the about:third-party page, though, so we
+ // have coverage in browser_aboutthirdparty.js that this is correct.
+ Assert.ok(found.payload.processes, "'processes' object exists");
+ Assert.ok(
+ gCurrentPidStr in found.payload.processes,
+ `Current process "${gCurrentPidStr}" is included in payload`
+ );
+
+ let ourProcInfo = found.payload.processes[gCurrentPidStr];
+ Assert.equal(ourProcInfo.processType, "browser", "'processType' is correct");
+ Assert.ok(typeof ourProcInfo.elapsed == "number", "'elapsed' exists");
+ Assert.equal(
+ ourProcInfo.sanitizationFailures,
+ 0,
+ "'sanitizationFailures' is 0"
+ );
+ Assert.equal(ourProcInfo.trustTestFailures, 0, "'trustTestFailures' is 0");
+
+ Assert.equal(
+ ourProcInfo.combinedStacks.stacks.length,
+ ourProcInfo.events.length,
+ "combinedStacks.stacks.length == events.length"
+ );
+
+ for (let event of ourProcInfo.events) {
+ Assert.ok(
+ typeof event.processUptimeMS == "number",
+ "'processUptimeMS' exists"
+ );
+ Assert.ok(typeof event.threadID == "number", "'threadID' exists");
+ Assert.ok(typeof event.baseAddress == "string", "'baseAddress' exists");
+
+ Assert.ok(typeof event.moduleIndex == "number", "'moduleIndex' exists");
+ Assert.ok(event.moduleIndex >= 0, "'moduleIndex' is non-negative");
+
+ Assert.ok(typeof event.isDependent == "boolean", "'isDependent' exists");
+ Assert.ok(!event.isDependent, "'isDependent' is false");
+
+ Assert.ok(typeof event.loadStatus == "number", "'loadStatus' exists");
+ Assert.ok(event.loadStatus == 0, "'loadStatus' is 0 (Loaded)");
+
+ let modRecord = found.payload.modules[event.moduleIndex];
+ Assert.ok(modRecord, "module record for this event exists");
+ Assert.ok(
+ typeof modRecord.resolvedDllName == "string",
+ "'resolvedDllName' exists"
+ );
+ Assert.ok(typeof modRecord.trustFlags == "number", "'trustFlags' exists");
+
+ let mod = expectedModules.find(function (elem) {
+ return elem.nameMatch.test(modRecord.resolvedDllName);
+ });
+
+ if (mod) {
+ mod.wasFound = true;
+ }
+ }
+
+ for (let x of expectedModules) {
+ Assert.equal(
+ !x.wasFound,
+ x.expectedTrusted,
+ `Trustworthiness == expected for module: ${x.nameMatch.source}`
+ );
+ }
+});
+
+// This tests the flags INCLUDE_OLD_LOADEVENTS and KEEP_LOADEVENTS_NEW
+// controls the method's return value and the internal storages
+// "Staging" and "Settled" correctly.
+add_task(async function test_new_old_instances() {
+ const kIncludeOld = Telemetry.INCLUDE_OLD_LOADEVENTS;
+ const kKeepNew = Telemetry.KEEP_LOADEVENTS_NEW;
+ const get_events_count = data => data.processes[gCurrentPidStr].events.length;
+
+ // Make sure |baseline| has at least one instance.
+ await load_and_free(kDllName);
+
+ // Make sure all instances are "old"
+ const baseline = await Telemetry.getUntrustedModuleLoadEvents(kIncludeOld);
+ const baseline_count = get_events_count(baseline);
+ print("baseline_count = " + baseline_count);
+ print("baseline = " + JSON.stringify(baseline));
+
+ await Assert.rejects(
+ Telemetry.getUntrustedModuleLoadEvents(),
+ e => e.result == Cr.NS_ERROR_NOT_AVAILABLE,
+ "New instances should not exist!"
+ );
+
+ await load_and_free(kDllName); // A
+
+ // Passing kIncludeOld and kKeepNew is unsupported. A is kept new.
+ await Assert.rejects(
+ Telemetry.getUntrustedModuleLoadEvents(kIncludeOld | kKeepNew),
+ e => e.result == Cr.NS_ERROR_INVALID_ARG,
+ "Passing unsupported flag combination should throw an exception!"
+ );
+
+ await load_and_free(kDllName); // B
+
+ // After newly loading B, the new instances we have is {A, B}
+ // Both A and B are still kept new.
+ let payload = await Telemetry.getUntrustedModuleLoadEvents(kKeepNew);
+ print("payload = " + JSON.stringify(payload));
+ Assert.equal(get_events_count(payload), 2);
+
+ await load_and_free(kDllName); // C
+
+ // After newly loading C, the new instances we have is {A, B, C}
+ // All of A, B, and C are now marked as old.
+ payload = await Telemetry.getUntrustedModuleLoadEvents();
+ Assert.equal(get_events_count(payload), 3);
+
+ payload = await Telemetry.getUntrustedModuleLoadEvents(kIncludeOld);
+ // payload is {baseline, A, B, C}
+ Assert.equal(get_events_count(payload), baseline_count + 3);
+});
+
+// This tests the flag INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS returns
+// data including private fields.
+add_task(async function test_private_fields() {
+ await load_and_free(kDllName);
+ const data = await Telemetry.getUntrustedModuleLoadEvents(
+ Telemetry.KEEP_LOADEVENTS_NEW |
+ Telemetry.INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS
+ );
+
+ for (const module of data.modules) {
+ Assert.ok(!("resolvedDllName" in module));
+ Assert.ok("dllFile" in module);
+ Assert.ok(module.dllFile.QueryInterface);
+ Assert.ok(module.dllFile.QueryInterface(Ci.nsIFile));
+ }
+});
+
+// This tests the flag EXCLUDE_STACKINFO_FROM_LOADEVENTS correctly
+// merges "Staging" and "Settled" on a JS object correctly, and
+// the "combinedStacks" field is really excluded.
+add_task(async function test_exclude_stack() {
+ const baseline = await Telemetry.getUntrustedModuleLoadEvents(
+ Telemetry.EXCLUDE_STACKINFO_FROM_LOADEVENTS |
+ Telemetry.INCLUDE_OLD_LOADEVENTS
+ );
+ Assert.ok(!("combinedStacks" in baseline.processes[gCurrentPidStr]));
+ const baseSet = baseline.processes[gCurrentPidStr].events.map(
+ x => x.processUptimeMS
+ );
+
+ await load_and_free(kDllName);
+ await load_and_free(kDllName);
+ const newLoadsWithStack = await Telemetry.getUntrustedModuleLoadEvents(
+ Telemetry.KEEP_LOADEVENTS_NEW
+ );
+ Assert.ok("combinedStacks" in newLoadsWithStack.processes[gCurrentPidStr]);
+ const newSet = newLoadsWithStack.processes[gCurrentPidStr].events.map(
+ x => x.processUptimeMS
+ );
+
+ const merged = baseSet.concat(newSet);
+
+ const allData = await Telemetry.getUntrustedModuleLoadEvents(
+ Telemetry.KEEP_LOADEVENTS_NEW |
+ Telemetry.EXCLUDE_STACKINFO_FROM_LOADEVENTS |
+ Telemetry.INCLUDE_OLD_LOADEVENTS
+ );
+ Assert.ok(!("combinedStacks" in allData.processes[gCurrentPidStr]));
+ const allSet = allData.processes[gCurrentPidStr].events.map(
+ x => x.processUptimeMS
+ );
+
+ Assert.deepEqual(allSet.sort(), merged.sort());
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_UninstallPing.js b/toolkit/components/telemetry/tests/unit/test_UninstallPing.js
new file mode 100644
index 0000000000..d619ebb10e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_UninstallPing.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const { BasePromiseWorker } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseWorker.sys.mjs"
+);
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+const gFakeInstallPathHash = "0123456789ABCDEF";
+let gFakeVendorDirectory;
+let gFakeGetUninstallPingPath;
+
+add_setup(async function setup() {
+ do_get_profile();
+
+ let fakeVendorDirectoryNSFile = new FileUtils.File(
+ PathUtils.join(PathUtils.profileDir, "uninstall-ping-test")
+ );
+ fakeVendorDirectoryNSFile.createUnique(
+ Ci.nsIFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY
+ );
+ gFakeVendorDirectory = fakeVendorDirectoryNSFile.path;
+
+ gFakeGetUninstallPingPath = id => ({
+ directory: fakeVendorDirectoryNSFile.clone(),
+ file: `uninstall_ping_${gFakeInstallPathHash}_${id}.json`,
+ });
+
+ fakeUninstallPingPath(gFakeGetUninstallPingPath);
+
+ registerCleanupFunction(async () => {
+ await IOUtils.remove(gFakeVendorDirectory, { recursive: true });
+ });
+});
+
+function ping_path(ping) {
+ let { directory: pingFile, file } = gFakeGetUninstallPingPath(ping.id);
+ pingFile.append(file);
+ return pingFile.path;
+}
+
+add_task(async function test_store_ping() {
+ // Remove shouldn't throw on an empty dir.
+ await TelemetryStorage.removeUninstallPings();
+
+ // Write ping
+ const ping1 = {
+ id: "58b63aac-999e-4efb-9d5a-20f368670721",
+ payload: { some: "thing" },
+ };
+ const ping1Path = ping_path(ping1);
+ await TelemetryStorage.saveUninstallPing(ping1);
+
+ // Check the ping
+ Assert.ok(await IOUtils.exists(ping1Path));
+ const readPing1 = await IOUtils.readJSON(ping1Path);
+ Assert.deepEqual(ping1, readPing1);
+
+ // Write another file that shouldn't match the pattern
+ const otherFilePath = PathUtils.join(gFakeVendorDirectory, "other_file.json");
+ await IOUtils.writeUTF8(otherFilePath, "");
+ Assert.ok(await IOUtils.exists(otherFilePath));
+
+ // Write another ping, should remove the earlier one
+ const ping2 = {
+ id: "7202c564-8f23-41b4-8a50-1744e9549260",
+ payload: { another: "thing" },
+ };
+ const ping2Path = ping_path(ping2);
+ await TelemetryStorage.saveUninstallPing(ping2);
+
+ Assert.ok(!(await IOUtils.exists(ping1Path)));
+ Assert.ok(await IOUtils.exists(ping2Path));
+ Assert.ok(await IOUtils.exists(otherFilePath));
+
+ // Write an additional file manually so there are multiple matching pings to remove
+ const ping3 = { id: "yada-yada" };
+ const ping3Path = ping_path(ping3);
+
+ await IOUtils.writeUTF8(ping3Path, "");
+ Assert.ok(await IOUtils.exists(ping3Path));
+
+ // Remove pings
+ await TelemetryStorage.removeUninstallPings();
+
+ // Check our pings are removed but other file isn't
+ Assert.ok(!(await IOUtils.exists(ping1Path)));
+ Assert.ok(!(await IOUtils.exists(ping2Path)));
+ Assert.ok(!(await IOUtils.exists(ping3Path)));
+ Assert.ok(await IOUtils.exists(otherFilePath));
+
+ // Remove again, confirming that the remove doesn't cause an error if nothing to remove
+ await TelemetryStorage.removeUninstallPings();
+
+ const ping4 = {
+ id: "1f113673-753c-4fbe-9143-fe197f936036",
+ payload: { any: "thing" },
+ };
+ const ping4Path = ping_path(ping4);
+ await TelemetryStorage.saveUninstallPing(ping4);
+
+ // Use a worker to keep the ping file open, so a delete should fail.
+ const worker = new BasePromiseWorker(
+ "resource://test/file_UninstallPing.worker.js"
+ );
+ await worker.post("open", [ping4Path]);
+
+ // Check that there is no error if the file can't be removed.
+ await TelemetryStorage.removeUninstallPings();
+
+ // And file should still exist.
+ Assert.ok(await IOUtils.exists(ping4Path));
+
+ // Close the file, so it should be possible to remove now.
+ await worker.post("close");
+ await TelemetryStorage.removeUninstallPings();
+ Assert.ok(!(await IOUtils.exists(ping4Path)));
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_UserInteraction.js b/toolkit/components/telemetry/tests/unit/test_UserInteraction.js
new file mode 100644
index 0000000000..5fc3c5ecd1
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_UserInteraction.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_USER_INTERACTION_ID = "testing.interaction";
+const TEST_VALUE_1 = "some value";
+const TEST_VALUE_2 = "some other value";
+const TEST_INVALID_VALUE =
+ "This is a value that is far too long - it has too many characters.";
+const TEST_ADDITIONAL_TEXT_1 = "some additional text";
+const TEST_ADDITIONAL_TEXT_2 = "some other additional text";
+
+function run_test() {
+ let obj1 = {};
+ let obj2 = {};
+
+ Assert.ok(UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1));
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1)
+ );
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj2)
+ );
+
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+
+ // Unlike TelemetryStopwatch, we can clobber UserInteractions.
+ Assert.ok(UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1));
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1)
+ );
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj2)
+ );
+
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+
+ // Ensure that we can finish a UserInteraction that was accidentally started
+ // twice
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID, obj2));
+
+ // Make sure we can't start or finish non-existent UserInteractions.
+ Assert.ok(!UserInteraction.start("non-existent.interaction", TEST_VALUE_1));
+ Assert.ok(
+ !UserInteraction.start("non-existent.interaction", TEST_VALUE_1, obj1)
+ );
+ Assert.ok(
+ !UserInteraction.start("non-existent.interaction", TEST_VALUE_1, obj2)
+ );
+ Assert.ok(!UserInteraction.running("non-existent.interaction"));
+ Assert.ok(!UserInteraction.running("non-existent.interaction", obj1));
+ Assert.ok(!UserInteraction.running("non-existent.interaction", obj2));
+ Assert.ok(!UserInteraction.finish("non-existent.interaction"));
+ Assert.ok(!UserInteraction.finish("non-existent.interaction", obj1));
+ Assert.ok(!UserInteraction.finish("non-existent.interaction", obj2));
+
+ // Ensure that we enforce the character limit on value strings.
+ Assert.ok(
+ !UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_INVALID_VALUE)
+ );
+ Assert.ok(
+ !UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_INVALID_VALUE, obj1)
+ );
+ Assert.ok(
+ !UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_INVALID_VALUE, obj2)
+ );
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+
+ // Verify that they can be used again
+ Assert.ok(UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2));
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj1)
+ );
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj2)
+ );
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID, obj2));
+
+ Assert.ok(!UserInteraction.finish(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.finish(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(!UserInteraction.finish(TEST_USER_INTERACTION_ID, obj2));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+
+ // Verify that they can be used again with different values.
+ Assert.ok(UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1));
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj1)
+ );
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj2)
+ );
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID));
+ Assert.ok(
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj1,
+ TEST_ADDITIONAL_TEXT_1
+ )
+ );
+ Assert.ok(
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj2,
+ TEST_ADDITIONAL_TEXT_2
+ )
+ );
+
+ // Test that they can be cancelled
+ Assert.ok(UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1));
+ Assert.ok(UserInteraction.cancel(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.finish(TEST_USER_INTERACTION_ID));
+
+ // Test that they cannot be cancelled twice
+ Assert.ok(!UserInteraction.cancel(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.cancel(TEST_USER_INTERACTION_ID));
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js b/toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js
new file mode 100644
index 0000000000..a10021e088
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js
@@ -0,0 +1,470 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+const HANG_TIME = 1000; // ms
+const TEST_USER_INTERACTION_ID = "testing.interaction";
+const TEST_CLOBBERED_USER_INTERACTION_ID = `${TEST_USER_INTERACTION_ID} (clobbered)`;
+const TEST_VALUE_1 = "some value";
+const TEST_VALUE_2 = "some other value";
+const TEST_ADDITIONAL_TEXT_1 = "some additional text";
+const TEST_ADDITIONAL_TEXT_2 = "some other additional text";
+
+/**
+ * Intentionally hangs the main thread in the parent process for
+ * HANG_TIME, and then returns the BHR hang report generated for
+ * that hang.
+ *
+ * @returns {Promise}
+ * @resolves {nsIHangDetails}
+ * The hang report that was created.
+ */
+async function hangAndWaitForReport(expectTestAnnotation) {
+ let hangPromise = TestUtils.topicObserved("bhr-thread-hang", subject => {
+ let hang = subject.QueryInterface(Ci.nsIHangDetails);
+ if (hang.thread != "Gecko") {
+ return false;
+ }
+
+ if (expectTestAnnotation) {
+ return hang.annotations.some(annotation =>
+ annotation[0].startsWith(TEST_USER_INTERACTION_ID)
+ );
+ }
+
+ return hang.annotations.every(
+ annotation => annotation[0] != TEST_USER_INTERACTION_ID
+ );
+ });
+
+ executeSoon(() => {
+ let startTime = Date.now();
+ // eslint-disable-next-line no-empty
+ while (Date.now() - startTime < HANG_TIME) {}
+ });
+
+ let [report] = await hangPromise;
+ return report;
+}
+
+/**
+ * Makes sure that the profiler is initialized. This has the added side-effect
+ * of making sure that BHR is initialized as well.
+ */
+function ensureProfilerInitialized() {
+ startProfiler();
+ stopProfiler();
+}
+
+function stopProfiler() {
+ Services.profiler.StopProfiler();
+}
+
+function startProfiler() {
+ // Starting and stopping the profiler with the "stackwalk" flag will cause the
+ // profiler's stackwalking features to be synchronously initialized. This
+ // should prevent us from not initializing BHR quickly enough.
+ Services.profiler.StartProfiler(1000, 10, ["stackwalk"]);
+}
+
+/**
+ * Given a performance profile object, returns a count of how many
+ * markers matched the value (and optional additionalText) that
+ * the UserInteraction backend added. This function only checks
+ * markers on thread 0.
+ *
+ * @param {Object} profile
+ * A profile returned from Services.profiler.getProfileData();
+ * @param {String} value
+ * The value that the marker is expected to have.
+ * @param {String} additionalText
+ * (Optional) If additionalText was provided when finishing the
+ * UserInteraction, then markerCount will check for a marker with
+ * text in the form of "value,additionalText".
+ * @returns {Number}
+ * A count of how many markers appear that match the criteria.
+ */
+function markerCount(profile, value, additionalText) {
+ let expectedName = value;
+ if (additionalText) {
+ expectedName = [value, additionalText].join(",");
+ }
+
+ let thread0 = profile.threads[0];
+ let stringTable = thread0.stringTable;
+ let markerStringIndex = stringTable.indexOf(TEST_USER_INTERACTION_ID);
+
+ let markers = thread0.markers.data.filter(markerData => {
+ return (
+ markerData[0] == markerStringIndex && markerData[5].name == expectedName
+ );
+ });
+
+ return markers.length;
+}
+
+/**
+ * Given an nsIHangReport, returns true if there are one or more annotations
+ * with the TEST_USER_INTERACTION_ID name, and the passed value.
+ *
+ * @param {nsIHangReport} report
+ * The hang report to check the annotations of.
+ * @param {String} value
+ * The value that the annotation should have.
+ * @returns {boolean}
+ * True if the annotation was found.
+ */
+function hasHangAnnotation(report, value) {
+ return report.annotations.some(annotation => {
+ return annotation[0] == TEST_USER_INTERACTION_ID && annotation[1] == value;
+ });
+}
+
+/**
+ * Given an nsIHangReport, returns true if there are one or more annotations
+ * with the TEST_CLOBBERED_USER_INTERACTION_ID name, and the passed value.
+ *
+ * This check should be used when we expect a pre-existing UserInteraction to
+ * have been clobbered by a new UserInteraction.
+ *
+ * @param {nsIHangReport} report
+ * The hang report to check the annotations of.
+ * @param {String} value
+ * The value that the annotation should have.
+ * @returns {boolean}
+ * True if the annotation was found.
+ */
+function hasClobberedHangAnnotation(report, value) {
+ return report.annotations.some(annotation => {
+ return (
+ annotation[0] == TEST_CLOBBERED_USER_INTERACTION_ID &&
+ annotation[1] == value
+ );
+ });
+}
+
+/**
+ * Tests that UserInteractions cause BHR annotations and profiler
+ * markers to be written.
+ */
+add_task(async function test_recording_annotations_and_markers() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ ensureProfilerInitialized();
+
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.OverridePreRelease,
+ true
+ );
+
+ // First, we'll check to see if we can get a single annotation and
+ // profiler marker to be set.
+ startProfiler();
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ let report = await hangAndWaitForReport(true);
+ UserInteraction.finish(TEST_USER_INTERACTION_ID);
+ let profile = Services.profiler.getProfileData();
+ stopProfiler();
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 1,
+ "Should have found the marker in the profile."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_1),
+ "Should have the BHR annotation set."
+ );
+
+ // Next, we'll make sure that when we're not running a UserInteraction,
+ // no marker or annotation is set.
+ startProfiler();
+
+ report = await hangAndWaitForReport(false);
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not find the marker in the profile."
+ );
+ Assert.ok(
+ !hasHangAnnotation(report),
+ "Should not have the BHR annotation set."
+ );
+
+ // Next, we'll ensure that we can set multiple markers and annotations
+ // by using the optional object argument to start() and finish().
+ startProfiler();
+
+ let obj1 = {};
+ let obj2 = {};
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1);
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj2);
+ report = await hangAndWaitForReport(true);
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj1,
+ TEST_ADDITIONAL_TEXT_1
+ );
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj2,
+ TEST_ADDITIONAL_TEXT_2
+ );
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1, TEST_ADDITIONAL_TEXT_1),
+ 1,
+ "Should have found first marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2, TEST_ADDITIONAL_TEXT_2),
+ 1,
+ "Should have found second marker in the profile."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_1),
+ "Should have the first BHR annotation set."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_2),
+ "Should have the second BHR annotation set."
+ );
+});
+
+/**
+ * Tests that UserInteractions can be updated, resulting in their BHR
+ * annotations and profiler markers to also be updated.
+ */
+add_task(async function test_updating_annotations_and_markers() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ ensureProfilerInitialized();
+
+ // First, we'll check to see if we can get a single annotation and
+ // profiler marker to be set.
+ startProfiler();
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ // Updating the UserInteraction means that a new value will overwrite
+ // the old.
+ UserInteraction.update(TEST_USER_INTERACTION_ID, TEST_VALUE_2);
+ let report = await hangAndWaitForReport(true);
+ UserInteraction.finish(TEST_USER_INTERACTION_ID);
+ let profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not have found the original marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2),
+ 1,
+ "Should have found the updated marker in the profile."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the original BHR annotation set."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_2),
+ "Should have the updated BHR annotation set."
+ );
+
+ // Next, we'll ensure that we can update multiple markers and annotations
+ // by using the optional object argument to start() and finish().
+ startProfiler();
+
+ let obj1 = {};
+ let obj2 = {};
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1);
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj2);
+
+ // Now swap the values between the two UserInteractions
+ UserInteraction.update(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj1);
+ UserInteraction.update(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj2);
+
+ report = await hangAndWaitForReport(true);
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj1,
+ TEST_ADDITIONAL_TEXT_1
+ );
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj2,
+ TEST_ADDITIONAL_TEXT_2
+ );
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2, TEST_ADDITIONAL_TEXT_1),
+ 1,
+ "Should have found first marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1, TEST_ADDITIONAL_TEXT_2),
+ 1,
+ "Should have found second marker in the profile."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_1),
+ "Should have the first BHR annotation set."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_2),
+ "Should have the second BHR annotation set."
+ );
+});
+
+/**
+ * Tests that UserInteractions can be cancelled, resulting in no BHR
+ * annotations and profiler markers being recorded.
+ */
+add_task(async function test_cancelling_annotations_and_markers() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ ensureProfilerInitialized();
+
+ // First, we'll check to see if we can get a single annotation and
+ // profiler marker to be set.
+ startProfiler();
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ UserInteraction.cancel(TEST_USER_INTERACTION_ID);
+ let report = await hangAndWaitForReport(false);
+
+ let profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not have found the marker in the profile."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the BHR annotation set."
+ );
+
+ // Next, we'll ensure that we can cancel multiple markers and annotations
+ // by using the optional object argument to start() and finish().
+ startProfiler();
+
+ let obj1 = {};
+ let obj2 = {};
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1);
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj2);
+
+ UserInteraction.cancel(TEST_USER_INTERACTION_ID, obj1);
+ UserInteraction.cancel(TEST_USER_INTERACTION_ID, obj2);
+
+ report = await hangAndWaitForReport(false);
+
+ Assert.ok(
+ !UserInteraction.finish(TEST_USER_INTERACTION_ID, obj1),
+ "Finishing a canceled UserInteraction should return false."
+ );
+
+ Assert.ok(
+ !UserInteraction.finish(TEST_USER_INTERACTION_ID, obj2),
+ "Finishing a canceled UserInteraction should return false."
+ );
+
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not have found the first marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2),
+ 0,
+ "Should not have found the second marker in the profile."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the first BHR annotation set."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_2),
+ "Should not have the second BHR annotation set."
+ );
+});
+
+/**
+ * Tests that starting UserInteractions with the same ID and object
+ * creates a clobber annotation.
+ */
+add_task(async function test_clobbered_annotations() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ // Now clobber the original UserInteraction
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2);
+
+ let report = await hangAndWaitForReport(true);
+ Assert.ok(
+ UserInteraction.finish(TEST_USER_INTERACTION_ID),
+ "Should have been able to finish the UserInteraction."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the original BHR annotation set."
+ );
+
+ Assert.ok(
+ hasClobberedHangAnnotation(report, TEST_VALUE_2),
+ "Should have the clobber BHR annotation set."
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_UtilityScalars.js b/toolkit/components/telemetry/tests/unit/test_UtilityScalars.js
new file mode 100644
index 0000000000..b6c0842910
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_UtilityScalars.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const UTILITY_ONLY_UINT_SCALAR = "telemetry.test.utility_only_uint";
+
+const utilityProcessTest = () => {
+ return Cc["@mozilla.org/utility-process-test;1"].createInstance(
+ Ci.nsIUtilityProcessTest
+ );
+};
+
+/**
+ * This function waits until utility scalars are reported into the
+ * scalar snapshot.
+ */
+async function waitForUtilityScalars() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const scalars = Telemetry.getSnapshotForScalars("main", false);
+ return Object.keys(scalars).includes("utility");
+ }, "Waiting for utility scalars to have been set");
+}
+
+async function waitForUtilityValue() {
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ UTILITY_ONLY_UINT_SCALAR in
+ Telemetry.getSnapshotForScalars("main", false).utility
+ );
+ }, "Waiting for utility uint value");
+}
+
+add_setup(async function setup_telemetry_utility() {
+ info("Start a UtilityProcess");
+ await utilityProcessTest().startProcess();
+
+ do_get_profile(true);
+ await TelemetryController.testSetup();
+});
+
+add_task(async function test_scalars_in_utility_process() {
+ Telemetry.clearScalars();
+ await utilityProcessTest().testTelemetryProbes();
+
+ // Once scalars are set by the utility process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ await waitForUtilityScalars();
+ await waitForUtilityValue();
+
+ Assert.equal(
+ Telemetry.getSnapshotForScalars("main", false).utility[
+ UTILITY_ONLY_UINT_SCALAR
+ ],
+ 42,
+ `${UTILITY_ONLY_UINT_SCALAR} must have the correct value (utility process).`
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_bug1555798.js b/toolkit/components/telemetry/tests/unit/test_bug1555798.js
new file mode 100644
index 0000000000..f5a84c200c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_bug1555798.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+add_task(async function test_bug1555798() {
+ /*
+ The idea behind this bug is that the registration of dynamic scalars causes
+ the position of the scalarinfo for telemetry.dynamic_events_count to move
+ which causes things to asplode.
+
+ So to test this we'll be registering two dynamic events, recording to one of
+ the events (to ensure the Scalar for event1 is allocated from the unmoved
+ DynamicScalarInfo&), registering several dynamic scalars to cause the
+ nsTArray of DynamicScalarInfo to realloc, and then recording to the second
+ event to make the Event Summary Scalar for event2 try to allocate from where
+ the DynamicScalarInfo used to be.
+ */
+ Telemetry.clearEvents();
+
+ const DYNAMIC_CATEGORY = "telemetry.test.dynamic.event";
+ Telemetry.registerEvents(DYNAMIC_CATEGORY, {
+ an_event: {
+ methods: ["a_method"],
+ objects: ["an_object", "another_object"],
+ record_on_release: true,
+ expired: false,
+ },
+ });
+ Telemetry.recordEvent(DYNAMIC_CATEGORY, "a_method", "an_object");
+
+ for (let i = 0; i < 100; ++i) {
+ Telemetry.registerScalars("telemetry.test.dynamic" + i, {
+ scalar_name: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ },
+ });
+ Telemetry.scalarAdd("telemetry.test.dynamic" + i + ".scalar_name", 1);
+ }
+
+ Telemetry.recordEvent(DYNAMIC_CATEGORY, "a_method", "another_object");
+
+ TelemetryTestUtils.assertNumberOfEvents(2, {}, { process: "dynamic" });
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_client_id.js b/toolkit/components/telemetry/tests/unit/test_client_id.js
new file mode 100644
index 0000000000..c20f70e2b2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_client_id.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+
+const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID";
+
+var drsPath;
+
+const uuidRegex =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+
+function run_test() {
+ do_get_profile();
+ drsPath = PathUtils.join(PathUtils.profileDir, "datareporting", "state.json");
+
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+ run_next_test();
+}
+
+add_task(function test_setup() {
+ // FOG needs a profile and to be init.
+ do_get_profile();
+ Services.fog.initializeFOG();
+});
+
+add_task(async function test_client_id() {
+ const invalidIDs = [
+ [-1, "setIntPref"],
+ [0.5, "setIntPref"],
+ ["INVALID-UUID", "setStringPref"],
+ [true, "setBoolPref"],
+ ["", "setStringPref"],
+ ["3d1e1560-682a-4043-8cf2-aaaaaaaaaaaZ", "setStringPref"],
+ ];
+
+ // If there is no DRS file, and no cached id, we should get a new client ID.
+ await ClientID._reset();
+ Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
+ await IOUtils.remove(drsPath, { ignoreAbsent: true });
+ let clientID = await ClientID.getClientID();
+ Assert.equal(typeof clientID, "string");
+ Assert.ok(uuidRegex.test(clientID));
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+
+ // We should be guarded against invalid DRS json.
+ await ClientID._reset();
+ Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
+ await IOUtils.writeUTF8(drsPath, "abcd", {
+ tmpPath: drsPath + ".tmp",
+ });
+ clientID = await ClientID.getClientID();
+ Assert.equal(typeof clientID, "string");
+ Assert.ok(uuidRegex.test(clientID));
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+
+ // If the DRS data is broken, we should end up with the cached ID.
+ let oldClientID = clientID;
+ for (let [invalidID] of invalidIDs) {
+ await ClientID._reset();
+ await IOUtils.writeJSON(drsPath, { clientID: invalidID });
+ clientID = await ClientID.getClientID();
+ Assert.equal(clientID, oldClientID);
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+ }
+
+ // Test that valid DRS actually works.
+ const validClientID = "5afebd62-a33c-416c-b519-5c60fb988e8e";
+ await ClientID._reset();
+ await IOUtils.writeJSON(drsPath, { clientID: validClientID });
+ clientID = await ClientID.getClientID();
+ Assert.equal(clientID, validClientID);
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+
+ // Test that reloading a valid DRS works.
+ await ClientID._reset();
+ Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
+ clientID = await ClientID.getClientID();
+ Assert.equal(clientID, validClientID);
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+
+ // Assure that cached IDs are being checked for validity.
+ for (let [invalidID, prefFunc] of invalidIDs) {
+ await ClientID._reset();
+ Services.prefs[prefFunc](PREF_CACHED_CLIENTID, invalidID);
+ let cachedID = ClientID.getCachedClientID();
+ Assert.strictEqual(
+ cachedID,
+ null,
+ "ClientID should ignore invalid cached IDs"
+ );
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(PREF_CACHED_CLIENTID),
+ "ClientID should reset invalid cached IDs"
+ );
+ Assert.ok(
+ Services.prefs.getPrefType(PREF_CACHED_CLIENTID) ==
+ Ci.nsIPrefBranch.PREF_INVALID,
+ "ClientID should reset invalid cached IDs"
+ );
+ }
+});
+
+add_task(async function test_setCanaryClientID() {
+ const KNOWN_UUID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0";
+
+ await ClientID._reset();
+
+ // We should be able to set a valid UUID
+ await ClientID.setCanaryClientID();
+ let clientID = await ClientID.getClientID();
+ Assert.equal(KNOWN_UUID, clientID);
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+});
+
+add_task(async function test_removeParallelGet() {
+ // We should get a valid UUID after reset
+ await ClientID.removeClientID();
+ let firstClientID = await ClientID.getClientID();
+ if (AppConstants.platform != "android") {
+ Assert.equal(firstClientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+
+ // We should get the same ID twice when requesting it in parallel to a reset.
+ let promiseRemoveClientID = ClientID.removeClientID();
+ let p = ClientID.getClientID();
+ let newClientID = await ClientID.getClientID();
+ await promiseRemoveClientID;
+ let otherClientID = await p;
+
+ Assert.notEqual(
+ firstClientID,
+ newClientID,
+ "After reset client ID should be different."
+ );
+ Assert.equal(
+ newClientID,
+ otherClientID,
+ "Getting the client ID in parallel to a reset should give the same id."
+ );
+ if (AppConstants.platform != "android") {
+ Assert.equal(newClientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_failover_retry.js b/toolkit/components/telemetry/tests/unit/test_failover_retry.js
new file mode 100644
index 0000000000..c66cb64aea
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_failover_retry.js
@@ -0,0 +1,261 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+// Enable the collection (during test) for all products so even products
+// that don't collect the data will be able to run the test without failure.
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+// Trigger a proper telemetry init.
+do_get_profile(true);
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+// setup and configure a proxy server that will just deny connections.
+const proxy = AddonTestUtils.createHttpServer();
+proxy.registerPrefixHandler("/", (request, response) => {
+ response.setStatusLine(request.httpVersion, 504, "hello proxy user");
+ response.write("ok!");
+});
+
+// Register a proxy to be used by TCPSocket connections later.
+let proxy_info;
+
+function getBadProxyPort() {
+ let server = new HttpServer();
+ server.start(-1);
+ const badPort = server.identity.primaryPort;
+ server.stop();
+ return badPort;
+}
+
+function registerProxy() {
+ let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(
+ Ci.nsIProtocolProxyService
+ );
+
+ const proxyFilter = {
+ applyFilter(uri, defaultProxyInfo, callback) {
+ if (
+ proxy_info &&
+ uri.host == PingServer.host &&
+ uri.port == PingServer.port
+ ) {
+ let proxyInfo = pps.newProxyInfo(
+ proxy_info.type,
+ proxy_info.host,
+ proxy_info.port,
+ "",
+ "",
+ 0,
+ 4096,
+ null
+ );
+ proxyInfo.sourceId = proxy_info.sourceId;
+ callback.onProxyFilterResult(proxyInfo);
+ } else {
+ callback.onProxyFilterResult(defaultProxyInfo);
+ }
+ },
+ };
+
+ pps.registerFilter(proxyFilter, 0);
+ registerCleanupFunction(() => {
+ pps.unregisterFilter(proxyFilter);
+ });
+}
+
+add_task(async function setup() {
+ fakeIntlReady();
+
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.HealthPingEnabled,
+ false
+ );
+ TelemetryStopwatch.setTestModeEnabled(true);
+
+ registerProxy();
+
+ PingServer.start();
+
+ // accept proxy connections for PingServer
+ proxy.identity.add("http", PingServer.host, PingServer.port);
+
+ await TelemetrySend.setup(true);
+ TelemetrySend.setTestModeEnabled(true);
+ TelemetrySend.setServer(`http://localhost:${PingServer.port}`);
+});
+
+function checkEvent() {
+ // ServiceRequest should have recorded an event for this.
+ let expected = [
+ "service_request",
+ "bypass",
+ "proxy_info",
+ "telemetry.send",
+ {
+ source: proxy_info.sourceId,
+ type: "api",
+ },
+ ];
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_ALL_CHANNELS,
+ false
+ );
+
+ let received = snapshot.parent[0];
+ received.shift();
+ Assert.deepEqual(
+ expected,
+ received,
+ `retry telemetry data matched ${JSON.stringify(received)}`
+ );
+ Telemetry.clearEvents();
+}
+
+async function submitPingWithDate(date, expected) {
+ fakeNow(new Date(date));
+ let pingId = await TelemetryController.submitExternalPing(
+ "test-send-date-header",
+ {}
+ );
+ let req = await PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+ Assert.equal(
+ req.getHeader("Date"),
+ expected,
+ "Telemetry should send the correct Date header with requests."
+ );
+ Assert.equal(ping.id, pingId, "Should have received the correct ping id.");
+}
+
+// While there is no specific indiction, this test causes the
+// telemetrySend doPing onload handler to be invoked.
+add_task(async function test_failed_server() {
+ proxy_info = {
+ type: "http",
+ host: proxy.identity.primaryHost,
+ port: proxy.identity.primaryPort,
+ sourceId: "failed_server_test",
+ };
+
+ await TelemetrySend.reset();
+ await submitPingWithDate(
+ Date.UTC(2011, 1, 1, 11, 0, 0),
+ "Tue, 01 Feb 2011 11:00:00 GMT"
+ );
+ checkEvent();
+});
+
+// While there is no specific indiction, this test causes the
+// telemetrySend doPing error handler to be invoked.
+add_task(async function test_no_server() {
+ // Make sure the underlying proxy failover is disabled to easily force
+ // telemetry to retry the request.
+ Services.prefs.setBoolPref("network.proxy.failover_direct", false);
+
+ proxy_info = {
+ type: "http",
+ host: "localhost",
+ port: getBadProxyPort(),
+ sourceId: "no_server_test",
+ };
+
+ await TelemetrySend.reset();
+ await submitPingWithDate(
+ Date.UTC(2012, 1, 1, 11, 0, 0),
+ "Wed, 01 Feb 2012 11:00:00 GMT"
+ );
+ checkEvent();
+});
+
+// Mock out the send timer activity.
+function waitForTimer() {
+ return new Promise(resolve => {
+ fakePingSendTimer(
+ (callback, timeout) => {
+ resolve([callback, timeout]);
+ },
+ () => {}
+ );
+ });
+}
+
+add_task(async function test_no_bypass() {
+ // Make sure the underlying proxy failover is disabled to easily force
+ // telemetry to retry the request.
+ Services.prefs.setBoolPref("network.proxy.failover_direct", false);
+ // Disable the retry and submit again.
+ Services.prefs.setBoolPref("network.proxy.allow_bypass", false);
+
+ proxy_info = {
+ type: "http",
+ host: "localhost",
+ port: getBadProxyPort(),
+ sourceId: "no_server_test",
+ };
+
+ await TelemetrySend.reset();
+
+ fakeNow(new Date(Date.UTC(2013, 1, 1, 11, 0, 0)));
+
+ let timerPromise = waitForTimer();
+ let pingId = await TelemetryController.submitExternalPing(
+ "test-send-date-header",
+ {}
+ );
+ let [pingSendTimerCallback] = await timerPromise;
+ Assert.ok(!!pingSendTimerCallback, "Should have a timer callback");
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 1,
+ "Should have correct pending ping count"
+ );
+
+ // Reset the proxy, trigger the next tick - we should receive the ping.
+ proxy_info = null;
+ pingSendTimerCallback();
+ let req = await PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+
+ // PingServer finished before telemetry, so make sure it's done.
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ Assert.equal(
+ req.getHeader("Date"),
+ "Fri, 01 Feb 2013 11:00:00 GMT",
+ "Telemetry should send the correct Date header with requests."
+ );
+ Assert.equal(ping.id, pingId, "Should have received the correct ping id.");
+
+ // reset to save any pending pings
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should not have any pending pings"
+ );
+
+ await TelemetrySend.reset();
+ PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/xpcshell.ini b/toolkit/components/telemetry/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..e451437ecb
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini
@@ -0,0 +1,134 @@
+[DEFAULT]
+head = head.js
+firefox-appdir = browser
+# The *.xpi files are only needed for test_TelemetryEnvironment.js, but
+# xpcshell fails to install tests if we move them under the test entry.
+support-files =
+ data/search-extensions/engines.json
+ data/search-extensions/telemetrySearchIdentifier/manifest.json
+ engine.xml
+ system.xpi
+ restartless.xpi
+ testUnicodePDB32.dll
+ testNoPDB32.dll
+ testUnicodePDB64.dll
+ testNoPDB64.dll
+ testUnicodePDBAArch64.dll
+ testNoPDBAArch64.dll
+ !/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+generated-files =
+ system.xpi
+ restartless.xpi
+
+[test_UserInteraction.js]
+[test_UserInteraction_annotations.js]
+# BHR is disabled on tsan, asan, android and outside of Nightly.
+skip-if =
+ debug
+ asan
+ tsan
+ os == "android"
+ release_or_beta
+ apple_catalina # Bug 1713329
+ apple_silicon # bug 1707747
+ os == "win" && bits == 32 && !debug # Bug 1781452
+ os == "linux" && !debug # Bug 1781452
+run-sequentially = very high failure rate in parallel
+[test_client_id.js]
+[test_MigratePendingPings.js]
+[test_TelemetryHistograms.js]
+[test_SubsessionChaining.js]
+tags = addons
+[test_SyncPingIntegration.js]
+skip-if = os == "android"
+[test_TelemetryEnvironment.js]
+skip-if =
+ os == "android"
+ os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807929
+tags = addons
+run-sequentially = very high failure rate in parallel
+[test_TelemetryEnvironment_search.js]
+skip-if =
+ os == "android"
+ os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807929
+[test_PingAPI.js]
+[test_TelemetryFlagClear.js]
+[test_TelemetryLateWrites.js]
+[test_TelemetryLockCount.js]
+[test_TelemetryController.js]
+[test_TelemetryClientID_reset.js]
+skip-if = os == "android" # Disabled as Android/GeckoView doesn't run TelemetryController
+[test_HealthPing.js]
+skip-if =
+ (verify && (os == 'win'))
+ (os == 'android' && processor == 'x86_64')
+tags = addons
+[test_TelemetryController_idle.js]
+[test_TelemetryControllerShutdown.js]
+skip-if =
+ (os == 'android' && processor == 'x86_64') # Bug 1784622
+tags = addons
+[test_TelemetryStopwatch.js]
+[test_TelemetryControllerBuildID.js]
+[test_TelemetrySendOldPings.js]
+skip-if = os == "android" # Disabled due to intermittent orange on Android
+tags = addons
+[test_TelemetrySession.js]
+tags = addons
+skip-if =
+ os == "linux" && verify && debug
+[test_TelemetrySession_abortedSessionQueued.js]
+skip-if = os == "android"
+[test_TelemetrySession_activeTicks.js]
+[test_TelemetrySend.js]
+skip-if =
+ os == "linux" && ccov # Bug 1701874
+[test_ChildHistograms.js]
+skip-if = os == "android" # Disabled due to crashes (see bug 1331366)
+tags = addons
+[test_ChildScalars.js]
+skip-if = os == "android" # Disabled due to crashes (see bug 1331366)
+[test_SocketScalars.js]
+run-if = socketprocess_networking # Needs socket process (bug 1716307)
+[test_TelemetryReportingPolicy.js]
+tags = addons
+[test_TelemetryScalars.js]
+[test_TelemetryScalars_buildFaster.js]
+skip-if =
+ os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807929
+[test_TelemetryScalars_impressionId.js]
+[test_TelemetryScalars_multistore.js]
+[test_TelemetryTimestamps.js]
+[test_TelemetryChildEvents_buildFaster.js]
+skip-if = os == "android" # Disabled due to crashes (see bug 1331366)
+[test_TelemetryEvents.js]
+[test_TelemetryEvents_buildFaster.js]
+skip-if =
+ os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807929
+[test_ChildEvents.js]
+skip-if = os == "android" # Disabled due to crashes (see bug 1331366)
+[test_ModulesPing.js]
+skip-if =
+ apple_silicon # bug 1707747
+ apple_catalina # Bug 1713329
+[test_PingSender.js]
+skip-if =
+ os == "android"
+[test_TelemetryAndroidEnvironment.js]
+[test_TelemetryUtils.js]
+[test_ThirdPartyModulesPing.js]
+run-if = (os == 'win' && !msix) # Disabled for MSIX due to https://bugzilla.mozilla.org/show_bug.cgi?id=1807929
+[test_EventPing.js]
+tags = coverage
+[test_CoveragePing.js]
+[test_bug1555798.js]
+[test_UninstallPing.js]
+support-files = file_UninstallPing.worker.js
+run-if = os == "win"
+[test_RDDScalars.js]
+skip-if =
+ os == "android" # RDD is not a thing on Android?
+[test_UtilityScalars.js]
+run-if = os == 'win'
+[test_failover_retry.js]
+skip-if = os == "android" # Android doesn't support telemetry though some tests manage to pass with xpcshell