Skip to content

Commit 83d54ba

Browse files
committed
Don't treat benign browser error events as client crashes
ResizeObserver loops and cross-origin 'Script error.' notifications arrive without an Error object and don't mean anything is broken, so they are ignored instead of closing the connection and showing the crash overlay. Also drop the location line when the browser has no real one (it passes the document URL with line/column 0), open the technical details whenever the user is asked to copy them, and close the connection before collecting the context so the reporter-failed overlay never sits on top of a live session.
1 parent 49765f6 commit 83d54ba

2 files changed

Lines changed: 48 additions & 7 deletions

File tree

client/js/tracing.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ export function describeError(error, fallback) {
3737
return fallback + (error === undefined || error === null ? '' : '\n' + stringifyValue(error));
3838
}
3939

40+
// some error events don't mean that the client is broken: a ResizeObserver loop is simply retried
41+
// on the next frame and a cross-origin 'Script error.' usually comes from a browser extension.
42+
// both are reported without an Error object, which is what tells them apart from a real crash.
43+
export function isNonFatalError(msg, error) {
44+
return !error && /^(ResizeObserver loop|Script error\.?$)/.test(`${msg}`);
45+
}
46+
4047
// a rejection reason is often a plain object like { status: 500 } - String() would turn that
4148
// into a useless [object Object], while JSON.stringify fails on cyclic values and BigInt
4249
function stringifyValue(value) {
@@ -76,7 +83,18 @@ onLoad(function() {
7683
showOverlay('clientErrorOverlay');
7784
}
7885

86+
// the status line asks the user to copy the technical details, so make sure they are visible
87+
const askForManualReport = function() {
88+
$('#clientErrorStatus').style.display = '';
89+
$('#clientErrorOverlay details').open = true;
90+
}
91+
7992
const reportError = function(description) {
93+
// close the connection before collecting the context: if that throws, the user still ends up
94+
// with a terminal overlay over a terminated session instead of a still running one
95+
preventReconnect();
96+
connection.close();
97+
8098
const details = {
8199
error: description,
82100
undoProtocol,
@@ -95,16 +113,14 @@ onLoad(function() {
95113
playerName,
96114
html: document.documentElement.outerHTML
97115
};
98-
preventReconnect();
99-
connection.close();
100116

101117
const button = $('#clientErrorOverlay button');
102118
// what the user typed is the most valuable part of the report - keep the textarea intact
103119
// and put the reason for the failure into the technical details instead
104120
const submitFailed = function(reason) {
105121
button.disabled = false;
106122
button.textContent = 'Try again';
107-
$('#clientErrorStatus').style.display = '';
123+
askForManualReport();
108124
$('#clientErrorStack').textContent = `${details.error}\n\nSubmitting the report failed:\n${reason}`;
109125
}
110126

@@ -158,7 +174,7 @@ onLoad(function() {
158174
// is nothing to submit in that case, so ask for a manual report and offer a plain reload.
159175
$('#clientErrorQuestion').style.display = 'none';
160176
$('#clientErrorInput').style.display = 'none';
161-
$('#clientErrorStatus').style.display = '';
177+
askForManualReport();
162178
const button = $('#clientErrorOverlay button');
163179
button.textContent = 'Reload';
164180
button.addEventListener('click', _=>window.location.reload());
@@ -167,8 +183,13 @@ onLoad(function() {
167183
}
168184

169185
window.onerror = function(msg, url, line, col, err) {
170-
// browsers report cross-origin errors without a location - 'at :0:0' would just look broken
171-
errorHandler(err, `${msg}` + (url ? `\n at ${url}:${line}:${col}` : ''));
186+
// tearing the session down over a non-fatal event would be worse than ignoring it, which is
187+
// what happened anyway before this handler learned to survive a missing Error object
188+
if(isNonFatalError(msg, err))
189+
return;
190+
// when the browser has no real location it passes the document URL with line and column 0 -
191+
// reporting 'at <page>:0:0' would just look like a truncation bug, so leave the line out
192+
errorHandler(err, `${msg}` + (url && line ? `\n at ${url}:${line}:${col}` : ''));
172193
};
173194
window.addEventListener("unhandledrejection", function(promiseRejectionEvent) {
174195
errorHandler(promiseRejectionEvent.reason, 'Unhandled promise rejection');

tests/client/error-report.test.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describeError } from '../../client/js/tracing.js';
1+
import { describeError, isNonFatalError } from '../../client/js/tracing.js';
22

33
describe("Scenarios: Describing a client error for the error report", () => {
44
describe("Given an Error object", () => {
@@ -46,3 +46,23 @@ describe("Scenarios: Describing a client error for the error report", () => {
4646
});
4747
});
4848
});
49+
50+
describe("Scenarios: Deciding whether an error event should crash the client", () => {
51+
describe("Given a browser event that does not indicate a broken client", () => {
52+
test("Then it is treated as non-fatal", () => {
53+
expect(isNonFatalError('ResizeObserver loop completed with undelivered notifications.')).toBe(true);
54+
expect(isNonFatalError('ResizeObserver loop limit exceeded')).toBe(true);
55+
expect(isNonFatalError('Script error.')).toBe(true);
56+
expect(isNonFatalError('Script error')).toBe(true);
57+
});
58+
});
59+
60+
describe("Given a real error", () => {
61+
test("Then it is reported even if the message looks harmless", () => {
62+
expect(isNonFatalError('Uncaught TypeError: x is not a function')).toBe(false);
63+
expect(isNonFatalError('Script error.', new Error('Script error.'))).toBe(false);
64+
expect(isNonFatalError('ResizeObserver loop completed', new Error('boom'))).toBe(false);
65+
expect(isNonFatalError(undefined)).toBe(false);
66+
});
67+
});
68+
});

0 commit comments

Comments
 (0)