services/healthreport/providers.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/services/healthreport/providers.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1446 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +/**
     1.9 + * This file contains metrics data providers for the Firefox Health
    1.10 + * Report. Ideally each provider in this file exists in separate modules
    1.11 + * and lives close to the code it is querying. However, because of the
    1.12 + * overhead of JS compartments (which are created for each module), we
    1.13 + * currently have all the code in one file. When the overhead of
    1.14 + * compartments reaches a reasonable level, this file should be split
    1.15 + * up.
    1.16 + */
    1.17 +
    1.18 +"use strict";
    1.19 +
    1.20 +#ifndef MERGED_COMPARTMENT
    1.21 +
    1.22 +this.EXPORTED_SYMBOLS = [
    1.23 +  "AddonsProvider",
    1.24 +  "AppInfoProvider",
    1.25 +#ifdef MOZ_CRASHREPORTER
    1.26 +  "CrashesProvider",
    1.27 +#endif
    1.28 +  "HealthReportProvider",
    1.29 +  "PlacesProvider",
    1.30 +  "SearchesProvider",
    1.31 +  "SessionsProvider",
    1.32 +  "SysInfoProvider",
    1.33 +];
    1.34 +
    1.35 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    1.36 +
    1.37 +Cu.import("resource://gre/modules/Metrics.jsm");
    1.38 +
    1.39 +#endif
    1.40 +
    1.41 +Cu.import("resource://gre/modules/Promise.jsm");
    1.42 +Cu.import("resource://gre/modules/osfile.jsm");
    1.43 +Cu.import("resource://gre/modules/Preferences.jsm");
    1.44 +Cu.import("resource://gre/modules/Services.jsm");
    1.45 +Cu.import("resource://gre/modules/Task.jsm");
    1.46 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.47 +Cu.import("resource://services-common/utils.js");
    1.48 +
    1.49 +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
    1.50 +                                  "resource://gre/modules/AddonManager.jsm");
    1.51 +XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
    1.52 +                                  "resource://gre/modules/UpdateChannel.jsm");
    1.53 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils",
    1.54 +                                  "resource://gre/modules/PlacesDBUtils.jsm");
    1.55 +
    1.56 +
    1.57 +const LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_LAST_NUMERIC};
    1.58 +const LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_LAST_TEXT};
    1.59 +const DAILY_DISCRETE_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC};
    1.60 +const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
    1.61 +const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
    1.62 +const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
    1.63 +
    1.64 +const TELEMETRY_PREF = "toolkit.telemetry.enabled";
    1.65 +
    1.66 +function isTelemetryEnabled(prefs) {
    1.67 +  return prefs.get(TELEMETRY_PREF, false);
    1.68 +}
    1.69 +
    1.70 +/**
    1.71 + * Represents basic application state.
    1.72 + *
    1.73 + * This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra
    1.74 + * pieces thrown in.
    1.75 + */
    1.76 +function AppInfoMeasurement() {
    1.77 +  Metrics.Measurement.call(this);
    1.78 +}
    1.79 +
    1.80 +AppInfoMeasurement.prototype = Object.freeze({
    1.81 +  __proto__: Metrics.Measurement.prototype,
    1.82 +
    1.83 +  name: "appinfo",
    1.84 +  version: 2,
    1.85 +
    1.86 +  fields: {
    1.87 +    vendor: LAST_TEXT_FIELD,
    1.88 +    name: LAST_TEXT_FIELD,
    1.89 +    id: LAST_TEXT_FIELD,
    1.90 +    version: LAST_TEXT_FIELD,
    1.91 +    appBuildID: LAST_TEXT_FIELD,
    1.92 +    platformVersion: LAST_TEXT_FIELD,
    1.93 +    platformBuildID: LAST_TEXT_FIELD,
    1.94 +    os: LAST_TEXT_FIELD,
    1.95 +    xpcomabi: LAST_TEXT_FIELD,
    1.96 +    updateChannel: LAST_TEXT_FIELD,
    1.97 +    distributionID: LAST_TEXT_FIELD,
    1.98 +    distributionVersion: LAST_TEXT_FIELD,
    1.99 +    hotfixVersion: LAST_TEXT_FIELD,
   1.100 +    locale: LAST_TEXT_FIELD,
   1.101 +    isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   1.102 +    isTelemetryEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   1.103 +    isBlocklistEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   1.104 +  },
   1.105 +});
   1.106 +
   1.107 +/**
   1.108 + * Legacy version of app info before Telemetry was added.
   1.109 + *
   1.110 + * The "last" fields have all been removed. We only report the longitudinal
   1.111 + * field.
   1.112 + */
   1.113 +function AppInfoMeasurement1() {
   1.114 +  Metrics.Measurement.call(this);
   1.115 +}
   1.116 +
   1.117 +AppInfoMeasurement1.prototype = Object.freeze({
   1.118 +  __proto__: Metrics.Measurement.prototype,
   1.119 +
   1.120 +  name: "appinfo",
   1.121 +  version: 1,
   1.122 +
   1.123 +  fields: {
   1.124 +    isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   1.125 +  },
   1.126 +});
   1.127 +
   1.128 +
   1.129 +function AppVersionMeasurement1() {
   1.130 +  Metrics.Measurement.call(this);
   1.131 +}
   1.132 +
   1.133 +AppVersionMeasurement1.prototype = Object.freeze({
   1.134 +  __proto__: Metrics.Measurement.prototype,
   1.135 +
   1.136 +  name: "versions",
   1.137 +  version: 1,
   1.138 +
   1.139 +  fields: {
   1.140 +    version: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
   1.141 +  },
   1.142 +});
   1.143 +
   1.144 +// Version 2 added the build ID.
   1.145 +function AppVersionMeasurement2() {
   1.146 +  Metrics.Measurement.call(this);
   1.147 +}
   1.148 +
   1.149 +AppVersionMeasurement2.prototype = Object.freeze({
   1.150 +  __proto__: Metrics.Measurement.prototype,
   1.151 +
   1.152 +  name: "versions",
   1.153 +  version: 2,
   1.154 +
   1.155 +  fields: {
   1.156 +    appVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
   1.157 +    platformVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
   1.158 +    appBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
   1.159 +    platformBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
   1.160 +  },
   1.161 +});
   1.162 +
   1.163 +/**
   1.164 + * Holds data on the application update functionality.
   1.165 + */
   1.166 +function AppUpdateMeasurement1() {
   1.167 +  Metrics.Measurement.call(this);
   1.168 +}
   1.169 +
   1.170 +AppUpdateMeasurement1.prototype = Object.freeze({
   1.171 +  __proto__: Metrics.Measurement.prototype,
   1.172 +
   1.173 +  name: "update",
   1.174 +  version: 1,
   1.175 +
   1.176 +  fields: {
   1.177 +    enabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   1.178 +    autoDownload: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
   1.179 +  },
   1.180 +});
   1.181 +
   1.182 +this.AppInfoProvider = function AppInfoProvider() {
   1.183 +  Metrics.Provider.call(this);
   1.184 +
   1.185 +  this._prefs = new Preferences({defaultBranch: null});
   1.186 +}
   1.187 +AppInfoProvider.prototype = Object.freeze({
   1.188 +  __proto__: Metrics.Provider.prototype,
   1.189 +
   1.190 +  name: "org.mozilla.appInfo",
   1.191 +
   1.192 +  measurementTypes: [
   1.193 +    AppInfoMeasurement,
   1.194 +    AppInfoMeasurement1,
   1.195 +    AppUpdateMeasurement1,
   1.196 +    AppVersionMeasurement1,
   1.197 +    AppVersionMeasurement2,
   1.198 +  ],
   1.199 +
   1.200 +  pullOnly: true,
   1.201 +
   1.202 +  appInfoFields: {
   1.203 +    // From nsIXULAppInfo.
   1.204 +    vendor: "vendor",
   1.205 +    name: "name",
   1.206 +    id: "ID",
   1.207 +    version: "version",
   1.208 +    appBuildID: "appBuildID",
   1.209 +    platformVersion: "platformVersion",
   1.210 +    platformBuildID: "platformBuildID",
   1.211 +
   1.212 +    // From nsIXULRuntime.
   1.213 +    os: "OS",
   1.214 +    xpcomabi: "XPCOMABI",
   1.215 +  },
   1.216 +
   1.217 +  postInit: function () {
   1.218 +    return Task.spawn(this._postInit.bind(this));
   1.219 +  },
   1.220 +
   1.221 +  _postInit: function () {
   1.222 +    let recordEmptyAppInfo = function () {
   1.223 +      this._setCurrentAppVersion("");
   1.224 +      this._setCurrentPlatformVersion("");
   1.225 +      this._setCurrentAppBuildID("");
   1.226 +      return this._setCurrentPlatformBuildID("");
   1.227 +    }.bind(this);
   1.228 +
   1.229 +    // Services.appInfo should always be defined for any reasonably behaving
   1.230 +    // Gecko app. If it isn't, we insert a empty string sentinel value.
   1.231 +    let ai;
   1.232 +    try {
   1.233 +      ai = Services.appinfo;
   1.234 +    } catch (ex) {
   1.235 +      this._log.error("Could not obtain Services.appinfo: " +
   1.236 +                     CommonUtils.exceptionStr(ex));
   1.237 +      yield recordEmptyAppInfo();
   1.238 +      return;
   1.239 +    }
   1.240 +
   1.241 +    if (!ai) {
   1.242 +      this._log.error("Services.appinfo is unavailable.");
   1.243 +      yield recordEmptyAppInfo();
   1.244 +      return;
   1.245 +    }
   1.246 +
   1.247 +    let currentAppVersion = ai.version;
   1.248 +    let currentPlatformVersion = ai.platformVersion;
   1.249 +    let currentAppBuildID = ai.appBuildID;
   1.250 +    let currentPlatformBuildID = ai.platformBuildID;
   1.251 +
   1.252 +    // State's name doesn't contain "app" for historical compatibility.
   1.253 +    let lastAppVersion = yield this.getState("lastVersion");
   1.254 +    let lastPlatformVersion = yield this.getState("lastPlatformVersion");
   1.255 +    let lastAppBuildID = yield this.getState("lastAppBuildID");
   1.256 +    let lastPlatformBuildID = yield this.getState("lastPlatformBuildID");
   1.257 +
   1.258 +    if (currentAppVersion != lastAppVersion) {
   1.259 +      yield this._setCurrentAppVersion(currentAppVersion);
   1.260 +    }
   1.261 +
   1.262 +    if (currentPlatformVersion != lastPlatformVersion) {
   1.263 +      yield this._setCurrentPlatformVersion(currentPlatformVersion);
   1.264 +    }
   1.265 +
   1.266 +    if (currentAppBuildID != lastAppBuildID) {
   1.267 +      yield this._setCurrentAppBuildID(currentAppBuildID);
   1.268 +    }
   1.269 +
   1.270 +    if (currentPlatformBuildID != lastPlatformBuildID) {
   1.271 +      yield this._setCurrentPlatformBuildID(currentPlatformBuildID);
   1.272 +    }
   1.273 +  },
   1.274 +
   1.275 +  _setCurrentAppVersion: function (version) {
   1.276 +    this._log.info("Recording new application version: " + version);
   1.277 +    let m = this.getMeasurement("versions", 2);
   1.278 +    m.addDailyDiscreteText("appVersion", version);
   1.279 +
   1.280 +    // "app" not encoded in key for historical compatibility.
   1.281 +    return this.setState("lastVersion", version);
   1.282 +  },
   1.283 +
   1.284 +  _setCurrentPlatformVersion: function (version) {
   1.285 +    this._log.info("Recording new platform version: " + version);
   1.286 +    let m = this.getMeasurement("versions", 2);
   1.287 +    m.addDailyDiscreteText("platformVersion", version);
   1.288 +    return this.setState("lastPlatformVersion", version);
   1.289 +  },
   1.290 +
   1.291 +  _setCurrentAppBuildID: function (build) {
   1.292 +    this._log.info("Recording new application build ID: " + build);
   1.293 +    let m = this.getMeasurement("versions", 2);
   1.294 +    m.addDailyDiscreteText("appBuildID", build);
   1.295 +    return this.setState("lastAppBuildID", build);
   1.296 +  },
   1.297 +
   1.298 +  _setCurrentPlatformBuildID: function (build) {
   1.299 +    this._log.info("Recording new platform build ID: " + build);
   1.300 +    let m = this.getMeasurement("versions", 2);
   1.301 +    m.addDailyDiscreteText("platformBuildID", build);
   1.302 +    return this.setState("lastPlatformBuildID", build);
   1.303 +  },
   1.304 +
   1.305 +
   1.306 +  collectConstantData: function () {
   1.307 +    return this.storage.enqueueTransaction(this._populateConstants.bind(this));
   1.308 +  },
   1.309 +
   1.310 +  _populateConstants: function () {
   1.311 +    let m = this.getMeasurement(AppInfoMeasurement.prototype.name,
   1.312 +                                AppInfoMeasurement.prototype.version);
   1.313 +
   1.314 +    let ai;
   1.315 +    try {
   1.316 +      ai = Services.appinfo;
   1.317 +    } catch (ex) {
   1.318 +      this._log.warn("Could not obtain Services.appinfo: " +
   1.319 +                     CommonUtils.exceptionStr(ex));
   1.320 +      throw ex;
   1.321 +    }
   1.322 +
   1.323 +    if (!ai) {
   1.324 +      this._log.warn("Services.appinfo is unavailable.");
   1.325 +      throw ex;
   1.326 +    }
   1.327 +
   1.328 +    for (let [k, v] in Iterator(this.appInfoFields)) {
   1.329 +      try {
   1.330 +        yield m.setLastText(k, ai[v]);
   1.331 +      } catch (ex) {
   1.332 +        this._log.warn("Error obtaining Services.appinfo." + v);
   1.333 +      }
   1.334 +    }
   1.335 +
   1.336 +    try {
   1.337 +      yield m.setLastText("updateChannel", UpdateChannel.get());
   1.338 +    } catch (ex) {
   1.339 +      this._log.warn("Could not obtain update channel: " +
   1.340 +                     CommonUtils.exceptionStr(ex));
   1.341 +    }
   1.342 +
   1.343 +    yield m.setLastText("distributionID", this._prefs.get("distribution.id", ""));
   1.344 +    yield m.setLastText("distributionVersion", this._prefs.get("distribution.version", ""));
   1.345 +    yield m.setLastText("hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
   1.346 +
   1.347 +    try {
   1.348 +      let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
   1.349 +                     .getService(Ci.nsIXULChromeRegistry)
   1.350 +                     .getSelectedLocale("global");
   1.351 +      yield m.setLastText("locale", locale);
   1.352 +    } catch (ex) {
   1.353 +      this._log.warn("Could not obtain application locale: " +
   1.354 +                     CommonUtils.exceptionStr(ex));
   1.355 +    }
   1.356 +
   1.357 +    // FUTURE this should be retrieved periodically or at upload time.
   1.358 +    yield this._recordIsTelemetryEnabled(m);
   1.359 +    yield this._recordIsBlocklistEnabled(m);
   1.360 +    yield this._recordDefaultBrowser(m);
   1.361 +  },
   1.362 +
   1.363 +  _recordIsTelemetryEnabled: function (m) {
   1.364 +    let enabled = isTelemetryEnabled(this._prefs);
   1.365 +    this._log.debug("Recording telemetry enabled (" + TELEMETRY_PREF + "): " + enabled);
   1.366 +    yield m.setDailyLastNumeric("isTelemetryEnabled", enabled ? 1 : 0);
   1.367 +  },
   1.368 +
   1.369 +  _recordIsBlocklistEnabled: function (m) {
   1.370 +    let enabled = this._prefs.get("extensions.blocklist.enabled", false);
   1.371 +    this._log.debug("Recording blocklist enabled: " + enabled);
   1.372 +    yield m.setDailyLastNumeric("isBlocklistEnabled", enabled ? 1 : 0);
   1.373 +  },
   1.374 +
   1.375 +  _recordDefaultBrowser: function (m) {
   1.376 +    let shellService;
   1.377 +    try {
   1.378 +      shellService = Cc["@mozilla.org/browser/shell-service;1"]
   1.379 +                       .getService(Ci.nsIShellService);
   1.380 +    } catch (ex) {
   1.381 +      this._log.warn("Could not obtain shell service: " +
   1.382 +                     CommonUtils.exceptionStr(ex));
   1.383 +    }
   1.384 +
   1.385 +    let isDefault = -1;
   1.386 +
   1.387 +    if (shellService) {
   1.388 +      try {
   1.389 +        // This uses the same set of flags used by the pref pane.
   1.390 +        isDefault = shellService.isDefaultBrowser(false, true) ? 1 : 0;
   1.391 +      } catch (ex) {
   1.392 +        this._log.warn("Could not determine if default browser: " +
   1.393 +                       CommonUtils.exceptionStr(ex));
   1.394 +      }
   1.395 +    }
   1.396 +
   1.397 +    return m.setDailyLastNumeric("isDefaultBrowser", isDefault);
   1.398 +  },
   1.399 +
   1.400 +  collectDailyData: function () {
   1.401 +    return this.storage.enqueueTransaction(function getDaily() {
   1.402 +      let m = this.getMeasurement(AppUpdateMeasurement1.prototype.name,
   1.403 +                                  AppUpdateMeasurement1.prototype.version);
   1.404 +
   1.405 +      let enabled = this._prefs.get("app.update.enabled", false);
   1.406 +      yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0);
   1.407 +
   1.408 +      let auto = this._prefs.get("app.update.auto", false);
   1.409 +      yield m.setDailyLastNumeric("autoDownload", auto ? 1 : 0);
   1.410 +    }.bind(this));
   1.411 +  },
   1.412 +});
   1.413 +
   1.414 +
   1.415 +function SysInfoMeasurement() {
   1.416 +  Metrics.Measurement.call(this);
   1.417 +}
   1.418 +
   1.419 +SysInfoMeasurement.prototype = Object.freeze({
   1.420 +  __proto__: Metrics.Measurement.prototype,
   1.421 +
   1.422 +  name: "sysinfo",
   1.423 +  version: 2,
   1.424 +
   1.425 +  fields: {
   1.426 +    cpuCount: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
   1.427 +    memoryMB: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
   1.428 +    manufacturer: LAST_TEXT_FIELD,
   1.429 +    device: LAST_TEXT_FIELD,
   1.430 +    hardware: LAST_TEXT_FIELD,
   1.431 +    name: LAST_TEXT_FIELD,
   1.432 +    version: LAST_TEXT_FIELD,
   1.433 +    architecture: LAST_TEXT_FIELD,
   1.434 +    isWow64: LAST_NUMERIC_FIELD,
   1.435 +  },
   1.436 +});
   1.437 +
   1.438 +
   1.439 +this.SysInfoProvider = function SysInfoProvider() {
   1.440 +  Metrics.Provider.call(this);
   1.441 +};
   1.442 +
   1.443 +SysInfoProvider.prototype = Object.freeze({
   1.444 +  __proto__: Metrics.Provider.prototype,
   1.445 +
   1.446 +  name: "org.mozilla.sysinfo",
   1.447 +
   1.448 +  measurementTypes: [SysInfoMeasurement],
   1.449 +
   1.450 +  pullOnly: true,
   1.451 +
   1.452 +  sysInfoFields: {
   1.453 +    cpucount: "cpuCount",
   1.454 +    memsize: "memoryMB",
   1.455 +    manufacturer: "manufacturer",
   1.456 +    device: "device",
   1.457 +    hardware: "hardware",
   1.458 +    name: "name",
   1.459 +    version: "version",
   1.460 +    arch: "architecture",
   1.461 +    isWow64: "isWow64",
   1.462 +  },
   1.463 +
   1.464 +  collectConstantData: function () {
   1.465 +    return this.storage.enqueueTransaction(this._populateConstants.bind(this));
   1.466 +  },
   1.467 +
   1.468 +  _populateConstants: function () {
   1.469 +    let m = this.getMeasurement(SysInfoMeasurement.prototype.name,
   1.470 +                                SysInfoMeasurement.prototype.version);
   1.471 +
   1.472 +    let si = Cc["@mozilla.org/system-info;1"]
   1.473 +               .getService(Ci.nsIPropertyBag2);
   1.474 +
   1.475 +    for (let [k, v] in Iterator(this.sysInfoFields)) {
   1.476 +      try {
   1.477 +        if (!si.hasKey(k)) {
   1.478 +          this._log.debug("Property not available: " + k);
   1.479 +          continue;
   1.480 +        }
   1.481 +
   1.482 +        let value = si.getProperty(k);
   1.483 +        let method = "setLastText";
   1.484 +
   1.485 +        if (["cpucount", "memsize"].indexOf(k) != -1) {
   1.486 +          let converted = parseInt(value, 10);
   1.487 +          if (Number.isNaN(converted)) {
   1.488 +            continue;
   1.489 +          }
   1.490 +
   1.491 +          value = converted;
   1.492 +          method = "setLastNumeric";
   1.493 +        }
   1.494 +
   1.495 +        switch (k) {
   1.496 +          case "memsize":
   1.497 +            // Round memory to mebibytes.
   1.498 +            value = Math.round(value / 1048576);
   1.499 +            break;
   1.500 +          case "isWow64":
   1.501 +            // Property is only present on Windows. hasKey() skipping from
   1.502 +            // above ensures undefined or null doesn't creep in here.
   1.503 +            value = value ? 1 : 0;
   1.504 +            method = "setLastNumeric";
   1.505 +            break;
   1.506 +        }
   1.507 +
   1.508 +        yield m[method](v, value);
   1.509 +      } catch (ex) {
   1.510 +        this._log.warn("Error obtaining system info field: " + k + " " +
   1.511 +                       CommonUtils.exceptionStr(ex));
   1.512 +      }
   1.513 +    }
   1.514 +  },
   1.515 +});
   1.516 +
   1.517 +
   1.518 +/**
   1.519 + * Holds information about the current/active session.
   1.520 + *
   1.521 + * The fields within the current session are moved to daily session fields when
   1.522 + * the application is shut down.
   1.523 + *
   1.524 + * This measurement is backed by the SessionRecorder, not the database.
   1.525 + */
   1.526 +function CurrentSessionMeasurement() {
   1.527 +  Metrics.Measurement.call(this);
   1.528 +}
   1.529 +
   1.530 +CurrentSessionMeasurement.prototype = Object.freeze({
   1.531 +  __proto__: Metrics.Measurement.prototype,
   1.532 +
   1.533 +  name: "current",
   1.534 +  version: 3,
   1.535 +
   1.536 +  // Storage is in preferences.
   1.537 +  fields: {},
   1.538 +
   1.539 +  /**
   1.540 +   * All data is stored in prefs, so we have a custom implementation.
   1.541 +   */
   1.542 +  getValues: function () {
   1.543 +    let sessions = this.provider.healthReporter.sessionRecorder;
   1.544 +
   1.545 +    let fields = new Map();
   1.546 +    let now = new Date();
   1.547 +    fields.set("startDay", [now, Metrics.dateToDays(sessions.startDate)]);
   1.548 +    fields.set("activeTicks", [now, sessions.activeTicks]);
   1.549 +    fields.set("totalTime", [now, sessions.totalTime]);
   1.550 +    fields.set("main", [now, sessions.main]);
   1.551 +    fields.set("firstPaint", [now, sessions.firstPaint]);
   1.552 +    fields.set("sessionRestored", [now, sessions.sessionRestored]);
   1.553 +
   1.554 +    return CommonUtils.laterTickResolvingPromise({
   1.555 +      days: new Metrics.DailyValues(),
   1.556 +      singular: fields,
   1.557 +    });
   1.558 +  },
   1.559 +
   1.560 +  _serializeJSONSingular: function (data) {
   1.561 +    let result = {"_v": this.version};
   1.562 +
   1.563 +    for (let [field, value] of data) {
   1.564 +      result[field] = value[1];
   1.565 +    }
   1.566 +
   1.567 +    return result;
   1.568 +  },
   1.569 +});
   1.570 +
   1.571 +/**
   1.572 + * Records a history of all application sessions.
   1.573 + */
   1.574 +function PreviousSessionsMeasurement() {
   1.575 +  Metrics.Measurement.call(this);
   1.576 +}
   1.577 +
   1.578 +PreviousSessionsMeasurement.prototype = Object.freeze({
   1.579 +  __proto__: Metrics.Measurement.prototype,
   1.580 +
   1.581 +  name: "previous",
   1.582 +  version: 3,
   1.583 +
   1.584 +  fields: {
   1.585 +    // Milliseconds of sessions that were properly shut down.
   1.586 +    cleanActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
   1.587 +    cleanTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
   1.588 +
   1.589 +    // Milliseconds of sessions that were not properly shut down.
   1.590 +    abortedActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
   1.591 +    abortedTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
   1.592 +
   1.593 +    // Startup times in milliseconds.
   1.594 +    main: DAILY_DISCRETE_NUMERIC_FIELD,
   1.595 +    firstPaint: DAILY_DISCRETE_NUMERIC_FIELD,
   1.596 +    sessionRestored: DAILY_DISCRETE_NUMERIC_FIELD,
   1.597 +  },
   1.598 +});
   1.599 +
   1.600 +
   1.601 +/**
   1.602 + * Records information about the current browser session.
   1.603 + *
   1.604 + * A browser session is defined as an application/process lifetime. We
   1.605 + * start a new session when the application starts (essentially when
   1.606 + * this provider is instantiated) and end the session on shutdown.
   1.607 + *
   1.608 + * As the application runs, we record basic information about the
   1.609 + * "activity" of the session. Activity is defined by the presence of
   1.610 + * physical input into the browser (key press, mouse click, touch, etc).
   1.611 + *
   1.612 + * We differentiate between regular sessions and "aborted" sessions. An
   1.613 + * aborted session is one that does not end expectedly. This is often the
   1.614 + * result of a crash. We detect aborted sessions by storing the current
   1.615 + * session separate from completed sessions. We normally move the
   1.616 + * current session to completed sessions on application shutdown. If a
   1.617 + * current session is present on application startup, that means that
   1.618 + * the previous session was aborted.
   1.619 + */
   1.620 +this.SessionsProvider = function () {
   1.621 +  Metrics.Provider.call(this);
   1.622 +};
   1.623 +
   1.624 +SessionsProvider.prototype = Object.freeze({
   1.625 +  __proto__: Metrics.Provider.prototype,
   1.626 +
   1.627 +  name: "org.mozilla.appSessions",
   1.628 +
   1.629 +  measurementTypes: [CurrentSessionMeasurement, PreviousSessionsMeasurement],
   1.630 +
   1.631 +  pullOnly: true,
   1.632 +
   1.633 +  collectConstantData: function () {
   1.634 +    let previous = this.getMeasurement("previous", 3);
   1.635 +
   1.636 +    return this.storage.enqueueTransaction(this._recordAndPruneSessions.bind(this));
   1.637 +  },
   1.638 +
   1.639 +  _recordAndPruneSessions: function () {
   1.640 +    this._log.info("Moving previous sessions from session recorder to storage.");
   1.641 +    let recorder = this.healthReporter.sessionRecorder;
   1.642 +    let sessions = recorder.getPreviousSessions();
   1.643 +    this._log.debug("Found " + Object.keys(sessions).length + " previous sessions.");
   1.644 +
   1.645 +    let daily = this.getMeasurement("previous", 3);
   1.646 +
   1.647 +    // Please note the coupling here between the session recorder and our state.
   1.648 +    // If the pruned index or the current index of the session recorder is ever
   1.649 +    // deleted or reset to 0, our stored state of a later index would mean that
   1.650 +    // new sessions would never be captured by this provider until the session
   1.651 +    // recorder index catches up to our last session ID. This should not happen
   1.652 +    // under normal circumstances, so we don't worry too much about it. We
   1.653 +    // should, however, consider this as part of implementing bug 841561.
   1.654 +    let lastRecordedSession = yield this.getState("lastSession");
   1.655 +    if (lastRecordedSession === null) {
   1.656 +      lastRecordedSession = -1;
   1.657 +    }
   1.658 +    this._log.debug("The last recorded session was #" + lastRecordedSession);
   1.659 +
   1.660 +    for (let [index, session] in Iterator(sessions)) {
   1.661 +      if (index <= lastRecordedSession) {
   1.662 +        this._log.warn("Already recorded session " + index + ". Did the last " +
   1.663 +                       "session crash or have an issue saving the prefs file?");
   1.664 +        continue;
   1.665 +      }
   1.666 +
   1.667 +      let type = session.clean ? "clean" : "aborted";
   1.668 +      let date = session.startDate;
   1.669 +      yield daily.addDailyDiscreteNumeric(type + "ActiveTicks", session.activeTicks, date);
   1.670 +      yield daily.addDailyDiscreteNumeric(type + "TotalTime", session.totalTime, date);
   1.671 +
   1.672 +      for (let field of ["main", "firstPaint", "sessionRestored"]) {
   1.673 +        yield daily.addDailyDiscreteNumeric(field, session[field], date);
   1.674 +      }
   1.675 +
   1.676 +      lastRecordedSession = index;
   1.677 +    }
   1.678 +
   1.679 +    yield this.setState("lastSession", "" + lastRecordedSession);
   1.680 +    recorder.pruneOldSessions(new Date());
   1.681 +  },
   1.682 +});
   1.683 +
   1.684 +/**
   1.685 + * Stores the set of active addons in storage.
   1.686 + *
   1.687 + * We do things a little differently than most other measurements. Because
   1.688 + * addons are difficult to shoehorn into distinct fields, we simply store a
   1.689 + * JSON blob in storage in a text field.
   1.690 + */
   1.691 +function ActiveAddonsMeasurement() {
   1.692 +  Metrics.Measurement.call(this);
   1.693 +
   1.694 +  this._serializers = {};
   1.695 +  this._serializers[this.SERIALIZE_JSON] = {
   1.696 +    singular: this._serializeJSONSingular.bind(this),
   1.697 +    // We don't need a daily serializer because we have none of this data.
   1.698 +  };
   1.699 +}
   1.700 +
   1.701 +ActiveAddonsMeasurement.prototype = Object.freeze({
   1.702 +  __proto__: Metrics.Measurement.prototype,
   1.703 +
   1.704 +  name: "addons",
   1.705 +  version: 2,
   1.706 +
   1.707 +  fields: {
   1.708 +    addons: LAST_TEXT_FIELD,
   1.709 +  },
   1.710 +
   1.711 +  _serializeJSONSingular: function (data) {
   1.712 +    if (!data.has("addons")) {
   1.713 +      this._log.warn("Don't have addons info. Weird.");
   1.714 +      return null;
   1.715 +    }
   1.716 +
   1.717 +    // Exceptions are caught in the caller.
   1.718 +    let result = JSON.parse(data.get("addons")[1]);
   1.719 +    result._v = this.version;
   1.720 +    return result;
   1.721 +  },
   1.722 +});
   1.723 +
   1.724 +/**
   1.725 + * Stores the set of active plugins in storage.
   1.726 + *
   1.727 + * This stores the data in a JSON blob in a text field similar to the
   1.728 + * ActiveAddonsMeasurement.
   1.729 + */
   1.730 +function ActivePluginsMeasurement() {
   1.731 +  Metrics.Measurement.call(this);
   1.732 +
   1.733 +  this._serializers = {};
   1.734 +  this._serializers[this.SERIALIZE_JSON] = {
   1.735 +    singular: this._serializeJSONSingular.bind(this),
   1.736 +    // We don't need a daily serializer because we have none of this data.
   1.737 +  };
   1.738 +}
   1.739 +
   1.740 +ActivePluginsMeasurement.prototype = Object.freeze({
   1.741 +  __proto__: Metrics.Measurement.prototype,
   1.742 +
   1.743 +  name: "plugins",
   1.744 +  version: 1,
   1.745 +
   1.746 +  fields: {
   1.747 +    plugins: LAST_TEXT_FIELD,
   1.748 +  },
   1.749 +
   1.750 +  _serializeJSONSingular: function (data) {
   1.751 +    if (!data.has("plugins")) {
   1.752 +      this._log.warn("Don't have plugins info. Weird.");
   1.753 +      return null;
   1.754 +    }
   1.755 +
   1.756 +    // Exceptions are caught in the caller.
   1.757 +    let result = JSON.parse(data.get("plugins")[1]);
   1.758 +    result._v = this.version;
   1.759 +    return result;
   1.760 +  },
   1.761 +});
   1.762 +
   1.763 +
   1.764 +function AddonCountsMeasurement() {
   1.765 +  Metrics.Measurement.call(this);
   1.766 +}
   1.767 +
   1.768 +AddonCountsMeasurement.prototype = Object.freeze({
   1.769 +  __proto__: Metrics.Measurement.prototype,
   1.770 +
   1.771 +  name: "counts",
   1.772 +  version: 2,
   1.773 +
   1.774 +  fields: {
   1.775 +    theme: DAILY_LAST_NUMERIC_FIELD,
   1.776 +    lwtheme: DAILY_LAST_NUMERIC_FIELD,
   1.777 +    plugin: DAILY_LAST_NUMERIC_FIELD,
   1.778 +    extension: DAILY_LAST_NUMERIC_FIELD,
   1.779 +    service: DAILY_LAST_NUMERIC_FIELD,
   1.780 +  },
   1.781 +});
   1.782 +
   1.783 +
   1.784 +/**
   1.785 + * Legacy version of addons counts before services was added.
   1.786 + */
   1.787 +function AddonCountsMeasurement1() {
   1.788 +  Metrics.Measurement.call(this);
   1.789 +}
   1.790 +
   1.791 +AddonCountsMeasurement1.prototype = Object.freeze({
   1.792 +  __proto__: Metrics.Measurement.prototype,
   1.793 +
   1.794 +  name: "counts",
   1.795 +  version: 1,
   1.796 +
   1.797 +  fields: {
   1.798 +    theme: DAILY_LAST_NUMERIC_FIELD,
   1.799 +    lwtheme: DAILY_LAST_NUMERIC_FIELD,
   1.800 +    plugin: DAILY_LAST_NUMERIC_FIELD,
   1.801 +    extension: DAILY_LAST_NUMERIC_FIELD,
   1.802 +  },
   1.803 +});
   1.804 +
   1.805 +
   1.806 +this.AddonsProvider = function () {
   1.807 +  Metrics.Provider.call(this);
   1.808 +
   1.809 +  this._prefs = new Preferences({defaultBranch: null});
   1.810 +};
   1.811 +
   1.812 +AddonsProvider.prototype = Object.freeze({
   1.813 +  __proto__: Metrics.Provider.prototype,
   1.814 +
   1.815 +  // Whenever these AddonListener callbacks are called, we repopulate
   1.816 +  // and store the set of addons. Note that these events will only fire
   1.817 +  // for restartless add-ons. For actions that require a restart, we
   1.818 +  // will catch the change after restart. The alternative is a lot of
   1.819 +  // state tracking here, which isn't desirable.
   1.820 +  ADDON_LISTENER_CALLBACKS: [
   1.821 +    "onEnabled",
   1.822 +    "onDisabled",
   1.823 +    "onInstalled",
   1.824 +    "onUninstalled",
   1.825 +  ],
   1.826 +
   1.827 +  // Add-on types for which full details are uploaded in the
   1.828 +  // ActiveAddonsMeasurement. All other types are ignored.
   1.829 +  FULL_DETAIL_TYPES: [
   1.830 +    "extension",
   1.831 +    "service",
   1.832 +  ],
   1.833 +
   1.834 +  name: "org.mozilla.addons",
   1.835 +
   1.836 +  measurementTypes: [
   1.837 +    ActiveAddonsMeasurement,
   1.838 +    ActivePluginsMeasurement,
   1.839 +    AddonCountsMeasurement1,
   1.840 +    AddonCountsMeasurement,
   1.841 +  ],
   1.842 +
   1.843 +  postInit: function () {
   1.844 +    let listener = {};
   1.845 +
   1.846 +    for (let method of this.ADDON_LISTENER_CALLBACKS) {
   1.847 +      listener[method] = this._collectAndStoreAddons.bind(this);
   1.848 +    }
   1.849 +
   1.850 +    this._listener = listener;
   1.851 +    AddonManager.addAddonListener(this._listener);
   1.852 +
   1.853 +    return CommonUtils.laterTickResolvingPromise();
   1.854 +  },
   1.855 +
   1.856 +  onShutdown: function () {
   1.857 +    AddonManager.removeAddonListener(this._listener);
   1.858 +    this._listener = null;
   1.859 +
   1.860 +    return CommonUtils.laterTickResolvingPromise();
   1.861 +  },
   1.862 +
   1.863 +  collectConstantData: function () {
   1.864 +    return this._collectAndStoreAddons();
   1.865 +  },
   1.866 +
   1.867 +  _collectAndStoreAddons: function () {
   1.868 +    let deferred = Promise.defer();
   1.869 +
   1.870 +    AddonManager.getAllAddons(function onAllAddons(addons) {
   1.871 +      let data;
   1.872 +      let addonsField;
   1.873 +      let pluginsField;
   1.874 +      try {
   1.875 +        data = this._createDataStructure(addons);
   1.876 +        addonsField = JSON.stringify(data.addons);
   1.877 +        pluginsField = JSON.stringify(data.plugins);
   1.878 +      } catch (ex) {
   1.879 +        this._log.warn("Exception when populating add-ons data structure: " +
   1.880 +                       CommonUtils.exceptionStr(ex));
   1.881 +        deferred.reject(ex);
   1.882 +        return;
   1.883 +      }
   1.884 +
   1.885 +      let now = new Date();
   1.886 +      let addons = this.getMeasurement("addons", 2);
   1.887 +      let plugins = this.getMeasurement("plugins", 1);
   1.888 +      let counts = this.getMeasurement(AddonCountsMeasurement.prototype.name,
   1.889 +                                       AddonCountsMeasurement.prototype.version);
   1.890 +
   1.891 +      this.enqueueStorageOperation(function storageAddons() {
   1.892 +        for (let type in data.counts) {
   1.893 +          try {
   1.894 +            counts.fieldID(type);
   1.895 +          } catch (ex) {
   1.896 +            this._log.warn("Add-on type without field: " + type);
   1.897 +            continue;
   1.898 +          }
   1.899 +
   1.900 +          counts.setDailyLastNumeric(type, data.counts[type], now);
   1.901 +        }
   1.902 +
   1.903 +        return addons.setLastText("addons", addonsField).then(
   1.904 +          function onSuccess() {
   1.905 +            return plugins.setLastText("plugins", pluginsField).then(
   1.906 +              function onSuccess() { deferred.resolve(); },
   1.907 +              function onError(error) { deferred.reject(error); }
   1.908 +            );
   1.909 +          },
   1.910 +          function onError(error) { deferred.reject(error); }
   1.911 +        );
   1.912 +      }.bind(this));
   1.913 +    }.bind(this));
   1.914 +
   1.915 +    return deferred.promise;
   1.916 +  },
   1.917 +
   1.918 +  COPY_ADDON_FIELDS: [
   1.919 +    "userDisabled",
   1.920 +    "appDisabled",
   1.921 +    "name",
   1.922 +    "version",
   1.923 +    "type",
   1.924 +    "scope",
   1.925 +    "description",
   1.926 +    "foreignInstall",
   1.927 +    "hasBinaryComponents",
   1.928 +  ],
   1.929 +
   1.930 +  COPY_PLUGIN_FIELDS: [
   1.931 +    "name",
   1.932 +    "version",
   1.933 +    "description",
   1.934 +    "blocklisted",
   1.935 +    "disabled",
   1.936 +    "clicktoplay",
   1.937 +  ],
   1.938 +
   1.939 +  _createDataStructure: function (addons) {
   1.940 +    let data = {
   1.941 +      addons: {},
   1.942 +      plugins: {},
   1.943 +      counts: {}
   1.944 +    };
   1.945 +
   1.946 +    for (let addon of addons) {
   1.947 +      let type = addon.type;
   1.948 +
   1.949 +      // We count plugins separately below.
   1.950 +      if (addon.type == "plugin")
   1.951 +        continue;
   1.952 +
   1.953 +      data.counts[type] = (data.counts[type] || 0) + 1;
   1.954 +
   1.955 +      if (this.FULL_DETAIL_TYPES.indexOf(addon.type) == -1) {
   1.956 +        continue;
   1.957 +      }
   1.958 +
   1.959 +      let obj = {};
   1.960 +      for (let field of this.COPY_ADDON_FIELDS) {
   1.961 +        obj[field] = addon[field];
   1.962 +      }
   1.963 +
   1.964 +      if (addon.installDate) {
   1.965 +        obj.installDay = this._dateToDays(addon.installDate);
   1.966 +      }
   1.967 +
   1.968 +      if (addon.updateDate) {
   1.969 +        obj.updateDay = this._dateToDays(addon.updateDate);
   1.970 +      }
   1.971 +
   1.972 +      data.addons[addon.id] = obj;
   1.973 +    }
   1.974 +
   1.975 +    let pluginTags = Cc["@mozilla.org/plugin/host;1"].
   1.976 +                       getService(Ci.nsIPluginHost).
   1.977 +                       getPluginTags({});
   1.978 +
   1.979 +    for (let tag of pluginTags) {
   1.980 +      let obj = {
   1.981 +        mimeTypes: tag.getMimeTypes({}),
   1.982 +      };
   1.983 +
   1.984 +      for (let field of this.COPY_PLUGIN_FIELDS) {
   1.985 +        obj[field] = tag[field];
   1.986 +      }
   1.987 +
   1.988 +      // Plugins need to have a filename and a name, so this can't be empty.
   1.989 +      let id = tag.filename + ":" + tag.name + ":" + tag.version + ":"
   1.990 +               + tag.description;
   1.991 +      data.plugins[id] = obj;
   1.992 +    }
   1.993 +
   1.994 +    data.counts["plugin"] = pluginTags.length;
   1.995 +
   1.996 +    return data;
   1.997 +  },
   1.998 +});
   1.999 +
  1.1000 +#ifdef MOZ_CRASHREPORTER
  1.1001 +
  1.1002 +function DailyCrashesMeasurement1() {
  1.1003 +  Metrics.Measurement.call(this);
  1.1004 +}
  1.1005 +
  1.1006 +DailyCrashesMeasurement1.prototype = Object.freeze({
  1.1007 +  __proto__: Metrics.Measurement.prototype,
  1.1008 +
  1.1009 +  name: "crashes",
  1.1010 +  version: 1,
  1.1011 +
  1.1012 +  fields: {
  1.1013 +    pending: DAILY_COUNTER_FIELD,
  1.1014 +    submitted: DAILY_COUNTER_FIELD,
  1.1015 +  },
  1.1016 +});
  1.1017 +
  1.1018 +function DailyCrashesMeasurement2() {
  1.1019 +  Metrics.Measurement.call(this);
  1.1020 +}
  1.1021 +
  1.1022 +DailyCrashesMeasurement2.prototype = Object.freeze({
  1.1023 +  __proto__: Metrics.Measurement.prototype,
  1.1024 +
  1.1025 +  name: "crashes",
  1.1026 +  version: 2,
  1.1027 +
  1.1028 +  fields: {
  1.1029 +    mainCrash: DAILY_LAST_NUMERIC_FIELD,
  1.1030 +  },
  1.1031 +});
  1.1032 +
  1.1033 +this.CrashesProvider = function () {
  1.1034 +  Metrics.Provider.call(this);
  1.1035 +
  1.1036 +  // So we can unit test.
  1.1037 +  this._manager = Services.crashmanager;
  1.1038 +};
  1.1039 +
  1.1040 +CrashesProvider.prototype = Object.freeze({
  1.1041 +  __proto__: Metrics.Provider.prototype,
  1.1042 +
  1.1043 +  name: "org.mozilla.crashes",
  1.1044 +
  1.1045 +  measurementTypes: [
  1.1046 +    DailyCrashesMeasurement1,
  1.1047 +    DailyCrashesMeasurement2,
  1.1048 +  ],
  1.1049 +
  1.1050 +  pullOnly: true,
  1.1051 +
  1.1052 +  collectDailyData: function () {
  1.1053 +    return this.storage.enqueueTransaction(this._populateCrashCounts.bind(this));
  1.1054 +  },
  1.1055 +
  1.1056 +  _populateCrashCounts: function () {
  1.1057 +    this._log.info("Grabbing crash counts from crash manager.");
  1.1058 +    let crashCounts = yield this._manager.getCrashCountsByDay();
  1.1059 +    let fields = {
  1.1060 +      "main-crash": "mainCrash",
  1.1061 +    };
  1.1062 +
  1.1063 +    let m = this.getMeasurement("crashes", 2);
  1.1064 +
  1.1065 +    for (let [day, types] of crashCounts) {
  1.1066 +      let date = Metrics.daysToDate(day);
  1.1067 +      for (let [type, count] of types) {
  1.1068 +        if (!(type in fields)) {
  1.1069 +          this._log.warn("Unknown crash type encountered: " + type);
  1.1070 +          continue;
  1.1071 +        }
  1.1072 +
  1.1073 +        yield m.setDailyLastNumeric(fields[type], count, date);
  1.1074 +      }
  1.1075 +    }
  1.1076 +  },
  1.1077 +});
  1.1078 +
  1.1079 +#endif
  1.1080 +
  1.1081 +
  1.1082 +/**
  1.1083 + * Holds basic statistics about the Places database.
  1.1084 + */
  1.1085 +function PlacesMeasurement() {
  1.1086 +  Metrics.Measurement.call(this);
  1.1087 +}
  1.1088 +
  1.1089 +PlacesMeasurement.prototype = Object.freeze({
  1.1090 +  __proto__: Metrics.Measurement.prototype,
  1.1091 +
  1.1092 +  name: "places",
  1.1093 +  version: 1,
  1.1094 +
  1.1095 +  fields: {
  1.1096 +    pages: DAILY_LAST_NUMERIC_FIELD,
  1.1097 +    bookmarks: DAILY_LAST_NUMERIC_FIELD,
  1.1098 +  },
  1.1099 +});
  1.1100 +
  1.1101 +
  1.1102 +/**
  1.1103 + * Collects information about Places.
  1.1104 + */
  1.1105 +this.PlacesProvider = function () {
  1.1106 +  Metrics.Provider.call(this);
  1.1107 +};
  1.1108 +
  1.1109 +PlacesProvider.prototype = Object.freeze({
  1.1110 +  __proto__: Metrics.Provider.prototype,
  1.1111 +
  1.1112 +  name: "org.mozilla.places",
  1.1113 +
  1.1114 +  measurementTypes: [PlacesMeasurement],
  1.1115 +
  1.1116 +  collectDailyData: function () {
  1.1117 +    return this.storage.enqueueTransaction(this._collectData.bind(this));
  1.1118 +  },
  1.1119 +
  1.1120 +  _collectData: function () {
  1.1121 +    let now = new Date();
  1.1122 +    let data = yield this._getDailyValues();
  1.1123 +
  1.1124 +    let m = this.getMeasurement("places", 1);
  1.1125 +
  1.1126 +    yield m.setDailyLastNumeric("pages", data.PLACES_PAGES_COUNT);
  1.1127 +    yield m.setDailyLastNumeric("bookmarks", data.PLACES_BOOKMARKS_COUNT);
  1.1128 +  },
  1.1129 +
  1.1130 +  _getDailyValues: function () {
  1.1131 +    let deferred = Promise.defer();
  1.1132 +
  1.1133 +    PlacesDBUtils.telemetry(null, function onResult(data) {
  1.1134 +      deferred.resolve(data);
  1.1135 +    });
  1.1136 +
  1.1137 +    return deferred.promise;
  1.1138 +  },
  1.1139 +});
  1.1140 +
  1.1141 +function SearchCountMeasurement1() {
  1.1142 +  Metrics.Measurement.call(this);
  1.1143 +}
  1.1144 +
  1.1145 +SearchCountMeasurement1.prototype = Object.freeze({
  1.1146 +  __proto__: Metrics.Measurement.prototype,
  1.1147 +
  1.1148 +  name: "counts",
  1.1149 +  version: 1,
  1.1150 +
  1.1151 +  // We only record searches for search engines that have partner agreements
  1.1152 +  // with Mozilla.
  1.1153 +  fields: {
  1.1154 +    "amazon.com.abouthome": DAILY_COUNTER_FIELD,
  1.1155 +    "amazon.com.contextmenu": DAILY_COUNTER_FIELD,
  1.1156 +    "amazon.com.searchbar": DAILY_COUNTER_FIELD,
  1.1157 +    "amazon.com.urlbar": DAILY_COUNTER_FIELD,
  1.1158 +    "bing.abouthome": DAILY_COUNTER_FIELD,
  1.1159 +    "bing.contextmenu": DAILY_COUNTER_FIELD,
  1.1160 +    "bing.searchbar": DAILY_COUNTER_FIELD,
  1.1161 +    "bing.urlbar": DAILY_COUNTER_FIELD,
  1.1162 +    "google.abouthome": DAILY_COUNTER_FIELD,
  1.1163 +    "google.contextmenu": DAILY_COUNTER_FIELD,
  1.1164 +    "google.searchbar": DAILY_COUNTER_FIELD,
  1.1165 +    "google.urlbar": DAILY_COUNTER_FIELD,
  1.1166 +    "yahoo.abouthome": DAILY_COUNTER_FIELD,
  1.1167 +    "yahoo.contextmenu": DAILY_COUNTER_FIELD,
  1.1168 +    "yahoo.searchbar": DAILY_COUNTER_FIELD,
  1.1169 +    "yahoo.urlbar": DAILY_COUNTER_FIELD,
  1.1170 +    "other.abouthome": DAILY_COUNTER_FIELD,
  1.1171 +    "other.contextmenu": DAILY_COUNTER_FIELD,
  1.1172 +    "other.searchbar": DAILY_COUNTER_FIELD,
  1.1173 +    "other.urlbar": DAILY_COUNTER_FIELD,
  1.1174 +  },
  1.1175 +});
  1.1176 +
  1.1177 +/**
  1.1178 + * Records search counts per day per engine and where search initiated.
  1.1179 + *
  1.1180 + * We want to record granular details for individual locale-specific search
  1.1181 + * providers, but only if they're Mozilla partners. In order to do this, we
  1.1182 + * track the nsISearchEngine identifier, which denotes shipped search engines,
  1.1183 + * and intersect those with our partner list.
  1.1184 + *
  1.1185 + * We don't use the search engine name directly, because it is shared across
  1.1186 + * locales; e.g., eBay-de and eBay both share the name "eBay".
  1.1187 + */
  1.1188 +function SearchCountMeasurementBase() {
  1.1189 +  this._fieldSpecs = {};
  1.1190 +  Metrics.Measurement.call(this);
  1.1191 +}
  1.1192 +
  1.1193 +SearchCountMeasurementBase.prototype = Object.freeze({
  1.1194 +  __proto__: Metrics.Measurement.prototype,
  1.1195 +
  1.1196 +
  1.1197 +  // Our fields are dynamic.
  1.1198 +  get fields() {
  1.1199 +    return this._fieldSpecs;
  1.1200 +  },
  1.1201 +
  1.1202 +  /**
  1.1203 +   * Override the default behavior: serializers should include every counter
  1.1204 +   * field from the DB, even if we don't currently have it registered.
  1.1205 +   *
  1.1206 +   * Do this so we don't have to register several hundred fields to match
  1.1207 +   * various Firefox locales.
  1.1208 +   *
  1.1209 +   * We use the "provider.type" syntax as a rudimentary check for validity.
  1.1210 +   *
  1.1211 +   * We trust that measurement versioning is sufficient to exclude old provider
  1.1212 +   * data.
  1.1213 +   */
  1.1214 +  shouldIncludeField: function (name) {
  1.1215 +    return name.contains(".");
  1.1216 +  },
  1.1217 +
  1.1218 +  /**
  1.1219 +   * The measurement type mechanism doesn't introspect the DB. Override it
  1.1220 +   * so that we can assume all unknown fields are counters.
  1.1221 +   */
  1.1222 +  fieldType: function (name) {
  1.1223 +    if (name in this.fields) {
  1.1224 +      return this.fields[name].type;
  1.1225 +    }
  1.1226 +
  1.1227 +    // Default to a counter.
  1.1228 +    return Metrics.Storage.FIELD_DAILY_COUNTER;
  1.1229 +  },
  1.1230 +
  1.1231 +  SOURCES: [
  1.1232 +    "abouthome",
  1.1233 +    "contextmenu",
  1.1234 +    "newtab",
  1.1235 +    "searchbar",
  1.1236 +    "urlbar",
  1.1237 +  ],
  1.1238 +});
  1.1239 +
  1.1240 +function SearchCountMeasurement2() {
  1.1241 +  SearchCountMeasurementBase.call(this);
  1.1242 +}
  1.1243 +
  1.1244 +SearchCountMeasurement2.prototype = Object.freeze({
  1.1245 +  __proto__: SearchCountMeasurementBase.prototype,
  1.1246 +  name: "counts",
  1.1247 +  version: 2,
  1.1248 +});
  1.1249 +
  1.1250 +function SearchCountMeasurement3() {
  1.1251 +  SearchCountMeasurementBase.call(this);
  1.1252 +}
  1.1253 +
  1.1254 +SearchCountMeasurement3.prototype = Object.freeze({
  1.1255 +  __proto__: SearchCountMeasurementBase.prototype,
  1.1256 +  name: "counts",
  1.1257 +  version: 3,
  1.1258 +
  1.1259 +  getEngines: function () {
  1.1260 +    return Services.search.getEngines();
  1.1261 +  },
  1.1262 +
  1.1263 +  getEngineID: function (engine) {
  1.1264 +    if (!engine) {
  1.1265 +      return "other";
  1.1266 +    }
  1.1267 +    if (engine.identifier) {
  1.1268 +      return engine.identifier;
  1.1269 +    }
  1.1270 +    return "other-" + engine.name;
  1.1271 +  },
  1.1272 +});
  1.1273 +
  1.1274 +function SearchEnginesMeasurement1() {
  1.1275 +  Metrics.Measurement.call(this);
  1.1276 +}
  1.1277 +
  1.1278 +SearchEnginesMeasurement1.prototype = Object.freeze({
  1.1279 +  __proto__: Metrics.Measurement.prototype,
  1.1280 +
  1.1281 +  name: "engines",
  1.1282 +  version: 1,
  1.1283 +
  1.1284 +  fields: {
  1.1285 +    default: DAILY_LAST_TEXT_FIELD,
  1.1286 +  },
  1.1287 +});
  1.1288 +
  1.1289 +this.SearchesProvider = function () {
  1.1290 +  Metrics.Provider.call(this);
  1.1291 +
  1.1292 +  this._prefs = new Preferences({defaultBranch: null});
  1.1293 +};
  1.1294 +
  1.1295 +this.SearchesProvider.prototype = Object.freeze({
  1.1296 +  __proto__: Metrics.Provider.prototype,
  1.1297 +
  1.1298 +  name: "org.mozilla.searches",
  1.1299 +  measurementTypes: [
  1.1300 +    SearchCountMeasurement1,
  1.1301 +    SearchCountMeasurement2,
  1.1302 +    SearchCountMeasurement3,
  1.1303 +    SearchEnginesMeasurement1,
  1.1304 +  ],
  1.1305 +
  1.1306 +  /**
  1.1307 +   * Initialize the search service before our measurements are touched.
  1.1308 +   */
  1.1309 +  preInit: function (storage) {
  1.1310 +    // Initialize search service.
  1.1311 +    let deferred = Promise.defer();
  1.1312 +    Services.search.init(function onInitComplete () {
  1.1313 +      deferred.resolve();
  1.1314 +    });
  1.1315 +    return deferred.promise;
  1.1316 +  },
  1.1317 +
  1.1318 +  collectDailyData: function () {
  1.1319 +    return this.storage.enqueueTransaction(function getDaily() {
  1.1320 +      // We currently only record this if Telemetry is enabled.
  1.1321 +      if (!isTelemetryEnabled(this._prefs)) {
  1.1322 +        return;
  1.1323 +      }
  1.1324 +
  1.1325 +      let m = this.getMeasurement(SearchEnginesMeasurement1.prototype.name,
  1.1326 +                                  SearchEnginesMeasurement1.prototype.version);
  1.1327 +
  1.1328 +      let engine;
  1.1329 +      try {
  1.1330 +        engine = Services.search.defaultEngine;
  1.1331 +      } catch (e) {}
  1.1332 +      let name;
  1.1333 +
  1.1334 +      if (!engine) {
  1.1335 +        name = "NONE";
  1.1336 +      } else if (engine.identifier) {
  1.1337 +        name = engine.identifier;
  1.1338 +      } else if (engine.name) {
  1.1339 +        name = "other-" + engine.name;
  1.1340 +      } else {
  1.1341 +        name = "UNDEFINED";
  1.1342 +      }
  1.1343 +
  1.1344 +      yield m.setDailyLastText("default", name);
  1.1345 +    }.bind(this));
  1.1346 +  },
  1.1347 +
  1.1348 +  /**
  1.1349 +   * Record that a search occurred.
  1.1350 +   *
  1.1351 +   * @param engine
  1.1352 +   *        (nsISearchEngine) The search engine used.
  1.1353 +   * @param source
  1.1354 +   *        (string) Where the search was initiated from. Must be one of the
  1.1355 +   *        SearchCountMeasurement2.SOURCES values.
  1.1356 +   *
  1.1357 +   * @return Promise<>
  1.1358 +   *         The promise is resolved when the storage operation completes.
  1.1359 +   */
  1.1360 +  recordSearch: function (engine, source) {
  1.1361 +    let m = this.getMeasurement("counts", 3);
  1.1362 +
  1.1363 +    if (m.SOURCES.indexOf(source) == -1) {
  1.1364 +      throw new Error("Unknown source for search: " + source);
  1.1365 +    }
  1.1366 +
  1.1367 +    let field = m.getEngineID(engine) + "." + source;
  1.1368 +    if (this.storage.hasFieldFromMeasurement(m.id, field,
  1.1369 +                                             this.storage.FIELD_DAILY_COUNTER)) {
  1.1370 +      let fieldID = this.storage.fieldIDFromMeasurement(m.id, field);
  1.1371 +      return this.enqueueStorageOperation(function recordSearchKnownField() {
  1.1372 +        return this.storage.incrementDailyCounterFromFieldID(fieldID);
  1.1373 +      }.bind(this));
  1.1374 +    }
  1.1375 +
  1.1376 +    // Otherwise, we first need to create the field.
  1.1377 +    return this.enqueueStorageOperation(function recordFieldAndSearch() {
  1.1378 +      // This function has to return a promise.
  1.1379 +      return Task.spawn(function () {
  1.1380 +        let fieldID = yield this.storage.registerField(m.id, field,
  1.1381 +                                                       this.storage.FIELD_DAILY_COUNTER);
  1.1382 +        yield this.storage.incrementDailyCounterFromFieldID(fieldID);
  1.1383 +      }.bind(this));
  1.1384 +    }.bind(this));
  1.1385 +  },
  1.1386 +});
  1.1387 +
  1.1388 +function HealthReportSubmissionMeasurement1() {
  1.1389 +  Metrics.Measurement.call(this);
  1.1390 +}
  1.1391 +
  1.1392 +HealthReportSubmissionMeasurement1.prototype = Object.freeze({
  1.1393 +  __proto__: Metrics.Measurement.prototype,
  1.1394 +
  1.1395 +  name: "submissions",
  1.1396 +  version: 1,
  1.1397 +
  1.1398 +  fields: {
  1.1399 +    firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
  1.1400 +    continuationUploadAttempt: DAILY_COUNTER_FIELD,
  1.1401 +    uploadSuccess: DAILY_COUNTER_FIELD,
  1.1402 +    uploadTransportFailure: DAILY_COUNTER_FIELD,
  1.1403 +    uploadServerFailure: DAILY_COUNTER_FIELD,
  1.1404 +    uploadClientFailure: DAILY_COUNTER_FIELD,
  1.1405 +  },
  1.1406 +});
  1.1407 +
  1.1408 +function HealthReportSubmissionMeasurement2() {
  1.1409 +  Metrics.Measurement.call(this);
  1.1410 +}
  1.1411 +
  1.1412 +HealthReportSubmissionMeasurement2.prototype = Object.freeze({
  1.1413 +  __proto__: Metrics.Measurement.prototype,
  1.1414 +
  1.1415 +  name: "submissions",
  1.1416 +  version: 2,
  1.1417 +
  1.1418 +  fields: {
  1.1419 +    firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
  1.1420 +    continuationUploadAttempt: DAILY_COUNTER_FIELD,
  1.1421 +    uploadSuccess: DAILY_COUNTER_FIELD,
  1.1422 +    uploadTransportFailure: DAILY_COUNTER_FIELD,
  1.1423 +    uploadServerFailure: DAILY_COUNTER_FIELD,
  1.1424 +    uploadClientFailure: DAILY_COUNTER_FIELD,
  1.1425 +    uploadAlreadyInProgress: DAILY_COUNTER_FIELD,
  1.1426 +  },
  1.1427 +});
  1.1428 +
  1.1429 +this.HealthReportProvider = function () {
  1.1430 +  Metrics.Provider.call(this);
  1.1431 +}
  1.1432 +
  1.1433 +HealthReportProvider.prototype = Object.freeze({
  1.1434 +  __proto__: Metrics.Provider.prototype,
  1.1435 +
  1.1436 +  name: "org.mozilla.healthreport",
  1.1437 +
  1.1438 +  measurementTypes: [
  1.1439 +    HealthReportSubmissionMeasurement1,
  1.1440 +    HealthReportSubmissionMeasurement2,
  1.1441 +  ],
  1.1442 +
  1.1443 +  recordEvent: function (event, date=new Date()) {
  1.1444 +    let m = this.getMeasurement("submissions", 2);
  1.1445 +    return this.enqueueStorageOperation(function recordCounter() {
  1.1446 +      return m.incrementDailyCounter(event, date);
  1.1447 +    });
  1.1448 +  },
  1.1449 +});

mercurial