breadcrumbs.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import { defineIntegration, convertIntegrationFnToClass, getClient, addBreadcrumb } from '@sentry/core';
  2. import { addConsoleInstrumentationHandler, addClickKeypressInstrumentationHandler, addXhrInstrumentationHandler, addFetchInstrumentationHandler, addHistoryInstrumentationHandler, getEventDescription, logger, htmlTreeAsString, getComponentName, severityLevelFromString, safeJoin, SENTRY_XHR_DATA_KEY, parseUrl } from '@sentry/utils';
  3. import { DEBUG_BUILD } from '../debug-build.js';
  4. import { WINDOW } from '../helpers.js';
  5. /* eslint-disable max-lines */
  6. /** maxStringLength gets capped to prevent 100 breadcrumbs exceeding 1MB event payload size */
  7. const MAX_ALLOWED_STRING_LENGTH = 1024;
  8. const INTEGRATION_NAME = 'Breadcrumbs';
  9. const _breadcrumbsIntegration = ((options = {}) => {
  10. const _options = {
  11. console: true,
  12. dom: true,
  13. fetch: true,
  14. history: true,
  15. sentry: true,
  16. xhr: true,
  17. ...options,
  18. };
  19. return {
  20. name: INTEGRATION_NAME,
  21. // TODO v8: Remove this
  22. setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function
  23. setup(client) {
  24. if (_options.console) {
  25. addConsoleInstrumentationHandler(_getConsoleBreadcrumbHandler(client));
  26. }
  27. if (_options.dom) {
  28. addClickKeypressInstrumentationHandler(_getDomBreadcrumbHandler(client, _options.dom));
  29. }
  30. if (_options.xhr) {
  31. addXhrInstrumentationHandler(_getXhrBreadcrumbHandler(client));
  32. }
  33. if (_options.fetch) {
  34. addFetchInstrumentationHandler(_getFetchBreadcrumbHandler(client));
  35. }
  36. if (_options.history) {
  37. addHistoryInstrumentationHandler(_getHistoryBreadcrumbHandler(client));
  38. }
  39. if (_options.sentry && client.on) {
  40. client.on('beforeSendEvent', _getSentryBreadcrumbHandler(client));
  41. }
  42. },
  43. };
  44. }) ;
  45. const breadcrumbsIntegration = defineIntegration(_breadcrumbsIntegration);
  46. /**
  47. * Default Breadcrumbs instrumentations
  48. *
  49. * @deprecated Use `breadcrumbsIntegration()` instead.
  50. */
  51. // eslint-disable-next-line deprecation/deprecation
  52. const Breadcrumbs = convertIntegrationFnToClass(INTEGRATION_NAME, breadcrumbsIntegration)
  53. ;
  54. /**
  55. * Adds a breadcrumb for Sentry events or transactions if this option is enabled.
  56. */
  57. function _getSentryBreadcrumbHandler(client) {
  58. return function addSentryBreadcrumb(event) {
  59. if (getClient() !== client) {
  60. return;
  61. }
  62. addBreadcrumb(
  63. {
  64. category: `sentry.${event.type === 'transaction' ? 'transaction' : 'event'}`,
  65. event_id: event.event_id,
  66. level: event.level,
  67. message: getEventDescription(event),
  68. },
  69. {
  70. event,
  71. },
  72. );
  73. };
  74. }
  75. /**
  76. * A HOC that creaes a function that creates breadcrumbs from DOM API calls.
  77. * This is a HOC so that we get access to dom options in the closure.
  78. */
  79. function _getDomBreadcrumbHandler(
  80. client,
  81. dom,
  82. ) {
  83. return function _innerDomBreadcrumb(handlerData) {
  84. if (getClient() !== client) {
  85. return;
  86. }
  87. let target;
  88. let componentName;
  89. let keyAttrs = typeof dom === 'object' ? dom.serializeAttribute : undefined;
  90. let maxStringLength =
  91. typeof dom === 'object' && typeof dom.maxStringLength === 'number' ? dom.maxStringLength : undefined;
  92. if (maxStringLength && maxStringLength > MAX_ALLOWED_STRING_LENGTH) {
  93. DEBUG_BUILD &&
  94. logger.warn(
  95. `\`dom.maxStringLength\` cannot exceed ${MAX_ALLOWED_STRING_LENGTH}, but a value of ${maxStringLength} was configured. Sentry will use ${MAX_ALLOWED_STRING_LENGTH} instead.`,
  96. );
  97. maxStringLength = MAX_ALLOWED_STRING_LENGTH;
  98. }
  99. if (typeof keyAttrs === 'string') {
  100. keyAttrs = [keyAttrs];
  101. }
  102. // Accessing event.target can throw (see getsentry/raven-js#838, #768)
  103. try {
  104. const event = handlerData.event ;
  105. const element = _isEvent(event) ? event.target : event;
  106. target = htmlTreeAsString(element, { keyAttrs, maxStringLength });
  107. componentName = getComponentName(element);
  108. } catch (e) {
  109. target = '<unknown>';
  110. }
  111. if (target.length === 0) {
  112. return;
  113. }
  114. const breadcrumb = {
  115. category: `ui.${handlerData.name}`,
  116. message: target,
  117. };
  118. if (componentName) {
  119. breadcrumb.data = { 'ui.component_name': componentName };
  120. }
  121. addBreadcrumb(breadcrumb, {
  122. event: handlerData.event,
  123. name: handlerData.name,
  124. global: handlerData.global,
  125. });
  126. };
  127. }
  128. /**
  129. * Creates breadcrumbs from console API calls
  130. */
  131. function _getConsoleBreadcrumbHandler(client) {
  132. return function _consoleBreadcrumb(handlerData) {
  133. if (getClient() !== client) {
  134. return;
  135. }
  136. const breadcrumb = {
  137. category: 'console',
  138. data: {
  139. arguments: handlerData.args,
  140. logger: 'console',
  141. },
  142. level: severityLevelFromString(handlerData.level),
  143. message: safeJoin(handlerData.args, ' '),
  144. };
  145. if (handlerData.level === 'assert') {
  146. if (handlerData.args[0] === false) {
  147. breadcrumb.message = `Assertion failed: ${safeJoin(handlerData.args.slice(1), ' ') || 'console.assert'}`;
  148. breadcrumb.data.arguments = handlerData.args.slice(1);
  149. } else {
  150. // Don't capture a breadcrumb for passed assertions
  151. return;
  152. }
  153. }
  154. addBreadcrumb(breadcrumb, {
  155. input: handlerData.args,
  156. level: handlerData.level,
  157. });
  158. };
  159. }
  160. /**
  161. * Creates breadcrumbs from XHR API calls
  162. */
  163. function _getXhrBreadcrumbHandler(client) {
  164. return function _xhrBreadcrumb(handlerData) {
  165. if (getClient() !== client) {
  166. return;
  167. }
  168. const { startTimestamp, endTimestamp } = handlerData;
  169. const sentryXhrData = handlerData.xhr[SENTRY_XHR_DATA_KEY];
  170. // We only capture complete, non-sentry requests
  171. if (!startTimestamp || !endTimestamp || !sentryXhrData) {
  172. return;
  173. }
  174. const { method, url, status_code, body } = sentryXhrData;
  175. const data = {
  176. method,
  177. url,
  178. status_code,
  179. };
  180. const hint = {
  181. xhr: handlerData.xhr,
  182. input: body,
  183. startTimestamp,
  184. endTimestamp,
  185. };
  186. addBreadcrumb(
  187. {
  188. category: 'xhr',
  189. data,
  190. type: 'http',
  191. },
  192. hint,
  193. );
  194. };
  195. }
  196. /**
  197. * Creates breadcrumbs from fetch API calls
  198. */
  199. function _getFetchBreadcrumbHandler(client) {
  200. return function _fetchBreadcrumb(handlerData) {
  201. if (getClient() !== client) {
  202. return;
  203. }
  204. const { startTimestamp, endTimestamp } = handlerData;
  205. // We only capture complete fetch requests
  206. if (!endTimestamp) {
  207. return;
  208. }
  209. if (handlerData.fetchData.url.match(/sentry_key/) && handlerData.fetchData.method === 'POST') {
  210. // We will not create breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests)
  211. return;
  212. }
  213. if (handlerData.error) {
  214. const data = handlerData.fetchData;
  215. const hint = {
  216. data: handlerData.error,
  217. input: handlerData.args,
  218. startTimestamp,
  219. endTimestamp,
  220. };
  221. addBreadcrumb(
  222. {
  223. category: 'fetch',
  224. data,
  225. level: 'error',
  226. type: 'http',
  227. },
  228. hint,
  229. );
  230. } else {
  231. const response = handlerData.response ;
  232. const data = {
  233. ...handlerData.fetchData,
  234. status_code: response && response.status,
  235. };
  236. const hint = {
  237. input: handlerData.args,
  238. response,
  239. startTimestamp,
  240. endTimestamp,
  241. };
  242. addBreadcrumb(
  243. {
  244. category: 'fetch',
  245. data,
  246. type: 'http',
  247. },
  248. hint,
  249. );
  250. }
  251. };
  252. }
  253. /**
  254. * Creates breadcrumbs from history API calls
  255. */
  256. function _getHistoryBreadcrumbHandler(client) {
  257. return function _historyBreadcrumb(handlerData) {
  258. if (getClient() !== client) {
  259. return;
  260. }
  261. let from = handlerData.from;
  262. let to = handlerData.to;
  263. const parsedLoc = parseUrl(WINDOW.location.href);
  264. let parsedFrom = from ? parseUrl(from) : undefined;
  265. const parsedTo = parseUrl(to);
  266. // Initial pushState doesn't provide `from` information
  267. if (!parsedFrom || !parsedFrom.path) {
  268. parsedFrom = parsedLoc;
  269. }
  270. // Use only the path component of the URL if the URL matches the current
  271. // document (almost all the time when using pushState)
  272. if (parsedLoc.protocol === parsedTo.protocol && parsedLoc.host === parsedTo.host) {
  273. to = parsedTo.relative;
  274. }
  275. if (parsedLoc.protocol === parsedFrom.protocol && parsedLoc.host === parsedFrom.host) {
  276. from = parsedFrom.relative;
  277. }
  278. addBreadcrumb({
  279. category: 'navigation',
  280. data: {
  281. from,
  282. to,
  283. },
  284. });
  285. };
  286. }
  287. function _isEvent(event) {
  288. return !!event && !!(event ).target;
  289. }
  290. export { Breadcrumbs, breadcrumbsIntegration };
  291. //# sourceMappingURL=breadcrumbs.js.map