services/healthreport/providers.jsm

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

     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 /**
     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
    12  * up.
    13  */
    15 "use strict";
    17 #ifndef MERGED_COMPARTMENT
    19 this.EXPORTED_SYMBOLS = [
    20   "AddonsProvider",
    21   "AppInfoProvider",
    22 #ifdef MOZ_CRASHREPORTER
    23   "CrashesProvider",
    24 #endif
    25   "HealthReportProvider",
    26   "PlacesProvider",
    27   "SearchesProvider",
    28   "SessionsProvider",
    29   "SysInfoProvider",
    30 ];
    32 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    34 Cu.import("resource://gre/modules/Metrics.jsm");
    36 #endif
    38 Cu.import("resource://gre/modules/Promise.jsm");
    39 Cu.import("resource://gre/modules/osfile.jsm");
    40 Cu.import("resource://gre/modules/Preferences.jsm");
    41 Cu.import("resource://gre/modules/Services.jsm");
    42 Cu.import("resource://gre/modules/Task.jsm");
    43 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    44 Cu.import("resource://services-common/utils.js");
    46 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
    47                                   "resource://gre/modules/AddonManager.jsm");
    48 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
    49                                   "resource://gre/modules/UpdateChannel.jsm");
    50 XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils",
    51                                   "resource://gre/modules/PlacesDBUtils.jsm");
    54 const LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_LAST_NUMERIC};
    55 const LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_LAST_TEXT};
    56 const DAILY_DISCRETE_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC};
    57 const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
    58 const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
    59 const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
    61 const TELEMETRY_PREF = "toolkit.telemetry.enabled";
    63 function isTelemetryEnabled(prefs) {
    64   return prefs.get(TELEMETRY_PREF, false);
    65 }
    67 /**
    68  * Represents basic application state.
    69  *
    70  * This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra
    71  * pieces thrown in.
    72  */
    73 function AppInfoMeasurement() {
    74   Metrics.Measurement.call(this);
    75 }
    77 AppInfoMeasurement.prototype = Object.freeze({
    78   __proto__: Metrics.Measurement.prototype,
    80   name: "appinfo",
    81   version: 2,
    83   fields: {
    84     vendor: LAST_TEXT_FIELD,
    85     name: LAST_TEXT_FIELD,
    86     id: LAST_TEXT_FIELD,
    87     version: LAST_TEXT_FIELD,
    88     appBuildID: LAST_TEXT_FIELD,
    89     platformVersion: LAST_TEXT_FIELD,
    90     platformBuildID: LAST_TEXT_FIELD,
    91     os: LAST_TEXT_FIELD,
    92     xpcomabi: LAST_TEXT_FIELD,
    93     updateChannel: LAST_TEXT_FIELD,
    94     distributionID: LAST_TEXT_FIELD,
    95     distributionVersion: LAST_TEXT_FIELD,
    96     hotfixVersion: LAST_TEXT_FIELD,
    97     locale: LAST_TEXT_FIELD,
    98     isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
    99     isTelemetryEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   100     isBlocklistEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   101   },
   102 });
   104 /**
   105  * Legacy version of app info before Telemetry was added.
   106  *
   107  * The "last" fields have all been removed. We only report the longitudinal
   108  * field.
   109  */
   110 function AppInfoMeasurement1() {
   111   Metrics.Measurement.call(this);
   112 }
   114 AppInfoMeasurement1.prototype = Object.freeze({
   115   __proto__: Metrics.Measurement.prototype,
   117   name: "appinfo",
   118   version: 1,
   120   fields: {
   121     isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   122   },
   123 });
   126 function AppVersionMeasurement1() {
   127   Metrics.Measurement.call(this);
   128 }
   130 AppVersionMeasurement1.prototype = Object.freeze({
   131   __proto__: Metrics.Measurement.prototype,
   133   name: "versions",
   134   version: 1,
   136   fields: {
   137     version: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
   138   },
   139 });
   141 // Version 2 added the build ID.
   142 function AppVersionMeasurement2() {
   143   Metrics.Measurement.call(this);
   144 }
   146 AppVersionMeasurement2.prototype = Object.freeze({
   147   __proto__: Metrics.Measurement.prototype,
   149   name: "versions",
   150   version: 2,
   152   fields: {
   153     appVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
   154     platformVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
   155     appBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
   156     platformBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
   157   },
   158 });
   160 /**
   161  * Holds data on the application update functionality.
   162  */
   163 function AppUpdateMeasurement1() {
   164   Metrics.Measurement.call(this);
   165 }
   167 AppUpdateMeasurement1.prototype = Object.freeze({
   168   __proto__: Metrics.Measurement.prototype,
   170   name: "update",
   171   version: 1,
   173   fields: {
   174     enabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   175     autoDownload: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   176   },
   177 });
   179 this.AppInfoProvider = function AppInfoProvider() {
   180   Metrics.Provider.call(this);
   182   this._prefs = new Preferences({defaultBranch: null});
   183 }
   184 AppInfoProvider.prototype = Object.freeze({
   185   __proto__: Metrics.Provider.prototype,
   187   name: "org.mozilla.appInfo",
   189   measurementTypes: [
   190     AppInfoMeasurement,
   191     AppInfoMeasurement1,
   192     AppUpdateMeasurement1,
   193     AppVersionMeasurement1,
   194     AppVersionMeasurement2,
   195   ],
   197   pullOnly: true,
   199   appInfoFields: {
   200     // From nsIXULAppInfo.
   201     vendor: "vendor",
   202     name: "name",
   203     id: "ID",
   204     version: "version",
   205     appBuildID: "appBuildID",
   206     platformVersion: "platformVersion",
   207     platformBuildID: "platformBuildID",
   209     // From nsIXULRuntime.
   210     os: "OS",
   211     xpcomabi: "XPCOMABI",
   212   },
   214   postInit: function () {
   215     return Task.spawn(this._postInit.bind(this));
   216   },
   218   _postInit: function () {
   219     let recordEmptyAppInfo = function () {
   220       this._setCurrentAppVersion("");
   221       this._setCurrentPlatformVersion("");
   222       this._setCurrentAppBuildID("");
   223       return this._setCurrentPlatformBuildID("");
   224     }.bind(this);
   226     // Services.appInfo should always be defined for any reasonably behaving
   227     // Gecko app. If it isn't, we insert a empty string sentinel value.
   228     let ai;
   229     try {
   230       ai = Services.appinfo;
   231     } catch (ex) {
   232       this._log.error("Could not obtain Services.appinfo: " +
   233                      CommonUtils.exceptionStr(ex));
   234       yield recordEmptyAppInfo();
   235       return;
   236     }
   238     if (!ai) {
   239       this._log.error("Services.appinfo is unavailable.");
   240       yield recordEmptyAppInfo();
   241       return;
   242     }
   244     let currentAppVersion = ai.version;
   245     let currentPlatformVersion = ai.platformVersion;
   246     let currentAppBuildID = ai.appBuildID;
   247     let currentPlatformBuildID = ai.platformBuildID;
   249     // State's name doesn't contain "app" for historical compatibility.
   250     let lastAppVersion = yield this.getState("lastVersion");
   251     let lastPlatformVersion = yield this.getState("lastPlatformVersion");
   252     let lastAppBuildID = yield this.getState("lastAppBuildID");
   253     let lastPlatformBuildID = yield this.getState("lastPlatformBuildID");
   255     if (currentAppVersion != lastAppVersion) {
   256       yield this._setCurrentAppVersion(currentAppVersion);
   257     }
   259     if (currentPlatformVersion != lastPlatformVersion) {
   260       yield this._setCurrentPlatformVersion(currentPlatformVersion);
   261     }
   263     if (currentAppBuildID != lastAppBuildID) {
   264       yield this._setCurrentAppBuildID(currentAppBuildID);
   265     }
   267     if (currentPlatformBuildID != lastPlatformBuildID) {
   268       yield this._setCurrentPlatformBuildID(currentPlatformBuildID);
   269     }
   270   },
   272   _setCurrentAppVersion: function (version) {
   273     this._log.info("Recording new application version: " + version);
   274     let m = this.getMeasurement("versions", 2);
   275     m.addDailyDiscreteText("appVersion", version);
   277     // "app" not encoded in key for historical compatibility.
   278     return this.setState("lastVersion", version);
   279   },
   281   _setCurrentPlatformVersion: function (version) {
   282     this._log.info("Recording new platform version: " + version);
   283     let m = this.getMeasurement("versions", 2);
   284     m.addDailyDiscreteText("platformVersion", version);
   285     return this.setState("lastPlatformVersion", version);
   286   },
   288   _setCurrentAppBuildID: function (build) {
   289     this._log.info("Recording new application build ID: " + build);
   290     let m = this.getMeasurement("versions", 2);
   291     m.addDailyDiscreteText("appBuildID", build);
   292     return this.setState("lastAppBuildID", build);
   293   },
   295   _setCurrentPlatformBuildID: function (build) {
   296     this._log.info("Recording new platform build ID: " + build);
   297     let m = this.getMeasurement("versions", 2);
   298     m.addDailyDiscreteText("platformBuildID", build);
   299     return this.setState("lastPlatformBuildID", build);
   300   },
   303   collectConstantData: function () {
   304     return this.storage.enqueueTransaction(this._populateConstants.bind(this));
   305   },
   307   _populateConstants: function () {
   308     let m = this.getMeasurement(AppInfoMeasurement.prototype.name,
   309                                 AppInfoMeasurement.prototype.version);
   311     let ai;
   312     try {
   313       ai = Services.appinfo;
   314     } catch (ex) {
   315       this._log.warn("Could not obtain Services.appinfo: " +
   316                      CommonUtils.exceptionStr(ex));
   317       throw ex;
   318     }
   320     if (!ai) {
   321       this._log.warn("Services.appinfo is unavailable.");
   322       throw ex;
   323     }
   325     for (let [k, v] in Iterator(this.appInfoFields)) {
   326       try {
   327         yield m.setLastText(k, ai[v]);
   328       } catch (ex) {
   329         this._log.warn("Error obtaining Services.appinfo." + v);
   330       }
   331     }
   333     try {
   334       yield m.setLastText("updateChannel", UpdateChannel.get());
   335     } catch (ex) {
   336       this._log.warn("Could not obtain update channel: " +
   337                      CommonUtils.exceptionStr(ex));
   338     }
   340     yield m.setLastText("distributionID", this._prefs.get("distribution.id", ""));
   341     yield m.setLastText("distributionVersion", this._prefs.get("distribution.version", ""));
   342     yield m.setLastText("hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
   344     try {
   345       let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
   346                      .getService(Ci.nsIXULChromeRegistry)
   347                      .getSelectedLocale("global");
   348       yield m.setLastText("locale", locale);
   349     } catch (ex) {
   350       this._log.warn("Could not obtain application locale: " +
   351                      CommonUtils.exceptionStr(ex));
   352     }
   354     // FUTURE this should be retrieved periodically or at upload time.
   355     yield this._recordIsTelemetryEnabled(m);
   356     yield this._recordIsBlocklistEnabled(m);
   357     yield this._recordDefaultBrowser(m);
   358   },
   360   _recordIsTelemetryEnabled: function (m) {
   361     let enabled = isTelemetryEnabled(this._prefs);
   362     this._log.debug("Recording telemetry enabled (" + TELEMETRY_PREF + "): " + enabled);
   363     yield m.setDailyLastNumeric("isTelemetryEnabled", enabled ? 1 : 0);
   364   },
   366   _recordIsBlocklistEnabled: function (m) {
   367     let enabled = this._prefs.get("extensions.blocklist.enabled", false);
   368     this._log.debug("Recording blocklist enabled: " + enabled);
   369     yield m.setDailyLastNumeric("isBlocklistEnabled", enabled ? 1 : 0);
   370   },
   372   _recordDefaultBrowser: function (m) {
   373     let shellService;
   374     try {
   375       shellService = Cc["@mozilla.org/browser/shell-service;1"]
   376                        .getService(Ci.nsIShellService);
   377     } catch (ex) {
   378       this._log.warn("Could not obtain shell service: " +
   379                      CommonUtils.exceptionStr(ex));
   380     }
   382     let isDefault = -1;
   384     if (shellService) {
   385       try {
   386         // This uses the same set of flags used by the pref pane.
   387         isDefault = shellService.isDefaultBrowser(false, true) ? 1 : 0;
   388       } catch (ex) {
   389         this._log.warn("Could not determine if default browser: " +
   390                        CommonUtils.exceptionStr(ex));
   391       }
   392     }
   394     return m.setDailyLastNumeric("isDefaultBrowser", isDefault);
   395   },
   397   collectDailyData: function () {
   398     return this.storage.enqueueTransaction(function getDaily() {
   399       let m = this.getMeasurement(AppUpdateMeasurement1.prototype.name,
   400                                   AppUpdateMeasurement1.prototype.version);
   402       let enabled = this._prefs.get("app.update.enabled", false);
   403       yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0);
   405       let auto = this._prefs.get("app.update.auto", false);
   406       yield m.setDailyLastNumeric("autoDownload", auto ? 1 : 0);
   407     }.bind(this));
   408   },
   409 });
   412 function SysInfoMeasurement() {
   413   Metrics.Measurement.call(this);
   414 }
   416 SysInfoMeasurement.prototype = Object.freeze({
   417   __proto__: Metrics.Measurement.prototype,
   419   name: "sysinfo",
   420   version: 2,
   422   fields: {
   423     cpuCount: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
   424     memoryMB: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
   425     manufacturer: LAST_TEXT_FIELD,
   426     device: LAST_TEXT_FIELD,
   427     hardware: LAST_TEXT_FIELD,
   428     name: LAST_TEXT_FIELD,
   429     version: LAST_TEXT_FIELD,
   430     architecture: LAST_TEXT_FIELD,
   431     isWow64: LAST_NUMERIC_FIELD,
   432   },
   433 });
   436 this.SysInfoProvider = function SysInfoProvider() {
   437   Metrics.Provider.call(this);
   438 };
   440 SysInfoProvider.prototype = Object.freeze({
   441   __proto__: Metrics.Provider.prototype,
   443   name: "org.mozilla.sysinfo",
   445   measurementTypes: [SysInfoMeasurement],
   447   pullOnly: true,
   449   sysInfoFields: {
   450     cpucount: "cpuCount",
   451     memsize: "memoryMB",
   452     manufacturer: "manufacturer",
   453     device: "device",
   454     hardware: "hardware",
   455     name: "name",
   456     version: "version",
   457     arch: "architecture",
   458     isWow64: "isWow64",
   459   },
   461   collectConstantData: function () {
   462     return this.storage.enqueueTransaction(this._populateConstants.bind(this));
   463   },
   465   _populateConstants: function () {
   466     let m = this.getMeasurement(SysInfoMeasurement.prototype.name,
   467                                 SysInfoMeasurement.prototype.version);
   469     let si = Cc["@mozilla.org/system-info;1"]
   470                .getService(Ci.nsIPropertyBag2);
   472     for (let [k, v] in Iterator(this.sysInfoFields)) {
   473       try {
   474         if (!si.hasKey(k)) {
   475           this._log.debug("Property not available: " + k);
   476           continue;
   477         }
   479         let value = si.getProperty(k);
   480         let method = "setLastText";
   482         if (["cpucount", "memsize"].indexOf(k) != -1) {
   483           let converted = parseInt(value, 10);
   484           if (Number.isNaN(converted)) {
   485             continue;
   486           }
   488           value = converted;
   489           method = "setLastNumeric";
   490         }
   492         switch (k) {
   493           case "memsize":
   494             // Round memory to mebibytes.
   495             value = Math.round(value / 1048576);
   496             break;
   497           case "isWow64":
   498             // Property is only present on Windows. hasKey() skipping from
   499             // above ensures undefined or null doesn't creep in here.
   500             value = value ? 1 : 0;
   501             method = "setLastNumeric";
   502             break;
   503         }
   505         yield m[method](v, value);
   506       } catch (ex) {
   507         this._log.warn("Error obtaining system info field: " + k + " " +
   508                        CommonUtils.exceptionStr(ex));
   509       }
   510     }
   511   },
   512 });
   515 /**
   516  * Holds information about the current/active session.
   517  *
   518  * The fields within the current session are moved to daily session fields when
   519  * the application is shut down.
   520  *
   521  * This measurement is backed by the SessionRecorder, not the database.
   522  */
   523 function CurrentSessionMeasurement() {
   524   Metrics.Measurement.call(this);
   525 }
   527 CurrentSessionMeasurement.prototype = Object.freeze({
   528   __proto__: Metrics.Measurement.prototype,
   530   name: "current",
   531   version: 3,
   533   // Storage is in preferences.
   534   fields: {},
   536   /**
   537    * All data is stored in prefs, so we have a custom implementation.
   538    */
   539   getValues: function () {
   540     let sessions = this.provider.healthReporter.sessionRecorder;
   542     let fields = new Map();
   543     let now = new Date();
   544     fields.set("startDay", [now, Metrics.dateToDays(sessions.startDate)]);
   545     fields.set("activeTicks", [now, sessions.activeTicks]);
   546     fields.set("totalTime", [now, sessions.totalTime]);
   547     fields.set("main", [now, sessions.main]);
   548     fields.set("firstPaint", [now, sessions.firstPaint]);
   549     fields.set("sessionRestored", [now, sessions.sessionRestored]);
   551     return CommonUtils.laterTickResolvingPromise({
   552       days: new Metrics.DailyValues(),
   553       singular: fields,
   554     });
   555   },
   557   _serializeJSONSingular: function (data) {
   558     let result = {"_v": this.version};
   560     for (let [field, value] of data) {
   561       result[field] = value[1];
   562     }
   564     return result;
   565   },
   566 });
   568 /**
   569  * Records a history of all application sessions.
   570  */
   571 function PreviousSessionsMeasurement() {
   572   Metrics.Measurement.call(this);
   573 }
   575 PreviousSessionsMeasurement.prototype = Object.freeze({
   576   __proto__: Metrics.Measurement.prototype,
   578   name: "previous",
   579   version: 3,
   581   fields: {
   582     // Milliseconds of sessions that were properly shut down.
   583     cleanActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
   584     cleanTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
   586     // Milliseconds of sessions that were not properly shut down.
   587     abortedActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
   588     abortedTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
   590     // Startup times in milliseconds.
   591     main: DAILY_DISCRETE_NUMERIC_FIELD,
   592     firstPaint: DAILY_DISCRETE_NUMERIC_FIELD,
   593     sessionRestored: DAILY_DISCRETE_NUMERIC_FIELD,
   594   },
   595 });
   598 /**
   599  * Records information about the current browser session.
   600  *
   601  * A browser session is defined as an application/process lifetime. We
   602  * start a new session when the application starts (essentially when
   603  * this provider is instantiated) and end the session on shutdown.
   604  *
   605  * As the application runs, we record basic information about the
   606  * "activity" of the session. Activity is defined by the presence of
   607  * physical input into the browser (key press, mouse click, touch, etc).
   608  *
   609  * We differentiate between regular sessions and "aborted" sessions. An
   610  * aborted session is one that does not end expectedly. This is often the
   611  * result of a crash. We detect aborted sessions by storing the current
   612  * session separate from completed sessions. We normally move the
   613  * current session to completed sessions on application shutdown. If a
   614  * current session is present on application startup, that means that
   615  * the previous session was aborted.
   616  */
   617 this.SessionsProvider = function () {
   618   Metrics.Provider.call(this);
   619 };
   621 SessionsProvider.prototype = Object.freeze({
   622   __proto__: Metrics.Provider.prototype,
   624   name: "org.mozilla.appSessions",
   626   measurementTypes: [CurrentSessionMeasurement, PreviousSessionsMeasurement],
   628   pullOnly: true,
   630   collectConstantData: function () {
   631     let previous = this.getMeasurement("previous", 3);
   633     return this.storage.enqueueTransaction(this._recordAndPruneSessions.bind(this));
   634   },
   636   _recordAndPruneSessions: function () {
   637     this._log.info("Moving previous sessions from session recorder to storage.");
   638     let recorder = this.healthReporter.sessionRecorder;
   639     let sessions = recorder.getPreviousSessions();
   640     this._log.debug("Found " + Object.keys(sessions).length + " previous sessions.");
   642     let daily = this.getMeasurement("previous", 3);
   644     // Please note the coupling here between the session recorder and our state.
   645     // If the pruned index or the current index of the session recorder is ever
   646     // deleted or reset to 0, our stored state of a later index would mean that
   647     // new sessions would never be captured by this provider until the session
   648     // recorder index catches up to our last session ID. This should not happen
   649     // under normal circumstances, so we don't worry too much about it. We
   650     // should, however, consider this as part of implementing bug 841561.
   651     let lastRecordedSession = yield this.getState("lastSession");
   652     if (lastRecordedSession === null) {
   653       lastRecordedSession = -1;
   654     }
   655     this._log.debug("The last recorded session was #" + lastRecordedSession);
   657     for (let [index, session] in Iterator(sessions)) {
   658       if (index <= lastRecordedSession) {
   659         this._log.warn("Already recorded session " + index + ". Did the last " +
   660                        "session crash or have an issue saving the prefs file?");
   661         continue;
   662       }
   664       let type = session.clean ? "clean" : "aborted";
   665       let date = session.startDate;
   666       yield daily.addDailyDiscreteNumeric(type + "ActiveTicks", session.activeTicks, date);
   667       yield daily.addDailyDiscreteNumeric(type + "TotalTime", session.totalTime, date);
   669       for (let field of ["main", "firstPaint", "sessionRestored"]) {
   670         yield daily.addDailyDiscreteNumeric(field, session[field], date);
   671       }
   673       lastRecordedSession = index;
   674     }
   676     yield this.setState("lastSession", "" + lastRecordedSession);
   677     recorder.pruneOldSessions(new Date());
   678   },
   679 });
   681 /**
   682  * Stores the set of active addons in storage.
   683  *
   684  * We do things a little differently than most other measurements. Because
   685  * addons are difficult to shoehorn into distinct fields, we simply store a
   686  * JSON blob in storage in a text field.
   687  */
   688 function ActiveAddonsMeasurement() {
   689   Metrics.Measurement.call(this);
   691   this._serializers = {};
   692   this._serializers[this.SERIALIZE_JSON] = {
   693     singular: this._serializeJSONSingular.bind(this),
   694     // We don't need a daily serializer because we have none of this data.
   695   };
   696 }
   698 ActiveAddonsMeasurement.prototype = Object.freeze({
   699   __proto__: Metrics.Measurement.prototype,
   701   name: "addons",
   702   version: 2,
   704   fields: {
   705     addons: LAST_TEXT_FIELD,
   706   },
   708   _serializeJSONSingular: function (data) {
   709     if (!data.has("addons")) {
   710       this._log.warn("Don't have addons info. Weird.");
   711       return null;
   712     }
   714     // Exceptions are caught in the caller.
   715     let result = JSON.parse(data.get("addons")[1]);
   716     result._v = this.version;
   717     return result;
   718   },
   719 });
   721 /**
   722  * Stores the set of active plugins in storage.
   723  *
   724  * This stores the data in a JSON blob in a text field similar to the
   725  * ActiveAddonsMeasurement.
   726  */
   727 function ActivePluginsMeasurement() {
   728   Metrics.Measurement.call(this);
   730   this._serializers = {};
   731   this._serializers[this.SERIALIZE_JSON] = {
   732     singular: this._serializeJSONSingular.bind(this),
   733     // We don't need a daily serializer because we have none of this data.
   734   };
   735 }
   737 ActivePluginsMeasurement.prototype = Object.freeze({
   738   __proto__: Metrics.Measurement.prototype,
   740   name: "plugins",
   741   version: 1,
   743   fields: {
   744     plugins: LAST_TEXT_FIELD,
   745   },
   747   _serializeJSONSingular: function (data) {
   748     if (!data.has("plugins")) {
   749       this._log.warn("Don't have plugins info. Weird.");
   750       return null;
   751     }
   753     // Exceptions are caught in the caller.
   754     let result = JSON.parse(data.get("plugins")[1]);
   755     result._v = this.version;
   756     return result;
   757   },
   758 });
   761 function AddonCountsMeasurement() {
   762   Metrics.Measurement.call(this);
   763 }
   765 AddonCountsMeasurement.prototype = Object.freeze({
   766   __proto__: Metrics.Measurement.prototype,
   768   name: "counts",
   769   version: 2,
   771   fields: {
   772     theme: DAILY_LAST_NUMERIC_FIELD,
   773     lwtheme: DAILY_LAST_NUMERIC_FIELD,
   774     plugin: DAILY_LAST_NUMERIC_FIELD,
   775     extension: DAILY_LAST_NUMERIC_FIELD,
   776     service: DAILY_LAST_NUMERIC_FIELD,
   777   },
   778 });
   781 /**
   782  * Legacy version of addons counts before services was added.
   783  */
   784 function AddonCountsMeasurement1() {
   785   Metrics.Measurement.call(this);
   786 }
   788 AddonCountsMeasurement1.prototype = Object.freeze({
   789   __proto__: Metrics.Measurement.prototype,
   791   name: "counts",
   792   version: 1,
   794   fields: {
   795     theme: DAILY_LAST_NUMERIC_FIELD,
   796     lwtheme: DAILY_LAST_NUMERIC_FIELD,
   797     plugin: DAILY_LAST_NUMERIC_FIELD,
   798     extension: DAILY_LAST_NUMERIC_FIELD,
   799   },
   800 });
   803 this.AddonsProvider = function () {
   804   Metrics.Provider.call(this);
   806   this._prefs = new Preferences({defaultBranch: null});
   807 };
   809 AddonsProvider.prototype = Object.freeze({
   810   __proto__: Metrics.Provider.prototype,
   812   // Whenever these AddonListener callbacks are called, we repopulate
   813   // and store the set of addons. Note that these events will only fire
   814   // for restartless add-ons. For actions that require a restart, we
   815   // will catch the change after restart. The alternative is a lot of
   816   // state tracking here, which isn't desirable.
   817   ADDON_LISTENER_CALLBACKS: [
   818     "onEnabled",
   819     "onDisabled",
   820     "onInstalled",
   821     "onUninstalled",
   822   ],
   824   // Add-on types for which full details are uploaded in the
   825   // ActiveAddonsMeasurement. All other types are ignored.
   826   FULL_DETAIL_TYPES: [
   827     "extension",
   828     "service",
   829   ],
   831   name: "org.mozilla.addons",
   833   measurementTypes: [
   834     ActiveAddonsMeasurement,
   835     ActivePluginsMeasurement,
   836     AddonCountsMeasurement1,
   837     AddonCountsMeasurement,
   838   ],
   840   postInit: function () {
   841     let listener = {};
   843     for (let method of this.ADDON_LISTENER_CALLBACKS) {
   844       listener[method] = this._collectAndStoreAddons.bind(this);
   845     }
   847     this._listener = listener;
   848     AddonManager.addAddonListener(this._listener);
   850     return CommonUtils.laterTickResolvingPromise();
   851   },
   853   onShutdown: function () {
   854     AddonManager.removeAddonListener(this._listener);
   855     this._listener = null;
   857     return CommonUtils.laterTickResolvingPromise();
   858   },
   860   collectConstantData: function () {
   861     return this._collectAndStoreAddons();
   862   },
   864   _collectAndStoreAddons: function () {
   865     let deferred = Promise.defer();
   867     AddonManager.getAllAddons(function onAllAddons(addons) {
   868       let data;
   869       let addonsField;
   870       let pluginsField;
   871       try {
   872         data = this._createDataStructure(addons);
   873         addonsField = JSON.stringify(data.addons);
   874         pluginsField = JSON.stringify(data.plugins);
   875       } catch (ex) {
   876         this._log.warn("Exception when populating add-ons data structure: " +
   877                        CommonUtils.exceptionStr(ex));
   878         deferred.reject(ex);
   879         return;
   880       }
   882       let now = new Date();
   883       let addons = this.getMeasurement("addons", 2);
   884       let plugins = this.getMeasurement("plugins", 1);
   885       let counts = this.getMeasurement(AddonCountsMeasurement.prototype.name,
   886                                        AddonCountsMeasurement.prototype.version);
   888       this.enqueueStorageOperation(function storageAddons() {
   889         for (let type in data.counts) {
   890           try {
   891             counts.fieldID(type);
   892           } catch (ex) {
   893             this._log.warn("Add-on type without field: " + type);
   894             continue;
   895           }
   897           counts.setDailyLastNumeric(type, data.counts[type], now);
   898         }
   900         return addons.setLastText("addons", addonsField).then(
   901           function onSuccess() {
   902             return plugins.setLastText("plugins", pluginsField).then(
   903               function onSuccess() { deferred.resolve(); },
   904               function onError(error) { deferred.reject(error); }
   905             );
   906           },
   907           function onError(error) { deferred.reject(error); }
   908         );
   909       }.bind(this));
   910     }.bind(this));
   912     return deferred.promise;
   913   },
   915   COPY_ADDON_FIELDS: [
   916     "userDisabled",
   917     "appDisabled",
   918     "name",
   919     "version",
   920     "type",
   921     "scope",
   922     "description",
   923     "foreignInstall",
   924     "hasBinaryComponents",
   925   ],
   927   COPY_PLUGIN_FIELDS: [
   928     "name",
   929     "version",
   930     "description",
   931     "blocklisted",
   932     "disabled",
   933     "clicktoplay",
   934   ],
   936   _createDataStructure: function (addons) {
   937     let data = {
   938       addons: {},
   939       plugins: {},
   940       counts: {}
   941     };
   943     for (let addon of addons) {
   944       let type = addon.type;
   946       // We count plugins separately below.
   947       if (addon.type == "plugin")
   948         continue;
   950       data.counts[type] = (data.counts[type] || 0) + 1;
   952       if (this.FULL_DETAIL_TYPES.indexOf(addon.type) == -1) {
   953         continue;
   954       }
   956       let obj = {};
   957       for (let field of this.COPY_ADDON_FIELDS) {
   958         obj[field] = addon[field];
   959       }
   961       if (addon.installDate) {
   962         obj.installDay = this._dateToDays(addon.installDate);
   963       }
   965       if (addon.updateDate) {
   966         obj.updateDay = this._dateToDays(addon.updateDate);
   967       }
   969       data.addons[addon.id] = obj;
   970     }
   972     let pluginTags = Cc["@mozilla.org/plugin/host;1"].
   973                        getService(Ci.nsIPluginHost).
   974                        getPluginTags({});
   976     for (let tag of pluginTags) {
   977       let obj = {
   978         mimeTypes: tag.getMimeTypes({}),
   979       };
   981       for (let field of this.COPY_PLUGIN_FIELDS) {
   982         obj[field] = tag[field];
   983       }
   985       // Plugins need to have a filename and a name, so this can't be empty.
   986       let id = tag.filename + ":" + tag.name + ":" + tag.version + ":"
   987                + tag.description;
   988       data.plugins[id] = obj;
   989     }
   991     data.counts["plugin"] = pluginTags.length;
   993     return data;
   994   },
   995 });
   997 #ifdef MOZ_CRASHREPORTER
   999 function DailyCrashesMeasurement1() {
  1000   Metrics.Measurement.call(this);
  1003 DailyCrashesMeasurement1.prototype = Object.freeze({
  1004   __proto__: Metrics.Measurement.prototype,
  1006   name: "crashes",
  1007   version: 1,
  1009   fields: {
  1010     pending: DAILY_COUNTER_FIELD,
  1011     submitted: DAILY_COUNTER_FIELD,
  1012   },
  1013 });
  1015 function DailyCrashesMeasurement2() {
  1016   Metrics.Measurement.call(this);
  1019 DailyCrashesMeasurement2.prototype = Object.freeze({
  1020   __proto__: Metrics.Measurement.prototype,
  1022   name: "crashes",
  1023   version: 2,
  1025   fields: {
  1026     mainCrash: DAILY_LAST_NUMERIC_FIELD,
  1027   },
  1028 });
  1030 this.CrashesProvider = function () {
  1031   Metrics.Provider.call(this);
  1033   // So we can unit test.
  1034   this._manager = Services.crashmanager;
  1035 };
  1037 CrashesProvider.prototype = Object.freeze({
  1038   __proto__: Metrics.Provider.prototype,
  1040   name: "org.mozilla.crashes",
  1042   measurementTypes: [
  1043     DailyCrashesMeasurement1,
  1044     DailyCrashesMeasurement2,
  1045   ],
  1047   pullOnly: true,
  1049   collectDailyData: function () {
  1050     return this.storage.enqueueTransaction(this._populateCrashCounts.bind(this));
  1051   },
  1053   _populateCrashCounts: function () {
  1054     this._log.info("Grabbing crash counts from crash manager.");
  1055     let crashCounts = yield this._manager.getCrashCountsByDay();
  1056     let fields = {
  1057       "main-crash": "mainCrash",
  1058     };
  1060     let m = this.getMeasurement("crashes", 2);
  1062     for (let [day, types] of crashCounts) {
  1063       let date = Metrics.daysToDate(day);
  1064       for (let [type, count] of types) {
  1065         if (!(type in fields)) {
  1066           this._log.warn("Unknown crash type encountered: " + type);
  1067           continue;
  1070         yield m.setDailyLastNumeric(fields[type], count, date);
  1073   },
  1074 });
  1076 #endif
  1079 /**
  1080  * Holds basic statistics about the Places database.
  1081  */
  1082 function PlacesMeasurement() {
  1083   Metrics.Measurement.call(this);
  1086 PlacesMeasurement.prototype = Object.freeze({
  1087   __proto__: Metrics.Measurement.prototype,
  1089   name: "places",
  1090   version: 1,
  1092   fields: {
  1093     pages: DAILY_LAST_NUMERIC_FIELD,
  1094     bookmarks: DAILY_LAST_NUMERIC_FIELD,
  1095   },
  1096 });
  1099 /**
  1100  * Collects information about Places.
  1101  */
  1102 this.PlacesProvider = function () {
  1103   Metrics.Provider.call(this);
  1104 };
  1106 PlacesProvider.prototype = Object.freeze({
  1107   __proto__: Metrics.Provider.prototype,
  1109   name: "org.mozilla.places",
  1111   measurementTypes: [PlacesMeasurement],
  1113   collectDailyData: function () {
  1114     return this.storage.enqueueTransaction(this._collectData.bind(this));
  1115   },
  1117   _collectData: function () {
  1118     let now = new Date();
  1119     let data = yield this._getDailyValues();
  1121     let m = this.getMeasurement("places", 1);
  1123     yield m.setDailyLastNumeric("pages", data.PLACES_PAGES_COUNT);
  1124     yield m.setDailyLastNumeric("bookmarks", data.PLACES_BOOKMARKS_COUNT);
  1125   },
  1127   _getDailyValues: function () {
  1128     let deferred = Promise.defer();
  1130     PlacesDBUtils.telemetry(null, function onResult(data) {
  1131       deferred.resolve(data);
  1132     });
  1134     return deferred.promise;
  1135   },
  1136 });
  1138 function SearchCountMeasurement1() {
  1139   Metrics.Measurement.call(this);
  1142 SearchCountMeasurement1.prototype = Object.freeze({
  1143   __proto__: Metrics.Measurement.prototype,
  1145   name: "counts",
  1146   version: 1,
  1148   // We only record searches for search engines that have partner agreements
  1149   // with Mozilla.
  1150   fields: {
  1151     "amazon.com.abouthome": DAILY_COUNTER_FIELD,
  1152     "amazon.com.contextmenu": DAILY_COUNTER_FIELD,
  1153     "amazon.com.searchbar": DAILY_COUNTER_FIELD,
  1154     "amazon.com.urlbar": DAILY_COUNTER_FIELD,
  1155     "bing.abouthome": DAILY_COUNTER_FIELD,
  1156     "bing.contextmenu": DAILY_COUNTER_FIELD,
  1157     "bing.searchbar": DAILY_COUNTER_FIELD,
  1158     "bing.urlbar": DAILY_COUNTER_FIELD,
  1159     "google.abouthome": DAILY_COUNTER_FIELD,
  1160     "google.contextmenu": DAILY_COUNTER_FIELD,
  1161     "google.searchbar": DAILY_COUNTER_FIELD,
  1162     "google.urlbar": DAILY_COUNTER_FIELD,
  1163     "yahoo.abouthome": DAILY_COUNTER_FIELD,
  1164     "yahoo.contextmenu": DAILY_COUNTER_FIELD,
  1165     "yahoo.searchbar": DAILY_COUNTER_FIELD,
  1166     "yahoo.urlbar": DAILY_COUNTER_FIELD,
  1167     "other.abouthome": DAILY_COUNTER_FIELD,
  1168     "other.contextmenu": DAILY_COUNTER_FIELD,
  1169     "other.searchbar": DAILY_COUNTER_FIELD,
  1170     "other.urlbar": DAILY_COUNTER_FIELD,
  1171   },
  1172 });
  1174 /**
  1175  * Records search counts per day per engine and where search initiated.
  1177  * We want to record granular details for individual locale-specific search
  1178  * providers, but only if they're Mozilla partners. In order to do this, we
  1179  * track the nsISearchEngine identifier, which denotes shipped search engines,
  1180  * and intersect those with our partner list.
  1182  * We don't use the search engine name directly, because it is shared across
  1183  * locales; e.g., eBay-de and eBay both share the name "eBay".
  1184  */
  1185 function SearchCountMeasurementBase() {
  1186   this._fieldSpecs = {};
  1187   Metrics.Measurement.call(this);
  1190 SearchCountMeasurementBase.prototype = Object.freeze({
  1191   __proto__: Metrics.Measurement.prototype,
  1194   // Our fields are dynamic.
  1195   get fields() {
  1196     return this._fieldSpecs;
  1197   },
  1199   /**
  1200    * Override the default behavior: serializers should include every counter
  1201    * field from the DB, even if we don't currently have it registered.
  1203    * Do this so we don't have to register several hundred fields to match
  1204    * various Firefox locales.
  1206    * We use the "provider.type" syntax as a rudimentary check for validity.
  1208    * We trust that measurement versioning is sufficient to exclude old provider
  1209    * data.
  1210    */
  1211   shouldIncludeField: function (name) {
  1212     return name.contains(".");
  1213   },
  1215   /**
  1216    * The measurement type mechanism doesn't introspect the DB. Override it
  1217    * so that we can assume all unknown fields are counters.
  1218    */
  1219   fieldType: function (name) {
  1220     if (name in this.fields) {
  1221       return this.fields[name].type;
  1224     // Default to a counter.
  1225     return Metrics.Storage.FIELD_DAILY_COUNTER;
  1226   },
  1228   SOURCES: [
  1229     "abouthome",
  1230     "contextmenu",
  1231     "newtab",
  1232     "searchbar",
  1233     "urlbar",
  1234   ],
  1235 });
  1237 function SearchCountMeasurement2() {
  1238   SearchCountMeasurementBase.call(this);
  1241 SearchCountMeasurement2.prototype = Object.freeze({
  1242   __proto__: SearchCountMeasurementBase.prototype,
  1243   name: "counts",
  1244   version: 2,
  1245 });
  1247 function SearchCountMeasurement3() {
  1248   SearchCountMeasurementBase.call(this);
  1251 SearchCountMeasurement3.prototype = Object.freeze({
  1252   __proto__: SearchCountMeasurementBase.prototype,
  1253   name: "counts",
  1254   version: 3,
  1256   getEngines: function () {
  1257     return Services.search.getEngines();
  1258   },
  1260   getEngineID: function (engine) {
  1261     if (!engine) {
  1262       return "other";
  1264     if (engine.identifier) {
  1265       return engine.identifier;
  1267     return "other-" + engine.name;
  1268   },
  1269 });
  1271 function SearchEnginesMeasurement1() {
  1272   Metrics.Measurement.call(this);
  1275 SearchEnginesMeasurement1.prototype = Object.freeze({
  1276   __proto__: Metrics.Measurement.prototype,
  1278   name: "engines",
  1279   version: 1,
  1281   fields: {
  1282     default: DAILY_LAST_TEXT_FIELD,
  1283   },
  1284 });
  1286 this.SearchesProvider = function () {
  1287   Metrics.Provider.call(this);
  1289   this._prefs = new Preferences({defaultBranch: null});
  1290 };
  1292 this.SearchesProvider.prototype = Object.freeze({
  1293   __proto__: Metrics.Provider.prototype,
  1295   name: "org.mozilla.searches",
  1296   measurementTypes: [
  1297     SearchCountMeasurement1,
  1298     SearchCountMeasurement2,
  1299     SearchCountMeasurement3,
  1300     SearchEnginesMeasurement1,
  1301   ],
  1303   /**
  1304    * Initialize the search service before our measurements are touched.
  1305    */
  1306   preInit: function (storage) {
  1307     // Initialize search service.
  1308     let deferred = Promise.defer();
  1309     Services.search.init(function onInitComplete () {
  1310       deferred.resolve();
  1311     });
  1312     return deferred.promise;
  1313   },
  1315   collectDailyData: function () {
  1316     return this.storage.enqueueTransaction(function getDaily() {
  1317       // We currently only record this if Telemetry is enabled.
  1318       if (!isTelemetryEnabled(this._prefs)) {
  1319         return;
  1322       let m = this.getMeasurement(SearchEnginesMeasurement1.prototype.name,
  1323                                   SearchEnginesMeasurement1.prototype.version);
  1325       let engine;
  1326       try {
  1327         engine = Services.search.defaultEngine;
  1328       } catch (e) {}
  1329       let name;
  1331       if (!engine) {
  1332         name = "NONE";
  1333       } else if (engine.identifier) {
  1334         name = engine.identifier;
  1335       } else if (engine.name) {
  1336         name = "other-" + engine.name;
  1337       } else {
  1338         name = "UNDEFINED";
  1341       yield m.setDailyLastText("default", name);
  1342     }.bind(this));
  1343   },
  1345   /**
  1346    * Record that a search occurred.
  1348    * @param engine
  1349    *        (nsISearchEngine) The search engine used.
  1350    * @param source
  1351    *        (string) Where the search was initiated from. Must be one of the
  1352    *        SearchCountMeasurement2.SOURCES values.
  1354    * @return Promise<>
  1355    *         The promise is resolved when the storage operation completes.
  1356    */
  1357   recordSearch: function (engine, source) {
  1358     let m = this.getMeasurement("counts", 3);
  1360     if (m.SOURCES.indexOf(source) == -1) {
  1361       throw new Error("Unknown source for search: " + source);
  1364     let field = m.getEngineID(engine) + "." + source;
  1365     if (this.storage.hasFieldFromMeasurement(m.id, field,
  1366                                              this.storage.FIELD_DAILY_COUNTER)) {
  1367       let fieldID = this.storage.fieldIDFromMeasurement(m.id, field);
  1368       return this.enqueueStorageOperation(function recordSearchKnownField() {
  1369         return this.storage.incrementDailyCounterFromFieldID(fieldID);
  1370       }.bind(this));
  1373     // Otherwise, we first need to create the field.
  1374     return this.enqueueStorageOperation(function recordFieldAndSearch() {
  1375       // This function has to return a promise.
  1376       return Task.spawn(function () {
  1377         let fieldID = yield this.storage.registerField(m.id, field,
  1378                                                        this.storage.FIELD_DAILY_COUNTER);
  1379         yield this.storage.incrementDailyCounterFromFieldID(fieldID);
  1380       }.bind(this));
  1381     }.bind(this));
  1382   },
  1383 });
  1385 function HealthReportSubmissionMeasurement1() {
  1386   Metrics.Measurement.call(this);
  1389 HealthReportSubmissionMeasurement1.prototype = Object.freeze({
  1390   __proto__: Metrics.Measurement.prototype,
  1392   name: "submissions",
  1393   version: 1,
  1395   fields: {
  1396     firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
  1397     continuationUploadAttempt: DAILY_COUNTER_FIELD,
  1398     uploadSuccess: DAILY_COUNTER_FIELD,
  1399     uploadTransportFailure: DAILY_COUNTER_FIELD,
  1400     uploadServerFailure: DAILY_COUNTER_FIELD,
  1401     uploadClientFailure: DAILY_COUNTER_FIELD,
  1402   },
  1403 });
  1405 function HealthReportSubmissionMeasurement2() {
  1406   Metrics.Measurement.call(this);
  1409 HealthReportSubmissionMeasurement2.prototype = Object.freeze({
  1410   __proto__: Metrics.Measurement.prototype,
  1412   name: "submissions",
  1413   version: 2,
  1415   fields: {
  1416     firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
  1417     continuationUploadAttempt: DAILY_COUNTER_FIELD,
  1418     uploadSuccess: DAILY_COUNTER_FIELD,
  1419     uploadTransportFailure: DAILY_COUNTER_FIELD,
  1420     uploadServerFailure: DAILY_COUNTER_FIELD,
  1421     uploadClientFailure: DAILY_COUNTER_FIELD,
  1422     uploadAlreadyInProgress: DAILY_COUNTER_FIELD,
  1423   },
  1424 });
  1426 this.HealthReportProvider = function () {
  1427   Metrics.Provider.call(this);
  1430 HealthReportProvider.prototype = Object.freeze({
  1431   __proto__: Metrics.Provider.prototype,
  1433   name: "org.mozilla.healthreport",
  1435   measurementTypes: [
  1436     HealthReportSubmissionMeasurement1,
  1437     HealthReportSubmissionMeasurement2,
  1438   ],
  1440   recordEvent: function (event, date=new Date()) {
  1441     let m = this.getMeasurement("submissions", 2);
  1442     return this.enqueueStorageOperation(function recordCounter() {
  1443       return m.incrementDailyCounter(event, date);
  1444     });
  1445   },
  1446 });

mercurial