Bug 1014524 - Report update hotfix results in FHR; r=rnewman

The v20140527.01 update hotfix deposited a JSON file in the profile
directory containing state. This file was purposefully not deleted
during hotfix uninstall so its contents could be later reported in
FHR.

This patch adds that reporting to FHR.

Currently, we only report a subset of the available fields. The
remaining "public" fields could be added easily enough. For now, it is
important that we capture the important ones.

--HG--
extra : rebase_source : 925d6ea04118296108327dc843323b150b87b208
This commit is contained in:
Gregory Szorc 2014-07-25 12:25:06 -07:00
parent 01db3688f7
commit 93ba218ccc
4 changed files with 381 additions and 0 deletions

View File

@ -1245,6 +1245,55 @@ the attempt, even if the result occurred on a different day from the attempt.
Therefore, the sum of the result counts should equal the result of the attempt
counts.
org.mozilla.hotfix.update
-------------------------
This measurement contains results from the Firefox update hotfix.
The Firefox update hotfix bypasses the built-in application update mechanism
and installs a modern Firefox.
Version 1
^^^^^^^^^
The fields in this measurement are dynamically created based on which
versions of the update hotfix state file are found on disk.
The general format of the fields is ``<version>.<thing>`` where ``version``
is a hotfix version like ``v20140527`` and ``thing`` is a key from the
hotfix state file, e.g. ``upgradedFrom``. Here are some of the ``things``
that can be defined.
upgradedFrom
String identifying the Firefox version that the hotfix upgraded from.
e.g. ``16.0`` or ``17.0.1``.
uninstallReason
String with enumerated values identifying why the hotfix was uninstalled.
Value will be ``STILL_INSTALLED`` if the hotfix is still installed.
downloadAttempts
Integer number of times the hotfix started downloading an installer.
Download resumes are part of this count.
downloadFailures
Integer count of times a download supposedly completed but couldn't
be validated. This likely represents something wrong with the network
connection. The ratio of this to ``downloadAttempts`` should be low.
installAttempts
Integer count of times the hotfix attempted to run the installer.
This should ideally be 1. It should only be greater than 1 if UAC
elevation was cancelled or not allowed.
installFailures
Integer count of total installation failures this client experienced.
Can be 0. ``installAttempts - installFailures`` implies install successes.
notificationsShown
Integer count of times a notification was displayed to the user that
they are running an older Firefox.
org.mozilla.places.places
-------------------------

View File

@ -23,6 +23,7 @@ this.EXPORTED_SYMBOLS = [
"CrashesProvider",
#endif
"HealthReportProvider",
"HotfixProvider",
"PlacesProvider",
"SearchesProvider",
"SessionsProvider",
@ -1127,6 +1128,157 @@ CrashesProvider.prototype = Object.freeze({
#endif
/**
* Records data from update hotfixes.
*
* This measurement has dynamic fields. Field names are of the form
* <version>.<thing> where <version> is the hotfix version that produced
* the data. e.g. "v20140527". The sub-version of the hotfix is omitted
* because hotfixes can go through multiple minor versions during development
* and we don't want to introduce more fields than necessary. Furthermore,
* the subsequent dots make parsing field names slightly harder. By stripping,
* we can just split on the first dot.
*/
function UpdateHotfixMeasurement1() {
Metrics.Measurement.call(this);
}
UpdateHotfixMeasurement1.prototype = Object.freeze({
__proto__: Metrics.Measurement.prototype,
name: "update",
version: 1,
hotfixFieldTypes: {
"upgradedFrom": Metrics.Storage.FIELD_LAST_TEXT,
"uninstallReason": Metrics.Storage.FIELD_LAST_TEXT,
"downloadAttempts": Metrics.Storage.FIELD_LAST_NUMERIC,
"downloadFailures": Metrics.Storage.FIELD_LAST_NUMERIC,
"installAttempts": Metrics.Storage.FIELD_LAST_NUMERIC,
"installFailures": Metrics.Storage.FIELD_LAST_NUMERIC,
"notificationsShown": Metrics.Storage.FIELD_LAST_NUMERIC,
},
fields: { },
// Our fields have dynamic names from the hotfix version that supplied them.
// We need to override the default behavior to deal with unknown fields.
shouldIncludeField: function (name) {
return name.contains(".");
},
fieldType: function (name) {
for (let known in this.hotfixFieldTypes) {
if (name.endsWith(known)) {
return this.hotfixFieldTypes[known];
}
}
return Metrics.Measurement.prototype.fieldType.call(this, name);
},
});
this.HotfixProvider = function () {
Metrics.Provider.call(this);
};
HotfixProvider.prototype = Object.freeze({
__proto__: Metrics.Provider.prototype,
name: "org.mozilla.hotfix",
measurementTypes: [
UpdateHotfixMeasurement1,
],
pullOnly: true,
collectDailyData: function () {
return this.storage.enqueueTransaction(this._populateHotfixData.bind(this));
},
_populateHotfixData: function* () {
let m = this.getMeasurement("update", 1);
// The update hotfix retains its JSON state file after uninstall.
// The initial update hotfix had a hard-coded filename. We treat it
// specially. Subsequent update hotfixes named their files in a
// recognizeable pattern so we don't need to update this probe code to
// know about them.
let files = [
["v20140527", OS.Path.join(OS.Constants.Path.profileDir,
"hotfix.v20140527.01.json")],
];
let it = new OS.File.DirectoryIterator(OS.Constants.Path.profileDir);
try {
yield it.forEach((e, index, it) => {
let m = e.name.match(/^updateHotfix\.([a-zA-Z0-9]+)\.json$/);
if (m) {
files.push([m[1], e.path]);
}
});
} finally {
it.close();
}
let decoder = new TextDecoder();
for (let e of files) {
let [version, path] = e;
let p;
try {
let data = yield OS.File.read(path);
p = JSON.parse(decoder.decode(data));
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
continue;
} catch (ex) {
this._log.warn("Error loading update hotfix payload: " + ex.message);
}
// Wrap just in case.
try {
for (let k in m.hotfixFieldTypes) {
if (!(k in p)) {
continue;
}
let value = p[k];
if (value === null && k == "uninstallReason") {
value = "STILL_INSTALLED";
}
let field = version + "." + k;
let fieldType;
let storageOp;
switch (typeof(value)) {
case "string":
fieldType = this.storage.FIELD_LAST_TEXT;
storageOp = "setLastTextFromFieldID";
break;
case "number":
fieldType = this.storage.FIELD_LAST_NUMERIC;
storageOp = "setLastNumericFromFieldID";
break;
default:
this._log.warn("Unknown value in hotfix state: " + k + "=" + value);
continue;
}
if (this.storage.hasFieldFromMeasurement(m.id, field, fieldType)) {
let fieldID = this.storage.fieldIDFromMeasurement(m.id, field);
yield this.storage[storageOp](fieldID, value);
} else {
let fieldID = yield this.storage.registerField(m.id, field,
fieldType);
yield this.storage[storageOp](fieldID, value);
}
}
} catch (ex) {
this._log.warn("Error processing update hotfix data: " + ex);
}
}
},
});
/**
* Holds basic statistics about the Places database.

View File

@ -0,0 +1,179 @@
/* 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/osfile.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
const EXAMPLE_2014052701 = {
"upgradedFrom":"13.0.1",
"uninstallReason":"SUCCESSFUL_UPGRADE",
"_entityID":null,
"forensicsID":"29525548-b653-49db-bfb8-a160cdfbeb4a",
"_installInProgress":false,
"_everCompatible":true,
"reportedWindowsVersion":[6,1,1],
"actualWindowsVersion":[6,1,1],
"firstNotifyDay":0,
"lastNotifyDay":0,
"downloadAttempts":1,
"downloadFailures":0,
"installAttempts":1,
"installSuccesses":1,
"installLauncherFailures":0,
"installFailures":0,
"notificationsShown":0,
"notificationsClicked":0,
"notificationsDismissed":0,
"notificationsRemoved":0,
"launcherExitCodes":{"0":1}
};
function run_test() {
run_next_test();
}
add_task(function* init() {
do_get_profile();
});
add_task(function test_constructor() {
new HotfixProvider();
});
add_task(function* test_init() {
let storage = yield Metrics.Storage("init");
let provider = new HotfixProvider();
yield provider.init(storage);
yield provider.shutdown();
yield storage.close();
});
add_task(function* test_collect_empty() {
let storage = yield Metrics.Storage("collect_empty");
let provider = new HotfixProvider();
yield provider.init(storage);
yield provider.collectDailyData();
let m = provider.getMeasurement("update", 1);
let data = yield m.getValues();
Assert.equal(data.singular.size, 0);
Assert.equal(data.days.size, 0);
yield storage.close();
});
add_task(function* test_collect_20140527() {
let storage = yield Metrics.Storage("collect_20140527");
let provider = new HotfixProvider();
yield provider.init(storage);
let path = OS.Path.join(OS.Constants.Path.profileDir,
"hotfix.v20140527.01.json");
let encoder = new TextEncoder();
yield OS.File.writeAtomic(path,
encoder.encode(JSON.stringify(EXAMPLE_2014052701)));
yield provider.collectDailyData();
let m = provider.getMeasurement("update", 1);
let data = yield m.getValues();
let s = data.singular;
Assert.equal(s.size, 7);
Assert.equal(s.get("v20140527.upgradedFrom")[1], "13.0.1");
Assert.equal(s.get("v20140527.uninstallReason")[1], "SUCCESSFUL_UPGRADE");
Assert.equal(s.get("v20140527.downloadAttempts")[1], 1);
Assert.equal(s.get("v20140527.downloadFailures")[1], 0);
Assert.equal(s.get("v20140527.installAttempts")[1], 1);
Assert.equal(s.get("v20140527.installFailures")[1], 0);
Assert.equal(s.get("v20140527.notificationsShown")[1], 0);
// Ensure the dynamic fields get serialized.
let serializer = m.serializer(m.SERIALIZE_JSON);
let d = serializer.singular(s);
Assert.deepEqual(d, {
"_v": 1,
"v20140527.upgradedFrom": "13.0.1",
"v20140527.uninstallReason": "SUCCESSFUL_UPGRADE",
"v20140527.downloadAttempts": 1,
"v20140527.downloadFailures": 0,
"v20140527.installAttempts": 1,
"v20140527.installFailures": 0,
"v20140527.notificationsShown": 0,
});
// Don't interfere with next test.
yield OS.File.remove(path);
yield storage.close();
});
add_task(function* test_collect_multiple_versions() {
let storage = yield Metrics.Storage("collect_multiple_versions");
let provider = new HotfixProvider();
yield provider.init(storage);
let p1 = {
upgradedFrom: "12.0",
uninstallReason: "SUCCESSFUL_UPGRADE",
downloadAttempts: 3,
downloadFailures: 1,
installAttempts: 1,
installFailures: 1,
notificationsShown: 2,
};
let p2 = {
downloadAttempts: 5,
downloadFailures: 3,
installAttempts: 2,
installFailures: 2,
uninstallReason: null,
notificationsShown: 1,
};
let path1 = OS.Path.join(OS.Constants.Path.profileDir, "updateHotfix.v20140601.json");
let path2 = OS.Path.join(OS.Constants.Path.profileDir, "updateHotfix.v20140701.json");
let encoder = new TextEncoder();
yield OS.File.writeAtomic(path1, encoder.encode(JSON.stringify(p1)));
yield OS.File.writeAtomic(path2, encoder.encode(JSON.stringify(p2)));
yield provider.collectDailyData();
let m = provider.getMeasurement("update", 1);
let data = yield m.getValues();
let serializer = m.serializer(m.SERIALIZE_JSON);
let d = serializer.singular(data.singular);
Assert.deepEqual(d, {
"_v": 1,
"v20140601.upgradedFrom": "12.0",
"v20140601.uninstallReason": "SUCCESSFUL_UPGRADE",
"v20140601.downloadAttempts": 3,
"v20140601.downloadFailures": 1,
"v20140601.installAttempts": 1,
"v20140601.installFailures": 1,
"v20140601.notificationsShown": 2,
"v20140701.uninstallReason": "STILL_INSTALLED",
"v20140701.downloadAttempts": 5,
"v20140701.downloadFailures": 3,
"v20140701.installAttempts": 2,
"v20140701.installFailures": 2,
"v20140701.notificationsShown": 1,
});
// Don't interfere with next test.
yield OS.File.remove(path1);
yield OS.File.remove(path2);
yield storage.close();
});

View File

@ -10,6 +10,7 @@ skip-if = buildapp == 'mulet'
[test_provider_appinfo.js]
[test_provider_crashes.js]
run-if = crashreporter
[test_provider_hotfix.js]
[test_provider_places.js]
[test_provider_searches.js]
[test_provider_sysinfo.js]