mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-06 19:29:24 +02:00
This includes some key metrics from the default profile targeting state in the background update Glean telemetry. It will allow us to measure the incidence of missing and malformed targeting state JSON, and how many background update clients have been lapsed for how long. This in turn will help us understand if the lapsed experiment targeting is working correctly. Differential Revision: https://phabricator.services.mozilla.com/D164026
405 lines
13 KiB
JavaScript
405 lines
13 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
|
* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
XPCOMUtils.defineLazyGetter(lazy, "log", () => {
|
|
let { ConsoleAPI } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/Console.sys.mjs"
|
|
);
|
|
let consoleOptions = {
|
|
// tip: set maxLogLevel to "debug" and use log.debug() to create detailed
|
|
// messages during development. See LOG_LEVELS in Console.sys.mjs for details.
|
|
maxLogLevel: "error",
|
|
maxLogLevelPref: "toolkit.backgroundtasks.loglevel",
|
|
prefix: "BackgroundTasksUtils",
|
|
};
|
|
return new ConsoleAPI(consoleOptions);
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"ProfileService",
|
|
"@mozilla.org/toolkit/profile-service;1",
|
|
"nsIToolkitProfileService"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(lazy, {
|
|
ASRouter: "resource://activity-stream/lib/ASRouter.jsm",
|
|
ASRouterDefaultConfig:
|
|
"resource://activity-stream/lib/ASRouterDefaultConfig.jsm",
|
|
ExperimentManager: "resource://nimbus/lib/ExperimentManager.jsm",
|
|
RemoteSettingsExperimentLoader:
|
|
"resource://nimbus/lib/RemoteSettingsExperimentLoader.jsm",
|
|
});
|
|
|
|
class CannotLockProfileError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = "CannotLockProfileError";
|
|
}
|
|
}
|
|
|
|
export var BackgroundTasksUtils = {
|
|
// Manage our own default profile that can be overridden for testing. It's
|
|
// easier to do this here rather than using the profile service itself.
|
|
_defaultProfileInitialized: false,
|
|
_defaultProfile: null,
|
|
|
|
getDefaultProfile() {
|
|
if (!this._defaultProfileInitialized) {
|
|
this._defaultProfileInitialized = true;
|
|
// This is all test-only.
|
|
let defaultProfilePath = Services.env.get(
|
|
"MOZ_BACKGROUNDTASKS_DEFAULT_PROFILE_PATH"
|
|
);
|
|
let noDefaultProfile = Services.env.get(
|
|
"MOZ_BACKGROUNDTASKS_NO_DEFAULT_PROFILE"
|
|
);
|
|
if (defaultProfilePath) {
|
|
lazy.log.info(
|
|
`getDefaultProfile: using default profile path ${defaultProfilePath}`
|
|
);
|
|
var tmpd = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
|
tmpd.initWithPath(defaultProfilePath);
|
|
// Sadly this writes to `profiles.ini`, but there's little to be done.
|
|
this._defaultProfile = lazy.ProfileService.createProfile(
|
|
tmpd,
|
|
`MOZ_BACKGROUNDTASKS_DEFAULT_PROFILE_PATH-${Date.now()}`
|
|
);
|
|
} else if (noDefaultProfile) {
|
|
lazy.log.info(`getDefaultProfile: setting default profile to null`);
|
|
this._defaultProfile = null;
|
|
} else {
|
|
try {
|
|
lazy.log.info(
|
|
`getDefaultProfile: using ProfileService.defaultProfile`
|
|
);
|
|
this._defaultProfile = lazy.ProfileService.defaultProfile;
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
return this._defaultProfile;
|
|
},
|
|
|
|
hasDefaultProfile() {
|
|
return this.getDefaultProfile() != null;
|
|
},
|
|
|
|
currentProfileIsDefaultProfile() {
|
|
let defaultProfile = this.getDefaultProfile();
|
|
let currentProfile = lazy.ProfileService.currentProfile;
|
|
// This comparison needs to accommodate null on both sides.
|
|
let isDefaultProfile = defaultProfile && currentProfile == defaultProfile;
|
|
return isDefaultProfile;
|
|
},
|
|
|
|
_throwIfNotLocked(lock) {
|
|
if (!(lock instanceof Ci.nsIProfileLock)) {
|
|
throw new Error("Passed lock was not an instance of nsIProfileLock");
|
|
}
|
|
|
|
try {
|
|
// In release builds, `.directory` throws NS_ERROR_NOT_INITIALIZED when
|
|
// unlocked. In debug builds, `.directory` when the profile is not locked
|
|
// will crash via `NS_ERROR`.
|
|
if (lock.directory) {
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
if (
|
|
!(
|
|
e instanceof Ci.nsIException &&
|
|
e.result == Cr.NS_ERROR_NOT_INITIALIZED
|
|
)
|
|
) {
|
|
throw e;
|
|
}
|
|
}
|
|
throw new Error("Profile is not locked");
|
|
},
|
|
|
|
/**
|
|
* Locks the given profile and provides the path to it to the callback.
|
|
* The callback should return a promise and once settled the profile is
|
|
* unlocked and then the promise returned back to the caller of this function.
|
|
*
|
|
* @template T
|
|
* @param {(lock: nsIProfileLock) => Promise<T>} callback
|
|
* @param {nsIToolkitProfile} [profile] defaults to default profile
|
|
* @return {Promise<T>}
|
|
*/
|
|
async withProfileLock(callback, profile = this.getDefaultProfile()) {
|
|
if (!profile) {
|
|
throw new Error("No default profile exists");
|
|
}
|
|
|
|
let lock;
|
|
try {
|
|
lock = profile.lock({});
|
|
lazy.log.info(
|
|
`withProfileLock: locked profile at ${lock.directory.path}`
|
|
);
|
|
} catch (e) {
|
|
throw new CannotLockProfileError(`Cannot lock profile: ${e}`);
|
|
}
|
|
|
|
try {
|
|
// We must await to ensure any logging is displayed after the callback resolves.
|
|
return await callback(lock);
|
|
} finally {
|
|
try {
|
|
lazy.log.info(
|
|
`withProfileLock: unlocking profile at ${lock.directory.path}`
|
|
);
|
|
lock.unlock();
|
|
lazy.log.info(`withProfileLock: unlocked profile`);
|
|
} catch (e) {
|
|
lazy.log.warn(`withProfileLock: error unlocking profile`, e);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Reads the preferences from "prefs.js" out of a profile, optionally
|
|
* returning only names satisfying a given predicate.
|
|
*
|
|
* If no `lock` is given, the default profile is locked and the preferences
|
|
* read from it. If `lock` is given, read from the given lock's directory.
|
|
*
|
|
* @param {(name: string) => boolean} [predicate] a predicate to filter
|
|
* preferences by; if not given, all preferences are accepted.
|
|
* @param {nsIProfileLock} [lock] optional lock to use
|
|
* @returns {object} with keys that are string preference names and values
|
|
* that are string|number|boolean preference values.
|
|
*/
|
|
async readPreferences(predicate = null, lock = null) {
|
|
if (!lock) {
|
|
return this.withProfileLock(profileLock =>
|
|
this.readPreferences(predicate, profileLock)
|
|
);
|
|
}
|
|
|
|
this._throwIfNotLocked(lock);
|
|
lazy.log.info(`readPreferences: profile is locked`);
|
|
|
|
let prefs = {};
|
|
let addPref = (kind, name, value, sticky, locked) => {
|
|
if (predicate && !predicate(name)) {
|
|
return;
|
|
}
|
|
prefs[name] = value;
|
|
};
|
|
|
|
// We ignore any "user.js" file, since usage is low and doing otherwise
|
|
// requires implementing a bit more of `nsIPrefsService` than feels safe.
|
|
let prefsFile = lock.directory.clone();
|
|
prefsFile.append("prefs.js");
|
|
lazy.log.info(`readPreferences: will parse prefs ${prefsFile.path}`);
|
|
|
|
let data = await IOUtils.read(prefsFile.path);
|
|
lazy.log.debug(
|
|
`readPreferences: parsing prefs from buffer of length ${data.length}`
|
|
);
|
|
|
|
Services.prefs.parsePrefsFromBuffer(
|
|
data,
|
|
{
|
|
onStringPref: addPref,
|
|
onIntPref: addPref,
|
|
onBoolPref: addPref,
|
|
onError(message) {
|
|
// Firefox itself manages "prefs.js", so errors should be infrequent.
|
|
lazy.log.error(message);
|
|
},
|
|
},
|
|
prefsFile.path
|
|
);
|
|
|
|
lazy.log.debug(`readPreferences: parsed prefs from buffer`, prefs);
|
|
return prefs;
|
|
},
|
|
|
|
/**
|
|
* Reads the snapshotted Firefox Messaging System targeting out of a profile.
|
|
*
|
|
* If no `lock` is given, the default profile is locked and the preferences
|
|
* read from it. If `lock` is given, read from the given lock's directory.
|
|
*
|
|
* @param {nsIProfileLock} [lock] optional lock to use
|
|
* @returns {object}
|
|
*/
|
|
async readFirefoxMessagingSystemTargetingSnapshot(lock = null) {
|
|
if (!lock) {
|
|
return this.withProfileLock(profileLock =>
|
|
this.readFirefoxMessagingSystemTargetingSnapshot(profileLock)
|
|
);
|
|
}
|
|
|
|
this._throwIfNotLocked(lock);
|
|
|
|
let snapshotFile = lock.directory.clone();
|
|
snapshotFile.append("targeting.snapshot.json");
|
|
|
|
lazy.log.info(
|
|
`readFirefoxMessagingSystemTargetingSnapshot: will read Firefox Messaging ` +
|
|
`System targeting snapshot from ${snapshotFile.path}`
|
|
);
|
|
|
|
return IOUtils.readJSON(snapshotFile.path);
|
|
},
|
|
|
|
/**
|
|
* Reads the Telemetry Client ID out of a profile.
|
|
*
|
|
* If no `lock` is given, the default profile is locked and the preferences
|
|
* read from it. If `lock` is given, read from the given lock's directory.
|
|
*
|
|
* @param {nsIProfileLock} [lock] optional lock to use
|
|
* @returns {string}
|
|
*/
|
|
async readTelemetryClientID(lock = null) {
|
|
if (!lock) {
|
|
return this.withProfileLock(profileLock =>
|
|
this.readTelemetryClientID(profileLock)
|
|
);
|
|
}
|
|
|
|
this._throwIfNotLocked(lock);
|
|
|
|
let stateFile = lock.directory.clone();
|
|
stateFile.append("datareporting");
|
|
stateFile.append("state.json");
|
|
|
|
lazy.log.info(
|
|
`readPreferences: will read Telemetry client ID from ${stateFile.path}`
|
|
);
|
|
|
|
let state = await IOUtils.readJSON(stateFile.path);
|
|
|
|
return state.clientID;
|
|
},
|
|
|
|
/**
|
|
* Enable the Nimbus experimentation framework.
|
|
*
|
|
* @param {nsICommandLine} commandLine if given, accept command line parameters
|
|
* like `--url about:studies?...` or
|
|
* `--url file:path/to.json` to explicitly
|
|
* opt-on to experiment branches.
|
|
* @param {object} defaultProfile snapshot of Firefox Messaging System
|
|
* targeting from default browsing profile.
|
|
*/
|
|
async enableNimbus(commandLine, defaultProfile = {}) {
|
|
try {
|
|
await lazy.ExperimentManager.onStartup({ defaultProfile });
|
|
} catch (err) {
|
|
lazy.log.error("Failed to initialize ExperimentManager:", err);
|
|
throw err;
|
|
}
|
|
|
|
try {
|
|
await lazy.RemoteSettingsExperimentLoader.init({ forceSync: true });
|
|
} catch (err) {
|
|
lazy.log.error(
|
|
"Failed to initialize RemoteSettingsExperimentLoader:",
|
|
err
|
|
);
|
|
throw err;
|
|
}
|
|
|
|
// Allow manual explicit opt-in to experiment branches to facilitate testing.
|
|
//
|
|
// Process command line arguments, like
|
|
// `--url about:studies?optin_slug=nalexander-ms-test1&optin_branch=treatment-a&optin_collection=nimbus-preview`
|
|
// or
|
|
// `--url file:///Users/nalexander/Mozilla/gecko/experiment.json?optin_branch=treatment-a`.
|
|
let ar;
|
|
while ((ar = commandLine?.handleFlagWithParam("url", false))) {
|
|
let uri = commandLine.resolveURI(ar);
|
|
const params = new URLSearchParams(uri.query);
|
|
|
|
if (uri.schemeIs("about") && uri.filePath == "studies") {
|
|
// Allow explicit opt-in. In the future, we might take this pref from
|
|
// the default browsing profile.
|
|
Services.prefs.setBoolPref("nimbus.debug", true);
|
|
|
|
const data = {
|
|
slug: params.get("optin_slug"),
|
|
branch: params.get("optin_branch"),
|
|
collection: params.get("optin_collection"),
|
|
};
|
|
await lazy.RemoteSettingsExperimentLoader.optInToExperiment(data);
|
|
lazy.log.info(`Opted in to experiment: ${JSON.stringify(data)}`);
|
|
}
|
|
|
|
if (uri.schemeIs("file")) {
|
|
let branchSlug = params.get("optin_branch");
|
|
let path = decodeURIComponent(uri.filePath);
|
|
let response = await fetch(uri.spec);
|
|
let recipe = await response.json();
|
|
if (recipe.permissions) {
|
|
// Saved directly from Experimenter, there's a top-level `data`. Hand
|
|
// written, that's not the norm.
|
|
recipe = recipe.data;
|
|
}
|
|
let branch = recipe.branches.find(b => b.slug == branchSlug);
|
|
|
|
lazy.ExperimentManager.forceEnroll(recipe, branch);
|
|
lazy.log.info(`Forced enrollment into: ${path}, branch: ${branchSlug}`);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Enable the Firefox Messaging System and, when successfully initialized,
|
|
* trigger a message with trigger id `backgroundTask`.
|
|
*
|
|
* @param {object} defaultProfile - snapshot of Firefox Messaging System
|
|
* targeting from default browsing profile.
|
|
*/
|
|
async enableFirefoxMessagingSystem(defaultProfile = {}) {
|
|
function logArgs(tag, ...args) {
|
|
lazy.log.debug(`FxMS invoked ${tag}: ${JSON.stringify(args)}`);
|
|
}
|
|
|
|
let {
|
|
messageHandler,
|
|
router,
|
|
createStorage,
|
|
} = lazy.ASRouterDefaultConfig();
|
|
|
|
if (!router.initialized) {
|
|
const storage = await createStorage();
|
|
await router.init({
|
|
storage,
|
|
// Background tasks never send legacy telemetry.
|
|
sendTelemetry: logArgs.bind(null, "sendTelemetry"),
|
|
dispatchCFRAction: messageHandler.handleCFRAction.bind(messageHandler),
|
|
// There's no child process involved in background tasks, so swallow all
|
|
// of these messages.
|
|
clearChildMessages: logArgs.bind(null, "clearChildMessages"),
|
|
clearChildProviders: logArgs.bind(null, "clearChildProviders"),
|
|
updateAdminState: () => {},
|
|
});
|
|
}
|
|
|
|
await lazy.ASRouter.waitForInitialized;
|
|
|
|
const triggerId = "backgroundTask";
|
|
await lazy.ASRouter.sendTriggerMessage({
|
|
browser: null,
|
|
id: triggerId,
|
|
context: {
|
|
defaultProfile,
|
|
},
|
|
});
|
|
lazy.log.info(
|
|
"Triggered Firefox Messaging System with trigger id 'backgroundTask'"
|
|
);
|
|
},
|
|
};
|