summaryrefslogtreecommitdiffstats
path: root/browser/components/ion/content/ion.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/ion/content/ion.js')
-rw-r--r--browser/components/ion/content/ion.js791
1 files changed, 791 insertions, 0 deletions
diff --git a/browser/components/ion/content/ion.js b/browser/components/ion/content/ion.js
new file mode 100644
index 0000000000..3c34328d58
--- /dev/null
+++ b/browser/components/ion/content/ion.js
@@ -0,0 +1,791 @@
+/* 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/. */
+
+/**
+ * Control panel for the Ion project, formerly known as Pioneer.
+ * This lives in `about:ion` and provides a UI for users to un/enroll in the
+ * overall program, and to un/enroll from individual studies.
+ *
+ * NOTE - prefs and Telemetry both still mention Pioneer for backwards-compatibility,
+ * this may change in the future.
+ */
+
+const { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+
+let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(
+ Ci.nsIParserUtils
+);
+
+const PREF_ION_ID = "toolkit.telemetry.pioneerId";
+const PREF_ION_NEW_STUDIES_AVAILABLE =
+ "toolkit.telemetry.pioneer-new-studies-available";
+const PREF_ION_COMPLETED_STUDIES =
+ "toolkit.telemetry.pioneer-completed-studies";
+
+/**
+ * Remote Settings keys for general content, and available studies.
+ */
+const CONTENT_COLLECTION_KEY = "pioneer-content-v2";
+const STUDY_ADDON_COLLECTION_KEY = "pioneer-study-addons-v2";
+
+const STUDY_LEAVE_REASONS = {
+ USER_ABANDONED: "user-abandoned",
+ STUDY_ENDED: "study-ended",
+};
+
+const PREF_TEST_CACHED_CONTENT = "toolkit.pioneer.testCachedContent";
+const PREF_TEST_CACHED_ADDONS = "toolkit.pioneer.testCachedAddons";
+const PREF_TEST_ADDONS = "toolkit.pioneer.testAddons";
+
+/**
+ * Use the in-tree HTML Sanitizer to ensure that HTML from remote-settings is safe to use.
+ * Note that RS does use content-signing, we're doing this extra step as an in-depth security measure.
+ *
+ * @param {string} htmlString - unsanitized HTML (content-signed by remote-settings)
+ * @returns {DocumentFragment} - sanitized DocumentFragment
+ */
+function sanitizeHtml(htmlString) {
+ const content = document.createElement("div");
+ const contentFragment = parserUtils.parseFragment(
+ htmlString,
+ Ci.nsIParserUtils.SanitizerDropForms |
+ Ci.nsIParserUtils.SanitizerAllowStyle |
+ Ci.nsIParserUtils.SanitizerLogRemovals,
+ false,
+ Services.io.newURI("about:ion"),
+ content
+ );
+
+ return contentFragment;
+}
+
+function showEnrollmentStatus() {
+ const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
+
+ const enrollmentButton = document.getElementById("enrollment-button");
+
+ document.l10n.setAttributes(
+ enrollmentButton,
+ `ion-${ionId ? "un" : ""}enrollment-button`
+ );
+ enrollmentButton.classList.toggle("primary", !ionId);
+
+ // collapse content above the fold if enrolled, otherwise open it.
+ for (const section of ["details", "data"]) {
+ const details = document.getElementById(section);
+ if (ionId) {
+ details.removeAttribute("open");
+ } else {
+ details.setAttribute("open", true);
+ }
+ }
+}
+
+function toggleContentBasedOnLocale() {
+ const requestedLocale = Services.locale.requestedLocale;
+ if (requestedLocale !== "en-US") {
+ const localeNotificationBar = document.getElementById(
+ "locale-notification"
+ );
+ localeNotificationBar.style.display = "block";
+
+ const reportContent = document.getElementById("report-content");
+ reportContent.style.display = "none";
+ }
+}
+
+async function toggleEnrolled(studyAddonId, cachedAddons) {
+ let addon;
+ let install;
+
+ const cachedAddon = cachedAddons.find(a => a.addon_id == studyAddonId);
+
+ if (Cu.isInAutomation) {
+ install = {
+ install: async () => {
+ let testAddons = Services.prefs.getStringPref(PREF_TEST_ADDONS, "[]");
+ testAddons = JSON.parse(testAddons);
+
+ testAddons.push(studyAddonId);
+ Services.prefs.setStringPref(
+ PREF_TEST_ADDONS,
+ JSON.stringify(testAddons)
+ );
+ },
+ };
+
+ let testAddons = Services.prefs.getStringPref(PREF_TEST_ADDONS, "[]");
+ testAddons = JSON.parse(testAddons);
+
+ for (const testAddon of testAddons) {
+ if (testAddon == studyAddonId) {
+ addon = {};
+ addon.uninstall = () => {
+ Services.prefs.setStringPref(
+ PREF_TEST_ADDONS,
+ JSON.stringify(testAddons.filter(a => a.id != testAddon.id))
+ );
+ };
+ }
+ }
+ } else {
+ addon = await AddonManager.getAddonByID(studyAddonId);
+ install = await AddonManager.getInstallForURL(cachedAddon.sourceURI.spec);
+ }
+
+ const completedStudies = Services.prefs.getStringPref(
+ PREF_ION_COMPLETED_STUDIES,
+ "{}"
+ );
+
+ const study = document.querySelector(`.card[id="${cachedAddon.addon_id}"`);
+ const joinBtn = study.querySelector(".join-button");
+
+ if (addon) {
+ joinBtn.disabled = true;
+ await addon.uninstall();
+ await sendDeletionPing(studyAddonId);
+
+ document.l10n.setAttributes(joinBtn, "ion-join-study");
+ joinBtn.disabled = false;
+
+ // Record that the user abandoned this study, since it may not be re-join-able.
+ if (completedStudies) {
+ const studies = JSON.parse(completedStudies);
+ studies[studyAddonId] = STUDY_LEAVE_REASONS.USER_ABANDONED;
+ Services.prefs.setStringPref(
+ PREF_ION_COMPLETED_STUDIES,
+ JSON.stringify(studies)
+ );
+ }
+ } else {
+ // Check if this study is re-join-able before enrollment.
+ const studies = JSON.parse(completedStudies);
+ if (studyAddonId in studies) {
+ if (
+ "canRejoin" in cachedAddons[studyAddonId] &&
+ cachedAddons[studyAddonId].canRejoin === false
+ ) {
+ console.error(
+ `Cannot rejoin ended study ${studyAddonId}, reason: ${studies[studyAddonId]}`
+ );
+ return;
+ }
+ }
+ joinBtn.disabled = true;
+ await install.install();
+ document.l10n.setAttributes(joinBtn, "ion-leave-study");
+ joinBtn.disabled = false;
+
+ // Send an enrollment ping for this study. Note that this could be sent again
+ // if we are re-joining.
+ await sendEnrollmentPing(studyAddonId);
+ }
+
+ await updateStudy(cachedAddon.addon_id);
+}
+
+async function showAvailableStudies(cachedAddons) {
+ const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
+ const defaultAddons = cachedAddons.filter(a => a.isDefault);
+ if (ionId) {
+ for (const defaultAddon of defaultAddons) {
+ let addon;
+ let install;
+ if (Cu.isInAutomation) {
+ install = {
+ install: async () => {
+ if (
+ defaultAddon.addon_id == "ion-v2-bad-default-example@mozilla.org"
+ ) {
+ throw new Error("Bad test default add-on");
+ }
+ },
+ };
+ } else {
+ addon = await AddonManager.getAddonByID(defaultAddon.addon_id);
+ install = await AddonManager.getInstallForURL(
+ defaultAddon.sourceURI.spec
+ );
+ }
+
+ if (!addon) {
+ // Any default add-ons are required, try to reinstall.
+ await install.install();
+ }
+ }
+ }
+
+ const studyAddons = cachedAddons.filter(a => !a.isDefault);
+ for (const cachedAddon of studyAddons) {
+ if (!cachedAddon) {
+ console.error(
+ `about:ion - Study addon ID not found in cache: ${studyAddonId}`
+ );
+ return;
+ }
+
+ const studyAddonId = cachedAddon.addon_id;
+
+ const study = document.createElement("div");
+ study.setAttribute("id", studyAddonId);
+ study.setAttribute("class", "card card-no-hover");
+
+ if (cachedAddon.icons && 32 in cachedAddon.icons) {
+ const iconName = document.createElement("img");
+ iconName.setAttribute("class", "card-icon");
+ iconName.setAttribute("src", cachedAddon.icons[32]);
+ study.appendChild(iconName);
+ }
+
+ const studyBody = document.createElement("div");
+ studyBody.classList.add("card-body");
+ study.appendChild(studyBody);
+
+ const studyName = document.createElement("h3");
+ studyName.setAttribute("class", "card-name");
+ studyName.textContent = cachedAddon.name;
+ studyBody.appendChild(studyName);
+
+ const studyAuthor = document.createElement("span");
+ studyAuthor.setAttribute("class", "card-author");
+ studyAuthor.textContent = cachedAddon.authors.name;
+ studyBody.appendChild(studyAuthor);
+
+ const actions = document.createElement("div");
+ actions.classList.add("card-actions");
+ study.appendChild(actions);
+
+ const joinBtn = document.createElement("button");
+ joinBtn.setAttribute("id", `${studyAddonId}-join-button`);
+ joinBtn.classList.add("primary");
+ joinBtn.classList.add("join-button");
+ document.l10n.setAttributes(joinBtn, "ion-join-study");
+
+ joinBtn.addEventListener("click", async () => {
+ let addon;
+ if (Cu.isInAutomation) {
+ const testAddons = Services.prefs.getStringPref(PREF_TEST_ADDONS, "[]");
+ for (const testAddon of JSON.parse(testAddons)) {
+ if (testAddon == studyAddonId) {
+ addon = {};
+ addon.uninstall = () => {
+ Services.prefs.setStringPref(PREF_TEST_ADDONS, "[]");
+ };
+ }
+ }
+ } else {
+ addon = await AddonManager.getAddonByID(studyAddonId);
+ }
+ let joinOrLeave = addon ? "leave" : "join";
+ let dialog = document.getElementById(
+ `${joinOrLeave}-study-consent-dialog`
+ );
+ dialog.setAttribute("addon-id", cachedAddon.addon_id);
+ const consentText = dialog.querySelector(
+ `[id=${joinOrLeave}-study-consent]`
+ );
+
+ // Clears out any existing children with a single #text node
+ consentText.textContent = "";
+
+ const contentFragment = sanitizeHtml(
+ cachedAddon[`${joinOrLeave}StudyConsent`]
+ );
+ consentText.appendChild(contentFragment);
+
+ dialog.showModal();
+ dialog.scrollTop = 0;
+
+ const openEvent = new Event("open");
+ dialog.dispatchEvent(openEvent);
+ });
+ actions.appendChild(joinBtn);
+
+ const studyDesc = document.createElement("div");
+ studyDesc.setAttribute("class", "card-description");
+
+ const contentFragment = sanitizeHtml(cachedAddon.description);
+ studyDesc.appendChild(contentFragment);
+
+ study.appendChild(studyDesc);
+
+ const studyDataCollected = document.createElement("div");
+ studyDataCollected.setAttribute("class", "card-data-collected");
+ study.appendChild(studyDataCollected);
+
+ const dataCollectionDetailsHeader = document.createElement("p");
+ dataCollectionDetailsHeader.textContent = "This study will collect:";
+ studyDataCollected.appendChild(dataCollectionDetailsHeader);
+
+ const dataCollectionDetails = document.createElement("ul");
+ for (const dataCollectionDetail of cachedAddon.dataCollectionDetails) {
+ const detailsBullet = document.createElement("li");
+ detailsBullet.textContent = dataCollectionDetail;
+ dataCollectionDetails.append(detailsBullet);
+ }
+ studyDataCollected.appendChild(dataCollectionDetails);
+
+ const availableStudies = document.getElementById("available-studies");
+ availableStudies.appendChild(study);
+
+ await updateStudy(studyAddonId);
+ }
+
+ const availableStudies = document.getElementById("header-available-studies");
+ document.l10n.setAttributes(availableStudies, "ion-current-studies");
+}
+
+async function updateStudy(studyAddonId) {
+ let addon;
+ if (Cu.isInAutomation) {
+ const testAddons = Services.prefs.getStringPref(PREF_TEST_ADDONS, "[]");
+ for (const testAddon of JSON.parse(testAddons)) {
+ if (testAddon == studyAddonId) {
+ addon = {
+ uninstall() {},
+ };
+ }
+ }
+ } else {
+ addon = await AddonManager.getAddonByID(studyAddonId);
+ }
+
+ const study = document.querySelector(`.card[id="${studyAddonId}"`);
+
+ const joinBtn = study.querySelector(".join-button");
+
+ const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
+
+ const completedStudies = Services.prefs.getStringPref(
+ PREF_ION_COMPLETED_STUDIES,
+ "{}"
+ );
+
+ const studies = JSON.parse(completedStudies);
+ if (studyAddonId in studies) {
+ study.style.opacity = 0.5;
+ joinBtn.disabled = true;
+ document.l10n.setAttributes(joinBtn, "ion-ended-study");
+ return;
+ }
+
+ if (ionId) {
+ study.style.opacity = 1;
+ joinBtn.disabled = false;
+
+ if (addon) {
+ document.l10n.setAttributes(joinBtn, "ion-leave-study");
+ } else {
+ document.l10n.setAttributes(joinBtn, "ion-join-study");
+ }
+ } else {
+ document.l10n.setAttributes(joinBtn, "ion-study-prompt");
+ study.style.opacity = 0.5;
+ joinBtn.disabled = true;
+ }
+}
+
+// equivalent to what we use for Telemetry IDs
+// https://searchfox.org/mozilla-central/rev/9193635dca8cfdcb68f114306194ffc860456044/toolkit/components/telemetry/app/TelemetryUtils.jsm#222
+function generateUUID() {
+ let str = Services.uuid.generateUUID().toString();
+ return str.substring(1, str.length - 1);
+}
+
+async function setup(cachedAddons) {
+ document
+ .getElementById("enrollment-button")
+ .addEventListener("click", async () => {
+ const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
+
+ if (ionId) {
+ let dialog = document.getElementById("leave-ion-consent-dialog");
+ dialog.showModal();
+ dialog.scrollTop = 0;
+ } else {
+ let dialog = document.getElementById("join-ion-consent-dialog");
+ dialog.showModal();
+ dialog.scrollTop = 0;
+ }
+ });
+
+ document
+ .getElementById("join-ion-cancel-dialog-button")
+ .addEventListener("click", () =>
+ document.getElementById("join-ion-consent-dialog").close()
+ );
+ document
+ .getElementById("leave-ion-cancel-dialog-button")
+ .addEventListener("click", () =>
+ document.getElementById("leave-ion-consent-dialog").close()
+ );
+ document
+ .getElementById("join-study-cancel-dialog-button")
+ .addEventListener("click", () =>
+ document.getElementById("join-study-consent-dialog").close()
+ );
+ document
+ .getElementById("leave-study-cancel-dialog-button")
+ .addEventListener("click", () =>
+ document.getElementById("leave-study-consent-dialog").close()
+ );
+
+ document
+ .getElementById("join-ion-accept-dialog-button")
+ .addEventListener("click", async event => {
+ const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
+
+ if (!ionId) {
+ let uuid = generateUUID();
+ Services.prefs.setStringPref(PREF_ION_ID, uuid);
+ for (const cachedAddon of cachedAddons) {
+ if (cachedAddon.isDefault) {
+ let install;
+ if (Cu.isInAutomation) {
+ install = {
+ install: async () => {
+ if (
+ cachedAddon.addon_id ==
+ "ion-v2-bad-default-example@mozilla.org"
+ ) {
+ throw new Error("Bad test default add-on");
+ }
+ },
+ };
+ } else {
+ install = await AddonManager.getInstallForURL(
+ cachedAddon.sourceURI.spec
+ );
+ }
+
+ try {
+ await install.install();
+ } catch (ex) {
+ // No need to throw here, we'll try again before letting users enroll in any studies.
+ console.error(
+ `Could not install default add-on ${cachedAddon.addon_id}`
+ );
+ const availableStudies =
+ document.getElementById("available-studies");
+ document.l10n.setAttributes(
+ availableStudies,
+ "ion-no-current-studies"
+ );
+ }
+ }
+ const study = document.getElementById(cachedAddon.addon_id);
+ if (study) {
+ await updateStudy(cachedAddon.addon_id);
+ }
+ }
+ document.querySelector("dialog").close();
+ }
+ // A this point we should have a valid ion id, so we should be able to send
+ // the enrollment ping.
+ await sendEnrollmentPing();
+
+ showEnrollmentStatus();
+ });
+
+ document
+ .getElementById("leave-ion-accept-dialog-button")
+ .addEventListener("click", async event => {
+ const completedStudies = Services.prefs.getStringPref(
+ PREF_ION_COMPLETED_STUDIES,
+ "{}"
+ );
+ const studies = JSON.parse(completedStudies);
+
+ // Send a deletion ping for all completed studies the user has been a part of.
+ for (const studyAddonId in studies) {
+ await sendDeletionPing(studyAddonId);
+ }
+
+ Services.prefs.clearUserPref(PREF_ION_COMPLETED_STUDIES);
+
+ for (const cachedAddon of cachedAddons) {
+ // Record any studies that have been marked as concluded on the server, in case they re-enroll.
+ if ("studyEnded" in cachedAddon && cachedAddon.studyEnded === true) {
+ studies[cachedAddon.addon_id] = STUDY_LEAVE_REASONS.STUDY_ENDED;
+
+ Services.prefs.setStringPref(
+ PREF_ION_COMPLETED_STUDIES,
+ JSON.stringify(studies)
+ );
+ }
+
+ let addon;
+ if (Cu.isInAutomation) {
+ addon = {};
+ addon.id = cachedAddon.addon_id;
+ addon.uninstall = () => {
+ let testAddons = Services.prefs.getStringPref(
+ PREF_TEST_ADDONS,
+ "[]"
+ );
+ testAddons = JSON.parse(testAddons);
+
+ Services.prefs.setStringPref(
+ PREF_TEST_ADDONS,
+ JSON.stringify(
+ testAddons.filter(a => a.id != cachedAddon.addon_id)
+ )
+ );
+ };
+ } else {
+ addon = await AddonManager.getAddonByID(cachedAddon.addon_id);
+ }
+ if (addon) {
+ await sendDeletionPing(addon.id);
+ await addon.uninstall();
+ }
+ }
+
+ Services.prefs.clearUserPref(PREF_ION_ID);
+ for (const cachedAddon of cachedAddons) {
+ const study = document.getElementById(cachedAddon.addon_id);
+ if (study) {
+ await updateStudy(cachedAddon.addon_id);
+ }
+ }
+
+ document.getElementById("leave-ion-consent-dialog").close();
+ showEnrollmentStatus();
+ });
+
+ document
+ .getElementById("join-study-accept-dialog-button")
+ .addEventListener("click", async event => {
+ const dialog = document.getElementById("join-study-consent-dialog");
+ const studyAddonId = dialog.getAttribute("addon-id");
+ toggleEnrolled(studyAddonId, cachedAddons).then(dialog.close());
+ });
+
+ document
+ .getElementById("leave-study-accept-dialog-button")
+ .addEventListener("click", async event => {
+ const dialog = document.getElementById("leave-study-consent-dialog");
+ const studyAddonId = dialog.getAttribute("addon-id");
+ await toggleEnrolled(studyAddonId, cachedAddons).then(dialog.close());
+ });
+
+ const onAddonEvent = async addon => {
+ for (const cachedAddon of cachedAddons) {
+ if (cachedAddon.addon_id == addon.id) {
+ await updateStudy(addon.id);
+ }
+ }
+ };
+
+ const addonsListener = {
+ onEnabled: onAddonEvent,
+ onDisabled: onAddonEvent,
+ onInstalled: onAddonEvent,
+ onUninstalled: onAddonEvent,
+ };
+ AddonManager.addAddonListener(addonsListener);
+
+ window.addEventListener("unload", event => {
+ AddonManager.removeAddonListener(addonsListener);
+ });
+}
+
+function removeBadge() {
+ Services.prefs.setBoolPref(PREF_ION_NEW_STUDIES_AVAILABLE, false);
+
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ const badge = win.document
+ .getElementById("ion-button")
+ .querySelector(".toolbarbutton-badge");
+ badge.classList.remove("feature-callout");
+ }
+}
+
+// Updates Ion HTML page contents from RemoteSettings.
+function updateContents(contents) {
+ for (const section of [
+ "title",
+ "summary",
+ "details",
+ "data",
+ "joinIonConsent",
+ "leaveIonConsent",
+ ]) {
+ if (contents && section in contents) {
+ // Generate a corresponding dom-id style ID for a camel-case domId style JS attribute.
+ // Dynamically set the tag type based on which section is getting updated.
+ const domId = section
+ .split(/(?=[A-Z])/)
+ .join("-")
+ .toLowerCase();
+ // Clears out any existing children with a single #text node.
+ document.getElementById(domId).textContent = "";
+
+ const contentFragment = sanitizeHtml(contents[section]);
+ document.getElementById(domId).appendChild(contentFragment);
+ }
+ }
+}
+
+document.addEventListener("DOMContentLoaded", async domEvent => {
+ toggleContentBasedOnLocale();
+
+ showEnrollmentStatus();
+
+ document.addEventListener("focus", removeBadge);
+ removeBadge();
+
+ const privacyPolicyLinks = document.querySelectorAll(
+ ".privacy-policy,.privacy-notice"
+ );
+ for (const privacyPolicyLink of privacyPolicyLinks) {
+ const privacyPolicyFormattedLink = Services.urlFormatter.formatURL(
+ privacyPolicyLink.href
+ );
+ privacyPolicyLink.href = privacyPolicyFormattedLink;
+ }
+
+ let cachedContent;
+ let cachedAddons;
+ if (Cu.isInAutomation) {
+ let testCachedAddons = Services.prefs.getStringPref(
+ PREF_TEST_CACHED_ADDONS,
+ null
+ );
+ if (testCachedAddons) {
+ cachedAddons = JSON.parse(testCachedAddons);
+ }
+
+ let testCachedContent = Services.prefs.getStringPref(
+ PREF_TEST_CACHED_CONTENT,
+ null
+ );
+ if (testCachedContent) {
+ cachedContent = JSON.parse(testCachedContent);
+ }
+ } else {
+ cachedContent = await RemoteSettings(CONTENT_COLLECTION_KEY).get();
+ cachedAddons = await RemoteSettings(STUDY_ADDON_COLLECTION_KEY).get();
+ }
+
+ // Replace existing contents immediately on page load.
+ for (const contents of cachedContent) {
+ updateContents(contents);
+ }
+
+ for (const cachedAddon of cachedAddons) {
+ // Record any studies that have been marked as concluded on the server.
+ if ("studyEnded" in cachedAddon && cachedAddon.studyEnded === true) {
+ const completedStudies = Services.prefs.getStringPref(
+ PREF_ION_COMPLETED_STUDIES,
+ "{}"
+ );
+ const studies = JSON.parse(completedStudies);
+ studies[cachedAddon.addon_id] = STUDY_LEAVE_REASONS.STUDY_ENDED;
+
+ Services.prefs.setStringPref(
+ PREF_ION_COMPLETED_STUDIES,
+ JSON.stringify(studies)
+ );
+ }
+ }
+
+ await setup(cachedAddons);
+
+ try {
+ await showAvailableStudies(cachedAddons);
+ } catch (ex) {
+ // No need to throw here, we'll try again before letting users enroll in any studies.
+ console.error(`Could not show available studies`, ex);
+ }
+});
+
+async function sendDeletionPing(studyAddonId) {
+ const type = "pioneer-study";
+
+ const options = {
+ studyName: studyAddonId,
+ addPioneerId: true,
+ useEncryption: true,
+ // NOTE - while we're not actually sending useful data in this payload, the current Pioneer v2 Telemetry
+ // pipeline requires that pings are shaped this way so they are routed to the correct environment.
+ //
+ // At the moment, the public key used here isn't important but we do need to use *something*.
+ encryptionKeyId: "discarded",
+ publicKey: {
+ crv: "P-256",
+ kty: "EC",
+ x: "XLkI3NaY3-AF2nRMspC63BT1u0Y3moXYSfss7VuQ0mk",
+ y: "SB0KnIW-pqk85OIEYZenoNkEyOOp5GeWQhS1KeRtEUE",
+ },
+ schemaName: "deletion-request",
+ schemaVersion: 1,
+ // The schema namespace needs to be the study addon id, as we
+ // want to route the ping to the specific study table.
+ schemaNamespace: studyAddonId,
+ };
+
+ const payload = {
+ encryptedData: "",
+ };
+
+ await TelemetryController.submitExternalPing(type, payload, options);
+}
+
+/**
+ * Sends a Pioneer enrollment ping.
+ *
+ * The `creationDate` provided by the telemetry APIs will be used as the timestamp for
+ * considering the user enrolled in pioneer and/or the study.
+ *
+ * @param {string} [studyAddonId] - optional study id. It's sent in the ping, if present,
+ * to signal that user enroled in the study.
+ */
+async function sendEnrollmentPing(studyAddonId) {
+ let options = {
+ studyName: "pioneer-meta",
+ addPioneerId: true,
+ useEncryption: true,
+ // NOTE - while we're not actually sending useful data in this payload, the current Pioneer v2 Telemetry
+ // pipeline requires that pings are shaped this way so they are routed to the correct environment.
+ //
+ // At the moment, the public key used here isn't important but we do need to use *something*.
+ encryptionKeyId: "discarded",
+ publicKey: {
+ crv: "P-256",
+ kty: "EC",
+ x: "XLkI3NaY3-AF2nRMspC63BT1u0Y3moXYSfss7VuQ0mk",
+ y: "SB0KnIW-pqk85OIEYZenoNkEyOOp5GeWQhS1KeRtEUE",
+ },
+ schemaName: "pioneer-enrollment",
+ schemaVersion: 1,
+ // Note that the schema namespace directly informs how data is segregated after ingestion.
+ // If this is an enrollment ping for the pioneer program (in contrast to the enrollment to
+ // a specific study), use a meta namespace.
+ schemaNamespace: "pioneer-meta",
+ };
+
+ // If we were provided with a study id, then this is an enrollment to a study.
+ // Send the id alongside with the data and change the schema namespace to simplify
+ // the work on the ingestion pipeline.
+ if (typeof studyAddonId != "undefined") {
+ options.studyName = studyAddonId;
+ // The schema namespace needs to be the study addon id, as we
+ // want to route the ping to the specific study table.
+ options.schemaNamespace = studyAddonId;
+ }
+
+ await TelemetryController.submitExternalPing("pioneer-study", {}, options);
+}