253 lines
8.2 KiB
JavaScript
253 lines
8.2 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import { BaseAction } from "resource://normandy/actions/BaseAction.sys.mjs";
|
|
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
ActionSchemas: "resource://normandy/actions/schemas/index.sys.mjs",
|
|
PrefUtils: "resource://normandy/lib/PrefUtils.sys.mjs",
|
|
PreferenceRollouts: "resource://normandy/lib/PreferenceRollouts.sys.mjs",
|
|
TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
|
|
TelemetryEvents: "resource://normandy/lib/TelemetryEvents.sys.mjs",
|
|
});
|
|
|
|
const PREFERENCE_TYPE_MAP = {
|
|
boolean: Services.prefs.PREF_BOOL,
|
|
string: Services.prefs.PREF_STRING,
|
|
number: Services.prefs.PREF_INT,
|
|
};
|
|
|
|
export class PreferenceRolloutAction extends BaseAction {
|
|
get schema() {
|
|
return lazy.ActionSchemas["preference-rollout"];
|
|
}
|
|
|
|
async _run(recipe) {
|
|
const args = recipe.arguments;
|
|
|
|
// Check if the rollout is on the list of rollouts to stop applying.
|
|
if (lazy.PreferenceRollouts.GRADUATION_SET.has(args.slug)) {
|
|
this.log.debug(
|
|
`Skipping rollout "${args.slug}" because it is in the graduation set.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Determine which preferences are already being managed, to avoid
|
|
// conflicts between recipes. This will throw if there is a problem.
|
|
await this._verifyRolloutPrefs(args);
|
|
|
|
const newRollout = {
|
|
slug: args.slug,
|
|
state: "active",
|
|
preferences: args.preferences.map(({ preferenceName, value }) => ({
|
|
preferenceName,
|
|
value,
|
|
previousValue: lazy.PrefUtils.getPref(preferenceName, {
|
|
branch: "default",
|
|
}),
|
|
})),
|
|
};
|
|
|
|
const existingRollout = await lazy.PreferenceRollouts.get(args.slug);
|
|
if (existingRollout) {
|
|
const anyChanged = await this._updatePrefsForExistingRollout(
|
|
existingRollout,
|
|
newRollout
|
|
);
|
|
|
|
// If anything was different about the new rollout, write it to the db and send an event about it
|
|
if (anyChanged) {
|
|
await lazy.PreferenceRollouts.update(newRollout);
|
|
lazy.TelemetryEvents.sendEvent(
|
|
"update",
|
|
"preference_rollout",
|
|
args.slug,
|
|
{
|
|
previousState: existingRollout.state,
|
|
}
|
|
);
|
|
|
|
switch (existingRollout.state) {
|
|
case lazy.PreferenceRollouts.STATE_ACTIVE: {
|
|
this.log.debug(`Updated preference rollout ${args.slug}`);
|
|
break;
|
|
}
|
|
case lazy.PreferenceRollouts.STATE_GRADUATED: {
|
|
this.log.debug(`Ungraduated preference rollout ${args.slug}`);
|
|
lazy.TelemetryEnvironment.setExperimentActive(
|
|
args.slug,
|
|
newRollout.state,
|
|
{ type: "normandy-prefrollout" }
|
|
);
|
|
break;
|
|
}
|
|
default: {
|
|
console.error(
|
|
new Error(
|
|
`Updated pref rollout in unexpected state: ${existingRollout.state}`
|
|
)
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
this.log.debug(`No updates to preference rollout ${args.slug}`);
|
|
}
|
|
} else {
|
|
// new enrollment
|
|
// Check if this rollout would be a no-op, which is not allowed.
|
|
if (
|
|
newRollout.preferences.every(
|
|
({ value, previousValue }) => value === previousValue
|
|
)
|
|
) {
|
|
lazy.TelemetryEvents.sendEvent(
|
|
"enrollFailed",
|
|
"preference_rollout",
|
|
args.slug,
|
|
{ reason: "would-be-no-op" }
|
|
);
|
|
// Throw so that this recipe execution is marked as a failure
|
|
throw new Error(
|
|
`New rollout ${args.slug} does not change any preferences.`
|
|
);
|
|
}
|
|
|
|
await lazy.PreferenceRollouts.add(newRollout);
|
|
|
|
for (const { preferenceName, value } of args.preferences) {
|
|
lazy.PrefUtils.setPref(preferenceName, value, { branch: "default" });
|
|
}
|
|
|
|
this.log.debug(`Enrolled in preference rollout ${args.slug}`);
|
|
lazy.TelemetryEnvironment.setExperimentActive(
|
|
args.slug,
|
|
newRollout.state,
|
|
{
|
|
type: "normandy-prefrollout",
|
|
}
|
|
);
|
|
lazy.TelemetryEvents.sendEvent(
|
|
"enroll",
|
|
"preference_rollout",
|
|
args.slug,
|
|
{}
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check that all the preferences in a rollout are ok to set. This means 1) no
|
|
* other rollout is managing them, and 2) they match the types of the builtin
|
|
* values.
|
|
* @param {PreferenceRollout} rollout The arguments from a rollout recipe.
|
|
* @throws If the preferences are not valid, with details in the error message.
|
|
*/
|
|
async _verifyRolloutPrefs({ slug, preferences }) {
|
|
const existingManagedPrefs = new Set();
|
|
for (const rollout of await lazy.PreferenceRollouts.getAllActive()) {
|
|
if (rollout.slug === slug) {
|
|
continue;
|
|
}
|
|
for (const prefSpec of rollout.preferences) {
|
|
existingManagedPrefs.add(prefSpec.preferenceName);
|
|
}
|
|
}
|
|
|
|
for (const prefSpec of preferences) {
|
|
if (existingManagedPrefs.has(prefSpec.preferenceName)) {
|
|
lazy.TelemetryEvents.sendEvent(
|
|
"enrollFailed",
|
|
"preference_rollout",
|
|
slug,
|
|
{
|
|
reason: "conflict",
|
|
preference: prefSpec.preferenceName,
|
|
}
|
|
);
|
|
// Throw so that this recipe execution is marked as a failure
|
|
throw new Error(
|
|
`Cannot start rollout ${slug}. Preference ${prefSpec.preferenceName} is already managed.`
|
|
);
|
|
}
|
|
const existingPrefType = Services.prefs.getPrefType(
|
|
prefSpec.preferenceName
|
|
);
|
|
const rolloutPrefType = PREFERENCE_TYPE_MAP[typeof prefSpec.value];
|
|
|
|
if (
|
|
existingPrefType !== Services.prefs.PREF_INVALID &&
|
|
existingPrefType !== rolloutPrefType
|
|
) {
|
|
lazy.TelemetryEvents.sendEvent(
|
|
"enrollFailed",
|
|
"preference_rollout",
|
|
slug,
|
|
{
|
|
reason: "invalid type",
|
|
preference: prefSpec.preferenceName,
|
|
}
|
|
);
|
|
// Throw so that this recipe execution is marked as a failure
|
|
throw new Error(
|
|
`Cannot start rollout "${slug}" on "${prefSpec.preferenceName}". ` +
|
|
`Existing preference is of type ${existingPrefType}, but rollout ` +
|
|
`specifies type ${rolloutPrefType}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async _updatePrefsForExistingRollout(existingRollout, newRollout) {
|
|
let anyChanged = false;
|
|
const oldPrefSpecs = new Map(
|
|
existingRollout.preferences.map(p => [p.preferenceName, p])
|
|
);
|
|
const newPrefSpecs = new Map(
|
|
newRollout.preferences.map(p => [p.preferenceName, p])
|
|
);
|
|
|
|
// Check for any preferences that no longer exist, and un-set them.
|
|
for (const { preferenceName, previousValue } of oldPrefSpecs.values()) {
|
|
if (!newPrefSpecs.has(preferenceName)) {
|
|
this.log.debug(
|
|
`updating ${existingRollout.slug}: ${preferenceName} no longer exists`
|
|
);
|
|
anyChanged = true;
|
|
lazy.PrefUtils.setPref(preferenceName, previousValue, {
|
|
branch: "default",
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for any preferences that are new and need added, or changed and need updated.
|
|
for (const prefSpec of Object.values(newRollout.preferences)) {
|
|
let oldValue = null;
|
|
if (oldPrefSpecs.has(prefSpec.preferenceName)) {
|
|
let oldPrefSpec = oldPrefSpecs.get(prefSpec.preferenceName);
|
|
oldValue = oldPrefSpec.value;
|
|
|
|
// Trust the old rollout for the values of `previousValue`, but don't
|
|
// consider this a change, since it already matches the DB, and doesn't
|
|
// have any other stateful effect.
|
|
prefSpec.previousValue = oldPrefSpec.previousValue;
|
|
}
|
|
if (oldValue !== newPrefSpecs.get(prefSpec.preferenceName).value) {
|
|
anyChanged = true;
|
|
this.log.debug(
|
|
`updating ${existingRollout.slug}: ${prefSpec.preferenceName} value changed from ${oldValue} to ${prefSpec.value}`
|
|
);
|
|
lazy.PrefUtils.setPref(prefSpec.preferenceName, prefSpec.value, {
|
|
branch: "default",
|
|
});
|
|
}
|
|
}
|
|
return anyChanged;
|
|
}
|
|
|
|
async _finalize() {
|
|
await lazy.PreferenceRollouts.saveStartupPrefs();
|
|
}
|
|
}
|