mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-29 07:42:04 +00:00
Bug 824528 - Health report provider for collecting add-on info; r=rnewman
This commit is contained in:
parent
ec15ad7dd0
commit
2a71ccc127
@ -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
|
||||
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
119
services/healthreport/tests/xpcshell/test_provider_addons.js
Normal file
119
services/healthreport/tests/xpcshell/test_provider_addons.js
Normal file
@ -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();
|
||||
});
|
||||
|
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user