Bug 1445009: Refactor BrowserErrorReporter.jsm and tests. draft
authorMichael Kelly <mkelly@mozilla.com>
Mon, 19 Mar 2018 12:41:30 -0700
changeset 770140 a5af344c86e9756d4dbef761e4a6060515c87a61
parent 769320 4f1014eb5039bdfdd7a39fb7785d102df1994a6f
child 770141 0f88d1072dbb66334a6790bc9c62e8ce4a57f097
push id103345
push userbmo:mkelly@mozilla.com
push dateTue, 20 Mar 2018 20:38:35 +0000
bugs1445009
milestone61.0a1
Bug 1445009: Refactor BrowserErrorReporter.jsm and tests. The transforms for turning an nsIScriptError into a payload that Sentry understands were getting a bit complex for a single function, so they're refactored into a list of transform functions that are applied in sequence to produce the payload. This will make it easier to manage adding new transforms to the list. Refactoring this revaled a problem with the test code: it assumed that listeners for console messages were notified in order of registration (since it used a temporary listener to determine when the rest of the listeners had been notified of a message). Changing the async evaluation of the code broke the tests, so they had to be refactored as well. Without a way to know when all console listeners have been notified, we can't assert that a message will not be received by BrowserErrorReporter. We do two things to get around this: - Where possible, call `observe` directly on the reporter instance. - Add constructor params for registering and unregistering listeners so we can test that logic without relying on messages being received or not. MozReview-Commit-ID: EEH6IROOuHD
browser/modules/BrowserErrorReporter.jsm
browser/modules/test/browser/browser_BrowserErrorReporter.js
--- a/browser/modules/BrowserErrorReporter.jsm
+++ b/browser/modules/BrowserErrorReporter.jsm
@@ -50,20 +50,26 @@ const REPORTED_CATEGORIES = new Set([
  * The outgoing requests are designed to be compatible with Sentry. See
  * https://docs.sentry.io/clientdev/ for details on the data format that Sentry
  * expects.
  *
  * Errors may contain PII, such as in messages or local file paths in stack
  * traces; see bug 1426482 for privacy review and server-side mitigation.
  */
 class BrowserErrorReporter {
-  constructor(fetchMethod = this._defaultFetch, chromeOnly = true) {
+  constructor(options = {}) {
     // Test arguments for mocks and changing behavior
-    this.fetch = fetchMethod;
-    this.chromeOnly = chromeOnly;
+    this.fetch = options.fetch || defaultFetch;
+    this.chromeOnly = options.chromeOnly !== undefined ? options.chromeOnly : true;
+    this.registerListener = (
+      options.registerListener || (() => Services.console.registerListener(this))
+    );
+    this.unregisterListener = (
+      options.unregisterListener || (() => Services.console.unregisterListener(this))
+    );
 
     // Values that don't change between error reports.
     this.requestBodyTemplate = {
       logger: "javascript",
       platform: "javascript",
       release: Services.appinfo.version,
       environment: UpdateUtils.getUpdateChannel(false),
       tags: {
@@ -94,38 +100,38 @@ class BrowserErrorReporter {
     logger.manageLevelFromPref(PREF_LOG_LEVEL);
 
     Object.defineProperty(this, "logger", {value: logger});
     return this.logger;
   }
 
   init() {
     if (this.collectionEnabled) {
-      Services.console.registerListener(this);
+      this.registerListener();
 
       // Processing already-logged messages in case any errors occurred before
       // startup.
       for (const message of Services.console.getMessageArray()) {
         this.observe(message);
       }
     }
   }
 
   uninit() {
     try {
-      Services.console.unregisterListener(this);
+      this.unregisterListener();
     } catch (err) {} // It probably wasn't registered.
   }
 
   handleEnabledPrefChanged(prefName, previousValue, newValue) {
     if (newValue) {
-      Services.console.registerListener(this);
+      this.registerListener();
     } else {
       try {
-        Services.console.unregisterListener(this);
+        this.unregisterListener();
       } catch (err) {} // It probably wasn't registered.
     }
   }
 
   async observe(message) {
     try {
       message.QueryInterface(Ci.nsIScriptError);
     } catch (err) {
@@ -139,70 +145,35 @@ class BrowserErrorReporter {
     }
 
     // Sample the amount of errors we send out
     const sampleRate = Number.parseFloat(Services.prefs.getCharPref(PREF_SAMPLE_RATE));
     if (!Number.isFinite(sampleRate) || (Math.random() >= sampleRate)) {
       return;
     }
 
-    const extensions = new Map();
-    for (let extension of WebExtensionPolicy.getActiveExtensions()) {
-      extensions.set(extension.mozExtensionHostname, extension);
-    }
-
-    // Replaces any instances of moz-extension:// URLs with internal UUIDs to use
-    // the add-on ID instead.
-    function mangleExtURL(string, anchored = true) {
-      let re = new RegExp(`${anchored ? "^" : ""}moz-extension://([^/]+)/`, "g");
-
-      return string.replace(re, (m0, m1) => {
-        let id = extensions.has(m1) ? extensions.get(m1).id : m1;
-        return `moz-extension://${id}/`;
-      });
+    const exceptionValue = {};
+    const transforms = [
+      addErrorMessage,
+      addStacktrace,
+      addModule,
+      mangleExtensionUrls,
+    ];
+    for (const transform of transforms) {
+      await transform(message, exceptionValue);
     }
 
-    // Parse the error type from the message if present (e.g. "TypeError: Whoops").
-    let errorMessage = message.errorMessage;
-    let errorName = "Error";
-    if (message.errorMessage.match(ERROR_PREFIX_RE)) {
-      const parts = message.errorMessage.split(":");
-      errorName = parts[0];
-      errorMessage = parts.slice(1).join(":").trim();
-    }
-
-    const frames = [];
-    let frame = message.stack;
-    // Avoid an infinite loop by limiting traces to 100 frames.
-    while (frame && frames.length < 100) {
-      const normalizedFrame = await this.normalizeStackFrame(frame);
-      normalizedFrame.module = mangleExtURL(normalizedFrame.module, false);
-      frames.push(normalizedFrame);
-      frame = frame.parent;
-    }
-    // Frames are sent in order from oldest to newest.
-    frames.reverse();
-
-    const requestBody = Object.assign({}, this.requestBodyTemplate, {
+    const requestBody = {
+      ...this.requestBodyTemplate,
       timestamp: new Date().toISOString().slice(0, -1), // Remove trailing "Z"
       project: Services.prefs.getCharPref(PREF_PROJECT_ID),
       exception: {
-        values: [
-          {
-            type: errorName,
-            value: mangleExtURL(errorMessage),
-            module: message.sourceName,
-            stacktrace: {
-              frames,
-            }
-          },
-        ],
+        values: [exceptionValue],
       },
-      culprit: message.sourceName,
-    });
+    };
 
     const url = new URL(Services.prefs.getCharPref(PREF_SUBMIT_URL));
     url.searchParams.set("sentry_client", `${SDK_NAME}/${SDK_VERSION}`);
     url.searchParams.set("sentry_version", "7");
     url.searchParams.set("sentry_key", Services.prefs.getCharPref(PREF_PUBLIC_KEY));
 
     try {
       await this.fetch(url, {
@@ -215,18 +186,46 @@ class BrowserErrorReporter {
         referrer: "https://fake.mozilla.org",
         body: JSON.stringify(requestBody)
       });
       this.logger.debug("Sent error successfully.");
     } catch (error) {
       this.logger.warn(`Failed to send error: ${error}`);
     }
   }
+}
 
-  async normalizeStackFrame(frame) {
+function defaultFetch(...args) {
+  // Do not make network requests while running in automation
+  if (Cu.isInAutomation) {
+    return null;
+  }
+
+  return fetch(...args);
+}
+
+function addErrorMessage(message, exceptionValue) {
+  // Parse the error type from the message if present (e.g. "TypeError: Whoops").
+  let errorMessage = message.errorMessage;
+  let errorName = "Error";
+  if (message.errorMessage.match(ERROR_PREFIX_RE)) {
+    const parts = message.errorMessage.split(":");
+    errorName = parts[0];
+    errorMessage = parts.slice(1).join(":").trim();
+  }
+
+  exceptionValue.type = errorName;
+  exceptionValue.value = errorMessage;
+}
+
+async function addStacktrace(message, exceptionValue) {
+  const frames = [];
+  let frame = message.stack;
+  // Avoid an infinite loop by limiting traces to 100 frames.
+  while (frame && frames.length < 100) {
     const normalizedFrame = {
       function: frame.functionDisplayName,
       module: frame.source,
       lineno: frame.line,
       colno: frame.column,
     };
 
     try {
@@ -251,20 +250,48 @@ class BrowserErrorReporter {
         lineIndex + 1,
         Math.min(lineIndex + 1 + CONTEXT_LINES, sourceLines.length),
       );
     } catch (err) {
       // Could be a fetch issue, could be a line index issue. Not much we can
       // do to recover in either case.
     }
 
-    return normalizedFrame;
+    frames.push(normalizedFrame);
+    frame = frame.parent;
+  }
+  // Frames are sent in order from oldest to newest.
+  frames.reverse();
+
+  exceptionValue.stacktrace = {frames};
+}
+
+function addModule(message, exceptionValue) {
+  exceptionValue.module = message.sourceName;
+}
+
+function mangleExtensionUrls(message, exceptionValue) {
+  const extensions = new Map();
+  for (let extension of WebExtensionPolicy.getActiveExtensions()) {
+    extensions.set(extension.mozExtensionHostname, extension);
   }
 
-  async _defaultFetch(...args) {
-    // Do not make network requests while running in automation
-    if (Cu.isInAutomation) {
-      return null;
+  // Replaces any instances of moz-extension:// URLs with internal UUIDs to use
+  // the add-on ID instead.
+  function mangleExtURL(string, anchored = true) {
+    if (!string) {
+      return string;
     }
 
-    return fetch(...args);
+    let re = new RegExp(`${anchored ? "^" : ""}moz-extension://([^/]+)/`, "g");
+
+    return string.replace(re, (m0, m1) => {
+      let id = extensions.has(m1) ? extensions.get(m1).id : m1;
+      return `moz-extension://${id}/`;
+    });
+  }
+
+  exceptionValue.value = mangleExtURL(exceptionValue.value, false);
+  exceptionValue.module = mangleExtURL(exceptionValue.module);
+  for (const frame of exceptionValue.stacktrace.frames) {
+    frame.module = mangleExtURL(frame.module);
   }
 }
--- a/browser/modules/test/browser/browser_BrowserErrorReporter.js
+++ b/browser/modules/test/browser/browser_BrowserErrorReporter.js
@@ -11,69 +11,48 @@ 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 || "",
     options.sourceName || null,
     options.sourceLine || null,
     options.lineNumber || null,
     options.columnNumber || null,
     options.flags || Ci.nsIScriptError.errorFlag,
     options.category || "chrome javascript",
   );
   return scriptError;
 }
 
-// Wrapper around Services.console.logMessage that waits for the message to be
-// logged before resolving, since messages are logged asynchronously.
-function logMessage(message) {
-  return new Promise(resolve => {
-    Services.console.registerListener({
-      observe(loggedMessage) {
-        if (loggedMessage.message === message.message) {
-          Services.console.unregisterListener(this);
-          resolve();
-        }
-      },
-    });
-    Services.console.logMessage(message);
-  });
+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();
 }
 
-// Wrapper similar to logMessage, but for logStringMessage.
-function logStringMessage(message) {
-  return new Promise(resolve => {
-    Services.console.registerListener({
-      observe(loggedMessage) {
-        if (loggedMessage.message === message) {
-          Services.console.unregisterListener(this);
-          resolve();
-        }
-      },
-    });
-    Services.console.logStringMessage(message);
-  });
-}
-
 // 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;
     }
   }
@@ -83,256 +62,235 @@ function fetchCallForMessage(fetchSpy, m
 
 // 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 testInitPrefDisabled() {
-  const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter(fetchSpy);
+  let listening = false;
+  const reporter = new BrowserErrorReporter({
+    registerListener() {
+      listening = true;
+    },
+  });
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_ENABLED, false],
-    [PREF_SAMPLE_RATE, "1.0"],
   ]});
 
   reporter.init();
-  await logMessage(createScriptError({message: "Logged while disabled"}));
-  ok(
-    !fetchPassedError(fetchSpy, "Logged while disabled"),
-    "Reporter does not listen for errors if the enabled pref is false.",
-  );
-  reporter.uninit();
-  resetConsole();
+  ok(!listening, "Reporter does not listen for errors if the enabled pref is false.");
 });
 
 add_task(async function testInitUninitPrefEnabled() {
-  const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter(fetchSpy);
+  let listening = false;
+  const reporter = new BrowserErrorReporter({
+    registerListener() {
+      listening = true;
+    },
+    unregisterListener() {
+      listening = false;
+    },
+  });
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_ENABLED, true],
-    [PREF_SAMPLE_RATE, "1.0"],
   ]});
 
   reporter.init();
-  await logMessage(createScriptError({message: "Logged after init"}));
-  ok(
-    fetchPassedError(fetchSpy, "Logged after init"),
-    "Reporter listens for errors if the enabled pref is true.",
-  );
+  ok(listening, "Reporter listens for errors if the enabled pref is true.");
 
-  fetchSpy.reset();
-  ok(!fetchSpy.called, "Fetch spy was reset.");
   reporter.uninit();
-  await logMessage(createScriptError({message: "Logged after uninit"}));
-  ok(
-    !fetchPassedError(fetchSpy, "Logged after uninit"),
-    "Reporter does not listen for errors after uninit.",
-  );
-
-  resetConsole();
+  ok(!listening, "Reporter does not listen for errors after uninit.");
 });
 
 add_task(async function testInitPastMessages() {
   const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter(fetchSpy);
+  const reporter = new BrowserErrorReporter({
+    fetch: fetchSpy,
+    registerListener: noop,
+    unregisterListener: noop,
+  });
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_ENABLED, true],
     [PREF_SAMPLE_RATE, "1.0"],
   ]});
 
-  await logMessage(createScriptError({message: "Logged before init"}));
+  resetConsole();
+  Services.console.logMessage(createScriptError({message: "Logged before init"}));
   reporter.init();
-  ok(
-    fetchPassedError(fetchSpy, "Logged before init"),
-    "Reporter collects errors logged before initialization.",
+
+  // 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",
   );
-  reporter.uninit();
-  resetConsole();
+  ok(errorWasLogged, "Reporter collects errors logged before initialization.");
+
 });
 
 add_task(async function testEnabledPrefWatcher() {
-  const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter(fetchSpy);
+  let listening = false;
+  const reporter = new BrowserErrorReporter({
+    registerListener() {
+      listening = true;
+    },
+    unregisterListener() {
+      listening = false;
+    },
+  });
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_ENABLED, false],
-    [PREF_SAMPLE_RATE, "1.0"],
   ]});
 
   reporter.init();
-  await logMessage(createScriptError({message: "Shouldn't report"}));
-  ok(
-    !fetchPassedError(fetchSpy, "Shouldn't report"),
-    "Reporter does not collect errors if the enable pref is false.",
-  );
+  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(
-    !fetchPassedError(fetchSpy, "Shouldn't report"),
-    "Reporter does not collect past-logged errors if it is enabled mid-run.",
-  );
-  await logMessage(createScriptError({message: "Should report"}));
-  ok(
-    fetchPassedError(fetchSpy, "Should report"),
-    "Reporter collects errors logged after the enabled pref is turned on mid-run",
-  );
-
-  reporter.uninit();
-  resetConsole();
+  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(fetchSpy);
+  const reporter = new BrowserErrorReporter({fetch: fetchSpy});
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_ENABLED, true],
     [PREF_SAMPLE_RATE, "1.0"],
   ]});
 
-  reporter.init();
-
-  await logStringMessage("Not a scripterror instance.");
+  reporter.observe({message: "Not a scripterror instance."});
   ok(
     !fetchPassedError(fetchSpy, "Not a scripterror instance."),
     "Reporter does not collect normal log messages or warnings.",
   );
 
-  await logMessage(createScriptError({
+  await reporter.observe(createScriptError({
     message: "Warning message",
     flags: Ci.nsIScriptError.warningFlag,
   }));
   ok(
     !fetchPassedError(fetchSpy, "Warning message"),
     "Reporter does not collect normal log messages or warnings.",
   );
 
-  await logMessage(createScriptError({
+  await reporter.observe(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 logMessage(createScriptError({message: "Is error"}));
+  await reporter.observe(createScriptError({message: "Is error"}));
   ok(
     fetchPassedError(fetchSpy, "Is error"),
     "Reporter collects error messages.",
   );
-
-  reporter.uninit();
-  resetConsole();
 });
 
 add_task(async function testSampling() {
   const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter(fetchSpy);
+  const reporter = new BrowserErrorReporter({fetch: fetchSpy});
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_ENABLED, true],
     [PREF_SAMPLE_RATE, "1.0"],
   ]});
 
-  reporter.init();
-  await logMessage(createScriptError({message: "Should log"}));
+  await reporter.observe(createScriptError({message: "Should log"}));
   ok(
     fetchPassedError(fetchSpy, "Should log"),
     "A 1.0 sample rate will cause the reporter to always collect errors.",
   );
 
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_SAMPLE_RATE, "0.0"],
   ]});
-  await logMessage(createScriptError({message: "Shouldn't log"}));
+  await reporter.observe(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 SpecialPowers.pushPrefEnv({set: [
     [PREF_SAMPLE_RATE, ")fasdf"],
   ]});
-  await logMessage(createScriptError({message: "Also shouldn't log"}));
+  await reporter.observe(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.",
   );
-
-  reporter.uninit();
-  resetConsole();
 });
 
 add_task(async function testNameMessage() {
   const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter(fetchSpy);
+  const reporter = new BrowserErrorReporter({fetch: fetchSpy});
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_ENABLED, true],
     [PREF_SAMPLE_RATE, "1.0"],
   ]});
 
-  reporter.init();
-  await logMessage(createScriptError({message: "No name"}));
+  await reporter.observe(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 logMessage(createScriptError({message: "FooError: Has name"}));
+  await reporter.observe(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 logMessage(createScriptError({message: "FooError: Has :extra: colons"}));
+  await reporter.observe(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.",
   );
-  reporter.uninit();
-  resetConsole();
 });
 
 add_task(async function testFetchArguments() {
   const fetchSpy = sinon.spy();
-  const reporter = new BrowserErrorReporter(fetchSpy);
+  const reporter = new BrowserErrorReporter({fetch: fetchSpy});
   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 () => {
@@ -400,28 +358,28 @@ add_task(async function testFetchArgumen
           },
         ],
       },
       "Reporter builds stack trace from scriptError correctly.",
     );
   });
 
   reporter.uninit();
-  resetConsole();
 });
 
 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(fetchSpy, false);
+  const reporter = new BrowserErrorReporter({fetch: fetchSpy, chromeOnly: false});
   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 },
@@ -442,10 +400,9 @@ add_task(async function testAddonIDMangl
   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();
-  resetConsole();
 });