Bug 974024 - Add FHR recording of Telemetry Experiments activity; r=bsmedberg

--HG--
extra : rebase_source : 1e875e53da49c69194ee740898ff943d1801d1cf
This commit is contained in:
Gregory Szorc 2014-03-20 14:16:00 -07:00
parent 8f8b2403d4
commit beddc2399c
7 changed files with 373 additions and 0 deletions

View File

@ -6,6 +6,7 @@
this.EXPORTED_SYMBOLS = [
"Experiments",
"ExperimentsProvider",
];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
@ -19,6 +20,7 @@ Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
"resource://gre/modules/UpdateChannel.jsm");
@ -388,6 +390,45 @@ Experiments.Experiments.prototype = {
);
},
/**
* Determine whether another date has the same UTC day as now().
*/
_dateIsTodayUTC: function (d) {
let now = this._policy.now();
return stripDateToMidnight(now).getTime() == stripDateToMidnight(d).getTime();
},
/**
* Obtain the entry of the most recent active experiment that was active
* today.
*
* If no experiment was active today, this resolves to nothing.
*
* Assumption: Only a single experiment can be active at a time.
*
* @return Promise<object>
*/
lastActiveToday: function () {
return Task.spawn(function* getMostRecentActiveExperimentTask() {
let experiments = yield this.getExperiments();
// Assumption: Ordered chronologically, descending, with active always
// first.
for (let experiment of experiments) {
if (experiment.active) {
return experiment;
}
if (experiment.endDate && this._dateIsTodayUTC(experiment.endDate)) {
return experiment;
}
}
return null;
}.bind(this));
},
/**
* Fetch an updated list of experiments and trigger experiment updates.
* Do only use when experiments are enabled.
@ -1432,3 +1473,105 @@ Experiments.ExperimentEntry.prototype = {
return true;
},
};
/**
* Strip a Date down to its UTC midnight.
*
* This will return a cloned Date object. The original is unchanged.
*/
let stripDateToMidnight = function (d) {
let m = new Date(d);
m.setUTCHours(0, 0, 0, 0);
return m;
};
function ExperimentsLastActiveMeasurement1() {
Metrics.Measurement.call(this);
}
const FIELD_DAILY_LAST_TEXT = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
ExperimentsLastActiveMeasurement1.prototype = Object.freeze({
__proto__: Metrics.Measurement.prototype,
name: "info",
version: 1,
fields: {
lastActive: FIELD_DAILY_LAST_TEXT,
}
});
this.ExperimentsProvider = function () {
Metrics.Provider.call(this);
this._experiments = null;
};
ExperimentsProvider.prototype = Object.freeze({
__proto__: Metrics.Provider.prototype,
name: "org.mozilla.experiments",
measurementTypes: [
ExperimentsLastActiveMeasurement1,
],
_OBSERVERS: [
OBSERVER_TOPIC,
],
postInit: function () {
this._experiments = Experiments.instance();
for (let o of this._OBSERVERS) {
Services.obs.addObserver(this, o, false);
}
return Promise.resolve();
},
onShutdown: function () {
for (let o of this._OBSERVERS) {
Services.obs.removeObserver(this, o);
}
return Promise.resolve();
},
observe: function (subject, topic, data) {
switch (topic) {
case OBSERVER_TOPIC:
this.recordLastActiveExperiment();
break;
}
},
collectDailyData: function () {
return this.recordLastActiveExperiment();
},
recordLastActiveExperiment: function () {
let m = this.getMeasurement(ExperimentsLastActiveMeasurement1.prototype.name,
ExperimentsLastActiveMeasurement1.prototype.version);
return this.enqueueStorageOperation(() => {
return Task.spawn(function* recordTask() {
let todayActive = yield this._experiments.lastActiveToday();
if (!todayActive) {
this._log.info("No active experiment on this day: " +
this._experiments._policy.now());
return;
}
this._log.info("Recording last active experiment: " + todayActive.id);
yield m.setDailyLastText("lastActive", todayActive.id,
this._experiments._policy.now());
}.bind(this));
});
},
});

View File

@ -2,3 +2,7 @@ component {f7800463-3b97-47f9-9341-b7617e6d8d49} ExperimentsService.js
contract @mozilla.org/browser/experiments-service;1 {f7800463-3b97-47f9-9341-b7617e6d8d49}
category update-timer ExperimentsService @mozilla.org/browser/experiments-service;1,getService,experiments-update-timer,experiments.manifest.fetchIntervalSeconds,86400
category profile-after-change ExperimentsService @mozilla.org/browser/experiments-service;1
category healthreport-js-provider-default ExperimentsProvider resource://gre/browser/modules/Experiments/Experiments.jsm

View File

@ -25,6 +25,35 @@ const EXPERIMENT2_ID = "test-experiment-2@tests.mozilla.org"
const EXPERIMENT2_XPI_SHA1 = "sha1:81877991ec70360fb48db84c34a9b2da7aa41d6a";
const EXPERIMENT2_XPI_NAME = "experiment-2.xpi";
const FAKE_EXPERIMENTS_1 = [
{
id: "id1",
name: "experiment1",
description: "experiment 1",
active: true,
detailUrl: "https://dummy/experiment1",
},
];
const FAKE_EXPERIMENTS_2 = [
{
id: "id2",
name: "experiment2",
description: "experiment 2",
active: false,
endDate: new Date(2014, 2, 11, 2, 4, 35, 42).getTime(),
detailUrl: "https://dummy/experiment2",
},
{
id: "id1",
name: "experiment1",
description: "experiment 1",
active: false,
endDate: new Date(2014, 2, 10, 0, 0, 0, 0).getTime(),
detailURL: "https://dummy/experiment1",
},
];
let gAppInfo = null;
function getReporter(name, uri, inspected) {
@ -129,3 +158,18 @@ function createAppInfo(options) {
registrar.registerFactory(XULAPPINFO_CID, "XULAppInfo",
XULAPPINFO_CONTRACTID, XULAppInfoFactory);
}
/**
* Replace the experiments on an Experiments with a new list.
*
* This monkeypatches getExperiments(). It doesn't monkeypatch the internal
* experiments list. So its utility is not as great as it could be.
*/
function replaceExperiments(experiment, list) {
Object.defineProperty(experiment, "getExperiments", {
writable: true,
value: () => {
return Promise.resolve(list);
},
});
}

View File

@ -264,6 +264,33 @@ add_task(function* test_getExperiments() {
yield removeCacheFile();
});
add_task(function* test_lastActiveToday() {
let experiments = new Experiments.Experiments(gPolicy);
replaceExperiments(experiments, FAKE_EXPERIMENTS_1);
let e = yield experiments.getExperiments();
Assert.equal(e.length, 1, "Monkeypatch successful.");
Assert.equal(e[0].id, "id1", "ID looks sane");
Assert.ok(e[0].active, "Experiment is active.");
let lastActive = yield experiments.lastActiveToday();
Assert.equal(e[0], lastActive, "Last active object is expected.");
replaceExperiments(experiments, FAKE_EXPERIMENTS_2);
e = yield experiments.getExperiments();
Assert.equal(e.length, 2, "Monkeypatch successful.");
defineNow(gPolicy, e[0].endDate);
lastActive = yield experiments.lastActiveToday();
Assert.ok(lastActive, "Have a last active experiment");
Assert.equal(lastActive, e[0], "Last active object is expected.");
yield experiments.uninit();
yield removeCacheFile();
});
// Test explicitly disabling experiments.
add_task(function* test_disableExperiment() {
@ -636,6 +663,9 @@ add_task(function* test_userDisabledAndUpdated() {
Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
Assert.equal(list[0].active, true, "Experiment 1 should be active.");
let todayActive = yield experiments.lastActiveToday();
Assert.ok(todayActive, "Last active for today reports a value.");
Assert.equal(todayActive.id, list[0].id, "The entry is what we expect.");
// Explicitly disable an experiment.
@ -649,6 +679,9 @@ add_task(function* test_userDisabledAndUpdated() {
Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
Assert.equal(list[0].active, false, "Experiment should not be active anymore.");
todayActive = yield experiments.lastActiveToday();
Assert.ok(todayActive, "Last active for today still returns a value.");
Assert.equal(todayActive.id, list[0].id, "The ID is still the same.");
// Trigger an update with a faked change for experiment 1.
@ -718,6 +751,9 @@ add_task(function* test_updateActiveExperiment() {
let list = yield experiments.getExperiments();
Assert.equal(list.length, 0, "Experiment list should be empty.");
let todayActive = yield experiments.lastActiveToday();
Assert.equal(todayActive, null, "No experiment active today.");
// Trigger update, clock set for the experiment to start.
now = futureDate(startDate, 10 * MS_IN_ONE_DAY);
@ -731,6 +767,9 @@ add_task(function* test_updateActiveExperiment() {
Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
Assert.equal(list[0].active, true, "Experiment 1 should be active.");
Assert.equal(list[0].name, EXPERIMENT1_NAME, "Experiments name should match.");
todayActive = yield experiments.lastActiveToday();
Assert.ok(todayActive, "todayActive() returns a value.");
Assert.equal(todayActive.id, list[0].id, "It returns the active experiment.");
// Trigger an update for the active experiment by changing it's hash (and xpi)
// in the manifest.
@ -748,6 +787,8 @@ add_task(function* test_updateActiveExperiment() {
Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
Assert.equal(list[0].active, true, "Experiment 1 should still be active.");
Assert.equal(list[0].name, EXPERIMENT1A_NAME, "Experiments name should have been updated.");
todayActive = yield experiments.lastActiveToday();
Assert.equal(todayActive.id, list[0].id, "last active today is still sane.");
// Cleanup.

View File

@ -0,0 +1,110 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource:///modules/experiments/Experiments.jsm");
Cu.import("resource://testing-common/services/healthreport/utils.jsm");
Cu.import("resource://testing-common/services-common/logging.js");
function getStorageAndProvider(name) {
return Task.spawn(function* get() {
let storage = yield Metrics.Storage(name);
let provider = new ExperimentsProvider();
yield provider.init(storage);
return [storage, provider];
});
}
function run_test() {
do_get_profile();
initTestLogging();
run_next_test();
}
add_task(function test_constructor() {
let provider = new ExperimentsProvider();
});
add_task(function* test_init() {
let storage = yield Metrics.Storage("init");
let provider = new ExperimentsProvider();
yield provider.init(storage);
yield provider.shutdown();
yield storage.close();
});
add_task(function* test_collect() {
let [storage, provider] = yield getStorageAndProvider("no_active");
// Initial state should not report anything.
yield provider.collectDailyData();
let m = provider.getMeasurement("info", 1);
let values = yield m.getValues();
Assert.equal(values.days.size, 0, "Have no data if no experiments known.");
// An old experiment that ended today should be reported.
replaceExperiments(provider._experiments, FAKE_EXPERIMENTS_2);
let now = new Date(FAKE_EXPERIMENTS_2[0].endDate);
defineNow(provider._experiments._policy, now.getTime());
yield provider.collectDailyData();
values = yield m.getValues();
Assert.equal(values.days.size, 1, "Have 1 day of data");
Assert.ok(values.days.hasDay(now), "Has day the experiment ended.");
let day = values.days.getDay(now);
Assert.ok(day.has("lastActive"), "Has lastActive field.");
Assert.equal(day.get("lastActive"), "id2", "Last active ID is sane.");
// Making an experiment active replaces the lastActive value.
replaceExperiments(provider._experiments, FAKE_EXPERIMENTS_1);
yield provider.collectDailyData();
values = yield m.getValues();
day = values.days.getDay(now);
Assert.equal(day.get("lastActive"), "id1", "Last active ID is the active experiment.");
// And make sure the observer works.
replaceExperiments(provider._experiments, FAKE_EXPERIMENTS_2);
Services.obs.notifyObservers(null, "experiments-changed", null);
// This may not wait long enough. It relies on the SQL insert happening
// in the same tick as the observer notification.
yield storage.enqueueOperation(function () {
return Promise.resolve();
});
values = yield m.getValues();
day = values.days.getDay(now);
Assert.equal(day.get("lastActive"), "id2", "Last active ID set by observer.");
yield provider.shutdown();
yield storage.close();
});
add_task(function* test_healthreporterJSON() {
let reporter = yield getHealthReporter("healthreporterJSON");
yield reporter.init();
try {
yield reporter._providerManager.registerProvider(new ExperimentsProvider());
let experiments = Experiments.instance();
defineNow(experiments._policy, Date.now());
replaceExperiments(experiments, FAKE_EXPERIMENTS_1);
yield reporter.collectMeasurements();
let payload = yield reporter.getJSONPayload(true);
let today = reporter._formatDate(reporter._policy.now());
Assert.ok(today in payload.data.days, "Day in payload.");
let day = payload.data.days[today];
Assert.ok("org.mozilla.experiments.info" in day, "Measurement present.");
let m = day["org.mozilla.experiments.info"];
Assert.ok("lastActive" in m, "lastActive field present.");
Assert.equal(m["lastActive"], "id1", "Last active ID proper.");
} finally {
reporter._shutdown();
}
});

View File

@ -12,3 +12,4 @@ support-files =
[test_api.js]
[test_conditions.js]
[test_fetch.js]
[test_healthreport.js]

View File

@ -245,6 +245,10 @@ Leading by example::
"google": 1
},
"_v": "4"
},
"org.mozilla.experiment": {
"lastActive": "some.experiment.id"
"_v": "1"
}
}
}
@ -1461,3 +1465,29 @@ Example
"version": "12.2.0"
}
org.mozilla.experiments.info
----------------------------------
Daily measurement reporting information about the Telemetry Experiments service.
Version 1
^^^^^^^^^
Property:
lastActive
ID of the final Telemetry Experiment that is active on a given day, if any.
Example
^^^^^^^
::
"org.mozilla.experiments.info": {
"_v": 1,
"lastActive": "some.experiment.id"
}