Skip to content

Commit

Permalink
fix: prevent window.onerror from being overriden
Browse files Browse the repository at this point in the history
  • Loading branch information
wqcstrong committed Jun 14, 2023
1 parent 7350ab2 commit c936ca7
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 24 deletions.
53 changes: 42 additions & 11 deletions src/plugins/error.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable class-methods-use-this */
import atom from 'src/utils/atom';
import { makeMessage, DEBUG_MESSAGE_TYPE } from 'src/utils/message';
import socketStore from 'src/utils/socket';
Expand All @@ -6,21 +7,49 @@ import type PageSpyPlugin from './index';
export default class ErrorPlugin implements PageSpyPlugin {
public name = 'ErrorPlugin';

private error: OnErrorEventHandler = null;

public onCreated() {
const that = this;
this.error = window.onerror;
this.onUncaughtError();
this.onResourceLoadError();
this.onUnhandledRejectionError();
}

// Uncaught error
window.onerror = function (...args) {
ErrorPlugin.sendMessage(args[4]);
if (that.error) {
that.error.apply(window, args);
}
};
private onUncaughtError() {
const userErr = window.onerror;
// @ts-ignore
const isConfigurable = delete window.onerror;
if (!isConfigurable) return;

let errorHandler: (this: Window, ev: ErrorEvent) => any;
Object.defineProperty(window, 'onerror', {
// Normally, users would simply capture errors by assigning to 'window.onerror',
// but to avoid conflicts with the logic of other libraries, we specify
// 'configurable' as false here.
configurable: false,
enumerable: true,
get() {
return errorHandler;
},
set(fn: OnErrorEventHandler) {
window.removeEventListener('error', errorHandler);
errorHandler = (e: ErrorEvent) => {
ErrorPlugin.sendMessage(e.error?.stack || e.message);
fn?.apply(window, [
e.message,
e.filename,
e.lineno,
e.colno,
e.error,
]);
};
window.addEventListener('error', errorHandler);
},
});
window.onerror = userErr;
}

private onResourceLoadError() {
// Resource load failed
// Track the error on capture-phase
window.addEventListener(
'error',
(evt: Event) => {
Expand All @@ -34,7 +63,9 @@ export default class ErrorPlugin implements PageSpyPlugin {
},
true,
);
}

private onUnhandledRejectionError() {
// Promise unhandledRejection Error
window.addEventListener(
'unhandledrejection',
Expand Down
64 changes: 51 additions & 13 deletions tests/plugins/error.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,68 @@
import ErrorPlugin from 'src/plugins/error';

beforeAll(() => {
window.onerror = function () {
// placeholder
};
new ErrorPlugin().onCreated();
});

const errorOcupied = jest.fn();
jest.spyOn(ErrorPlugin, 'sendMessage').mockImplementation(errorOcupied);
const errorOccupied = jest.fn();
jest.spyOn(ErrorPlugin, 'sendMessage').mockImplementation(errorOccupied);
afterEach(() => {
jest.restoreAllMocks();
});

describe('Error plugin', () => {
it('window.onerror', () => {
expect(window.onerror).not.toBe(null);
window.onerror!('', '', 0, 0, new Error('throw error'));
expect(errorOcupied).toHaveBeenCalledTimes(1);
describe('Uncaught Error', () => {
it('Have initiator value', (done) => {
expect(window.onerror).not.toBeFalsy();

setTimeout(() => {
throw new Error('Unit test');
});
setTimeout(() => {
expect(errorOccupied).toHaveBeenCalledTimes(1);
done();
}, 10);
});
it('Register new error function', (done) => {
const fn = jest.fn();
window.onerror = fn;
setTimeout(() => {
throw new Error('Unit test');
});

setTimeout(() => {
expect(fn).toHaveBeenCalledTimes(1);
expect(errorOccupied).toHaveBeenCalledTimes(1);
done();
}, 10);
});
it('Only the last registration will take effect if assign many times', (done) => {
const fn1 = jest.fn();
const fn2 = jest.fn();
const fn3 = jest.fn();
window.onerror = fn1;
window.onerror = fn2;
window.onerror = fn3;

setTimeout(() => {
throw new Error('Unit test');
});
setTimeout(() => {
expect([
fn1.mock.calls.length,
fn2.mock.calls.length,
fn3.mock.calls.length,
]).toEqual([0, 0, 1]);
done();
}, 10);
});
});
it('window.addEventListener("error", fn)', () => {
it('Resource load failed error', () => {
window.dispatchEvent(new Event('error'));
expect(errorOcupied).toHaveBeenCalledTimes(1);
expect(errorOccupied).toHaveBeenCalledTimes(1);
});
it('Promise unhandledrejection', () => {
it('Unhandledrejection error', () => {
window.dispatchEvent(new Event('unhandledrejection'));
expect(errorOcupied).toHaveBeenCalledTimes(1);
expect(errorOccupied).toHaveBeenCalledTimes(1);
});
});

0 comments on commit c936ca7

Please sign in to comment.