/* 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 [studyAddonid=undefined] - 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);
}
