browser.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { isString } from './is.js';
  2. import { getGlobalObject } from './worldwide.js';
  3. // eslint-disable-next-line deprecation/deprecation
  4. const WINDOW = getGlobalObject();
  5. const DEFAULT_MAX_STRING_LENGTH = 80;
  6. /**
  7. * Given a child DOM element, returns a query-selector statement describing that
  8. * and its ancestors
  9. * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz]
  10. * @returns generated DOM path
  11. */
  12. function htmlTreeAsString(
  13. elem,
  14. options = {},
  15. ) {
  16. if (!elem) {
  17. return '<unknown>';
  18. }
  19. // try/catch both:
  20. // - accessing event.target (see getsentry/raven-js#838, #768)
  21. // - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly
  22. // - can throw an exception in some circumstances.
  23. try {
  24. let currentElem = elem ;
  25. const MAX_TRAVERSE_HEIGHT = 5;
  26. const out = [];
  27. let height = 0;
  28. let len = 0;
  29. const separator = ' > ';
  30. const sepLength = separator.length;
  31. let nextStr;
  32. const keyAttrs = Array.isArray(options) ? options : options.keyAttrs;
  33. const maxStringLength = (!Array.isArray(options) && options.maxStringLength) || DEFAULT_MAX_STRING_LENGTH;
  34. while (currentElem && height++ < MAX_TRAVERSE_HEIGHT) {
  35. nextStr = _htmlElementAsString(currentElem, keyAttrs);
  36. // bail out if
  37. // - nextStr is the 'html' element
  38. // - the length of the string that would be created exceeds maxStringLength
  39. // (ignore this limit if we are on the first iteration)
  40. if (nextStr === 'html' || (height > 1 && len + out.length * sepLength + nextStr.length >= maxStringLength)) {
  41. break;
  42. }
  43. out.push(nextStr);
  44. len += nextStr.length;
  45. currentElem = currentElem.parentNode;
  46. }
  47. return out.reverse().join(separator);
  48. } catch (_oO) {
  49. return '<unknown>';
  50. }
  51. }
  52. /**
  53. * Returns a simple, query-selector representation of a DOM element
  54. * e.g. [HTMLElement] => input#foo.btn[name=baz]
  55. * @returns generated DOM path
  56. */
  57. function _htmlElementAsString(el, keyAttrs) {
  58. const elem = el
  59. ;
  60. const out = [];
  61. let className;
  62. let classes;
  63. let key;
  64. let attr;
  65. let i;
  66. if (!elem || !elem.tagName) {
  67. return '';
  68. }
  69. // @ts-expect-error WINDOW has HTMLElement
  70. if (WINDOW.HTMLElement) {
  71. // If using the component name annotation plugin, this value may be available on the DOM node
  72. if (elem instanceof HTMLElement && elem.dataset && elem.dataset['sentryComponent']) {
  73. return elem.dataset['sentryComponent'];
  74. }
  75. }
  76. out.push(elem.tagName.toLowerCase());
  77. // Pairs of attribute keys defined in `serializeAttribute` and their values on element.
  78. const keyAttrPairs =
  79. keyAttrs && keyAttrs.length
  80. ? keyAttrs.filter(keyAttr => elem.getAttribute(keyAttr)).map(keyAttr => [keyAttr, elem.getAttribute(keyAttr)])
  81. : null;
  82. if (keyAttrPairs && keyAttrPairs.length) {
  83. keyAttrPairs.forEach(keyAttrPair => {
  84. out.push(`[${keyAttrPair[0]}="${keyAttrPair[1]}"]`);
  85. });
  86. } else {
  87. if (elem.id) {
  88. out.push(`#${elem.id}`);
  89. }
  90. // eslint-disable-next-line prefer-const
  91. className = elem.className;
  92. if (className && isString(className)) {
  93. classes = className.split(/\s+/);
  94. for (i = 0; i < classes.length; i++) {
  95. out.push(`.${classes[i]}`);
  96. }
  97. }
  98. }
  99. const allowedAttrs = ['aria-label', 'type', 'name', 'title', 'alt'];
  100. for (i = 0; i < allowedAttrs.length; i++) {
  101. key = allowedAttrs[i];
  102. attr = elem.getAttribute(key);
  103. if (attr) {
  104. out.push(`[${key}="${attr}"]`);
  105. }
  106. }
  107. return out.join('');
  108. }
  109. /**
  110. * A safe form of location.href
  111. */
  112. function getLocationHref() {
  113. try {
  114. return WINDOW.document.location.href;
  115. } catch (oO) {
  116. return '';
  117. }
  118. }
  119. /**
  120. * Gets a DOM element by using document.querySelector.
  121. *
  122. * This wrapper will first check for the existance of the function before
  123. * actually calling it so that we don't have to take care of this check,
  124. * every time we want to access the DOM.
  125. *
  126. * Reason: DOM/querySelector is not available in all environments.
  127. *
  128. * We have to cast to any because utils can be consumed by a variety of environments,
  129. * and we don't want to break TS users. If you know what element will be selected by
  130. * `document.querySelector`, specify it as part of the generic call. For example,
  131. * `const element = getDomElement<Element>('selector');`
  132. *
  133. * @param selector the selector string passed on to document.querySelector
  134. */
  135. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  136. function getDomElement(selector) {
  137. if (WINDOW.document && WINDOW.document.querySelector) {
  138. return WINDOW.document.querySelector(selector) ;
  139. }
  140. return null;
  141. }
  142. /**
  143. * Given a DOM element, traverses up the tree until it finds the first ancestor node
  144. * that has the `data-sentry-component` attribute. This attribute is added at build-time
  145. * by projects that have the component name annotation plugin installed.
  146. *
  147. * @returns a string representation of the component for the provided DOM element, or `null` if not found
  148. */
  149. function getComponentName(elem) {
  150. // @ts-expect-error WINDOW has HTMLElement
  151. if (!WINDOW.HTMLElement) {
  152. return null;
  153. }
  154. let currentElem = elem ;
  155. const MAX_TRAVERSE_HEIGHT = 5;
  156. for (let i = 0; i < MAX_TRAVERSE_HEIGHT; i++) {
  157. if (!currentElem) {
  158. return null;
  159. }
  160. if (currentElem instanceof HTMLElement && currentElem.dataset['sentryComponent']) {
  161. return currentElem.dataset['sentryComponent'];
  162. }
  163. currentElem = currentElem.parentNode;
  164. }
  165. return null;
  166. }
  167. export { getComponentName, getDomElement, getLocationHref, htmlTreeAsString };
  168. //# sourceMappingURL=browser.js.map