xhr.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import { isString } from '../is.js';
  2. import { fill } from '../object.js';
  3. import { GLOBAL_OBJ } from '../worldwide.js';
  4. import { addHandler, maybeInstrument, triggerHandlers } from './_handlers.js';
  5. const WINDOW = GLOBAL_OBJ ;
  6. const SENTRY_XHR_DATA_KEY = '__sentry_xhr_v3__';
  7. /**
  8. * Add an instrumentation handler for when an XHR request happens.
  9. * The handler function is called once when the request starts and once when it ends,
  10. * which can be identified by checking if it has an `endTimestamp`.
  11. *
  12. * Use at your own risk, this might break without changelog notice, only used internally.
  13. * @hidden
  14. */
  15. function addXhrInstrumentationHandler(handler) {
  16. const type = 'xhr';
  17. addHandler(type, handler);
  18. maybeInstrument(type, instrumentXHR);
  19. }
  20. /** Exported only for tests. */
  21. function instrumentXHR() {
  22. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  23. if (!(WINDOW ).XMLHttpRequest) {
  24. return;
  25. }
  26. const xhrproto = XMLHttpRequest.prototype;
  27. fill(xhrproto, 'open', function (originalOpen) {
  28. return function ( ...args) {
  29. const startTimestamp = Date.now();
  30. // open() should always be called with two or more arguments
  31. // But to be on the safe side, we actually validate this and bail out if we don't have a method & url
  32. const method = isString(args[0]) ? args[0].toUpperCase() : undefined;
  33. const url = parseUrl(args[1]);
  34. if (!method || !url) {
  35. return originalOpen.apply(this, args);
  36. }
  37. this[SENTRY_XHR_DATA_KEY] = {
  38. method,
  39. url,
  40. request_headers: {},
  41. };
  42. // if Sentry key appears in URL, don't capture it as a request
  43. if (method === 'POST' && url.match(/sentry_key/)) {
  44. this.__sentry_own_request__ = true;
  45. }
  46. const onreadystatechangeHandler = () => {
  47. // For whatever reason, this is not the same instance here as from the outer method
  48. const xhrInfo = this[SENTRY_XHR_DATA_KEY];
  49. if (!xhrInfo) {
  50. return;
  51. }
  52. if (this.readyState === 4) {
  53. try {
  54. // touching statusCode in some platforms throws
  55. // an exception
  56. xhrInfo.status_code = this.status;
  57. } catch (e) {
  58. /* do nothing */
  59. }
  60. const handlerData = {
  61. args: [method, url],
  62. endTimestamp: Date.now(),
  63. startTimestamp,
  64. xhr: this,
  65. };
  66. triggerHandlers('xhr', handlerData);
  67. }
  68. };
  69. if ('onreadystatechange' in this && typeof this.onreadystatechange === 'function') {
  70. fill(this, 'onreadystatechange', function (original) {
  71. return function ( ...readyStateArgs) {
  72. onreadystatechangeHandler();
  73. return original.apply(this, readyStateArgs);
  74. };
  75. });
  76. } else {
  77. this.addEventListener('readystatechange', onreadystatechangeHandler);
  78. }
  79. // Intercepting `setRequestHeader` to access the request headers of XHR instance.
  80. // This will only work for user/library defined headers, not for the default/browser-assigned headers.
  81. // Request cookies are also unavailable for XHR, as `Cookie` header can't be defined by `setRequestHeader`.
  82. fill(this, 'setRequestHeader', function (original) {
  83. return function ( ...setRequestHeaderArgs) {
  84. const [header, value] = setRequestHeaderArgs;
  85. const xhrInfo = this[SENTRY_XHR_DATA_KEY];
  86. if (xhrInfo && isString(header) && isString(value)) {
  87. xhrInfo.request_headers[header.toLowerCase()] = value;
  88. }
  89. return original.apply(this, setRequestHeaderArgs);
  90. };
  91. });
  92. return originalOpen.apply(this, args);
  93. };
  94. });
  95. fill(xhrproto, 'send', function (originalSend) {
  96. return function ( ...args) {
  97. const sentryXhrData = this[SENTRY_XHR_DATA_KEY];
  98. if (!sentryXhrData) {
  99. return originalSend.apply(this, args);
  100. }
  101. if (args[0] !== undefined) {
  102. sentryXhrData.body = args[0];
  103. }
  104. const handlerData = {
  105. args: [sentryXhrData.method, sentryXhrData.url],
  106. startTimestamp: Date.now(),
  107. xhr: this,
  108. };
  109. triggerHandlers('xhr', handlerData);
  110. return originalSend.apply(this, args);
  111. };
  112. });
  113. }
  114. function parseUrl(url) {
  115. if (isString(url)) {
  116. return url;
  117. }
  118. try {
  119. // url can be a string or URL
  120. // but since URL is not available in IE11, we do not check for it,
  121. // but simply assume it is an URL and return `toString()` from it (which returns the full URL)
  122. // If that fails, we just return undefined
  123. return (url ).toString();
  124. } catch (e2) {} // eslint-disable-line no-empty
  125. return undefined;
  126. }
  127. export { SENTRY_XHR_DATA_KEY, addXhrInstrumentationHandler, instrumentXHR };
  128. //# sourceMappingURL=xhr.js.map