Bug 1693550 - Update about:welcome to use ExperimentFeature r=k88hudson

Differential Revision: https://phabricator.services.mozilla.com/D105738
This commit is contained in:
Andrei Oprea
2021-03-04 21:20:19 +00:00
parent 504df4d357
commit 3b57c6a6a3
9 changed files with 227 additions and 281 deletions

View File

@@ -1467,7 +1467,7 @@ pref("trailhead.firstrun.newtab.triplets", "");
// Separate about welcome
pref("browser.aboutwelcome.enabled", true);
// Used to set multistage welcome UX
pref("browser.aboutwelcome.overrideContent", "");
pref("browser.aboutwelcome.screens", "");
pref("browser.aboutwelcome.skipFocus", false);
// The pref that controls if the What's New panel is enabled.

View File

@@ -24,6 +24,13 @@ XPCOMUtils.defineLazyGetter(this, "log", () => {
return new Logger("AboutWelcomeChild");
});
XPCOMUtils.defineLazyGetter(this, "aboutWelcomeFeature", () => {
const { ExperimentFeature } = ChromeUtils.import(
"resource://nimbus/ExperimentAPI.jsm"
);
return new ExperimentFeature("aboutwelcome");
});
XPCOMUtils.defineLazyGetter(this, "tippyTopProvider", () =>
(async () => {
const provider = new TippyTopProvider();
@@ -32,25 +39,6 @@ XPCOMUtils.defineLazyGetter(this, "tippyTopProvider", () =>
})()
);
function _parseOverrideContent(value) {
let result = {};
try {
result = value ? JSON.parse(value) : {};
} catch (e) {
Cu.reportError(e);
}
return result;
}
XPCOMUtils.defineLazyPreferenceGetter(
this,
"multiStageAboutWelcomeContent",
"browser.aboutwelcome.overrideContent",
"",
null,
_parseOverrideContent
);
const SEARCH_REGION_PREF = "browser.search.region";
XPCOMUtils.defineLazyPreferenceGetter(
@@ -164,20 +152,14 @@ class AboutWelcomeChild extends JSWindowActorChild {
exportFunctions() {
let window = this.contentWindow;
Cu.exportFunction(this.AWGetExperimentData.bind(this), window, {
defineAs: "AWGetExperimentData",
Cu.exportFunction(this.AWGetFeatureConfig.bind(this), window, {
defineAs: "AWGetFeatureConfig",
});
Cu.exportFunction(this.AWGetAttributionData.bind(this), window, {
defineAs: "AWGetAttributionData",
});
// For local dev, checks for JSON content inside pref browser.aboutwelcome.overrideContent
// that is used to override default welcome UI
Cu.exportFunction(this.AWGetWelcomeOverrideContent.bind(this), window, {
defineAs: "AWGetWelcomeOverrideContent",
});
Cu.exportFunction(this.AWGetFxAMetricsFlowURI.bind(this), window, {
defineAs: "AWGetFxAMetricsFlowURI",
});
@@ -228,16 +210,6 @@ class AboutWelcomeChild extends JSWindowActorChild {
);
}
/**
* Send multistage welcome JSON data read from aboutwelcome.overrideConetent pref to page
*/
AWGetWelcomeOverrideContent() {
return Cu.cloneInto(
multiStageAboutWelcomeContent || {},
this.contentWindow
);
}
AWSelectTheme(data) {
return this.wrapPromise(
this.sendQuery("AWPage:SELECT_THEME", data.toUpperCase())
@@ -309,21 +281,31 @@ class AboutWelcomeChild extends JSWindowActorChild {
/**
* Send initial data to page including experiment information
*/
AWGetExperimentData() {
// Note that we specifically don't wait for experiments to be loaded from disk so if
AWGetFeatureConfig() {
// Note that we specifically don't wait for `ready` so if
// about:welcome loads outside of the "FirstStartup" scenario this will likely not be ready
let experimentData = ExperimentAPI.getExperiment({
featureId: "aboutwelcome",
});
let experimentMetadata =
ExperimentAPI.getExperimentMetaData({
featureId: "aboutwelcome",
}) || {};
let featureConfig = aboutWelcomeFeature.getValue({ defaultValue: {} });
if (experimentData?.slug) {
if (experimentMetadata?.slug) {
log.debug(
`Loading about:welcome with experiment: ${experimentData.slug}`
`Loading about:welcome with experiment: ${experimentMetadata.slug}`
);
} else {
log.debug("Loading about:welcome without experiment");
}
return Cu.cloneInto(experimentData || {}, this.contentWindow);
return Cu.cloneInto(
{
// All experimentation right now is using the multistage template
template: "multistage",
...experimentMetadata,
...featureConfig,
},
this.contentWindow
);
}
AWGetFxAMetricsFlowURI() {

View File

@@ -212,49 +212,25 @@ function ComputeTelemetryInfo(welcomeContent, experimentId, branchId) {
}
async function retrieveRenderContent() {
var _aboutWelcomeProps;
// Check for featureConfig and retrieve content
const featureConfig = await window.AWGetFeatureConfig();
let aboutWelcomeProps;
// Check for override content in pref browser.aboutwelcome.overrideContent
let aboutWelcomeProps = await window.AWGetWelcomeOverrideContent();
if ((_aboutWelcomeProps = aboutWelcomeProps) !== null && _aboutWelcomeProps !== void 0 && _aboutWelcomeProps.template) {
let {
messageId,
UTMTerm
} = ComputeTelemetryInfo(aboutWelcomeProps);
return {
aboutWelcomeProps,
messageId,
UTMTerm
};
} // Check for experiment and retrieve content
const {
slug,
branch
} = await window.AWGetExperimentData();
aboutWelcomeProps = branch !== null && branch !== void 0 && branch.feature ? branch.feature.value : {}; // Check if there is any attribution data, this could take a while to await in series
// especially when there is an add-on that requires remote lookup
// Moving RTAMO as part of another screen of multistage is one option to fix the delay
// as it will allow the initial page to be fast while we fetch attribution data in parallel for a later screen.
const attribution = await window.AWGetAttributionData();
if (attribution !== null && attribution !== void 0 && attribution.template) {
var _aboutWelcomeProps2;
aboutWelcomeProps = { ...aboutWelcomeProps,
// If part of an experiment, render experiment template
template: (_aboutWelcomeProps2 = aboutWelcomeProps) !== null && _aboutWelcomeProps2 !== void 0 && _aboutWelcomeProps2.template ? aboutWelcomeProps.template : attribution.template,
if (!featureConfig.screens) {
const attribution = await window.AWGetAttributionData();
aboutWelcomeProps = {
template: attribution.template,
...attribution.extraProps
};
} else {
// If screens is defined then we have multi stage AW content to show
aboutWelcomeProps = featureConfig.screens ? featureConfig : {};
}
let {
messageId,
UTMTerm
} = ComputeTelemetryInfo(aboutWelcomeProps, slug, branch && branch.slug);
} = ComputeTelemetryInfo(aboutWelcomeProps, featureConfig.slug, featureConfig.branch && featureConfig.branch.slug);
return {
aboutWelcomeProps,
messageId,

View File

@@ -105,37 +105,25 @@ function ComputeTelemetryInfo(welcomeContent, experimentId, branchId) {
}
async function retrieveRenderContent() {
// Check for override content in pref browser.aboutwelcome.overrideContent
let aboutWelcomeProps = await window.AWGetWelcomeOverrideContent();
if (aboutWelcomeProps?.template) {
let { messageId, UTMTerm } = ComputeTelemetryInfo(aboutWelcomeProps);
return { aboutWelcomeProps, messageId, UTMTerm };
}
// Check for featureConfig and retrieve content
const featureConfig = await window.AWGetFeatureConfig();
let aboutWelcomeProps;
// Check for experiment and retrieve content
const { slug, branch } = await window.AWGetExperimentData();
aboutWelcomeProps = branch?.feature ? branch.feature.value : {};
// Check if there is any attribution data, this could take a while to await in series
// especially when there is an add-on that requires remote lookup
// Moving RTAMO as part of another screen of multistage is one option to fix the delay
// as it will allow the initial page to be fast while we fetch attribution data in parallel for a later screen.
const attribution = await window.AWGetAttributionData();
if (attribution?.template) {
if (!featureConfig.screens) {
const attribution = await window.AWGetAttributionData();
aboutWelcomeProps = {
...aboutWelcomeProps,
// If part of an experiment, render experiment template
template: aboutWelcomeProps?.template
? aboutWelcomeProps.template
: attribution.template,
template: attribution.template,
...attribution.extraProps,
};
} else {
// If screens is defined then we have multi stage AW content to show
aboutWelcomeProps = featureConfig.screens ? featureConfig : {};
}
let { messageId, UTMTerm } = ComputeTelemetryInfo(
aboutWelcomeProps,
slug,
branch && branch.slug
featureConfig.slug,
featureConfig.branch && featureConfig.branch.slug
);
return { aboutWelcomeProps, messageId, UTMTerm };
}

View File

@@ -21,6 +21,7 @@ prefs =
[browser_aboutwelcome_simplified.js]
[browser_aboutwelcome_multistage.js]
[browser_aboutwelcome_rtamo.js]
skip-if = (os == "linux") # Test setup only implemented for OSX and Windows
[browser_aboutwelcome_attribution.js]
skip-if = (os == "linux") # Test setup only implemented for OSX and Windows
[browser_aboutwelcome_observer.js]

View File

@@ -1,22 +1,17 @@
"use strict";
const ABOUT_WELCOME_OVERRIDE_CONTENT_PREF =
"browser.aboutwelcome.overrideContent";
const ABOUT_WELCOME_OVERRIDE_CONTENT_PREF = "browser.aboutwelcome.screens";
const ABOUT_WELCOME_FOCUS_PREF = "browser.aboutwelcome.skipFocus";
const TEST_MULTISTAGE_JSON = {
id: "multi-stage-welcome",
template: "multistage",
screens: [
{
id: "AW_STEP1",
order: 0,
content: {
title: "Step 1",
},
const TEST_MULTISTAGE_JSON = [
{
id: "AW_STEP1",
order: 0,
content: {
title: "Step 1",
},
],
};
},
];
async function setAboutWelcomeOverrideContent(value) {
return pushPrefs([ABOUT_WELCOME_OVERRIDE_CONTENT_PREF, value]);

View File

@@ -11,122 +11,111 @@ const { FxAccounts } = ChromeUtils.import(
);
const SEPARATE_ABOUT_WELCOME_PREF = "browser.aboutwelcome.enabled";
const ABOUT_WELCOME_OVERRIDE_CONTENT_PREF =
"browser.aboutwelcome.overrideContent";
const ABOUT_WELCOME_OVERRIDE_CONTENT_PREF = "browser.aboutwelcome.screens";
const DID_SEE_ABOUT_WELCOME_PREF = "trailhead.firstrun.didSeeAboutWelcome";
const TEST_MULTISTAGE_CONTENT = {
id: "multi-stage-welcome",
template: "multistage",
screens: [
{
id: "AW_STEP1",
order: 0,
content: {
zap: true,
title: "Step 1",
tiles: {
type: "theme",
action: {
theme: "<event>",
const TEST_MULTISTAGE_CONTENT = [
{
id: "AW_STEP1",
order: 0,
content: {
zap: true,
title: "Step 1",
tiles: {
type: "theme",
action: {
theme: "<event>",
},
data: [
{
theme: "automatic",
label: "theme-1",
tooltip: "test-tooltip",
},
data: [
{
theme: "automatic",
label: "theme-1",
tooltip: "test-tooltip",
},
{
theme: "dark",
label: "theme-2",
},
],
},
primary_button: {
label: "Next",
action: {
navigate: true,
{
theme: "dark",
label: "theme-2",
},
],
},
primary_button: {
label: "Next",
action: {
navigate: true,
},
secondary_button: {
label: "link",
},
secondary_button_top: {
label: "link top",
action: {
type: "SHOW_FIREFOX_ACCOUNTS",
data: { entrypoint: "test" },
},
},
help_text: {
text: { string_id: "onboarding-multistage-set-default-subtitle" },
position: "footer",
},
secondary_button: {
label: "link",
},
secondary_button_top: {
label: "link top",
action: {
type: "SHOW_FIREFOX_ACCOUNTS",
data: { entrypoint: "test" },
},
},
},
{
id: "AW_STEP2",
order: 1,
content: {
zap: true,
title: "Step 2 longzaptest",
tiles: {
type: "topsites",
info: true,
},
primary_button: {
label: "Next",
action: {
navigate: true,
},
},
secondary_button: {
label: "link",
},
help_text: {
text: "Here's some helptext with an icon",
img: {
src:
"chrome://activity-stream/content/data/content/assets/cfr_fb_container.png",
},
position: "footer",
},
{
id: "AW_STEP2",
order: 1,
content: {
zap: true,
title: "Step 2 longzaptest",
tiles: {
type: "topsites",
info: true,
},
primary_button: {
label: "Next",
action: {
navigate: true,
},
},
},
{
id: "AW_STEP3",
order: 2,
content: {
title: "Step 3",
tiles: {
type: "image",
media_type: "test-img",
source: {
default:
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iIzQ1YTFmZiIgZmlsbC1vcGFjaXR5PSJjb250ZXh0LWZpbGwtb3BhY2l0eSIgZD0iTTE1Ljg0NSA2LjA2NEExLjEgMS4xIDAgMCAwIDE1IDUuMzMxTDEwLjkxMSA0LjYgOC45ODUuNzM1YTEuMSAxLjEgMCAwIDAtMS45NjkgMEw1LjA4OSA0LjZsLTQuMDgxLjcyOWExLjEgMS4xIDAgMCAwLS42MTUgMS44MzRMMy4zMiAxMC4zMWwtLjYwOSA0LjM2YTEuMSAxLjEgMCAwIDAgMS42IDEuMTI3TDggMTMuODczbDMuNjkgMS45MjdhMS4xIDEuMSAwIDAgMCAxLjYtMS4xMjdsLS42MS00LjM2MyAyLjkyNi0zLjE0NmExLjEgMS4xIDAgMCAwIC4yMzktMS4xeiIvPjwvc3ZnPg==",
},
},
primary_button: {
label: "Next",
action: {
navigate: true,
},
},
secondary_button: {
label: "Import",
action: {
type: "SHOW_MIGRATION_WIZARD",
data: { source: "chrome" },
},
},
help_text: {
text: "Here's some sample help text",
position: "default",
},
secondary_button: {
label: "link",
},
},
],
};
},
{
id: "AW_STEP3",
order: 2,
content: {
title: "Step 3",
tiles: {
type: "image",
media_type: "test-img",
source: {
default:
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iIzQ1YTFmZiIgZmlsbC1vcGFjaXR5PSJjb250ZXh0LWZpbGwtb3BhY2l0eSIgZD0iTTE1Ljg0NSA2LjA2NEExLjEgMS4xIDAgMCAwIDE1IDUuMzMxTDEwLjkxMSA0LjYgOC45ODUuNzM1YTEuMSAxLjEgMCAwIDAtMS45NjkgMEw1LjA4OSA0LjZsLTQuMDgxLjcyOWExLjEgMS4xIDAgMCAwLS42MTUgMS44MzRMMy4zMiAxMC4zMWwtLjYwOSA0LjM2YTEuMSAxLjEgMCAwIDAgMS42IDEuMTI3TDggMTMuODczbDMuNjkgMS45MjdhMS4xIDEuMSAwIDAgMCAxLjYtMS4xMjdsLS42MS00LjM2MyAyLjkyNi0zLjE0NmExLjEgMS4xIDAgMCAwIC4yMzktMS4xeiIvPjwvc3ZnPg==",
},
},
primary_button: {
label: "Next",
action: {
navigate: true,
},
},
secondary_button: {
label: "Import",
action: {
type: "SHOW_MIGRATION_WIZARD",
data: { source: "chrome" },
},
},
help_text: {
text: "Here's some sample help text",
position: "default",
},
},
},
];
async function getAboutWelcomeParent(browser) {
let windowGlobalParent = browser.browsingContext.currentWindowGlobal;
return windowGlobalParent.getActor("AboutWelcome");
}
const TEST_MULTISTAGE_JSON = JSON.stringify(TEST_MULTISTAGE_CONTENT);
/**
* Sets the aboutwelcome pref to enabled simplified welcome UI
@@ -135,7 +124,7 @@ async function setAboutWelcomePref(value) {
return pushPrefs([SEPARATE_ABOUT_WELCOME_PREF, value]);
}
async function setAboutWelcomeMultiStage(value) {
async function setAboutWelcomeMultiStage(value = "") {
return pushPrefs([ABOUT_WELCOME_OVERRIDE_CONTENT_PREF, value]);
}
@@ -277,7 +266,6 @@ add_task(async function test_multistage_zeroOnboarding_experimentAPI() {
);
await doExperimentCleanup();
Assert.equal(ExperimentAPI._store.getAll().length, 0, "Cleanup done");
});
/**
@@ -297,7 +285,10 @@ add_task(async function test_multistage_aboutwelcome_experimentAPI() {
feature: {
enabled: true,
featureId: "aboutwelcome",
value: TEST_MULTISTAGE_CONTENT,
value: {
id: "my-mochitest-experiment",
screens: TEST_MULTISTAGE_CONTENT,
},
},
},
],
@@ -313,12 +304,18 @@ add_task(async function test_multistage_aboutwelcome_experimentAPI() {
"about:welcome",
true
);
registerCleanupFunction(() => {
BrowserTestUtils.removeTab(tab);
});
const browser = tab.linkedBrowser;
let aboutWelcomeActor = await getAboutWelcomeParent(browser);
const sandbox = sinon.createSandbox();
// Stub AboutWelcomeParent Content Message Handler
sandbox.spy(aboutWelcomeActor, "onContentMessage");
registerCleanupFunction(() => {
BrowserTestUtils.removeTab(tab);
sandbox.restore();
});
await test_screen_content(
browser,
"multistage step 1",
@@ -335,13 +332,22 @@ add_task(async function test_multistage_aboutwelcome_experimentAPI() {
"label.theme",
"input[type='radio']",
"div.indicator.current",
"p.helptext.footer[data-l10n-id='onboarding-multistage-set-default-subtitle']",
],
// Unexpected selectors:
["main.AW_STEP2", "main.AW_STEP3", "div.tiles-container.info"]
);
await onButtonClick(browser, "button.primary");
Assert.ok(
aboutWelcomeActor.onContentMessage.args.find(
args =>
args[1].event === "CLICK_BUTTON" &&
args[1].message_id === "MY-MOCHITEST-EXPERIMENT_AW_STEP1"
),
"Telemetry should join id defined in feature value with screen"
);
await test_screen_content(
browser,
"multistage step 2",
@@ -353,8 +359,6 @@ add_task(async function test_multistage_aboutwelcome_experimentAPI() {
"h1.welcomeZap",
"span.zap.long",
"div.tiles-container.info",
"p.helptext",
"img.helptext-img",
],
// Unexpected selectors:
["main.AW_STEP1", "main.AW_STEP3", "div.secondary-cta.top", "div.test-img"]
@@ -386,7 +390,6 @@ add_task(async function test_multistage_aboutwelcome_experimentAPI() {
);
await doExperimentCleanup();
Assert.equal(ExperimentAPI._store.getAll().length, 0, "Cleanup done");
});
/**
@@ -499,11 +502,6 @@ add_task(async function test_Multistage_About_Welcome_navigation() {
);
});
async function getAboutWelcomeParent(browser) {
let windowGlobalParent = browser.browsingContext.currentWindowGlobal;
return windowGlobalParent.getActor("AboutWelcome");
}
/**
* Test the multistage welcome UI primary button action
*/
@@ -572,7 +570,7 @@ add_task(async function test_AWMultistage_Primary_Action() {
);
Assert.equal(
impressionCall.args[1].message_id,
`${TEST_MULTISTAGE_CONTENT.id}_SITES`.toUpperCase(),
"DEFAULT_ABOUTWELCOME_SITES",
"SITES MessageId sent in impression event telemetry"
);
}
@@ -606,7 +604,7 @@ add_task(async function test_AWMultistage_Primary_Action() {
);
Assert.equal(
performanceCall.args[1].message_id,
TEST_MULTISTAGE_CONTENT.id.toUpperCase(),
"DEFAULT_ABOUTWELCOME",
"MessageId sent in performance event telemetry"
);
}
@@ -628,12 +626,14 @@ add_task(async function test_AWMultistage_Primary_Action() {
);
Assert.equal(
clickCall.args[1].message_id,
`${TEST_MULTISTAGE_CONTENT.id}_${TEST_MULTISTAGE_CONTENT.screens[0].id}`.toUpperCase(),
`DEFAULT_ABOUTWELCOME_${TEST_MULTISTAGE_CONTENT[0].id}`.toUpperCase(),
"MessageId sent in click event telemetry"
);
});
add_task(async function test_AWMultistage_Secondary_Open_URL_Action() {
let { doExperimentCleanup } = ExperimentFakes.enrollmentHelper();
await doExperimentCleanup();
let browser = await openAboutWelcome();
let aboutWelcomeActor = await getAboutWelcomeParent(browser);
const sandbox = sinon.createSandbox();

View File

@@ -1,56 +1,56 @@
"use strict";
const ABOUT_WELCOME_OVERRIDE_CONTENT_PREF =
"browser.aboutwelcome.overrideContent";
const TEST_RTAMO_WELCOME_CONTENT = {
template: "return_to_amo",
name: "Test add on",
url: "https://test.xpi",
iconURL: "https://test.svg",
content: {
header: { string_id: "onboarding-welcome-header" },
subtitle: { string_id: "return-to-amo-subtitle" },
text: {
string_id: "return-to-amo-addon-title",
},
primary_button: {
label: { string_id: "return-to-amo-add-extension-label" },
action: {
type: "INSTALL_ADDON_FROM_URL",
data: { url: null, telemetrySource: "rtamo" },
},
},
startButton: {
label: {
string_id: "onboarding-start-browsing-button-label",
},
message_id: "RTAMO_START_BROWSING_BUTTON",
action: {
type: "OPEN_AWESOME_BAR",
},
},
},
};
const TEST_RTAMO_WELCOME_JSON = JSON.stringify(TEST_RTAMO_WELCOME_CONTENT);
async function setAboutWelcomeOverride(value) {
return pushPrefs([ABOUT_WELCOME_OVERRIDE_CONTENT_PREF, value]);
}
const { ASRouter } = ChromeUtils.import(
"resource://activity-stream/lib/ASRouter.jsm"
);
const { AddonRepository } = ChromeUtils.import(
"resource://gre/modules/addons/AddonRepository.jsm"
);
const { AttributionCode } = ChromeUtils.import(
"resource:///modules/AttributionCode.jsm"
);
async function openRTAMOWelcomePage() {
await setAboutWelcomeOverride(TEST_RTAMO_WELCOME_JSON);
let sandbox = sinon.createSandbox();
// Can't properly stub the child/parent actors so instead
// we stub the modules they depend on for the RTAMO flow
// to ensure the right thing is rendered.
await ASRouter.forceAttribution({
source: "addons.mozilla.org",
medium: "referral",
campaign: "non-fx-button",
content: "iridium@particlecore.github.io",
experiment: "ua-onboarding",
variation: "chrome",
ua: "Google Chrome 123",
dltoken: "00000000-0000-0000-0000-000000000000",
});
sandbox
.stub(AddonRepository, "getAddonsByIDs")
.resolves([
{ sourceURI: { scheme: "https", spec: "https://test.xpi" }, icons: {} },
]);
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"about:welcome",
true
);
registerCleanupFunction(() => {
registerCleanupFunction(async () => {
sandbox.restore();
BrowserTestUtils.removeTab(tab);
pushPrefs([ABOUT_WELCOME_OVERRIDE_CONTENT_PREF, ""]);
// Clear cache call is only possible in a testing environment
let env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
env.set("XPCSHELL_TEST_PROFILE_DIR", "testing");
// Clear and refresh Attribution, and then fetch the messages again to update
AttributionCode._clearCache();
await AttributionCode.getAttrDataAsync();
});
return tab.linkedBrowser;
}

View File

@@ -17,6 +17,10 @@ const MANIFEST = {
description: "The about:welcome page",
enabledFallbackPref: "browser.aboutwelcome.enabled",
variables: {
screens: {
type: "json",
fallbackPref: "browser.aboutwelcome.screens",
},
skipFocus: {
type: "boolean",
fallbackPref: "browser.aboutwelcome.skipFocus",