diff options
Diffstat (limited to 'browser/components/aboutwelcome/modules')
3 files changed, 1252 insertions, 0 deletions
diff --git a/browser/components/aboutwelcome/modules/AWScreenUtils.sys.mjs b/browser/components/aboutwelcome/modules/AWScreenUtils.sys.mjs new file mode 100644 index 0000000000..715ab650a4 --- /dev/null +++ b/browser/components/aboutwelcome/modules/AWScreenUtils.sys.mjs @@ -0,0 +1,74 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", + ASRouterTargeting: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs", +}); + +export const AWScreenUtils = { + /** + * Filter the given screens in place with a predicate. + * + * @param {object[]} screens - The screens to filter. + * @param {Function} callback - The predicate for filtering the screens. + */ + async removeScreens(screens, callback) { + for (let i = 0; i < screens?.length; i++) { + if (await callback(screens[i], i)) { + screens.splice(i--, 1); + } + } + }, + /** + * Given a JEXL expression, returns the evaluation of the expression or returns + * true if the expression did not evaluate successfully + * + * @param {string} targeting - The JEXL expression that will be evaluated + * @returns {boolean} + */ + async evaluateScreenTargeting(targeting) { + const result = await lazy.ASRouter.evaluateExpression({ + expression: targeting, + context: lazy.ASRouterTargeting.Environment, + }); + if (result?.evaluationStatus?.success) { + return result.evaluationStatus.result; + } + + return true; + }, + /** + * Filter out screens whose targeting do not match. + * + * Given an array of screens, each screen will have it's `targeting` property + * evaluated, and removed if it's targeting evaluates to false + * + * @param {object[]} screens - An array of screens that will be looped + * through to be evaluated for removal + * @returns {object[]} - A new array containing the screens that were not removed + */ + async evaluateTargetingAndRemoveScreens(screens) { + const filteredScreens = [...screens]; + await this.removeScreens(filteredScreens, async screen => { + if (screen.targeting === undefined) { + // Don't remove the screen if we don't have a targeting property + return false; + } + + const result = await this.evaluateScreenTargeting(screen.targeting); + // Flipping the value because a true evaluation means we + // don't want to remove the screen, while false means we do + return !result; + }); + + return filteredScreens; + }, + + async addScreenImpression(screen) { + await lazy.ASRouter.addScreenImpression(screen); + }, +}; diff --git a/browser/components/aboutwelcome/modules/AboutWelcomeDefaults.sys.mjs b/browser/components/aboutwelcome/modules/AboutWelcomeDefaults.sys.mjs new file mode 100644 index 0000000000..828839c8d0 --- /dev/null +++ b/browser/components/aboutwelcome/modules/AboutWelcomeDefaults.sys.mjs @@ -0,0 +1,902 @@ +/* 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/. */ + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// AppConstants, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs", + AttributionCode: "resource:///modules/AttributionCode.sys.mjs", + AWScreenUtils: "resource:///modules/aboutwelcome/AWScreenUtils.sys.mjs", + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", +}); + +// Message to be updated based on finalized MR designs +const MR_ABOUT_WELCOME_DEFAULT = { + id: "MR_WELCOME_DEFAULT", + template: "multistage", + // Allow tests to easily disable transitions. + transitions: Services.prefs.getBoolPref( + "browser.aboutwelcome.transitions", + true + ), + backdrop: + "var(--mr-welcome-background-color) var(--mr-welcome-background-gradient)", + screens: [ + { + id: "AW_WELCOME_BACK", + targeting: "isDeviceMigration", + content: { + position: "split", + split_narrow_bkg_position: "-100px", + image_alt_text: { + string_id: "onboarding-device-migration-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/device-migration.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "onboarding-device-migration-title", + }, + subtitle: { + string_id: "onboarding-device-migration-subtitle2", + }, + primary_button: { + label: { + string_id: "onboarding-device-migration-primary-button-label", + }, + action: { + type: "FXA_SIGNIN_FLOW", + navigate: "actionResult", + data: { + entrypoint: "fx-device-migration-onboarding", + extraParams: { + utm_content: "migration-onboarding", + utm_source: "fx-new-device-sync", + utm_medium: "firefox-desktop", + utm_campaign: "migration", + }, + }, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + }, + }, + { + id: "AW_EASY_SETUP_NEEDS_DEFAULT_AND_PIN", + targeting: + "doesAppNeedPin && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser", + content: { + position: "split", + split_narrow_bkg_position: "-60px", + image_alt_text: { + string_id: "mr2022-onboarding-default-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + hide_secondary_section: "responsive", + logo: {}, + title: { + string_id: "onboarding-easy-setup-security-and-privacy-title", + }, + subtitle: { + string_id: "onboarding-easy-setup-security-and-privacy-subtitle", + }, + tiles: { + type: "multiselect", + data: [ + { + id: "checkbox-1", + defaultValue: true, + label: { + string_id: "mr2022-onboarding-pin-primary-button-label", + }, + action: { + type: "PIN_FIREFOX_TO_TASKBAR", + }, + }, + { + id: "checkbox-2", + defaultValue: true, + label: { + string_id: + "mr2022-onboarding-easy-setup-set-default-checkbox-label", + }, + action: { + type: "SET_DEFAULT_BROWSER", + }, + }, + { + id: "checkbox-3", + defaultValue: true, + label: { + string_id: "mr2022-onboarding-easy-setup-import-checkbox-label", + }, + uncheckedAction: { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { + pref: { + name: "showEmbeddedImport", + }, + }, + }, + ], + }, + }, + checkedAction: { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { + pref: { + name: "showEmbeddedImport", + value: true, + }, + }, + }, + ], + }, + }, + }, + ], + }, + primary_button: { + label: { + string_id: "mr2022-onboarding-easy-setup-primary-button-label", + }, + action: { + type: "MULTI_ACTION", + collectSelect: true, + navigate: true, + data: { + actions: [], + }, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + secondary_button_top: { + label: { + string_id: "mr1-onboarding-sign-in-button-label", + }, + action: { + data: { + entrypoint: "activity-stream-firstrun", + where: "tab", + }, + type: "SHOW_FIREFOX_ACCOUNTS", + addFlowParams: true, + }, + }, + }, + }, + { + id: "AW_EASY_SETUP_NEEDS_DEFAULT", + targeting: + "!doesAppNeedPin && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser", + content: { + position: "split", + split_narrow_bkg_position: "-60px", + image_alt_text: { + string_id: "mr2022-onboarding-default-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "onboarding-easy-setup-security-and-privacy-title", + }, + subtitle: { + string_id: "onboarding-easy-setup-security-and-privacy-subtitle", + }, + tiles: { + type: "multiselect", + data: [ + { + id: "checkbox-1", + defaultValue: true, + label: { + string_id: + "mr2022-onboarding-easy-setup-set-default-checkbox-label", + }, + action: { + type: "SET_DEFAULT_BROWSER", + }, + }, + { + id: "checkbox-2", + defaultValue: true, + label: { + string_id: "mr2022-onboarding-easy-setup-import-checkbox-label", + }, + uncheckedAction: { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { + pref: { + name: "showEmbeddedImport", + }, + }, + }, + ], + }, + }, + checkedAction: { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { + pref: { + name: "showEmbeddedImport", + value: true, + }, + }, + }, + ], + }, + }, + }, + ], + }, + primary_button: { + label: { + string_id: "mr2022-onboarding-easy-setup-primary-button-label", + }, + action: { + type: "MULTI_ACTION", + collectSelect: true, + navigate: true, + data: { + actions: [], + }, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + secondary_button_top: { + label: { + string_id: "mr1-onboarding-sign-in-button-label", + }, + action: { + data: { + entrypoint: "activity-stream-firstrun", + where: "tab", + }, + type: "SHOW_FIREFOX_ACCOUNTS", + addFlowParams: true, + }, + }, + }, + }, + { + id: "AW_EASY_SETUP_NEEDS_PIN", + targeting: + "doesAppNeedPin && (!'browser.shell.checkDefaultBrowser'|preferenceValue || isDefaultBrowser)", + content: { + position: "split", + split_narrow_bkg_position: "-60px", + image_alt_text: { + string_id: "mr2022-onboarding-default-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "onboarding-easy-setup-security-and-privacy-title", + }, + subtitle: { + string_id: "onboarding-easy-setup-security-and-privacy-subtitle", + }, + tiles: { + type: "multiselect", + data: [ + { + id: "checkbox-1", + defaultValue: true, + label: { + string_id: "mr2022-onboarding-pin-primary-button-label", + }, + action: { + type: "PIN_FIREFOX_TO_TASKBAR", + }, + }, + { + id: "checkbox-2", + defaultValue: true, + label: { + string_id: "mr2022-onboarding-easy-setup-import-checkbox-label", + }, + uncheckedAction: { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { + pref: { + name: "showEmbeddedImport", + }, + }, + }, + ], + }, + }, + checkedAction: { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { + pref: { + name: "showEmbeddedImport", + value: true, + }, + }, + }, + ], + }, + }, + }, + ], + }, + primary_button: { + label: { + string_id: "mr2022-onboarding-easy-setup-primary-button-label", + }, + action: { + type: "MULTI_ACTION", + collectSelect: true, + navigate: true, + data: { + actions: [], + }, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + secondary_button_top: { + label: { + string_id: "mr1-onboarding-sign-in-button-label", + }, + action: { + data: { + entrypoint: "activity-stream-firstrun", + where: "tab", + }, + type: "SHOW_FIREFOX_ACCOUNTS", + addFlowParams: true, + }, + }, + }, + }, + { + id: "AW_EASY_SETUP_ONLY_IMPORT", + targeting: + "!doesAppNeedPin && (!'browser.shell.checkDefaultBrowser'|preferenceValue || isDefaultBrowser)", + content: { + position: "split", + split_narrow_bkg_position: "-60px", + image_alt_text: { + string_id: "mr2022-onboarding-default-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "onboarding-easy-setup-security-and-privacy-title", + }, + subtitle: { + string_id: "onboarding-easy-setup-security-and-privacy-subtitle", + }, + tiles: { + type: "multiselect", + data: [ + { + id: "checkbox-1", + defaultValue: true, + label: { + string_id: "mr2022-onboarding-easy-setup-import-checkbox-label", + }, + uncheckedAction: { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { + pref: { + name: "showEmbeddedImport", + }, + }, + }, + ], + }, + }, + checkedAction: { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { + pref: { + name: "showEmbeddedImport", + value: true, + }, + }, + }, + ], + }, + }, + }, + ], + }, + primary_button: { + label: { + string_id: "mr2022-onboarding-easy-setup-primary-button-label", + }, + action: { + type: "MULTI_ACTION", + collectSelect: true, + navigate: true, + data: { + actions: [], + }, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + secondary_button_top: { + label: { + string_id: "mr1-onboarding-sign-in-button-label", + }, + action: { + data: { + entrypoint: "activity-stream-firstrun", + where: "tab", + }, + type: "SHOW_FIREFOX_ACCOUNTS", + addFlowParams: true, + }, + }, + }, + }, + { + id: "AW_LANGUAGE_MISMATCH", + content: { + position: "split", + background: "var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "mr2022-onboarding-live-language-text", + }, + subtitle: { + string_id: "mr2022-language-mismatch-subtitle", + }, + hero_text: { + string_id: "mr2022-onboarding-live-language-text", + useLangPack: true, + }, + languageSwitcher: { + downloading: { + string_id: "onboarding-live-language-button-label-downloading", + }, + cancel: { + string_id: "onboarding-live-language-secondary-cancel-download", + }, + waiting: { string_id: "onboarding-live-language-waiting-button" }, + skip: { string_id: "mr2022-onboarding-secondary-skip-button-label" }, + action: { + navigate: true, + }, + switch: { + string_id: "mr2022-onboarding-live-language-switch-to", + useLangPack: true, + }, + continue: { + string_id: "mr2022-onboarding-live-language-continue-in", + }, + }, + }, + }, + { + id: "AW_IMPORT_SETTINGS_EMBEDDED", + targeting: `("messaging-system-action.showEmbeddedImport" |preferenceValue == true) && useEmbeddedMigrationWizard`, + content: { + tiles: { type: "migration-wizard" }, + position: "split", + split_narrow_bkg_position: "-42px", + image_alt_text: { + string_id: "mr2022-onboarding-import-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-import.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + hide_secondary_section: "responsive", + migrate_start: { + action: {}, + }, + migrate_close: { + action: { + navigate: true, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + }, + }, + { + id: "AW_MOBILE_DOWNLOAD", + // The mobile download screen should only be shown to users who + // are either not logged into FxA, or don't have any mobile devices syncing + targeting: "!isFxASignedIn || sync.mobileDevices == 0", + content: { + position: "split", + split_narrow_bkg_position: "-160px", + image_alt_text: { + string_id: "mr2022-onboarding-mobile-download-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-mobilecrosspromo.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "onboarding-mobile-download-security-and-privacy-title", + }, + subtitle: { + string_id: "onboarding-mobile-download-security-and-privacy-subtitle", + }, + hero_image: { + url: "chrome://activity-stream/content/data/content/assets/mobile-download-qr-new-user.svg", + }, + cta_paragraph: { + text: { + string_id: "mr2022-onboarding-mobile-download-cta-text", + string_name: "download-label", + }, + action: { + type: "OPEN_URL", + data: { + args: "https://www.mozilla.org/firefox/mobile/get-app/?utm_medium=firefox-desktop&utm_source=onboarding-modal&utm_campaign=mr2022&utm_content=new-global", + where: "tab", + }, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + }, + }, + { + id: "AW_AMO_INTRODUCE", + // Show to en-* locales only + targeting: "localeLanguageCode == 'en'", + content: { + position: "split", + split_narrow_bkg_position: "-58px", + background: + "url('chrome://activity-stream/content/data/content/assets/mr-amo-collection.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { string_id: "amo-screen-title" }, + subtitle: { string_id: "amo-screen-subtitle" }, + primary_button: { + label: { string_id: "amo-screen-primary-cta" }, + action: { + type: "OPEN_URL", + data: { + args: "https://addons.mozilla.org/en-US/firefox/collections/4757633/25c2b44583534b3fa8fea977c419cd/?page=1&collection_sort=-added", + where: "tabshifted", + }, + navigate: true, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + }, + }, + }, + { + id: "AW_GRATITUDE", + content: { + position: "split", + split_narrow_bkg_position: "-228px", + image_alt_text: { + string_id: "mr2022-onboarding-gratitude-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-gratitude.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "onboarding-gratitude-security-and-privacy-title", + }, + subtitle: { + string_id: "onboarding-gratitude-security-and-privacy-subtitle", + }, + primary_button: { + label: { + string_id: "mr2-onboarding-start-browsing-button-label", + }, + action: { + navigate: true, + }, + }, + }, + }, + ], +}; + +async function getAddonFromRepository(data) { + const [addonInfo] = await lazy.AddonRepository.getAddonsByIDs([data]); + if (addonInfo.sourceURI.scheme !== "https") { + return null; + } + + return { + name: addonInfo.name, + url: addonInfo.sourceURI.spec, + iconURL: addonInfo.icons["64"] || addonInfo.icons["32"], + type: addonInfo.type, + screenshots: addonInfo.screenshots, + }; +} + +async function getAddonInfo(attrbObj) { + let { content, source } = attrbObj; + try { + if (!content || source !== "addons.mozilla.org") { + return null; + } + // Attribution data can be double encoded + while (content.includes("%")) { + try { + const result = decodeURIComponent(content); + if (result === content) { + break; + } + content = result; + } catch (e) { + break; + } + } + // return_to_amo embeds the addon id in the content + // param, prefixed with "rta:". Translating that + // happens in AddonRepository, however we can avoid + // an API call if we check up front here. + if (content.startsWith("rta:")) { + return await getAddonFromRepository(content); + } + } catch (e) { + console.error("Failed to get the latest add-on version for Return to AMO"); + } + return null; +} + +async function getAttributionContent() { + let attribution = await lazy.AttributionCode.getAttrDataAsync(); + if (attribution?.source === "addons.mozilla.org") { + let addonInfo = await getAddonInfo(attribution); + if (addonInfo) { + return { + ...addonInfo, + template: "return_to_amo", + }; + } + } + if (attribution?.ua) { + return { + ua: decodeURIComponent(attribution.ua), + }; + } + return null; +} + +// Return default multistage welcome content +function getDefaults() { + return Cu.cloneInto(MR_ABOUT_WELCOME_DEFAULT, {}); +} + +let gSourceL10n = null; + +// Localize Firefox download source from user agent attribution to show inside +// import primary button label such as 'Import from <localized browser name>'. +// no firefox as import wizard doesn't show it +const allowedUAs = ["chrome", "edge", "ie"]; +function getLocalizedUA(ua) { + if (!gSourceL10n) { + gSourceL10n = new Localization(["browser/migrationWizard.ftl"]); + } + if (allowedUAs.includes(ua)) { + return gSourceL10n.formatValue(`migration-source-name-${ua.toLowerCase()}`); + } + return null; +} + +function prepareMobileDownload(content) { + let mobileContent = content?.screens?.find( + screen => screen.id === "AW_MOBILE_DOWNLOAD" + )?.content; + + if (!mobileContent) { + return content; + } + if (!lazy.BrowserUtils.sendToDeviceEmailsSupported()) { + // If send to device emails are not supported for a user's locale, + // remove the send to device link and update the screen text + delete mobileContent.cta_paragraph.action; + mobileContent.cta_paragraph.text = { + string_id: "mr2022-onboarding-no-mobile-download-cta-text", + }; + } + // Update CN specific QRCode url + if (AppConstants.isChinaRepack()) { + mobileContent.hero_image.url = `${mobileContent.hero_image.url.slice( + 0, + mobileContent.hero_image.url.indexOf(".svg") + )}-cn.svg`; + } + + return content; +} + +async function prepareContentForReact(content) { + const { screens } = content; + + if (content?.template === "return_to_amo") { + return content; + } + + // Set the primary import button source based on attribution. + if (content?.ua) { + // If available, add the browser source to action data + // and localized browser string args to primary button label + const { label, action } = + content?.screens?.find( + screen => + screen?.content?.primary_button?.action?.type === + "SHOW_MIGRATION_WIZARD" + )?.content?.primary_button ?? {}; + + if (action) { + action.data = { ...action.data, source: content.ua }; + } + + let browserStr = await getLocalizedUA(content.ua); + + if (label?.string_id) { + label.string_id = browserStr + ? "mr1-onboarding-import-primary-button-label-attribution" + : "mr2022-onboarding-import-primary-button-label-no-attribution"; + + label.args = browserStr ? { previous: browserStr } : {}; + } + } + + // Remove Firefox Accounts related UI and prevent related metrics. + if (!Services.prefs.getBoolPref("identity.fxaccounts.enabled", false)) { + delete content.screens?.find( + screen => + screen.content?.secondary_button_top?.action?.type === + "SHOW_FIREFOX_ACCOUNTS" + )?.content.secondary_button_top; + content.skipFxA = true; + } + + let shouldRemoveLanguageMismatchScreen = true; + if (content.languageMismatchEnabled) { + const screen = content?.screens?.find(s => s.id === "AW_LANGUAGE_MISMATCH"); + if (screen && content.appAndSystemLocaleInfo.canLiveReload) { + // Add the display names for the OS and Firefox languages, like "American English". + function addMessageArgs(obj) { + for (const value of Object.values(obj)) { + if (value?.string_id) { + value.args = content.appAndSystemLocaleInfo.displayNames; + } + } + } + + addMessageArgs(screen.content.languageSwitcher); + addMessageArgs(screen.content); + shouldRemoveLanguageMismatchScreen = false; + } + } + + if (shouldRemoveLanguageMismatchScreen) { + await lazy.AWScreenUtils.removeScreens( + screens, + screen => screen.id === "AW_LANGUAGE_MISMATCH" + ); + } + + return prepareMobileDownload(content); +} + +export const AboutWelcomeDefaults = { + prepareContentForReact, + getDefaults, + getAttributionContent, +}; diff --git a/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs b/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs new file mode 100644 index 0000000000..1447d3ebde --- /dev/null +++ b/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs @@ -0,0 +1,276 @@ +/* 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 = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AttributionCode: "resource:///modules/AttributionCode.sys.mjs", + ClientID: "resource://gre/modules/ClientID.sys.mjs", + TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "telemetryClientId", () => + lazy.ClientID.getClientID() +); +ChromeUtils.defineLazyGetter( + lazy, + "browserSessionId", + () => lazy.TelemetrySession.getMetadata("").sessionId +); + +ChromeUtils.defineLazyGetter(lazy, "log", () => { + const { Logger } = ChromeUtils.importESModule( + "resource://messaging-system/lib/Logger.sys.mjs" + ); + return new Logger("AboutWelcomeTelemetry"); +}); + +export class AboutWelcomeTelemetry { + constructor() { + XPCOMUtils.defineLazyPreferenceGetter( + this, + "telemetryEnabled", + "browser.newtabpage.activity-stream.telemetry", + false + ); + } + + /** + * Attach browser attribution data to a ping payload. + * + * It intentionally queries the *cached* attribution data other than calling + * `getAttrDataAsync()` in order to minimize the overhead here. + * For the same reason, we are not querying the attribution data from + * `TelemetryEnvironment.currentEnvironment.settings`. + * + * In practice, it's very likely that the attribution data is already read + * and cached at some point by `AboutWelcomeParent`, so it should be able to + * read the cached results for the most if not all of the pings. + */ + _maybeAttachAttribution(ping) { + const attribution = lazy.AttributionCode.getCachedAttributionData(); + if (attribution && Object.keys(attribution).length) { + ping.attribution = attribution; + } + return ping; + } + + async _createPing(event) { + if (event.event_context && typeof event.event_context === "object") { + event.event_context = JSON.stringify(event.event_context); + } + let ping = { + ...event, + addon_version: Services.appinfo.appBuildID, + locale: Services.locale.appLocaleAsBCP47, + client_id: await lazy.telemetryClientId, + browser_session_id: lazy.browserSessionId, + }; + + return this._maybeAttachAttribution(ping); + } + + /** + * Augment the provided event with some metadata and then send it + * to the messaging-system's onboarding endpoint. + * + * Is sometimes used by non-onboarding events. + * + * @param event - an object almost certainly from an onboarding flow (though + * there is a case where spotlight may use this, too) + * containing a nested structure of data for reporting as + * telemetry, as documented in + * https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/data_events.html + * Does not have all of its data (`_createPing` will augment + * with ids and attribution if available). + */ + async sendTelemetry(event) { + if (!this.telemetryEnabled) { + return; + } + + const ping = await this._createPing(event); + + try { + this.submitGleanPingForPing(ping); + } catch (e) { + // Though Glean APIs are forbidden to throw, it may be possible that a + // mismatch between the shape of `ping` and the defined metrics is not + // adequately handled. + Glean.messagingSystem.gleanPingForPingFailures.add(1); + } + } + + /** + * Tries to infer appropriate Glean metrics on the "messaging-system" ping, + * sets them, and submits a "messaging-system" ping. + * + * Does not check if telemetry is enabled. + * (Though Glean will check the global prefs). + * + * Note: This is a very unusual use of Glean that is specific to the use- + * cases of Messaging System. Please do not copy this pattern. + */ + submitGleanPingForPing(ping) { + lazy.log.debug(`Submitting Glean ping for ${JSON.stringify(ping)}`); + // event.event_context is an object, but it may have been stringified. + let event_context = ping?.event_context; + let shopping_callout_impression = + ping?.message_id?.startsWith("FAKESPOT_CALLOUT") && + ping?.event === "IMPRESSION"; + + if (typeof event_context === "string") { + try { + event_context = JSON.parse(event_context); + // This code is for directing Shopping component based clicks into + // the Glean Events ping. + if ( + event_context?.page === "about:shoppingsidebar" || + shopping_callout_impression + ) { + this.handleShoppingPings(ping, event_context); + } + } catch (e) { + // The Empty JSON strings and non-objects often provided by the + // existing telemetry we need to send failing to parse do not fit in + // the spirit of what this error is meant to capture. Instead, we want + // to capture when what we got should have been an object, + // but failed to parse. + if (event_context.length && event_context.includes("{")) { + Glean.messagingSystem.eventContextParseError.add(1); + } + } + } + + // We echo certain properties from event_context into their own metrics + // to aid analysis. + if (event_context?.reason) { + Glean.messagingSystem.eventReason.set(event_context.reason); + } + if (event_context?.page) { + Glean.messagingSystem.eventPage.set(event_context.page); + } + if (event_context?.source) { + Glean.messagingSystem.eventSource.set(event_context.source); + } + if (event_context?.screen_family) { + Glean.messagingSystem.eventScreenFamily.set(event_context.screen_family); + } + // Screen_index was being coerced into a boolean value + // which resulted in 0 (first screen index) being ignored. + if (Number.isInteger(event_context?.screen_index)) { + Glean.messagingSystem.eventScreenIndex.set(event_context.screen_index); + } + if (event_context?.screen_id) { + Glean.messagingSystem.eventScreenId.set(event_context.screen_id); + } + if (event_context?.screen_initials) { + Glean.messagingSystem.eventScreenInitials.set( + event_context.screen_initials + ); + } + + // The event_context is also provided as-is as stringified JSON. + if (event_context) { + Glean.messagingSystem.eventContext.set(JSON.stringify(event_context)); + } + + if ("attribution" in ping) { + for (const [key, value] of Object.entries(ping.attribution)) { + const camelKey = this._snakeToCamelCase(key); + try { + Glean.messagingSystemAttribution[camelKey].set(value); + } catch (e) { + // We here acknowledge that we don't know the full breadth of data + // being collected. Ideally AttributionCode will later centralize + // definition and reporting of attribution data and we can be rid of + // this fail-safe for collecting the names of unknown keys. + Glean.messagingSystemAttribution.unknownKeys[camelKey].add(1); + } + } + } + + // List of keys handled above. + const handledKeys = ["event_context", "attribution"]; + + for (const [key, value] of Object.entries(ping)) { + if (handledKeys.includes(key)) { + continue; + } + const camelKey = this._snakeToCamelCase(key); + try { + // We here acknowledge that even known keys might have non-scalar + // values. We're pretty sure we handled them all with handledKeys, + // but we might not have. + // Ideally this can later be removed after running for a version or two + // with no values seen in messaging_system.invalid_nested_data + if (typeof value === "object") { + Glean.messagingSystem.invalidNestedData[camelKey].add(1); + } else { + Glean.messagingSystem[camelKey].set(value); + } + } catch (e) { + // We here acknowledge that we don't know the full breadth of data being + // collected. Ideally we will later gain that confidence and can remove + // this fail-safe for collecting the names of unknown keys. + Glean.messagingSystem.unknownKeys[camelKey].add(1); + // TODO(bug 1600008): For testing, also record the overall count. + Glean.messagingSystem.unknownKeyCount.add(1); + } + } + + // With all the metrics set, now it's time to submit this ping. + GleanPings.messagingSystem.submit(); + } + + _snakeToCamelCase(s) { + return s.toString().replace(/_([a-z])/gi, (_str, group) => { + return group.toUpperCase(); + }); + } + + handleShoppingPings(ping, event_context) { + const message_id = ping?.message_id; + // This function helps direct a shopping ping to the correct Glean event. + if (message_id.startsWith("FAKESPOT_OPTIN_DEFAULT")) { + // Onboarding page message IDs are generated, but can reliably be + // assumed to start in this manner. + switch (ping?.event) { + case "CLICK_BUTTON": + switch (event_context?.source) { + case "privacy_policy": + Glean.shopping.surfaceShowPrivacyPolicyClicked.record(); + break; + case "terms_of_use": + Glean.shopping.surfaceShowTermsClicked.record(); + break; + case "primary_button": + // corresponds to 'Analyze Reviews' + Glean.shopping.surfaceOptInClicked.record(); + break; + case "additional_button": + // corresponds to "Not Now" + Glean.shopping.surfaceNotNowClicked.record(); + break; + case "learn_more": + Glean.shopping.surfaceLearnMoreClicked.record(); + break; + } + break; + case "IMPRESSION": + Glean.shopping.surfaceOnboardingDisplayed.record({ + configuration: ping?.message_id, + }); + break; + } + } + if (message_id.startsWith("FAKESPOT_CALLOUT")) { + Glean.shopping.addressBarFeatureCalloutDisplayed.record({ + configuration: message_id, + }); + } + } +} |