lib/experimentUtils.ts (80 lines of code) (raw):
/**
* This is a cross-platform list of Nimbus feature IDs that correspond to
* messaging experiments in both desktop and android. Other Nimbus features
* contain specific variables whose keys are enumerated in FeatureManifest.yaml.
* Conversely, messaging experiment features contain actual messages, with the
* usual message keys like `template` and `targeting`.
* @see FeatureManifest.yaml
*
* Copied from @see https://searchfox.org/mozilla-central/source/browser/components/newtab/lib/MessagingExperimentConstants.sys.mjs
*
* Should be manually update when that file changes.
*/
export const MESSAGING_EXPERIMENTS_DEFAULT_FEATURES: string[] = [
// Desktop features
"aboutwelcome",
"backgroundTaskMessage", // XXX need to backport this to tree
"cfr",
"featureCallout",
"fxms-message-1",
"fxms-message-2",
"fxms-message-3",
"fxms-message-4",
"fxms-message-5",
"fxms-message-6",
"fxms-message-7",
"fxms-message-8",
"fxms-message-9",
"fxms-message-10",
"fxms-message-11",
"fxms-message-12",
"fxms-message-13",
"fxms-message-14",
"fxms-message-15",
"infobar",
"moments-page",
"pbNewtab",
"spotlight",
"testFeature",
"whatsNewPage",
// Android features
"cfr",
"encourage-search-cfr",
"messaging",
"juno-onboarding",
"set-to-default-prompt",
];
/**
* If we have experiment dashboards with serious problems, we can hide those
* dashboards by adding the experiment slugs to this array.
*/
export const HIDE_DASHBOARD_EXPERIMENTS: string[] = [];
/**
*
* @param startDate - may be null, as NimbusExperiment types allow this.
* returns null in this case.
* @param proposedDuration - may be undefined as NimbusExperiment types
* allow this. returns null in this case.
*/
export function getProposedEndDate(
startDate: string | null,
proposedDuration: number | undefined,
): string | null {
if (startDate === null || proposedDuration === undefined) {
return null;
}
// XXX need to verify that experimenter actually uses UTC here
const jsDate = new Date(startDate);
jsDate.setUTCDate(jsDate.getUTCDate() + proposedDuration);
const formattedDate = jsDate.toISOString().slice(0, 10);
return formattedDate;
}
/**
*
* @param dateString The date to format in a string.
* @param daysToAdd Optional number of days to add from dateString. A
* negative value will result in days subtracted from dateString.
* @returns The dateString + daysToAdd as a string value in ISO format.
*/
export function formatDate(dateString: string, daysToAdd: number = 0) {
const date = new Date(dateString);
date.setUTCDate(date.getUTCDate() + daysToAdd);
return date.toISOString().slice(0, 10);
}
// XXX this should really be a method on NimbusRecipe, though it'll need some
// refactoring to get there.
/**
* Do recursive locale substitution on the values, if applicable.
*
* If there are no localizations provided, the value will be returned as-is.
*
* If the value is an object containing an $l10n key, its substitution will be
* returned.
*
* Otherwise, the value will be recursively substituted.
*
* Right now, we are manually passing the first locale out of the localization object.
* Eventually we'll want to select a locale on the dashboard (probably with a dropdown.)
*
* @param values - The values to perform substitutions upon; message contents
* @param localizations - The localization object from the recipe, contains
* substitutions for a specific locale. May be null.
* @returns {any} The values, potentially locale substituted.
*/
// XXX there are some existing issues with looping over && assigning object keys in Typescript;
// using the "any" type here is a workaround for those issues.
export function _substituteLocalizations(
values: any,
localizations?: any,
): object {
// If the recipe is not localized, we don't need to do anything.
// Likewise, if the value we are attempting to localize is not an
// object, there is nothing to localize.
if (
typeof localizations === "undefined" ||
typeof values !== "object" ||
values === null
) {
return values;
}
if (Array.isArray(values)) {
return values.map((value) =>
_substituteLocalizations(value, localizations),
);
}
const substituted = Object.assign({}, values);
// Loop over "$l10n" objects in the recipe and assign the appropriate string IDs from the
// localizations object
for (const [key, value] of Object.entries(values)) {
if (
key === "$l10n" &&
typeof value === "object" &&
value !== null &&
(value as any)?.id
) {
return localizations[(value as any).id];
}
substituted[key] = _substituteLocalizations(value, localizations);
}
return substituted;
}