summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/aboutwelcome/lib
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/aboutwelcome/lib')
-rw-r--r--browser/components/newtab/aboutwelcome/lib/AboutWelcomeDefaults.jsm707
-rw-r--r--browser/components/newtab/aboutwelcome/lib/AboutWelcomeTelemetry.jsm242
2 files changed, 949 insertions, 0 deletions
diff --git a/browser/components/newtab/aboutwelcome/lib/AboutWelcomeDefaults.jsm b/browser/components/newtab/aboutwelcome/lib/AboutWelcomeDefaults.jsm
new file mode 100644
index 0000000000..0e17a0b3ae
--- /dev/null
+++ b/browser/components/newtab/aboutwelcome/lib/AboutWelcomeDefaults.jsm
@@ -0,0 +1,707 @@
+/* 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/. */
+"use strict";
+
+const EXPORTED_SYMBOLS = ["AboutWelcomeDefaults"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+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",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ AWScreenUtils: "resource://activity-stream/lib/AWScreenUtils.jsm",
+});
+
+// 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-subtitle",
+ },
+ 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",
+ targeting:
+ "os.windowsBuildNumber >= 15063 && !isDefaultBrowser && !doesAppNeedPin",
+ 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: "mr2022-onboarding-set-default-title",
+ },
+ subtitle: {
+ string_id: "mr2022-onboarding-set-default-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",
+ },
+ action: {
+ type: "SHOW_MIGRATION_WIZARD",
+ data: {},
+ },
+ },
+ ],
+ },
+ 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_PIN_FIREFOX",
+ targeting:
+ "!(os.windowsBuildNumber >= 15063 && !isDefaultBrowser && !doesAppNeedPin)",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-155px",
+ image_alt_text: {
+ string_id: "mr2022-onboarding-pin-image-alt",
+ },
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-pintaskbar.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
+ progress_bar: true,
+ logo: {},
+ title: {
+ string_id: "mr2022-onboarding-welcome-pin-header",
+ },
+ subtitle: {
+ string_id: "mr2022-onboarding-welcome-pin-subtitle",
+ },
+ primary_button: {
+ label: {
+ string_id: "mr2022-onboarding-pin-primary-button-label",
+ },
+ action: {
+ navigate: true,
+ type: "PIN_FIREFOX_TO_TASKBAR",
+ },
+ },
+ 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_SET_DEFAULT",
+ targeting:
+ "!(os.windowsBuildNumber >= 15063 && !isDefaultBrowser && !doesAppNeedPin)",
+ 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: "mr2022-onboarding-set-default-title",
+ },
+ subtitle: {
+ string_id: "mr2022-onboarding-set-default-subtitle",
+ },
+ primary_button: {
+ label: {
+ string_id: "mr2022-onboarding-set-default-primary-button-label",
+ },
+ action: {
+ navigate: true,
+ type: "SET_DEFAULT_BROWSER",
+ },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ has_arrow_icon: true,
+ },
+ },
+ },
+ {
+ id: "AW_IMPORT_SETTINGS",
+ targeting:
+ "!(os.windowsBuildNumber >= 15063 && !isDefaultBrowser && !doesAppNeedPin) && !useEmbeddedMigrationWizard",
+ content: {
+ 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,
+ logo: {},
+ title: {
+ string_id: "mr2022-onboarding-import-header",
+ },
+ subtitle: {
+ string_id: "mr2022-onboarding-import-subtitle",
+ },
+ primary_button: {
+ label: {
+ string_id:
+ "mr2022-onboarding-import-primary-button-label-no-attribution",
+ },
+ action: {
+ type: "SHOW_MIGRATION_WIZARD",
+ data: {},
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ has_arrow_icon: true,
+ },
+ },
+ },
+ {
+ id: "AW_IMPORT_SETTINGS_EMBEDDED",
+ targeting:
+ "!(os.windowsBuildNumber >= 15063 && !isDefaultBrowser && !doesAppNeedPin) && 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,
+ 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: "mr2022-onboarding-mobile-download-title",
+ },
+ subtitle: {
+ string_id: "mr2022-onboarding-mobile-download-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_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: "mr2022-onboarding-gratitude-title",
+ },
+ subtitle: {
+ string_id: "mr2022-onboarding-gratitude-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/migration.ftl"]);
+ }
+ if (allowedUAs.includes(ua)) {
+ return gSourceL10n.formatValue(`source-name-${ua.toLowerCase()}`);
+ }
+ return null;
+}
+
+// Function to evalute the appropriate string for the welcome screen button label
+function evaluateWelcomeScreenButtonLabel(removeDefault) {
+ return removeDefault
+ ? "mr2022-onboarding-get-started-primary-button-label"
+ : "mr2022-onboarding-set-default-primary-button-label";
+}
+
+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;
+ }
+
+ // Change content for Windows 7 because non-light themes aren't quite right.
+ if (AppConstants.isPlatformAndVersionAtMost("win", "6.1")) {
+ await lazy.AWScreenUtils.removeScreens(screens, screen =>
+ ["theme"].includes(screen.content?.tiles?.type)
+ );
+ }
+
+ // 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 } : {};
+ }
+ }
+
+ // If already pinned, convert "pin" screen to "welcome" with desired action.
+ let removeDefault = !content.needDefault;
+ if (!content.needPin) {
+ const pinScreen = content.screens?.find(screen =>
+ screen.id?.startsWith("AW_PIN_FIREFOX")
+ );
+ if (pinScreen?.content) {
+ pinScreen.id = removeDefault ? "AW_GET_STARTED" : "AW_ONLY_DEFAULT";
+ pinScreen.content.title = {
+ string_id: "mr2022-onboarding-welcome-pin-header",
+ };
+
+ pinScreen.content.subtitle = {
+ string_id: removeDefault
+ ? "mr2022-onboarding-get-started-primary-subtitle"
+ : "mr2022-onboarding-set-default-only-subtitle",
+ };
+
+ pinScreen.content.primary_button = {
+ label: {
+ string_id: evaluateWelcomeScreenButtonLabel(removeDefault, content),
+ },
+ action: {
+ navigate: true,
+ },
+ };
+ // Get started content will navigate without action, so remove "Not now."
+ if (!removeDefault) {
+ // The "pin" screen will now handle "default" so remove other "default."
+ pinScreen.content.primary_button.action.type = "SET_DEFAULT_BROWSER";
+ removeDefault = true;
+ }
+ }
+ }
+ if (removeDefault) {
+ await lazy.AWScreenUtils.removeScreens(screens, screen =>
+ screen.id?.startsWith("AW_SET_DEFAULT")
+ );
+ }
+
+ // 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;
+ }
+
+ // Remove the English-only image caption.
+ if (Services.locale.appLocaleAsBCP47.split("-")[0] !== "en") {
+ delete content.screens?.find(
+ screen => screen.content?.help_text?.deleteIfNotEn
+ )?.content.help_text;
+ }
+
+ 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);
+}
+
+const AboutWelcomeDefaults = {
+ prepareContentForReact,
+ getDefaults,
+ getAttributionContent,
+};
diff --git a/browser/components/newtab/aboutwelcome/lib/AboutWelcomeTelemetry.jsm b/browser/components/newtab/aboutwelcome/lib/AboutWelcomeTelemetry.jsm
new file mode 100644
index 0000000000..b8bdd44794
--- /dev/null
+++ b/browser/components/newtab/aboutwelcome/lib/AboutWelcomeTelemetry.jsm
@@ -0,0 +1,242 @@
+/* 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/. */
+"use strict";
+
+const EXPORTED_SYMBOLS = ["AboutWelcomeTelemetry"];
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "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",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ PingCentre: "resource:///modules/PingCentre.jsm",
+});
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "structuredIngestionEndpointBase",
+ "browser.newtabpage.activity-stream.telemetry.structuredIngestion.endpoint",
+ ""
+);
+XPCOMUtils.defineLazyGetter(lazy, "telemetryClientId", () =>
+ lazy.ClientID.getClientID()
+);
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "browserSessionId",
+ () => lazy.TelemetrySession.getMetadata("").sessionId
+);
+
+XPCOMUtils.defineLazyGetter(lazy, "log", () => {
+ const { Logger } = ChromeUtils.importESModule(
+ "resource://messaging-system/lib/Logger.sys.mjs"
+ );
+ return new Logger("AboutWelcomeTelemetry");
+});
+
+const TELEMETRY_TOPIC = "about:welcome";
+const PING_TYPE = "onboarding";
+const PING_VERSION = "1";
+const STRUCTURED_INGESTION_NAMESPACE_MS = "messaging-system";
+
+class AboutWelcomeTelemetry {
+ constructor() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "telemetryEnabled",
+ "browser.newtabpage.activity-stream.telemetry",
+ false
+ );
+ }
+
+ /**
+ * Lazily initialize PingCentre for Activity Stream to send pings
+ */
+ get pingCentre() {
+ Object.defineProperty(this, "pingCentre", {
+ value: new lazy.PingCentre({ topic: TELEMETRY_TOPIC }),
+ });
+ return this.pingCentre;
+ }
+
+ _generateStructuredIngestionEndpoint() {
+ const uuid = Services.uuid.generateUUID().toString();
+ // Structured Ingestion does not support the UUID generated by Services.uuid,
+ // because it contains leading and trailing braces. Need to trim them first.
+ const docID = uuid.slice(1, -1);
+ const extension = `${STRUCTURED_INGESTION_NAMESPACE_MS}/${PING_TYPE}/${PING_VERSION}/${docID}`;
+ return `${lazy.structuredIngestionEndpointBase}/${extension}`;
+ }
+
+ /**
+ * 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);
+ }
+
+ this.pingCentre.sendStructuredIngestionPing(
+ ping,
+ this._generateStructuredIngestionEndpoint(),
+ STRUCTURED_INGESTION_NAMESPACE_MS
+ );
+ }
+
+ /**
+ * 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;
+ if (typeof event_context === "string") {
+ try {
+ event_context = JSON.parse(event_context);
+ } catch (e) {
+ 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);
+ }
+
+ // 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();
+ });
+ }
+}