Bug 1461439: Enable browser error telemetry on non-Nightly channels. draft
authorMichael Kelly <mkelly@mozilla.com>
Tue, 05 Jun 2018 11:15:54 -0700
changeset 808456 bcae14975e9247bfcb079118a86ab8f66d46748c
parent 807714 0b5495dc100dd3bfda0886a4ad563a3c729c9b72
push id113391
push userbmo:mkelly@mozilla.com
push dateTue, 19 Jun 2018 16:24:38 +0000
bugs1461439
milestone62.0a1
Bug 1461439: Enable browser error telemetry on non-Nightly channels. MozReview-Commit-ID: GAwbFC49b8H
browser/components/nsBrowserGlue.js
browser/modules/BrowserErrorReporter.jsm
browser/modules/test/browser/browser.ini
browser/modules/test/browser/browser_BrowserErrorReporter.js
browser/modules/test/browser/browser_BrowserErrorReporter_nightly.js
browser/modules/test/browser/head_BrowserErrorReporter.js
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1108,35 +1108,37 @@ BrowserGlue.prototype = {
     }
 
     PageThumbs.uninit();
     NewTabUtils.uninit();
     AboutPrivateBrowsingHandler.uninit();
     AutoCompletePopup.uninit();
     DateTimePickerParent.uninit();
 
-    // Browser errors are only collected on Nightly
-    if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_DATA_REPORTING) {
+    // Browser errors are only collected on Nightly, but telemetry for
+    // them is collected on all channels.
+    if (AppConstants.MOZ_DATA_REPORTING) {
       this.browserErrorReporter.uninit();
     }
 
     Normandy.uninit();
 
     SavantShieldStudy.uninit();
   },
 
   // All initial windows have opened.
   _onWindowsRestored: function BG__onWindowsRestored() {
     if (this._windowsWereRestored) {
       return;
     }
     this._windowsWereRestored = true;
 
-    // Browser errors are only collected on Nightly
-    if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_DATA_REPORTING) {
+    // Browser errors are only collected on Nightly, but telemetry for
+    // them is collected on all channels.
+    if (AppConstants.MOZ_DATA_REPORTING) {
       this.browserErrorReporter.init();
     }
 
     BrowserUsageTelemetry.init();
 
     // Show update notification, if needed.
     if (Services.prefs.prefHasUserValue("app.update.postupdate"))
       this._showUpdateNotification();
--- a/browser/modules/BrowserErrorReporter.jsm
+++ b/browser/modules/BrowserErrorReporter.jsm
@@ -239,18 +239,19 @@ class BrowserErrorReporter {
     if (message.stack) {
       Services.telemetry.scalarAdd(TELEMETRY_ERROR_COLLECTED_STACK, 1);
     }
     if (message.sourceName) {
       const key = this.errorCollectedFilenameKey(message.sourceName);
       Services.telemetry.keyedScalarAdd(TELEMETRY_ERROR_COLLECTED_FILENAME, key.slice(0, 69), 1);
     }
 
-    // Old builds should not send errors to Sentry
-    if (!this.isRecentBuild()) {
+    // We do not collect errors on non-Nightly channels, just telemetry.
+    // Also, old builds should not send errors to Sentry
+    if (!AppConstants.NIGHTLY_BUILD || !this.isRecentBuild()) {
       return;
     }
 
     // Sample the amount of errors we send out
     let sampleRate = Number.parseFloat(this.sampleRatePref);
     for (const [regex, rate] of MODULE_SAMPLE_RATES) {
       if (message.sourceName.match(regex)) {
         sampleRate = rate;
--- a/browser/modules/test/browser/browser.ini
+++ b/browser/modules/test/browser/browser.ini
@@ -1,15 +1,20 @@
 [DEFAULT]
 support-files =
   head.js
 
 [browser_BrowserErrorReporter.js]
 skip-if = (verify && !debug && (os == 'mac' || os == 'win'))
 support-files =
+  head_BrowserErrorReporter.js
+[browser_BrowserErrorReporter_nightly.js]
+skip-if = !nightly_build || (verify && !debug && (os == 'mac' || os == 'win'))
+support-files =
+  head_BrowserErrorReporter.js
   browser_BrowserErrorReporter.html
 [browser_BrowserWindowTracker.js]
 [browser_ContentSearch.js]
 support-files =
   contentSearch.js
   contentSearchBadImage.xml
   contentSearchSuggestions.sjs
   contentSearchSuggestions.xml
--- a/browser/modules/test/browser/browser_BrowserErrorReporter.js
+++ b/browser/modules/test/browser/browser_BrowserErrorReporter.js
@@ -1,82 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
+// This file contains BrowserErrorReporter tests that don't depend on
+// errors being collected, which is only enabled on Nightly builds.
+
 ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this);
 ChromeUtils.import("resource:///modules/BrowserErrorReporter.jsm", this);
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm", this);
 
 /* global sinon */
-Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
-registerCleanupFunction(function() {
-  delete window.sinon;
-});
-
-const PREF_ENABLED = "browser.chrome.errorReporter.enabled";
-const PREF_PROJECT_ID = "browser.chrome.errorReporter.projectId";
-const PREF_PUBLIC_KEY = "browser.chrome.errorReporter.publicKey";
-const PREF_SAMPLE_RATE = "browser.chrome.errorReporter.sampleRate";
-const PREF_SUBMIT_URL = "browser.chrome.errorReporter.submitUrl";
-const TELEMETRY_ERROR_COLLECTED = "browser.errors.collected_count";
-const TELEMETRY_ERROR_COLLECTED_FILENAME = "browser.errors.collected_count_by_filename";
-const TELEMETRY_ERROR_COLLECTED_STACK = "browser.errors.collected_with_stack_count";
-const TELEMETRY_ERROR_REPORTED = "browser.errors.reported_success_count";
-const TELEMETRY_ERROR_REPORTED_FAIL = "browser.errors.reported_failure_count";
-const TELEMETRY_ERROR_SAMPLE_RATE = "browser.errors.sample_rate";
-
-function createScriptError(options = {}) {
-  const scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
-  scriptError.init(
-    options.message || "",
-    "sourceName" in options ? options.sourceName : null,
-    options.sourceLine || null,
-    options.lineNumber || null,
-    options.columnNumber || null,
-    options.flags || Ci.nsIScriptError.errorFlag,
-    options.category || "chrome javascript",
-  );
-  return scriptError;
-}
-
-function noop() {
-  // Do nothing
-}
-
-// Clears the console of any previous messages. Should be called at the end of
-// each test that logs to the console.
-function resetConsole() {
-  Services.console.logStringMessage("");
-  Services.console.reset();
-}
-
-// Finds the fetch spy call for an error with a matching message.
-function fetchCallForMessage(fetchSpy, message) {
-  for (const call of fetchSpy.getCalls()) {
-    const body = JSON.parse(call.args[1].body);
-    if (body.exception.values[0].value.includes(message)) {
-      return call;
-    }
-  }
-
-  return null;
-}
-
-// Helper to test if a fetch spy was called with the given error message.
-// Used in tests where unrelated JS errors from other code are logged.
-function fetchPassedError(fetchSpy, message) {
-  return fetchCallForMessage(fetchSpy, message) !== null;
-}
-
-add_task(async function testSetup() {
-  const canRecordExtended = Services.telemetry.canRecordExtended;
-  Services.telemetry.canRecordExtended = true;
-  registerCleanupFunction(() => Services.telemetry.canRecordExtended = canRecordExtended);
-});
+Services.scriptloader.loadSubScript(new URL("head_BrowserErrorReporter.js", gTestPath).href, this);
 
 add_task(async function testInitPrefDisabled() {
   let listening = false;
   const reporter = new BrowserErrorReporter({
     registerListener() {
       listening = true;
     },
   });
@@ -104,42 +43,16 @@ add_task(async function testInitUninitPr
 
   reporter.init();
   ok(listening, "Reporter listens for errors if the enabled pref is true.");
 
   reporter.uninit();
   ok(!listening, "Reporter does not listen for errors after uninit.");
 });
 
-add_task(async function testInitPastMessages() {
-  const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter({
-    fetch: fetchSpy,
-    registerListener: noop,
-    unregisterListener: noop,
-    now: BrowserErrorReporter.getAppBuildIdDate(),
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, true],
-    [PREF_SAMPLE_RATE, "1.0"],
-  ]});
-
-  resetConsole();
-  Services.console.logMessage(createScriptError({message: "Logged before init"}));
-  reporter.init();
-
-  // Include ok() to satisfy mochitest warning for test without any assertions
-  const errorWasLogged = await TestUtils.waitForCondition(
-    () => fetchPassedError(fetchSpy, "Logged before init"),
-    "Waiting for message to be logged",
-  );
-  ok(errorWasLogged, "Reporter collects errors logged before initialization.");
-
-});
-
 add_task(async function testEnabledPrefWatcher() {
   let listening = false;
   const reporter = new BrowserErrorReporter({
     registerListener() {
       listening = true;
     },
     unregisterListener() {
       listening = false;
@@ -155,386 +68,16 @@ add_task(async function testEnabledPrefW
 
   Services.console.logMessage(createScriptError({message: "Shouldn't report"}));
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_ENABLED, true],
   ]});
   ok(listening, "Reporter collects errors if the enabled pref switches to true.");
 });
 
-add_task(async function testNonErrorLogs() {
-  const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter({
-    fetch: fetchSpy,
-    now: BrowserErrorReporter.getAppBuildIdDate(),
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, true],
-    [PREF_SAMPLE_RATE, "1.0"],
-  ]});
-
-  await reporter.handleMessage({message: "Not a scripterror instance."});
-  ok(
-    !fetchPassedError(fetchSpy, "Not a scripterror instance."),
-    "Reporter does not collect normal log messages or warnings.",
-  );
-
-  await reporter.handleMessage(createScriptError({
-    message: "Warning message",
-    flags: Ci.nsIScriptError.warningFlag,
-  }));
-  ok(
-    !fetchPassedError(fetchSpy, "Warning message"),
-    "Reporter does not collect normal log messages or warnings.",
-  );
-
-  await reporter.handleMessage(createScriptError({
-    message: "Non-chrome category",
-    category: "totally from a website",
-  }));
-  ok(
-    !fetchPassedError(fetchSpy, "Non-chrome category"),
-    "Reporter does not collect normal log messages or warnings.",
-  );
-
-  await reporter.handleMessage(createScriptError({message: "Is error"}));
-  ok(
-    fetchPassedError(fetchSpy, "Is error"),
-    "Reporter collects error messages.",
-  );
-});
-
-add_task(async function testSampling() {
-  const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter({
-    fetch: fetchSpy,
-    now: BrowserErrorReporter.getAppBuildIdDate(),
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, true],
-    [PREF_SAMPLE_RATE, "1.0"],
-  ]});
-
-  await reporter.handleMessage(createScriptError({message: "Should log"}));
-  ok(
-    fetchPassedError(fetchSpy, "Should log"),
-    "A 1.0 sample rate will cause the reporter to always collect errors.",
-  );
-
-  await reporter.handleMessage(createScriptError({message: "undefined", sourceName: undefined}));
-  ok(
-    fetchPassedError(fetchSpy, "undefined"),
-    "A missing sourceName doesn't break reporting.",
-  );
-
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_SAMPLE_RATE, "0.0"],
-  ]});
-  await reporter.handleMessage(createScriptError({message: "Shouldn't log"}));
-  ok(
-    !fetchPassedError(fetchSpy, "Shouldn't log"),
-    "A 0.0 sample rate will cause the reporter to never collect errors.",
-  );
-
-  await reporter.handleMessage(createScriptError({
-    message: "chromedevtools",
-    sourceName: "chrome://devtools/Foo.jsm",
-  }));
-  ok(
-    fetchPassedError(fetchSpy, "chromedevtools"),
-    "chrome://devtools/ paths are sampled at 100% even if the default rate is 0.0.",
-  );
-
-  await reporter.handleMessage(createScriptError({
-    message: "resourcedevtools",
-    sourceName: "resource://devtools/Foo.jsm",
-  }));
-  ok(
-    fetchPassedError(fetchSpy, "resourcedevtools"),
-    "resource://devtools/ paths are sampled at 100% even if the default rate is 0.0.",
-  );
-
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_SAMPLE_RATE, ")fasdf"],
-  ]});
-  await reporter.handleMessage(createScriptError({message: "Also shouldn't log"}));
-  ok(
-    !fetchPassedError(fetchSpy, "Also shouldn't log"),
-    "An invalid sample rate will cause the reporter to never collect errors.",
-  );
-});
-
-add_task(async function testNameMessage() {
-  const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter({
-    fetch: fetchSpy,
-    now: BrowserErrorReporter.getAppBuildIdDate(),
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, true],
-    [PREF_SAMPLE_RATE, "1.0"],
-  ]});
-
-  await reporter.handleMessage(createScriptError({message: "No name"}));
-  let call = fetchCallForMessage(fetchSpy, "No name");
-  let body = JSON.parse(call.args[1].body);
-  is(
-    body.exception.values[0].type,
-    "Error",
-    "Reporter uses a generic type when no name is in the message.",
-  );
-  is(
-    body.exception.values[0].value,
-    "No name",
-    "Reporter uses error message as the exception value.",
-  );
-
-  await reporter.handleMessage(createScriptError({message: "FooError: Has name"}));
-  call = fetchCallForMessage(fetchSpy, "Has name");
-  body = JSON.parse(call.args[1].body);
-  is(
-    body.exception.values[0].type,
-    "FooError",
-    "Reporter uses the error type from the message.",
-  );
-  is(
-    body.exception.values[0].value,
-    "Has name",
-    "Reporter uses error message as the value parameter.",
-  );
-
-  await reporter.handleMessage(createScriptError({message: "FooError: Has :extra: colons"}));
-  call = fetchCallForMessage(fetchSpy, "Has :extra: colons");
-  body = JSON.parse(call.args[1].body);
-  is(
-    body.exception.values[0].type,
-    "FooError",
-    "Reporter uses the error type from the message.",
-  );
-  is(
-    body.exception.values[0].value,
-    "Has :extra: colons",
-    "Reporter uses error message as the value parameter.",
-  );
-});
-
-add_task(async function testRecentBuild() {
-  // Create date that is guaranteed to be a month newer than the build date.
-  const nowDate = BrowserErrorReporter.getAppBuildIdDate();
-  nowDate.setMonth(nowDate.getMonth() + 1);
-
-  const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter({
-    fetch: fetchSpy,
-    now: nowDate,
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, true],
-    [PREF_SAMPLE_RATE, "1.0"],
-  ]});
-
-  await reporter.handleMessage(createScriptError({message: "Is error"}));
-  ok(
-    !fetchPassedError(fetchSpy, "Is error"),
-    "Reporter does not collect errors from builds older than a week.",
-  );
-});
-
-add_task(async function testFetchArguments() {
-  const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter({
-    fetch: fetchSpy,
-    now: BrowserErrorReporter.getAppBuildIdDate(),
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, true],
-    [PREF_SAMPLE_RATE, "1.0"],
-    [PREF_PROJECT_ID, "123"],
-    [PREF_PUBLIC_KEY, "foobar"],
-    [PREF_SUBMIT_URL, "https://errors.example.com/api/123/store/"],
-  ]});
-
-  resetConsole();
-  reporter.init();
-  const testPageUrl = (
-    "chrome://mochitests/content/browser/browser/modules/test/browser/" +
-    "browser_BrowserErrorReporter.html"
-  );
-
-  SimpleTest.expectUncaughtException();
-  await BrowserTestUtils.withNewTab(testPageUrl, async () => {
-    const call = await TestUtils.waitForCondition(
-      () => fetchCallForMessage(fetchSpy, "testFetchArguments error"),
-      "Wait for error from browser_BrowserErrorReporter.html to be logged",
-    );
-    const body = JSON.parse(call.args[1].body);
-    const url = new URL(call.args[0]);
-
-    is(url.origin, "https://errors.example.com", "Reporter builds API url from DSN pref.");
-    is(url.pathname, "/api/123/store/", "Reporter builds API url from DSN pref.");
-    is(
-      url.searchParams.get("sentry_client"),
-      "firefox-error-reporter/1.0.0",
-      "Reporter identifies itself in the outgoing request",
-    );
-    is(url.searchParams.get("sentry_version"), "7", "Reporter is compatible with Sentry 7.");
-    is(url.searchParams.get("sentry_key"), "foobar", "Reporter pulls API key from DSN pref.");
-    is(body.project, "123", "Reporter pulls project ID from DSN pref.");
-    is(
-      body.tags.changeset,
-      AppConstants.SOURCE_REVISION_URL,
-      "Reporter pulls changeset tag from AppConstants",
-    );
-    is(call.args[1].referrer, "https://fake.mozilla.org", "Reporter uses a fake referer.");
-
-    const response = await fetch(testPageUrl);
-    const pageText = await response.text();
-    const pageLines = pageText.split("\n");
-    Assert.deepEqual(
-      body.exception,
-      {
-        values: [
-          {
-            type: "Error",
-            value: "testFetchArguments error",
-            module: testPageUrl,
-            stacktrace: {
-              frames: [
-                {
-                  function: null,
-                  module: testPageUrl,
-                  lineno: 17,
-                  colno: 7,
-                  pre_context: pageLines.slice(11, 16),
-                  context_line: pageLines[16],
-                  post_context: pageLines.slice(17, 22),
-                },
-                {
-                  function: "madeToFail",
-                  module: testPageUrl,
-                  lineno: 12,
-                  colno: 9,
-                  pre_context: pageLines.slice(6, 11),
-                  context_line: pageLines[11],
-                  post_context: pageLines.slice(12, 17),
-                },
-                {
-                  function: "madeToFail2",
-                  module: testPageUrl,
-                  lineno: 15,
-                  colno: 15,
-                  pre_context: pageLines.slice(9, 14),
-                  context_line: pageLines[14],
-                  post_context: pageLines.slice(15, 20),
-                },
-              ],
-            },
-          },
-        ],
-      },
-      "Reporter builds stack trace from scriptError correctly.",
-    );
-  });
-
-  reporter.uninit();
-});
-
-add_task(async function testAddonIDMangle() {
-  const fetchSpy = sinon.spy();
-  // Passing false here disables category checks on errors, which would
-  // otherwise block errors directly from extensions.
-  const reporter = new BrowserErrorReporter({
-    fetch: fetchSpy,
-    chromeOnly: false,
-    now: BrowserErrorReporter.getAppBuildIdDate(),
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, true],
-    [PREF_SAMPLE_RATE, "1.0"],
-  ]});
-  resetConsole();
-  reporter.init();
-
-  // Create and install test add-on
-  const id = "browsererrorcollection@example.com";
-  const extension = ExtensionTestUtils.loadExtension({
-    manifest: {
-      applications: {
-        gecko: { id },
-      },
-    },
-    background() {
-      throw new Error("testAddonIDMangle error");
-    },
-  });
-  await extension.startup();
-
-  // Just in case the error hasn't been thrown before add-on startup.
-  const call = await TestUtils.waitForCondition(
-    () => fetchCallForMessage(fetchSpy, "testAddonIDMangle error"),
-    `Wait for error from ${id} to be logged`,
-  );
-  const body = JSON.parse(call.args[1].body);
-  const stackFrame = body.exception.values[0].stacktrace.frames[0];
-  ok(
-    stackFrame.module.startsWith(`moz-extension://${id}/`),
-    "Stack frame filenames use the proper add-on ID instead of internal UUIDs.",
-  );
-
-  await extension.unload();
-  reporter.uninit();
-});
-
-add_task(async function testExtensionTag() {
-  const fetchSpy = sinon.spy();
-  // Passing false here disables category checks on errors, which would
-  // otherwise block errors directly from extensions.
-  const reporter = new BrowserErrorReporter({
-    fetch: fetchSpy,
-    chromeOnly: false,
-    now: BrowserErrorReporter.getAppBuildIdDate(),
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, true],
-    [PREF_SAMPLE_RATE, "1.0"],
-  ]});
-  resetConsole();
-  reporter.init();
-
-  // Create and install test add-on
-  const id = "browsererrorcollection@example.com";
-  const extension = ExtensionTestUtils.loadExtension({
-    manifest: {
-      applications: {
-        gecko: { id },
-      },
-    },
-    background() {
-      throw new Error("testExtensionTag error");
-    },
-  });
-  await extension.startup();
-
-  // Just in case the error hasn't been thrown before add-on startup.
-  let call = await TestUtils.waitForCondition(
-    () => fetchCallForMessage(fetchSpy, "testExtensionTag error"),
-    `Wait for error from ${id} to be logged`,
-  );
-  let body = JSON.parse(call.args[1].body);
-  ok(body.tags.isExtensionError, "Errors from extensions have an isExtensionError=true tag.");
-
-  await extension.unload();
-  reporter.uninit();
-
-  await reporter.handleMessage(createScriptError({message: "testExtensionTag not from extension"}));
-  call = fetchCallForMessage(fetchSpy, "testExtensionTag not from extension");
-  body = JSON.parse(call.args[1].body);
-  is(body.tags.isExtensionError, false, "Normal errors have an isExtensionError=false tag.");
-});
-
 add_task(async function testScalars() {
   const fetchStub = sinon.stub();
   const reporter = new BrowserErrorReporter({
     fetch: fetchStub,
     now: BrowserErrorReporter.getAppBuildIdDate(),
   });
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_ENABLED, true],
@@ -562,46 +105,24 @@ add_task(async function testScalars() {
     ),
   ];
 
   // Use observe to avoid errors from other code messing up our counts.
   for (const message of messages) {
     await reporter.handleMessage(message);
   }
 
-  await SpecialPowers.pushPrefEnv({set: [[PREF_SAMPLE_RATE, "0.0"]]});
-  await reporter.handleMessage(createScriptError({message: "Additionally no name"}));
-
-  await SpecialPowers.pushPrefEnv({set: [[PREF_SAMPLE_RATE, "1.0"]]});
-  fetchStub.rejects(new Error("Could not report"));
-  await reporter.handleMessage(createScriptError({message: "Maybe name?"}));
-
   const optin = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN;
   const scalars = Services.telemetry.snapshotScalars(optin, false).parent;
   is(
     scalars[TELEMETRY_ERROR_COLLECTED],
-    9,
+    7,
     `${TELEMETRY_ERROR_COLLECTED} is incremented when an error is collected.`,
   );
   is(
-    scalars[TELEMETRY_ERROR_SAMPLE_RATE],
-    "1.0",
-    `${TELEMETRY_ERROR_SAMPLE_RATE} contains the last sample rate used.`,
-  );
-  is(
-    scalars[TELEMETRY_ERROR_REPORTED],
-    7,
-    `${TELEMETRY_ERROR_REPORTED} is incremented when an error is reported.`,
-  );
-  is(
-    scalars[TELEMETRY_ERROR_REPORTED_FAIL],
-    1,
-    `${TELEMETRY_ERROR_REPORTED_FAIL} is incremented when an error fails to be reported.`,
-  );
-  is(
     scalars[TELEMETRY_ERROR_COLLECTED_STACK],
     1,
     `${TELEMETRY_ERROR_REPORTED_FAIL} is incremented when an error with a stack trace is collected.`,
   );
 
   const keyedScalars = Services.telemetry.snapshotKeyedScalars(optin, false).parent;
   Assert.deepEqual(
     keyedScalars[TELEMETRY_ERROR_COLLECTED_FILENAME],
copy from browser/modules/test/browser/browser_BrowserErrorReporter.js
copy to browser/modules/test/browser/browser_BrowserErrorReporter_nightly.js
--- a/browser/modules/test/browser/browser_BrowserErrorReporter.js
+++ b/browser/modules/test/browser/browser_BrowserErrorReporter_nightly.js
@@ -1,118 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
+// This file contains BrowserErrorReporter tests that depend on errors
+// being collected, which is only enabled on Nightly builds.
+
 ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this);
 ChromeUtils.import("resource:///modules/BrowserErrorReporter.jsm", this);
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm", this);
 
 /* global sinon */
-Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
-registerCleanupFunction(function() {
-  delete window.sinon;
-});
-
-const PREF_ENABLED = "browser.chrome.errorReporter.enabled";
-const PREF_PROJECT_ID = "browser.chrome.errorReporter.projectId";
-const PREF_PUBLIC_KEY = "browser.chrome.errorReporter.publicKey";
-const PREF_SAMPLE_RATE = "browser.chrome.errorReporter.sampleRate";
-const PREF_SUBMIT_URL = "browser.chrome.errorReporter.submitUrl";
-const TELEMETRY_ERROR_COLLECTED = "browser.errors.collected_count";
-const TELEMETRY_ERROR_COLLECTED_FILENAME = "browser.errors.collected_count_by_filename";
-const TELEMETRY_ERROR_COLLECTED_STACK = "browser.errors.collected_with_stack_count";
-const TELEMETRY_ERROR_REPORTED = "browser.errors.reported_success_count";
-const TELEMETRY_ERROR_REPORTED_FAIL = "browser.errors.reported_failure_count";
-const TELEMETRY_ERROR_SAMPLE_RATE = "browser.errors.sample_rate";
-
-function createScriptError(options = {}) {
-  const scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
-  scriptError.init(
-    options.message || "",
-    "sourceName" in options ? options.sourceName : null,
-    options.sourceLine || null,
-    options.lineNumber || null,
-    options.columnNumber || null,
-    options.flags || Ci.nsIScriptError.errorFlag,
-    options.category || "chrome javascript",
-  );
-  return scriptError;
-}
-
-function noop() {
-  // Do nothing
-}
-
-// Clears the console of any previous messages. Should be called at the end of
-// each test that logs to the console.
-function resetConsole() {
-  Services.console.logStringMessage("");
-  Services.console.reset();
-}
-
-// Finds the fetch spy call for an error with a matching message.
-function fetchCallForMessage(fetchSpy, message) {
-  for (const call of fetchSpy.getCalls()) {
-    const body = JSON.parse(call.args[1].body);
-    if (body.exception.values[0].value.includes(message)) {
-      return call;
-    }
-  }
-
-  return null;
-}
-
-// Helper to test if a fetch spy was called with the given error message.
-// Used in tests where unrelated JS errors from other code are logged.
-function fetchPassedError(fetchSpy, message) {
-  return fetchCallForMessage(fetchSpy, message) !== null;
-}
-
-add_task(async function testSetup() {
-  const canRecordExtended = Services.telemetry.canRecordExtended;
-  Services.telemetry.canRecordExtended = true;
-  registerCleanupFunction(() => Services.telemetry.canRecordExtended = canRecordExtended);
-});
-
-add_task(async function testInitPrefDisabled() {
-  let listening = false;
-  const reporter = new BrowserErrorReporter({
-    registerListener() {
-      listening = true;
-    },
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, false],
-  ]});
-
-  reporter.init();
-  ok(!listening, "Reporter does not listen for errors if the enabled pref is false.");
-});
-
-add_task(async function testInitUninitPrefEnabled() {
-  let listening = false;
-  const reporter = new BrowserErrorReporter({
-    registerListener() {
-      listening = true;
-    },
-    unregisterListener() {
-      listening = false;
-    },
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, true],
-  ]});
-
-  reporter.init();
-  ok(listening, "Reporter listens for errors if the enabled pref is true.");
-
-  reporter.uninit();
-  ok(!listening, "Reporter does not listen for errors after uninit.");
-});
+Services.scriptloader.loadSubScript(new URL("head_BrowserErrorReporter.js", gTestPath).href, this);
 
 add_task(async function testInitPastMessages() {
   const fetchSpy = sinon.spy();
   const reporter = new BrowserErrorReporter({
     fetch: fetchSpy,
     registerListener: noop,
     unregisterListener: noop,
     now: BrowserErrorReporter.getAppBuildIdDate(),
@@ -130,41 +33,16 @@ add_task(async function testInitPastMess
   const errorWasLogged = await TestUtils.waitForCondition(
     () => fetchPassedError(fetchSpy, "Logged before init"),
     "Waiting for message to be logged",
   );
   ok(errorWasLogged, "Reporter collects errors logged before initialization.");
 
 });
 
-add_task(async function testEnabledPrefWatcher() {
-  let listening = false;
-  const reporter = new BrowserErrorReporter({
-    registerListener() {
-      listening = true;
-    },
-    unregisterListener() {
-      listening = false;
-    },
-    now: BrowserErrorReporter.getAppBuildIdDate(),
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, false],
-  ]});
-
-  reporter.init();
-  ok(!listening, "Reporter does not collect errors if the enable pref is false.");
-
-  Services.console.logMessage(createScriptError({message: "Shouldn't report"}));
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, true],
-  ]});
-  ok(listening, "Reporter collects errors if the enabled pref switches to true.");
-});
-
 add_task(async function testNonErrorLogs() {
   const fetchSpy = sinon.spy();
   const reporter = new BrowserErrorReporter({
     fetch: fetchSpy,
     now: BrowserErrorReporter.getAppBuildIdDate(),
   });
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_ENABLED, true],
@@ -538,137 +416,45 @@ add_task(async function testScalars() {
   });
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_ENABLED, true],
     [PREF_SAMPLE_RATE, "1.0"],
   ]});
 
   Services.telemetry.clearScalars();
 
-  const messages = [
-    createScriptError({message: "No name"}),
-    createScriptError({message: "Also no name", sourceName: "resource://gre/modules/Foo.jsm"}),
-    createScriptError({message: "More no name", sourceName: "resource://gre/modules/Bar.jsm"}),
-    createScriptError({message: "Yeah sures", sourceName: "unsafe://gre/modules/Bar.jsm"}),
-    createScriptError({message: "Addon", sourceName: "moz-extension://foo/Bar.jsm"}),
-    createScriptError({
-      message: "long",
-      sourceName: "resource://gre/modules/long/long/long/long/long/long/long/long/long/long/",
-    }),
-    {message: "Not a scripterror instance."},
+  // Basic count
+  await reporter.handleMessage(createScriptError({message: "No name"}));
 
-    // No easy way to create an nsIScriptError with a stack, so let's pretend.
-    Object.create(
-      createScriptError({message: "Whatever"}),
-      {stack: {value: new Error().stack}},
-    ),
-  ];
-
-  // Use observe to avoid errors from other code messing up our counts.
-  for (const message of messages) {
-    await reporter.handleMessage(message);
-  }
-
+  // Sample rate affects counts
   await SpecialPowers.pushPrefEnv({set: [[PREF_SAMPLE_RATE, "0.0"]]});
   await reporter.handleMessage(createScriptError({message: "Additionally no name"}));
 
+  // Failed fetches should be counted too
   await SpecialPowers.pushPrefEnv({set: [[PREF_SAMPLE_RATE, "1.0"]]});
   fetchStub.rejects(new Error("Could not report"));
   await reporter.handleMessage(createScriptError({message: "Maybe name?"}));
 
   const optin = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN;
   const scalars = Services.telemetry.snapshotScalars(optin, false).parent;
   is(
     scalars[TELEMETRY_ERROR_COLLECTED],
-    9,
+    3,
     `${TELEMETRY_ERROR_COLLECTED} is incremented when an error is collected.`,
   );
   is(
     scalars[TELEMETRY_ERROR_SAMPLE_RATE],
     "1.0",
     `${TELEMETRY_ERROR_SAMPLE_RATE} contains the last sample rate used.`,
   );
   is(
     scalars[TELEMETRY_ERROR_REPORTED],
-    7,
+    1,
     `${TELEMETRY_ERROR_REPORTED} is incremented when an error is reported.`,
   );
   is(
     scalars[TELEMETRY_ERROR_REPORTED_FAIL],
     1,
     `${TELEMETRY_ERROR_REPORTED_FAIL} is incremented when an error fails to be reported.`,
   );
-  is(
-    scalars[TELEMETRY_ERROR_COLLECTED_STACK],
-    1,
-    `${TELEMETRY_ERROR_REPORTED_FAIL} is incremented when an error with a stack trace is collected.`,
-  );
-
-  const keyedScalars = Services.telemetry.snapshotKeyedScalars(optin, false).parent;
-  Assert.deepEqual(
-    keyedScalars[TELEMETRY_ERROR_COLLECTED_FILENAME],
-    {
-      "FILTERED": 1,
-      "MOZEXTENSION": 1,
-      "resource://gre/modules/Foo.jsm": 1,
-      "resource://gre/modules/Bar.jsm": 1,
-      // Cut off at 70-character limit
-      "resource://gre/modules/long/long/long/long/long/long/long/long/long/l": 1,
-    },
-    `${TELEMETRY_ERROR_COLLECTED_FILENAME} is incremented when an error is collected.`,
-  );
 
   resetConsole();
 });
-
-add_task(async function testCollectedFilenameScalar() {
-  const fetchStub = sinon.stub();
-  const reporter = new BrowserErrorReporter({
-    fetch: fetchStub,
-    now: BrowserErrorReporter.getAppBuildIdDate(),
-  });
-  await SpecialPowers.pushPrefEnv({set: [
-    [PREF_ENABLED, true],
-    [PREF_SAMPLE_RATE, "1.0"],
-  ]});
-
-  const testCases = [
-    ["chrome://unknown/module.jsm", false],
-    ["resource://unknown/module.jsm", false],
-    ["unknown://unknown/module.jsm", false],
-
-    ["resource://gre/modules/Foo.jsm", true],
-    ["resource:///modules/Foo.jsm", true],
-    ["chrome://global/Foo.jsm", true],
-    ["chrome://browser/Foo.jsm", true],
-    ["chrome://devtools/Foo.jsm", true],
-  ];
-
-  for (const [filename, shouldMatch] of testCases) {
-    Services.telemetry.clearScalars();
-
-    // Use observe to avoid errors from other code messing up our counts.
-    await reporter.handleMessage(createScriptError({
-      message: "Fine",
-      sourceName: filename,
-    }));
-
-    const keyedScalars = (
-      Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false).parent
-    );
-
-    let matched = null;
-    if (shouldMatch) {
-      matched = keyedScalars[TELEMETRY_ERROR_COLLECTED_FILENAME][filename] === 1;
-    } else {
-      matched = keyedScalars[TELEMETRY_ERROR_COLLECTED_FILENAME].FILTERED === 1;
-    }
-
-    ok(
-      matched,
-      shouldMatch
-        ? `${TELEMETRY_ERROR_COLLECTED_FILENAME} logs a key for ${filename}.`
-        : `${TELEMETRY_ERROR_COLLECTED_FILENAME} logs a FILTERED key for ${filename}.`,
-    );
-  }
-
-  resetConsole();
-});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser/head_BrowserErrorReporter.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* exported sinon */
+Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
+registerCleanupFunction(function() {
+  delete window.sinon;
+});
+
+const PREF_ENABLED = "browser.chrome.errorReporter.enabled";
+const PREF_PROJECT_ID = "browser.chrome.errorReporter.projectId";
+const PREF_PUBLIC_KEY = "browser.chrome.errorReporter.publicKey";
+const PREF_SAMPLE_RATE = "browser.chrome.errorReporter.sampleRate";
+const PREF_SUBMIT_URL = "browser.chrome.errorReporter.submitUrl";
+const TELEMETRY_ERROR_COLLECTED = "browser.errors.collected_count";
+const TELEMETRY_ERROR_COLLECTED_FILENAME = "browser.errors.collected_count_by_filename";
+const TELEMETRY_ERROR_COLLECTED_STACK = "browser.errors.collected_with_stack_count";
+const TELEMETRY_ERROR_REPORTED = "browser.errors.reported_success_count";
+const TELEMETRY_ERROR_REPORTED_FAIL = "browser.errors.reported_failure_count";
+const TELEMETRY_ERROR_SAMPLE_RATE = "browser.errors.sample_rate";
+
+add_task(async function testSetup() {
+  const canRecordExtended = Services.telemetry.canRecordExtended;
+  Services.telemetry.canRecordExtended = true;
+  registerCleanupFunction(() => Services.telemetry.canRecordExtended = canRecordExtended);
+});
+
+function createScriptError(options = {}) {
+  const scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
+  scriptError.init(
+    options.message || "",
+    "sourceName" in options ? options.sourceName : null,
+    options.sourceLine || null,
+    options.lineNumber || null,
+    options.columnNumber || null,
+    options.flags || Ci.nsIScriptError.errorFlag,
+    options.category || "chrome javascript",
+  );
+  return scriptError;
+}
+
+function noop() {
+  // Do nothing
+}
+
+// Clears the console of any previous messages. Should be called at the end of
+// each test that logs to the console.
+function resetConsole() {
+  Services.console.logStringMessage("");
+  Services.console.reset();
+}
+
+// Finds the fetch spy call for an error with a matching message.
+function fetchCallForMessage(fetchSpy, message) {
+  for (const call of fetchSpy.getCalls()) {
+    const body = JSON.parse(call.args[1].body);
+    if (body.exception.values[0].value.includes(message)) {
+      return call;
+    }
+  }
+
+  return null;
+}
+
+// Helper to test if a fetch spy was called with the given error message.
+// Used in tests where unrelated JS errors from other code are logged.
+function fetchPassedError(fetchSpy, message) {
+  return fetchCallForMessage(fetchSpy, message) !== null;
+}