Bug 1486507 - Record telemetry for browser language changes. r=rpl,flod,chutten, a=RyanVM
authorMark Striemer <mstriemer@mozilla.com>
Sat, 10 Nov 2018 10:45:23 -0600
changeset 506644 a902a8dcea8d2d51debed83ddf0f589ae4c88129
parent 506643 07265912b0c7eaccbe864f27f3b1ac60b4831700
child 506645 9104a4e2e3a4670bb75334e3ab1994f7728362b9
push id10497
push userryanvm@gmail.com
push dateSat, 12 Jan 2019 18:08:31 +0000
treeherdermozilla-beta@c0445e5ce388 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrpl, flod, chutten, RyanVM
bugs1486507
milestone65.0
Bug 1486507 - Record telemetry for browser language changes. r=rpl,flod,chutten, a=RyanVM Differential Revision: https://phabricator.services.mozilla.com/D11795
browser/components/preferences/browserLanguages.js
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/tests/browser_browser_languages_subdialog.js
toolkit/components/telemetry/Events.yaml
--- a/browser/components/preferences/browserLanguages.js
+++ b/browser/components/preferences/browserLanguages.js
@@ -22,40 +22,47 @@ ChromeUtils.defineModuleGetter(this, "Se
  * requested locales must be installed and enabled. Available locales could be
  * installed and enabled, or fetched from the AMO language tools API.
  *
  * If a langpack is disabled, there is no way to determine what locale it is for and
  * it will only be listed as available if that locale is also available on AMO and
  * the user has opted to search for more languages.
  */
 
-async function installFromUrl(url, hash) {
+async function installFromUrl(url, hash, callback) {
+  let telemetryInfo = {
+    source: "about:preferences",
+  };
   let install = await AddonManager.getInstallForURL(
-    url, "application/x-xpinstall", hash);
+    url, "application/x-xpinstall", hash, null, null, null, null, telemetryInfo);
+  if (callback) {
+    callback(install.installId.toString());
+  }
   await install.install();
   return install.addon;
 }
 
 async function dictionaryIdsForLocale(locale) {
   let entries = await RemoteSettings("language-dictionaries").get({
     filters: {id: locale},
   });
   if (entries.length > 0) {
     return entries[0].dictionaries;
   }
   return [];
 }
 
 class OrderedListBox {
-  constructor({richlistbox, upButton, downButton, removeButton, onRemove}) {
+  constructor({richlistbox, upButton, downButton, removeButton, onRemove, onReorder}) {
     this.richlistbox = richlistbox;
     this.upButton = upButton;
     this.downButton = downButton;
     this.removeButton = removeButton;
     this.onRemove = onRemove;
+    this.onReorder = onReorder;
 
     this.items = [];
 
     this.richlistbox.addEventListener("select", () => this.setButtonState());
     this.upButton.addEventListener("command", () => this.moveUp());
     this.downButton.addEventListener("command", () => this.moveDown());
     this.removeButton.addEventListener("command", () => this.removeItem());
   }
@@ -82,16 +89,18 @@ class OrderedListBox {
     let prevItem = items[selectedIndex - 1];
     items[selectedIndex - 1] = items[selectedIndex];
     items[selectedIndex] = prevItem;
     let prevEl = document.getElementById(prevItem.id);
     let selectedEl = document.getElementById(selectedItem.id);
     this.richlistbox.insertBefore(selectedEl, prevEl);
     this.richlistbox.ensureElementIsVisible(selectedEl);
     this.setButtonState();
+
+    this.onReorder();
   }
 
   moveDown() {
     let {selectedIndex} = this.richlistbox;
     if (selectedIndex == this.items.length - 1) {
       return;
     }
     let {items} = this;
@@ -99,16 +108,18 @@ class OrderedListBox {
     let nextItem = items[selectedIndex + 1];
     items[selectedIndex + 1] = items[selectedIndex];
     items[selectedIndex] = nextItem;
     let nextEl = document.getElementById(nextItem.id);
     let selectedEl = document.getElementById(selectedItem.id);
     this.richlistbox.insertBefore(nextEl, selectedEl);
     this.richlistbox.ensureElementIsVisible(selectedEl);
     this.setButtonState();
+
+    this.onReorder();
   }
 
   removeItem() {
     let {selectedIndex} = this.richlistbox;
 
     if (selectedIndex == -1) {
       return;
     }
@@ -300,33 +311,42 @@ function compareItems(a, b) {
   // One of them is a label, put it first.
   } else if (a.value) {
     return 1;
   }
   return -1;
 }
 
 var gBrowserLanguagesDialog = {
+  telemetryId: null,
+  accepted: false,
   _availableLocales: null,
   _selectedLocales: null,
   selectedLocales: null,
 
   get downloadEnabled() {
     // Downloading langpacks isn't always supported, check the pref.
     return Services.prefs.getBoolPref("intl.multilingual.downloadEnabled");
   },
 
+  recordTelemetry(method, extra = null) {
+    Services.telemetry.recordEvent(
+      "intl.ui.browserLanguage", method, "dialog", this.telemetryId, extra);
+  },
+
   beforeAccept() {
     this.selected = this.getSelectedLocales();
+    this.accepted = true;
     return true;
   },
 
   async onLoad() {
     // Maintain the previously selected locales even if we cancel out.
-    let {selected, search} = window.arguments[0] || {};
+    let {telemetryId, selected, search} = window.arguments[0];
+    this.telemetryId = telemetryId;
     this.selectedLocales = selected;
 
     // This is a list of available locales that the user selected. It's more
     // restricted than the Intl notion of `requested` as it only contains
     // locale codes for which we have matching locales available.
     // The first time this dialog is opened, populate with appLocalesAsBCP47.
     let selectedLocales = this.selectedLocales || Services.locale.appLocalesAsBCP47;
     let selectedLocaleSet = new Set(selectedLocales);
@@ -347,29 +367,33 @@ var gBrowserLanguagesDialog = {
 
   initSelectedLocales(selectedLocales) {
     this._selectedLocales = new OrderedListBox({
       richlistbox: document.getElementById("selectedLocales"),
       upButton: document.getElementById("up"),
       downButton: document.getElementById("down"),
       removeButton: document.getElementById("remove"),
       onRemove: (item) => this.selectedLocaleRemoved(item),
+      onReorder: () => this.recordTelemetry("reorder"),
     });
     this._selectedLocales.setItems(getLocaleDisplayInfo(selectedLocales));
   },
 
   async initAvailableLocales(available, search) {
     this._availableLocales = new SortedItemSelectList({
       menulist: document.getElementById("availableLocales"),
       button: document.getElementById("add"),
       compareFn: compareItems,
       onSelect: (item) => this.availableLanguageSelected(item),
       onChange: (item) => {
         this.hideError();
         if (item.value == "search") {
+          // Record the search event here so we don't track the search from
+          // the main preferences pane twice.
+          this.recordTelemetry("search");
           this.loadLocalesFromAMO();
         }
       },
     });
 
     // Populate the list with the installed locales even if the user is
     // searching in case the download fails.
     await this.loadLocalesFromInstalled(available);
@@ -445,18 +469,20 @@ var gBrowserLanguagesDialog = {
         value: "search",
       });
     }
     this._availableLocales.setItems(items);
   },
 
   async availableLanguageSelected(item) {
     if (Services.locale.availableLocales.includes(item.value)) {
+      this.recordTelemetry("add");
       this.requestLocalLanguage(item);
     } else if (this.availableLangpacks.has(item.value)) {
+      // Telemetry is tracked in requestRemoteLanguage.
       await this.requestRemoteLanguage(item);
     } else {
       this.showError();
     }
   },
 
   requestLocalLanguage(item, available) {
     this._selectedLocales.addItem(item);
@@ -474,17 +500,18 @@ var gBrowserLanguagesDialog = {
   async requestRemoteLanguage(item) {
     this._availableLocales.disableWithMessageId(
       "browser-languages-downloading");
 
     let {url, hash} = this.availableLangpacks.get(item.value);
     let addon;
 
     try {
-      addon = await installFromUrl(url, hash);
+      addon = await installFromUrl(
+        url, hash, (installId) => this.recordTelemetry("add", {installId}));
     } catch (e) {
       this.showError();
       return;
     }
 
     // If the add-on was previously installed, it might be disabled still.
     if (addon.userDisabled) {
       await addon.enable();
@@ -530,16 +557,18 @@ var gBrowserLanguagesDialog = {
     document.getElementById("warning-message").hidden = true;
   },
 
   getSelectedLocales() {
     return this._selectedLocales.items.map(item => item.value);
   },
 
   async selectedLocaleRemoved(item) {
+    this.recordTelemetry("remove");
+
     this._availableLocales.addItem(item);
 
     // If the item we added is at the top of the list, it needs the label.
     if (this._availableLocales.items[0] == item) {
       this._availableLocales.addItem(await this.createInstalledLabel());
     }
   },
 
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -751,16 +751,25 @@ var gMainPane = {
         warnOnQuitCheckbox.removeAttribute("disabled");
       } else {
         warnOnQuitCheckbox.setAttribute("disabled", "true");
       }
     }
   },
 
   initBrowserLocale() {
+    // Enable telemetry.
+    Services.telemetry.setEventRecordingEnabled("intl.ui.browserLanguage", true);
+
+    // This will register the "command" listener.
+    let menulist = document.getElementById("defaultBrowserLanguage");
+    new SelectionChangedMenulist(menulist, event => {
+      gMainPane.onBrowserLanguageChange(event);
+    });
+
     gMainPane.setBrowserLocales(Services.locale.appLocaleAsBCP47);
   },
 
   /**
    * Update the available list of locales and select the locale that the user
    * is "selecting". This could be the currently requested locale or a locale
    * that the user would like to switch to after confirmation.
    */
@@ -789,21 +798,16 @@ var gMainPane = {
     }
 
     let menulist = document.getElementById("defaultBrowserLanguage");
     let menupopup = menulist.querySelector("menupopup");
     menupopup.textContent = "";
     menupopup.appendChild(fragment);
     menulist.value = selected;
 
-    // This will register the "command" listener.
-    new SelectionChangedMenulist(menulist, event => {
-      gMainPane.onBrowserLanguageChange(event);
-    });
-
     document.getElementById("browserLanguagesBox").hidden = false;
   },
 
   /* Show the confirmation message bar to allow a restart into the new locales. */
   async showConfirmLanguageChangeMessageBar(locales) {
     let messageBar = document.getElementById("confirmBrowserLanguage");
 
     // Get the bundle for the new locale.
@@ -862,16 +866,19 @@ var gMainPane = {
   confirmBrowserLanguageChange(event) {
     let localesString = (event.target.getAttribute("locales") || "").trim();
     if (!localesString || localesString.length == 0) {
       return;
     }
     let locales = localesString.split(",");
     Services.locale.requestedLocales = locales;
 
+    // Record the change in telemetry before we restart.
+    gMainPane.recordBrowserLanguagesTelemetry("apply");
+
     // Restart with the new locale.
     let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
     Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
     if (!cancelQuit.data) {
       Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
     }
   },
 
@@ -882,16 +889,19 @@ var gMainPane = {
     if (locale == "search") {
       gMainPane.showBrowserLanguages({search: true});
       return;
     } else if (locale == Services.locale.appLocaleAsBCP47) {
       this.hideConfirmLanguageChangeMessageBar();
       return;
     }
 
+    // Note the change in telemetry.
+    gMainPane.recordBrowserLanguagesTelemetry("reorder");
+
     let locales = Array.from(new Set([
       locale,
       ...Services.locale.requestedLocales,
     ]).values());
     this.showConfirmLanguageChangeMessageBar(locales);
   },
 
   onBrowserRestoreSessionChange(event) {
@@ -1019,28 +1029,39 @@ var gMainPane = {
 
   /**
    * Shows a dialog in which the preferred language for web content may be set.
    */
   showLanguages() {
     gSubDialog.open("chrome://browser/content/preferences/languages.xul");
   },
 
+  recordBrowserLanguagesTelemetry(method, value = null) {
+    Services.telemetry.recordEvent("intl.ui.browserLanguage", method, "main", value);
+  },
+
   showBrowserLanguages({search}) {
-    let opts = {selected: gMainPane.selectedLocales, search};
+    // Record the telemetry event with an id to associate related actions.
+    let telemetryId = parseInt(Services.telemetry.msSinceProcessStart(), 10).toString();
+    let method = search ? "search" : "manage";
+    gMainPane.recordBrowserLanguagesTelemetry(method, telemetryId);
+
+    let opts = {selected: gMainPane.selectedLocales, search, telemetryId};
     gSubDialog.open(
       "chrome://browser/content/preferences/browserLanguages.xul",
       null, opts, this.browserLanguagesClosed);
   },
 
   /* Show or hide the confirm change message bar based on the updated ordering. */
   browserLanguagesClosed() {
-    let selected = this.gBrowserLanguagesDialog.selected;
+    let {accepted, selected} = this.gBrowserLanguagesDialog;
     let active = Services.locale.appLocalesAsBCP47;
 
+    this.gBrowserLanguagesDialog.recordTelemetry(accepted ? "accept" : "cancel");
+
     // Prepare for changing the locales if they are different than the current locales.
     if (selected && selected.join(",") != active.join(",")) {
       gMainPane.showConfirmLanguageChangeMessageBar(selected);
       gMainPane.setBrowserLocales(selected[0]);
       return;
     }
 
     // They matched, so we can reset the UI.
--- a/browser/components/preferences/in-content/tests/browser_browser_languages_subdialog.js
+++ b/browser/components/preferences/in-content/tests/browser_browser_languages_subdialog.js
@@ -4,16 +4,17 @@
 ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this);
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 
 AddonTestUtils.initMochitest(this);
 
 const BROWSER_LANGUAGES_URL = "chrome://browser/content/preferences/browserLanguages.xul";
 const DICTIONARY_ID_PL = "pl@dictionaries.addons.mozilla.org";
+const TELEMETRY_CATEGORY = "intl.ui.browserLanguage";
 
 function langpackId(locale) {
   return `langpack-${locale}@firefox.mozilla.org`;
 }
 
 function getManifestData(locale, version = "2.0") {
   return {
     langpack_id: locale,
@@ -144,16 +145,36 @@ function assertAvailableLocales(list, lo
   let listLocales = items
     .filter(item => item.value && item.value != "search");
   is(listLocales.length, locales.length, "The right number of locales are available");
   is(listLocales.map(item => item.value).sort(),
      locales.sort().join(","), "The available locales match");
   is(items[0].getAttribute("class"), "label-item", "The first row is a label");
 }
 
+function getDialogId(dialogDoc) {
+  return dialogDoc.ownerGlobal.arguments[0].telemetryId;
+}
+
+function assertTelemetryRecorded(events) {
+  let snapshot = Services.telemetry.snapshotEvents(
+    Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+
+  // Make sure we got some data.
+  ok(snapshot.parent && snapshot.parent.length > 0, "Got parent telemetry events in the snapshot");
+
+  // Only look at the related events after stripping the timestamp and category.
+  let relatedEvents = snapshot.parent
+    .filter(([timestamp, category]) => category == TELEMETRY_CATEGORY)
+    .map(relatedEvent => relatedEvent.slice(2, 6));
+
+  // Events are now an array of: method, object[, value[, extra]] as expected.
+  Assert.deepEqual(relatedEvents, events, "The events are recorded correctly");
+}
+
 function selectLocale(localeCode, available, dialogDoc) {
   let [locale] = Array.from(available.firstElementChild.children)
     .filter(item => item.value == localeCode);
   available.selectedItem = locale;
   dialogDoc.getElementById("add").doCommand();
 }
 
 async function openDialog(doc, search = false) {
@@ -243,18 +264,33 @@ add_task(async function testDisabledBrow
     target => selected.itemCount == 3);
   assertLocaleOrder(selected, "pl,en-US,he");
 
   // Find pl again since it's been upgraded.
   pl = await AddonManager.getAddonByID(langpackId("pl"));
   is(pl.userDisabled, false, "pl is now enabled");
   is(pl.version, "2.0", "pl is upgraded to version 2.0");
 
+  let dialogId = getDialogId(dialogDoc);
+  ok(dialogId, "There's a dialogId");
+  let {installId} = pl.install;
+  ok(installId, "There's an installId");
+
   await Promise.all(addons.map(addon => addon.uninstall()));
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+  // FIXME: Also here
+  assertTelemetryRecorded([
+    ["manage", "main", dialogId],
+    ["search", "dialog", dialogId],
+    ["add", "dialog", dialogId, {installId}],
+
+    // Cancel is recorded when the tab is closed.
+    ["cancel", "dialog", dialogId],
+  ]);
 });
 
 add_task(async function testReorderingBrowserLanguages() {
   await SpecialPowers.pushPrefEnv({
     set: [
       ["intl.multilingual.enabled", true],
       ["intl.multilingual.downloadEnabled", true],
       ["intl.locale.requested", "en-US,pl,he,de"],
@@ -272,16 +308,17 @@ add_task(async function testReorderingBr
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
 
   let doc = gBrowser.contentDocument;
   let messageBar = doc.getElementById("confirmBrowserLanguage");
   is(messageBar.hidden, true, "The message bar is hidden at first");
 
   // Open the dialog.
   let {dialog, dialogDoc, selected} = await openDialog(doc);
+  let firstDialogId = getDialogId(dialogDoc);
 
   // The initial order is set by the pref, filtered by available.
   assertLocaleOrder(selected, "en-US,pl,he");
 
   // Moving pl down changes the order.
   selected.selectedItem = selected.querySelector("[value='pl']");
   dialogDoc.getElementById("down").doCommand();
   assertLocaleOrder(selected, "en-US,he,pl");
@@ -293,16 +330,17 @@ add_task(async function testReorderingBr
   is(messageBar.hidden, false, "The message bar is now visible");
   is(messageBar.querySelector("button").getAttribute("locales"), "en-US,he,pl",
      "The locales are set on the message bar button");
 
   // Open the dialog again.
   let newDialog = await openDialog(doc);
   dialog = newDialog.dialog;
   dialogDoc = newDialog.dialogDoc;
+  let secondDialogId = getDialogId(dialogDoc);
   selected = newDialog.selected;
 
   // The initial order comes from the previous settings.
   assertLocaleOrder(selected, "en-US,he,pl");
 
   // Select pl in the list.
   selected.selectedItem = selected.querySelector("[value='pl']");
   // Move pl back up.
@@ -310,19 +348,32 @@ add_task(async function testReorderingBr
   assertLocaleOrder(selected, "en-US,pl,he");
 
   // Accepting the change hides the confirm message bar.
   dialogClosed = BrowserTestUtils.waitForEvent(dialogDoc.documentElement, "dialogclosing");
   dialog.acceptDialog();
   await dialogClosed;
   is(messageBar.hidden, true, "The message bar is hidden again");
 
+  ok(firstDialogId, "There was an id on the first dialog");
+  ok(secondDialogId, "There was an id on the second dialog");
+  ok(firstDialogId != secondDialogId, "The dialog ids are different");
+  ok(firstDialogId < secondDialogId, "The second dialog id is larger than the first");
+
   await Promise.all(addons.map(addon => addon.uninstall()));
+  BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
-  BrowserTestUtils.removeTab(gBrowser.selectedTab);
+  assertTelemetryRecorded([
+    ["manage", "main", firstDialogId],
+    ["reorder", "dialog", firstDialogId],
+    ["accept", "dialog", firstDialogId],
+    ["manage", "main", secondDialogId],
+    ["reorder", "dialog", secondDialogId],
+    ["accept", "dialog", secondDialogId],
+  ]);
 });
 
 add_task(async function testAddAndRemoveSelectedLanguages() {
   await SpecialPowers.pushPrefEnv({
     set: [
       ["intl.multilingual.enabled", true],
       ["intl.multilingual.downloadEnabled", true],
       ["intl.locale.requested", "en-US"],
@@ -339,16 +390,17 @@ add_task(async function testAddAndRemove
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
 
   let doc = gBrowser.contentDocument;
   let messageBar = doc.getElementById("confirmBrowserLanguage");
   is(messageBar.hidden, true, "The message bar is hidden at first");
 
   // Open the dialog.
   let {dialog, dialogDoc, available, selected} = await openDialog(doc);
+  let dialogId = getDialogId(dialogDoc);
 
   // The initial order is set by the pref.
   assertLocaleOrder(selected, "en-US");
   assertAvailableLocales(available, ["fr", "pl", "he"]);
 
   // Add pl and fr to selected.
   selectLocale("pl", available, dialogDoc);
   selectLocale("fr", available, dialogDoc);
@@ -377,18 +429,31 @@ add_task(async function testAddAndRemove
     {attributes: true, attributeFilter: ["hidden"]},
     target => !target.hidden);
 
   is(messageBar.hidden, false, "The message bar is now visible");
   is(messageBar.querySelector("button").getAttribute("locales"), "he,en-US",
     "The locales are set on the message bar button");
 
   await Promise.all(addons.map(addon => addon.uninstall()));
+  BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
-  BrowserTestUtils.removeTab(gBrowser.selectedTab);
+  assertTelemetryRecorded([
+    ["manage", "main", dialogId],
+
+    // Install id is not recorded since it was already installed.
+    ["add", "dialog", dialogId],
+    ["add", "dialog", dialogId],
+
+    ["remove", "dialog", dialogId],
+    ["remove", "dialog", dialogId],
+
+    ["add", "dialog", dialogId],
+    ["accept", "dialog", dialogId],
+  ]);
 });
 
 add_task(async function testInstallFromAMO() {
   let langpacks = await AddonManager.getAddonsByTypes(["locale"]);
   is(langpacks.length, 0, "There are no langpacks installed");
 
   let langpacksFile = await createLanguageToolsFile();
   let langpacksUrl = Services.io.newFileURI(langpacksFile).spec;
@@ -409,16 +474,17 @@ add_task(async function testInstallFromA
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
 
   let doc = gBrowser.contentDocument;
   let messageBar = doc.getElementById("confirmBrowserLanguage");
   is(messageBar.hidden, true, "The message bar is hidden at first");
 
   // Open the dialog.
   let {dialog, dialogDoc, available, selected} = await openDialog(doc, true);
+  let firstDialogId = getDialogId(dialogDoc);
 
   // Make sure the message bar is still hidden.
   is(messageBar.hidden, true, "The message bar is still hidden after searching");
 
   if (available.itemCount == 1) {
     await waitForMutation(
       available.firstElementChild,
       {childList: true},
@@ -440,16 +506,22 @@ add_task(async function testInstallFromA
 
   // Wait for the langpack to install and be added to the list.
   let selectedLocales = dialogDoc.getElementById("selectedLocales");
   await waitForMutation(
     selectedLocales,
     {childList: true},
     target => selectedLocales.itemCount == 2);
 
+  let langpack = await AddonManager.getAddonByID(langpackId("pl"));
+  Assert.deepEqual(
+    langpack.installTelemetryInfo,
+    {source: "about:preferences"},
+    "The source is set to preferences");
+
   // Verify the list is correct.
   assertLocaleOrder(selected, "pl,en-US");
   assertAvailableLocales(available, ["fr", "he"]);
   is(Services.locale.availableLocales.sort().join(","),
      "en-US,pl", "Polish is now installed");
 
   await BrowserTestUtils.waitForCondition(async () => {
     let newDicts = await AddonManager.getAddonsByTypes(["dictionary"]);
@@ -467,20 +539,22 @@ add_task(async function testInstallFromA
   assertLocaleOrder(selected, "en-US,pl");
 
   // Test that disabling the langpack removes it from the list.
   let dialogClosed = BrowserTestUtils.waitForEvent(dialogDoc.documentElement, "dialogclosing");
   dialog.acceptDialog();
   await dialogClosed;
 
   // Disable the Polish langpack.
-  let langpack = await AddonManager.getAddonByID("langpack-pl@firefox.mozilla.org");
+  langpack = await AddonManager.getAddonByID("langpack-pl@firefox.mozilla.org");
+  let installId = langpack.install.installId;
   await langpack.disable();
 
   ({dialogDoc, available, selected} = await openDialog(doc, true));
+  let secondDialogId = getDialogId(dialogDoc);
 
   // Wait for the available langpacks to load.
   if (available.itemCount == 1) {
     await waitForMutation(
       available.firstElementChild,
       {childList: true},
       target => available.itemCount > 1);
   }
@@ -488,16 +562,32 @@ add_task(async function testInstallFromA
   assertAvailableLocales(available, ["fr", "he", "pl"]);
 
   // Uninstall the langpack and dictionary.
   let installs = await AddonManager.getAddonsByTypes(["locale", "dictionary"]);
   is(installs.length, 2, "There is one langpack and one dictionary installed");
   await Promise.all(installs.map(item => item.uninstall()));
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+  ok(installId, "The langpack has an installId");
+  // FIXME: Most are here
+  assertTelemetryRecorded([
+    // First dialog installs a locale and accepts.
+    ["search", "main", firstDialogId],
+    // It has an installId since it was downloaded.
+    ["add", "dialog", firstDialogId, {installId}],
+    // It got moved down to avoid errors with finding translations.
+    ["reorder", "dialog", firstDialogId],
+    ["accept", "dialog", firstDialogId],
+
+    // The second dialog just checks the state and is closed with the tab.
+    ["search", "main", secondDialogId],
+    ["cancel", "dialog", secondDialogId],
+  ]);
 });
 
 let hasSearchOption = popup => Array.from(popup.children).some(el => el.value == "search");
 
 add_task(async function testDownloadEnabled() {
   await SpecialPowers.pushPrefEnv({
     set: [
       ["intl.multilingual.enabled", true],
@@ -532,8 +622,60 @@ add_task(async function testDownloadDisa
   let defaultMenulist = doc.getElementById("defaultBrowserLanguage");
   ok(!hasSearchOption(defaultMenulist.firstChild), "There's no search option in the General pane");
 
   let { available } = await openDialog(doc, false);
   ok(!hasSearchOption(available.firstChild), "There's no search option in the dialog");
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
+
+add_task(async function testReorderMainPane() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["intl.multilingual.enabled", true],
+      ["intl.multilingual.downloadEnabled", false],
+      ["intl.locale.requested", "en-US"],
+      ["extensions.langpacks.signatures.required", false],
+    ],
+  });
+
+  // Clear the telemetry from other tests.
+  Services.telemetry.clearEvents();
+
+  let langpacks = await createTestLangpacks();
+  let addons = await Promise.all(langpacks.map(async ([locale, file]) => {
+    let install = await AddonTestUtils.promiseInstallFile(file);
+    return install.addon;
+  }));
+
+  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+  let doc = gBrowser.contentDocument;
+
+  let messageBar = doc.getElementById("confirmBrowserLanguage");
+  is(messageBar.hidden, true, "The message bar is hidden at first");
+
+  let available = doc.getElementById("defaultBrowserLanguage");
+  let availableLocales = Array.from(available.firstElementChild.children);
+  let availableCodes = availableLocales.map(item => item.value).sort().join(",");
+  is(availableCodes, "en-US,fr,he,pl",
+     "All of the available locales are listed");
+
+  is(available.selectedItem.value, "en-US", "English is selected");
+
+  let hebrew = availableLocales[availableLocales.findIndex(item => item.value == "he")];
+  hebrew.click();
+  available.firstElementChild.hidePopup();
+
+  await BrowserTestUtils.waitForCondition(
+    () => !messageBar.hidden, "Wait for message bar to show");
+
+  is(messageBar.hidden, false, "The message bar is now shown");
+  is(messageBar.querySelector("button").getAttribute("locales"), "he,en-US",
+     "The locales are set on the message bar button");
+
+  await Promise.all(addons.map(addon => addon.uninstall()));
+  BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+  assertTelemetryRecorded([
+    ["reorder", "main"],
+  ]);
+});
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -647,8 +647,39 @@ security.ui.identitypopup:
     release_channel_collection: opt-in
     record_in_processes:
       - main
     extra_keys:
       tp: Whether Tracking Protection was active while the user interacted with the UI
       cr: Whether Cookie Restrictions was active while the user interacted with the UI
     products:
       - firefox
+
+intl.ui.browserLanguage:
+  action:
+    description: >
+      User interactions for the browser language within about-preferences in the main pane and in
+      the browser language dialog. Each dialog event (on the dialog object, and the manage and
+      search methods of the main object) has a value which is a monotonically increasing number
+      that links it with other events related to the same dialog instance.
+    objects:
+      - dialog
+      - main
+    methods:
+      - manage
+      - search
+      - add
+      - remove
+      - reorder
+      - apply
+      - accept
+      - cancel
+    extra_keys:
+      installId: The id for an install.
+    products:
+      - firefox
+    expiry_version: "70"
+    notification_emails:
+      - flod@mozilla.com
+      - mstriemer@mozilla.com
+    release_channel_collection: opt-out
+    record_in_processes: ["main"]
+    bug_numbers: [1486507]