Skip to content

Commit

Permalink
Small error-related improvements (#3616)
Browse files Browse the repository at this point in the history
Co-authored-by: Todd Schiller <todd.schiller@gmail.com>
  • Loading branch information
fregante and twschiller authored Jun 9, 2022
1 parent 4bc15b5 commit 37d8b1b
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 21 deletions.
14 changes: 10 additions & 4 deletions src/development/errorsBadge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,32 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { getErrorMessage } from "@/errors/errorHelpers";
import { uncaughtErrorHandlers } from "@/telemetry/reportUncaughtErrors";

let counter = 0;
let timer: NodeJS.Timeout;

function updateCounter(): void {
function updateBadge(errorMessage: string | null): void {
void chrome.browserAction.setTitle({
title: errorMessage,
});
void chrome.browserAction.setBadgeText({
text: counter ? String(counter) : undefined,
});
void chrome.browserAction.setBadgeBackgroundColor({ color: "#F00" });
}

function backgroundErrorsBadge() {
function backgroundErrorsBadge(_: unknown, error: unknown) {
counter++;
updateCounter();
// Show the last error as tooltip
updateBadge(getErrorMessage(error));

// Reset the counter after a minute of inactivity
clearTimeout(timer);
timer = setTimeout(() => {
counter = 0;
updateCounter();
updateBadge(null); // Resets it
}, 60_000);
}

Expand Down
20 changes: 13 additions & 7 deletions src/errors/errorHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
IGNORED_ERROR_PATTERNS,
isErrorObject,
selectError,
selectErrorFromEvent,
selectErrorFromRejectionEvent,
selectSpecificError,
} from "@/errors/errorHelpers";
import { range } from "lodash";
Expand Down Expand Up @@ -215,14 +217,16 @@ describe("selectError", () => {
'[Error: {"my":"object"}]'
);
});
});

describe("selectErrorFromEvent", () => {
it("extracts error from ErrorEvent", () => {
const error = new Error("This won’t be caught");
const errorEvent = new ErrorEvent("error", {
error,
});

expect(selectError(errorEvent)).toBe(error);
expect(selectErrorFromEvent(errorEvent)).toBe(error);
});

it("handles ErrorEvent with null message and error", () => {
Expand All @@ -231,7 +235,7 @@ describe("selectError", () => {
message: null,
});

const selectedError = selectError(errorEvent);
const selectedError = selectErrorFromEvent(errorEvent);
expect(selectedError).toMatchInlineSnapshot("[Error: Unknown error event]");
});

Expand All @@ -247,7 +251,7 @@ describe("selectError", () => {
message: eventMessage,
});

const selectedError = selectError(errorEvent);
const selectedError = selectErrorFromEvent(errorEvent);

expect(selectedError.message).toBe(eventMessage);

Expand All @@ -266,22 +270,24 @@ describe("selectError", () => {
error,
});

const selectedError = selectError(errorEvent);
const selectedError = selectErrorFromEvent(errorEvent);
expect(selectedError).toMatchInlineSnapshot("[Error: It’s a non-error]");
expect(selectedError.stack).toMatchInlineSnapshot(`
"Error: It’s a non-error
at unknown (yoshi://mushroom-kingdom/bowser.js:2:10)"
`);
});
});

describe("selectErrorFromRejectionEvent", () => {
it("extracts error from PromiseRejectionEvent", () => {
const error = new Error("This won’t be caught");
const errorEvent = new PromiseRejectionEvent(
"error",
createUncaughtRejection(error)
);

expect(selectError(errorEvent)).toBe(error);
expect(selectErrorFromRejectionEvent(errorEvent)).toBe(error);
});

it("handles PromiseRejectionEvent with null reason", () => {
Expand All @@ -290,7 +296,7 @@ describe("selectError", () => {
createUncaughtRejection(null)
);

const selectedError = selectError(errorEvent);
const selectedError = selectErrorFromRejectionEvent(errorEvent);
expect(selectedError).toMatchInlineSnapshot(
"[Error: Unknown promise rejection]"
);
Expand All @@ -302,7 +308,7 @@ describe("selectError", () => {
createUncaughtRejection("It's a non-error")
);

expect(selectError(errorEvent)).toMatchInlineSnapshot(
expect(selectErrorFromRejectionEvent(errorEvent)).toMatchInlineSnapshot(
"[Error: It's a non-error]"
);
});
Expand Down
16 changes: 13 additions & 3 deletions src/errors/errorHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
selectNetworkErrorMessage,
selectServerErrorMessage,
} from "@/errors/networkErrorHelpers";
import { errorTabDoesntExist, errorTargetClosedEarly } from "webext-messenger";

const DEFAULT_ERROR_MESSAGE = "Unknown error";

Expand Down Expand Up @@ -57,6 +58,8 @@ export const IGNORED_ERROR_PATTERNS = [
/No frame with id \d+ in tab \d+/,
/^No tab with id/,
"The tab was closed.",
errorTabDoesntExist,
errorTargetClosedEarly,
...CONNECTION_ERROR_MESSAGES,
];

Expand Down Expand Up @@ -136,6 +139,7 @@ export function isBusinessError(error: unknown): boolean {
// - because not all of our errors can be deserialized with the right class:
// https://github.com/sindresorhus/serialize-error/issues/72
const CLIENT_REQUEST_ERROR_NAMES = new Set([
"ClientRequestError",
"RemoteServiceError",
"ClientNetworkPermissionError",
"ClientNetworkError",
Expand Down Expand Up @@ -212,7 +216,7 @@ export function getErrorMessage(
* Handle ErrorEvents, i.e., generated from window.onerror
* @param event the error event
*/
function selectErrorFromEvent(event: ErrorEvent): Error {
export function selectErrorFromEvent(event: ErrorEvent): Error {
// https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent

Expand Down Expand Up @@ -251,7 +255,9 @@ function selectErrorFromEvent(event: ErrorEvent): Error {
* Handle unhandled promise rejections
* @param event the promise rejection event
*/
function selectErrorFromRejectionEvent(event: PromiseRejectionEvent): Error {
export function selectErrorFromRejectionEvent(
event: PromiseRejectionEvent
): Error {
// WARNING: don't prefix the error message, e.g., with "Asynchronous error:" because that breaks
// message-based error filtering via IGNORED_ERROR_PATTERNS
if (typeof event.reason === "string" || event.reason == null) {
Expand All @@ -262,15 +268,19 @@ function selectErrorFromRejectionEvent(event: PromiseRejectionEvent): Error {
}

/**
* Finds or creates an Error starting from strings, error event, or real Errors.
* Finds or creates an Error starting from strings or real Errors.
*
* The result is suitable for passing to Rollbar (which treats Errors and objects differently.)
*/
export function selectError(originalError: unknown): Error {
// Be defensive here for ErrorEvent. In practice, this method will only be called with errors (as opposed to events,
// though.) See reportUncaughtErrors
if (originalError instanceof ErrorEvent) {
return selectErrorFromEvent(originalError);
}

// Be defensive here for PromiseRejectionEvent. In practice, this method will only be called with errors (as opposed
// to events, though.) See reportUncaughtErrors
if (originalError instanceof PromiseRejectionEvent) {
return selectErrorFromRejectionEvent(originalError);
}
Expand Down
30 changes: 23 additions & 7 deletions src/telemetry/reportUncaughtErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,50 @@
/**
* @file This file must be imported as early as possible in each entrypoint, once
*/
import { isConnectionError } from "@/errors/errorHelpers";
import {
getErrorMessage,
IGNORED_ERROR_PATTERNS,
selectErrorFromEvent,
selectErrorFromRejectionEvent,
} from "@/errors/errorHelpers";
import reportError from "@/telemetry/reportError";
import { matchesAnyPattern } from "@/utils";

function ignoreConnectionErrors(
errorEvent: ErrorEvent | PromiseRejectionEvent
function ignoreKnownPatterns(
errorEvent: ErrorEvent | PromiseRejectionEvent,
error: unknown
): void {
if (isConnectionError(errorEvent)) {
if (matchesAnyPattern(getErrorMessage(error), IGNORED_ERROR_PATTERNS)) {
console.debug("Ignoring error matching IGNORED_ERROR_PATTERNS", {
error,
});

errorEvent.preventDefault();
}
}

function errorListener(errorEvent: ErrorEvent | PromiseRejectionEvent): void {
const error =
errorEvent instanceof PromiseRejectionEvent
? selectErrorFromRejectionEvent(errorEvent)
: selectErrorFromEvent(errorEvent);

for (const handler of uncaughtErrorHandlers) {
handler(errorEvent);
handler(errorEvent, error);
if (errorEvent.defaultPrevented) {
return;
}
}

// The browser already shows uncaught errors in the console
reportError(errorEvent, undefined, { logToConsole: false });
reportError(error, undefined, { logToConsole: false });
}

/**
* Array of handlers to run in order before the default one.
* They can call `event.preventDefault()` to avoid reporting the error.
*/
export const uncaughtErrorHandlers = [ignoreConnectionErrors];
export const uncaughtErrorHandlers = [ignoreKnownPatterns];

// Refactor beware: Do not add an `init` function or it will run too late.
// When imported, the file will be executed immediately, whereas if it exports
Expand Down

0 comments on commit 37d8b1b

Please sign in to comment.