this repo has no description
1import { fill, getFunctionName, getOriginalFunction } from '@sentry/utils';
2import { WINDOW, wrap } from '../helpers.js';
3
4const DEFAULT_EVENT_TARGET = [
5 'EventTarget',
6 'Window',
7 'Node',
8 'ApplicationCache',
9 'AudioTrackList',
10 'ChannelMergerNode',
11 'CryptoOperation',
12 'EventSource',
13 'FileReader',
14 'HTMLUnknownElement',
15 'IDBDatabase',
16 'IDBRequest',
17 'IDBTransaction',
18 'KeyOperation',
19 'MediaController',
20 'MessagePort',
21 'ModalWindow',
22 'Notification',
23 'SVGElementInstance',
24 'Screen',
25 'TextTrack',
26 'TextTrackCue',
27 'TextTrackList',
28 'WebSocket',
29 'WebSocketWorker',
30 'Worker',
31 'XMLHttpRequest',
32 'XMLHttpRequestEventTarget',
33 'XMLHttpRequestUpload',
34];
35
36/** Wrap timer functions and event targets to catch errors and provide better meta data */
37class TryCatch {
38 /**
39 * @inheritDoc
40 */
41 static __initStatic() {this.id = 'TryCatch';}
42
43 /**
44 * @inheritDoc
45 */
46 __init() {this.name = TryCatch.id;}
47
48 /** JSDoc */
49
50 /**
51 * @inheritDoc
52 */
53 constructor(options) {TryCatch.prototype.__init.call(this);
54 this._options = {
55 XMLHttpRequest: true,
56 eventTarget: true,
57 requestAnimationFrame: true,
58 setInterval: true,
59 setTimeout: true,
60 ...options,
61 };
62 }
63
64 /**
65 * Wrap timer functions and event targets to catch errors
66 * and provide better metadata.
67 */
68 setupOnce() {
69 if (this._options.setTimeout) {
70 fill(WINDOW, 'setTimeout', _wrapTimeFunction);
71 }
72
73 if (this._options.setInterval) {
74 fill(WINDOW, 'setInterval', _wrapTimeFunction);
75 }
76
77 if (this._options.requestAnimationFrame) {
78 fill(WINDOW, 'requestAnimationFrame', _wrapRAF);
79 }
80
81 if (this._options.XMLHttpRequest && 'XMLHttpRequest' in WINDOW) {
82 fill(XMLHttpRequest.prototype, 'send', _wrapXHR);
83 }
84
85 const eventTargetOption = this._options.eventTarget;
86 if (eventTargetOption) {
87 const eventTarget = Array.isArray(eventTargetOption) ? eventTargetOption : DEFAULT_EVENT_TARGET;
88 eventTarget.forEach(_wrapEventTarget);
89 }
90 }
91} TryCatch.__initStatic();
92
93/** JSDoc */
94function _wrapTimeFunction(original) {
95 // eslint-disable-next-line @typescript-eslint/no-explicit-any
96 return function ( ...args) {
97 const originalCallback = args[0];
98 args[0] = wrap(originalCallback, {
99 mechanism: {
100 data: { function: getFunctionName(original) },
101 handled: true,
102 type: 'instrument',
103 },
104 });
105 return original.apply(this, args);
106 };
107}
108
109/** JSDoc */
110// eslint-disable-next-line @typescript-eslint/no-explicit-any
111function _wrapRAF(original) {
112 // eslint-disable-next-line @typescript-eslint/no-explicit-any
113 return function ( callback) {
114 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
115 return original.apply(this, [
116 wrap(callback, {
117 mechanism: {
118 data: {
119 function: 'requestAnimationFrame',
120 handler: getFunctionName(original),
121 },
122 handled: true,
123 type: 'instrument',
124 },
125 }),
126 ]);
127 };
128}
129
130/** JSDoc */
131function _wrapXHR(originalSend) {
132 // eslint-disable-next-line @typescript-eslint/no-explicit-any
133 return function ( ...args) {
134 // eslint-disable-next-line @typescript-eslint/no-this-alias
135 const xhr = this;
136 const xmlHttpRequestProps = ['onload', 'onerror', 'onprogress', 'onreadystatechange'];
137
138 xmlHttpRequestProps.forEach(prop => {
139 if (prop in xhr && typeof xhr[prop] === 'function') {
140 // eslint-disable-next-line @typescript-eslint/no-explicit-any
141 fill(xhr, prop, function (original) {
142 const wrapOptions = {
143 mechanism: {
144 data: {
145 function: prop,
146 handler: getFunctionName(original),
147 },
148 handled: true,
149 type: 'instrument',
150 },
151 };
152
153 // If Instrument integration has been called before TryCatch, get the name of original function
154 const originalFunction = getOriginalFunction(original);
155 if (originalFunction) {
156 wrapOptions.mechanism.data.handler = getFunctionName(originalFunction);
157 }
158
159 // Otherwise wrap directly
160 return wrap(original, wrapOptions);
161 });
162 }
163 });
164
165 return originalSend.apply(this, args);
166 };
167}
168
169/** JSDoc */
170function _wrapEventTarget(target) {
171 // eslint-disable-next-line @typescript-eslint/no-explicit-any
172 const globalObject = WINDOW ;
173 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
174 const proto = globalObject[target] && globalObject[target].prototype;
175
176 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-prototype-builtins
177 if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) {
178 return;
179 }
180
181 fill(proto, 'addEventListener', function (original)
182
183 {
184 return function (
185 // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
187 eventName,
188 fn,
189 options,
190 ) {
191 try {
192 if (typeof fn.handleEvent === 'function') {
193 // ESlint disable explanation:
194 // First, it is generally safe to call `wrap` with an unbound function. Furthermore, using `.bind()` would
195 // introduce a bug here, because bind returns a new function that doesn't have our
196 // flags(like __sentry_original__) attached. `wrap` checks for those flags to avoid unnecessary wrapping.
197 // Without those flags, every call to addEventListener wraps the function again, causing a memory leak.
198 // eslint-disable-next-line @typescript-eslint/unbound-method
199 fn.handleEvent = wrap(fn.handleEvent, {
200 mechanism: {
201 data: {
202 function: 'handleEvent',
203 handler: getFunctionName(fn),
204 target,
205 },
206 handled: true,
207 type: 'instrument',
208 },
209 });
210 }
211 } catch (err) {
212 // can sometimes get 'Permission denied to access property "handle Event'
213 }
214
215 return original.apply(this, [
216 eventName,
217 // eslint-disable-next-line @typescript-eslint/no-explicit-any
218 wrap(fn , {
219 mechanism: {
220 data: {
221 function: 'addEventListener',
222 handler: getFunctionName(fn),
223 target,
224 },
225 handled: true,
226 type: 'instrument',
227 },
228 }),
229 options,
230 ]);
231 };
232 });
233
234 fill(
235 proto,
236 'removeEventListener',
237 function (
238 originalRemoveEventListener,
239 // eslint-disable-next-line @typescript-eslint/no-explicit-any
240 ) {
241 return function (
242 // eslint-disable-next-line @typescript-eslint/no-explicit-any
243
244 eventName,
245 fn,
246 options,
247 ) {
248 /**
249 * There are 2 possible scenarios here:
250 *
251 * 1. Someone passes a callback, which was attached prior to Sentry initialization, or by using unmodified
252 * method, eg. `document.addEventListener.call(el, name, handler). In this case, we treat this function
253 * as a pass-through, and call original `removeEventListener` with it.
254 *
255 * 2. Someone passes a callback, which was attached after Sentry was initialized, which means that it was using
256 * our wrapped version of `addEventListener`, which internally calls `wrap` helper.
257 * This helper "wraps" whole callback inside a try/catch statement, and attached appropriate metadata to it,
258 * in order for us to make a distinction between wrapped/non-wrapped functions possible.
259 * If a function was wrapped, it has additional property of `__sentry_wrapped__`, holding the handler.
260 *
261 * When someone adds a handler prior to initialization, and then do it again, but after,
262 * then we have to detach both of them. Otherwise, if we'd detach only wrapped one, it'd be impossible
263 * to get rid of the initial handler and it'd stick there forever.
264 */
265 const wrappedEventHandler = fn ;
266 try {
267 const originalEventHandler = wrappedEventHandler && wrappedEventHandler.__sentry_wrapped__;
268 if (originalEventHandler) {
269 originalRemoveEventListener.call(this, eventName, originalEventHandler, options);
270 }
271 } catch (e) {
272 // ignore, accessing __sentry_wrapped__ will throw in some Selenium environments
273 }
274 return originalRemoveEventListener.call(this, eventName, wrappedEventHandler, options);
275 };
276 },
277 );
278}
279
280export { TryCatch };
281//# sourceMappingURL=trycatch.js.map