trycatch.js 8.8 KB

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