Bug 1486507 - Record telemetry for browser language changes r=rpl,flod,chutten
authorMark Striemer <mstriemer@mozilla.com>
Sat, 10 Nov 2018 10:45:23 -0600
changeset 509917 a94a05a0a11d5bfb885eac7c0d138d12355a668e
parent 509916 f6f97217b46cbcfecc46d844540ec94f862d9d34
child 509918 9b9dc72ed3444122f72145872490e75bbe9f3028
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrpl, flod, chutten
bugs1486507
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1486507 - Record telemetry for browser language changes r=rpl,flod,chutten 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]