Skip to content

Commit be73e38

Browse files
authored
ref(types): Avoid some any type casting around wrap code (#14463)
We use a heavy dose of `any`, that I would like to reduce. So I started out to look into `wrap()`, which made heave use of this, and tried to rewrite it to avoid `any` as much as possible. This required some changes around it, but should now have much better type inferrence etc. than before, and be more "realistic" in what it tells you. While at this, I also removed the `before` argument that we were not using anymore - `wrap` is not exported anymore, so this is purely internal.
1 parent f93ccbe commit be73e38

File tree

7 files changed

+164
-128
lines changed

7 files changed

+164
-128
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const btn = document.createElement('button');
2+
btn.id = 'btn';
3+
document.body.appendChild(btn);
4+
5+
const functionListener = function () {
6+
throw new Error('event_listener_error');
7+
};
8+
9+
btn.addEventListener('click', functionListener);
10+
11+
btn.click();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';
6+
7+
sentryTest('should capture target name in mechanism data', async ({ getLocalTestUrl, page }) => {
8+
const url = await getLocalTestUrl({ testDir: __dirname });
9+
10+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
11+
12+
expect(eventData.exception?.values).toHaveLength(1);
13+
expect(eventData.exception?.values?.[0]).toMatchObject({
14+
type: 'Error',
15+
value: 'event_listener_error',
16+
mechanism: {
17+
type: 'instrument',
18+
handled: false,
19+
data: {
20+
function: 'addEventListener',
21+
handler: 'functionListener',
22+
target: 'EventTarget',
23+
},
24+
},
25+
stacktrace: {
26+
frames: expect.any(Array),
27+
},
28+
});
29+
});

packages/browser/src/helpers.ts

+41-26
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ export function ignoreNextOnError(): void {
3131
});
3232
}
3333

34+
// eslint-disable-next-line @typescript-eslint/ban-types
35+
type WrappableFunction = Function;
36+
37+
export function wrap<T extends WrappableFunction>(
38+
fn: T,
39+
options?: {
40+
mechanism?: Mechanism;
41+
},
42+
): WrappedFunction<T>;
43+
export function wrap<NonFunction>(
44+
fn: NonFunction,
45+
options?: {
46+
mechanism?: Mechanism;
47+
},
48+
): NonFunction;
3449
/**
3550
* Instruments the given function and sends an event to Sentry every time the
3651
* function throws an exception.
@@ -40,29 +55,31 @@ export function ignoreNextOnError(): void {
4055
* @returns The wrapped function.
4156
* @hidden
4257
*/
43-
export function wrap(
44-
fn: WrappedFunction,
58+
export function wrap<T extends WrappableFunction, NonFunction>(
59+
fn: T | NonFunction,
4560
options: {
4661
mechanism?: Mechanism;
4762
} = {},
48-
before?: WrappedFunction,
49-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
50-
): any {
63+
): NonFunction | WrappedFunction<T> {
5164
// for future readers what this does is wrap a function and then create
5265
// a bi-directional wrapping between them.
5366
//
5467
// example: wrapped = wrap(original);
5568
// original.__sentry_wrapped__ -> wrapped
5669
// wrapped.__sentry_original__ -> original
5770

58-
if (typeof fn !== 'function') {
71+
function isFunction(fn: T | NonFunction): fn is T {
72+
return typeof fn === 'function';
73+
}
74+
75+
if (!isFunction(fn)) {
5976
return fn;
6077
}
6178

6279
try {
6380
// if we're dealing with a function that was previously wrapped, return
6481
// the original wrapper.
65-
const wrapper = fn.__sentry_wrapped__;
82+
const wrapper = (fn as WrappedFunction<T>).__sentry_wrapped__;
6683
if (wrapper) {
6784
if (typeof wrapper === 'function') {
6885
return wrapper;
@@ -84,18 +101,12 @@ export function wrap(
84101
return fn;
85102
}
86103

87-
/* eslint-disable prefer-rest-params */
104+
// Wrap the function itself
88105
// It is important that `sentryWrapped` is not an arrow function to preserve the context of `this`
89-
const sentryWrapped: WrappedFunction = function (this: unknown): void {
90-
const args = Array.prototype.slice.call(arguments);
91-
106+
const sentryWrapped = function (this: unknown, ...args: unknown[]): unknown {
92107
try {
93-
if (before && typeof before === 'function') {
94-
before.apply(this, arguments);
95-
}
96-
97-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
98-
const wrappedArguments = args.map((arg: any) => wrap(arg, options));
108+
// Also wrap arguments that are themselves functions
109+
const wrappedArguments = args.map(arg => wrap(arg, options));
99110

100111
// Attempt to invoke user-land function
101112
// NOTE: If you are a Sentry user, and you are seeing this stack frame, it
@@ -125,18 +136,19 @@ export function wrap(
125136

126137
throw ex;
127138
}
128-
};
129-
/* eslint-enable prefer-rest-params */
139+
} as unknown as WrappedFunction<T>;
130140

131-
// Accessing some objects may throw
132-
// ref: https://github.com/getsentry/sentry-javascript/issues/1168
141+
// Wrap the wrapped function in a proxy, to ensure any other properties of the original function remain available
133142
try {
134143
for (const property in fn) {
135144
if (Object.prototype.hasOwnProperty.call(fn, property)) {
136-
sentryWrapped[property] = fn[property];
145+
sentryWrapped[property as keyof T] = fn[property as keyof T];
137146
}
138147
}
139-
} catch (_oO) {} // eslint-disable-line no-empty
148+
} catch {
149+
// Accessing some objects may throw
150+
// ref: https://github.com/getsentry/sentry-javascript/issues/1168
151+
}
140152

141153
// Signal that this function has been wrapped/filled already
142154
// for both debugging and to prevent it to being wrapped/filled twice
@@ -146,16 +158,19 @@ export function wrap(
146158

147159
// Restore original function name (not all browsers allow that)
148160
try {
149-
const descriptor = Object.getOwnPropertyDescriptor(sentryWrapped, 'name') as PropertyDescriptor;
161+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
162+
const descriptor = Object.getOwnPropertyDescriptor(sentryWrapped, 'name')!;
150163
if (descriptor.configurable) {
151164
Object.defineProperty(sentryWrapped, 'name', {
152165
get(): string {
153166
return fn.name;
154167
},
155168
});
156169
}
157-
// eslint-disable-next-line no-empty
158-
} catch (_oO) {}
170+
} catch {
171+
// This may throw if e.g. the descriptor does not exist, or a browser does not allow redefining `name`.
172+
// to save some bytes we simply try-catch this
173+
}
159174

160175
return sentryWrapped;
161176
}

packages/browser/src/integrations/browserapierrors.ts

+51-74
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,7 @@ const _browserApiErrorsIntegration = ((options: Partial<BrowserApiErrorsOptions>
9696
export const browserApiErrorsIntegration = defineIntegration(_browserApiErrorsIntegration);
9797

9898
function _wrapTimeFunction(original: () => void): () => number {
99-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
100-
return function (this: any, ...args: any[]): number {
99+
return function (this: unknown, ...args: unknown[]): number {
101100
const originalCallback = args[0];
102101
args[0] = wrap(originalCallback, {
103102
mechanism: {
@@ -110,11 +109,8 @@ function _wrapTimeFunction(original: () => void): () => number {
110109
};
111110
}
112111

113-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
114-
function _wrapRAF(original: any): (callback: () => void) => any {
115-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
116-
return function (this: any, callback: () => void): () => void {
117-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
112+
function _wrapRAF(original: () => void): (callback: () => void) => unknown {
113+
return function (this: unknown, callback: () => void): () => void {
118114
return original.apply(this, [
119115
wrap(callback, {
120116
mechanism: {
@@ -131,16 +127,14 @@ function _wrapRAF(original: any): (callback: () => void) => any {
131127
}
132128

133129
function _wrapXHR(originalSend: () => void): () => void {
134-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
135-
return function (this: XMLHttpRequest, ...args: any[]): void {
130+
return function (this: XMLHttpRequest, ...args: unknown[]): void {
136131
// eslint-disable-next-line @typescript-eslint/no-this-alias
137132
const xhr = this;
138133
const xmlHttpRequestProps: XMLHttpRequestProp[] = ['onload', 'onerror', 'onprogress', 'onreadystatechange'];
139134

140135
xmlHttpRequestProps.forEach(prop => {
141136
if (prop in xhr && typeof xhr[prop] === 'function') {
142-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
143-
fill(xhr, prop, function (original: WrappedFunction): () => any {
137+
fill(xhr, prop, function (original) {
144138
const wrapOptions = {
145139
mechanism: {
146140
data: {
@@ -169,30 +163,21 @@ function _wrapXHR(originalSend: () => void): () => void {
169163
}
170164

171165
function _wrapEventTarget(target: string): void {
172-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
173-
const globalObject = WINDOW as { [key: string]: any };
174-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
175-
const proto = globalObject[target] && globalObject[target].prototype;
166+
const globalObject = WINDOW as unknown as Record<string, { prototype?: object }>;
167+
const targetObj = globalObject[target];
168+
const proto = targetObj && targetObj.prototype;
176169

177-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-prototype-builtins
170+
// eslint-disable-next-line no-prototype-builtins
178171
if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) {
179172
return;
180173
}
181174

182175
fill(proto, 'addEventListener', function (original: VoidFunction,): (
183-
eventName: string,
184-
fn: EventListenerObject,
185-
options?: boolean | AddEventListenerOptions,
186-
) => void {
187-
return function (
188-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
189-
this: any,
190-
eventName: string,
191-
fn: EventListenerObject,
192-
options?: boolean | AddEventListenerOptions,
193-
): (eventName: string, fn: EventListenerObject, capture?: boolean, secure?: boolean) => void {
176+
...args: Parameters<typeof WINDOW.addEventListener>
177+
) => ReturnType<typeof WINDOW.addEventListener> {
178+
return function (this: unknown, eventName, fn, options): VoidFunction {
194179
try {
195-
if (typeof fn.handleEvent === 'function') {
180+
if (isEventListenerObject(fn)) {
196181
// ESlint disable explanation:
197182
// First, it is generally safe to call `wrap` with an unbound function. Furthermore, using `.bind()` would
198183
// introduce a bug here, because bind returns a new function that doesn't have our
@@ -211,14 +196,13 @@ function _wrapEventTarget(target: string): void {
211196
},
212197
});
213198
}
214-
} catch (err) {
199+
} catch {
215200
// can sometimes get 'Permission denied to access property "handle Event'
216201
}
217202

218203
return original.apply(this, [
219204
eventName,
220-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
221-
wrap(fn as any as WrappedFunction, {
205+
wrap(fn, {
222206
mechanism: {
223207
data: {
224208
function: 'addEventListener',
@@ -234,48 +218,41 @@ function _wrapEventTarget(target: string): void {
234218
};
235219
});
236220

237-
fill(
238-
proto,
239-
'removeEventListener',
240-
function (
241-
originalRemoveEventListener: () => void,
242-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
243-
): (this: any, eventName: string, fn: EventListenerObject, options?: boolean | EventListenerOptions) => () => void {
244-
return function (
245-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
246-
this: any,
247-
eventName: string,
248-
fn: EventListenerObject,
249-
options?: boolean | EventListenerOptions,
250-
): () => void {
251-
/**
252-
* There are 2 possible scenarios here:
253-
*
254-
* 1. Someone passes a callback, which was attached prior to Sentry initialization, or by using unmodified
255-
* method, eg. `document.addEventListener.call(el, name, handler). In this case, we treat this function
256-
* as a pass-through, and call original `removeEventListener` with it.
257-
*
258-
* 2. Someone passes a callback, which was attached after Sentry was initialized, which means that it was using
259-
* our wrapped version of `addEventListener`, which internally calls `wrap` helper.
260-
* This helper "wraps" whole callback inside a try/catch statement, and attached appropriate metadata to it,
261-
* in order for us to make a distinction between wrapped/non-wrapped functions possible.
262-
* If a function was wrapped, it has additional property of `__sentry_wrapped__`, holding the handler.
263-
*
264-
* When someone adds a handler prior to initialization, and then do it again, but after,
265-
* then we have to detach both of them. Otherwise, if we'd detach only wrapped one, it'd be impossible
266-
* to get rid of the initial handler and it'd stick there forever.
267-
*/
268-
const wrappedEventHandler = fn as unknown as WrappedFunction;
269-
try {
270-
const originalEventHandler = wrappedEventHandler && wrappedEventHandler.__sentry_wrapped__;
271-
if (originalEventHandler) {
272-
originalRemoveEventListener.call(this, eventName, originalEventHandler, options);
273-
}
274-
} catch (e) {
275-
// ignore, accessing __sentry_wrapped__ will throw in some Selenium environments
221+
fill(proto, 'removeEventListener', function (originalRemoveEventListener: VoidFunction,): (
222+
this: unknown,
223+
...args: Parameters<typeof WINDOW.removeEventListener>
224+
) => ReturnType<typeof WINDOW.removeEventListener> {
225+
return function (this: unknown, eventName, fn, options): VoidFunction {
226+
/**
227+
* There are 2 possible scenarios here:
228+
*
229+
* 1. Someone passes a callback, which was attached prior to Sentry initialization, or by using unmodified
230+
* method, eg. `document.addEventListener.call(el, name, handler). In this case, we treat this function
231+
* as a pass-through, and call original `removeEventListener` with it.
232+
*
233+
* 2. Someone passes a callback, which was attached after Sentry was initialized, which means that it was using
234+
* our wrapped version of `addEventListener`, which internally calls `wrap` helper.
235+
* This helper "wraps" whole callback inside a try/catch statement, and attached appropriate metadata to it,
236+
* in order for us to make a distinction between wrapped/non-wrapped functions possible.
237+
* If a function was wrapped, it has additional property of `__sentry_wrapped__`, holding the handler.
238+
*
239+
* When someone adds a handler prior to initialization, and then do it again, but after,
240+
* then we have to detach both of them. Otherwise, if we'd detach only wrapped one, it'd be impossible
241+
* to get rid of the initial handler and it'd stick there forever.
242+
*/
243+
try {
244+
const originalEventHandler = (fn as WrappedFunction).__sentry_wrapped__;
245+
if (originalEventHandler) {
246+
originalRemoveEventListener.call(this, eventName, originalEventHandler, options);
276247
}
277-
return originalRemoveEventListener.call(this, eventName, wrappedEventHandler, options);
278-
};
279-
},
280-
);
248+
} catch (e) {
249+
// ignore, accessing __sentry_wrapped__ will throw in some Selenium environments
250+
}
251+
return originalRemoveEventListener.call(this, eventName, fn, options);
252+
};
253+
});
254+
}
255+
256+
function isEventListenerObject(obj: unknown): obj is EventListenerObject {
257+
return typeof (obj as EventListenerObject).handleEvent === 'function';
281258
}

0 commit comments

Comments
 (0)