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/. */
6 * This file contains metrics data providers for the Firefox Health
7 * Report. Ideally each provider in this file exists in separate modules
8 * and lives close to the code it is querying. However, because of the
9 * overhead of JS compartments (which are created for each module), we
10 * currently have all the code in one file. When the overhead of
11 * compartments reaches a reasonable level, this file should be split
15 #ifndef MERGED_COMPARTMENT
19 this.EXPORTED_SYMBOLS = [
22 #ifdef MOZ_CRASHREPORTER
25 "HealthReportProvider",
33 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
35 Cu.import("resource://gre/modules/Metrics.jsm");
39 Cu.import("resource://gre/modules/Promise.jsm");
40 Cu.import("resource://gre/modules/osfile.jsm");
41 Cu.import("resource://gre/modules/Preferences.jsm");
42 Cu.import("resource://gre/modules/Services.jsm");
43 Cu.import("resource://gre/modules/Task.jsm");
44 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
45 Cu.import("resource://services-common/utils.js");
47 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
48 "resource://gre/modules/AddonManager.jsm");
49 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
50 "resource://gre/modules/UpdateChannel.jsm");
51 XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils",
52 "resource://gre/modules/PlacesDBUtils.jsm");
55 const LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_LAST_NUMERIC};
56 const LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_LAST_TEXT};
57 const DAILY_DISCRETE_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC};
58 const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
59 const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
60 const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
62 const TELEMETRY_PREF = "toolkit.telemetry.enabled";
64 function isTelemetryEnabled(prefs) {
65 return prefs.get(TELEMETRY_PREF, false);
69 * Represents basic application state.
71 * This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra
74 function AppInfoMeasurement() {
75 Metrics.Measurement.call(this);
78 AppInfoMeasurement.prototype = Object.freeze({
79 __proto__: Metrics.Measurement.prototype,
85 vendor: LAST_TEXT_FIELD,
86 name: LAST_TEXT_FIELD,
88 version: LAST_TEXT_FIELD,
89 appBuildID: LAST_TEXT_FIELD,
90 platformVersion: LAST_TEXT_FIELD,
91 platformBuildID: LAST_TEXT_FIELD,
93 xpcomabi: LAST_TEXT_FIELD,
94 updateChannel: LAST_TEXT_FIELD,
95 distributionID: LAST_TEXT_FIELD,
96 distributionVersion: LAST_TEXT_FIELD,
97 hotfixVersion: LAST_TEXT_FIELD,
98 locale: LAST_TEXT_FIELD,
99 isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
100 isTelemetryEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
101 isBlocklistEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
106 * Legacy version of app info before Telemetry was added.
108 * The "last" fields have all been removed. We only report the longitudinal
111 function AppInfoMeasurement1() {
112 Metrics.Measurement.call(this);
115 AppInfoMeasurement1.prototype = Object.freeze({
116 __proto__: Metrics.Measurement.prototype,
122 isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
127 function AppVersionMeasurement1() {
128 Metrics.Measurement.call(this);
131 AppVersionMeasurement1.prototype = Object.freeze({
132 __proto__: Metrics.Measurement.prototype,
138 version: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
142 // Version 2 added the build ID.
143 function AppVersionMeasurement2() {
144 Metrics.Measurement.call(this);
147 AppVersionMeasurement2.prototype = Object.freeze({
148 __proto__: Metrics.Measurement.prototype,
154 appVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
155 platformVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
156 appBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
157 platformBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
162 * Holds data on the application update functionality.
164 function AppUpdateMeasurement1() {
165 Metrics.Measurement.call(this);
168 AppUpdateMeasurement1.prototype = Object.freeze({
169 __proto__: Metrics.Measurement.prototype,
175 enabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
176 autoDownload: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
180 this.AppInfoProvider = function AppInfoProvider() {
181 Metrics.Provider.call(this);
183 this._prefs = new Preferences({defaultBranch: null});
185 AppInfoProvider.prototype = Object.freeze({
186 __proto__: Metrics.Provider.prototype,
188 name: "org.mozilla.appInfo",
193 AppUpdateMeasurement1,
194 AppVersionMeasurement1,
195 AppVersionMeasurement2,
201 // From nsIXULAppInfo.
206 appBuildID: "appBuildID",
207 platformVersion: "platformVersion",
208 platformBuildID: "platformBuildID",
210 // From nsIXULRuntime.
212 xpcomabi: "XPCOMABI",
215 postInit: function () {
216 return Task.spawn(this._postInit.bind(this));
219 _postInit: function () {
220 let recordEmptyAppInfo = function () {
221 this._setCurrentAppVersion("");
222 this._setCurrentPlatformVersion("");
223 this._setCurrentAppBuildID("");
224 return this._setCurrentPlatformBuildID("");
227 // Services.appInfo should always be defined for any reasonably behaving
228 // Gecko app. If it isn't, we insert a empty string sentinel value.
231 ai = Services.appinfo;
233 this._log.error("Could not obtain Services.appinfo: " +
234 CommonUtils.exceptionStr(ex));
235 yield recordEmptyAppInfo();
240 this._log.error("Services.appinfo is unavailable.");
241 yield recordEmptyAppInfo();
245 let currentAppVersion = ai.version;
246 let currentPlatformVersion = ai.platformVersion;
247 let currentAppBuildID = ai.appBuildID;
248 let currentPlatformBuildID = ai.platformBuildID;
250 // State's name doesn't contain "app" for historical compatibility.
251 let lastAppVersion = yield this.getState("lastVersion");
252 let lastPlatformVersion = yield this.getState("lastPlatformVersion");
253 let lastAppBuildID = yield this.getState("lastAppBuildID");
254 let lastPlatformBuildID = yield this.getState("lastPlatformBuildID");
256 if (currentAppVersion != lastAppVersion) {
257 yield this._setCurrentAppVersion(currentAppVersion);
260 if (currentPlatformVersion != lastPlatformVersion) {
261 yield this._setCurrentPlatformVersion(currentPlatformVersion);
264 if (currentAppBuildID != lastAppBuildID) {
265 yield this._setCurrentAppBuildID(currentAppBuildID);
268 if (currentPlatformBuildID != lastPlatformBuildID) {
269 yield this._setCurrentPlatformBuildID(currentPlatformBuildID);
273 _setCurrentAppVersion: function (version) {
274 this._log.info("Recording new application version: " + version);
275 let m = this.getMeasurement("versions", 2);
276 m.addDailyDiscreteText("appVersion", version);
278 // "app" not encoded in key for historical compatibility.
279 return this.setState("lastVersion", version);
282 _setCurrentPlatformVersion: function (version) {
283 this._log.info("Recording new platform version: " + version);
284 let m = this.getMeasurement("versions", 2);
285 m.addDailyDiscreteText("platformVersion", version);
286 return this.setState("lastPlatformVersion", version);
289 _setCurrentAppBuildID: function (build) {
290 this._log.info("Recording new application build ID: " + build);
291 let m = this.getMeasurement("versions", 2);
292 m.addDailyDiscreteText("appBuildID", build);
293 return this.setState("lastAppBuildID", build);
296 _setCurrentPlatformBuildID: function (build) {
297 this._log.info("Recording new platform build ID: " + build);
298 let m = this.getMeasurement("versions", 2);
299 m.addDailyDiscreteText("platformBuildID", build);
300 return this.setState("lastPlatformBuildID", build);
304 collectConstantData: function () {
305 return this.storage.enqueueTransaction(this._populateConstants.bind(this));
308 _populateConstants: function () {
309 let m = this.getMeasurement(AppInfoMeasurement.prototype.name,
310 AppInfoMeasurement.prototype.version);
314 ai = Services.appinfo;
316 this._log.warn("Could not obtain Services.appinfo: " +
317 CommonUtils.exceptionStr(ex));
322 this._log.warn("Services.appinfo is unavailable.");
326 for (let [k, v] in Iterator(this.appInfoFields)) {
328 yield m.setLastText(k, ai[v]);
330 this._log.warn("Error obtaining Services.appinfo." + v);
335 yield m.setLastText("updateChannel", UpdateChannel.get());
337 this._log.warn("Could not obtain update channel: " +
338 CommonUtils.exceptionStr(ex));
341 yield m.setLastText("distributionID", this._prefs.get("distribution.id", ""));
342 yield m.setLastText("distributionVersion", this._prefs.get("distribution.version", ""));
343 yield m.setLastText("hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
346 let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
347 .getService(Ci.nsIXULChromeRegistry)
348 .getSelectedLocale("global");
349 yield m.setLastText("locale", locale);
351 this._log.warn("Could not obtain application locale: " +
352 CommonUtils.exceptionStr(ex));
355 // FUTURE this should be retrieved periodically or at upload time.
356 yield this._recordIsTelemetryEnabled(m);
357 yield this._recordIsBlocklistEnabled(m);
358 yield this._recordDefaultBrowser(m);
361 _recordIsTelemetryEnabled: function (m) {
362 let enabled = isTelemetryEnabled(this._prefs);
363 this._log.debug("Recording telemetry enabled (" + TELEMETRY_PREF + "): " + enabled);
364 yield m.setDailyLastNumeric("isTelemetryEnabled", enabled ? 1 : 0);
367 _recordIsBlocklistEnabled: function (m) {
368 let enabled = this._prefs.get("extensions.blocklist.enabled", false);
369 this._log.debug("Recording blocklist enabled: " + enabled);
370 yield m.setDailyLastNumeric("isBlocklistEnabled", enabled ? 1 : 0);
373 _recordDefaultBrowser: function (m) {
376 shellService = Cc["@mozilla.org/browser/shell-service;1"]
377 .getService(Ci.nsIShellService);
379 this._log.warn("Could not obtain shell service: " +
380 CommonUtils.exceptionStr(ex));
387 // This uses the same set of flags used by the pref pane.
388 isDefault = shellService.isDefaultBrowser(false, true) ? 1 : 0;
390 this._log.warn("Could not determine if default browser: " +
391 CommonUtils.exceptionStr(ex));
395 return m.setDailyLastNumeric("isDefaultBrowser", isDefault);
398 collectDailyData: function () {
399 return this.storage.enqueueTransaction(function getDaily() {
400 let m = this.getMeasurement(AppUpdateMeasurement1.prototype.name,
401 AppUpdateMeasurement1.prototype.version);
403 let enabled = this._prefs.get("app.update.enabled", false);
404 yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0);
406 let auto = this._prefs.get("app.update.auto", false);
407 yield m.setDailyLastNumeric("autoDownload", auto ? 1 : 0);
413 function SysInfoMeasurement() {
414 Metrics.Measurement.call(this);
417 SysInfoMeasurement.prototype = Object.freeze({
418 __proto__: Metrics.Measurement.prototype,
424 cpuCount: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
425 memoryMB: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
426 manufacturer: LAST_TEXT_FIELD,
427 device: LAST_TEXT_FIELD,
428 hardware: LAST_TEXT_FIELD,
429 name: LAST_TEXT_FIELD,
430 version: LAST_TEXT_FIELD,
431 architecture: LAST_TEXT_FIELD,
432 isWow64: LAST_NUMERIC_FIELD,
437 this.SysInfoProvider = function SysInfoProvider() {
438 Metrics.Provider.call(this);
441 SysInfoProvider.prototype = Object.freeze({
442 __proto__: Metrics.Provider.prototype,
444 name: "org.mozilla.sysinfo",
446 measurementTypes: [SysInfoMeasurement],
451 cpucount: "cpuCount",
453 manufacturer: "manufacturer",
455 hardware: "hardware",
458 arch: "architecture",
462 collectConstantData: function () {
463 return this.storage.enqueueTransaction(this._populateConstants.bind(this));
466 _populateConstants: function () {
467 let m = this.getMeasurement(SysInfoMeasurement.prototype.name,
468 SysInfoMeasurement.prototype.version);
470 let si = Cc["@mozilla.org/system-info;1"]
471 .getService(Ci.nsIPropertyBag2);
473 for (let [k, v] in Iterator(this.sysInfoFields)) {
476 this._log.debug("Property not available: " + k);
480 let value = si.getProperty(k);
481 let method = "setLastText";
483 if (["cpucount", "memsize"].indexOf(k) != -1) {
484 let converted = parseInt(value, 10);
485 if (Number.isNaN(converted)) {
490 method = "setLastNumeric";
495 // Round memory to mebibytes.
496 value = Math.round(value / 1048576);
499 // Property is only present on Windows. hasKey() skipping from
500 // above ensures undefined or null doesn't creep in here.
501 value = value ? 1 : 0;
502 method = "setLastNumeric";
506 yield m[method](v, value);
508 this._log.warn("Error obtaining system info field: " + k + " " +
509 CommonUtils.exceptionStr(ex));
517 * Holds information about the current/active session.
519 * The fields within the current session are moved to daily session fields when
520 * the application is shut down.
522 * This measurement is backed by the SessionRecorder, not the database.
524 function CurrentSessionMeasurement() {
525 Metrics.Measurement.call(this);
528 CurrentSessionMeasurement.prototype = Object.freeze({
529 __proto__: Metrics.Measurement.prototype,
534 // Storage is in preferences.
538 * All data is stored in prefs, so we have a custom implementation.
540 getValues: function () {
541 let sessions = this.provider.healthReporter.sessionRecorder;
543 let fields = new Map();
544 let now = new Date();
545 fields.set("startDay", [now, Metrics.dateToDays(sessions.startDate)]);
546 fields.set("activeTicks", [now, sessions.activeTicks]);
547 fields.set("totalTime", [now, sessions.totalTime]);
548 fields.set("main", [now, sessions.main]);
549 fields.set("firstPaint", [now, sessions.firstPaint]);
550 fields.set("sessionRestored", [now, sessions.sessionRestored]);
552 return CommonUtils.laterTickResolvingPromise({
553 days: new Metrics.DailyValues(),
558 _serializeJSONSingular: function (data) {
559 let result = {"_v": this.version};
561 for (let [field, value] of data) {
562 result[field] = value[1];
570 * Records a history of all application sessions.
572 function PreviousSessionsMeasurement() {
573 Metrics.Measurement.call(this);
576 PreviousSessionsMeasurement.prototype = Object.freeze({
577 __proto__: Metrics.Measurement.prototype,
583 // Milliseconds of sessions that were properly shut down.
584 cleanActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
585 cleanTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
587 // Milliseconds of sessions that were not properly shut down.
588 abortedActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
589 abortedTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
591 // Startup times in milliseconds.
592 main: DAILY_DISCRETE_NUMERIC_FIELD,
593 firstPaint: DAILY_DISCRETE_NUMERIC_FIELD,
594 sessionRestored: DAILY_DISCRETE_NUMERIC_FIELD,
600 * Records information about the current browser session.
602 * A browser session is defined as an application/process lifetime. We
603 * start a new session when the application starts (essentially when
604 * this provider is instantiated) and end the session on shutdown.
606 * As the application runs, we record basic information about the
607 * "activity" of the session. Activity is defined by the presence of
608 * physical input into the browser (key press, mouse click, touch, etc).
610 * We differentiate between regular sessions and "aborted" sessions. An
611 * aborted session is one that does not end expectedly. This is often the
612 * result of a crash. We detect aborted sessions by storing the current
613 * session separate from completed sessions. We normally move the
614 * current session to completed sessions on application shutdown. If a
615 * current session is present on application startup, that means that
616 * the previous session was aborted.
618 this.SessionsProvider = function () {
619 Metrics.Provider.call(this);
622 SessionsProvider.prototype = Object.freeze({
623 __proto__: Metrics.Provider.prototype,
625 name: "org.mozilla.appSessions",
627 measurementTypes: [CurrentSessionMeasurement, PreviousSessionsMeasurement],
631 collectConstantData: function () {
632 let previous = this.getMeasurement("previous", 3);
634 return this.storage.enqueueTransaction(this._recordAndPruneSessions.bind(this));
637 _recordAndPruneSessions: function () {
638 this._log.info("Moving previous sessions from session recorder to storage.");
639 let recorder = this.healthReporter.sessionRecorder;
640 let sessions = recorder.getPreviousSessions();
641 this._log.debug("Found " + Object.keys(sessions).length + " previous sessions.");
643 let daily = this.getMeasurement("previous", 3);
645 // Please note the coupling here between the session recorder and our state.
646 // If the pruned index or the current index of the session recorder is ever
647 // deleted or reset to 0, our stored state of a later index would mean that
648 // new sessions would never be captured by this provider until the session
649 // recorder index catches up to our last session ID. This should not happen
650 // under normal circumstances, so we don't worry too much about it. We
651 // should, however, consider this as part of implementing bug 841561.
652 let lastRecordedSession = yield this.getState("lastSession");
653 if (lastRecordedSession === null) {
654 lastRecordedSession = -1;
656 this._log.debug("The last recorded session was #" + lastRecordedSession);
658 for (let [index, session] in Iterator(sessions)) {
659 if (index <= lastRecordedSession) {
660 this._log.warn("Already recorded session " + index + ". Did the last " +
661 "session crash or have an issue saving the prefs file?");
665 let type = session.clean ? "clean" : "aborted";
666 let date = session.startDate;
667 yield daily.addDailyDiscreteNumeric(type + "ActiveTicks", session.activeTicks, date);
668 yield daily.addDailyDiscreteNumeric(type + "TotalTime", session.totalTime, date);
670 for (let field of ["main", "firstPaint", "sessionRestored"]) {
671 yield daily.addDailyDiscreteNumeric(field, session[field], date);
674 lastRecordedSession = index;
677 yield this.setState("lastSession", "" + lastRecordedSession);
678 recorder.pruneOldSessions(new Date());
683 * Stores the set of active addons in storage.
685 * We do things a little differently than most other measurements. Because
686 * addons are difficult to shoehorn into distinct fields, we simply store a
687 * JSON blob in storage in a text field.
689 function ActiveAddonsMeasurement() {
690 Metrics.Measurement.call(this);
692 this._serializers = {};
693 this._serializers[this.SERIALIZE_JSON] = {
694 singular: this._serializeJSONSingular.bind(this),
695 // We don't need a daily serializer because we have none of this data.
699 ActiveAddonsMeasurement.prototype = Object.freeze({
700 __proto__: Metrics.Measurement.prototype,
706 addons: LAST_TEXT_FIELD,
709 _serializeJSONSingular: function (data) {
710 if (!data.has("addons")) {
711 this._log.warn("Don't have addons info. Weird.");
715 // Exceptions are caught in the caller.
716 let result = JSON.parse(data.get("addons")[1]);
717 result._v = this.version;
723 * Stores the set of active plugins in storage.
725 * This stores the data in a JSON blob in a text field similar to the
726 * ActiveAddonsMeasurement.
728 function ActivePluginsMeasurement() {
729 Metrics.Measurement.call(this);
731 this._serializers = {};
732 this._serializers[this.SERIALIZE_JSON] = {
733 singular: this._serializeJSONSingular.bind(this),
734 // We don't need a daily serializer because we have none of this data.
738 ActivePluginsMeasurement.prototype = Object.freeze({
739 __proto__: Metrics.Measurement.prototype,
745 plugins: LAST_TEXT_FIELD,
748 _serializeJSONSingular: function (data) {
749 if (!data.has("plugins")) {
750 this._log.warn("Don't have plugins info. Weird.");
754 // Exceptions are caught in the caller.
755 let result = JSON.parse(data.get("plugins")[1]);
756 result._v = this.version;
761 function ActiveGMPluginsMeasurement() {
762 Metrics.Measurement.call(this);
764 this._serializers = {};
765 this._serializers[this.SERIALIZE_JSON] = {
766 singular: this._serializeJSONSingular.bind(this),
770 ActiveGMPluginsMeasurement.prototype = Object.freeze({
771 __proto__: Metrics.Measurement.prototype,
777 "gm-plugins": LAST_TEXT_FIELD,
780 _serializeJSONSingular: function (data) {
781 if (!data.has("gm-plugins")) {
782 this._log.warn("Don't have GM plugins info. Weird.");
786 let result = JSON.parse(data.get("gm-plugins")[1]);
787 result._v = this.version;
792 function AddonCountsMeasurement() {
793 Metrics.Measurement.call(this);
796 AddonCountsMeasurement.prototype = Object.freeze({
797 __proto__: Metrics.Measurement.prototype,
803 theme: DAILY_LAST_NUMERIC_FIELD,
804 lwtheme: DAILY_LAST_NUMERIC_FIELD,
805 plugin: DAILY_LAST_NUMERIC_FIELD,
806 extension: DAILY_LAST_NUMERIC_FIELD,
807 service: DAILY_LAST_NUMERIC_FIELD,
813 * Legacy version of addons counts before services was added.
815 function AddonCountsMeasurement1() {
816 Metrics.Measurement.call(this);
819 AddonCountsMeasurement1.prototype = Object.freeze({
820 __proto__: Metrics.Measurement.prototype,
826 theme: DAILY_LAST_NUMERIC_FIELD,
827 lwtheme: DAILY_LAST_NUMERIC_FIELD,
828 plugin: DAILY_LAST_NUMERIC_FIELD,
829 extension: DAILY_LAST_NUMERIC_FIELD,
834 this.AddonsProvider = function () {
835 Metrics.Provider.call(this);
837 this._prefs = new Preferences({defaultBranch: null});
840 AddonsProvider.prototype = Object.freeze({
841 __proto__: Metrics.Provider.prototype,
843 // Whenever these AddonListener callbacks are called, we repopulate
844 // and store the set of addons. Note that these events will only fire
845 // for restartless add-ons. For actions that require a restart, we
846 // will catch the change after restart. The alternative is a lot of
847 // state tracking here, which isn't desirable.
848 ADDON_LISTENER_CALLBACKS: [
855 // Add-on types for which full details are uploaded in the
856 // ActiveAddonsMeasurement. All other types are ignored.
862 name: "org.mozilla.addons",
865 ActiveAddonsMeasurement,
866 ActivePluginsMeasurement,
867 ActiveGMPluginsMeasurement,
868 AddonCountsMeasurement1,
869 AddonCountsMeasurement,
872 postInit: function () {
875 for (let method of this.ADDON_LISTENER_CALLBACKS) {
876 listener[method] = this._collectAndStoreAddons.bind(this);
879 this._listener = listener;
880 AddonManager.addAddonListener(this._listener);
882 return CommonUtils.laterTickResolvingPromise();
885 onShutdown: function () {
886 AddonManager.removeAddonListener(this._listener);
887 this._listener = null;
889 return CommonUtils.laterTickResolvingPromise();
892 collectConstantData: function () {
893 return this._collectAndStoreAddons();
896 _collectAndStoreAddons: function () {
897 let deferred = Promise.defer();
899 AddonManager.getAllAddons(function onAllAddons(allAddons) {
905 data = this._createDataStructure(allAddons);
906 addonsField = JSON.stringify(data.addons);
907 pluginsField = JSON.stringify(data.plugins);
908 gmPluginsField = JSON.stringify(data.gmPlugins);
910 this._log.warn("Exception when populating add-ons data structure: " +
911 CommonUtils.exceptionStr(ex));
916 let now = new Date();
917 let addons = this.getMeasurement("addons", 2);
918 let plugins = this.getMeasurement("plugins", 1);
919 let gmPlugins = this.getMeasurement("gm-plugins", 1);
920 let counts = this.getMeasurement(AddonCountsMeasurement.prototype.name,
921 AddonCountsMeasurement.prototype.version);
923 this.enqueueStorageOperation(function storageAddons() {
924 for (let type in data.counts) {
926 counts.fieldID(type);
928 this._log.warn("Add-on type without field: " + type);
932 counts.setDailyLastNumeric(type, data.counts[type], now);
935 return addons.setLastText("addons", addonsField).then(
936 function onSuccess() {
937 return plugins.setLastText("plugins", pluginsField).then(
938 function onSuccess() {
939 return gmPlugins.setLastText("gm-plugins", gmPluginsField).then(
940 function onSuccess() {
943 function onError(error) {
944 deferred.reject(error);
947 function onError(error) { deferred.reject(error); }
950 function onError(error) { deferred.reject(error); }
955 return deferred.promise;
967 "hasBinaryComponents",
970 COPY_PLUGIN_FIELDS: [
979 _createDataStructure: function (addons) {
987 for (let addon of addons) {
988 let type = addon.type;
990 // We count plugins separately below.
991 if (addon.type == "plugin") {
992 if (addon.isGMPlugin) {
993 data.gmPlugins[addon.id] = {
994 version: addon.version,
995 userDisabled: addon.userDisabled,
996 applyBackgroundUpdates: addon.applyBackgroundUpdates,
1002 data.counts[type] = (data.counts[type] || 0) + 1;
1004 if (this.FULL_DETAIL_TYPES.indexOf(addon.type) == -1) {
1009 for (let field of this.COPY_ADDON_FIELDS) {
1010 obj[field] = addon[field];
1013 if (addon.installDate) {
1014 obj.installDay = this._dateToDays(addon.installDate);
1017 if (addon.updateDate) {
1018 obj.updateDay = this._dateToDays(addon.updateDate);
1021 data.addons[addon.id] = obj;
1024 let pluginTags = Cc["@mozilla.org/plugin/host;1"].
1025 getService(Ci.nsIPluginHost).
1028 for (let tag of pluginTags) {
1030 mimeTypes: tag.getMimeTypes({}),
1033 for (let field of this.COPY_PLUGIN_FIELDS) {
1034 obj[field] = tag[field];
1037 // Plugins need to have a filename and a name, so this can't be empty.
1038 let id = tag.filename + ":" + tag.name + ":" + tag.version + ":"
1040 data.plugins[id] = obj;
1043 data.counts["plugin"] = pluginTags.length;
1049 #ifdef MOZ_CRASHREPORTER
1051 function DailyCrashesMeasurement1() {
1052 Metrics.Measurement.call(this);
1055 DailyCrashesMeasurement1.prototype = Object.freeze({
1056 __proto__: Metrics.Measurement.prototype,
1062 pending: DAILY_COUNTER_FIELD,
1063 submitted: DAILY_COUNTER_FIELD,
1067 function DailyCrashesMeasurement2() {
1068 Metrics.Measurement.call(this);
1071 DailyCrashesMeasurement2.prototype = Object.freeze({
1072 __proto__: Metrics.Measurement.prototype,
1078 mainCrash: DAILY_LAST_NUMERIC_FIELD,
1082 function DailyCrashesMeasurement3() {
1083 Metrics.Measurement.call(this);
1086 DailyCrashesMeasurement3.prototype = Object.freeze({
1087 __proto__: Metrics.Measurement.prototype,
1093 "main-crash": DAILY_LAST_NUMERIC_FIELD,
1094 "main-hang": DAILY_LAST_NUMERIC_FIELD,
1095 "content-crash": DAILY_LAST_NUMERIC_FIELD,
1096 "content-hang": DAILY_LAST_NUMERIC_FIELD,
1097 "plugin-crash": DAILY_LAST_NUMERIC_FIELD,
1098 "plugin-hang": DAILY_LAST_NUMERIC_FIELD,
1102 function DailyCrashesMeasurement4() {
1103 Metrics.Measurement.call(this);
1106 DailyCrashesMeasurement4.prototype = Object.freeze({
1107 __proto__: Metrics.Measurement.prototype,
1113 "main-crash": DAILY_LAST_NUMERIC_FIELD,
1114 "main-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1115 "main-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1116 "main-hang": DAILY_LAST_NUMERIC_FIELD,
1117 "main-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1118 "main-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1119 "content-crash": DAILY_LAST_NUMERIC_FIELD,
1120 "content-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1121 "content-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1122 "content-hang": DAILY_LAST_NUMERIC_FIELD,
1123 "content-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1124 "content-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1125 "plugin-crash": DAILY_LAST_NUMERIC_FIELD,
1126 "plugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1127 "plugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1128 "plugin-hang": DAILY_LAST_NUMERIC_FIELD,
1129 "plugin-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1130 "plugin-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1134 function DailyCrashesMeasurement5() {
1135 Metrics.Measurement.call(this);
1138 DailyCrashesMeasurement5.prototype = Object.freeze({
1139 __proto__: Metrics.Measurement.prototype,
1145 "main-crash": DAILY_LAST_NUMERIC_FIELD,
1146 "main-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1147 "main-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1148 "main-hang": DAILY_LAST_NUMERIC_FIELD,
1149 "main-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1150 "main-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1151 "content-crash": DAILY_LAST_NUMERIC_FIELD,
1152 "content-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1153 "content-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1154 "content-hang": DAILY_LAST_NUMERIC_FIELD,
1155 "content-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1156 "content-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1157 "plugin-crash": DAILY_LAST_NUMERIC_FIELD,
1158 "plugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1159 "plugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1160 "plugin-hang": DAILY_LAST_NUMERIC_FIELD,
1161 "plugin-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1162 "plugin-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1163 "gmplugin-crash": DAILY_LAST_NUMERIC_FIELD,
1164 "gmplugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1165 "gmplugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1169 this.CrashesProvider = function () {
1170 Metrics.Provider.call(this);
1172 // So we can unit test.
1173 this._manager = Services.crashmanager;
1176 CrashesProvider.prototype = Object.freeze({
1177 __proto__: Metrics.Provider.prototype,
1179 name: "org.mozilla.crashes",
1182 DailyCrashesMeasurement1,
1183 DailyCrashesMeasurement2,
1184 DailyCrashesMeasurement3,
1185 DailyCrashesMeasurement4,
1186 DailyCrashesMeasurement5,
1191 collectDailyData: function () {
1192 return this.storage.enqueueTransaction(this._populateCrashCounts.bind(this));
1195 _populateCrashCounts: function () {
1196 this._log.info("Grabbing crash counts from crash manager.");
1197 let crashCounts = yield this._manager.getCrashCountsByDay();
1199 // TODO: CrashManager no longer stores submissions as crashes, but we still
1200 // want to send the submission data to FHR. As a temporary workaround, we
1201 // populate |crashCounts| with the submission data to match past behaviour.
1203 let crashes = yield this._manager.getCrashes();
1204 for (let crash of crashes) {
1205 for (let [submissionID, submission] of crash.submissions) {
1206 if (!submission.responseDate) {
1210 let day = Metrics.dateToDays(submission.responseDate);
1211 if (!crashCounts.has(day)) {
1212 crashCounts.set(day, new Map());
1216 submission.result == this._manager.SUBMISSION_RESULT_OK;
1217 let type = crash.type + "-submission-" + (succeeded ? "succeeded" :
1220 let count = (crashCounts.get(day).get(type) || 0) + 1;
1221 crashCounts.get(day).set(type, count);
1225 let m = this.getMeasurement("crashes", 5);
1226 let fields = DailyCrashesMeasurement5.prototype.fields;
1228 for (let [day, types] of crashCounts) {
1229 let date = Metrics.daysToDate(day);
1230 for (let [type, count] of types) {
1231 if (!(type in fields)) {
1232 this._log.warn("Unknown crash type encountered: " + type);
1236 yield m.setDailyLastNumeric(type, count, date);
1245 * Records data from update hotfixes.
1247 * This measurement has dynamic fields. Field names are of the form
1248 * <version>.<thing> where <version> is the hotfix version that produced
1249 * the data. e.g. "v20140527". The sub-version of the hotfix is omitted
1250 * because hotfixes can go through multiple minor versions during development
1251 * and we don't want to introduce more fields than necessary. Furthermore,
1252 * the subsequent dots make parsing field names slightly harder. By stripping,
1253 * we can just split on the first dot.
1255 function UpdateHotfixMeasurement1() {
1256 Metrics.Measurement.call(this);
1259 UpdateHotfixMeasurement1.prototype = Object.freeze({
1260 __proto__: Metrics.Measurement.prototype,
1266 "upgradedFrom": Metrics.Storage.FIELD_LAST_TEXT,
1267 "uninstallReason": Metrics.Storage.FIELD_LAST_TEXT,
1268 "downloadAttempts": Metrics.Storage.FIELD_LAST_NUMERIC,
1269 "downloadFailures": Metrics.Storage.FIELD_LAST_NUMERIC,
1270 "installAttempts": Metrics.Storage.FIELD_LAST_NUMERIC,
1271 "installFailures": Metrics.Storage.FIELD_LAST_NUMERIC,
1272 "notificationsShown": Metrics.Storage.FIELD_LAST_NUMERIC,
1277 // Our fields have dynamic names from the hotfix version that supplied them.
1278 // We need to override the default behavior to deal with unknown fields.
1279 shouldIncludeField: function (name) {
1280 return name.contains(".");
1283 fieldType: function (name) {
1284 for (let known in this.hotfixFieldTypes) {
1285 if (name.endsWith(known)) {
1286 return this.hotfixFieldTypes[known];
1290 return Metrics.Measurement.prototype.fieldType.call(this, name);
1294 this.HotfixProvider = function () {
1295 Metrics.Provider.call(this);
1298 HotfixProvider.prototype = Object.freeze({
1299 __proto__: Metrics.Provider.prototype,
1301 name: "org.mozilla.hotfix",
1303 UpdateHotfixMeasurement1,
1308 collectDailyData: function () {
1309 return this.storage.enqueueTransaction(this._populateHotfixData.bind(this));
1312 _populateHotfixData: function* () {
1313 let m = this.getMeasurement("update", 1);
1315 // The update hotfix retains its JSON state file after uninstall.
1316 // The initial update hotfix had a hard-coded filename. We treat it
1317 // specially. Subsequent update hotfixes named their files in a
1318 // recognizeable pattern so we don't need to update this probe code to
1321 ["v20140527", OS.Path.join(OS.Constants.Path.profileDir,
1322 "hotfix.v20140527.01.json")],
1325 let it = new OS.File.DirectoryIterator(OS.Constants.Path.profileDir);
1327 yield it.forEach((e, index, it) => {
1328 let m = e.name.match(/^updateHotfix\.([a-zA-Z0-9]+)\.json$/);
1330 files.push([m[1], e.path]);
1337 let decoder = new TextDecoder();
1338 for (let e of files) {
1339 let [version, path] = e;
1342 let data = yield OS.File.read(path);
1343 p = JSON.parse(decoder.decode(data));
1344 } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
1347 this._log.warn("Error loading update hotfix payload: " + ex.message);
1350 // Wrap just in case.
1352 for (let k in m.hotfixFieldTypes) {
1358 if (value === null && k == "uninstallReason") {
1359 value = "STILL_INSTALLED";
1362 let field = version + "." + k;
1365 switch (typeof(value)) {
1367 fieldType = this.storage.FIELD_LAST_TEXT;
1368 storageOp = "setLastTextFromFieldID";
1371 fieldType = this.storage.FIELD_LAST_NUMERIC;
1372 storageOp = "setLastNumericFromFieldID";
1375 this._log.warn("Unknown value in hotfix state: " + k + "=" + value);
1379 if (this.storage.hasFieldFromMeasurement(m.id, field, fieldType)) {
1380 let fieldID = this.storage.fieldIDFromMeasurement(m.id, field);
1381 yield this.storage[storageOp](fieldID, value);
1383 let fieldID = yield this.storage.registerField(m.id, field,
1385 yield this.storage[storageOp](fieldID, value);
1390 this._log.warn("Error processing update hotfix data: " + ex);
1397 * Holds basic statistics about the Places database.
1399 function PlacesMeasurement() {
1400 Metrics.Measurement.call(this);
1403 PlacesMeasurement.prototype = Object.freeze({
1404 __proto__: Metrics.Measurement.prototype,
1410 pages: DAILY_LAST_NUMERIC_FIELD,
1411 bookmarks: DAILY_LAST_NUMERIC_FIELD,
1417 * Collects information about Places.
1419 this.PlacesProvider = function () {
1420 Metrics.Provider.call(this);
1423 PlacesProvider.prototype = Object.freeze({
1424 __proto__: Metrics.Provider.prototype,
1426 name: "org.mozilla.places",
1428 measurementTypes: [PlacesMeasurement],
1430 collectDailyData: function () {
1431 return this.storage.enqueueTransaction(this._collectData.bind(this));
1434 _collectData: function () {
1435 let now = new Date();
1436 let data = yield this._getDailyValues();
1438 let m = this.getMeasurement("places", 1);
1440 yield m.setDailyLastNumeric("pages", data.PLACES_PAGES_COUNT);
1441 yield m.setDailyLastNumeric("bookmarks", data.PLACES_BOOKMARKS_COUNT);
1444 _getDailyValues: function () {
1445 let deferred = Promise.defer();
1447 PlacesDBUtils.telemetry(null, function onResult(data) {
1448 deferred.resolve(data);
1451 return deferred.promise;
1455 function SearchCountMeasurement1() {
1456 Metrics.Measurement.call(this);
1459 SearchCountMeasurement1.prototype = Object.freeze({
1460 __proto__: Metrics.Measurement.prototype,
1465 // We only record searches for search engines that have partner agreements
1468 "amazon.com.abouthome": DAILY_COUNTER_FIELD,
1469 "amazon.com.contextmenu": DAILY_COUNTER_FIELD,
1470 "amazon.com.searchbar": DAILY_COUNTER_FIELD,
1471 "amazon.com.urlbar": DAILY_COUNTER_FIELD,
1472 "bing.abouthome": DAILY_COUNTER_FIELD,
1473 "bing.contextmenu": DAILY_COUNTER_FIELD,
1474 "bing.searchbar": DAILY_COUNTER_FIELD,
1475 "bing.urlbar": DAILY_COUNTER_FIELD,
1476 "google.abouthome": DAILY_COUNTER_FIELD,
1477 "google.contextmenu": DAILY_COUNTER_FIELD,
1478 "google.searchbar": DAILY_COUNTER_FIELD,
1479 "google.urlbar": DAILY_COUNTER_FIELD,
1480 "yahoo.abouthome": DAILY_COUNTER_FIELD,
1481 "yahoo.contextmenu": DAILY_COUNTER_FIELD,
1482 "yahoo.searchbar": DAILY_COUNTER_FIELD,
1483 "yahoo.urlbar": DAILY_COUNTER_FIELD,
1484 "other.abouthome": DAILY_COUNTER_FIELD,
1485 "other.contextmenu": DAILY_COUNTER_FIELD,
1486 "other.searchbar": DAILY_COUNTER_FIELD,
1487 "other.urlbar": DAILY_COUNTER_FIELD,
1492 * Records search counts per day per engine and where search initiated.
1494 * We want to record granular details for individual locale-specific search
1495 * providers, but only if they're Mozilla partners. In order to do this, we
1496 * track the nsISearchEngine identifier, which denotes shipped search engines,
1497 * and intersect those with our partner list.
1499 * We don't use the search engine name directly, because it is shared across
1500 * locales; e.g., eBay-de and eBay both share the name "eBay".
1502 function SearchCountMeasurementBase() {
1503 this._fieldSpecs = {};
1504 Metrics.Measurement.call(this);
1507 SearchCountMeasurementBase.prototype = Object.freeze({
1508 __proto__: Metrics.Measurement.prototype,
1511 // Our fields are dynamic.
1513 return this._fieldSpecs;
1517 * Override the default behavior: serializers should include every counter
1518 * field from the DB, even if we don't currently have it registered.
1520 * Do this so we don't have to register several hundred fields to match
1521 * various Firefox locales.
1523 * We use the "provider.type" syntax as a rudimentary check for validity.
1525 * We trust that measurement versioning is sufficient to exclude old provider
1528 shouldIncludeField: function (name) {
1529 return name.contains(".");
1533 * The measurement type mechanism doesn't introspect the DB. Override it
1534 * so that we can assume all unknown fields are counters.
1536 fieldType: function (name) {
1537 if (name in this.fields) {
1538 return this.fields[name].type;
1541 // Default to a counter.
1542 return Metrics.Storage.FIELD_DAILY_COUNTER;
1554 function SearchCountMeasurement2() {
1555 SearchCountMeasurementBase.call(this);
1558 SearchCountMeasurement2.prototype = Object.freeze({
1559 __proto__: SearchCountMeasurementBase.prototype,
1564 function SearchCountMeasurement3() {
1565 SearchCountMeasurementBase.call(this);
1568 SearchCountMeasurement3.prototype = Object.freeze({
1569 __proto__: SearchCountMeasurementBase.prototype,
1573 getEngines: function () {
1574 return Services.search.getEngines();
1577 getEngineID: function (engine) {
1581 if (engine.identifier) {
1582 return engine.identifier;
1584 return "other-" + engine.name;
1588 function SearchEnginesMeasurement1() {
1589 Metrics.Measurement.call(this);
1592 SearchEnginesMeasurement1.prototype = Object.freeze({
1593 __proto__: Metrics.Measurement.prototype,
1599 default: DAILY_LAST_TEXT_FIELD,
1603 this.SearchesProvider = function () {
1604 Metrics.Provider.call(this);
1606 this._prefs = new Preferences({defaultBranch: null});
1609 this.SearchesProvider.prototype = Object.freeze({
1610 __proto__: Metrics.Provider.prototype,
1612 name: "org.mozilla.searches",
1614 SearchCountMeasurement1,
1615 SearchCountMeasurement2,
1616 SearchCountMeasurement3,
1617 SearchEnginesMeasurement1,
1621 * Initialize the search service before our measurements are touched.
1623 preInit: function (storage) {
1624 // Initialize search service.
1625 let deferred = Promise.defer();
1626 Services.search.init(function onInitComplete () {
1629 return deferred.promise;
1632 collectDailyData: function () {
1633 return this.storage.enqueueTransaction(function getDaily() {
1634 // We currently only record this if Telemetry is enabled.
1635 if (!isTelemetryEnabled(this._prefs)) {
1639 let m = this.getMeasurement(SearchEnginesMeasurement1.prototype.name,
1640 SearchEnginesMeasurement1.prototype.version);
1644 engine = Services.search.defaultEngine;
1650 } else if (engine.identifier) {
1651 name = engine.identifier;
1652 } else if (engine.name) {
1653 name = "other-" + engine.name;
1658 yield m.setDailyLastText("default", name);
1663 * Record that a search occurred.
1666 * (nsISearchEngine) The search engine used.
1668 * (string) Where the search was initiated from. Must be one of the
1669 * SearchCountMeasurement2.SOURCES values.
1672 * The promise is resolved when the storage operation completes.
1674 recordSearch: function (engine, source) {
1675 let m = this.getMeasurement("counts", 3);
1677 if (m.SOURCES.indexOf(source) == -1) {
1678 throw new Error("Unknown source for search: " + source);
1681 let field = m.getEngineID(engine) + "." + source;
1682 if (this.storage.hasFieldFromMeasurement(m.id, field,
1683 this.storage.FIELD_DAILY_COUNTER)) {
1684 let fieldID = this.storage.fieldIDFromMeasurement(m.id, field);
1685 return this.enqueueStorageOperation(function recordSearchKnownField() {
1686 return this.storage.incrementDailyCounterFromFieldID(fieldID);
1690 // Otherwise, we first need to create the field.
1691 return this.enqueueStorageOperation(function recordFieldAndSearch() {
1692 // This function has to return a promise.
1693 return Task.spawn(function () {
1694 let fieldID = yield this.storage.registerField(m.id, field,
1695 this.storage.FIELD_DAILY_COUNTER);
1696 yield this.storage.incrementDailyCounterFromFieldID(fieldID);
1702 function HealthReportSubmissionMeasurement1() {
1703 Metrics.Measurement.call(this);
1706 HealthReportSubmissionMeasurement1.prototype = Object.freeze({
1707 __proto__: Metrics.Measurement.prototype,
1709 name: "submissions",
1713 firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
1714 continuationUploadAttempt: DAILY_COUNTER_FIELD,
1715 uploadSuccess: DAILY_COUNTER_FIELD,
1716 uploadTransportFailure: DAILY_COUNTER_FIELD,
1717 uploadServerFailure: DAILY_COUNTER_FIELD,
1718 uploadClientFailure: DAILY_COUNTER_FIELD,
1722 function HealthReportSubmissionMeasurement2() {
1723 Metrics.Measurement.call(this);
1726 HealthReportSubmissionMeasurement2.prototype = Object.freeze({
1727 __proto__: Metrics.Measurement.prototype,
1729 name: "submissions",
1733 firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
1734 continuationUploadAttempt: DAILY_COUNTER_FIELD,
1735 uploadSuccess: DAILY_COUNTER_FIELD,
1736 uploadTransportFailure: DAILY_COUNTER_FIELD,
1737 uploadServerFailure: DAILY_COUNTER_FIELD,
1738 uploadClientFailure: DAILY_COUNTER_FIELD,
1739 uploadAlreadyInProgress: DAILY_COUNTER_FIELD,
1743 this.HealthReportProvider = function () {
1744 Metrics.Provider.call(this);
1747 HealthReportProvider.prototype = Object.freeze({
1748 __proto__: Metrics.Provider.prototype,
1750 name: "org.mozilla.healthreport",
1753 HealthReportSubmissionMeasurement1,
1754 HealthReportSubmissionMeasurement2,
1757 recordEvent: function (event, date=new Date()) {
1758 let m = this.getMeasurement("submissions", 2);
1759 return this.enqueueStorageOperation(function recordCounter() {
1760 return m.incrementDailyCounter(event, date);