Backed out changeset f0a2aa2ffe6d (bug 1832704) for causing xpc failures in toolkit...
[gecko.git] / toolkit / components / telemetry / app / TelemetryEnvironment.sys.mjs
blobfbb17d65bf1f2e7740921464d4fa15e2f65b0d56
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { Log } from "resource://gre/modules/Log.sys.mjs";
7 import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
9 import { ObjectUtils } from "resource://gre/modules/ObjectUtils.sys.mjs";
10 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
11 import { UpdateUtils } from "resource://gre/modules/UpdateUtils.sys.mjs";
13 const Utils = TelemetryUtils;
15 import {
16   AddonManager,
17   AddonManagerPrivate,
18 } from "resource://gre/modules/AddonManager.sys.mjs";
20 const lazy = {};
22 ChromeUtils.defineESModuleGetters(lazy, {
23   AttributionCode: "resource:///modules/AttributionCode.sys.mjs",
24   ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
25   WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
26   WindowsVersionInfo:
27     "resource://gre/modules/components-utils/WindowsVersionInfo.sys.mjs",
28 });
30 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
31   return ChromeUtils.importESModule(
32     "resource://gre/modules/FxAccounts.sys.mjs"
33   ).getFxAccountsSingleton();
34 });
36 // The maximum length of a string (e.g. description) in the addons section.
37 const MAX_ADDON_STRING_LENGTH = 100;
38 // The maximum length of a string value in the settings.attribution object.
39 const MAX_ATTRIBUTION_STRING_LENGTH = 100;
40 // The maximum lengths for the experiment id and branch in the experiments section.
41 const MAX_EXPERIMENT_ID_LENGTH = 100;
42 const MAX_EXPERIMENT_BRANCH_LENGTH = 100;
43 const MAX_EXPERIMENT_TYPE_LENGTH = 20;
44 const MAX_EXPERIMENT_ENROLLMENT_ID_LENGTH = 40;
46 /**
47  * This is a policy object used to override behavior for testing.
48  */
49 // eslint-disable-next-line no-unused-vars
50 export var Policy = {
51   now: () => new Date(),
52   _intlLoaded: false,
53   _browserDelayedStartup() {
54     if (Policy._intlLoaded) {
55       return Promise.resolve();
56     }
57     return new Promise(resolve => {
58       let startupTopic = "browser-delayed-startup-finished";
59       Services.obs.addObserver(function observer(subject, topic) {
60         if (topic == startupTopic) {
61           Services.obs.removeObserver(observer, startupTopic);
62           resolve();
63         }
64       }, startupTopic);
65     });
66   },
69 // This is used to buffer calls to setExperimentActive and friends, so that we
70 // don't prematurely initialize our environment if it is called early during
71 // startup.
72 var gActiveExperimentStartupBuffer = new Map();
74 var gGlobalEnvironment;
75 function getGlobal() {
76   if (!gGlobalEnvironment) {
77     gGlobalEnvironment = new EnvironmentCache();
78   }
79   return gGlobalEnvironment;
82 export var TelemetryEnvironment = {
83   get currentEnvironment() {
84     return getGlobal().currentEnvironment;
85   },
87   onInitialized() {
88     return getGlobal().onInitialized();
89   },
91   delayedInit() {
92     return getGlobal().delayedInit();
93   },
95   registerChangeListener(name, listener) {
96     return getGlobal().registerChangeListener(name, listener);
97   },
99   unregisterChangeListener(name) {
100     return getGlobal().unregisterChangeListener(name);
101   },
103   /**
104    * Add an experiment annotation to the environment.
105    * If an annotation with the same id already exists, it will be overwritten.
106    * This triggers a new subsession, subject to throttling.
107    *
108    * @param {String} id The id of the active experiment.
109    * @param {String} branch The experiment branch.
110    * @param {Object} [options] Optional object with options.
111    * @param {String} [options.type=false] The specific experiment type.
112    * @param {String} [options.enrollmentId=undefined] The id of the enrollment.
113    */
114   setExperimentActive(id, branch, options = {}) {
115     if (gGlobalEnvironment) {
116       gGlobalEnvironment.setExperimentActive(id, branch, options);
117     } else {
118       gActiveExperimentStartupBuffer.set(id, { branch, options });
119     }
120   },
122   /**
123    * Remove an experiment annotation from the environment.
124    * If the annotation exists, a new subsession will triggered.
125    *
126    * @param {String} id The id of the active experiment.
127    */
128   setExperimentInactive(id) {
129     if (gGlobalEnvironment) {
130       gGlobalEnvironment.setExperimentInactive(id);
131     } else {
132       gActiveExperimentStartupBuffer.delete(id);
133     }
134   },
136   /**
137    * Returns an object containing the data for the active experiments.
138    *
139    * The returned object is of the format:
140    *
141    * {
142    *   "<experiment id>": { branch: "<branch>" },
143    *   // …
144    * }
145    */
146   getActiveExperiments() {
147     if (gGlobalEnvironment) {
148       return gGlobalEnvironment.getActiveExperiments();
149     }
151     const result = {};
152     for (const [id, { branch }] of gActiveExperimentStartupBuffer.entries()) {
153       result[id] = branch;
154     }
155     return result;
156   },
158   shutdown() {
159     return getGlobal().shutdown();
160   },
162   // Policy to use when saving preferences. Exported for using them in tests.
163   // Reports "<user-set>" if there is a value set on the user branch
164   RECORD_PREF_STATE: 1,
166   // Reports the value set on the user branch, if one is set
167   RECORD_PREF_VALUE: 2,
169   // Reports the active value (set on either the user or default branch)
170   // for this pref, if one is set
171   RECORD_DEFAULTPREF_VALUE: 3,
173   // Reports "<set>" if a value for this pref is defined on either the user
174   // or default branch
175   RECORD_DEFAULTPREF_STATE: 4,
177   // Testing method
178   async testWatchPreferences(prefMap) {
179     return getGlobal()._watchPreferences(prefMap);
180   },
182   /**
183    * Intended for use in tests only.
184    *
185    * In multiple tests we need a way to shut and re-start telemetry together
186    * with TelemetryEnvironment. This is problematic due to the fact that
187    * TelemetryEnvironment is a singleton. We, therefore, need this helper
188    * method to be able to re-set TelemetryEnvironment.
189    */
190   testReset() {
191     return getGlobal().reset();
192   },
194   /**
195    * Intended for use in tests only.
196    */
197   testCleanRestart() {
198     getGlobal().shutdown();
199     gGlobalEnvironment = null;
200     gActiveExperimentStartupBuffer = new Map();
201     return getGlobal();
202   },
205 const RECORD_PREF_STATE = TelemetryEnvironment.RECORD_PREF_STATE;
206 const RECORD_PREF_VALUE = TelemetryEnvironment.RECORD_PREF_VALUE;
207 const RECORD_DEFAULTPREF_VALUE = TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE;
208 const RECORD_DEFAULTPREF_STATE = TelemetryEnvironment.RECORD_DEFAULTPREF_STATE;
209 const DEFAULT_ENVIRONMENT_PREFS = new Map([
210   ["app.feedback.baseURL", { what: RECORD_PREF_VALUE }],
211   ["app.support.baseURL", { what: RECORD_PREF_VALUE }],
212   ["accessibility.browsewithcaret", { what: RECORD_PREF_VALUE }],
213   ["accessibility.force_disabled", { what: RECORD_PREF_VALUE }],
214   ["app.normandy.test-prefs.bool", { what: RECORD_PREF_VALUE }],
215   ["app.normandy.test-prefs.integer", { what: RECORD_PREF_VALUE }],
216   ["app.normandy.test-prefs.string", { what: RECORD_PREF_VALUE }],
217   ["app.shield.optoutstudies.enabled", { what: RECORD_PREF_VALUE }],
218   ["app.update.interval", { what: RECORD_PREF_VALUE }],
219   ["app.update.service.enabled", { what: RECORD_PREF_VALUE }],
220   ["app.update.silent", { what: RECORD_PREF_VALUE }],
221   ["browser.cache.disk.enable", { what: RECORD_PREF_VALUE }],
222   ["browser.cache.disk.capacity", { what: RECORD_PREF_VALUE }],
223   ["browser.cache.memory.enable", { what: RECORD_PREF_VALUE }],
224   ["browser.formfill.enable", { what: RECORD_PREF_VALUE }],
225   ["browser.migrate.interactions.bookmarks", { what: RECORD_PREF_VALUE }],
226   ["browser.migrate.interactions.csvpasswords", { what: RECORD_PREF_VALUE }],
227   ["browser.migrate.interactions.history", { what: RECORD_PREF_VALUE }],
228   ["browser.migrate.interactions.passwords", { what: RECORD_PREF_VALUE }],
229   ["browser.newtabpage.enabled", { what: RECORD_PREF_VALUE }],
230   ["browser.privatebrowsing.autostart", { what: RECORD_PREF_VALUE }],
231   ["browser.shell.checkDefaultBrowser", { what: RECORD_PREF_VALUE }],
232   ["browser.search.region", { what: RECORD_PREF_VALUE }],
233   ["browser.search.suggest.enabled", { what: RECORD_PREF_VALUE }],
234   ["browser.search.widget.inNavBar", { what: RECORD_DEFAULTPREF_VALUE }],
235   ["browser.startup.homepage", { what: RECORD_PREF_STATE }],
236   ["browser.startup.page", { what: RECORD_PREF_VALUE }],
237   ["browser.tabs.firefox-view", { what: RECORD_PREF_VALUE }],
238   ["browser.tabs.firefox-view-next", { what: RECORD_PREF_VALUE }],
239   ["browser.urlbar.autoFill", { what: RECORD_DEFAULTPREF_VALUE }],
240   [
241     "browser.urlbar.autoFill.adaptiveHistory.enabled",
242     { what: RECORD_DEFAULTPREF_VALUE },
243   ],
244   [
245     "browser.urlbar.dnsResolveSingleWordsAfterSearch",
246     { what: RECORD_DEFAULTPREF_VALUE },
247   ],
248   [
249     "browser.urlbar.quicksuggest.onboardingDialogChoice",
250     { what: RECORD_DEFAULTPREF_VALUE },
251   ],
252   [
253     "browser.urlbar.quicksuggest.dataCollection.enabled",
254     { what: RECORD_DEFAULTPREF_VALUE },
255   ],
256   ["browser.urlbar.showSearchSuggestionsFirst", { what: RECORD_PREF_VALUE }],
257   ["browser.urlbar.showSearchTerms.enabled", { what: RECORD_PREF_VALUE }],
258   [
259     "browser.urlbar.suggest.quicksuggest.nonsponsored",
260     { what: RECORD_DEFAULTPREF_VALUE },
261   ],
262   [
263     "browser.urlbar.suggest.quicksuggest.sponsored",
264     { what: RECORD_DEFAULTPREF_VALUE },
265   ],
266   ["browser.urlbar.suggest.bestmatch", { what: RECORD_DEFAULTPREF_VALUE }],
267   ["browser.urlbar.suggest.searches", { what: RECORD_PREF_VALUE }],
268   ["devtools.chrome.enabled", { what: RECORD_PREF_VALUE }],
269   ["devtools.debugger.enabled", { what: RECORD_PREF_VALUE }],
270   ["devtools.debugger.remote-enabled", { what: RECORD_PREF_VALUE }],
271   ["doh-rollout.doorhanger-decision", { what: RECORD_PREF_VALUE }],
272   ["dom.ipc.processCount", { what: RECORD_PREF_VALUE }],
273   ["dom.max_script_run_time", { what: RECORD_PREF_VALUE }],
274   ["editor.truncate_user_pastes", { what: RECORD_PREF_VALUE }],
275   ["extensions.InstallTrigger.enabled", { what: RECORD_PREF_VALUE }],
276   ["extensions.InstallTriggerImpl.enabled", { what: RECORD_PREF_VALUE }],
277   ["extensions.autoDisableScopes", { what: RECORD_PREF_VALUE }],
278   ["extensions.blocklist.enabled", { what: RECORD_PREF_VALUE }],
279   ["extensions.enabledScopes", { what: RECORD_PREF_VALUE }],
280   ["extensions.eventPages.enabled", { what: RECORD_PREF_VALUE }],
281   ["extensions.formautofill.addresses.enabled", { what: RECORD_PREF_VALUE }],
282   [
283     "extensions.formautofill.addresses.capture.enabled",
284     { what: RECORD_PREF_VALUE },
285   ],
286   ["extensions.formautofill.creditCards.enabled", { what: RECORD_PREF_VALUE }],
287   ["extensions.manifestV3.enabled", { what: RECORD_PREF_VALUE }],
288   ["extensions.quarantinedDomains.enabled", { what: RECORD_PREF_VALUE }],
289   ["extensions.strictCompatibility", { what: RECORD_PREF_VALUE }],
290   ["extensions.update.enabled", { what: RECORD_PREF_VALUE }],
291   ["extensions.update.url", { what: RECORD_PREF_VALUE }],
292   ["extensions.update.background.url", { what: RECORD_PREF_VALUE }],
293   ["extensions.screenshots.disabled", { what: RECORD_PREF_VALUE }],
294   ["general.config.filename", { what: RECORD_DEFAULTPREF_STATE }],
295   ["general.smoothScroll", { what: RECORD_PREF_VALUE }],
296   ["gfx.direct2d.disabled", { what: RECORD_PREF_VALUE }],
297   ["gfx.direct2d.force-enabled", { what: RECORD_PREF_VALUE }],
298   ["gfx.webrender.all", { what: RECORD_PREF_VALUE }],
299   ["layers.acceleration.disabled", { what: RECORD_PREF_VALUE }],
300   ["layers.acceleration.force-enabled", { what: RECORD_PREF_VALUE }],
301   ["layers.async-pan-zoom.enabled", { what: RECORD_PREF_VALUE }],
302   ["layers.async-video-oop.enabled", { what: RECORD_PREF_VALUE }],
303   ["layers.d3d11.disable-warp", { what: RECORD_PREF_VALUE }],
304   ["layers.d3d11.force-warp", { what: RECORD_PREF_VALUE }],
305   [
306     "layers.offmainthreadcomposition.force-disabled",
307     { what: RECORD_PREF_VALUE },
308   ],
309   ["layers.prefer-d3d9", { what: RECORD_PREF_VALUE }],
310   ["layers.prefer-opengl", { what: RECORD_PREF_VALUE }],
311   ["layout.css.devPixelsPerPx", { what: RECORD_PREF_VALUE }],
312   ["media.gmp-gmpopenh264.enabled", { what: RECORD_PREF_VALUE }],
313   ["media.gmp-gmpopenh264.lastInstallFailed", { what: RECORD_PREF_VALUE }],
314   ["media.gmp-gmpopenh264.lastInstallFailReason", { what: RECORD_PREF_VALUE }],
315   ["media.gmp-gmpopenh264.lastInstallStart", { what: RECORD_PREF_VALUE }],
316   ["media.gmp-gmpopenh264.lastDownload", { what: RECORD_PREF_VALUE }],
317   ["media.gmp-gmpopenh264.lastDownloadFailed", { what: RECORD_PREF_VALUE }],
318   ["media.gmp-gmpopenh264.lastDownloadFailReason", { what: RECORD_PREF_VALUE }],
319   ["media.gmp-gmpopenh264.lastUpdate", { what: RECORD_PREF_VALUE }],
320   ["media.gmp-gmpopenh264.visible", { what: RECORD_PREF_VALUE }],
321   ["media.gmp-widevinecdm.enabled", { what: RECORD_PREF_VALUE }],
322   ["media.gmp-widevinecdm.lastInstallFailed", { what: RECORD_PREF_VALUE }],
323   ["media.gmp-widevinecdm.lastInstallFailReason", { what: RECORD_PREF_VALUE }],
324   ["media.gmp-widevinecdm.lastInstallStart", { what: RECORD_PREF_VALUE }],
325   ["media.gmp-widevinecdm.lastDownload", { what: RECORD_PREF_VALUE }],
326   ["media.gmp-widevinecdm.lastDownloadFailed", { what: RECORD_PREF_VALUE }],
327   ["media.gmp-widevinecdm.lastDownloadFailReason", { what: RECORD_PREF_VALUE }],
328   ["media.gmp-widevinecdm.lastUpdate", { what: RECORD_PREF_VALUE }],
329   ["media.gmp-widevinecdm.visible", { what: RECORD_PREF_VALUE }],
330   ["media.gmp-manager.lastCheck", { what: RECORD_PREF_VALUE }],
331   ["media.gmp-manager.lastEmptyCheck", { what: RECORD_PREF_VALUE }],
332   ["network.http.windows-sso.enabled", { what: RECORD_PREF_VALUE }],
333   ["network.proxy.autoconfig_url", { what: RECORD_PREF_STATE }],
334   ["network.proxy.http", { what: RECORD_PREF_STATE }],
335   ["network.proxy.ssl", { what: RECORD_PREF_STATE }],
336   ["network.trr.mode", { what: RECORD_PREF_VALUE }],
337   ["network.trr.strict_native_fallback", { what: RECORD_DEFAULTPREF_VALUE }],
338   ["pdfjs.disabled", { what: RECORD_PREF_VALUE }],
339   ["places.history.enabled", { what: RECORD_PREF_VALUE }],
340   ["privacy.firstparty.isolate", { what: RECORD_PREF_VALUE }],
341   ["privacy.resistFingerprinting", { what: RECORD_PREF_VALUE }],
342   ["privacy.fingerprintingProtection", { what: RECORD_PREF_VALUE }],
343   ["privacy.fingerprintingProtection.pbmode", { what: RECORD_PREF_VALUE }],
344   ["privacy.trackingprotection.enabled", { what: RECORD_PREF_VALUE }],
345   ["privacy.donottrackheader.enabled", { what: RECORD_PREF_VALUE }],
346   ["security.enterprise_roots.auto-enabled", { what: RECORD_PREF_VALUE }],
347   ["security.enterprise_roots.enabled", { what: RECORD_PREF_VALUE }],
348   ["security.pki.mitm_detected", { what: RECORD_PREF_VALUE }],
349   ["security.mixed_content.block_active_content", { what: RECORD_PREF_VALUE }],
350   ["security.mixed_content.block_display_content", { what: RECORD_PREF_VALUE }],
351   ["security.tls.version.enable-deprecated", { what: RECORD_PREF_VALUE }],
352   ["signon.management.page.breach-alerts.enabled", { what: RECORD_PREF_VALUE }],
353   ["signon.autofillForms", { what: RECORD_PREF_VALUE }],
354   ["signon.generation.enabled", { what: RECORD_PREF_VALUE }],
355   ["signon.rememberSignons", { what: RECORD_PREF_VALUE }],
356   ["signon.firefoxRelay.feature", { what: RECORD_PREF_VALUE }],
357   ["toolkit.telemetry.pioneerId", { what: RECORD_PREF_STATE }],
358   [
359     "widget.content.gtk-high-contrast.enabled",
360     { what: RECORD_DEFAULTPREF_VALUE },
361   ],
362   ["xpinstall.signatures.required", { what: RECORD_PREF_VALUE }],
363   ["nimbus.debug", { what: RECORD_PREF_VALUE }],
366 const LOGGER_NAME = "Toolkit.Telemetry";
368 const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
369 const PREF_DISTRIBUTION_ID = "distribution.id";
370 const PREF_DISTRIBUTION_VERSION = "distribution.version";
371 const PREF_DISTRIBUTOR = "app.distributor";
372 const PREF_DISTRIBUTOR_CHANNEL = "app.distributor.channel";
373 const PREF_APP_PARTNER_BRANCH = "app.partner.";
374 const PREF_PARTNER_ID = "mozilla.partner.id";
376 const COMPOSITOR_CREATED_TOPIC = "compositor:created";
377 const COMPOSITOR_PROCESS_ABORTED_TOPIC = "compositor:process-aborted";
378 const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC =
379   "distribution-customization-complete";
380 const GFX_FEATURES_READY_TOPIC = "gfx-features-ready";
381 const SEARCH_ENGINE_MODIFIED_TOPIC = "browser-search-engine-modified";
382 const SEARCH_SERVICE_TOPIC = "browser-search-service";
383 const SESSIONSTORE_WINDOWS_RESTORED_TOPIC = "sessionstore-windows-restored";
384 const PREF_CHANGED_TOPIC = "nsPref:changed";
385 const GMP_PROVIDER_REGISTERED_TOPIC = "gmp-provider-registered";
386 const AUTO_UPDATE_PREF_CHANGE_TOPIC =
387   UpdateUtils.PER_INSTALLATION_PREFS["app.update.auto"].observerTopic;
388 const BACKGROUND_UPDATE_PREF_CHANGE_TOPIC =
389   UpdateUtils.PER_INSTALLATION_PREFS["app.update.background.enabled"]
390     .observerTopic;
391 const SERVICES_INFO_CHANGE_TOPIC = "sync-ui-state:update";
394  * Enforces the parameter to a boolean value.
395  * @param aValue The input value.
396  * @return {Boolean|Object} If aValue is a boolean or a number, returns its truthfulness
397  *         value. Otherwise, return null.
398  */
399 function enforceBoolean(aValue) {
400   if (typeof aValue !== "number" && typeof aValue !== "boolean") {
401     return null;
402   }
403   return Boolean(aValue);
407  * Get the current browser locale.
408  * @return a string with the locale or null on failure.
409  */
410 function getBrowserLocale() {
411   try {
412     return Services.locale.appLocaleAsBCP47;
413   } catch (e) {
414     return null;
415   }
419  * Get the current OS locale.
420  * @return a string with the OS locale or null on failure.
421  */
422 function getSystemLocale() {
423   try {
424     return Cc["@mozilla.org/intl/ospreferences;1"].getService(
425       Ci.mozIOSPreferences
426     ).systemLocale;
427   } catch (e) {
428     return null;
429   }
433  * Get the current OS locales.
434  * @return an array of strings with the OS locales or null on failure.
435  */
436 function getSystemLocales() {
437   try {
438     return Cc["@mozilla.org/intl/ospreferences;1"].getService(
439       Ci.mozIOSPreferences
440     ).systemLocales;
441   } catch (e) {
442     return null;
443   }
447  * Get the current OS regional preference locales.
448  * @return an array of strings with the OS regional preference locales or null on failure.
449  */
450 function getRegionalPrefsLocales() {
451   try {
452     return Cc["@mozilla.org/intl/ospreferences;1"].getService(
453       Ci.mozIOSPreferences
454     ).regionalPrefsLocales;
455   } catch (e) {
456     return null;
457   }
460 function getIntlSettings() {
461   return {
462     requestedLocales: Services.locale.requestedLocales,
463     availableLocales: Services.locale.availableLocales,
464     appLocales: Services.locale.appLocalesAsBCP47,
465     systemLocales: getSystemLocales(),
466     regionalPrefsLocales: getRegionalPrefsLocales(),
467     acceptLanguages: Services.prefs
468       .getComplexValue("intl.accept_languages", Ci.nsIPrefLocalizedString)
469       .data.split(",")
470       .map(str => str.trim()),
471   };
475  * Safely get a sysinfo property and return its value. If the property is not
476  * available, return aDefault.
478  * @param aPropertyName the property name to get.
479  * @param aDefault the value to return if aPropertyName is not available.
480  * @return The property value, if available, or aDefault.
481  */
482 function getSysinfoProperty(aPropertyName, aDefault) {
483   try {
484     // |getProperty| may throw if |aPropertyName| does not exist.
485     return Services.sysinfo.getProperty(aPropertyName);
486   } catch (e) {}
488   return aDefault;
492  * Safely get a gfxInfo field and return its value. If the field is not available, return
493  * aDefault.
495  * @param aPropertyName the property name to get.
496  * @param aDefault the value to return if aPropertyName is not available.
497  * @return The property value, if available, or aDefault.
498  */
499 function getGfxField(aPropertyName, aDefault) {
500   let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
502   try {
503     // Accessing the field may throw if |aPropertyName| does not exist.
504     let gfxProp = gfxInfo[aPropertyName];
505     if (gfxProp !== undefined && gfxProp !== "") {
506       return gfxProp;
507     }
508   } catch (e) {}
510   return aDefault;
514  * Returns a substring of the input string.
516  * @param {String} aString The input string.
517  * @param {Integer} aMaxLength The maximum length of the returned substring. If this is
518  *        greater than the length of the input string, we return the whole input string.
519  * @return {String} The substring or null if the input string is null.
520  */
521 function limitStringToLength(aString, aMaxLength) {
522   if (typeof aString !== "string") {
523     return null;
524   }
525   return aString.substring(0, aMaxLength);
529  * Force a value to be a string.
530  * Only if the value is null, null is returned instead.
531  */
532 function forceToStringOrNull(aValue) {
533   if (aValue === null) {
534     return null;
535   }
537   return String(aValue);
541  * Get the information about a graphic adapter.
543  * @param aSuffix A suffix to add to the properties names.
544  * @return An object containing the adapter properties.
545  */
546 function getGfxAdapter(aSuffix = "") {
547   // Note that gfxInfo, and so getGfxField, might return "Unknown" for the RAM on failures,
548   // not null.
549   let memoryMB = parseInt(getGfxField("adapterRAM" + aSuffix, null), 10);
550   if (Number.isNaN(memoryMB)) {
551     memoryMB = null;
552   }
554   return {
555     description: getGfxField("adapterDescription" + aSuffix, null),
556     vendorID: getGfxField("adapterVendorID" + aSuffix, null),
557     deviceID: getGfxField("adapterDeviceID" + aSuffix, null),
558     subsysID: getGfxField("adapterSubsysID" + aSuffix, null),
559     RAM: memoryMB,
560     driver: getGfxField("adapterDriver" + aSuffix, null),
561     driverVendor: getGfxField("adapterDriverVendor" + aSuffix, null),
562     driverVersion: getGfxField("adapterDriverVersion" + aSuffix, null),
563     driverDate: getGfxField("adapterDriverDate" + aSuffix, null),
564   };
568  * Encapsulates the asynchronous magic interfacing with the addon manager. The builder
569  * is owned by a parent environment object and is an addon listener.
570  */
571 function EnvironmentAddonBuilder(environment) {
572   this._environment = environment;
574   // The pending task blocks addon manager shutdown. It can either be the initial load
575   // or a change load.
576   this._pendingTask = null;
578   // Have we added an observer to listen for blocklist changes that still needs to be
579   // removed:
580   this._gmpProviderObserverAdded = false;
582   // Set to true once initial load is complete and we're watching for changes.
583   this._loaded = false;
585   // The state reported by the shutdown blocker if we hang shutdown.
586   this._shutdownState = "Initial";
588 EnvironmentAddonBuilder.prototype = {
589   /**
590    * Get the initial set of addons.
591    * @returns Promise<void> when the initial load is complete.
592    */
593   async init() {
594     AddonManager.beforeShutdown.addBlocker(
595       "EnvironmentAddonBuilder",
596       () => this._shutdownBlocker(),
597       { fetchState: () => this._shutdownState }
598     );
600     this._pendingTask = (async () => {
601       try {
602         this._shutdownState = "Awaiting _updateAddons";
603         // Gather initial addons details
604         await this._updateAddons();
606         if (!this._environment._addonsAreFull) {
607           // The addon database has not been loaded, wait for it to
608           // initialize and gather full data as soon as it does.
609           this._shutdownState = "Awaiting AddonManagerPrivate.databaseReady";
610           await AddonManagerPrivate.databaseReady;
612           // Now gather complete addons details.
613           this._shutdownState = "Awaiting second _updateAddons";
614           await this._updateAddons();
615         }
616       } catch (err) {
617         this._environment._log.error("init - Exception in _updateAddons", err);
618       } finally {
619         this._pendingTask = null;
620         this._shutdownState = "_pendingTask init complete. No longer blocking.";
621       }
622     })();
624     return this._pendingTask;
625   },
627   /**
628    * Register an addon listener and watch for changes.
629    */
630   watchForChanges() {
631     this._loaded = true;
632     AddonManager.addAddonListener(this);
633   },
635   // AddonListener
636   onEnabled(addon) {
637     this._onAddonChange(addon);
638   },
639   onDisabled(addon) {
640     this._onAddonChange(addon);
641   },
642   onInstalled(addon) {
643     this._onAddonChange(addon);
644   },
645   onUninstalling(addon) {
646     this._onAddonChange(addon);
647   },
648   onUninstalled(addon) {
649     this._onAddonChange(addon);
650   },
651   onPropertyChanged(addon, propertiesChanged) {
652     // Avoid to update the telemetry environment for onPropertyChanged
653     // calls that we are not actually interested in (and quarantineIgnoredByApp
654     // is not expected to change at runtime, unless the entire active addons
655     // entry is also replaced, e.g. on the extension being uninstalled and
656     // installed again).
657     if (!propertiesChanged.includes("quarantineIgnoredByUser")) {
658       return;
659     }
660     this._onAddonChange(addon);
661   },
663   _onAddonChange(addon) {
664     if (addon && addon.isBuiltin && !addon.isSystem) {
665       return;
666     }
667     this._environment._log.trace("_onAddonChange");
668     this._checkForChanges("addons-changed");
669   },
671   // nsIObserver
672   observe(aSubject, aTopic, aData) {
673     this._environment._log.trace("observe - Topic " + aTopic);
674     if (aTopic == GMP_PROVIDER_REGISTERED_TOPIC) {
675       Services.obs.removeObserver(this, GMP_PROVIDER_REGISTERED_TOPIC);
676       this._gmpProviderObserverAdded = false;
677       let gmpPluginsPromise = this._getActiveGMPlugins();
678       gmpPluginsPromise.then(
679         gmpPlugins => {
680           let { addons } = this._environment._currentEnvironment;
681           addons.activeGMPlugins = gmpPlugins;
682         },
683         err => {
684           this._environment._log.error(
685             "blocklist observe: Error collecting plugins",
686             err
687           );
688         }
689       );
690     }
691   },
693   _checkForChanges(changeReason) {
694     if (this._pendingTask) {
695       this._environment._log.trace(
696         "_checkForChanges - task already pending, dropping change with reason " +
697           changeReason
698       );
699       return;
700     }
702     this._shutdownState = "_checkForChanges awaiting _updateAddons";
703     this._pendingTask = this._updateAddons().then(
704       result => {
705         this._pendingTask = null;
706         this._shutdownState = "No longer blocking, _updateAddons resolved";
707         if (result.changed) {
708           this._environment._onEnvironmentChange(
709             changeReason,
710             result.oldEnvironment
711           );
712         }
713       },
714       err => {
715         this._pendingTask = null;
716         this._shutdownState = "No longer blocking, _updateAddons rejected";
717         this._environment._log.error(
718           "_checkForChanges: Error collecting addons",
719           err
720         );
721       }
722     );
723   },
725   _shutdownBlocker() {
726     if (this._loaded) {
727       AddonManager.removeAddonListener(this);
728       if (this._gmpProviderObserverAdded) {
729         Services.obs.removeObserver(this, GMP_PROVIDER_REGISTERED_TOPIC);
730       }
731     }
733     // At startup, _pendingTask is set to a Promise that does not resolve
734     // until the addons database has been read so complete details about
735     // addons are available.  Returning it here will cause it to block
736     // profileBeforeChange, guranteeing that full information will be
737     // available by the time profileBeforeChangeTelemetry is fired.
738     return this._pendingTask;
739   },
741   /**
742    * Collect the addon data for the environment.
743    *
744    * This should only be called from _pendingTask; otherwise we risk
745    * running this during addon manager shutdown.
746    *
747    * @returns Promise<Object> This returns a Promise resolved with a status object with the following members:
748    *   changed - Whether the environment changed.
749    *   oldEnvironment - Only set if a change occured, contains the environment data before the change.
750    */
751   async _updateAddons() {
752     this._environment._log.trace("_updateAddons");
754     let addons = {
755       activeAddons: await this._getActiveAddons(),
756       theme: await this._getActiveTheme(),
757       activeGMPlugins: await this._getActiveGMPlugins(),
758     };
760     let result = {
761       changed:
762         !this._environment._currentEnvironment.addons ||
763         !ObjectUtils.deepEqual(
764           addons.activeAddons,
765           this._environment._currentEnvironment.addons.activeAddons
766         ),
767     };
769     if (result.changed) {
770       this._environment._log.trace("_updateAddons: addons differ");
771       result.oldEnvironment = Cu.cloneInto(
772         this._environment._currentEnvironment,
773         {}
774       );
775     }
776     this._environment._currentEnvironment.addons = addons;
778     return result;
779   },
781   /**
782    * Get the addon data in object form.
783    * @return Promise<object> containing the addon data.
784    */
785   async _getActiveAddons() {
786     // Request addons, asynchronously.
787     // "theme" is excluded because it is already handled by _getActiveTheme.
788     let { addons: allAddons, fullData } = await AddonManager.getActiveAddons(
789       AddonManagerPrivate.getAddonTypesByProvider("XPIProvider").filter(
790         addonType => addonType != "theme"
791       )
792     );
794     this._environment._addonsAreFull = fullData;
795     let activeAddons = {};
796     for (let addon of allAddons) {
797       // Don't collect any information about the new built-in search webextensions
798       if (addon.isBuiltin && !addon.isSystem) {
799         continue;
800       }
801       // Weird addon data in the wild can lead to exceptions while collecting
802       // the data.
803       try {
804         // Make sure to have valid dates.
805         let updateDate = new Date(Math.max(0, addon.updateDate));
807         activeAddons[addon.id] = {
808           version: limitStringToLength(addon.version, MAX_ADDON_STRING_LENGTH),
809           scope: addon.scope,
810           type: addon.type,
811           updateDay: Utils.millisecondsToDays(updateDate.getTime()),
812           isSystem: addon.isSystem,
813           isWebExtension: addon.isWebExtension,
814           multiprocessCompatible: true,
815         };
817         // getActiveAddons() gives limited data during startup and full
818         // data after the addons database is loaded.
819         if (fullData) {
820           let installDate = new Date(Math.max(0, addon.installDate));
821           Object.assign(activeAddons[addon.id], {
822             blocklisted:
823               addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
824             description: limitStringToLength(
825               addon.description,
826               MAX_ADDON_STRING_LENGTH
827             ),
828             name: limitStringToLength(addon.name, MAX_ADDON_STRING_LENGTH),
829             userDisabled: enforceBoolean(addon.userDisabled),
830             appDisabled: addon.appDisabled,
831             foreignInstall: enforceBoolean(addon.foreignInstall),
832             hasBinaryComponents: false,
833             installDay: Utils.millisecondsToDays(installDate.getTime()),
834             signedState: addon.signedState,
835             quarantineIgnoredByApp: enforceBoolean(
836               addon.quarantineIgnoredByApp
837             ),
838             quarantineIgnoredByUser: enforceBoolean(
839               addon.quarantineIgnoredByUser
840             ),
841           });
842         }
843       } catch (ex) {
844         this._environment._log.error(
845           "_getActiveAddons - An addon was discarded due to an error",
846           ex
847         );
848         continue;
849       }
850     }
852     return activeAddons;
853   },
855   /**
856    * Get the currently active theme data in object form.
857    * @return Promise<object> containing the active theme data.
858    */
859   async _getActiveTheme() {
860     // Request themes, asynchronously.
861     let { addons: themes } = await AddonManager.getActiveAddons(["theme"]);
863     let activeTheme = {};
864     // We only store information about the active theme.
865     let theme = themes.find(theme => theme.isActive);
866     if (theme) {
867       // Make sure to have valid dates.
868       let installDate = new Date(Math.max(0, theme.installDate));
869       let updateDate = new Date(Math.max(0, theme.updateDate));
871       activeTheme = {
872         id: theme.id,
873         blocklisted:
874           theme.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
875         description: limitStringToLength(
876           theme.description,
877           MAX_ADDON_STRING_LENGTH
878         ),
879         name: limitStringToLength(theme.name, MAX_ADDON_STRING_LENGTH),
880         userDisabled: enforceBoolean(theme.userDisabled),
881         appDisabled: theme.appDisabled,
882         version: limitStringToLength(theme.version, MAX_ADDON_STRING_LENGTH),
883         scope: theme.scope,
884         foreignInstall: enforceBoolean(theme.foreignInstall),
885         hasBinaryComponents: false,
886         installDay: Utils.millisecondsToDays(installDate.getTime()),
887         updateDay: Utils.millisecondsToDays(updateDate.getTime()),
888       };
889     }
891     return activeTheme;
892   },
894   /**
895    * Get the GMPlugins data in object form.
896    *
897    * @return Object containing the GMPlugins data.
898    *
899    * This should only be called from _pendingTask; otherwise we risk
900    * running this during addon manager shutdown.
901    */
902   async _getActiveGMPlugins() {
903     // If we haven't yet loaded the blocklist, pass back dummy data for now,
904     // and add an observer to update this data as soon as we get it.
905     if (!AddonManager.hasProvider("GMPProvider")) {
906       if (!this._gmpProviderObserverAdded) {
907         Services.obs.addObserver(this, GMP_PROVIDER_REGISTERED_TOPIC);
908         this._gmpProviderObserverAdded = true;
909       }
910       return {
911         "dummy-gmp": {
912           version: "0.1",
913           userDisabled: false,
914           applyBackgroundUpdates: 1,
915         },
916       };
917     }
918     // Request plugins, asynchronously.
919     let allPlugins = await AddonManager.getAddonsByTypes(["plugin"]);
921     let activeGMPlugins = {};
922     for (let plugin of allPlugins) {
923       // Only get info for active GMplugins.
924       if (!plugin.isGMPlugin || !plugin.isActive) {
925         continue;
926       }
928       try {
929         activeGMPlugins[plugin.id] = {
930           version: plugin.version,
931           userDisabled: enforceBoolean(plugin.userDisabled),
932           applyBackgroundUpdates: plugin.applyBackgroundUpdates,
933         };
934       } catch (ex) {
935         this._environment._log.error(
936           "_getActiveGMPlugins - A GMPlugin was discarded due to an error",
937           ex
938         );
939         continue;
940       }
941     }
943     return activeGMPlugins;
944   },
947 function EnvironmentCache() {
948   this._log = Log.repository.getLoggerWithMessagePrefix(
949     LOGGER_NAME,
950     "TelemetryEnvironment::"
951   );
952   this._log.trace("constructor");
954   this._shutdown = false;
955   // Don't allow querying the search service too early to prevent
956   // impacting the startup performance.
957   this._canQuerySearch = false;
958   // To guard against slowing down startup, defer gathering heavy environment
959   // entries until the session is restored.
960   this._sessionWasRestored = false;
962   // A map of listeners that will be called on environment changes.
963   this._changeListeners = new Map();
965   // A map of watched preferences which trigger an Environment change when
966   // modified. Every entry contains a recording policy (RECORD_PREF_*).
967   this._watchedPrefs = DEFAULT_ENVIRONMENT_PREFS;
969   this._currentEnvironment = {
970     build: this._getBuild(),
971     partner: this._getPartner(),
972     system: this._getSystem(),
973   };
975   this._addObservers();
977   // Build the remaining asynchronous parts of the environment. Don't register change listeners
978   // until the initial environment has been built.
980   let p = [this._updateSettings()];
981   this._addonBuilder = new EnvironmentAddonBuilder(this);
982   p.push(this._addonBuilder.init());
984   this._currentEnvironment.profile = {};
985   p.push(this._updateProfile());
986   if (AppConstants.MOZ_BUILD_APP == "browser") {
987     p.push(this._loadAttributionAsync());
988   }
989   p.push(this._loadAsyncUpdateSettings());
990   p.push(this._loadIntlData());
992   for (const [
993     id,
994     { branch, options },
995   ] of gActiveExperimentStartupBuffer.entries()) {
996     this.setExperimentActive(id, branch, options);
997   }
998   gActiveExperimentStartupBuffer = null;
1000   let setup = () => {
1001     this._initTask = null;
1002     this._startWatchingPrefs();
1003     this._addonBuilder.watchForChanges();
1004     this._updateGraphicsFeatures();
1005     return this.currentEnvironment;
1006   };
1008   this._initTask = Promise.all(p).then(
1009     () => setup(),
1010     err => {
1011       // log errors but eat them for consumers
1012       this._log.error("EnvironmentCache - error while initializing", err);
1013       return setup();
1014     }
1015   );
1017   // Addons may contain partial or full data depending on whether the Addons DB
1018   // has had a chance to load. Do we have full data yet?
1019   this._addonsAreFull = false;
1021 EnvironmentCache.prototype = {
1022   /**
1023    * The current environment data. The returned data is cloned to avoid
1024    * unexpected sharing or mutation.
1025    * @returns object
1026    */
1027   get currentEnvironment() {
1028     return Cu.cloneInto(this._currentEnvironment, {});
1029   },
1031   /**
1032    * Wait for the current enviroment to be fully initialized.
1033    * @returns Promise<object>
1034    */
1035   onInitialized() {
1036     if (this._initTask) {
1037       return this._initTask;
1038     }
1039     return Promise.resolve(this.currentEnvironment);
1040   },
1042   /**
1043    * This gets called when the delayed init completes.
1044    */
1045   async delayedInit() {
1046     this._processData = await Services.sysinfo.processInfo;
1047     let processData = await Services.sysinfo.processInfo;
1048     // Remove isWow64 and isWowARM64 from processData
1049     // to strip it down to just CPU info
1050     delete processData.isWow64;
1051     delete processData.isWowARM64;
1053     let oldEnv = null;
1054     if (!this._initTask) {
1055       oldEnv = this.currentEnvironment;
1056     }
1058     this._cpuData = this._getCPUData();
1059     // Augment the return value from the promises with cached values
1060     this._cpuData = { ...processData, ...this._cpuData };
1062     this._currentEnvironment.system.cpu = this._getCPUData();
1064     if (AppConstants.platform == "win") {
1065       this._hddData = await Services.sysinfo.diskInfo;
1066       let osData = await Services.sysinfo.osInfo;
1068       if (!this._initTask) {
1069         // We've finished creating the initial env, so notify for the update
1070         // This is all a bit awkward because `currentEnvironment` clones
1071         // the object, which we need to pass to the notification, but we
1072         // should only notify once we've updated the current environment...
1073         // Ideally, _onEnvironmentChange should somehow deal with all this
1074         // instead of all the consumers.
1075         oldEnv = this.currentEnvironment;
1076       }
1078       this._osData = this._getOSData();
1080       // Augment the return values from the promises with cached values
1081       this._osData = Object.assign(osData, this._osData);
1083       this._currentEnvironment.system.os = this._getOSData();
1084       this._currentEnvironment.system.hdd = this._getHDDData();
1086       // Windows only values stored in processData
1087       this._currentEnvironment.system.isWow64 = this._getProcessData().isWow64;
1088       this._currentEnvironment.system.isWowARM64 =
1089         this._getProcessData().isWowARM64;
1090     }
1092     if (!this._initTask) {
1093       this._onEnvironmentChange("system-info", oldEnv);
1094     }
1095   },
1097   /**
1098    * Register a listener for environment changes.
1099    * @param name The name of the listener. If a new listener is registered
1100    *             with the same name, the old listener will be replaced.
1101    * @param listener function(reason, oldEnvironment) - Will receive a reason for
1102                      the change and the environment data before the change.
1103    */
1104   registerChangeListener(name, listener) {
1105     this._log.trace("registerChangeListener for " + name);
1106     if (this._shutdown) {
1107       this._log.warn("registerChangeListener - already shutdown");
1108       return;
1109     }
1110     this._changeListeners.set(name, listener);
1111   },
1113   /**
1114    * Unregister from listening to environment changes.
1115    * It's fine to call this on an unitialized TelemetryEnvironment.
1116    * @param name The name of the listener to remove.
1117    */
1118   unregisterChangeListener(name) {
1119     this._log.trace("unregisterChangeListener for " + name);
1120     if (this._shutdown) {
1121       this._log.warn("registerChangeListener - already shutdown");
1122       return;
1123     }
1124     this._changeListeners.delete(name);
1125   },
1127   setExperimentActive(id, branch, options) {
1128     this._log.trace(`setExperimentActive - id: ${id}, branch: ${branch}`);
1129     // Make sure both the id and the branch have sane lengths.
1130     const saneId = limitStringToLength(id, MAX_EXPERIMENT_ID_LENGTH);
1131     const saneBranch = limitStringToLength(
1132       branch,
1133       MAX_EXPERIMENT_BRANCH_LENGTH
1134     );
1135     if (!saneId || !saneBranch) {
1136       this._log.error(
1137         "setExperimentActive - the provided arguments are not strings."
1138       );
1139       return;
1140     }
1142     // Warn the user about any content truncation.
1143     if (saneId.length != id.length || saneBranch.length != branch.length) {
1144       this._log.warn(
1145         "setExperimentActive - the experiment id or branch were truncated."
1146       );
1147     }
1149     // Truncate the experiment type if present.
1150     if (options.hasOwnProperty("type")) {
1151       let type = limitStringToLength(options.type, MAX_EXPERIMENT_TYPE_LENGTH);
1152       if (type.length != options.type.length) {
1153         options.type = type;
1154         this._log.warn(
1155           "setExperimentActive - the experiment type was truncated."
1156         );
1157       }
1158     }
1160     // Truncate the enrollment id if present.
1161     if (options.hasOwnProperty("enrollmentId")) {
1162       let enrollmentId = limitStringToLength(
1163         options.enrollmentId,
1164         MAX_EXPERIMENT_ENROLLMENT_ID_LENGTH
1165       );
1166       if (enrollmentId.length != options.enrollmentId.length) {
1167         options.enrollmentId = enrollmentId;
1168         this._log.warn(
1169           "setExperimentActive - the enrollment id was truncated."
1170         );
1171       }
1172     }
1174     let oldEnvironment = Cu.cloneInto(this._currentEnvironment, {});
1175     // Add the experiment annotation.
1176     let experiments = this._currentEnvironment.experiments || {};
1177     experiments[saneId] = { branch: saneBranch };
1178     if (options.hasOwnProperty("type")) {
1179       experiments[saneId].type = options.type;
1180     }
1181     if (options.hasOwnProperty("enrollmentId")) {
1182       experiments[saneId].enrollmentId = options.enrollmentId;
1183     }
1184     this._currentEnvironment.experiments = experiments;
1185     // Notify of the change.
1186     this._onEnvironmentChange("experiment-annotation-changed", oldEnvironment);
1187   },
1189   setExperimentInactive(id) {
1190     this._log.trace("setExperimentInactive");
1191     let experiments = this._currentEnvironment.experiments || {};
1192     if (id in experiments) {
1193       // Only attempt to notify if a previous annotation was found and removed.
1194       let oldEnvironment = Cu.cloneInto(this._currentEnvironment, {});
1195       // Remove the experiment annotation.
1196       delete this._currentEnvironment.experiments[id];
1197       // Notify of the change.
1198       this._onEnvironmentChange(
1199         "experiment-annotation-changed",
1200         oldEnvironment
1201       );
1202     }
1203   },
1205   getActiveExperiments() {
1206     return Cu.cloneInto(this._currentEnvironment.experiments || {}, {});
1207   },
1209   shutdown() {
1210     this._log.trace("shutdown");
1211     this._shutdown = true;
1212   },
1214   /**
1215    * Only used in tests, set the preferences to watch.
1216    * @param aPreferences A map of preferences names and their recording policy.
1217    */
1218   _watchPreferences(aPreferences) {
1219     this._stopWatchingPrefs();
1220     this._watchedPrefs = aPreferences;
1221     this._updateSettings();
1222     this._startWatchingPrefs();
1223   },
1225   /**
1226    * Get an object containing the values for the watched preferences. Depending on the
1227    * policy, the value for a preference or whether it was changed by user is reported.
1228    *
1229    * @return An object containing the preferences values.
1230    */
1231   _getPrefData() {
1232     let prefData = {};
1233     for (let [pref, policy] of this._watchedPrefs.entries()) {
1234       let prefValue = this._getPrefValue(pref, policy.what);
1236       if (prefValue === undefined) {
1237         continue;
1238       }
1240       prefData[pref] = prefValue;
1241     }
1242     return prefData;
1243   },
1245   /**
1246    * Get the value of a preference given the preference name and the policy.
1247    * @param pref Name of the preference.
1248    * @param what Policy of the preference.
1249    *
1250    * @returns The value we need to store for this preference. It can be undefined
1251    *          or null if the preference is invalid or has a value set by the user.
1252    */
1253   _getPrefValue(pref, what) {
1254     // Check the policy for the preference and decide if we need to store its value
1255     // or whether it changed from the default value.
1256     let prefType = Services.prefs.getPrefType(pref);
1258     if (
1259       what == TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE ||
1260       what == TelemetryEnvironment.RECORD_DEFAULTPREF_STATE
1261     ) {
1262       // For default prefs, make sure they exist
1263       if (prefType == Ci.nsIPrefBranch.PREF_INVALID) {
1264         return undefined;
1265       }
1266     } else if (!Services.prefs.prefHasUserValue(pref)) {
1267       // For user prefs, make sure they are set
1268       return undefined;
1269     }
1271     if (what == TelemetryEnvironment.RECORD_DEFAULTPREF_STATE) {
1272       return "<set>";
1273     } else if (what == TelemetryEnvironment.RECORD_PREF_STATE) {
1274       return "<user-set>";
1275     } else if (prefType == Ci.nsIPrefBranch.PREF_STRING) {
1276       return Services.prefs.getStringPref(pref);
1277     } else if (prefType == Ci.nsIPrefBranch.PREF_BOOL) {
1278       return Services.prefs.getBoolPref(pref);
1279     } else if (prefType == Ci.nsIPrefBranch.PREF_INT) {
1280       return Services.prefs.getIntPref(pref);
1281     } else if (prefType == Ci.nsIPrefBranch.PREF_INVALID) {
1282       return null;
1283     }
1284     throw new Error(
1285       `Unexpected preference type ("${prefType}") for "${pref}".`
1286     );
1287   },
1289   QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
1291   /**
1292    * Start watching the preferences.
1293    */
1294   _startWatchingPrefs() {
1295     this._log.trace("_startWatchingPrefs - " + this._watchedPrefs);
1297     Services.prefs.addObserver("", this, true);
1298   },
1300   _onPrefChanged(aData) {
1301     this._log.trace("_onPrefChanged");
1302     let oldEnvironment = Cu.cloneInto(this._currentEnvironment, {});
1303     this._currentEnvironment.settings.userPrefs[aData] = this._getPrefValue(
1304       aData,
1305       this._watchedPrefs.get(aData).what
1306     );
1307     this._onEnvironmentChange("pref-changed", oldEnvironment);
1308   },
1310   /**
1311    * Do not receive any more change notifications for the preferences.
1312    */
1313   _stopWatchingPrefs() {
1314     this._log.trace("_stopWatchingPrefs");
1316     Services.prefs.removeObserver("", this);
1317   },
1319   _addObservers() {
1320     // Watch the search engine change and service topics.
1321     Services.obs.addObserver(this, SESSIONSTORE_WINDOWS_RESTORED_TOPIC);
1322     Services.obs.addObserver(this, COMPOSITOR_CREATED_TOPIC);
1323     Services.obs.addObserver(this, COMPOSITOR_PROCESS_ABORTED_TOPIC);
1324     Services.obs.addObserver(this, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);
1325     Services.obs.addObserver(this, GFX_FEATURES_READY_TOPIC);
1326     Services.obs.addObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC);
1327     Services.obs.addObserver(this, SEARCH_SERVICE_TOPIC);
1328     Services.obs.addObserver(this, AUTO_UPDATE_PREF_CHANGE_TOPIC);
1329     Services.obs.addObserver(this, BACKGROUND_UPDATE_PREF_CHANGE_TOPIC);
1330     Services.obs.addObserver(this, SERVICES_INFO_CHANGE_TOPIC);
1331   },
1333   _removeObservers() {
1334     Services.obs.removeObserver(this, SESSIONSTORE_WINDOWS_RESTORED_TOPIC);
1335     Services.obs.removeObserver(this, COMPOSITOR_CREATED_TOPIC);
1336     Services.obs.removeObserver(this, COMPOSITOR_PROCESS_ABORTED_TOPIC);
1337     try {
1338       Services.obs.removeObserver(
1339         this,
1340         DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC
1341       );
1342     } catch (ex) {}
1343     Services.obs.removeObserver(this, GFX_FEATURES_READY_TOPIC);
1344     Services.obs.removeObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC);
1345     Services.obs.removeObserver(this, SEARCH_SERVICE_TOPIC);
1346     Services.obs.removeObserver(this, AUTO_UPDATE_PREF_CHANGE_TOPIC);
1347     Services.obs.removeObserver(this, BACKGROUND_UPDATE_PREF_CHANGE_TOPIC);
1348     Services.obs.removeObserver(this, SERVICES_INFO_CHANGE_TOPIC);
1349   },
1351   observe(aSubject, aTopic, aData) {
1352     this._log.trace("observe - aTopic: " + aTopic + ", aData: " + aData);
1353     switch (aTopic) {
1354       case SEARCH_ENGINE_MODIFIED_TOPIC:
1355         if (
1356           aData != "engine-default" &&
1357           aData != "engine-default-private" &&
1358           aData != "engine-changed"
1359         ) {
1360           return;
1361         }
1362         if (
1363           aData == "engine-changed" &&
1364           aSubject.QueryInterface(Ci.nsISearchEngine) &&
1365           Services.search.defaultEngine != aSubject
1366         ) {
1367           return;
1368         }
1369         // Record the new default search choice and send the change notification.
1370         this._onSearchEngineChange();
1371         break;
1372       case SEARCH_SERVICE_TOPIC:
1373         if (aData != "init-complete") {
1374           return;
1375         }
1376         // Now that the search engine init is complete, record the default search choice.
1377         this._canQuerySearch = true;
1378         this._updateSearchEngine();
1379         break;
1380       case GFX_FEATURES_READY_TOPIC:
1381       case COMPOSITOR_CREATED_TOPIC:
1382         // Full graphics information is not available until we have created at
1383         // least one off-main-thread-composited window. Thus we wait for the
1384         // first compositor to be created and then query nsIGfxInfo again.
1385         this._updateGraphicsFeatures();
1386         break;
1387       case COMPOSITOR_PROCESS_ABORTED_TOPIC:
1388         // Our compositor process has been killed for whatever reason, so refresh
1389         // our reported graphics features and trigger an environment change.
1390         this._onCompositorProcessAborted();
1391         break;
1392       case DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC:
1393         // Distribution customizations are applied after final-ui-startup. query
1394         // partner prefs again when they are ready.
1395         this._updatePartner();
1396         Services.obs.removeObserver(this, aTopic);
1397         break;
1398       case SESSIONSTORE_WINDOWS_RESTORED_TOPIC:
1399         this._sessionWasRestored = true;
1400         // Make sure to initialize the search service once we've done restoring
1401         // the windows, so that we don't risk loosing search data.
1402         Services.search.init();
1403         // The default browser check could take some time, so just call it after
1404         // the session was restored.
1405         this._updateDefaultBrowser();
1406         break;
1407       case PREF_CHANGED_TOPIC:
1408         let options = this._watchedPrefs.get(aData);
1409         if (options && !options.requiresRestart) {
1410           this._onPrefChanged(aData);
1411         }
1412         break;
1413       case AUTO_UPDATE_PREF_CHANGE_TOPIC:
1414         this._currentEnvironment.settings.update.autoDownload = aData == "true";
1415         break;
1416       case BACKGROUND_UPDATE_PREF_CHANGE_TOPIC:
1417         this._currentEnvironment.settings.update.background = aData == "true";
1418         break;
1419       case SERVICES_INFO_CHANGE_TOPIC:
1420         this._updateServicesInfo();
1421         break;
1422     }
1423   },
1425   /**
1426    * Update the default search engine value.
1427    */
1428   _updateSearchEngine() {
1429     if (!this._canQuerySearch) {
1430       this._log.trace("_updateSearchEngine - ignoring early call");
1431       return;
1432     }
1434     this._log.trace(
1435       "_updateSearchEngine - isInitialized: " + Services.search.isInitialized
1436     );
1437     if (!Services.search.isInitialized) {
1438       return;
1439     }
1441     // Make sure we have a settings section.
1442     this._currentEnvironment.settings = this._currentEnvironment.settings || {};
1444     // Update the search engine entry in the current environment.
1445     const defaultEngineInfo = Services.search.getDefaultEngineInfo();
1446     this._currentEnvironment.settings.defaultSearchEngine =
1447       defaultEngineInfo.defaultSearchEngine;
1448     this._currentEnvironment.settings.defaultSearchEngineData = {
1449       ...defaultEngineInfo.defaultSearchEngineData,
1450     };
1451     if ("defaultPrivateSearchEngine" in defaultEngineInfo) {
1452       this._currentEnvironment.settings.defaultPrivateSearchEngine =
1453         defaultEngineInfo.defaultPrivateSearchEngine;
1454     }
1455     if ("defaultPrivateSearchEngineData" in defaultEngineInfo) {
1456       this._currentEnvironment.settings.defaultPrivateSearchEngineData = {
1457         ...defaultEngineInfo.defaultPrivateSearchEngineData,
1458       };
1459     }
1460   },
1462   /**
1463    * Update the default search engine value and trigger the environment change.
1464    */
1465   _onSearchEngineChange() {
1466     this._log.trace("_onSearchEngineChange");
1468     // Finally trigger the environment change notification.
1469     let oldEnvironment = Cu.cloneInto(this._currentEnvironment, {});
1470     this._updateSearchEngine();
1471     this._onEnvironmentChange("search-engine-changed", oldEnvironment);
1472   },
1474   /**
1475    * Refresh the Telemetry environment and trigger an environment change due to
1476    * a change in compositor process (normally this will mean we've fallen back
1477    * from out-of-process to in-process compositing).
1478    */
1479   _onCompositorProcessAborted() {
1480     this._log.trace("_onCompositorProcessAborted");
1482     // Trigger the environment change notification.
1483     let oldEnvironment = Cu.cloneInto(this._currentEnvironment, {});
1484     this._updateGraphicsFeatures();
1485     this._onEnvironmentChange("gfx-features-changed", oldEnvironment);
1486   },
1488   /**
1489    * Update the graphics features object.
1490    */
1491   _updateGraphicsFeatures() {
1492     let gfxData = this._currentEnvironment.system.gfx;
1493     try {
1494       let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
1495       gfxData.features = gfxInfo.getFeatures();
1496     } catch (e) {
1497       this._log.error("nsIGfxInfo.getFeatures() caught error", e);
1498     }
1499   },
1501   /**
1502    * Update the partner prefs.
1503    */
1504   _updatePartner() {
1505     this._currentEnvironment.partner = this._getPartner();
1506   },
1508   /**
1509    * Get the build data in object form.
1510    * @return Object containing the build data.
1511    */
1512   _getBuild() {
1513     let buildData = {
1514       applicationId: Services.appinfo.ID || null,
1515       applicationName: Services.appinfo.name || null,
1516       architecture: Services.sysinfo.get("arch"),
1517       buildId: Services.appinfo.appBuildID || null,
1518       version: Services.appinfo.version || null,
1519       vendor: Services.appinfo.vendor || null,
1520       displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY || null,
1521       platformVersion: Services.appinfo.platformVersion || null,
1522       xpcomAbi: Services.appinfo.XPCOMABI,
1523       updaterAvailable: AppConstants.MOZ_UPDATER,
1524     };
1526     return buildData;
1527   },
1529   /**
1530    * Determine if we're the default browser.
1531    * @returns null on error, true if we are the default browser, or false otherwise.
1532    */
1533   _isDefaultBrowser() {
1534     let isDefault = (service, ...args) => {
1535       try {
1536         return !!service.isDefaultBrowser(...args);
1537       } catch (ex) {
1538         this._log.error(
1539           "_isDefaultBrowser - Could not determine if default browser",
1540           ex
1541         );
1542         return null;
1543       }
1544     };
1546     if (!("@mozilla.org/browser/shell-service;1" in Cc)) {
1547       this._log.info(
1548         "_isDefaultBrowser - Could not obtain browser shell service"
1549       );
1550       return null;
1551     }
1553     try {
1554       let { ShellService } = ChromeUtils.importESModule(
1555         "resource:///modules/ShellService.sys.mjs"
1556       );
1557       // This uses the same set of flags used by the pref pane.
1558       return isDefault(ShellService, false, true);
1559     } catch (ex) {
1560       this._log.error("_isDefaultBrowser - Could not obtain shell service JSM");
1561     }
1563     try {
1564       let shellService = Cc["@mozilla.org/browser/shell-service;1"].getService(
1565         Ci.nsIShellService
1566       );
1567       // This uses the same set of flags used by the pref pane.
1568       return isDefault(shellService, true);
1569     } catch (ex) {
1570       this._log.error("_isDefaultBrowser - Could not obtain shell service", ex);
1571       return null;
1572     }
1573   },
1575   _updateDefaultBrowser() {
1576     if (AppConstants.platform === "android") {
1577       return;
1578     }
1579     // Make sure to have a settings section.
1580     this._currentEnvironment.settings = this._currentEnvironment.settings || {};
1581     this._currentEnvironment.settings.isDefaultBrowser = this
1582       ._sessionWasRestored
1583       ? this._isDefaultBrowser()
1584       : null;
1585   },
1587   /**
1588    * Update the cached settings data.
1589    */
1590   _updateSettings() {
1591     let updateChannel = null;
1592     try {
1593       updateChannel = Utils.getUpdateChannel();
1594     } catch (e) {}
1596     this._currentEnvironment.settings = {
1597       blocklistEnabled: Services.prefs.getBoolPref(
1598         PREF_BLOCKLIST_ENABLED,
1599         true
1600       ),
1601       e10sEnabled: Services.appinfo.browserTabsRemoteAutostart,
1602       e10sMultiProcesses: Services.appinfo.maxWebProcessCount,
1603       fissionEnabled: Services.appinfo.fissionAutostart,
1604       telemetryEnabled: Utils.isTelemetryEnabled,
1605       locale: getBrowserLocale(),
1606       // We need to wait for browser-delayed-startup-finished to ensure that the locales
1607       // have settled, once that's happened we can get the intl data directly.
1608       intl: Policy._intlLoaded ? getIntlSettings() : {},
1609       update: {
1610         channel: updateChannel,
1611         enabled: !Services.policies || Services.policies.isAllowed("appUpdate"),
1612       },
1613       userPrefs: this._getPrefData(),
1614       sandbox: this._getSandboxData(),
1615     };
1617     // Services.appinfo.launcherProcessState is not available in all build
1618     // configurations, in which case an exception may be thrown.
1619     try {
1620       this._currentEnvironment.settings.launcherProcessState =
1621         Services.appinfo.launcherProcessState;
1622     } catch (e) {}
1624     this._currentEnvironment.settings.addonCompatibilityCheckEnabled =
1625       AddonManager.checkCompatibility;
1627     if (AppConstants.MOZ_BUILD_APP == "browser") {
1628       this._updateAttribution();
1629     }
1630     this._updateDefaultBrowser();
1631     this._updateSearchEngine();
1632     this._loadAsyncUpdateSettingsFromCache();
1633   },
1635   _getSandboxData() {
1636     let effectiveContentProcessLevel = null;
1637     let contentWin32kLockdownState = null;
1638     try {
1639       let sandboxSettings = Cc[
1640         "@mozilla.org/sandbox/sandbox-settings;1"
1641       ].getService(Ci.mozISandboxSettings);
1642       effectiveContentProcessLevel =
1643         sandboxSettings.effectiveContentSandboxLevel;
1645       // The possible values for this are defined in the ContentWin32kLockdownState
1646       // enum in security/sandbox/common/SandboxSettings.h
1647       contentWin32kLockdownState = sandboxSettings.contentWin32kLockdownState;
1648     } catch (e) {}
1649     return {
1650       effectiveContentProcessLevel,
1651       contentWin32kLockdownState,
1652     };
1653   },
1655   /**
1656    * Update the cached profile data.
1657    * @returns Promise<> resolved when the I/O is complete.
1658    */
1659   async _updateProfile() {
1660     let profileAccessor = await lazy.ProfileAge();
1662     let creationDate = await profileAccessor.created;
1663     let resetDate = await profileAccessor.reset;
1664     let firstUseDate = await profileAccessor.firstUse;
1666     this._currentEnvironment.profile.creationDate =
1667       Utils.millisecondsToDays(creationDate);
1668     if (resetDate) {
1669       this._currentEnvironment.profile.resetDate =
1670         Utils.millisecondsToDays(resetDate);
1671     }
1672     if (firstUseDate) {
1673       this._currentEnvironment.profile.firstUseDate =
1674         Utils.millisecondsToDays(firstUseDate);
1675     }
1676   },
1678   /**
1679    * Load the attribution data object and updates the environment.
1680    * @returns Promise<> resolved when the I/O is complete.
1681    */
1682   async _loadAttributionAsync() {
1683     try {
1684       await lazy.AttributionCode.getAttrDataAsync();
1685     } catch (e) {
1686       // The AttributionCode.sys.mjs module might not be always available
1687       // (e.g. tests). Gracefully handle this.
1688       return;
1689     }
1690     this._updateAttribution();
1691   },
1693   /**
1694    * Update the environment with the cached attribution data.
1695    */
1696   _updateAttribution() {
1697     let data = null;
1698     try {
1699       data = lazy.AttributionCode.getCachedAttributionData();
1700     } catch (e) {
1701       // The AttributionCode.sys.mjs module might not be always available
1702       // (e.g. tests). Gracefully handle this.
1703     }
1705     if (!data || !Object.keys(data).length) {
1706       return;
1707     }
1709     let attributionData = {};
1710     for (let key in data) {
1711       attributionData[key] =
1712         // At least one of these may be boolean, and limitStringToLength
1713         // returns null for non-string inputs.
1714         typeof data[key] === "string"
1715           ? limitStringToLength(data[key], MAX_ATTRIBUTION_STRING_LENGTH)
1716           : data[key];
1717     }
1718     this._currentEnvironment.settings.attribution = attributionData;
1719   },
1721   /**
1722    * Load the per-installation update settings, cache them, and add them to the
1723    * environment.
1724    */
1725   async _loadAsyncUpdateSettings() {
1726     if (AppConstants.MOZ_UPDATER) {
1727       this._updateAutoDownloadCache =
1728         await UpdateUtils.getAppUpdateAutoEnabled();
1729       this._updateBackgroundCache = await UpdateUtils.readUpdateConfigSetting(
1730         "app.update.background.enabled"
1731       );
1732     } else {
1733       this._updateAutoDownloadCache = false;
1734       this._updateBackgroundCache = false;
1735     }
1736     this._loadAsyncUpdateSettingsFromCache();
1737   },
1739   /**
1740    * Update the environment with the cached values for per-installation update
1741    * settings.
1742    */
1743   _loadAsyncUpdateSettingsFromCache() {
1744     if (this._updateAutoDownloadCache !== undefined) {
1745       this._currentEnvironment.settings.update.autoDownload =
1746         this._updateAutoDownloadCache;
1747     }
1748     if (this._updateBackgroundCache !== undefined) {
1749       this._currentEnvironment.settings.update.background =
1750         this._updateBackgroundCache;
1751     }
1752   },
1754   /**
1755    * Get i18n data about the system.
1756    * @return A promise of completion.
1757    */
1758   async _loadIntlData() {
1759     // Wait for the startup topic.
1760     await Policy._browserDelayedStartup();
1761     this._currentEnvironment.settings.intl = getIntlSettings();
1762     Policy._intlLoaded = true;
1763   },
1764   // This exists as a separate function for testing.
1765   async _getFxaSignedInUser() {
1766     return lazy.fxAccounts.getSignedInUser();
1767   },
1769   async _updateServicesInfo() {
1770     let syncEnabled = false;
1771     let accountEnabled = false;
1772     let weaveService =
1773       Cc["@mozilla.org/weave/service;1"].getService().wrappedJSObject;
1774     syncEnabled = weaveService && weaveService.enabled;
1775     if (syncEnabled) {
1776       // All sync users are account users, definitely.
1777       accountEnabled = true;
1778     } else {
1779       // Not all account users are sync users. See if they're signed into FxA.
1780       try {
1781         let user = await this._getFxaSignedInUser();
1782         if (user) {
1783           accountEnabled = true;
1784         }
1785       } catch (e) {
1786         // We don't know. This might be a transient issue which will clear
1787         // itself up later, but the information in telemetry is quite possibly stale
1788         // (this is called from a change listener), so clear it out to avoid
1789         // reporting data which might be wrong until we can figure it out.
1790         delete this._currentEnvironment.services;
1791         this._log.error("_updateServicesInfo() caught error", e);
1792         return;
1793       }
1794     }
1795     this._currentEnvironment.services = {
1796       accountEnabled,
1797       syncEnabled,
1798     };
1799   },
1801   /**
1802    * Get the partner data in object form.
1803    * @return Object containing the partner data.
1804    */
1805   _getPartner() {
1806     let defaults = Services.prefs.getDefaultBranch(null);
1807     let partnerData = {
1808       distributionId: defaults.getStringPref(PREF_DISTRIBUTION_ID, null),
1809       distributionVersion: defaults.getCharPref(
1810         PREF_DISTRIBUTION_VERSION,
1811         null
1812       ),
1813       partnerId: defaults.getCharPref(PREF_PARTNER_ID, null),
1814       distributor: defaults.getCharPref(PREF_DISTRIBUTOR, null),
1815       distributorChannel: defaults.getCharPref(PREF_DISTRIBUTOR_CHANNEL, null),
1816     };
1818     // Get the PREF_APP_PARTNER_BRANCH branch and append its children to partner data.
1819     let partnerBranch = Services.prefs.getDefaultBranch(
1820       PREF_APP_PARTNER_BRANCH
1821     );
1822     partnerData.partnerNames = partnerBranch.getChildList("");
1824     return partnerData;
1825   },
1827   _cpuData: null,
1828   /**
1829    * Get the CPU information.
1830    * @return Object containing the CPU information data.
1831    */
1832   _getCPUData() {
1833     if (this._cpuData) {
1834       return this._cpuData;
1835     }
1837     this._cpuData = {};
1839     const CPU_EXTENSIONS = [
1840       "hasMMX",
1841       "hasSSE",
1842       "hasSSE2",
1843       "hasSSE3",
1844       "hasSSSE3",
1845       "hasSSE4A",
1846       "hasSSE4_1",
1847       "hasSSE4_2",
1848       "hasAVX",
1849       "hasAVX2",
1850       "hasAES",
1851       "hasEDSP",
1852       "hasARMv6",
1853       "hasARMv7",
1854       "hasNEON",
1855       "hasUserCET",
1856     ];
1858     // Enumerate the available CPU extensions.
1859     let availableExts = [];
1860     for (let ext of CPU_EXTENSIONS) {
1861       if (getSysinfoProperty(ext, false)) {
1862         availableExts.push(ext);
1863       }
1864     }
1866     this._cpuData.extensions = availableExts;
1868     return this._cpuData;
1869   },
1871   _processData: null,
1872   /**
1873    * Get the process information.
1874    * @return Object containing the process information data.
1875    */
1876   _getProcessData() {
1877     if (this._processData) {
1878       return this._processData;
1879     }
1880     return {};
1881   },
1883   /**
1884    * Get the device information, if we are on a portable device.
1885    * @return Object containing the device information data, or null if
1886    * not a portable device.
1887    */
1888   _getDeviceData() {
1889     if (AppConstants.platform !== "android") {
1890       return null;
1891     }
1893     return {
1894       model: getSysinfoProperty("device", null),
1895       manufacturer: getSysinfoProperty("manufacturer", null),
1896       hardware: getSysinfoProperty("hardware", null),
1897       isTablet: getSysinfoProperty("tablet", null),
1898     };
1899   },
1901   _osData: null,
1902   /**
1903    * Get the OS information.
1904    * @return Object containing the OS data.
1905    */
1906   _getOSData() {
1907     if (this._osData) {
1908       return this._osData;
1909     }
1910     this._osData = {
1911       name: forceToStringOrNull(getSysinfoProperty("name", null)),
1912       version: forceToStringOrNull(getSysinfoProperty("version", null)),
1913       locale: forceToStringOrNull(getSystemLocale()),
1914     };
1916     if (AppConstants.platform == "android") {
1917       this._osData.kernelVersion = forceToStringOrNull(
1918         getSysinfoProperty("kernel_version", null)
1919       );
1920     } else if (AppConstants.platform === "win") {
1921       // The path to the "UBR" key, queried to get additional version details on Windows.
1922       const WINDOWS_UBR_KEY_PATH =
1923         "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
1925       let versionInfo = lazy.WindowsVersionInfo.get({ throwOnError: false });
1926       this._osData.servicePackMajor = versionInfo.servicePackMajor;
1927       this._osData.servicePackMinor = versionInfo.servicePackMinor;
1928       this._osData.windowsBuildNumber = versionInfo.buildNumber;
1929       // We only need the UBR if we're at or above Windows 10.
1930       if (
1931         typeof this._osData.version === "string" &&
1932         Services.vc.compare(this._osData.version, "10") >= 0
1933       ) {
1934         // Query the UBR key and only add it to the environment if it's available.
1935         // |readRegKey| doesn't throw, but rather returns 'undefined' on error.
1936         let ubr = lazy.WindowsRegistry.readRegKey(
1937           Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
1938           WINDOWS_UBR_KEY_PATH,
1939           "UBR",
1940           Ci.nsIWindowsRegKey.WOW64_64
1941         );
1942         this._osData.windowsUBR = ubr !== undefined ? ubr : null;
1943       }
1944     }
1946     return this._osData;
1947   },
1949   _hddData: null,
1950   /**
1951    * Get the HDD information.
1952    * @return Object containing the HDD data.
1953    */
1954   _getHDDData() {
1955     if (this._hddData) {
1956       return this._hddData;
1957     }
1958     let nullData = { model: null, revision: null, type: null };
1959     return { profile: nullData, binary: nullData, system: nullData };
1960   },
1962   /**
1963    * Get registered security product information.
1964    * @return Object containing the security product data
1965    */
1966   _getSecurityAppData() {
1967     const maxStringLength = 256;
1969     const keys = [
1970       ["registeredAntiVirus", "antivirus"],
1971       ["registeredAntiSpyware", "antispyware"],
1972       ["registeredFirewall", "firewall"],
1973     ];
1975     let result = {};
1977     for (let [inKey, outKey] of keys) {
1978       let prop = getSysinfoProperty(inKey, null);
1979       if (prop) {
1980         prop = limitStringToLength(prop, maxStringLength).split(";");
1981       }
1983       result[outKey] = prop;
1984     }
1986     return result;
1987   },
1989   /**
1990    * Get the GFX information.
1991    * @return Object containing the GFX data.
1992    */
1993   _getGFXData() {
1994     let gfxData = {
1995       D2DEnabled: getGfxField("D2DEnabled", null),
1996       DWriteEnabled: getGfxField("DWriteEnabled", null),
1997       ContentBackend: getGfxField("ContentBackend", null),
1998       Headless: getGfxField("isHeadless", null),
1999       EmbeddedInFirefoxReality: getGfxField("EmbeddedInFirefoxReality", null),
2000       TargetFrameRate: getGfxField("TargetFrameRate", null),
2001       // The following line is disabled due to main thread jank and will be enabled
2002       // again as part of bug 1154500.
2003       // DWriteVersion: getGfxField("DWriteVersion", null),
2004       adapters: [],
2005       monitors: [],
2006       features: {},
2007     };
2009     if (AppConstants.platform !== "android") {
2010       let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
2011       try {
2012         gfxData.monitors = gfxInfo.getMonitors();
2013       } catch (e) {
2014         this._log.error("nsIGfxInfo.getMonitors() caught error", e);
2015       }
2016     }
2018     try {
2019       let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
2020       gfxData.features = gfxInfo.getFeatures();
2021     } catch (e) {
2022       this._log.error("nsIGfxInfo.getFeatures() caught error", e);
2023     }
2025     // GfxInfo does not yet expose a way to iterate through all the adapters.
2026     gfxData.adapters.push(getGfxAdapter(""));
2027     gfxData.adapters[0].GPUActive = true;
2029     // If we have a second adapter add it to the gfxData.adapters section.
2030     let hasGPU2 = getGfxField("adapterDeviceID2", null) !== null;
2031     if (!hasGPU2) {
2032       this._log.trace("_getGFXData - Only one display adapter detected.");
2033       return gfxData;
2034     }
2036     this._log.trace("_getGFXData - Two display adapters detected.");
2038     gfxData.adapters.push(getGfxAdapter("2"));
2039     gfxData.adapters[1].GPUActive = getGfxField("isGPU2Active", null);
2041     return gfxData;
2042   },
2044   /**
2045    * Get the system data in object form.
2046    * @return Object containing the system data.
2047    */
2048   _getSystem() {
2049     let memoryMB = getSysinfoProperty("memsize", null);
2050     if (memoryMB) {
2051       // Send RAM size in megabytes. Rounding because sysinfo doesn't
2052       // always provide RAM in multiples of 1024.
2053       memoryMB = Math.round(memoryMB / 1024 / 1024);
2054     }
2056     let virtualMB = getSysinfoProperty("virtualmemsize", null);
2057     if (virtualMB) {
2058       // Send the total virtual memory size in megabytes. Rounding because
2059       // sysinfo doesn't always provide RAM in multiples of 1024.
2060       virtualMB = Math.round(virtualMB / 1024 / 1024);
2061     }
2063     let data = {
2064       memoryMB,
2065       virtualMaxMB: virtualMB,
2066       cpu: this._getCPUData(),
2067       os: this._getOSData(),
2068       hdd: this._getHDDData(),
2069       gfx: this._getGFXData(),
2070       appleModelId: getSysinfoProperty("appleModelId", null),
2071       hasWinPackageId: getSysinfoProperty("hasWinPackageId", null),
2072     };
2074     if (AppConstants.platform === "win") {
2075       // This is only sent for Mozilla produced MSIX packages
2076       let winPackageFamilyName = getSysinfoProperty("winPackageFamilyName", "");
2077       if (
2078         winPackageFamilyName.startsWith("Mozilla.") ||
2079         winPackageFamilyName.startsWith("MozillaCorporation.")
2080       ) {
2081         data = { winPackageFamilyName, ...data };
2082       }
2083       data = { ...this._getProcessData(), ...data };
2084     } else if (AppConstants.platform == "android") {
2085       data.device = this._getDeviceData();
2086     }
2088     // Windows 8+
2089     if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
2090       data.sec = this._getSecurityAppData();
2091     }
2093     return data;
2094   },
2096   _onEnvironmentChange(what, oldEnvironment) {
2097     this._log.trace("_onEnvironmentChange for " + what);
2099     // We are already skipping change events in _checkChanges if there is a pending change task running.
2100     if (this._shutdown) {
2101       this._log.trace("_onEnvironmentChange - Already shut down.");
2102       return;
2103     }
2105     if (ObjectUtils.deepEqual(this._currentEnvironment, oldEnvironment)) {
2106       this._log.trace("_onEnvironmentChange - Environment didn't change");
2107       return;
2108     }
2110     for (let [name, listener] of this._changeListeners) {
2111       try {
2112         this._log.debug("_onEnvironmentChange - calling " + name);
2113         listener(what, oldEnvironment);
2114       } catch (e) {
2115         this._log.error(
2116           "_onEnvironmentChange - listener " + name + " caught error",
2117           e
2118         );
2119       }
2120     }
2121   },
2123   reset() {
2124     this._shutdown = false;
2125   },