this repo has no description
at main 320 lines 8.4 kB view raw
1import { getCurrentHub } from '@sentry/core'; 2import { addInstrumentationHandler, getEventDescription, severityLevelFromString, safeJoin, SENTRY_XHR_DATA_KEY, parseUrl, logger, htmlTreeAsString } from '@sentry/utils'; 3import { WINDOW } from '../helpers.js'; 4 5/* eslint-disable @typescript-eslint/no-unsafe-member-access */ 6 7/** maxStringLength gets capped to prevent 100 breadcrumbs exceeding 1MB event payload size */ 8const MAX_ALLOWED_STRING_LENGTH = 1024; 9 10const BREADCRUMB_INTEGRATION_ID = 'Breadcrumbs'; 11 12/** 13 * Default Breadcrumbs instrumentations 14 * TODO: Deprecated - with v6, this will be renamed to `Instrument` 15 */ 16class Breadcrumbs { 17 /** 18 * @inheritDoc 19 */ 20 static __initStatic() {this.id = BREADCRUMB_INTEGRATION_ID;} 21 22 /** 23 * @inheritDoc 24 */ 25 __init() {this.name = Breadcrumbs.id;} 26 27 /** 28 * Options of the breadcrumbs integration. 29 */ 30 // This field is public, because we use it in the browser client to check if the `sentry` option is enabled. 31 32 /** 33 * @inheritDoc 34 */ 35 constructor(options) {Breadcrumbs.prototype.__init.call(this); 36 this.options = { 37 console: true, 38 dom: true, 39 fetch: true, 40 history: true, 41 sentry: true, 42 xhr: true, 43 ...options, 44 }; 45 } 46 47 /** 48 * Instrument browser built-ins w/ breadcrumb capturing 49 * - Console API 50 * - DOM API (click/typing) 51 * - XMLHttpRequest API 52 * - Fetch API 53 * - History API 54 */ 55 setupOnce() { 56 if (this.options.console) { 57 addInstrumentationHandler('console', _consoleBreadcrumb); 58 } 59 if (this.options.dom) { 60 addInstrumentationHandler('dom', _domBreadcrumb(this.options.dom)); 61 } 62 if (this.options.xhr) { 63 addInstrumentationHandler('xhr', _xhrBreadcrumb); 64 } 65 if (this.options.fetch) { 66 addInstrumentationHandler('fetch', _fetchBreadcrumb); 67 } 68 if (this.options.history) { 69 addInstrumentationHandler('history', _historyBreadcrumb); 70 } 71 } 72 73 /** 74 * Adds a breadcrumb for Sentry events or transactions if this option is enabled. 75 */ 76 addSentryBreadcrumb(event) { 77 if (this.options.sentry) { 78 getCurrentHub().addBreadcrumb( 79 { 80 category: `sentry.${event.type === 'transaction' ? 'transaction' : 'event'}`, 81 event_id: event.event_id, 82 level: event.level, 83 message: getEventDescription(event), 84 }, 85 { 86 event, 87 }, 88 ); 89 } 90 } 91} Breadcrumbs.__initStatic(); 92 93/** 94 * A HOC that creaes a function that creates breadcrumbs from DOM API calls. 95 * This is a HOC so that we get access to dom options in the closure. 96 */ 97function _domBreadcrumb(dom) { 98 function _innerDomBreadcrumb(handlerData) { 99 let target; 100 let keyAttrs = typeof dom === 'object' ? dom.serializeAttribute : undefined; 101 102 let maxStringLength = 103 typeof dom === 'object' && typeof dom.maxStringLength === 'number' ? dom.maxStringLength : undefined; 104 if (maxStringLength && maxStringLength > MAX_ALLOWED_STRING_LENGTH) { 105 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && 106 logger.warn( 107 `\`dom.maxStringLength\` cannot exceed ${MAX_ALLOWED_STRING_LENGTH}, but a value of ${maxStringLength} was configured. Sentry will use ${MAX_ALLOWED_STRING_LENGTH} instead.`, 108 ); 109 maxStringLength = MAX_ALLOWED_STRING_LENGTH; 110 } 111 112 if (typeof keyAttrs === 'string') { 113 keyAttrs = [keyAttrs]; 114 } 115 116 // Accessing event.target can throw (see getsentry/raven-js#838, #768) 117 try { 118 const event = handlerData.event ; 119 target = _isEvent(event) 120 ? htmlTreeAsString(event.target, { keyAttrs, maxStringLength }) 121 : htmlTreeAsString(event, { keyAttrs, maxStringLength }); 122 } catch (e) { 123 target = '<unknown>'; 124 } 125 126 if (target.length === 0) { 127 return; 128 } 129 130 getCurrentHub().addBreadcrumb( 131 { 132 category: `ui.${handlerData.name}`, 133 message: target, 134 }, 135 { 136 event: handlerData.event, 137 name: handlerData.name, 138 global: handlerData.global, 139 }, 140 ); 141 } 142 143 return _innerDomBreadcrumb; 144} 145 146/** 147 * Creates breadcrumbs from console API calls 148 */ 149function _consoleBreadcrumb(handlerData) { 150 // This is a hack to fix a Vue3-specific bug that causes an infinite loop of 151 // console warnings. This happens when a Vue template is rendered with 152 // an undeclared variable, which we try to stringify, ultimately causing 153 // Vue to issue another warning which repeats indefinitely. 154 // see: https://github.com/getsentry/sentry-javascript/pull/6010 155 // see: https://github.com/getsentry/sentry-javascript/issues/5916 156 for (let i = 0; i < handlerData.args.length; i++) { 157 if (handlerData.args[i] === 'ref=Ref<') { 158 handlerData.args[i + 1] = 'viewRef'; 159 break; 160 } 161 } 162 const breadcrumb = { 163 category: 'console', 164 data: { 165 arguments: handlerData.args, 166 logger: 'console', 167 }, 168 level: severityLevelFromString(handlerData.level), 169 message: safeJoin(handlerData.args, ' '), 170 }; 171 172 if (handlerData.level === 'assert') { 173 if (handlerData.args[0] === false) { 174 breadcrumb.message = `Assertion failed: ${safeJoin(handlerData.args.slice(1), ' ') || 'console.assert'}`; 175 breadcrumb.data.arguments = handlerData.args.slice(1); 176 } else { 177 // Don't capture a breadcrumb for passed assertions 178 return; 179 } 180 } 181 182 getCurrentHub().addBreadcrumb(breadcrumb, { 183 input: handlerData.args, 184 level: handlerData.level, 185 }); 186} 187 188/** 189 * Creates breadcrumbs from XHR API calls 190 */ 191function _xhrBreadcrumb(handlerData) { 192 const { startTimestamp, endTimestamp } = handlerData; 193 194 const sentryXhrData = handlerData.xhr[SENTRY_XHR_DATA_KEY]; 195 196 // We only capture complete, non-sentry requests 197 if (!startTimestamp || !endTimestamp || !sentryXhrData) { 198 return; 199 } 200 201 const { method, url, status_code, body } = sentryXhrData; 202 203 const data = { 204 method, 205 url, 206 status_code, 207 }; 208 209 const hint = { 210 xhr: handlerData.xhr, 211 input: body, 212 startTimestamp, 213 endTimestamp, 214 }; 215 216 getCurrentHub().addBreadcrumb( 217 { 218 category: 'xhr', 219 data, 220 type: 'http', 221 }, 222 hint, 223 ); 224} 225 226/** 227 * Creates breadcrumbs from fetch API calls 228 */ 229function _fetchBreadcrumb(handlerData) { 230 const { startTimestamp, endTimestamp } = handlerData; 231 232 // We only capture complete fetch requests 233 if (!endTimestamp) { 234 return; 235 } 236 237 if (handlerData.fetchData.url.match(/sentry_key/) && handlerData.fetchData.method === 'POST') { 238 // We will not create breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests) 239 return; 240 } 241 242 if (handlerData.error) { 243 const data = handlerData.fetchData; 244 const hint = { 245 data: handlerData.error, 246 input: handlerData.args, 247 startTimestamp, 248 endTimestamp, 249 }; 250 251 getCurrentHub().addBreadcrumb( 252 { 253 category: 'fetch', 254 data, 255 level: 'error', 256 type: 'http', 257 }, 258 hint, 259 ); 260 } else { 261 const data = { 262 ...handlerData.fetchData, 263 status_code: handlerData.response && handlerData.response.status, 264 }; 265 const hint = { 266 input: handlerData.args, 267 response: handlerData.response, 268 startTimestamp, 269 endTimestamp, 270 }; 271 getCurrentHub().addBreadcrumb( 272 { 273 category: 'fetch', 274 data, 275 type: 'http', 276 }, 277 hint, 278 ); 279 } 280} 281 282/** 283 * Creates breadcrumbs from history API calls 284 */ 285function _historyBreadcrumb(handlerData) { 286 let from = handlerData.from; 287 let to = handlerData.to; 288 const parsedLoc = parseUrl(WINDOW.location.href); 289 let parsedFrom = parseUrl(from); 290 const parsedTo = parseUrl(to); 291 292 // Initial pushState doesn't provide `from` information 293 if (!parsedFrom.path) { 294 parsedFrom = parsedLoc; 295 } 296 297 // Use only the path component of the URL if the URL matches the current 298 // document (almost all the time when using pushState) 299 if (parsedLoc.protocol === parsedTo.protocol && parsedLoc.host === parsedTo.host) { 300 to = parsedTo.relative; 301 } 302 if (parsedLoc.protocol === parsedFrom.protocol && parsedLoc.host === parsedFrom.host) { 303 from = parsedFrom.relative; 304 } 305 306 getCurrentHub().addBreadcrumb({ 307 category: 'navigation', 308 data: { 309 from, 310 to, 311 }, 312 }); 313} 314 315function _isEvent(event) { 316 return !!event && !!(event ).target; 317} 318 319export { BREADCRUMB_INTEGRATION_ID, Breadcrumbs }; 320//# sourceMappingURL=breadcrumbs.js.map