зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1868676 - Part 2: Include experiment configuration in targeting snapshot. r=barret,mpohle,application-update-reviewers,bytesized,TravisLong
This does 2 things: 1. It includes the current experiment configuration in the targeting snapshots written by browsing profiles. This will allow to target experiments in the background update task based on the experiments that the default browsing profile is enrolled into. Hopefully this will avoid tricky `setPref` based coordination problems like the one that caused Bug 1852093. 2. It instruments the targeting snapshot experiment configuration during the background update, using the Glean experiments API, and thereby including it in all Glean pings (including `background-update` pings). Hopefully this will allow easier analysis; we've had a difficult time joining background update tasks through to browsing profiles in particular experiments, and this should co-locate the required data, allowing to inspect only background update pings. N.b. `defineProperty` without `enumerable: true` means that `Object.keys(...)` does not properly iterate the properties. Presumably this was an oversight. Differential Revision: https://phabricator.services.mozilla.com/D200125
This commit is contained in:
Родитель
2814902b49
Коммит
987827d807
|
@ -17,6 +17,9 @@
|
|||
const { ASRouterTargeting } = ChromeUtils.importESModule(
|
||||
"resource:///modules/asrouter/ASRouterTargeting.sys.mjs"
|
||||
);
|
||||
const { ExperimentFakes } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/NimbusTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
// These randomization IDs were extracted by hand from Firefox instances.
|
||||
// Randomization is sufficiently stable to hard-code these IDs rather than
|
||||
|
@ -37,11 +40,12 @@ const BRANCH_MAP = {
|
|||
setupProfileService();
|
||||
|
||||
let taskProfile;
|
||||
let manager;
|
||||
|
||||
// Arrange a dummy Remote Settings server so that no non-local network
|
||||
// connections are opened.
|
||||
// And arrange dummy task profile.
|
||||
add_setup(() => {
|
||||
add_setup(async () => {
|
||||
info("Setting up profile service");
|
||||
let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
|
||||
Ci.nsIToolkitProfileService
|
||||
|
@ -57,6 +61,23 @@ add_setup(() => {
|
|||
registerCleanupFunction(() => {
|
||||
taskProfile.remove(true);
|
||||
});
|
||||
|
||||
// Arrange fake experiment enrollment details.
|
||||
manager = ExperimentFakes.manager();
|
||||
|
||||
await manager.onStartup();
|
||||
await manager.store.addEnrollment(ExperimentFakes.experiment("foo"));
|
||||
manager.unenroll("foo", "some-reason");
|
||||
await manager.store.addEnrollment(
|
||||
ExperimentFakes.experiment("bar", { active: false })
|
||||
);
|
||||
await manager.store.addEnrollment(
|
||||
ExperimentFakes.experiment("baz", { active: true })
|
||||
);
|
||||
|
||||
manager.store.addEnrollment(ExperimentFakes.rollout("rol1"));
|
||||
manager.unenroll("rol1", "some-reason");
|
||||
manager.store.addEnrollment(ExperimentFakes.rollout("rol2"));
|
||||
});
|
||||
|
||||
function resetProfile(profile) {
|
||||
|
@ -264,6 +285,14 @@ const TARGETING_LIST = [
|
|||
// Filter based on `defaultProfile` targeting snapshot.
|
||||
["(currentDate|date - defaultProfile.currentDate|date) > 0", 1],
|
||||
["(currentDate|date - defaultProfile.currentDate|date) > 999999", 0],
|
||||
// Filter based on `defaultProfile` experiment enrollment details.
|
||||
["'baz' in defaultProfile.activeExperiments", 1],
|
||||
["'bar' in defaultProfile.previousExperiments", 1],
|
||||
["'rol2' in defaultProfile.activeRollouts", 1],
|
||||
["'rol1' in defaultProfile.previousRollouts", 1],
|
||||
["defaultProfile.enrollmentsMap['baz'] == 'treatment'", 1],
|
||||
["defaultProfile.enrollmentsMap['bar'] == 'treatment'", 1],
|
||||
["'unknown' in defaultProfile.enrollmentsMap", 0],
|
||||
];
|
||||
|
||||
// Test that background tasks targeting works for Nimbus experiments.
|
||||
|
@ -278,7 +307,7 @@ add_task(async function test_backgroundtask_Nimbus_targeting() {
|
|||
firefoxVersion: ASRouterTargeting.Environment.firefoxVersion,
|
||||
};
|
||||
let targetSnapshot = await ASRouterTargeting.getEnvironmentSnapshot({
|
||||
targets: [target],
|
||||
targets: [manager.createTargetingContext(), target],
|
||||
});
|
||||
|
||||
for (let [targeting, expectedLength] of TARGETING_LIST) {
|
||||
|
@ -334,7 +363,7 @@ add_task(async function test_backgroundtask_Messaging_targeting() {
|
|||
firefoxVersion: ASRouterTargeting.Environment.firefoxVersion,
|
||||
};
|
||||
let targetSnapshot = await ASRouterTargeting.getEnvironmentSnapshot({
|
||||
targets: [target],
|
||||
targets: [manager.createTargetingContext(), target],
|
||||
});
|
||||
|
||||
for (let [targeting, expectedLength] of TARGETING_LIST) {
|
||||
|
|
|
@ -102,18 +102,21 @@ export class _ExperimentManager {
|
|||
},
|
||||
};
|
||||
Object.defineProperty(context, "activeExperiments", {
|
||||
enumerable: true,
|
||||
get: async () => {
|
||||
await this.store.ready();
|
||||
return this.store.getAllActiveExperiments().map(exp => exp.slug);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(context, "activeRollouts", {
|
||||
enumerable: true,
|
||||
get: async () => {
|
||||
await this.store.ready();
|
||||
return this.store.getAllActiveRollouts().map(rollout => rollout.slug);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(context, "previousExperiments", {
|
||||
enumerable: true,
|
||||
get: async () => {
|
||||
await this.store.ready();
|
||||
return this.store
|
||||
|
@ -123,6 +126,7 @@ export class _ExperimentManager {
|
|||
},
|
||||
});
|
||||
Object.defineProperty(context, "previousRollouts", {
|
||||
enumerable: true,
|
||||
get: async () => {
|
||||
await this.store.ready();
|
||||
return this.store
|
||||
|
@ -132,12 +136,14 @@ export class _ExperimentManager {
|
|||
},
|
||||
});
|
||||
Object.defineProperty(context, "enrollments", {
|
||||
enumerable: true,
|
||||
get: async () => {
|
||||
await this.store.ready();
|
||||
return this.store.getAll().map(enrollment => enrollment.slug);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(context, "enrollmentsMap", {
|
||||
enumerable: true,
|
||||
get: async () => {
|
||||
await this.store.ready();
|
||||
return this.store.getAll().reduce((acc, enrollment) => {
|
||||
|
|
|
@ -15,6 +15,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
|
||||
"resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
|
||||
BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs",
|
||||
ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs",
|
||||
JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
|
||||
TaskScheduler: "resource://gre/modules/TaskScheduler.sys.mjs",
|
||||
UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
|
||||
|
@ -777,7 +778,12 @@ export var BackgroundUpdate = {
|
|||
// to restrict the targeting data collected, but it's hard to
|
||||
// distinguish expensive collection operations and the system loses
|
||||
// flexibility when restrictions of this type are added.
|
||||
let latestData = await lazy.ASRouterTargeting.getEnvironmentSnapshot();
|
||||
let latestData = await lazy.ASRouterTargeting.getEnvironmentSnapshot({
|
||||
targets: [
|
||||
lazy.ExperimentManager.createTargetingContext(),
|
||||
lazy.ASRouterTargeting.Environment,
|
||||
],
|
||||
});
|
||||
// We expect to always have data, but: belt-and-braces.
|
||||
if (snapshot?.data?.environment) {
|
||||
Object.assign(snapshot.data.environment, latestData.environment);
|
||||
|
@ -790,7 +796,10 @@ export var BackgroundUpdate = {
|
|||
|
||||
// We don't `load`, since we don't care about reading existing (now stale)
|
||||
// data.
|
||||
snapshot.data = await lazy.ASRouterTargeting.getEnvironmentSnapshot();
|
||||
snapshot.data = await lazy.ASRouterTargeting.getEnvironmentSnapshot(
|
||||
lazy.ASRouterTargeting.Environment,
|
||||
lazy.ExperimentManager.createTargetingContext()
|
||||
);
|
||||
|
||||
// Persist.
|
||||
snapshot.saveSoon();
|
||||
|
@ -883,26 +892,74 @@ export var BackgroundUpdate = {
|
|||
defaultProfileTargetingSnapshot.version
|
||||
);
|
||||
}
|
||||
if (defaultProfileTargetingSnapshot?.environment?.firefoxVersion) {
|
||||
Glean.backgroundUpdate.targetingEnvFirefoxVersion.set(
|
||||
defaultProfileTargetingSnapshot.environment.firefoxVersion
|
||||
);
|
||||
}
|
||||
if (defaultProfileTargetingSnapshot?.environment?.currentDate) {
|
||||
Glean.backgroundUpdate.targetingEnvCurrentDate.set(
|
||||
// Glean date times are provided in nanoseconds, `getTime()` yields
|
||||
// milliseconds (after the Unix epoch).
|
||||
new Date(
|
||||
defaultProfileTargetingSnapshot.environment.currentDate
|
||||
).getTime() * 1000
|
||||
);
|
||||
}
|
||||
if (defaultProfileTargetingSnapshot?.environment?.profileAgeCreated) {
|
||||
Glean.backgroundUpdate.targetingEnvProfileAge.set(
|
||||
// Glean date times are provided in nanoseconds, `profileAgeCreated`
|
||||
// is in milliseconds (after the Unix epoch).
|
||||
defaultProfileTargetingSnapshot.environment.profileAgeCreated * 1000
|
||||
);
|
||||
|
||||
let environment = defaultProfileTargetingSnapshot?.environment;
|
||||
if (environment) {
|
||||
if (environment.firefoxVersion) {
|
||||
Glean.backgroundUpdate.targetingEnvFirefoxVersion.set(
|
||||
environment.firefoxVersion
|
||||
);
|
||||
}
|
||||
if (environment.currentDate) {
|
||||
Glean.backgroundUpdate.targetingEnvCurrentDate.set(
|
||||
// Glean date times are provided in nanoseconds, `getTime()` yields
|
||||
// milliseconds (after the Unix epoch).
|
||||
new Date(environment.currentDate).getTime() * 1000
|
||||
);
|
||||
}
|
||||
if (environment.profileAgeCreated) {
|
||||
Glean.backgroundUpdate.targetingEnvProfileAge.set(
|
||||
// Glean date times are provided in nanoseconds, `profileAgeCreated`
|
||||
// is in milliseconds (after the Unix epoch).
|
||||
environment.profileAgeCreated * 1000
|
||||
);
|
||||
}
|
||||
|
||||
// Experiment details.
|
||||
let activeExperiments = (
|
||||
environment.activeExperiments || []
|
||||
).toSorted();
|
||||
let activeRollouts = (environment.activeRollouts || []).toSorted();
|
||||
let previousExperiments = (
|
||||
environment.previousExperiments || []
|
||||
).toSorted();
|
||||
let previousRollouts = (environment.previousRollouts || []).toSorted();
|
||||
|
||||
// Add default profile experiments to background task profile Glean experiments.
|
||||
for (let slug of Object.keys(environment.enrollmentsMap || [])) {
|
||||
let branch = environment.enrollmentsMap[slug];
|
||||
let source = "defaultProfile";
|
||||
|
||||
// Experiments have type "nimbus-nimbus", rollouts type "nimbus-rollout".
|
||||
let type;
|
||||
if (
|
||||
activeExperiments.includes(slug) ||
|
||||
previousExperiments.includes(slug)
|
||||
) {
|
||||
type = "nimbus-nimbus";
|
||||
} else if (
|
||||
activeRollouts.includes(slug) ||
|
||||
previousRollouts.includes(slug)
|
||||
) {
|
||||
type = "nimbus-rollout";
|
||||
} else {
|
||||
// This shouldn't happen, but it's not worth failing.
|
||||
lazy.log.warn(
|
||||
`${SLUG}: enrollment not recognized as experiment or rollout: '${slug}'`
|
||||
);
|
||||
type = "nimbus-unexpected";
|
||||
}
|
||||
|
||||
let extras = { type, source };
|
||||
Services.fog.setExperimentActive(slug, branch, extras);
|
||||
|
||||
if (
|
||||
previousExperiments.includes(slug) ||
|
||||
previousRollouts.includes(slug)
|
||||
) {
|
||||
Services.fog.setExperimentInactive(slug, branch, extras);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (f) {
|
||||
if (DOMException.isInstance(f) && f.name === "NotFoundError") {
|
||||
|
|
|
@ -9,10 +9,12 @@
|
|||
const { ASRouterTargeting } = ChromeUtils.importESModule(
|
||||
"resource:///modules/asrouter/ASRouterTargeting.sys.mjs"
|
||||
);
|
||||
|
||||
const { BackgroundUpdate } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/BackgroundUpdate.sys.mjs"
|
||||
);
|
||||
const { ExperimentFakes } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/NimbusTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
const { maybeSubmitBackgroundUpdatePing } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/backgroundtasks/BackgroundTask_backgroundupdate.sys.mjs"
|
||||
|
@ -169,8 +171,26 @@ add_task(async function test_targeting_exists() {
|
|||
profileAgeCreated: ASRouterTargeting.Environment.profileAgeCreated,
|
||||
firefoxVersion: ASRouterTargeting.Environment.firefoxVersion,
|
||||
};
|
||||
|
||||
// Arrange fake experiment enrollment details.
|
||||
const manager = ExperimentFakes.manager();
|
||||
|
||||
await manager.onStartup();
|
||||
await manager.store.addEnrollment(ExperimentFakes.experiment("foo"));
|
||||
manager.unenroll("foo", "some-reason");
|
||||
await manager.store.addEnrollment(
|
||||
ExperimentFakes.experiment("bar", { active: false })
|
||||
);
|
||||
await manager.store.addEnrollment(
|
||||
ExperimentFakes.experiment("baz", { active: true })
|
||||
);
|
||||
|
||||
manager.store.addEnrollment(ExperimentFakes.rollout("rol1"));
|
||||
manager.unenroll("rol1", "some-reason");
|
||||
manager.store.addEnrollment(ExperimentFakes.rollout("rol2"));
|
||||
|
||||
let targetSnapshot = await ASRouterTargeting.getEnvironmentSnapshot({
|
||||
targets: [target],
|
||||
targets: [manager.createTargetingContext(), target],
|
||||
});
|
||||
|
||||
await do_readTargeting(JSON.stringify(targetSnapshot), reason => {
|
||||
|
@ -222,5 +242,34 @@ add_task(async function test_targeting_exists() {
|
|||
targetCurrentDate.setHours(0, 0, 0, 0);
|
||||
|
||||
Assert.equal(targetCurrentDate.toISOString(), currentDate.toISOString());
|
||||
|
||||
// Verify active experiments.
|
||||
Assert.deepEqual(
|
||||
{
|
||||
branch: "treatment",
|
||||
extra: { source: "defaultProfile", type: "nimbus-nimbus" },
|
||||
},
|
||||
Services.fog.testGetExperimentData("baz"),
|
||||
"experiment data for active experiment 'baz' is correct"
|
||||
);
|
||||
|
||||
Assert.deepEqual(
|
||||
{
|
||||
branch: "treatment",
|
||||
extra: { source: "defaultProfile", type: "nimbus-rollout" },
|
||||
},
|
||||
Services.fog.testGetExperimentData("rol2"),
|
||||
"experiment data for active experiment 'rol2' is correct"
|
||||
);
|
||||
|
||||
// Bug 1879247: there is currently no API (even test-only) to get experiment
|
||||
// data for inactive experiments.
|
||||
for (let inactive of ["bar", "foo", "rol1"]) {
|
||||
Assert.equal(
|
||||
null,
|
||||
Services.fog.testGetExperimentData(inactive),
|
||||
`no experiment data for inactive experiment '${inactive}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче