Skip to content

ref(browser): Extract trycatch private methods into functions #4305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 15, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
319 changes: 160 additions & 159 deletions packages/browser/src/integrations/trycatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,202 +83,203 @@ export class TryCatch implements Integration {
const global = getGlobalObject();

if (this._options.setTimeout) {
fill(global, 'setTimeout', this._wrapTimeFunction.bind(this));
fill(global, 'setTimeout', _wrapTimeFunction);
}

if (this._options.setInterval) {
fill(global, 'setInterval', this._wrapTimeFunction.bind(this));
fill(global, 'setInterval', _wrapTimeFunction);
}

if (this._options.requestAnimationFrame) {
fill(global, 'requestAnimationFrame', this._wrapRAF.bind(this));
fill(global, 'requestAnimationFrame', _wrapRAF);
}

if (this._options.XMLHttpRequest && 'XMLHttpRequest' in global) {
fill(XMLHttpRequest.prototype, 'send', this._wrapXHR.bind(this));
fill(XMLHttpRequest.prototype, 'send', _wrapXHR);
}

if (this._options.eventTarget) {
const eventTarget = Array.isArray(this._options.eventTarget) ? this._options.eventTarget : DEFAULT_EVENT_TARGET;
eventTarget.forEach(this._wrapEventTarget.bind(this));
const eventTargetOption = this._options.eventTarget;
if (eventTargetOption) {
const eventTarget = Array.isArray(eventTargetOption) ? eventTargetOption : DEFAULT_EVENT_TARGET;
eventTarget.forEach(_wrapEventTarget);
}
}
}

/** JSDoc */
private _wrapTimeFunction(original: () => void): () => number {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function(this: any, ...args: any[]): number {
const originalCallback = args[0];
args[0] = wrap(originalCallback, {
/** JSDoc */
function _wrapTimeFunction(original: () => void): () => number {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function(this: any, ...args: any[]): number {
const originalCallback = args[0];
args[0] = wrap(originalCallback, {
mechanism: {
data: { function: getFunctionName(original) },
handled: true,
type: 'instrument',
},
});
return original.apply(this, args);
};
}

/** JSDoc */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function _wrapRAF(original: any): (callback: () => void) => any {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function(this: any, callback: () => void): () => void {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return original.call(
this,
wrap(callback, {
mechanism: {
data: { function: getFunctionName(original) },
data: {
function: 'requestAnimationFrame',
handler: getFunctionName(original),
},
handled: true,
type: 'instrument',
},
});
return original.apply(this, args);
};
}
}),
);
};
}

/** JSDoc */
/** JSDoc */
function _wrapXHR(originalSend: () => void): () => void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _wrapRAF(original: any): (callback: () => void) => any {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function(this: any, callback: () => void): () => void {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return original.call(
this,
wrap(callback, {
mechanism: {
data: {
function: 'requestAnimationFrame',
handler: getFunctionName(original),
},
handled: true,
type: 'instrument',
},
}),
);
};
}

/** JSDoc */
private _wrapEventTarget(target: string): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const global = getGlobalObject() as { [key: string]: any };
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const proto = global[target] && global[target].prototype;

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-prototype-builtins
if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) {
return;
}
return function(this: XMLHttpRequest, ...args: any[]): void {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const xhr = this;
const xmlHttpRequestProps: XMLHttpRequestProp[] = ['onload', 'onerror', 'onprogress', 'onreadystatechange'];

fill(proto, 'addEventListener', function(
original: () => void,
): (eventName: string, fn: EventListenerObject, options?: boolean | AddEventListenerOptions) => void {
return function(
xmlHttpRequestProps.forEach(prop => {
if (prop in xhr && typeof xhr[prop] === 'function') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this: any,
eventName: string,
fn: EventListenerObject,
options?: boolean | AddEventListenerOptions,
): (eventName: string, fn: EventListenerObject, capture?: boolean, secure?: boolean) => void {
try {
if (typeof fn.handleEvent === 'function') {
fn.handleEvent = wrap(fn.handleEvent.bind(fn), {
mechanism: {
data: {
function: 'handleEvent',
handler: getFunctionName(fn),
target,
},
handled: true,
type: 'instrument',
},
});
}
} catch (err) {
// can sometimes get 'Permission denied to access property "handle Event'
}

return original.call(
this,
eventName,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
wrap((fn as any) as WrappedFunction, {
fill(xhr, prop, function(original: WrappedFunction): () => any {
const wrapOptions = {
mechanism: {
data: {
function: 'addEventListener',
handler: getFunctionName(fn),
target,
function: prop,
handler: getFunctionName(original),
},
handled: true,
type: 'instrument',
},
}),
options,
);
};
});
};

fill(proto, 'removeEventListener', function(
originalRemoveEventListener: () => void,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): (this: any, eventName: string, fn: EventListenerObject, options?: boolean | EventListenerOptions) => () => void {
return function(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this: any,
eventName: string,
fn: EventListenerObject,
options?: boolean | EventListenerOptions,
): () => void {
/**
* There are 2 possible scenarios here:
*
* 1. Someone passes a callback, which was attached prior to Sentry initialization, or by using unmodified
* method, eg. `document.addEventListener.call(el, name, handler). In this case, we treat this function
* as a pass-through, and call original `removeEventListener` with it.
*
* 2. Someone passes a callback, which was attached after Sentry was initialized, which means that it was using
* our wrapped version of `addEventListener`, which internally calls `wrap` helper.
* This helper "wraps" whole callback inside a try/catch statement, and attached appropriate metadata to it,
* in order for us to make a distinction between wrapped/non-wrapped functions possible.
* If a function was wrapped, it has additional property of `__sentry_wrapped__`, holding the handler.
*
* When someone adds a handler prior to initialization, and then do it again, but after,
* then we have to detach both of them. Otherwise, if we'd detach only wrapped one, it'd be impossible
* to get rid of the initial handler and it'd stick there forever.
*/
const wrappedEventHandler = (fn as unknown) as WrappedFunction;
try {
const originalEventHandler = wrappedEventHandler && wrappedEventHandler.__sentry_wrapped__;
if (originalEventHandler) {
originalRemoveEventListener.call(this, eventName, originalEventHandler, options);
// If Instrument integration has been called before TryCatch, get the name of original function
if (original.__sentry_original__) {
wrapOptions.mechanism.data.handler = getFunctionName(original.__sentry_original__);
}
} catch (e) {
// ignore, accessing __sentry_wrapped__ will throw in some Selenium environments
}
return originalRemoveEventListener.call(this, eventName, wrappedEventHandler, options);
};

// Otherwise wrap directly
return wrap(original, wrapOptions);
});
}
});
}

/** JSDoc */
private _wrapXHR(originalSend: () => void): () => void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function(this: XMLHttpRequest, ...args: any[]): void {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const xhr = this;
const xmlHttpRequestProps: XMLHttpRequestProp[] = ['onload', 'onerror', 'onprogress', 'onreadystatechange'];
return originalSend.apply(this, args);
};
}

xmlHttpRequestProps.forEach(prop => {
if (prop in xhr && typeof xhr[prop] === 'function') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fill(xhr, prop, function(original: WrappedFunction): () => any {
const wrapOptions = {
mechanism: {
data: {
function: prop,
handler: getFunctionName(original),
},
handled: true,
type: 'instrument',
},
};
/** JSDoc */
function _wrapEventTarget(target: string): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const global = getGlobalObject() as { [key: string]: any };
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const proto = global[target] && global[target].prototype;

// If Instrument integration has been called before TryCatch, get the name of original function
if (original.__sentry_original__) {
wrapOptions.mechanism.data.handler = getFunctionName(original.__sentry_original__);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-prototype-builtins
if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) {
return;
}

// Otherwise wrap directly
return wrap(original, wrapOptions);
fill(proto, 'addEventListener', function(
original: () => void,
): (eventName: string, fn: EventListenerObject, options?: boolean | AddEventListenerOptions) => void {
return function(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this: any,
eventName: string,
fn: EventListenerObject,
options?: boolean | AddEventListenerOptions,
): (eventName: string, fn: EventListenerObject, capture?: boolean, secure?: boolean) => void {
try {
if (typeof fn.handleEvent === 'function') {
fn.handleEvent = wrap(fn.handleEvent.bind(fn), {
mechanism: {
data: {
function: 'handleEvent',
handler: getFunctionName(fn),
target,
},
handled: true,
type: 'instrument',
},
});
}
});
} catch (err) {
// can sometimes get 'Permission denied to access property "handle Event'
}

return original.call(
this,
eventName,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
wrap((fn as any) as WrappedFunction, {
mechanism: {
data: {
function: 'addEventListener',
handler: getFunctionName(fn),
target,
},
handled: true,
type: 'instrument',
},
}),
options,
);
};
});

return originalSend.apply(this, args);
fill(proto, 'removeEventListener', function(
originalRemoveEventListener: () => void,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): (this: any, eventName: string, fn: EventListenerObject, options?: boolean | EventListenerOptions) => () => void {
return function(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this: any,
eventName: string,
fn: EventListenerObject,
options?: boolean | EventListenerOptions,
): () => void {
/**
* There are 2 possible scenarios here:
*
* 1. Someone passes a callback, which was attached prior to Sentry initialization, or by using unmodified
* method, eg. `document.addEventListener.call(el, name, handler). In this case, we treat this function
* as a pass-through, and call original `removeEventListener` with it.
*
* 2. Someone passes a callback, which was attached after Sentry was initialized, which means that it was using
* our wrapped version of `addEventListener`, which internally calls `wrap` helper.
* This helper "wraps" whole callback inside a try/catch statement, and attached appropriate metadata to it,
* in order for us to make a distinction between wrapped/non-wrapped functions possible.
* If a function was wrapped, it has additional property of `__sentry_wrapped__`, holding the handler.
*
* When someone adds a handler prior to initialization, and then do it again, but after,
* then we have to detach both of them. Otherwise, if we'd detach only wrapped one, it'd be impossible
* to get rid of the initial handler and it'd stick there forever.
*/
const wrappedEventHandler = (fn as unknown) as WrappedFunction;
try {
const originalEventHandler = wrappedEventHandler && wrappedEventHandler.__sentry_wrapped__;
if (originalEventHandler) {
originalRemoveEventListener.call(this, eventName, originalEventHandler, options);
}
} catch (e) {
// ignore, accessing __sentry_wrapped__ will throw in some Selenium environments
}
return originalRemoveEventListener.call(this, eventName, wrappedEventHandler, options);
};
}
});
}