diff --git a/services/healthreport/HealthReportComponents.manifest b/services/healthreport/HealthReportComponents.manifest index c32e2f470a08..84e12e101bb0 100644 --- a/services/healthreport/HealthReportComponents.manifest +++ b/services/healthreport/HealthReportComponents.manifest @@ -9,6 +9,7 @@ component {e354c59b-b252-4040-b6dd-b71864e3e35c} HealthReportService.js contract @mozilla.org/healthreport/service;1 {e354c59b-b252-4040-b6dd-b71864e3e35c} category app-startup HealthReportService service,@mozilla.org/healthreport/service;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} +category healthreport-js-provider AddonsProvider resource://gre/modules/services/healthreport/providers.jsm category healthreport-js-provider AppInfoProvider resource://gre/modules/services/healthreport/providers.jsm category healthreport-js-provider SysInfoProvider resource://gre/modules/services/healthreport/providers.jsm category healthreport-js-provider ProfileMetadataProvider resource://gre/modules/services/healthreport/profile.jsm diff --git a/services/healthreport/providers.jsm b/services/healthreport/providers.jsm index d93e7dd32892..faf4d1eced38 100644 --- a/services/healthreport/providers.jsm +++ b/services/healthreport/providers.jsm @@ -15,6 +15,7 @@ "use strict"; this.EXPORTED_SYMBOLS = [ + "AddonsProvider", "AppInfoProvider", "SessionsProvider", "SysInfoProvider", @@ -22,6 +23,7 @@ this.EXPORTED_SYMBOLS = [ const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +Cu.import("resource://gre/modules/commonjs/promise/core.js"); Cu.import("resource://gre/modules/Metrics.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Task.jsm"); @@ -29,7 +31,8 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://services-common/preferences.js"); Cu.import("resource://services-common/utils.js"); - +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", + "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel", "resource://gre/modules/UpdateChannel.jsm"); @@ -610,3 +613,201 @@ SessionsProvider.prototype = Object.freeze({ }, }); +/** + * Stores the set of active addons in storage. + * + * We do things a little differently than most other measurements. Because + * addons are difficult to shoehorn into distinct fields, we simply store a + * JSON blob in storage in a text field. + */ +function ActiveAddonsMeasurement() { + Metrics.Measurement.call(this); + + this._serializers = {}; + this._serializers[this.SERIALIZE_JSON] = { + singular: this._serializeJSONSingular.bind(this), + // We don't need a daily serializer because we have none of this data. + }; +} + +ActiveAddonsMeasurement.prototype = Object.freeze({ + __proto__: Metrics.Measurement.prototype, + + name: "active", + version: 1, + + configureStorage: function () { + return this.registerStorageField("addons", this.storage.FIELD_LAST_TEXT); + }, + + _serializeJSONSingular: function (data) { + if (!data.has("addons")) { + this._log.warn("Don't have active addons info. Weird."); + return null; + } + + // Exceptions are caught in the caller. + return JSON.parse(data.get("addons")[1]); + }, +}); + + +function AddonCountsMeasurement() { + Metrics.Measurement.call(this); +} + +AddonCountsMeasurement.prototype = Object.freeze({ + __proto__: Metrics.Measurement.prototype, + + name: "counts", + version: 1, + + configureStorage: function () { + return Task.spawn(function registerFields() { + yield this.registerStorageField("theme", this.storage.FIELD_DAILY_LAST_NUMERIC); + yield this.registerStorageField("lwtheme", this.storage.FIELD_DAILY_LAST_NUMERIC); + yield this.registerStorageField("plugin", this.storage.FIELD_DAILY_LAST_NUMERIC); + yield this.registerStorageField("extension", this.storage.FIELD_DAILY_LAST_NUMERIC); + }.bind(this)); + }, +}); + + +this.AddonsProvider = function () { + Metrics.Provider.call(this); + + this._prefs = new Preferences({defaultBranch: null}); +}; + +AddonsProvider.prototype = Object.freeze({ + __proto__: Metrics.Provider.prototype, + + // Whenever these AddonListener callbacks are called, we repopulate + // and store the set of addons. Note that these events will only fire + // for restartless add-ons. For actions that require a restart, we + // will catch the change after restart. The alternative is a lot of + // state tracking here, which isn't desirable. + ADDON_LISTENER_CALLBACKS: [ + "onEnabled", + "onDisabled", + "onInstalled", + "onUninstalled", + ], + + name: "org.mozilla.addons", + + measurementTypes: [ + ActiveAddonsMeasurement, + AddonCountsMeasurement, + ], + + onInit: function () { + let listener = {}; + + for (let method of this.ADDON_LISTENER_CALLBACKS) { + listener[method] = this._collectAndStoreAddons.bind(this); + } + + this._listener = listener; + AddonManager.addAddonListener(this._listener); + + return Promise.resolve(); + }, + + onShutdown: function () { + AddonManager.removeAddonListener(this._listener); + this._listener = null; + + return Promise.resolve(); + }, + + collectConstantData: function () { + return this._collectAndStoreAddons(); + }, + + _collectAndStoreAddons: function () { + let deferred = Promise.defer(); + + AddonManager.getAllAddons(function onAllAddons(addons) { + let data; + let addonsField; + try { + data = this._createDataStructure(addons); + addonsField = JSON.stringify(data.addons); + } catch (ex) { + this._log.warn("Exception when populating add-ons data structure: " + + CommonUtils.exceptionStr(ex)); + deferred.reject(ex); + return; + } + + let now = new Date(); + let active = this.getMeasurement("active", 1); + let counts = this.getMeasurement("counts", 1); + + this.enqueueStorageOperation(function storageAddons() { + for (let type in data.counts) { + try { + counts.fieldID(type); + } catch (ex) { + this._log.warn("Add-on type without field: " + type); + continue; + } + + counts.setDailyLastNumeric(type, data.counts[type], now); + } + + return active.setLastText("addons", addonsField).then( + function onSuccess() { deferred.resolve(); }, + function onError(error) { deferred.reject(error); } + ); + }.bind(this)); + }.bind(this)); + + return deferred.promise; + }, + + COPY_FIELDS: [ + "userDisabled", + "appDisabled", + "version", + "type", + "scope", + "foreignInstall", + "hasBinaryComponents", + ], + + _createDataStructure: function (addons) { + let data = {addons: {}, counts: {}}; + + for (let addon of addons) { + let optOutPref = "extensions." + addon.id + ".getAddons.cache.enabled"; + if (!this._prefs.get(optOutPref, true)) { + this._log.debug("Ignoring add-on that's opted out of AMO updates: " + + addon.id); + continue; + } + + let obj = {}; + for (let field of this.COPY_FIELDS) { + obj[field] = addon[field]; + } + + if (addon.installDate) { + obj.installDay = this._dateToDays(addon.installDate); + } + + if (addon.updateDate) { + obj.updateDay = this._dateToDays(addon.updateDate); + } + + data.addons[addon.id] = obj; + + let type = addon.type; + data.counts[type] = (data.counts[type] || 0) + 1; + } + + return data; + }, +}); + diff --git a/services/healthreport/tests/xpcshell/head.js b/services/healthreport/tests/xpcshell/head.js index 3bc93ef202ac..d2664b48b1d5 100644 --- a/services/healthreport/tests/xpcshell/head.js +++ b/services/healthreport/tests/xpcshell/head.js @@ -20,3 +20,18 @@ do_get_profile(); ns.updateAppInfo(); }).call(this); +// The hack, it burns. This could go away if extensions code exposed its +// test environment setup functions as a testing-only JSM. See similar +// code in Sync's head_helpers.js. +let gGlobalScope = this; +function loadAddonManager() { + let ns = {}; + Components.utils.import("resource://gre/modules/Services.jsm", ns); + let head = "../../../../toolkit/mozapps/extensions/test/xpcshell/head_addons.js"; + let file = do_get_file(head); + let uri = ns.Services.io.newFileURI(file); + ns.Services.scriptloader.loadSubScript(uri.spec, gGlobalScope); + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + startupManager(); +} + diff --git a/services/healthreport/tests/xpcshell/test_provider_addons.js b/services/healthreport/tests/xpcshell/test_provider_addons.js new file mode 100644 index 000000000000..db7934cb1eb5 --- /dev/null +++ b/services/healthreport/tests/xpcshell/test_provider_addons.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {utils: Cu} = Components; + + +Cu.import("resource://gre/modules/Metrics.jsm"); +Cu.import("resource://gre/modules/services/healthreport/providers.jsm"); + + +function run_test() { + loadAddonManager(); + run_next_test(); +} + +add_test(function test_constructor() { + let provider = new AddonsProvider(); + + run_next_test(); +}); + +add_task(function test_init() { + let storage = yield Metrics.Storage("init"); + let provider = new AddonsProvider(); + yield provider.init(storage); + yield provider.shutdown(); + + yield storage.close(); +}); + +function monkeypatchAddons(provider, addons) { + if (!Array.isArray(addons)) { + throw new Error("Must define array of addon objects."); + } + + Object.defineProperty(provider, "_createDataStructure", { + value: function _createDataStructure() { + return AddonsProvider.prototype._createDataStructure.call(provider, addons); + }, + }); +} + +add_task(function test_collect() { + let storage = yield Metrics.Storage("collect"); + let provider = new AddonsProvider(); + yield provider.init(storage); + + let now = new Date(); + + // FUTURE install add-on via AddonManager and don't use monkeypatching. + let addons = [ + { + id: "addon0", + userDisabled: false, + appDisabled: false, + version: "1", + type: "extension", + scope: 1, + foreignInstall: false, + hasBinaryComponents: false, + installDate: now, + updateDate: now, + }, + { + id: "addon1", + userDisabled: false, + appDisabled: false, + version: "2", + type: "plugin", + scope: 1, + foreignInstall: false, + hasBinaryComponents: false, + installDate: now, + updateDate: now, + }, + ]; + + monkeypatchAddons(provider, addons); + + yield provider.collectConstantData(); + + let active = provider.getMeasurement("active", 1); + let data = yield active.getValues(); + + do_check_eq(data.days.size, 0); + do_check_eq(data.singular.size, 1); + do_check_true(data.singular.has("addons")); + + let json = data.singular.get("addons")[1]; + let value = JSON.parse(json); + do_check_eq(typeof(value), "object"); + do_check_eq(Object.keys(value).length, 2); + do_check_true("addon0" in value); + do_check_true("addon1" in value); + + let serializer = active.serializer(active.SERIALIZE_JSON); + let serialized = serializer.singular(data.singular); + do_check_eq(typeof(serialized), "object"); + do_check_eq(Object.keys(serialized).length, 2); + do_check_true("addon0" in serialized); + do_check_true("addon1" in serialized); + + let counts = provider.getMeasurement("counts", 1); + data = yield counts.getValues(); + do_check_eq(data.days.size, 1); + do_check_eq(data.singular.size, 0); + do_check_true(data.days.hasDay(now)); + + value = data.days.getDay(now); + do_check_eq(value.size, 2); + do_check_eq(value.get("extension"), 1); + do_check_eq(value.get("plugin"), 1); + + yield provider.shutdown(); + yield storage.close(); +}); + diff --git a/services/healthreport/tests/xpcshell/xpcshell.ini b/services/healthreport/tests/xpcshell/xpcshell.ini index 3c46a411aec4..ed46966f1e25 100644 --- a/services/healthreport/tests/xpcshell/xpcshell.ini +++ b/services/healthreport/tests/xpcshell/xpcshell.ini @@ -6,6 +6,7 @@ tail = [test_profile.js] [test_policy.js] [test_healthreporter.js] +[test_provider_addons.js] [test_provider_appinfo.js] [test_provider_sysinfo.js] [test_provider_sessions.js]