Merge mozilla-b2g34 to 2.1s. a=merge
[gecko.git] / services / healthreport / providers.jsm
blob30e1bb6ceb563e95cfff5ce8829b1f7814b649bc
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 #ifndef MERGED_COMPARTMENT
17 "use strict";
19 this.EXPORTED_SYMBOLS = [
20   "AddonsProvider",
21   "AppInfoProvider",
22 #ifdef MOZ_CRASHREPORTER
23   "CrashesProvider",
24 #endif
25   "HealthReportProvider",
26   "HotfixProvider",
27   "PlacesProvider",
28   "SearchesProvider",
29   "SessionsProvider",
30   "SysInfoProvider",
33 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
35 Cu.import("resource://gre/modules/Metrics.jsm");
37 #endif
39 Cu.import("resource://gre/modules/Promise.jsm");
40 Cu.import("resource://gre/modules/osfile.jsm");
41 Cu.import("resource://gre/modules/Preferences.jsm");
42 Cu.import("resource://gre/modules/Services.jsm");
43 Cu.import("resource://gre/modules/Task.jsm");
44 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
45 Cu.import("resource://services-common/utils.js");
47 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
48                                   "resource://gre/modules/AddonManager.jsm");
49 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
50                                   "resource://gre/modules/UpdateChannel.jsm");
51 XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils",
52                                   "resource://gre/modules/PlacesDBUtils.jsm");
55 const LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_LAST_NUMERIC};
56 const LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_LAST_TEXT};
57 const DAILY_DISCRETE_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC};
58 const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
59 const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
60 const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
62 const TELEMETRY_PREF = "toolkit.telemetry.enabled";
64 function isTelemetryEnabled(prefs) {
65   return prefs.get(TELEMETRY_PREF, false);
68 /**
69  * Represents basic application state.
70  *
71  * This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra
72  * pieces thrown in.
73  */
74 function AppInfoMeasurement() {
75   Metrics.Measurement.call(this);
78 AppInfoMeasurement.prototype = Object.freeze({
79   __proto__: Metrics.Measurement.prototype,
81   name: "appinfo",
82   version: 2,
84   fields: {
85     vendor: LAST_TEXT_FIELD,
86     name: LAST_TEXT_FIELD,
87     id: LAST_TEXT_FIELD,
88     version: LAST_TEXT_FIELD,
89     appBuildID: LAST_TEXT_FIELD,
90     platformVersion: LAST_TEXT_FIELD,
91     platformBuildID: LAST_TEXT_FIELD,
92     os: LAST_TEXT_FIELD,
93     xpcomabi: LAST_TEXT_FIELD,
94     updateChannel: LAST_TEXT_FIELD,
95     distributionID: LAST_TEXT_FIELD,
96     distributionVersion: LAST_TEXT_FIELD,
97     hotfixVersion: LAST_TEXT_FIELD,
98     locale: LAST_TEXT_FIELD,
99     isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
100     isTelemetryEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
101     isBlocklistEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
102   },
106  * Legacy version of app info before Telemetry was added.
108  * The "last" fields have all been removed. We only report the longitudinal
109  * field.
110  */
111 function AppInfoMeasurement1() {
112   Metrics.Measurement.call(this);
115 AppInfoMeasurement1.prototype = Object.freeze({
116   __proto__: Metrics.Measurement.prototype,
118   name: "appinfo",
119   version: 1,
121   fields: {
122     isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
123   },
127 function AppVersionMeasurement1() {
128   Metrics.Measurement.call(this);
131 AppVersionMeasurement1.prototype = Object.freeze({
132   __proto__: Metrics.Measurement.prototype,
134   name: "versions",
135   version: 1,
137   fields: {
138     version: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
139   },
142 // Version 2 added the build ID.
143 function AppVersionMeasurement2() {
144   Metrics.Measurement.call(this);
147 AppVersionMeasurement2.prototype = Object.freeze({
148   __proto__: Metrics.Measurement.prototype,
150   name: "versions",
151   version: 2,
153   fields: {
154     appVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
155     platformVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
156     appBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
157     platformBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
158   },
162  * Holds data on the application update functionality.
163  */
164 function AppUpdateMeasurement1() {
165   Metrics.Measurement.call(this);
168 AppUpdateMeasurement1.prototype = Object.freeze({
169   __proto__: Metrics.Measurement.prototype,
171   name: "update",
172   version: 1,
174   fields: {
175     enabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
176     autoDownload: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
177   },
180 this.AppInfoProvider = function AppInfoProvider() {
181   Metrics.Provider.call(this);
183   this._prefs = new Preferences({defaultBranch: null});
185 AppInfoProvider.prototype = Object.freeze({
186   __proto__: Metrics.Provider.prototype,
188   name: "org.mozilla.appInfo",
190   measurementTypes: [
191     AppInfoMeasurement,
192     AppInfoMeasurement1,
193     AppUpdateMeasurement1,
194     AppVersionMeasurement1,
195     AppVersionMeasurement2,
196   ],
198   pullOnly: true,
200   appInfoFields: {
201     // From nsIXULAppInfo.
202     vendor: "vendor",
203     name: "name",
204     id: "ID",
205     version: "version",
206     appBuildID: "appBuildID",
207     platformVersion: "platformVersion",
208     platformBuildID: "platformBuildID",
210     // From nsIXULRuntime.
211     os: "OS",
212     xpcomabi: "XPCOMABI",
213   },
215   postInit: function () {
216     return Task.spawn(this._postInit.bind(this));
217   },
219   _postInit: function () {
220     let recordEmptyAppInfo = function () {
221       this._setCurrentAppVersion("");
222       this._setCurrentPlatformVersion("");
223       this._setCurrentAppBuildID("");
224       return this._setCurrentPlatformBuildID("");
225     }.bind(this);
227     // Services.appInfo should always be defined for any reasonably behaving
228     // Gecko app. If it isn't, we insert a empty string sentinel value.
229     let ai;
230     try {
231       ai = Services.appinfo;
232     } catch (ex) {
233       this._log.error("Could not obtain Services.appinfo: " +
234                      CommonUtils.exceptionStr(ex));
235       yield recordEmptyAppInfo();
236       return;
237     }
239     if (!ai) {
240       this._log.error("Services.appinfo is unavailable.");
241       yield recordEmptyAppInfo();
242       return;
243     }
245     let currentAppVersion = ai.version;
246     let currentPlatformVersion = ai.platformVersion;
247     let currentAppBuildID = ai.appBuildID;
248     let currentPlatformBuildID = ai.platformBuildID;
250     // State's name doesn't contain "app" for historical compatibility.
251     let lastAppVersion = yield this.getState("lastVersion");
252     let lastPlatformVersion = yield this.getState("lastPlatformVersion");
253     let lastAppBuildID = yield this.getState("lastAppBuildID");
254     let lastPlatformBuildID = yield this.getState("lastPlatformBuildID");
256     if (currentAppVersion != lastAppVersion) {
257       yield this._setCurrentAppVersion(currentAppVersion);
258     }
260     if (currentPlatformVersion != lastPlatformVersion) {
261       yield this._setCurrentPlatformVersion(currentPlatformVersion);
262     }
264     if (currentAppBuildID != lastAppBuildID) {
265       yield this._setCurrentAppBuildID(currentAppBuildID);
266     }
268     if (currentPlatformBuildID != lastPlatformBuildID) {
269       yield this._setCurrentPlatformBuildID(currentPlatformBuildID);
270     }
271   },
273   _setCurrentAppVersion: function (version) {
274     this._log.info("Recording new application version: " + version);
275     let m = this.getMeasurement("versions", 2);
276     m.addDailyDiscreteText("appVersion", version);
278     // "app" not encoded in key for historical compatibility.
279     return this.setState("lastVersion", version);
280   },
282   _setCurrentPlatformVersion: function (version) {
283     this._log.info("Recording new platform version: " + version);
284     let m = this.getMeasurement("versions", 2);
285     m.addDailyDiscreteText("platformVersion", version);
286     return this.setState("lastPlatformVersion", version);
287   },
289   _setCurrentAppBuildID: function (build) {
290     this._log.info("Recording new application build ID: " + build);
291     let m = this.getMeasurement("versions", 2);
292     m.addDailyDiscreteText("appBuildID", build);
293     return this.setState("lastAppBuildID", build);
294   },
296   _setCurrentPlatformBuildID: function (build) {
297     this._log.info("Recording new platform build ID: " + build);
298     let m = this.getMeasurement("versions", 2);
299     m.addDailyDiscreteText("platformBuildID", build);
300     return this.setState("lastPlatformBuildID", build);
301   },
304   collectConstantData: function () {
305     return this.storage.enqueueTransaction(this._populateConstants.bind(this));
306   },
308   _populateConstants: function () {
309     let m = this.getMeasurement(AppInfoMeasurement.prototype.name,
310                                 AppInfoMeasurement.prototype.version);
312     let ai;
313     try {
314       ai = Services.appinfo;
315     } catch (ex) {
316       this._log.warn("Could not obtain Services.appinfo: " +
317                      CommonUtils.exceptionStr(ex));
318       throw ex;
319     }
321     if (!ai) {
322       this._log.warn("Services.appinfo is unavailable.");
323       throw ex;
324     }
326     for (let [k, v] in Iterator(this.appInfoFields)) {
327       try {
328         yield m.setLastText(k, ai[v]);
329       } catch (ex) {
330         this._log.warn("Error obtaining Services.appinfo." + v);
331       }
332     }
334     try {
335       yield m.setLastText("updateChannel", UpdateChannel.get());
336     } catch (ex) {
337       this._log.warn("Could not obtain update channel: " +
338                      CommonUtils.exceptionStr(ex));
339     }
341     yield m.setLastText("distributionID", this._prefs.get("distribution.id", ""));
342     yield m.setLastText("distributionVersion", this._prefs.get("distribution.version", ""));
343     yield m.setLastText("hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
345     try {
346       let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
347                      .getService(Ci.nsIXULChromeRegistry)
348                      .getSelectedLocale("global");
349       yield m.setLastText("locale", locale);
350     } catch (ex) {
351       this._log.warn("Could not obtain application locale: " +
352                      CommonUtils.exceptionStr(ex));
353     }
355     // FUTURE this should be retrieved periodically or at upload time.
356     yield this._recordIsTelemetryEnabled(m);
357     yield this._recordIsBlocklistEnabled(m);
358     yield this._recordDefaultBrowser(m);
359   },
361   _recordIsTelemetryEnabled: function (m) {
362     let enabled = isTelemetryEnabled(this._prefs);
363     this._log.debug("Recording telemetry enabled (" + TELEMETRY_PREF + "): " + enabled);
364     yield m.setDailyLastNumeric("isTelemetryEnabled", enabled ? 1 : 0);
365   },
367   _recordIsBlocklistEnabled: function (m) {
368     let enabled = this._prefs.get("extensions.blocklist.enabled", false);
369     this._log.debug("Recording blocklist enabled: " + enabled);
370     yield m.setDailyLastNumeric("isBlocklistEnabled", enabled ? 1 : 0);
371   },
373   _recordDefaultBrowser: function (m) {
374     let shellService;
375     try {
376       shellService = Cc["@mozilla.org/browser/shell-service;1"]
377                        .getService(Ci.nsIShellService);
378     } catch (ex) {
379       this._log.warn("Could not obtain shell service: " +
380                      CommonUtils.exceptionStr(ex));
381     }
383     let isDefault = -1;
385     if (shellService) {
386       try {
387         // This uses the same set of flags used by the pref pane.
388         isDefault = shellService.isDefaultBrowser(false, true) ? 1 : 0;
389       } catch (ex) {
390         this._log.warn("Could not determine if default browser: " +
391                        CommonUtils.exceptionStr(ex));
392       }
393     }
395     return m.setDailyLastNumeric("isDefaultBrowser", isDefault);
396   },
398   collectDailyData: function () {
399     return this.storage.enqueueTransaction(function getDaily() {
400       let m = this.getMeasurement(AppUpdateMeasurement1.prototype.name,
401                                   AppUpdateMeasurement1.prototype.version);
403       let enabled = this._prefs.get("app.update.enabled", false);
404       yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0);
406       let auto = this._prefs.get("app.update.auto", false);
407       yield m.setDailyLastNumeric("autoDownload", auto ? 1 : 0);
408     }.bind(this));
409   },
413 function SysInfoMeasurement() {
414   Metrics.Measurement.call(this);
417 SysInfoMeasurement.prototype = Object.freeze({
418   __proto__: Metrics.Measurement.prototype,
420   name: "sysinfo",
421   version: 2,
423   fields: {
424     cpuCount: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
425     memoryMB: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
426     manufacturer: LAST_TEXT_FIELD,
427     device: LAST_TEXT_FIELD,
428     hardware: LAST_TEXT_FIELD,
429     name: LAST_TEXT_FIELD,
430     version: LAST_TEXT_FIELD,
431     architecture: LAST_TEXT_FIELD,
432     isWow64: LAST_NUMERIC_FIELD,
433   },
437 this.SysInfoProvider = function SysInfoProvider() {
438   Metrics.Provider.call(this);
441 SysInfoProvider.prototype = Object.freeze({
442   __proto__: Metrics.Provider.prototype,
444   name: "org.mozilla.sysinfo",
446   measurementTypes: [SysInfoMeasurement],
448   pullOnly: true,
450   sysInfoFields: {
451     cpucount: "cpuCount",
452     memsize: "memoryMB",
453     manufacturer: "manufacturer",
454     device: "device",
455     hardware: "hardware",
456     name: "name",
457     version: "version",
458     arch: "architecture",
459     isWow64: "isWow64",
460   },
462   collectConstantData: function () {
463     return this.storage.enqueueTransaction(this._populateConstants.bind(this));
464   },
466   _populateConstants: function () {
467     let m = this.getMeasurement(SysInfoMeasurement.prototype.name,
468                                 SysInfoMeasurement.prototype.version);
470     let si = Cc["@mozilla.org/system-info;1"]
471                .getService(Ci.nsIPropertyBag2);
473     for (let [k, v] in Iterator(this.sysInfoFields)) {
474       try {
475         if (!si.hasKey(k)) {
476           this._log.debug("Property not available: " + k);
477           continue;
478         }
480         let value = si.getProperty(k);
481         let method = "setLastText";
483         if (["cpucount", "memsize"].indexOf(k) != -1) {
484           let converted = parseInt(value, 10);
485           if (Number.isNaN(converted)) {
486             continue;
487           }
489           value = converted;
490           method = "setLastNumeric";
491         }
493         switch (k) {
494           case "memsize":
495             // Round memory to mebibytes.
496             value = Math.round(value / 1048576);
497             break;
498           case "isWow64":
499             // Property is only present on Windows. hasKey() skipping from
500             // above ensures undefined or null doesn't creep in here.
501             value = value ? 1 : 0;
502             method = "setLastNumeric";
503             break;
504         }
506         yield m[method](v, value);
507       } catch (ex) {
508         this._log.warn("Error obtaining system info field: " + k + " " +
509                        CommonUtils.exceptionStr(ex));
510       }
511     }
512   },
517  * Holds information about the current/active session.
519  * The fields within the current session are moved to daily session fields when
520  * the application is shut down.
522  * This measurement is backed by the SessionRecorder, not the database.
523  */
524 function CurrentSessionMeasurement() {
525   Metrics.Measurement.call(this);
528 CurrentSessionMeasurement.prototype = Object.freeze({
529   __proto__: Metrics.Measurement.prototype,
531   name: "current",
532   version: 3,
534   // Storage is in preferences.
535   fields: {},
537   /**
538    * All data is stored in prefs, so we have a custom implementation.
539    */
540   getValues: function () {
541     let sessions = this.provider.healthReporter.sessionRecorder;
543     let fields = new Map();
544     let now = new Date();
545     fields.set("startDay", [now, Metrics.dateToDays(sessions.startDate)]);
546     fields.set("activeTicks", [now, sessions.activeTicks]);
547     fields.set("totalTime", [now, sessions.totalTime]);
548     fields.set("main", [now, sessions.main]);
549     fields.set("firstPaint", [now, sessions.firstPaint]);
550     fields.set("sessionRestored", [now, sessions.sessionRestored]);
552     return CommonUtils.laterTickResolvingPromise({
553       days: new Metrics.DailyValues(),
554       singular: fields,
555     });
556   },
558   _serializeJSONSingular: function (data) {
559     let result = {"_v": this.version};
561     for (let [field, value] of data) {
562       result[field] = value[1];
563     }
565     return result;
566   },
570  * Records a history of all application sessions.
571  */
572 function PreviousSessionsMeasurement() {
573   Metrics.Measurement.call(this);
576 PreviousSessionsMeasurement.prototype = Object.freeze({
577   __proto__: Metrics.Measurement.prototype,
579   name: "previous",
580   version: 3,
582   fields: {
583     // Milliseconds of sessions that were properly shut down.
584     cleanActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
585     cleanTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
587     // Milliseconds of sessions that were not properly shut down.
588     abortedActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
589     abortedTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
591     // Startup times in milliseconds.
592     main: DAILY_DISCRETE_NUMERIC_FIELD,
593     firstPaint: DAILY_DISCRETE_NUMERIC_FIELD,
594     sessionRestored: DAILY_DISCRETE_NUMERIC_FIELD,
595   },
600  * Records information about the current browser session.
602  * A browser session is defined as an application/process lifetime. We
603  * start a new session when the application starts (essentially when
604  * this provider is instantiated) and end the session on shutdown.
606  * As the application runs, we record basic information about the
607  * "activity" of the session. Activity is defined by the presence of
608  * physical input into the browser (key press, mouse click, touch, etc).
610  * We differentiate between regular sessions and "aborted" sessions. An
611  * aborted session is one that does not end expectedly. This is often the
612  * result of a crash. We detect aborted sessions by storing the current
613  * session separate from completed sessions. We normally move the
614  * current session to completed sessions on application shutdown. If a
615  * current session is present on application startup, that means that
616  * the previous session was aborted.
617  */
618 this.SessionsProvider = function () {
619   Metrics.Provider.call(this);
622 SessionsProvider.prototype = Object.freeze({
623   __proto__: Metrics.Provider.prototype,
625   name: "org.mozilla.appSessions",
627   measurementTypes: [CurrentSessionMeasurement, PreviousSessionsMeasurement],
629   pullOnly: true,
631   collectConstantData: function () {
632     let previous = this.getMeasurement("previous", 3);
634     return this.storage.enqueueTransaction(this._recordAndPruneSessions.bind(this));
635   },
637   _recordAndPruneSessions: function () {
638     this._log.info("Moving previous sessions from session recorder to storage.");
639     let recorder = this.healthReporter.sessionRecorder;
640     let sessions = recorder.getPreviousSessions();
641     this._log.debug("Found " + Object.keys(sessions).length + " previous sessions.");
643     let daily = this.getMeasurement("previous", 3);
645     // Please note the coupling here between the session recorder and our state.
646     // If the pruned index or the current index of the session recorder is ever
647     // deleted or reset to 0, our stored state of a later index would mean that
648     // new sessions would never be captured by this provider until the session
649     // recorder index catches up to our last session ID. This should not happen
650     // under normal circumstances, so we don't worry too much about it. We
651     // should, however, consider this as part of implementing bug 841561.
652     let lastRecordedSession = yield this.getState("lastSession");
653     if (lastRecordedSession === null) {
654       lastRecordedSession = -1;
655     }
656     this._log.debug("The last recorded session was #" + lastRecordedSession);
658     for (let [index, session] in Iterator(sessions)) {
659       if (index <= lastRecordedSession) {
660         this._log.warn("Already recorded session " + index + ". Did the last " +
661                        "session crash or have an issue saving the prefs file?");
662         continue;
663       }
665       let type = session.clean ? "clean" : "aborted";
666       let date = session.startDate;
667       yield daily.addDailyDiscreteNumeric(type + "ActiveTicks", session.activeTicks, date);
668       yield daily.addDailyDiscreteNumeric(type + "TotalTime", session.totalTime, date);
670       for (let field of ["main", "firstPaint", "sessionRestored"]) {
671         yield daily.addDailyDiscreteNumeric(field, session[field], date);
672       }
674       lastRecordedSession = index;
675     }
677     yield this.setState("lastSession", "" + lastRecordedSession);
678     recorder.pruneOldSessions(new Date());
679   },
683  * Stores the set of active addons in storage.
685  * We do things a little differently than most other measurements. Because
686  * addons are difficult to shoehorn into distinct fields, we simply store a
687  * JSON blob in storage in a text field.
688  */
689 function ActiveAddonsMeasurement() {
690   Metrics.Measurement.call(this);
692   this._serializers = {};
693   this._serializers[this.SERIALIZE_JSON] = {
694     singular: this._serializeJSONSingular.bind(this),
695     // We don't need a daily serializer because we have none of this data.
696   };
699 ActiveAddonsMeasurement.prototype = Object.freeze({
700   __proto__: Metrics.Measurement.prototype,
702   name: "addons",
703   version: 2,
705   fields: {
706     addons: LAST_TEXT_FIELD,
707   },
709   _serializeJSONSingular: function (data) {
710     if (!data.has("addons")) {
711       this._log.warn("Don't have addons info. Weird.");
712       return null;
713     }
715     // Exceptions are caught in the caller.
716     let result = JSON.parse(data.get("addons")[1]);
717     result._v = this.version;
718     return result;
719   },
723  * Stores the set of active plugins in storage.
725  * This stores the data in a JSON blob in a text field similar to the
726  * ActiveAddonsMeasurement.
727  */
728 function ActivePluginsMeasurement() {
729   Metrics.Measurement.call(this);
731   this._serializers = {};
732   this._serializers[this.SERIALIZE_JSON] = {
733     singular: this._serializeJSONSingular.bind(this),
734     // We don't need a daily serializer because we have none of this data.
735   };
738 ActivePluginsMeasurement.prototype = Object.freeze({
739   __proto__: Metrics.Measurement.prototype,
741   name: "plugins",
742   version: 1,
744   fields: {
745     plugins: LAST_TEXT_FIELD,
746   },
748   _serializeJSONSingular: function (data) {
749     if (!data.has("plugins")) {
750       this._log.warn("Don't have plugins info. Weird.");
751       return null;
752     }
754     // Exceptions are caught in the caller.
755     let result = JSON.parse(data.get("plugins")[1]);
756     result._v = this.version;
757     return result;
758   },
761 function ActiveGMPluginsMeasurement() {
762   Metrics.Measurement.call(this);
764   this._serializers = {};
765   this._serializers[this.SERIALIZE_JSON] = {
766     singular: this._serializeJSONSingular.bind(this),
767   };
770 ActiveGMPluginsMeasurement.prototype = Object.freeze({
771   __proto__: Metrics.Measurement.prototype,
773   name: "gm-plugins",
774   version: 1,
776   fields: {
777     "gm-plugins": LAST_TEXT_FIELD,
778   },
780   _serializeJSONSingular: function (data) {
781     if (!data.has("gm-plugins")) {
782       this._log.warn("Don't have GM plugins info. Weird.");
783       return null;
784     }
786     let result = JSON.parse(data.get("gm-plugins")[1]);
787     result._v = this.version;
788     return result;
789   },
792 function AddonCountsMeasurement() {
793   Metrics.Measurement.call(this);
796 AddonCountsMeasurement.prototype = Object.freeze({
797   __proto__: Metrics.Measurement.prototype,
799   name: "counts",
800   version: 2,
802   fields: {
803     theme: DAILY_LAST_NUMERIC_FIELD,
804     lwtheme: DAILY_LAST_NUMERIC_FIELD,
805     plugin: DAILY_LAST_NUMERIC_FIELD,
806     extension: DAILY_LAST_NUMERIC_FIELD,
807     service: DAILY_LAST_NUMERIC_FIELD,
808   },
813  * Legacy version of addons counts before services was added.
814  */
815 function AddonCountsMeasurement1() {
816   Metrics.Measurement.call(this);
819 AddonCountsMeasurement1.prototype = Object.freeze({
820   __proto__: Metrics.Measurement.prototype,
822   name: "counts",
823   version: 1,
825   fields: {
826     theme: DAILY_LAST_NUMERIC_FIELD,
827     lwtheme: DAILY_LAST_NUMERIC_FIELD,
828     plugin: DAILY_LAST_NUMERIC_FIELD,
829     extension: DAILY_LAST_NUMERIC_FIELD,
830   },
834 this.AddonsProvider = function () {
835   Metrics.Provider.call(this);
837   this._prefs = new Preferences({defaultBranch: null});
840 AddonsProvider.prototype = Object.freeze({
841   __proto__: Metrics.Provider.prototype,
843   // Whenever these AddonListener callbacks are called, we repopulate
844   // and store the set of addons. Note that these events will only fire
845   // for restartless add-ons. For actions that require a restart, we
846   // will catch the change after restart. The alternative is a lot of
847   // state tracking here, which isn't desirable.
848   ADDON_LISTENER_CALLBACKS: [
849     "onEnabled",
850     "onDisabled",
851     "onInstalled",
852     "onUninstalled",
853   ],
855   // Add-on types for which full details are uploaded in the
856   // ActiveAddonsMeasurement. All other types are ignored.
857   FULL_DETAIL_TYPES: [
858     "extension",
859     "service",
860   ],
862   name: "org.mozilla.addons",
864   measurementTypes: [
865     ActiveAddonsMeasurement,
866     ActivePluginsMeasurement,
867     ActiveGMPluginsMeasurement,
868     AddonCountsMeasurement1,
869     AddonCountsMeasurement,
870   ],
872   postInit: function () {
873     let listener = {};
875     for (let method of this.ADDON_LISTENER_CALLBACKS) {
876       listener[method] = this._collectAndStoreAddons.bind(this);
877     }
879     this._listener = listener;
880     AddonManager.addAddonListener(this._listener);
882     return CommonUtils.laterTickResolvingPromise();
883   },
885   onShutdown: function () {
886     AddonManager.removeAddonListener(this._listener);
887     this._listener = null;
889     return CommonUtils.laterTickResolvingPromise();
890   },
892   collectConstantData: function () {
893     return this._collectAndStoreAddons();
894   },
896   _collectAndStoreAddons: function () {
897     let deferred = Promise.defer();
899     AddonManager.getAllAddons(function onAllAddons(allAddons) {
900       let data;
901       let addonsField;
902       let pluginsField;
903       let gmPluginsField;
904       try {
905         data = this._createDataStructure(allAddons);
906         addonsField = JSON.stringify(data.addons);
907         pluginsField = JSON.stringify(data.plugins);
908         gmPluginsField = JSON.stringify(data.gmPlugins);
909       } catch (ex) {
910         this._log.warn("Exception when populating add-ons data structure: " +
911                        CommonUtils.exceptionStr(ex));
912         deferred.reject(ex);
913         return;
914       }
916       let now = new Date();
917       let addons = this.getMeasurement("addons", 2);
918       let plugins = this.getMeasurement("plugins", 1);
919       let gmPlugins = this.getMeasurement("gm-plugins", 1);
920       let counts = this.getMeasurement(AddonCountsMeasurement.prototype.name,
921                                        AddonCountsMeasurement.prototype.version);
923       this.enqueueStorageOperation(function storageAddons() {
924         for (let type in data.counts) {
925           try {
926             counts.fieldID(type);
927           } catch (ex) {
928             this._log.warn("Add-on type without field: " + type);
929             continue;
930           }
932           counts.setDailyLastNumeric(type, data.counts[type], now);
933         }
935         return addons.setLastText("addons", addonsField).then(
936           function onSuccess() {
937             return plugins.setLastText("plugins", pluginsField).then(
938               function onSuccess() {
939                 return gmPlugins.setLastText("gm-plugins", gmPluginsField).then(
940                   function onSuccess() {
941                     deferred.resolve();
942                   },
943                   function onError(error) {
944                     deferred.reject(error);
945                   });
946               },
947               function onError(error) { deferred.reject(error); }
948             );
949           },
950           function onError(error) { deferred.reject(error); }
951         );
952       }.bind(this));
953     }.bind(this));
955     return deferred.promise;
956   },
958   COPY_ADDON_FIELDS: [
959     "userDisabled",
960     "appDisabled",
961     "name",
962     "version",
963     "type",
964     "scope",
965     "description",
966     "foreignInstall",
967     "hasBinaryComponents",
968   ],
970   COPY_PLUGIN_FIELDS: [
971     "name",
972     "version",
973     "description",
974     "blocklisted",
975     "disabled",
976     "clicktoplay",
977   ],
979   _createDataStructure: function (addons) {
980     let data = {
981       addons: {},
982       plugins: {},
983       gmPlugins: {},
984       counts: {}
985     };
987     for (let addon of addons) {
988       let type = addon.type;
990       // We count plugins separately below.
991       if (addon.type == "plugin") {
992         if (addon.isGMPlugin) {
993           data.gmPlugins[addon.id] = {
994             version: addon.version,
995             userDisabled: addon.userDisabled,
996             applyBackgroundUpdates: addon.applyBackgroundUpdates,
997           };
998         }
999         continue;
1000       }
1002       data.counts[type] = (data.counts[type] || 0) + 1;
1004       if (this.FULL_DETAIL_TYPES.indexOf(addon.type) == -1) {
1005         continue;
1006       }
1008       let obj = {};
1009       for (let field of this.COPY_ADDON_FIELDS) {
1010         obj[field] = addon[field];
1011       }
1013       if (addon.installDate) {
1014         obj.installDay = this._dateToDays(addon.installDate);
1015       }
1017       if (addon.updateDate) {
1018         obj.updateDay = this._dateToDays(addon.updateDate);
1019       }
1021       data.addons[addon.id] = obj;
1022     }
1024     let pluginTags = Cc["@mozilla.org/plugin/host;1"].
1025                        getService(Ci.nsIPluginHost).
1026                        getPluginTags({});
1028     for (let tag of pluginTags) {
1029       let obj = {
1030         mimeTypes: tag.getMimeTypes({}),
1031       };
1033       for (let field of this.COPY_PLUGIN_FIELDS) {
1034         obj[field] = tag[field];
1035       }
1037       // Plugins need to have a filename and a name, so this can't be empty.
1038       let id = tag.filename + ":" + tag.name + ":" + tag.version + ":"
1039                + tag.description;
1040       data.plugins[id] = obj;
1041     }
1043     data.counts["plugin"] = pluginTags.length;
1045     return data;
1046   },
1049 #ifdef MOZ_CRASHREPORTER
1051 function DailyCrashesMeasurement1() {
1052   Metrics.Measurement.call(this);
1055 DailyCrashesMeasurement1.prototype = Object.freeze({
1056   __proto__: Metrics.Measurement.prototype,
1058   name: "crashes",
1059   version: 1,
1061   fields: {
1062     pending: DAILY_COUNTER_FIELD,
1063     submitted: DAILY_COUNTER_FIELD,
1064   },
1067 function DailyCrashesMeasurement2() {
1068   Metrics.Measurement.call(this);
1071 DailyCrashesMeasurement2.prototype = Object.freeze({
1072   __proto__: Metrics.Measurement.prototype,
1074   name: "crashes",
1075   version: 2,
1077   fields: {
1078     mainCrash: DAILY_LAST_NUMERIC_FIELD,
1079   },
1082 function DailyCrashesMeasurement3() {
1083   Metrics.Measurement.call(this);
1086 DailyCrashesMeasurement3.prototype = Object.freeze({
1087   __proto__: Metrics.Measurement.prototype,
1089   name: "crashes",
1090   version: 3,
1092   fields: {
1093     "main-crash": DAILY_LAST_NUMERIC_FIELD,
1094     "main-hang": DAILY_LAST_NUMERIC_FIELD,
1095     "content-crash": DAILY_LAST_NUMERIC_FIELD,
1096     "content-hang": DAILY_LAST_NUMERIC_FIELD,
1097     "plugin-crash": DAILY_LAST_NUMERIC_FIELD,
1098     "plugin-hang": DAILY_LAST_NUMERIC_FIELD,
1099   },
1102 function DailyCrashesMeasurement4() {
1103   Metrics.Measurement.call(this);
1106 DailyCrashesMeasurement4.prototype = Object.freeze({
1107   __proto__: Metrics.Measurement.prototype,
1109   name: "crashes",
1110   version: 4,
1112   fields: {
1113     "main-crash": DAILY_LAST_NUMERIC_FIELD,
1114     "main-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1115     "main-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1116     "main-hang": DAILY_LAST_NUMERIC_FIELD,
1117     "main-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1118     "main-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1119     "content-crash": DAILY_LAST_NUMERIC_FIELD,
1120     "content-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1121     "content-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1122     "content-hang": DAILY_LAST_NUMERIC_FIELD,
1123     "content-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1124     "content-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1125     "plugin-crash": DAILY_LAST_NUMERIC_FIELD,
1126     "plugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1127     "plugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1128     "plugin-hang": DAILY_LAST_NUMERIC_FIELD,
1129     "plugin-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1130     "plugin-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1131   },
1134 function DailyCrashesMeasurement5() {
1135   Metrics.Measurement.call(this);
1138 DailyCrashesMeasurement5.prototype = Object.freeze({
1139   __proto__: Metrics.Measurement.prototype,
1141   name: "crashes",
1142   version: 5,
1144   fields: {
1145     "main-crash": DAILY_LAST_NUMERIC_FIELD,
1146     "main-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1147     "main-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1148     "main-hang": DAILY_LAST_NUMERIC_FIELD,
1149     "main-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1150     "main-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1151     "content-crash": DAILY_LAST_NUMERIC_FIELD,
1152     "content-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1153     "content-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1154     "content-hang": DAILY_LAST_NUMERIC_FIELD,
1155     "content-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1156     "content-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1157     "plugin-crash": DAILY_LAST_NUMERIC_FIELD,
1158     "plugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1159     "plugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1160     "plugin-hang": DAILY_LAST_NUMERIC_FIELD,
1161     "plugin-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1162     "plugin-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1163     "gmplugin-crash": DAILY_LAST_NUMERIC_FIELD,
1164     "gmplugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
1165     "gmplugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
1166   },
1169 this.CrashesProvider = function () {
1170   Metrics.Provider.call(this);
1172   // So we can unit test.
1173   this._manager = Services.crashmanager;
1176 CrashesProvider.prototype = Object.freeze({
1177   __proto__: Metrics.Provider.prototype,
1179   name: "org.mozilla.crashes",
1181   measurementTypes: [
1182     DailyCrashesMeasurement1,
1183     DailyCrashesMeasurement2,
1184     DailyCrashesMeasurement3,
1185     DailyCrashesMeasurement4,
1186     DailyCrashesMeasurement5,
1187   ],
1189   pullOnly: true,
1191   collectDailyData: function () {
1192     return this.storage.enqueueTransaction(this._populateCrashCounts.bind(this));
1193   },
1195   _populateCrashCounts: function () {
1196     this._log.info("Grabbing crash counts from crash manager.");
1197     let crashCounts = yield this._manager.getCrashCountsByDay();
1199     // TODO: CrashManager no longer stores submissions as crashes, but we still
1200     // want to send the submission data to FHR. As a temporary workaround, we
1201     // populate |crashCounts| with the submission data to match past behaviour.
1202     // See bug 1056160.
1203     let crashes = yield this._manager.getCrashes();
1204     for (let crash of crashes) {
1205       for (let [submissionID, submission] of crash.submissions) {
1206         if (!submission.responseDate) {
1207           continue;
1208         }
1210         let day = Metrics.dateToDays(submission.responseDate);
1211         if (!crashCounts.has(day)) {
1212           crashCounts.set(day, new Map());
1213         }
1215         let succeeded =
1216           submission.result == this._manager.SUBMISSION_RESULT_OK;
1217         let type = crash.type + "-submission-" + (succeeded ? "succeeded" :
1218                                                               "failed");
1220         let count = (crashCounts.get(day).get(type) || 0) + 1;
1221         crashCounts.get(day).set(type, count);
1222       }
1223     }
1225     let m = this.getMeasurement("crashes", 5);
1226     let fields = DailyCrashesMeasurement5.prototype.fields;
1228     for (let [day, types] of crashCounts) {
1229       let date = Metrics.daysToDate(day);
1230       for (let [type, count] of types) {
1231         if (!(type in fields)) {
1232           this._log.warn("Unknown crash type encountered: " + type);
1233           continue;
1234         }
1236         yield m.setDailyLastNumeric(type, count, date);
1237       }
1238     }
1239   },
1242 #endif
1245  * Records data from update hotfixes.
1247  * This measurement has dynamic fields. Field names are of the form
1248  * <version>.<thing> where <version> is the hotfix version that produced
1249  * the data. e.g. "v20140527". The sub-version of the hotfix is omitted
1250  * because hotfixes can go through multiple minor versions during development
1251  * and we don't want to introduce more fields than necessary. Furthermore,
1252  * the subsequent dots make parsing field names slightly harder. By stripping,
1253  * we can just split on the first dot.
1254  */
1255 function UpdateHotfixMeasurement1() {
1256   Metrics.Measurement.call(this);
1259 UpdateHotfixMeasurement1.prototype = Object.freeze({
1260   __proto__: Metrics.Measurement.prototype,
1262   name: "update",
1263   version: 1,
1265   hotfixFieldTypes: {
1266     "upgradedFrom": Metrics.Storage.FIELD_LAST_TEXT,
1267     "uninstallReason": Metrics.Storage.FIELD_LAST_TEXT,
1268     "downloadAttempts": Metrics.Storage.FIELD_LAST_NUMERIC,
1269     "downloadFailures": Metrics.Storage.FIELD_LAST_NUMERIC,
1270     "installAttempts": Metrics.Storage.FIELD_LAST_NUMERIC,
1271     "installFailures": Metrics.Storage.FIELD_LAST_NUMERIC,
1272     "notificationsShown": Metrics.Storage.FIELD_LAST_NUMERIC,
1273   },
1275   fields: { },
1277   // Our fields have dynamic names from the hotfix version that supplied them.
1278   // We need to override the default behavior to deal with unknown fields.
1279   shouldIncludeField: function (name) {
1280     return name.contains(".");
1281   },
1283   fieldType: function (name) {
1284     for (let known in this.hotfixFieldTypes) {
1285       if (name.endsWith(known)) {
1286         return this.hotfixFieldTypes[known];
1287       }
1288     }
1290     return Metrics.Measurement.prototype.fieldType.call(this, name);
1291   },
1294 this.HotfixProvider = function () {
1295   Metrics.Provider.call(this);
1298 HotfixProvider.prototype = Object.freeze({
1299   __proto__: Metrics.Provider.prototype,
1301   name: "org.mozilla.hotfix",
1302   measurementTypes: [
1303     UpdateHotfixMeasurement1,
1304   ],
1306   pullOnly: true,
1308   collectDailyData: function () {
1309     return this.storage.enqueueTransaction(this._populateHotfixData.bind(this));
1310   },
1312   _populateHotfixData: function* () {
1313     let m = this.getMeasurement("update", 1);
1315     // The update hotfix retains its JSON state file after uninstall.
1316     // The initial update hotfix had a hard-coded filename. We treat it
1317     // specially. Subsequent update hotfixes named their files in a
1318     // recognizeable pattern so we don't need to update this probe code to
1319     // know about them.
1320     let files = [
1321         ["v20140527", OS.Path.join(OS.Constants.Path.profileDir,
1322                                    "hotfix.v20140527.01.json")],
1323     ];
1325     let it = new OS.File.DirectoryIterator(OS.Constants.Path.profileDir);
1326     try {
1327       yield it.forEach((e, index, it) => {
1328         let m = e.name.match(/^updateHotfix\.([a-zA-Z0-9]+)\.json$/);
1329         if (m) {
1330           files.push([m[1], e.path]);
1331         }
1332       });
1333     } finally {
1334       it.close();
1335     }
1337     let decoder = new TextDecoder();
1338     for (let e of files) {
1339       let [version, path] = e;
1340       let p;
1341       try {
1342         let data = yield OS.File.read(path);
1343         p = JSON.parse(decoder.decode(data));
1344       } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
1345         continue;
1346       } catch (ex) {
1347         this._log.warn("Error loading update hotfix payload: " + ex.message);
1348       }
1350       // Wrap just in case.
1351       try {
1352         for (let k in m.hotfixFieldTypes) {
1353           if (!(k in p)) {
1354             continue;
1355           }
1357           let value = p[k];
1358           if (value === null && k == "uninstallReason") {
1359             value = "STILL_INSTALLED";
1360           }
1362           let field = version + "." + k;
1363           let fieldType;
1364           let storageOp;
1365           switch (typeof(value)) {
1366             case "string":
1367               fieldType = this.storage.FIELD_LAST_TEXT;
1368               storageOp = "setLastTextFromFieldID";
1369               break;
1370             case "number":
1371               fieldType = this.storage.FIELD_LAST_NUMERIC;
1372               storageOp = "setLastNumericFromFieldID";
1373               break;
1374             default:
1375               this._log.warn("Unknown value in hotfix state: " + k + "=" + value);
1376               continue;
1377           }
1379           if (this.storage.hasFieldFromMeasurement(m.id, field, fieldType)) {
1380             let fieldID = this.storage.fieldIDFromMeasurement(m.id, field);
1381             yield this.storage[storageOp](fieldID, value);
1382           } else {
1383             let fieldID = yield this.storage.registerField(m.id, field,
1384                                                            fieldType);
1385             yield this.storage[storageOp](fieldID, value);
1386           }
1387         }
1389       } catch (ex) {
1390         this._log.warn("Error processing update hotfix data: " + ex);
1391       }
1392     }
1393   },
1397  * Holds basic statistics about the Places database.
1398  */
1399 function PlacesMeasurement() {
1400   Metrics.Measurement.call(this);
1403 PlacesMeasurement.prototype = Object.freeze({
1404   __proto__: Metrics.Measurement.prototype,
1406   name: "places",
1407   version: 1,
1409   fields: {
1410     pages: DAILY_LAST_NUMERIC_FIELD,
1411     bookmarks: DAILY_LAST_NUMERIC_FIELD,
1412   },
1417  * Collects information about Places.
1418  */
1419 this.PlacesProvider = function () {
1420   Metrics.Provider.call(this);
1423 PlacesProvider.prototype = Object.freeze({
1424   __proto__: Metrics.Provider.prototype,
1426   name: "org.mozilla.places",
1428   measurementTypes: [PlacesMeasurement],
1430   collectDailyData: function () {
1431     return this.storage.enqueueTransaction(this._collectData.bind(this));
1432   },
1434   _collectData: function () {
1435     let now = new Date();
1436     let data = yield this._getDailyValues();
1438     let m = this.getMeasurement("places", 1);
1440     yield m.setDailyLastNumeric("pages", data.PLACES_PAGES_COUNT);
1441     yield m.setDailyLastNumeric("bookmarks", data.PLACES_BOOKMARKS_COUNT);
1442   },
1444   _getDailyValues: function () {
1445     let deferred = Promise.defer();
1447     PlacesDBUtils.telemetry(null, function onResult(data) {
1448       deferred.resolve(data);
1449     });
1451     return deferred.promise;
1452   },
1455 function SearchCountMeasurement1() {
1456   Metrics.Measurement.call(this);
1459 SearchCountMeasurement1.prototype = Object.freeze({
1460   __proto__: Metrics.Measurement.prototype,
1462   name: "counts",
1463   version: 1,
1465   // We only record searches for search engines that have partner agreements
1466   // with Mozilla.
1467   fields: {
1468     "amazon.com.abouthome": DAILY_COUNTER_FIELD,
1469     "amazon.com.contextmenu": DAILY_COUNTER_FIELD,
1470     "amazon.com.searchbar": DAILY_COUNTER_FIELD,
1471     "amazon.com.urlbar": DAILY_COUNTER_FIELD,
1472     "bing.abouthome": DAILY_COUNTER_FIELD,
1473     "bing.contextmenu": DAILY_COUNTER_FIELD,
1474     "bing.searchbar": DAILY_COUNTER_FIELD,
1475     "bing.urlbar": DAILY_COUNTER_FIELD,
1476     "google.abouthome": DAILY_COUNTER_FIELD,
1477     "google.contextmenu": DAILY_COUNTER_FIELD,
1478     "google.searchbar": DAILY_COUNTER_FIELD,
1479     "google.urlbar": DAILY_COUNTER_FIELD,
1480     "yahoo.abouthome": DAILY_COUNTER_FIELD,
1481     "yahoo.contextmenu": DAILY_COUNTER_FIELD,
1482     "yahoo.searchbar": DAILY_COUNTER_FIELD,
1483     "yahoo.urlbar": DAILY_COUNTER_FIELD,
1484     "other.abouthome": DAILY_COUNTER_FIELD,
1485     "other.contextmenu": DAILY_COUNTER_FIELD,
1486     "other.searchbar": DAILY_COUNTER_FIELD,
1487     "other.urlbar": DAILY_COUNTER_FIELD,
1488   },
1492  * Records search counts per day per engine and where search initiated.
1494  * We want to record granular details for individual locale-specific search
1495  * providers, but only if they're Mozilla partners. In order to do this, we
1496  * track the nsISearchEngine identifier, which denotes shipped search engines,
1497  * and intersect those with our partner list.
1499  * We don't use the search engine name directly, because it is shared across
1500  * locales; e.g., eBay-de and eBay both share the name "eBay".
1501  */
1502 function SearchCountMeasurementBase() {
1503   this._fieldSpecs = {};
1504   Metrics.Measurement.call(this);
1507 SearchCountMeasurementBase.prototype = Object.freeze({
1508   __proto__: Metrics.Measurement.prototype,
1511   // Our fields are dynamic.
1512   get fields() {
1513     return this._fieldSpecs;
1514   },
1516   /**
1517    * Override the default behavior: serializers should include every counter
1518    * field from the DB, even if we don't currently have it registered.
1519    *
1520    * Do this so we don't have to register several hundred fields to match
1521    * various Firefox locales.
1522    *
1523    * We use the "provider.type" syntax as a rudimentary check for validity.
1524    *
1525    * We trust that measurement versioning is sufficient to exclude old provider
1526    * data.
1527    */
1528   shouldIncludeField: function (name) {
1529     return name.contains(".");
1530   },
1532   /**
1533    * The measurement type mechanism doesn't introspect the DB. Override it
1534    * so that we can assume all unknown fields are counters.
1535    */
1536   fieldType: function (name) {
1537     if (name in this.fields) {
1538       return this.fields[name].type;
1539     }
1541     // Default to a counter.
1542     return Metrics.Storage.FIELD_DAILY_COUNTER;
1543   },
1545   SOURCES: [
1546     "abouthome",
1547     "contextmenu",
1548     "newtab",
1549     "searchbar",
1550     "urlbar",
1551   ],
1554 function SearchCountMeasurement2() {
1555   SearchCountMeasurementBase.call(this);
1558 SearchCountMeasurement2.prototype = Object.freeze({
1559   __proto__: SearchCountMeasurementBase.prototype,
1560   name: "counts",
1561   version: 2,
1564 function SearchCountMeasurement3() {
1565   SearchCountMeasurementBase.call(this);
1568 SearchCountMeasurement3.prototype = Object.freeze({
1569   __proto__: SearchCountMeasurementBase.prototype,
1570   name: "counts",
1571   version: 3,
1573   getEngines: function () {
1574     return Services.search.getEngines();
1575   },
1577   getEngineID: function (engine) {
1578     if (!engine) {
1579       return "other";
1580     }
1581     if (engine.identifier) {
1582       return engine.identifier;
1583     }
1584     return "other-" + engine.name;
1585   },
1588 function SearchEnginesMeasurement1() {
1589   Metrics.Measurement.call(this);
1592 SearchEnginesMeasurement1.prototype = Object.freeze({
1593   __proto__: Metrics.Measurement.prototype,
1595   name: "engines",
1596   version: 1,
1598   fields: {
1599     default: DAILY_LAST_TEXT_FIELD,
1600   },
1603 this.SearchesProvider = function () {
1604   Metrics.Provider.call(this);
1606   this._prefs = new Preferences({defaultBranch: null});
1609 this.SearchesProvider.prototype = Object.freeze({
1610   __proto__: Metrics.Provider.prototype,
1612   name: "org.mozilla.searches",
1613   measurementTypes: [
1614     SearchCountMeasurement1,
1615     SearchCountMeasurement2,
1616     SearchCountMeasurement3,
1617     SearchEnginesMeasurement1,
1618   ],
1620   /**
1621    * Initialize the search service before our measurements are touched.
1622    */
1623   preInit: function (storage) {
1624     // Initialize search service.
1625     let deferred = Promise.defer();
1626     Services.search.init(function onInitComplete () {
1627       deferred.resolve();
1628     });
1629     return deferred.promise;
1630   },
1632   collectDailyData: function () {
1633     return this.storage.enqueueTransaction(function getDaily() {
1634       // We currently only record this if Telemetry is enabled.
1635       if (!isTelemetryEnabled(this._prefs)) {
1636         return;
1637       }
1639       let m = this.getMeasurement(SearchEnginesMeasurement1.prototype.name,
1640                                   SearchEnginesMeasurement1.prototype.version);
1642       let engine;
1643       try {
1644         engine = Services.search.defaultEngine;
1645       } catch (e) {}
1646       let name;
1648       if (!engine) {
1649         name = "NONE";
1650       } else if (engine.identifier) {
1651         name = engine.identifier;
1652       } else if (engine.name) {
1653         name = "other-" + engine.name;
1654       } else {
1655         name = "UNDEFINED";
1656       }
1658       yield m.setDailyLastText("default", name);
1659     }.bind(this));
1660   },
1662   /**
1663    * Record that a search occurred.
1664    *
1665    * @param engine
1666    *        (nsISearchEngine) The search engine used.
1667    * @param source
1668    *        (string) Where the search was initiated from. Must be one of the
1669    *        SearchCountMeasurement2.SOURCES values.
1670    *
1671    * @return Promise<>
1672    *         The promise is resolved when the storage operation completes.
1673    */
1674   recordSearch: function (engine, source) {
1675     let m = this.getMeasurement("counts", 3);
1677     if (m.SOURCES.indexOf(source) == -1) {
1678       throw new Error("Unknown source for search: " + source);
1679     }
1681     let field = m.getEngineID(engine) + "." + source;
1682     if (this.storage.hasFieldFromMeasurement(m.id, field,
1683                                              this.storage.FIELD_DAILY_COUNTER)) {
1684       let fieldID = this.storage.fieldIDFromMeasurement(m.id, field);
1685       return this.enqueueStorageOperation(function recordSearchKnownField() {
1686         return this.storage.incrementDailyCounterFromFieldID(fieldID);
1687       }.bind(this));
1688     }
1690     // Otherwise, we first need to create the field.
1691     return this.enqueueStorageOperation(function recordFieldAndSearch() {
1692       // This function has to return a promise.
1693       return Task.spawn(function () {
1694         let fieldID = yield this.storage.registerField(m.id, field,
1695                                                        this.storage.FIELD_DAILY_COUNTER);
1696         yield this.storage.incrementDailyCounterFromFieldID(fieldID);
1697       }.bind(this));
1698     }.bind(this));
1699   },
1702 function HealthReportSubmissionMeasurement1() {
1703   Metrics.Measurement.call(this);
1706 HealthReportSubmissionMeasurement1.prototype = Object.freeze({
1707   __proto__: Metrics.Measurement.prototype,
1709   name: "submissions",
1710   version: 1,
1712   fields: {
1713     firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
1714     continuationUploadAttempt: DAILY_COUNTER_FIELD,
1715     uploadSuccess: DAILY_COUNTER_FIELD,
1716     uploadTransportFailure: DAILY_COUNTER_FIELD,
1717     uploadServerFailure: DAILY_COUNTER_FIELD,
1718     uploadClientFailure: DAILY_COUNTER_FIELD,
1719   },
1722 function HealthReportSubmissionMeasurement2() {
1723   Metrics.Measurement.call(this);
1726 HealthReportSubmissionMeasurement2.prototype = Object.freeze({
1727   __proto__: Metrics.Measurement.prototype,
1729   name: "submissions",
1730   version: 2,
1732   fields: {
1733     firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
1734     continuationUploadAttempt: DAILY_COUNTER_FIELD,
1735     uploadSuccess: DAILY_COUNTER_FIELD,
1736     uploadTransportFailure: DAILY_COUNTER_FIELD,
1737     uploadServerFailure: DAILY_COUNTER_FIELD,
1738     uploadClientFailure: DAILY_COUNTER_FIELD,
1739     uploadAlreadyInProgress: DAILY_COUNTER_FIELD,
1740   },
1743 this.HealthReportProvider = function () {
1744   Metrics.Provider.call(this);
1747 HealthReportProvider.prototype = Object.freeze({
1748   __proto__: Metrics.Provider.prototype,
1750   name: "org.mozilla.healthreport",
1752   measurementTypes: [
1753     HealthReportSubmissionMeasurement1,
1754     HealthReportSubmissionMeasurement2,
1755   ],
1757   recordEvent: function (event, date=new Date()) {
1758     let m = this.getMeasurement("submissions", 2);
1759     return this.enqueueStorageOperation(function recordCounter() {
1760       return m.incrementDailyCounter(event, date);
1761     });
1762   },