Bug 824528 - Health report provider for collecting add-on info; r=rnewman

This commit is contained in:
Gregory Szorc 2013-01-06 14:40:40 -08:00
parent ec15ad7dd0
commit 2a71ccc127
5 changed files with 338 additions and 1 deletions

View File

@ -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

View File

@ -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;
},
});

View File

@ -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();
}

View 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();
});

View File

@ -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]