index.js
1 import { getCurrentHub, addGlobalEventProcessor, prepareEvent, setContext, captureException } from '@sentry/core'; 2 import { GLOBAL_OBJ, normalize, fill, htmlTreeAsString, logger, uuid4, SENTRY_XHR_DATA_KEY, dropUndefinedKeys, stringMatchesSomePattern, addInstrumentationHandler, browserPerformanceTimeOrigin, createEnvelope, createEventEnvelopeHeaders, getSdkMetadataForEnvelopeHeader, isNodeEnv } from '@sentry/utils'; 3 4 // exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser` 5 // prevents the browser package from being bundled in the CDN bundle, and avoids a 6 // circular dependency between the browser and replay packages should `@sentry/browser` import 7 // from `@sentry/replay` in the future 8 const WINDOW = GLOBAL_OBJ ; 9 10 const REPLAY_SESSION_KEY = 'sentryReplaySession'; 11 const REPLAY_EVENT_NAME = 'replay_event'; 12 const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay'; 13 14 // The idle limit for a session after which recording is paused. 15 const SESSION_IDLE_PAUSE_DURATION = 300000; // 5 minutes in ms 16 17 // The idle limit for a session after which the session expires. 18 const SESSION_IDLE_EXPIRE_DURATION = 900000; // 15 minutes in ms 19 20 // The maximum length of a session 21 const MAX_SESSION_LIFE = 3600000; // 60 minutes in ms 22 23 /** Default flush delays */ 24 const DEFAULT_FLUSH_MIN_DELAY = 5000; 25 // XXX: Temp fix for our debounce logic where `maxWait` would never occur if it 26 // was the same as `wait` 27 const DEFAULT_FLUSH_MAX_DELAY = 5500; 28 29 /* How long to wait for error checkouts */ 30 const BUFFER_CHECKOUT_TIME = 60000; 31 32 const RETRY_BASE_INTERVAL = 5000; 33 const RETRY_MAX_COUNT = 3; 34 35 /* The max (uncompressed) size in bytes of a network body. Any body larger than this will be truncated. */ 36 const NETWORK_BODY_MAX_SIZE = 150000; 37 38 /* The max size of a single console arg that is captured. Any arg larger than this will be truncated. */ 39 const CONSOLE_ARG_MAX_SIZE = 5000; 40 41 /* Min. time to wait before we consider something a slow click. */ 42 const SLOW_CLICK_THRESHOLD = 3000; 43 /* For scroll actions after a click, we only look for a very short time period to detect programmatic scrolling. */ 44 const SLOW_CLICK_SCROLL_TIMEOUT = 300; 45 /* Clicks in this time period are considered e.g. double/triple clicks. */ 46 const MULTI_CLICK_TIMEOUT = 1000; 47 48 /** When encountering a total segment size exceeding this size, stop the replay (as we cannot properly ingest it). */ 49 const REPLAY_MAX_EVENT_BUFFER_SIZE = 20000000; // ~20MB 50 51 var NodeType$1; 52 (function (NodeType) { 53 NodeType[NodeType["Document"] = 0] = "Document"; 54 NodeType[NodeType["DocumentType"] = 1] = "DocumentType"; 55 NodeType[NodeType["Element"] = 2] = "Element"; 56 NodeType[NodeType["Text"] = 3] = "Text"; 57 NodeType[NodeType["CDATA"] = 4] = "CDATA"; 58 NodeType[NodeType["Comment"] = 5] = "Comment"; 59 })(NodeType$1 || (NodeType$1 = {})); 60 61 function isElement(n) { 62 return n.nodeType === n.ELEMENT_NODE; 63 } 64 function isShadowRoot(n) { 65 const host = n === null || n === void 0 ? void 0 : n.host; 66 return Boolean(host && host.shadowRoot && host.shadowRoot === n); 67 } 68 function isInputTypeMasked({ maskInputOptions, tagName, type, }) { 69 if (tagName.toLowerCase() === 'option') { 70 tagName = 'select'; 71 } 72 const actualType = typeof type === 'string' ? type.toLowerCase() : undefined; 73 return (maskInputOptions[tagName.toLowerCase()] || 74 (actualType && maskInputOptions[actualType]) || 75 actualType === 'password' || 76 (tagName === 'input' && !type && maskInputOptions['text'])); 77 } 78 function hasInputMaskOptions({ tagName, type, maskInputOptions, maskInputSelector, }) { 79 return (maskInputSelector || isInputTypeMasked({ maskInputOptions, tagName, type })); 80 } 81 function maskInputValue({ input, maskInputSelector, unmaskInputSelector, maskInputOptions, tagName, type, value, maskInputFn, }) { 82 let text = value || ''; 83 if (unmaskInputSelector && input.matches(unmaskInputSelector)) { 84 return text; 85 } 86 if (input.hasAttribute('data-rr-is-password')) { 87 type = 'password'; 88 } 89 if (isInputTypeMasked({ maskInputOptions, tagName, type }) || 90 (maskInputSelector && input.matches(maskInputSelector))) { 91 if (maskInputFn) { 92 text = maskInputFn(text); 93 } 94 else { 95 text = '*'.repeat(text.length); 96 } 97 } 98 return text; 99 } 100 const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; 101 function is2DCanvasBlank(canvas) { 102 const ctx = canvas.getContext('2d'); 103 if (!ctx) 104 return true; 105 const chunkSize = 50; 106 for (let x = 0; x < canvas.width; x += chunkSize) { 107 for (let y = 0; y < canvas.height; y += chunkSize) { 108 const getImageData = ctx.getImageData; 109 const originalGetImageData = ORIGINAL_ATTRIBUTE_NAME in getImageData 110 ? getImageData[ORIGINAL_ATTRIBUTE_NAME] 111 : getImageData; 112 const pixelBuffer = new Uint32Array(originalGetImageData.call(ctx, x, y, Math.min(chunkSize, canvas.width - x), Math.min(chunkSize, canvas.height - y)).data.buffer); 113 if (pixelBuffer.some((pixel) => pixel !== 0)) 114 return false; 115 } 116 } 117 return true; 118 } 119 function getInputType(element) { 120 const type = element.type; 121 return element.hasAttribute('data-rr-is-password') 122 ? 'password' 123 : type 124 ? type.toLowerCase() 125 : null; 126 } 127 function getInputValue(el, tagName, type) { 128 typeof type === 'string' ? type.toLowerCase() : ''; 129 if (tagName === 'INPUT' && (type === 'radio' || type === 'checkbox')) { 130 return el.getAttribute('value') || ''; 131 } 132 return el.value; 133 } 134 135 let _id = 1; 136 const tagNameRegex = new RegExp('[^a-z0-9-_:]'); 137 const IGNORED_NODE = -2; 138 function defaultMaskFn(str) { 139 return str ? str.replace(/[\S]/g, '*') : ''; 140 } 141 function genId() { 142 return _id++; 143 } 144 function getValidTagName(element) { 145 if (element instanceof HTMLFormElement) { 146 return 'form'; 147 } 148 const processedTagName = element.tagName.toLowerCase().trim(); 149 if (tagNameRegex.test(processedTagName)) { 150 return 'div'; 151 } 152 return processedTagName; 153 } 154 function getCssRulesString(s) { 155 try { 156 const rules = s.rules || s.cssRules; 157 return rules ? Array.from(rules).map(getCssRuleString).join('') : null; 158 } 159 catch (error) { 160 return null; 161 } 162 } 163 function getCssRuleString(rule) { 164 let cssStringified = rule.cssText; 165 if (isCSSImportRule(rule)) { 166 try { 167 cssStringified = getCssRulesString(rule.styleSheet) || cssStringified; 168 } 169 catch (_a) { 170 } 171 } 172 return validateStringifiedCssRule(cssStringified); 173 } 174 function validateStringifiedCssRule(cssStringified) { 175 if (cssStringified.indexOf(':') > -1) { 176 const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm; 177 return cssStringified.replace(regex, '$1\\$2'); 178 } 179 return cssStringified; 180 } 181 function isCSSImportRule(rule) { 182 return 'styleSheet' in rule; 183 } 184 function stringifyStyleSheet(sheet) { 185 return sheet.cssRules 186 ? Array.from(sheet.cssRules) 187 .map((rule) => rule.cssText ? validateStringifiedCssRule(rule.cssText) : '') 188 .join('') 189 : ''; 190 } 191 function extractOrigin(url) { 192 let origin = ''; 193 if (url.indexOf('//') > -1) { 194 origin = url.split('/').slice(0, 3).join('/'); 195 } 196 else { 197 origin = url.split('/')[0]; 198 } 199 origin = origin.split('?')[0]; 200 return origin; 201 } 202 let canvasService; 203 let canvasCtx; 204 const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; 205 const RELATIVE_PATH = /^(?!www\.|(?:http|ftp)s?:\/\/|[A-Za-z]:\\|\/\/|#).*/; 206 const DATA_URI = /^(data:)([^,]*),(.*)/i; 207 function absoluteToStylesheet(cssText, href) { 208 return (cssText || '').replace(URL_IN_CSS_REF, (origin, quote1, path1, quote2, path2, path3) => { 209 const filePath = path1 || path2 || path3; 210 const maybeQuote = quote1 || quote2 || ''; 211 if (!filePath) { 212 return origin; 213 } 214 if (!RELATIVE_PATH.test(filePath)) { 215 return `url(${maybeQuote}${filePath}${maybeQuote})`; 216 } 217 if (DATA_URI.test(filePath)) { 218 return `url(${maybeQuote}${filePath}${maybeQuote})`; 219 } 220 if (filePath[0] === '/') { 221 return `url(${maybeQuote}${extractOrigin(href) + filePath}${maybeQuote})`; 222 } 223 const stack = href.split('/'); 224 const parts = filePath.split('/'); 225 stack.pop(); 226 for (const part of parts) { 227 if (part === '.') { 228 continue; 229 } 230 else if (part === '..') { 231 stack.pop(); 232 } 233 else { 234 stack.push(part); 235 } 236 } 237 return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; 238 }); 239 } 240 const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; 241 const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/; 242 function getAbsoluteSrcsetString(doc, attributeValue) { 243 if (attributeValue.trim() === '') { 244 return attributeValue; 245 } 246 let pos = 0; 247 function collectCharacters(regEx) { 248 let chars; 249 let match = regEx.exec(attributeValue.substring(pos)); 250 if (match) { 251 chars = match[0]; 252 pos += chars.length; 253 return chars; 254 } 255 return ''; 256 } 257 let output = []; 258 while (true) { 259 collectCharacters(SRCSET_COMMAS_OR_SPACES); 260 if (pos >= attributeValue.length) { 261 break; 262 } 263 let url = collectCharacters(SRCSET_NOT_SPACES); 264 if (url.slice(-1) === ',') { 265 url = absoluteToDoc(doc, url.substring(0, url.length - 1)); 266 output.push(url); 267 } 268 else { 269 let descriptorsStr = ''; 270 url = absoluteToDoc(doc, url); 271 let inParens = false; 272 while (true) { 273 let c = attributeValue.charAt(pos); 274 if (c === '') { 275 output.push((url + descriptorsStr).trim()); 276 break; 277 } 278 else if (!inParens) { 279 if (c === ',') { 280 pos += 1; 281 output.push((url + descriptorsStr).trim()); 282 break; 283 } 284 else if (c === '(') { 285 inParens = true; 286 } 287 } 288 else { 289 if (c === ')') { 290 inParens = false; 291 } 292 } 293 descriptorsStr += c; 294 pos += 1; 295 } 296 } 297 } 298 return output.join(', '); 299 } 300 function absoluteToDoc(doc, attributeValue) { 301 if (!attributeValue || attributeValue.trim() === '') { 302 return attributeValue; 303 } 304 const a = doc.createElement('a'); 305 a.href = attributeValue; 306 return a.href; 307 } 308 function isSVGElement(el) { 309 return Boolean(el.tagName === 'svg' || el.ownerSVGElement); 310 } 311 function getHref() { 312 const a = document.createElement('a'); 313 a.href = ''; 314 return a.href; 315 } 316 function transformAttribute(doc, element, _tagName, _name, value, maskAllText, unmaskTextSelector, maskTextFn) { 317 if (!value) { 318 return value; 319 } 320 const name = _name.toLowerCase(); 321 const tagName = _tagName.toLowerCase(); 322 if (name === 'src' || name === 'href') { 323 return absoluteToDoc(doc, value); 324 } 325 else if (name === 'xlink:href' && value[0] !== '#') { 326 return absoluteToDoc(doc, value); 327 } 328 else if (name === 'background' && 329 (tagName === 'table' || tagName === 'td' || tagName === 'th')) { 330 return absoluteToDoc(doc, value); 331 } 332 else if (name === 'srcset') { 333 return getAbsoluteSrcsetString(doc, value); 334 } 335 else if (name === 'style') { 336 return absoluteToStylesheet(value, getHref()); 337 } 338 else if (tagName === 'object' && name === 'data') { 339 return absoluteToDoc(doc, value); 340 } 341 else if (maskAllText && 342 _shouldMaskAttribute(element, name, tagName, unmaskTextSelector)) { 343 return maskTextFn ? maskTextFn(value) : defaultMaskFn(value); 344 } 345 return value; 346 } 347 function _shouldMaskAttribute(element, attribute, tagName, unmaskTextSelector) { 348 if (unmaskTextSelector && element.matches(unmaskTextSelector)) { 349 return false; 350 } 351 return (['placeholder', 'title', 'aria-label'].indexOf(attribute) > -1 || 352 (tagName === 'input' && 353 attribute === 'value' && 354 element.hasAttribute('type') && 355 ['submit', 'button'].indexOf(element.getAttribute('type').toLowerCase()) > -1)); 356 } 357 function _isBlockedElement(element, blockClass, blockSelector, unblockSelector) { 358 if (unblockSelector && element.matches(unblockSelector)) { 359 return false; 360 } 361 if (typeof blockClass === 'string') { 362 if (element.classList.contains(blockClass)) { 363 return true; 364 } 365 } 366 else { 367 for (let eIndex = 0; eIndex < element.classList.length; eIndex++) { 368 const className = element.classList[eIndex]; 369 if (blockClass.test(className)) { 370 return true; 371 } 372 } 373 } 374 if (blockSelector) { 375 return element.matches(blockSelector); 376 } 377 return false; 378 } 379 function needMaskingText(node, maskTextClass, maskTextSelector, unmaskTextSelector, maskAllText) { 380 if (!node) { 381 return false; 382 } 383 if (node.nodeType !== node.ELEMENT_NODE) { 384 return needMaskingText(node.parentNode, maskTextClass, maskTextSelector, unmaskTextSelector, maskAllText); 385 } 386 if (unmaskTextSelector) { 387 if (node.matches(unmaskTextSelector) || 388 node.closest(unmaskTextSelector)) { 389 return false; 390 } 391 } 392 if (maskAllText) { 393 return true; 394 } 395 if (typeof maskTextClass === 'string') { 396 if (node.classList.contains(maskTextClass)) { 397 return true; 398 } 399 } 400 else { 401 for (let eIndex = 0; eIndex < node.classList.length; eIndex++) { 402 const className = node.classList[eIndex]; 403 if (maskTextClass.test(className)) { 404 return true; 405 } 406 } 407 } 408 if (maskTextSelector) { 409 if (node.matches(maskTextSelector)) { 410 return true; 411 } 412 } 413 return needMaskingText(node.parentNode, maskTextClass, maskTextSelector, unmaskTextSelector, maskAllText); 414 } 415 function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) { 416 const win = iframeEl.contentWindow; 417 if (!win) { 418 return; 419 } 420 let fired = false; 421 let readyState; 422 try { 423 readyState = win.document.readyState; 424 } 425 catch (error) { 426 return; 427 } 428 if (readyState !== 'complete') { 429 const timer = setTimeout(() => { 430 if (!fired) { 431 listener(); 432 fired = true; 433 } 434 }, iframeLoadTimeout); 435 iframeEl.addEventListener('load', () => { 436 clearTimeout(timer); 437 fired = true; 438 listener(); 439 }); 440 return; 441 } 442 const blankUrl = 'about:blank'; 443 if (win.location.href !== blankUrl || 444 iframeEl.src === blankUrl || 445 iframeEl.src === '') { 446 setTimeout(listener, 0); 447 return; 448 } 449 iframeEl.addEventListener('load', listener); 450 } 451 function serializeNode(n, options) { 452 var _a; 453 const { doc, blockClass, blockSelector, unblockSelector, maskTextClass, maskTextSelector, unmaskTextSelector, inlineStylesheet, maskInputSelector, unmaskInputSelector, maskAllText, maskInputOptions = {}, maskTextFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, } = options; 454 let rootId; 455 if (doc.__sn) { 456 const docId = doc.__sn.id; 457 rootId = docId === 1 ? undefined : docId; 458 } 459 switch (n.nodeType) { 460 case n.DOCUMENT_NODE: 461 if (n.compatMode !== 'CSS1Compat') { 462 return { 463 type: NodeType$1.Document, 464 childNodes: [], 465 compatMode: n.compatMode, 466 rootId, 467 }; 468 } 469 else { 470 return { 471 type: NodeType$1.Document, 472 childNodes: [], 473 rootId, 474 }; 475 } 476 case n.DOCUMENT_TYPE_NODE: 477 return { 478 type: NodeType$1.DocumentType, 479 name: n.name, 480 publicId: n.publicId, 481 systemId: n.systemId, 482 rootId, 483 }; 484 case n.ELEMENT_NODE: 485 const needBlock = _isBlockedElement(n, blockClass, blockSelector, unblockSelector); 486 const tagName = getValidTagName(n); 487 let attributes = {}; 488 for (const { name, value } of Array.from(n.attributes)) { 489 if (!skipAttribute(tagName, name)) { 490 attributes[name] = transformAttribute(doc, n, tagName, name, value, maskAllText, unmaskTextSelector, maskTextFn); 491 } 492 } 493 if (tagName === 'link' && inlineStylesheet) { 494 const stylesheet = Array.from(doc.styleSheets).find((s) => { 495 return s.href === n.href; 496 }); 497 let cssText = null; 498 if (stylesheet) { 499 cssText = getCssRulesString(stylesheet); 500 } 501 if (cssText) { 502 delete attributes.rel; 503 delete attributes.href; 504 attributes._cssText = absoluteToStylesheet(cssText, stylesheet.href); 505 } 506 } 507 if (tagName === 'style' && 508 n.sheet && 509 !(n.innerText || 510 n.textContent || 511 '').trim().length) { 512 const cssText = getCssRulesString(n.sheet); 513 if (cssText) { 514 attributes._cssText = absoluteToStylesheet(cssText, getHref()); 515 } 516 } 517 if (tagName === 'input' || 518 tagName === 'textarea' || 519 tagName === 'select' || 520 tagName === 'option') { 521 const el = n; 522 const type = getInputType(el); 523 const value = getInputValue(el, tagName.toUpperCase(), type); 524 const checked = n.checked; 525 if (type !== 'submit' && 526 type !== 'button' && 527 value) { 528 attributes.value = maskInputValue({ 529 input: el, 530 type, 531 tagName, 532 value, 533 maskInputSelector, 534 unmaskInputSelector, 535 maskInputOptions, 536 maskInputFn, 537 }); 538 } 539 if (checked) { 540 attributes.checked = checked; 541 } 542 } 543 if (tagName === 'option') { 544 if (n.selected && !maskInputOptions['select']) { 545 attributes.selected = true; 546 } 547 else { 548 delete attributes.selected; 549 } 550 } 551 if (tagName === 'canvas' && recordCanvas) { 552 if (n.__context === '2d') { 553 if (!is2DCanvasBlank(n)) { 554 attributes.rr_dataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); 555 } 556 } 557 else if (!('__context' in n)) { 558 const canvasDataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); 559 const blankCanvas = document.createElement('canvas'); 560 blankCanvas.width = n.width; 561 blankCanvas.height = n.height; 562 const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality); 563 if (canvasDataURL !== blankCanvasDataURL) { 564 attributes.rr_dataURL = canvasDataURL; 565 } 566 } 567 } 568 if (tagName === 'img' && inlineImages) { 569 if (!canvasService) { 570 canvasService = doc.createElement('canvas'); 571 canvasCtx = canvasService.getContext('2d'); 572 } 573 const image = n; 574 const oldValue = image.crossOrigin; 575 image.crossOrigin = 'anonymous'; 576 const recordInlineImage = () => { 577 try { 578 canvasService.width = image.naturalWidth; 579 canvasService.height = image.naturalHeight; 580 canvasCtx.drawImage(image, 0, 0); 581 attributes.rr_dataURL = canvasService.toDataURL(dataURLOptions.type, dataURLOptions.quality); 582 } 583 catch (err) { 584 console.warn(`Cannot inline img src=${image.currentSrc}! Error: ${err}`); 585 } 586 oldValue 587 ? (attributes.crossOrigin = oldValue) 588 : delete attributes.crossOrigin; 589 }; 590 if (image.complete && image.naturalWidth !== 0) 591 recordInlineImage(); 592 else 593 image.onload = recordInlineImage; 594 } 595 if (tagName === 'audio' || tagName === 'video') { 596 attributes.rr_mediaState = n.paused 597 ? 'paused' 598 : 'played'; 599 attributes.rr_mediaCurrentTime = n.currentTime; 600 } 601 if (n.scrollLeft) { 602 attributes.rr_scrollLeft = n.scrollLeft; 603 } 604 if (n.scrollTop) { 605 attributes.rr_scrollTop = n.scrollTop; 606 } 607 if (needBlock) { 608 const { width, height } = n.getBoundingClientRect(); 609 attributes = { 610 class: attributes.class, 611 rr_width: `${width}px`, 612 rr_height: `${height}px`, 613 }; 614 } 615 if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src)) { 616 if (!n.contentDocument) { 617 attributes.rr_src = attributes.src; 618 } 619 delete attributes.src; 620 } 621 return { 622 type: NodeType$1.Element, 623 tagName, 624 attributes, 625 childNodes: [], 626 isSVG: isSVGElement(n) || undefined, 627 needBlock, 628 rootId, 629 }; 630 case n.TEXT_NODE: 631 const parentTagName = n.parentNode && n.parentNode.tagName; 632 let textContent = n.textContent; 633 const isStyle = parentTagName === 'STYLE' ? true : undefined; 634 const isScript = parentTagName === 'SCRIPT' ? true : undefined; 635 if (isStyle && textContent) { 636 try { 637 if (n.nextSibling || n.previousSibling) { 638 } 639 else if ((_a = n.parentNode.sheet) === null || _a === void 0 ? void 0 : _a.cssRules) { 640 textContent = stringifyStyleSheet(n.parentNode.sheet); 641 } 642 } 643 catch (err) { 644 console.warn(`Cannot get CSS styles from text's parentNode. Error: ${err}`, n); 645 } 646 textContent = absoluteToStylesheet(textContent, getHref()); 647 } 648 if (isScript) { 649 textContent = 'SCRIPT_PLACEHOLDER'; 650 } 651 if (parentTagName === 'TEXTAREA' && textContent) { 652 textContent = ''; 653 } 654 else if (parentTagName === 'OPTION' && textContent) { 655 const option = n.parentNode; 656 textContent = maskInputValue({ 657 input: option, 658 type: null, 659 tagName: parentTagName, 660 value: textContent, 661 maskInputSelector, 662 unmaskInputSelector, 663 maskInputOptions, 664 maskInputFn, 665 }); 666 } 667 else if (!isStyle && 668 !isScript && 669 needMaskingText(n, maskTextClass, maskTextSelector, unmaskTextSelector, maskAllText) && 670 textContent) { 671 textContent = maskTextFn 672 ? maskTextFn(textContent) 673 : defaultMaskFn(textContent); 674 } 675 return { 676 type: NodeType$1.Text, 677 textContent: textContent || '', 678 isStyle, 679 rootId, 680 }; 681 case n.CDATA_SECTION_NODE: 682 return { 683 type: NodeType$1.CDATA, 684 textContent: '', 685 rootId, 686 }; 687 case n.COMMENT_NODE: 688 return { 689 type: NodeType$1.Comment, 690 textContent: n.textContent || '', 691 rootId, 692 }; 693 default: 694 return false; 695 } 696 } 697 function lowerIfExists(maybeAttr) { 698 if (maybeAttr === undefined || maybeAttr === null) { 699 return ''; 700 } 701 else { 702 return maybeAttr.toLowerCase(); 703 } 704 } 705 function slimDOMExcluded(sn, slimDOMOptions) { 706 if (slimDOMOptions.comment && sn.type === NodeType$1.Comment) { 707 return true; 708 } 709 else if (sn.type === NodeType$1.Element) { 710 if (slimDOMOptions.script && 711 (sn.tagName === 'script' || 712 (sn.tagName === 'link' && 713 (sn.attributes.rel === 'preload' || 714 sn.attributes.rel === 'modulepreload') && 715 sn.attributes.as === 'script') || 716 (sn.tagName === 'link' && 717 sn.attributes.rel === 'prefetch' && 718 typeof sn.attributes.href === 'string' && 719 sn.attributes.href.endsWith('.js')))) { 720 return true; 721 } 722 else if (slimDOMOptions.headFavicon && 723 ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') || 724 (sn.tagName === 'meta' && 725 (lowerIfExists(sn.attributes.name).match(/^msapplication-tile(image|color)$/) || 726 lowerIfExists(sn.attributes.name) === 'application-name' || 727 lowerIfExists(sn.attributes.rel) === 'icon' || 728 lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' || 729 lowerIfExists(sn.attributes.rel) === 'shortcut icon')))) { 730 return true; 731 } 732 else if (sn.tagName === 'meta') { 733 if (slimDOMOptions.headMetaDescKeywords && 734 lowerIfExists(sn.attributes.name).match(/^description|keywords$/)) { 735 return true; 736 } 737 else if (slimDOMOptions.headMetaSocial && 738 (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) || 739 lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) || 740 lowerIfExists(sn.attributes.name) === 'pinterest')) { 741 return true; 742 } 743 else if (slimDOMOptions.headMetaRobots && 744 (lowerIfExists(sn.attributes.name) === 'robots' || 745 lowerIfExists(sn.attributes.name) === 'googlebot' || 746 lowerIfExists(sn.attributes.name) === 'bingbot')) { 747 return true; 748 } 749 else if (slimDOMOptions.headMetaHttpEquiv && 750 sn.attributes['http-equiv'] !== undefined) { 751 return true; 752 } 753 else if (slimDOMOptions.headMetaAuthorship && 754 (lowerIfExists(sn.attributes.name) === 'author' || 755 lowerIfExists(sn.attributes.name) === 'generator' || 756 lowerIfExists(sn.attributes.name) === 'framework' || 757 lowerIfExists(sn.attributes.name) === 'publisher' || 758 lowerIfExists(sn.attributes.name) === 'progid' || 759 lowerIfExists(sn.attributes.property).match(/^article:/) || 760 lowerIfExists(sn.attributes.property).match(/^product:/))) { 761 return true; 762 } 763 else if (slimDOMOptions.headMetaVerification && 764 (lowerIfExists(sn.attributes.name) === 'google-site-verification' || 765 lowerIfExists(sn.attributes.name) === 'yandex-verification' || 766 lowerIfExists(sn.attributes.name) === 'csrf-token' || 767 lowerIfExists(sn.attributes.name) === 'p:domain_verify' || 768 lowerIfExists(sn.attributes.name) === 'verify-v1' || 769 lowerIfExists(sn.attributes.name) === 'verification' || 770 lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token')) { 771 return true; 772 } 773 } 774 } 775 return false; 776 } 777 function serializeNodeWithId(n, options) { 778 const { doc, map, blockClass, blockSelector, unblockSelector, maskTextClass, maskTextSelector, unmaskTextSelector, skipChild = false, inlineStylesheet = true, maskInputSelector, unmaskInputSelector, maskAllText, maskInputOptions = {}, maskTextFn, maskInputFn, slimDOMOptions, dataURLOptions = {}, inlineImages = false, recordCanvas = false, onSerialize, onIframeLoad, iframeLoadTimeout = 5000, keepIframeSrcFn = () => false, } = options; 779 let { preserveWhiteSpace = true } = options; 780 const _serializedNode = serializeNode(n, { 781 doc, 782 blockClass, 783 blockSelector, 784 unblockSelector, 785 maskTextClass, 786 maskTextSelector, 787 unmaskTextSelector, 788 inlineStylesheet, 789 maskInputSelector, 790 unmaskInputSelector, 791 maskAllText, 792 maskInputOptions, 793 maskTextFn, 794 maskInputFn, 795 dataURLOptions, 796 inlineImages, 797 recordCanvas, 798 keepIframeSrcFn, 799 }); 800 if (!_serializedNode) { 801 console.warn(n, 'not serialized'); 802 return null; 803 } 804 let id; 805 if ('__sn' in n) { 806 id = n.__sn.id; 807 } 808 else if (slimDOMExcluded(_serializedNode, slimDOMOptions) || 809 (!preserveWhiteSpace && 810 _serializedNode.type === NodeType$1.Text && 811 !_serializedNode.isStyle && 812 !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)) { 813 id = IGNORED_NODE; 814 } 815 else { 816 id = genId(); 817 } 818 const serializedNode = Object.assign(_serializedNode, { id }); 819 n.__sn = serializedNode; 820 if (id === IGNORED_NODE) { 821 return null; 822 } 823 map[id] = n; 824 if (onSerialize) { 825 onSerialize(n); 826 } 827 let recordChild = !skipChild; 828 if (serializedNode.type === NodeType$1.Element) { 829 recordChild = recordChild && !serializedNode.needBlock; 830 delete serializedNode.needBlock; 831 if (n.shadowRoot) 832 serializedNode.isShadowHost = true; 833 } 834 if ((serializedNode.type === NodeType$1.Document || 835 serializedNode.type === NodeType$1.Element) && 836 recordChild) { 837 if (slimDOMOptions.headWhitespace && 838 _serializedNode.type === NodeType$1.Element && 839 _serializedNode.tagName === 'head') { 840 preserveWhiteSpace = false; 841 } 842 const bypassOptions = { 843 doc, 844 map, 845 blockClass, 846 blockSelector, 847 unblockSelector, 848 maskTextClass, 849 maskTextSelector, 850 unmaskTextSelector, 851 skipChild, 852 inlineStylesheet, 853 maskInputSelector, 854 unmaskInputSelector, 855 maskAllText, 856 maskInputOptions, 857 maskTextFn, 858 maskInputFn, 859 slimDOMOptions, 860 dataURLOptions, 861 inlineImages, 862 recordCanvas, 863 preserveWhiteSpace, 864 onSerialize, 865 onIframeLoad, 866 iframeLoadTimeout, 867 keepIframeSrcFn, 868 }; 869 for (const childN of Array.from(n.childNodes)) { 870 const serializedChildNode = serializeNodeWithId(childN, bypassOptions); 871 if (serializedChildNode) { 872 serializedNode.childNodes.push(serializedChildNode); 873 } 874 } 875 if (isElement(n) && n.shadowRoot) { 876 for (const childN of Array.from(n.shadowRoot.childNodes)) { 877 const serializedChildNode = serializeNodeWithId(childN, bypassOptions); 878 if (serializedChildNode) { 879 serializedChildNode.isShadow = true; 880 serializedNode.childNodes.push(serializedChildNode); 881 } 882 } 883 } 884 } 885 if (n.parentNode && isShadowRoot(n.parentNode)) { 886 serializedNode.isShadow = true; 887 } 888 if (serializedNode.type === NodeType$1.Element && 889 serializedNode.tagName === 'iframe') { 890 onceIframeLoaded(n, () => { 891 const iframeDoc = n.contentDocument; 892 if (iframeDoc && onIframeLoad) { 893 const serializedIframeNode = serializeNodeWithId(iframeDoc, { 894 doc: iframeDoc, 895 map, 896 blockClass, 897 blockSelector, 898 unblockSelector, 899 maskTextClass, 900 maskTextSelector, 901 unmaskTextSelector, 902 skipChild: false, 903 inlineStylesheet, 904 maskInputSelector, 905 unmaskInputSelector, 906 maskAllText, 907 maskInputOptions, 908 maskTextFn, 909 maskInputFn, 910 slimDOMOptions, 911 dataURLOptions, 912 inlineImages, 913 recordCanvas, 914 preserveWhiteSpace, 915 onSerialize, 916 onIframeLoad, 917 iframeLoadTimeout, 918 keepIframeSrcFn, 919 }); 920 if (serializedIframeNode) { 921 onIframeLoad(n, serializedIframeNode); 922 } 923 } 924 }, iframeLoadTimeout); 925 } 926 return serializedNode; 927 } 928 function snapshot(n, options) { 929 const { blockClass = 'rr-block', blockSelector = null, unblockSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, unmaskTextSelector = null, inlineStylesheet = true, inlineImages = false, recordCanvas = false, maskInputSelector = null, unmaskInputSelector = null, maskAllText = false, maskAllInputs = false, maskTextFn, maskInputFn, slimDOM = false, dataURLOptions, preserveWhiteSpace, onSerialize, onIframeLoad, iframeLoadTimeout, keepIframeSrcFn = () => false, } = options || {}; 930 const idNodeMap = {}; 931 const maskInputOptions = maskAllInputs === true 932 ? { 933 color: true, 934 date: true, 935 'datetime-local': true, 936 email: true, 937 month: true, 938 number: true, 939 range: true, 940 search: true, 941 tel: true, 942 text: true, 943 time: true, 944 url: true, 945 week: true, 946 textarea: true, 947 select: true, 948 } 949 : maskAllInputs === false 950 ? {} 951 : maskAllInputs; 952 const slimDOMOptions = slimDOM === true || slimDOM === 'all' 953 ? 954 { 955 script: true, 956 comment: true, 957 headFavicon: true, 958 headWhitespace: true, 959 headMetaDescKeywords: slimDOM === 'all', 960 headMetaSocial: true, 961 headMetaRobots: true, 962 headMetaHttpEquiv: true, 963 headMetaAuthorship: true, 964 headMetaVerification: true, 965 } 966 : slimDOM === false 967 ? {} 968 : slimDOM; 969 return [ 970 serializeNodeWithId(n, { 971 doc: n, 972 map: idNodeMap, 973 blockClass, 974 blockSelector, 975 unblockSelector, 976 maskTextClass, 977 maskTextSelector, 978 unmaskTextSelector, 979 skipChild: false, 980 inlineStylesheet, 981 maskInputSelector, 982 unmaskInputSelector, 983 maskAllText, 984 maskInputOptions, 985 maskTextFn, 986 maskInputFn, 987 slimDOMOptions, 988 dataURLOptions, 989 inlineImages, 990 recordCanvas, 991 preserveWhiteSpace, 992 onSerialize, 993 onIframeLoad, 994 iframeLoadTimeout, 995 keepIframeSrcFn, 996 }), 997 idNodeMap, 998 ]; 999 } 1000 function skipAttribute(tagName, attributeName, value) { 1001 return ((tagName === 'video' || tagName === 'audio') && attributeName === 'autoplay'); 1002 } 1003 1004 var EventType; 1005 (function (EventType) { 1006 EventType[EventType["DomContentLoaded"] = 0] = "DomContentLoaded"; 1007 EventType[EventType["Load"] = 1] = "Load"; 1008 EventType[EventType["FullSnapshot"] = 2] = "FullSnapshot"; 1009 EventType[EventType["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; 1010 EventType[EventType["Meta"] = 4] = "Meta"; 1011 EventType[EventType["Custom"] = 5] = "Custom"; 1012 EventType[EventType["Plugin"] = 6] = "Plugin"; 1013 })(EventType || (EventType = {})); 1014 var IncrementalSource; 1015 (function (IncrementalSource) { 1016 IncrementalSource[IncrementalSource["Mutation"] = 0] = "Mutation"; 1017 IncrementalSource[IncrementalSource["MouseMove"] = 1] = "MouseMove"; 1018 IncrementalSource[IncrementalSource["MouseInteraction"] = 2] = "MouseInteraction"; 1019 IncrementalSource[IncrementalSource["Scroll"] = 3] = "Scroll"; 1020 IncrementalSource[IncrementalSource["ViewportResize"] = 4] = "ViewportResize"; 1021 IncrementalSource[IncrementalSource["Input"] = 5] = "Input"; 1022 IncrementalSource[IncrementalSource["TouchMove"] = 6] = "TouchMove"; 1023 IncrementalSource[IncrementalSource["MediaInteraction"] = 7] = "MediaInteraction"; 1024 IncrementalSource[IncrementalSource["StyleSheetRule"] = 8] = "StyleSheetRule"; 1025 IncrementalSource[IncrementalSource["CanvasMutation"] = 9] = "CanvasMutation"; 1026 IncrementalSource[IncrementalSource["Font"] = 10] = "Font"; 1027 IncrementalSource[IncrementalSource["Log"] = 11] = "Log"; 1028 IncrementalSource[IncrementalSource["Drag"] = 12] = "Drag"; 1029 IncrementalSource[IncrementalSource["StyleDeclaration"] = 13] = "StyleDeclaration"; 1030 })(IncrementalSource || (IncrementalSource = {})); 1031 var MouseInteractions; 1032 (function (MouseInteractions) { 1033 MouseInteractions[MouseInteractions["MouseUp"] = 0] = "MouseUp"; 1034 MouseInteractions[MouseInteractions["MouseDown"] = 1] = "MouseDown"; 1035 MouseInteractions[MouseInteractions["Click"] = 2] = "Click"; 1036 MouseInteractions[MouseInteractions["ContextMenu"] = 3] = "ContextMenu"; 1037 MouseInteractions[MouseInteractions["DblClick"] = 4] = "DblClick"; 1038 MouseInteractions[MouseInteractions["Focus"] = 5] = "Focus"; 1039 MouseInteractions[MouseInteractions["Blur"] = 6] = "Blur"; 1040 MouseInteractions[MouseInteractions["TouchStart"] = 7] = "TouchStart"; 1041 MouseInteractions[MouseInteractions["TouchMove_Departed"] = 8] = "TouchMove_Departed"; 1042 MouseInteractions[MouseInteractions["TouchEnd"] = 9] = "TouchEnd"; 1043 MouseInteractions[MouseInteractions["TouchCancel"] = 10] = "TouchCancel"; 1044 })(MouseInteractions || (MouseInteractions = {})); 1045 var CanvasContext; 1046 (function (CanvasContext) { 1047 CanvasContext[CanvasContext["2D"] = 0] = "2D"; 1048 CanvasContext[CanvasContext["WebGL"] = 1] = "WebGL"; 1049 CanvasContext[CanvasContext["WebGL2"] = 2] = "WebGL2"; 1050 })(CanvasContext || (CanvasContext = {})); 1051 var MediaInteractions; 1052 (function (MediaInteractions) { 1053 MediaInteractions[MediaInteractions["Play"] = 0] = "Play"; 1054 MediaInteractions[MediaInteractions["Pause"] = 1] = "Pause"; 1055 MediaInteractions[MediaInteractions["Seeked"] = 2] = "Seeked"; 1056 MediaInteractions[MediaInteractions["VolumeChange"] = 3] = "VolumeChange"; 1057 })(MediaInteractions || (MediaInteractions = {})); 1058 var ReplayerEvents; 1059 (function (ReplayerEvents) { 1060 ReplayerEvents["Start"] = "start"; 1061 ReplayerEvents["Pause"] = "pause"; 1062 ReplayerEvents["Resume"] = "resume"; 1063 ReplayerEvents["Resize"] = "resize"; 1064 ReplayerEvents["Finish"] = "finish"; 1065 ReplayerEvents["FullsnapshotRebuilded"] = "fullsnapshot-rebuilded"; 1066 ReplayerEvents["LoadStylesheetStart"] = "load-stylesheet-start"; 1067 ReplayerEvents["LoadStylesheetEnd"] = "load-stylesheet-end"; 1068 ReplayerEvents["SkipStart"] = "skip-start"; 1069 ReplayerEvents["SkipEnd"] = "skip-end"; 1070 ReplayerEvents["MouseInteraction"] = "mouse-interaction"; 1071 ReplayerEvents["EventCast"] = "event-cast"; 1072 ReplayerEvents["CustomEvent"] = "custom-event"; 1073 ReplayerEvents["Flush"] = "flush"; 1074 ReplayerEvents["StateChange"] = "state-change"; 1075 ReplayerEvents["PlayBack"] = "play-back"; 1076 })(ReplayerEvents || (ReplayerEvents = {})); 1077 1078 function on(type, fn, target = document) { 1079 const options = { capture: true, passive: true }; 1080 target.addEventListener(type, fn, options); 1081 return () => target.removeEventListener(type, fn, options); 1082 } 1083 function createMirror() { 1084 return { 1085 map: {}, 1086 getId(n) { 1087 if (!n || !n.__sn) { 1088 return -1; 1089 } 1090 return n.__sn.id; 1091 }, 1092 getNode(id) { 1093 return this.map[id] || null; 1094 }, 1095 removeNodeFromMap(n) { 1096 const id = n.__sn && n.__sn.id; 1097 delete this.map[id]; 1098 if (n.childNodes) { 1099 n.childNodes.forEach((child) => this.removeNodeFromMap(child)); 1100 } 1101 }, 1102 has(id) { 1103 return this.map.hasOwnProperty(id); 1104 }, 1105 reset() { 1106 this.map = {}; 1107 }, 1108 }; 1109 } 1110 const DEPARTED_MIRROR_ACCESS_WARNING = 'Please stop import mirror directly. Instead of that,' + 1111 '\r\n' + 1112 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' + 1113 '\r\n' + 1114 'or you can use record.mirror to access the mirror instance during recording.'; 1115 let _mirror = { 1116 map: {}, 1117 getId() { 1118 console.error(DEPARTED_MIRROR_ACCESS_WARNING); 1119 return -1; 1120 }, 1121 getNode() { 1122 console.error(DEPARTED_MIRROR_ACCESS_WARNING); 1123 return null; 1124 }, 1125 removeNodeFromMap() { 1126 console.error(DEPARTED_MIRROR_ACCESS_WARNING); 1127 }, 1128 has() { 1129 console.error(DEPARTED_MIRROR_ACCESS_WARNING); 1130 return false; 1131 }, 1132 reset() { 1133 console.error(DEPARTED_MIRROR_ACCESS_WARNING); 1134 }, 1135 }; 1136 if (typeof window !== 'undefined' && window.Proxy && window.Reflect) { 1137 _mirror = new Proxy(_mirror, { 1138 get(target, prop, receiver) { 1139 if (prop === 'map') { 1140 console.error(DEPARTED_MIRROR_ACCESS_WARNING); 1141 } 1142 return Reflect.get(target, prop, receiver); 1143 }, 1144 }); 1145 } 1146 function throttle$1(func, wait, options = {}) { 1147 let timeout = null; 1148 let previous = 0; 1149 return function (arg) { 1150 let now = Date.now(); 1151 if (!previous && options.leading === false) { 1152 previous = now; 1153 } 1154 let remaining = wait - (now - previous); 1155 let context = this; 1156 let args = arguments; 1157 if (remaining <= 0 || remaining > wait) { 1158 if (timeout) { 1159 clearTimeout(timeout); 1160 timeout = null; 1161 } 1162 previous = now; 1163 func.apply(context, args); 1164 } 1165 else if (!timeout && options.trailing !== false) { 1166 timeout = setTimeout(() => { 1167 previous = options.leading === false ? 0 : Date.now(); 1168 timeout = null; 1169 func.apply(context, args); 1170 }, remaining); 1171 } 1172 }; 1173 } 1174 function hookSetter(target, key, d, isRevoked, win = window) { 1175 const original = win.Object.getOwnPropertyDescriptor(target, key); 1176 win.Object.defineProperty(target, key, isRevoked 1177 ? d 1178 : { 1179 set(value) { 1180 setTimeout(() => { 1181 d.set.call(this, value); 1182 }, 0); 1183 if (original && original.set) { 1184 original.set.call(this, value); 1185 } 1186 }, 1187 }); 1188 return () => hookSetter(target, key, original || {}, true); 1189 } 1190 function patch(source, name, replacement) { 1191 try { 1192 if (!(name in source)) { 1193 return () => { }; 1194 } 1195 const original = source[name]; 1196 const wrapped = replacement(original); 1197 if (typeof wrapped === 'function') { 1198 wrapped.prototype = wrapped.prototype || {}; 1199 Object.defineProperties(wrapped, { 1200 __rrweb_original__: { 1201 enumerable: false, 1202 value: original, 1203 }, 1204 }); 1205 } 1206 source[name] = wrapped; 1207 return () => { 1208 source[name] = original; 1209 }; 1210 } 1211 catch (_a) { 1212 return () => { }; 1213 } 1214 } 1215 function getWindowHeight() { 1216 return (window.innerHeight || 1217 (document.documentElement && document.documentElement.clientHeight) || 1218 (document.body && document.body.clientHeight)); 1219 } 1220 function getWindowWidth() { 1221 return (window.innerWidth || 1222 (document.documentElement && document.documentElement.clientWidth) || 1223 (document.body && document.body.clientWidth)); 1224 } 1225 function isBlocked(node, blockClass, blockSelector, unblockSelector) { 1226 if (!node) { 1227 return false; 1228 } 1229 if (node.nodeType === node.ELEMENT_NODE) { 1230 let needBlock = false; 1231 const needUnblock = unblockSelector && node.matches(unblockSelector); 1232 if (typeof blockClass === 'string') { 1233 if (node.closest !== undefined) { 1234 needBlock = 1235 !needUnblock && 1236 node.closest('.' + blockClass) !== null; 1237 } 1238 else { 1239 needBlock = 1240 !needUnblock && node.classList.contains(blockClass); 1241 } 1242 } 1243 else { 1244 !needUnblock && 1245 node.classList.forEach((className) => { 1246 if (blockClass.test(className)) { 1247 needBlock = true; 1248 } 1249 }); 1250 } 1251 if (!needBlock && blockSelector) { 1252 needBlock = node.matches(blockSelector); 1253 } 1254 return ((!needUnblock && needBlock) || 1255 isBlocked(node.parentNode, blockClass, blockSelector, unblockSelector)); 1256 } 1257 if (node.nodeType === node.TEXT_NODE) { 1258 return isBlocked(node.parentNode, blockClass, blockSelector, unblockSelector); 1259 } 1260 return isBlocked(node.parentNode, blockClass, blockSelector, unblockSelector); 1261 } 1262 function isIgnored(n) { 1263 if ('__sn' in n) { 1264 return n.__sn.id === IGNORED_NODE; 1265 } 1266 return false; 1267 } 1268 function isAncestorRemoved(target, mirror) { 1269 if (isShadowRoot(target)) { 1270 return false; 1271 } 1272 const id = mirror.getId(target); 1273 if (!mirror.has(id)) { 1274 return true; 1275 } 1276 if (target.parentNode && 1277 target.parentNode.nodeType === target.DOCUMENT_NODE) { 1278 return false; 1279 } 1280 if (!target.parentNode) { 1281 return true; 1282 } 1283 return isAncestorRemoved(target.parentNode, mirror); 1284 } 1285 function isTouchEvent(event) { 1286 return Boolean(event.changedTouches); 1287 } 1288 function polyfill(win = window) { 1289 if ('NodeList' in win && !win.NodeList.prototype.forEach) { 1290 win.NodeList.prototype.forEach = Array.prototype 1291 .forEach; 1292 } 1293 if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) { 1294 win.DOMTokenList.prototype.forEach = Array.prototype 1295 .forEach; 1296 } 1297 if (!Node.prototype.contains) { 1298 Node.prototype.contains = function contains(node) { 1299 if (!(0 in arguments)) { 1300 throw new TypeError('1 argument is required'); 1301 } 1302 do { 1303 if (this === node) { 1304 return true; 1305 } 1306 } while ((node = node && node.parentNode)); 1307 return false; 1308 }; 1309 } 1310 } 1311 function isIframeINode(node) { 1312 if ('__sn' in node) { 1313 return (node.__sn.type === NodeType$1.Element && node.__sn.tagName === 'iframe'); 1314 } 1315 return false; 1316 } 1317 function hasShadowRoot(n) { 1318 return Boolean(n === null || n === void 0 ? void 0 : n.shadowRoot); 1319 } 1320 1321 function isNodeInLinkedList(n) { 1322 return '__ln' in n; 1323 } 1324 class DoubleLinkedList { 1325 constructor() { 1326 this.length = 0; 1327 this.head = null; 1328 } 1329 get(position) { 1330 if (position >= this.length) { 1331 throw new Error('Position outside of list range'); 1332 } 1333 let current = this.head; 1334 for (let index = 0; index < position; index++) { 1335 current = (current === null || current === void 0 ? void 0 : current.next) || null; 1336 } 1337 return current; 1338 } 1339 addNode(n) { 1340 const node = { 1341 value: n, 1342 previous: null, 1343 next: null, 1344 }; 1345 n.__ln = node; 1346 if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) { 1347 const current = n.previousSibling.__ln.next; 1348 node.next = current; 1349 node.previous = n.previousSibling.__ln; 1350 n.previousSibling.__ln.next = node; 1351 if (current) { 1352 current.previous = node; 1353 } 1354 } 1355 else if (n.nextSibling && 1356 isNodeInLinkedList(n.nextSibling) && 1357 n.nextSibling.__ln.previous) { 1358 const current = n.nextSibling.__ln.previous; 1359 node.previous = current; 1360 node.next = n.nextSibling.__ln; 1361 n.nextSibling.__ln.previous = node; 1362 if (current) { 1363 current.next = node; 1364 } 1365 } 1366 else { 1367 if (this.head) { 1368 this.head.previous = node; 1369 } 1370 node.next = this.head; 1371 this.head = node; 1372 } 1373 this.length++; 1374 } 1375 removeNode(n) { 1376 const current = n.__ln; 1377 if (!this.head) { 1378 return; 1379 } 1380 if (!current.previous) { 1381 this.head = current.next; 1382 if (this.head) { 1383 this.head.previous = null; 1384 } 1385 } 1386 else { 1387 current.previous.next = current.next; 1388 if (current.next) { 1389 current.next.previous = current.previous; 1390 } 1391 } 1392 if (n.__ln) { 1393 delete n.__ln; 1394 } 1395 this.length--; 1396 } 1397 } 1398 const moveKey = (id, parentId) => `${id}@${parentId}`; 1399 function isINode(n) { 1400 return '__sn' in n; 1401 } 1402 class MutationBuffer { 1403 constructor() { 1404 this.frozen = false; 1405 this.locked = false; 1406 this.texts = []; 1407 this.attributes = []; 1408 this.removes = []; 1409 this.mapRemoves = []; 1410 this.movedMap = {}; 1411 this.addedSet = new Set(); 1412 this.movedSet = new Set(); 1413 this.droppedSet = new Set(); 1414 this.processMutations = (mutations) => { 1415 mutations.forEach(this.processMutation); 1416 this.emit(); 1417 }; 1418 this.emit = () => { 1419 if (this.frozen || this.locked) { 1420 return; 1421 } 1422 const adds = []; 1423 const addList = new DoubleLinkedList(); 1424 const getNextId = (n) => { 1425 let ns = n; 1426 let nextId = IGNORED_NODE; 1427 while (nextId === IGNORED_NODE) { 1428 ns = ns && ns.nextSibling; 1429 nextId = ns && this.mirror.getId(ns); 1430 } 1431 return nextId; 1432 }; 1433 const pushAdd = (n) => { 1434 var _a, _b, _c, _d, _e; 1435 const shadowHost = n.getRootNode 1436 ? (_a = n.getRootNode()) === null || _a === void 0 ? void 0 : _a.host 1437 : null; 1438 let rootShadowHost = shadowHost; 1439 while ((_c = (_b = rootShadowHost === null || rootShadowHost === void 0 ? void 0 : rootShadowHost.getRootNode) === null || _b === void 0 ? void 0 : _b.call(rootShadowHost)) === null || _c === void 0 ? void 0 : _c.host) 1440 rootShadowHost = 1441 ((_e = (_d = rootShadowHost === null || rootShadowHost === void 0 ? void 0 : rootShadowHost.getRootNode) === null || _d === void 0 ? void 0 : _d.call(rootShadowHost)) === null || _e === void 0 ? void 0 : _e.host) || 1442 null; 1443 const notInDoc = !this.doc.contains(n) && 1444 (!rootShadowHost || !this.doc.contains(rootShadowHost)); 1445 if (!n.parentNode || notInDoc) { 1446 return; 1447 } 1448 const parentId = isShadowRoot(n.parentNode) 1449 ? this.mirror.getId(shadowHost) 1450 : this.mirror.getId(n.parentNode); 1451 const nextId = getNextId(n); 1452 if (parentId === -1 || nextId === -1) { 1453 return addList.addNode(n); 1454 } 1455 let sn = serializeNodeWithId(n, { 1456 doc: this.doc, 1457 map: this.mirror.map, 1458 blockClass: this.blockClass, 1459 blockSelector: this.blockSelector, 1460 unblockSelector: this.unblockSelector, 1461 maskTextClass: this.maskTextClass, 1462 maskTextSelector: this.maskTextSelector, 1463 unmaskTextSelector: this.unmaskTextSelector, 1464 maskInputSelector: this.maskInputSelector, 1465 unmaskInputSelector: this.unmaskInputSelector, 1466 skipChild: true, 1467 inlineStylesheet: this.inlineStylesheet, 1468 maskAllText: this.maskAllText, 1469 maskInputOptions: this.maskInputOptions, 1470 maskTextFn: this.maskTextFn, 1471 maskInputFn: this.maskInputFn, 1472 slimDOMOptions: this.slimDOMOptions, 1473 recordCanvas: this.recordCanvas, 1474 inlineImages: this.inlineImages, 1475 onSerialize: (currentN) => { 1476 if (isIframeINode(currentN)) { 1477 this.iframeManager.addIframe(currentN); 1478 } 1479 if (hasShadowRoot(n)) { 1480 this.shadowDomManager.addShadowRoot(n.shadowRoot, document); 1481 } 1482 }, 1483 onIframeLoad: (iframe, childSn) => { 1484 this.iframeManager.attachIframe(iframe, childSn); 1485 this.shadowDomManager.observeAttachShadow(iframe); 1486 }, 1487 }); 1488 if (sn) { 1489 adds.push({ 1490 parentId, 1491 nextId, 1492 node: sn, 1493 }); 1494 } 1495 }; 1496 while (this.mapRemoves.length) { 1497 this.mirror.removeNodeFromMap(this.mapRemoves.shift()); 1498 } 1499 for (const n of this.movedSet) { 1500 if (isParentRemoved(this.removes, n, this.mirror) && 1501 !this.movedSet.has(n.parentNode)) { 1502 continue; 1503 } 1504 pushAdd(n); 1505 } 1506 for (const n of this.addedSet) { 1507 if (!isAncestorInSet(this.droppedSet, n) && 1508 !isParentRemoved(this.removes, n, this.mirror)) { 1509 pushAdd(n); 1510 } 1511 else if (isAncestorInSet(this.movedSet, n)) { 1512 pushAdd(n); 1513 } 1514 else { 1515 this.droppedSet.add(n); 1516 } 1517 } 1518 let candidate = null; 1519 while (addList.length) { 1520 let node = null; 1521 if (candidate) { 1522 const parentId = this.mirror.getId(candidate.value.parentNode); 1523 const nextId = getNextId(candidate.value); 1524 if (parentId !== -1 && nextId !== -1) { 1525 node = candidate; 1526 } 1527 } 1528 if (!node) { 1529 for (let index = addList.length - 1; index >= 0; index--) { 1530 const _node = addList.get(index); 1531 if (_node) { 1532 const parentId = this.mirror.getId(_node.value.parentNode); 1533 const nextId = getNextId(_node.value); 1534 if (parentId !== -1 && nextId !== -1) { 1535 node = _node; 1536 break; 1537 } 1538 } 1539 } 1540 } 1541 if (!node) { 1542 while (addList.head) { 1543 addList.removeNode(addList.head.value); 1544 } 1545 break; 1546 } 1547 candidate = node.previous; 1548 addList.removeNode(node.value); 1549 pushAdd(node.value); 1550 } 1551 const payload = { 1552 texts: this.texts 1553 .map((text) => ({ 1554 id: this.mirror.getId(text.node), 1555 value: text.value, 1556 })) 1557 .filter((text) => this.mirror.has(text.id)), 1558 attributes: this.attributes 1559 .map((attribute) => ({ 1560 id: this.mirror.getId(attribute.node), 1561 attributes: attribute.attributes, 1562 })) 1563 .filter((attribute) => this.mirror.has(attribute.id)), 1564 removes: this.removes, 1565 adds, 1566 }; 1567 if (!payload.texts.length && 1568 !payload.attributes.length && 1569 !payload.removes.length && 1570 !payload.adds.length) { 1571 return; 1572 } 1573 this.texts = []; 1574 this.attributes = []; 1575 this.removes = []; 1576 this.addedSet = new Set(); 1577 this.movedSet = new Set(); 1578 this.droppedSet = new Set(); 1579 this.movedMap = {}; 1580 this.mutationCb(payload); 1581 }; 1582 this.processMutation = (m) => { 1583 if (isIgnored(m.target)) { 1584 return; 1585 } 1586 switch (m.type) { 1587 case 'characterData': { 1588 const value = m.target.textContent; 1589 if (!isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector) && value !== m.oldValue) { 1590 this.texts.push({ 1591 value: needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, this.unmaskTextSelector, this.maskAllText) && value 1592 ? this.maskTextFn 1593 ? this.maskTextFn(value) 1594 : value.replace(/[\S]/g, '*') 1595 : value, 1596 node: m.target, 1597 }); 1598 } 1599 break; 1600 } 1601 case 'attributes': { 1602 const target = m.target; 1603 let value = target.getAttribute(m.attributeName); 1604 if (m.attributeName === 'value') { 1605 value = maskInputValue({ 1606 input: target, 1607 maskInputSelector: this.maskInputSelector, 1608 unmaskInputSelector: this.unmaskInputSelector, 1609 maskInputOptions: this.maskInputOptions, 1610 tagName: target.tagName, 1611 type: target.getAttribute('type'), 1612 value, 1613 maskInputFn: this.maskInputFn, 1614 }); 1615 } 1616 if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector) || value === m.oldValue) { 1617 return; 1618 } 1619 let item = this.attributes.find((a) => a.node === m.target); 1620 if (!item) { 1621 item = { 1622 node: m.target, 1623 attributes: {}, 1624 }; 1625 this.attributes.push(item); 1626 } 1627 if (m.attributeName === 'type' && 1628 target.tagName === 'INPUT' && 1629 (m.oldValue || '').toLowerCase() === 'password') { 1630 target.setAttribute('data-rr-is-password', 'true'); 1631 } 1632 if (m.attributeName === 'style') { 1633 const old = this.doc.createElement('span'); 1634 if (m.oldValue) { 1635 old.setAttribute('style', m.oldValue); 1636 } 1637 if (item.attributes.style === undefined || 1638 item.attributes.style === null) { 1639 item.attributes.style = {}; 1640 } 1641 try { 1642 const styleObj = item.attributes.style; 1643 for (const pname of Array.from(target.style)) { 1644 const newValue = target.style.getPropertyValue(pname); 1645 const newPriority = target.style.getPropertyPriority(pname); 1646 if (newValue !== old.style.getPropertyValue(pname) || 1647 newPriority !== old.style.getPropertyPriority(pname)) { 1648 if (newPriority === '') { 1649 styleObj[pname] = newValue; 1650 } 1651 else { 1652 styleObj[pname] = [newValue, newPriority]; 1653 } 1654 } 1655 } 1656 for (const pname of Array.from(old.style)) { 1657 if (target.style.getPropertyValue(pname) === '') { 1658 styleObj[pname] = false; 1659 } 1660 } 1661 } 1662 catch (error) { 1663 console.warn('[rrweb] Error when parsing update to style attribute:', error); 1664 } 1665 } 1666 else { 1667 const element = m.target; 1668 item.attributes[m.attributeName] = transformAttribute(this.doc, element, element.tagName, m.attributeName, value, this.maskAllText, this.unmaskTextSelector, this.maskTextFn); 1669 } 1670 break; 1671 } 1672 case 'childList': { 1673 m.addedNodes.forEach((n) => this.genAdds(n, m.target)); 1674 m.removedNodes.forEach((n) => { 1675 const nodeId = this.mirror.getId(n); 1676 const parentId = isShadowRoot(m.target) 1677 ? this.mirror.getId(m.target.host) 1678 : this.mirror.getId(m.target); 1679 if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector) || isIgnored(n)) { 1680 return; 1681 } 1682 if (this.addedSet.has(n)) { 1683 deepDelete(this.addedSet, n); 1684 this.droppedSet.add(n); 1685 } 1686 else if (this.addedSet.has(m.target) && nodeId === -1) ; 1687 else if (isAncestorRemoved(m.target, this.mirror)) ; 1688 else if (this.movedSet.has(n) && 1689 this.movedMap[moveKey(nodeId, parentId)]) { 1690 deepDelete(this.movedSet, n); 1691 } 1692 else { 1693 this.removes.push({ 1694 parentId, 1695 id: nodeId, 1696 isShadow: isShadowRoot(m.target) ? true : undefined, 1697 }); 1698 } 1699 this.mapRemoves.push(n); 1700 }); 1701 break; 1702 } 1703 } 1704 }; 1705 this.genAdds = (n, target) => { 1706 if (target && isBlocked(target, this.blockClass, this.blockSelector, this.unblockSelector)) { 1707 return; 1708 } 1709 if (isINode(n)) { 1710 if (isIgnored(n)) { 1711 return; 1712 } 1713 this.movedSet.add(n); 1714 let targetId = null; 1715 if (target && isINode(target)) { 1716 targetId = target.__sn.id; 1717 } 1718 if (targetId) { 1719 this.movedMap[moveKey(n.__sn.id, targetId)] = true; 1720 } 1721 } 1722 else { 1723 this.addedSet.add(n); 1724 this.droppedSet.delete(n); 1725 } 1726 if (!isBlocked(n, this.blockClass, this.blockSelector, this.unblockSelector)) 1727 n.childNodes.forEach((childN) => this.genAdds(childN)); 1728 }; 1729 } 1730 init(options) { 1731 [ 1732 'mutationCb', 1733 'blockClass', 1734 'blockSelector', 1735 'unblockSelector', 1736 'maskTextClass', 1737 'maskTextSelector', 1738 'unmaskTextSelector', 1739 'maskInputSelector', 1740 'unmaskInputSelector', 1741 'inlineStylesheet', 1742 'maskAllText', 1743 'maskInputOptions', 1744 'maskTextFn', 1745 'maskInputFn', 1746 'recordCanvas', 1747 'inlineImages', 1748 'slimDOMOptions', 1749 'doc', 1750 'mirror', 1751 'iframeManager', 1752 'shadowDomManager', 1753 'canvasManager', 1754 ].forEach((key) => { 1755 this[key] = options[key]; 1756 }); 1757 } 1758 freeze() { 1759 this.frozen = true; 1760 this.canvasManager.freeze(); 1761 } 1762 unfreeze() { 1763 this.frozen = false; 1764 this.canvasManager.unfreeze(); 1765 this.emit(); 1766 } 1767 isFrozen() { 1768 return this.frozen; 1769 } 1770 lock() { 1771 this.locked = true; 1772 this.canvasManager.lock(); 1773 } 1774 unlock() { 1775 this.locked = false; 1776 this.canvasManager.unlock(); 1777 this.emit(); 1778 } 1779 reset() { 1780 this.shadowDomManager.reset(); 1781 this.canvasManager.reset(); 1782 } 1783 } 1784 function deepDelete(addsSet, n) { 1785 addsSet.delete(n); 1786 n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); 1787 } 1788 function isParentRemoved(removes, n, mirror) { 1789 const { parentNode } = n; 1790 if (!parentNode) { 1791 return false; 1792 } 1793 const parentId = mirror.getId(parentNode); 1794 if (removes.some((r) => r.id === parentId)) { 1795 return true; 1796 } 1797 return isParentRemoved(removes, parentNode, mirror); 1798 } 1799 function isAncestorInSet(set, n) { 1800 const { parentNode } = n; 1801 if (!parentNode) { 1802 return false; 1803 } 1804 if (set.has(parentNode)) { 1805 return true; 1806 } 1807 return isAncestorInSet(set, parentNode); 1808 } 1809 1810 const callbackWrapper = (cb) => { 1811 const rrwebWrapped = (...rest) => { 1812 try { 1813 return cb(...rest); 1814 } 1815 catch (error) { 1816 try { 1817 error.__rrweb__ = true; 1818 } 1819 catch (_a) { 1820 } 1821 throw error; 1822 } 1823 }; 1824 return rrwebWrapped; 1825 }; 1826 1827 const mutationBuffers = []; 1828 function getEventTarget(event) { 1829 try { 1830 if ('composedPath' in event) { 1831 const path = event.composedPath(); 1832 if (path.length) { 1833 return path[0]; 1834 } 1835 } 1836 else if ('path' in event && event.path.length) { 1837 return event.path[0]; 1838 } 1839 } 1840 catch (_a) { } 1841 return event && event.target; 1842 } 1843 function initMutationObserver(options, rootEl) { 1844 var _a, _b; 1845 const mutationBuffer = new MutationBuffer(); 1846 mutationBuffers.push(mutationBuffer); 1847 mutationBuffer.init(options); 1848 let mutationObserverCtor = window.MutationObserver || 1849 window.__rrMutationObserver; 1850 const angularZoneSymbol = (_b = (_a = window === null || window === void 0 ? void 0 : window.Zone) === null || _a === void 0 ? void 0 : _a.__symbol__) === null || _b === void 0 ? void 0 : _b.call(_a, 'MutationObserver'); 1851 if (angularZoneSymbol && 1852 window[angularZoneSymbol]) { 1853 mutationObserverCtor = window[angularZoneSymbol]; 1854 } 1855 const observer = new mutationObserverCtor(callbackWrapper((mutations) => { 1856 if (options.onMutation && options.onMutation(mutations) === false) { 1857 return; 1858 } 1859 mutationBuffer.processMutations(mutations); 1860 })); 1861 observer.observe(rootEl, { 1862 attributes: true, 1863 attributeOldValue: true, 1864 characterData: true, 1865 characterDataOldValue: true, 1866 childList: true, 1867 subtree: true, 1868 }); 1869 return observer; 1870 } 1871 function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) { 1872 if (sampling.mousemove === false) { 1873 return () => { }; 1874 } 1875 const threshold = typeof sampling.mousemove === 'number' ? sampling.mousemove : 50; 1876 const callbackThreshold = typeof sampling.mousemoveCallback === 'number' 1877 ? sampling.mousemoveCallback 1878 : 500; 1879 let positions = []; 1880 let timeBaseline; 1881 const wrappedCb = throttle$1((source) => { 1882 const totalOffset = Date.now() - timeBaseline; 1883 callbackWrapper(mousemoveCb)(positions.map((p) => { 1884 p.timeOffset -= totalOffset; 1885 return p; 1886 }), source); 1887 positions = []; 1888 timeBaseline = null; 1889 }, callbackThreshold); 1890 const updatePosition = throttle$1((evt) => { 1891 const target = getEventTarget(evt); 1892 const { clientX, clientY } = isTouchEvent(evt) 1893 ? evt.changedTouches[0] 1894 : evt; 1895 if (!timeBaseline) { 1896 timeBaseline = Date.now(); 1897 } 1898 positions.push({ 1899 x: clientX, 1900 y: clientY, 1901 id: mirror.getId(target), 1902 timeOffset: Date.now() - timeBaseline, 1903 }); 1904 wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent 1905 ? IncrementalSource.Drag 1906 : evt instanceof MouseEvent 1907 ? IncrementalSource.MouseMove 1908 : IncrementalSource.TouchMove); 1909 }, threshold, { 1910 trailing: false, 1911 }); 1912 const handlers = [ 1913 on('mousemove', callbackWrapper(updatePosition), doc), 1914 on('touchmove', callbackWrapper(updatePosition), doc), 1915 on('drag', callbackWrapper(updatePosition), doc), 1916 ]; 1917 return callbackWrapper(() => { 1918 handlers.forEach((h) => h()); 1919 }); 1920 } 1921 function initMouseInteractionObserver({ mouseInteractionCb, doc, mirror, blockClass, blockSelector, unblockSelector, sampling, }) { 1922 if (sampling.mouseInteraction === false) { 1923 return () => { }; 1924 } 1925 const disableMap = sampling.mouseInteraction === true || 1926 sampling.mouseInteraction === undefined 1927 ? {} 1928 : sampling.mouseInteraction; 1929 const handlers = []; 1930 const getHandler = (eventKey) => { 1931 return (event) => { 1932 const target = getEventTarget(event); 1933 if (isBlocked(target, blockClass, blockSelector, unblockSelector)) { 1934 return; 1935 } 1936 const e = isTouchEvent(event) ? event.changedTouches[0] : event; 1937 if (!e) { 1938 return; 1939 } 1940 const id = mirror.getId(target); 1941 const { clientX, clientY } = e; 1942 callbackWrapper(mouseInteractionCb)({ 1943 type: MouseInteractions[eventKey], 1944 id, 1945 x: clientX, 1946 y: clientY, 1947 }); 1948 }; 1949 }; 1950 Object.keys(MouseInteractions) 1951 .filter((key) => Number.isNaN(Number(key)) && 1952 !key.endsWith('_Departed') && 1953 disableMap[key] !== false) 1954 .forEach((eventKey) => { 1955 const eventName = eventKey.toLowerCase(); 1956 const handler = callbackWrapper(getHandler(eventKey)); 1957 handlers.push(on(eventName, handler, doc)); 1958 }); 1959 return callbackWrapper(() => { 1960 handlers.forEach((h) => h()); 1961 }); 1962 } 1963 function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, unblockSelector, sampling, }) { 1964 const updatePosition = throttle$1((evt) => { 1965 const target = getEventTarget(evt); 1966 if (!target || 1967 isBlocked(target, blockClass, blockSelector, unblockSelector)) { 1968 return; 1969 } 1970 const id = mirror.getId(target); 1971 if (target === doc) { 1972 const scrollEl = (doc.scrollingElement || doc.documentElement); 1973 callbackWrapper(scrollCb)({ 1974 id, 1975 x: scrollEl.scrollLeft, 1976 y: scrollEl.scrollTop, 1977 }); 1978 } 1979 else { 1980 callbackWrapper(scrollCb)({ 1981 id, 1982 x: target.scrollLeft, 1983 y: target.scrollTop, 1984 }); 1985 } 1986 }, sampling.scroll || 100); 1987 return on('scroll', callbackWrapper(updatePosition), doc); 1988 } 1989 function initViewportResizeObserver({ viewportResizeCb, }) { 1990 let lastH = -1; 1991 let lastW = -1; 1992 const updateDimension = throttle$1(() => { 1993 const height = getWindowHeight(); 1994 const width = getWindowWidth(); 1995 if (lastH !== height || lastW !== width) { 1996 callbackWrapper(viewportResizeCb)({ 1997 width: Number(width), 1998 height: Number(height), 1999 }); 2000 lastH = height; 2001 lastW = width; 2002 } 2003 }, 200); 2004 return on('resize', callbackWrapper(updateDimension), window); 2005 } 2006 function wrapEventWithUserTriggeredFlag(v, enable) { 2007 const value = Object.assign({}, v); 2008 if (!enable) 2009 delete value.userTriggered; 2010 return value; 2011 } 2012 const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; 2013 const lastInputValueMap = new WeakMap(); 2014 function initInputObserver({ inputCb, doc, mirror, blockClass, blockSelector, unblockSelector, ignoreClass, ignoreSelector, maskInputSelector, unmaskInputSelector, maskInputOptions, maskInputFn, sampling, userTriggeredOnInput, }) { 2015 function eventHandler(event) { 2016 let target = getEventTarget(event); 2017 const tagName = target && target.tagName; 2018 const userTriggered = event.isTrusted; 2019 if (tagName === 'OPTION') 2020 target = target.parentElement; 2021 if (!target || 2022 !tagName || 2023 INPUT_TAGS.indexOf(tagName) < 0 || 2024 isBlocked(target, blockClass, blockSelector, unblockSelector)) { 2025 return; 2026 } 2027 const el = target; 2028 const type = getInputType(el); 2029 if (el.classList.contains(ignoreClass) || 2030 (ignoreSelector && el.matches(ignoreSelector))) { 2031 return; 2032 } 2033 let text = getInputValue(el, tagName, type); 2034 let isChecked = false; 2035 if (type === 'radio' || type === 'checkbox') { 2036 isChecked = target.checked; 2037 } 2038 if (hasInputMaskOptions({ 2039 maskInputOptions, 2040 maskInputSelector, 2041 tagName, 2042 type, 2043 })) { 2044 text = maskInputValue({ 2045 input: el, 2046 maskInputOptions, 2047 maskInputSelector, 2048 unmaskInputSelector, 2049 tagName, 2050 type, 2051 value: text, 2052 maskInputFn, 2053 }); 2054 } 2055 cbWithDedup(target, callbackWrapper(wrapEventWithUserTriggeredFlag)({ text, isChecked, userTriggered }, userTriggeredOnInput)); 2056 const name = target.name; 2057 if (type === 'radio' && name && isChecked) { 2058 doc 2059 .querySelectorAll(`input[type="radio"][name="${name}"]`) 2060 .forEach((el) => { 2061 if (el !== target) { 2062 const text = maskInputValue({ 2063 input: el, 2064 maskInputOptions, 2065 maskInputSelector, 2066 unmaskInputSelector, 2067 tagName, 2068 type, 2069 value: getInputValue(el, tagName, type), 2070 maskInputFn, 2071 }); 2072 cbWithDedup(el, callbackWrapper(wrapEventWithUserTriggeredFlag)({ 2073 text, 2074 isChecked: !isChecked, 2075 userTriggered: false, 2076 }, userTriggeredOnInput)); 2077 } 2078 }); 2079 } 2080 } 2081 function cbWithDedup(target, v) { 2082 const lastInputValue = lastInputValueMap.get(target); 2083 if (!lastInputValue || 2084 lastInputValue.text !== v.text || 2085 lastInputValue.isChecked !== v.isChecked) { 2086 lastInputValueMap.set(target, v); 2087 const id = mirror.getId(target); 2088 inputCb(Object.assign(Object.assign({}, v), { id })); 2089 } 2090 } 2091 const events = sampling.input === 'last' ? ['change'] : ['input', 'change']; 2092 const handlers = events.map((eventName) => on(eventName, callbackWrapper(eventHandler), doc)); 2093 const propertyDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); 2094 const hookProperties = [ 2095 [HTMLInputElement.prototype, 'value'], 2096 [HTMLInputElement.prototype, 'checked'], 2097 [HTMLSelectElement.prototype, 'value'], 2098 [HTMLTextAreaElement.prototype, 'value'], 2099 [HTMLSelectElement.prototype, 'selectedIndex'], 2100 [HTMLOptionElement.prototype, 'selected'], 2101 ]; 2102 if (propertyDescriptor && propertyDescriptor.set) { 2103 handlers.push(...hookProperties.map((p) => hookSetter(p[0], p[1], { 2104 set() { 2105 callbackWrapper(eventHandler)({ target: this }); 2106 }, 2107 }))); 2108 } 2109 return callbackWrapper(() => { 2110 handlers.forEach((h) => h()); 2111 }); 2112 } 2113 function getNestedCSSRulePositions(rule) { 2114 const positions = []; 2115 function recurse(childRule, pos) { 2116 if ((hasNestedCSSRule('CSSGroupingRule') && 2117 childRule.parentRule instanceof CSSGroupingRule) || 2118 (hasNestedCSSRule('CSSMediaRule') && 2119 childRule.parentRule instanceof CSSMediaRule) || 2120 (hasNestedCSSRule('CSSSupportsRule') && 2121 childRule.parentRule instanceof CSSSupportsRule) || 2122 (hasNestedCSSRule('CSSConditionRule') && 2123 childRule.parentRule instanceof CSSConditionRule)) { 2124 const rules = Array.from(childRule.parentRule.cssRules); 2125 const index = rules.indexOf(childRule); 2126 pos.unshift(index); 2127 } 2128 else { 2129 const rules = Array.from(childRule.parentStyleSheet.cssRules); 2130 const index = rules.indexOf(childRule); 2131 pos.unshift(index); 2132 } 2133 return pos; 2134 } 2135 return recurse(rule, positions); 2136 } 2137 function initStyleSheetObserver({ styleSheetRuleCb, mirror }, { win }) { 2138 if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) { 2139 return () => { }; 2140 } 2141 const insertRule = win.CSSStyleSheet.prototype.insertRule; 2142 win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, { 2143 apply: callbackWrapper((target, thisArg, argumentsList) => { 2144 const [rule, index] = argumentsList; 2145 const id = mirror.getId(thisArg.ownerNode); 2146 if (id !== -1) { 2147 styleSheetRuleCb({ 2148 id, 2149 adds: [{ rule, index }], 2150 }); 2151 } 2152 return target.apply(thisArg, argumentsList); 2153 }), 2154 }); 2155 const deleteRule = win.CSSStyleSheet.prototype.deleteRule; 2156 win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, { 2157 apply: callbackWrapper((target, thisArg, argumentsList) => { 2158 const [index] = argumentsList; 2159 const id = mirror.getId(thisArg.ownerNode); 2160 if (id !== -1) { 2161 styleSheetRuleCb({ 2162 id, 2163 removes: [{ index }], 2164 }); 2165 } 2166 return target.apply(thisArg, argumentsList); 2167 }), 2168 }); 2169 const supportedNestedCSSRuleTypes = {}; 2170 if (canMonkeyPatchNestedCSSRule('CSSGroupingRule')) { 2171 supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule; 2172 } 2173 else { 2174 if (canMonkeyPatchNestedCSSRule('CSSMediaRule')) { 2175 supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule; 2176 } 2177 if (canMonkeyPatchNestedCSSRule('CSSConditionRule')) { 2178 supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule; 2179 } 2180 if (canMonkeyPatchNestedCSSRule('CSSSupportsRule')) { 2181 supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule; 2182 } 2183 } 2184 const unmodifiedFunctions = {}; 2185 Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { 2186 unmodifiedFunctions[typeKey] = { 2187 insertRule: type.prototype.insertRule, 2188 deleteRule: type.prototype.deleteRule, 2189 }; 2190 type.prototype.insertRule = new Proxy(unmodifiedFunctions[typeKey].insertRule, { 2191 apply: callbackWrapper((target, thisArg, argumentsList) => { 2192 const [rule, index] = argumentsList; 2193 const id = mirror.getId(thisArg.parentStyleSheet.ownerNode); 2194 if (id !== -1) { 2195 styleSheetRuleCb({ 2196 id, 2197 adds: [ 2198 { 2199 rule, 2200 index: [ 2201 ...getNestedCSSRulePositions(thisArg), 2202 index || 0, 2203 ], 2204 }, 2205 ], 2206 }); 2207 } 2208 return target.apply(thisArg, argumentsList); 2209 }), 2210 }); 2211 type.prototype.deleteRule = new Proxy(unmodifiedFunctions[typeKey].deleteRule, { 2212 apply: callbackWrapper((target, thisArg, argumentsList) => { 2213 const [index] = argumentsList; 2214 const id = mirror.getId(thisArg.parentStyleSheet.ownerNode); 2215 if (id !== -1) { 2216 styleSheetRuleCb({ 2217 id, 2218 removes: [ 2219 { index: [...getNestedCSSRulePositions(thisArg), index] }, 2220 ], 2221 }); 2222 } 2223 return target.apply(thisArg, argumentsList); 2224 }), 2225 }); 2226 }); 2227 return callbackWrapper(() => { 2228 win.CSSStyleSheet.prototype.insertRule = insertRule; 2229 win.CSSStyleSheet.prototype.deleteRule = deleteRule; 2230 Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { 2231 type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; 2232 type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; 2233 }); 2234 }); 2235 } 2236 function initStyleDeclarationObserver({ styleDeclarationCb, mirror }, { win }) { 2237 const setProperty = win.CSSStyleDeclaration.prototype.setProperty; 2238 win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, { 2239 apply: callbackWrapper((target, thisArg, argumentsList) => { 2240 var _a, _b; 2241 const [property, value, priority] = argumentsList; 2242 const id = mirror.getId((_b = (_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet) === null || _b === void 0 ? void 0 : _b.ownerNode); 2243 if (id !== -1) { 2244 styleDeclarationCb({ 2245 id, 2246 set: { 2247 property, 2248 value, 2249 priority, 2250 }, 2251 index: getNestedCSSRulePositions(thisArg.parentRule), 2252 }); 2253 } 2254 return target.apply(thisArg, argumentsList); 2255 }), 2256 }); 2257 const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty; 2258 win.CSSStyleDeclaration.prototype.removeProperty = new Proxy(removeProperty, { 2259 apply: callbackWrapper((target, thisArg, argumentsList) => { 2260 var _a, _b; 2261 const [property] = argumentsList; 2262 const id = mirror.getId((_b = (_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet) === null || _b === void 0 ? void 0 : _b.ownerNode); 2263 if (id !== -1) { 2264 styleDeclarationCb({ 2265 id, 2266 remove: { 2267 property, 2268 }, 2269 index: getNestedCSSRulePositions(thisArg.parentRule), 2270 }); 2271 } 2272 return target.apply(thisArg, argumentsList); 2273 }), 2274 }); 2275 return callbackWrapper(() => { 2276 win.CSSStyleDeclaration.prototype.setProperty = setProperty; 2277 win.CSSStyleDeclaration.prototype.removeProperty = removeProperty; 2278 }); 2279 } 2280 function initMediaInteractionObserver({ mediaInteractionCb, blockClass, blockSelector, unblockSelector, mirror, sampling, }) { 2281 const handler = (type) => throttle$1(callbackWrapper((event) => { 2282 const target = getEventTarget(event); 2283 if (!target || 2284 isBlocked(target, blockClass, blockSelector, unblockSelector)) { 2285 return; 2286 } 2287 const { currentTime, volume, muted } = target; 2288 mediaInteractionCb({ 2289 type, 2290 id: mirror.getId(target), 2291 currentTime, 2292 volume, 2293 muted, 2294 }); 2295 }), sampling.media || 500); 2296 const handlers = [ 2297 on('play', handler(0)), 2298 on('pause', handler(1)), 2299 on('seeked', handler(2)), 2300 on('volumechange', handler(3)), 2301 ]; 2302 return callbackWrapper(() => { 2303 handlers.forEach((h) => h()); 2304 }); 2305 } 2306 function initFontObserver({ fontCb, doc }) { 2307 const win = doc.defaultView; 2308 if (!win) { 2309 return () => { }; 2310 } 2311 const handlers = []; 2312 const fontMap = new WeakMap(); 2313 const originalFontFace = win.FontFace; 2314 win.FontFace = function FontFace(family, source, descriptors) { 2315 const fontFace = new originalFontFace(family, source, descriptors); 2316 fontMap.set(fontFace, { 2317 family, 2318 buffer: typeof source !== 'string', 2319 descriptors, 2320 fontSource: typeof source === 'string' 2321 ? source 2322 : 2323 JSON.stringify(Array.from(new Uint8Array(source))), 2324 }); 2325 return fontFace; 2326 }; 2327 const restoreHandler = patch(doc.fonts, 'add', function (original) { 2328 return function (fontFace) { 2329 setTimeout(() => { 2330 const p = fontMap.get(fontFace); 2331 if (p) { 2332 fontCb(p); 2333 fontMap.delete(fontFace); 2334 } 2335 }, 0); 2336 return original.apply(this, [fontFace]); 2337 }; 2338 }); 2339 handlers.push(() => { 2340 win.FontFace = originalFontFace; 2341 }); 2342 handlers.push(restoreHandler); 2343 return callbackWrapper(() => { 2344 handlers.forEach((h) => h()); 2345 }); 2346 } 2347 function mergeHooks(o, hooks) { 2348 const { mutationCb, mousemoveCb, mouseInteractionCb, scrollCb, viewportResizeCb, inputCb, mediaInteractionCb, styleSheetRuleCb, styleDeclarationCb, canvasMutationCb, fontCb, } = o; 2349 o.mutationCb = (...p) => { 2350 if (hooks.mutation) { 2351 hooks.mutation(...p); 2352 } 2353 mutationCb(...p); 2354 }; 2355 o.mousemoveCb = (...p) => { 2356 if (hooks.mousemove) { 2357 hooks.mousemove(...p); 2358 } 2359 mousemoveCb(...p); 2360 }; 2361 o.mouseInteractionCb = (...p) => { 2362 if (hooks.mouseInteraction) { 2363 hooks.mouseInteraction(...p); 2364 } 2365 mouseInteractionCb(...p); 2366 }; 2367 o.scrollCb = (...p) => { 2368 if (hooks.scroll) { 2369 hooks.scroll(...p); 2370 } 2371 scrollCb(...p); 2372 }; 2373 o.viewportResizeCb = (...p) => { 2374 if (hooks.viewportResize) { 2375 hooks.viewportResize(...p); 2376 } 2377 viewportResizeCb(...p); 2378 }; 2379 o.inputCb = (...p) => { 2380 if (hooks.input) { 2381 hooks.input(...p); 2382 } 2383 inputCb(...p); 2384 }; 2385 o.mediaInteractionCb = (...p) => { 2386 if (hooks.mediaInteaction) { 2387 hooks.mediaInteaction(...p); 2388 } 2389 mediaInteractionCb(...p); 2390 }; 2391 o.styleSheetRuleCb = (...p) => { 2392 if (hooks.styleSheetRule) { 2393 hooks.styleSheetRule(...p); 2394 } 2395 styleSheetRuleCb(...p); 2396 }; 2397 o.styleDeclarationCb = (...p) => { 2398 if (hooks.styleDeclaration) { 2399 hooks.styleDeclaration(...p); 2400 } 2401 styleDeclarationCb(...p); 2402 }; 2403 o.canvasMutationCb = (...p) => { 2404 if (hooks.canvasMutation) { 2405 hooks.canvasMutation(...p); 2406 } 2407 canvasMutationCb(...p); 2408 }; 2409 o.fontCb = (...p) => { 2410 if (hooks.font) { 2411 hooks.font(...p); 2412 } 2413 fontCb(...p); 2414 }; 2415 } 2416 function initObservers(o, hooks = {}) { 2417 const currentWindow = o.doc.defaultView; 2418 if (!currentWindow) { 2419 return () => { }; 2420 } 2421 mergeHooks(o, hooks); 2422 const mutationObserver = initMutationObserver(o, o.doc); 2423 const mousemoveHandler = initMoveObserver(o); 2424 const mouseInteractionHandler = initMouseInteractionObserver(o); 2425 const scrollHandler = initScrollObserver(o); 2426 const viewportResizeHandler = initViewportResizeObserver(o); 2427 const inputHandler = initInputObserver(o); 2428 const mediaInteractionHandler = initMediaInteractionObserver(o); 2429 const styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow }); 2430 const styleDeclarationObserver = initStyleDeclarationObserver(o, { 2431 win: currentWindow, 2432 }); 2433 const fontObserver = o.collectFonts ? initFontObserver(o) : () => { }; 2434 const pluginHandlers = []; 2435 for (const plugin of o.plugins) { 2436 pluginHandlers.push(plugin.observer(plugin.callback, currentWindow, plugin.options)); 2437 } 2438 return callbackWrapper(() => { 2439 mutationBuffers.forEach((b) => b.reset()); 2440 mutationObserver.disconnect(); 2441 mousemoveHandler(); 2442 mouseInteractionHandler(); 2443 scrollHandler(); 2444 viewportResizeHandler(); 2445 inputHandler(); 2446 mediaInteractionHandler(); 2447 try { 2448 styleSheetObserver(); 2449 styleDeclarationObserver(); 2450 } 2451 catch (e) { 2452 } 2453 fontObserver(); 2454 pluginHandlers.forEach((h) => h()); 2455 }); 2456 } 2457 function hasNestedCSSRule(prop) { 2458 return typeof window[prop] !== 'undefined'; 2459 } 2460 function canMonkeyPatchNestedCSSRule(prop) { 2461 return Boolean(typeof window[prop] !== 'undefined' && 2462 window[prop].prototype && 2463 'insertRule' in window[prop].prototype && 2464 'deleteRule' in window[prop].prototype); 2465 } 2466 2467 class IframeManager { 2468 constructor(options) { 2469 this.iframes = new WeakMap(); 2470 this.mutationCb = options.mutationCb; 2471 } 2472 addIframe(iframeEl) { 2473 this.iframes.set(iframeEl, true); 2474 } 2475 addLoadListener(cb) { 2476 this.loadListener = cb; 2477 } 2478 attachIframe(iframeEl, childSn) { 2479 var _a; 2480 this.mutationCb({ 2481 adds: [ 2482 { 2483 parentId: iframeEl.__sn.id, 2484 nextId: null, 2485 node: childSn, 2486 }, 2487 ], 2488 removes: [], 2489 texts: [], 2490 attributes: [], 2491 isAttachIframe: true, 2492 }); 2493 (_a = this.loadListener) === null || _a === void 0 ? void 0 : _a.call(this, iframeEl); 2494 } 2495 } 2496 2497 class ShadowDomManager { 2498 constructor(options) { 2499 this.restorePatches = []; 2500 this.mutationCb = options.mutationCb; 2501 this.scrollCb = options.scrollCb; 2502 this.bypassOptions = options.bypassOptions; 2503 this.mirror = options.mirror; 2504 const manager = this; 2505 this.restorePatches.push(patch(HTMLElement.prototype, 'attachShadow', function (original) { 2506 return function () { 2507 const shadowRoot = original.apply(this, arguments); 2508 if (this.shadowRoot) 2509 manager.addShadowRoot(this.shadowRoot, this.ownerDocument); 2510 return shadowRoot; 2511 }; 2512 })); 2513 } 2514 addShadowRoot(shadowRoot, doc) { 2515 initMutationObserver(Object.assign(Object.assign({}, this.bypassOptions), { doc, mutationCb: this.mutationCb, mirror: this.mirror, shadowDomManager: this }), shadowRoot); 2516 initScrollObserver(Object.assign(Object.assign({}, this.bypassOptions), { scrollCb: this.scrollCb, doc: shadowRoot, mirror: this.mirror })); 2517 } 2518 observeAttachShadow(iframeElement) { 2519 if (iframeElement.contentWindow) { 2520 const manager = this; 2521 this.restorePatches.push(patch(iframeElement.contentWindow.HTMLElement.prototype, 'attachShadow', function (original) { 2522 return function () { 2523 const shadowRoot = original.apply(this, arguments); 2524 if (this.shadowRoot) 2525 manager.addShadowRoot(this.shadowRoot, iframeElement.contentDocument); 2526 return shadowRoot; 2527 }; 2528 })); 2529 } 2530 } 2531 reset() { 2532 this.restorePatches.forEach((restorePatch) => restorePatch()); 2533 } 2534 } 2535 2536 /****************************************************************************** 2537 Copyright (c) Microsoft Corporation. 2538 2539 Permission to use, copy, modify, and/or distribute this software for any 2540 purpose with or without fee is hereby granted. 2541 2542 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 2543 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 2544 AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 2545 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 2546 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 2547 OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 2548 PERFORMANCE OF THIS SOFTWARE. 2549 ***************************************************************************** */ 2550 2551 function __rest(s, e) { 2552 var t = {}; 2553 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 2554 t[p] = s[p]; 2555 if (s != null && typeof Object.getOwnPropertySymbols === "function") 2556 for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { 2557 if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) 2558 t[p[i]] = s[p[i]]; 2559 } 2560 return t; 2561 } 2562 2563 function initCanvas2DMutationObserver(cb, win, blockClass, unblockSelector, blockSelector, mirror) { 2564 const handlers = []; 2565 const props2D = Object.getOwnPropertyNames(win.CanvasRenderingContext2D.prototype); 2566 for (const prop of props2D) { 2567 try { 2568 if (typeof win.CanvasRenderingContext2D.prototype[prop] !== 'function') { 2569 continue; 2570 } 2571 const restoreHandler = patch(win.CanvasRenderingContext2D.prototype, prop, function (original) { 2572 return function (...args) { 2573 if (!isBlocked(this.canvas, blockClass, blockSelector, unblockSelector)) { 2574 setTimeout(() => { 2575 const recordArgs = [...args]; 2576 if (prop === 'drawImage') { 2577 if (recordArgs[0] && 2578 recordArgs[0] instanceof HTMLCanvasElement) { 2579 const canvas = recordArgs[0]; 2580 const ctx = canvas.getContext('2d'); 2581 let imgd = ctx === null || ctx === void 0 ? void 0 : ctx.getImageData(0, 0, canvas.width, canvas.height); 2582 let pix = imgd === null || imgd === void 0 ? void 0 : imgd.data; 2583 recordArgs[0] = JSON.stringify(pix); 2584 } 2585 } 2586 cb(this.canvas, { 2587 type: CanvasContext['2D'], 2588 property: prop, 2589 args: recordArgs, 2590 }); 2591 }, 0); 2592 } 2593 return original.apply(this, args); 2594 }; 2595 }); 2596 handlers.push(restoreHandler); 2597 } 2598 catch (_a) { 2599 const hookHandler = hookSetter(win.CanvasRenderingContext2D.prototype, prop, { 2600 set(v) { 2601 cb(this.canvas, { 2602 type: CanvasContext['2D'], 2603 property: prop, 2604 args: [v], 2605 setter: true, 2606 }); 2607 }, 2608 }); 2609 handlers.push(hookHandler); 2610 } 2611 } 2612 return () => { 2613 handlers.forEach((h) => h()); 2614 }; 2615 } 2616 2617 function initCanvasContextObserver(win, blockClass, blockSelector, unblockSelector) { 2618 const handlers = []; 2619 try { 2620 const restoreHandler = patch(win.HTMLCanvasElement.prototype, 'getContext', function (original) { 2621 return function (contextType, ...args) { 2622 if (!isBlocked(this, blockClass, blockSelector, unblockSelector)) { 2623 if (!('__context' in this)) 2624 this.__context = contextType; 2625 } 2626 return original.apply(this, [contextType, ...args]); 2627 }; 2628 }); 2629 handlers.push(restoreHandler); 2630 } 2631 catch (_a) { 2632 console.error('failed to patch HTMLCanvasElement.prototype.getContext'); 2633 } 2634 return () => { 2635 handlers.forEach((h) => h()); 2636 }; 2637 } 2638 2639 /* 2640 * base64-arraybuffer 1.0.2 <https://github.com/niklasvh/base64-arraybuffer> 2641 * Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com> 2642 * Released under MIT License 2643 */ 2644 var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 2645 // Use a lookup table to find the index. 2646 var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); 2647 for (var i = 0; i < chars.length; i++) { 2648 lookup[chars.charCodeAt(i)] = i; 2649 } 2650 var encode = function (arraybuffer) { 2651 var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; 2652 for (i = 0; i < len; i += 3) { 2653 base64 += chars[bytes[i] >> 2]; 2654 base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; 2655 base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; 2656 base64 += chars[bytes[i + 2] & 63]; 2657 } 2658 if (len % 3 === 2) { 2659 base64 = base64.substring(0, base64.length - 1) + '='; 2660 } 2661 else if (len % 3 === 1) { 2662 base64 = base64.substring(0, base64.length - 2) + '=='; 2663 } 2664 return base64; 2665 }; 2666 2667 const webGLVarMap = new Map(); 2668 function variableListFor(ctx, ctor) { 2669 let contextMap = webGLVarMap.get(ctx); 2670 if (!contextMap) { 2671 contextMap = new Map(); 2672 webGLVarMap.set(ctx, contextMap); 2673 } 2674 if (!contextMap.has(ctor)) { 2675 contextMap.set(ctor, []); 2676 } 2677 return contextMap.get(ctor); 2678 } 2679 const saveWebGLVar = (value, win, ctx) => { 2680 if (!value || 2681 !(isInstanceOfWebGLObject(value, win) || typeof value === 'object')) 2682 return; 2683 const name = value.constructor.name; 2684 const list = variableListFor(ctx, name); 2685 let index = list.indexOf(value); 2686 if (index === -1) { 2687 index = list.length; 2688 list.push(value); 2689 } 2690 return index; 2691 }; 2692 function serializeArg(value, win, ctx) { 2693 if (value instanceof Array) { 2694 return value.map((arg) => serializeArg(arg, win, ctx)); 2695 } 2696 else if (value === null) { 2697 return value; 2698 } 2699 else if (value instanceof Float32Array || 2700 value instanceof Float64Array || 2701 value instanceof Int32Array || 2702 value instanceof Uint32Array || 2703 value instanceof Uint8Array || 2704 value instanceof Uint16Array || 2705 value instanceof Int16Array || 2706 value instanceof Int8Array || 2707 value instanceof Uint8ClampedArray) { 2708 const name = value.constructor.name; 2709 return { 2710 rr_type: name, 2711 args: [Object.values(value)], 2712 }; 2713 } 2714 else if (value instanceof ArrayBuffer) { 2715 const name = value.constructor.name; 2716 const base64 = encode(value); 2717 return { 2718 rr_type: name, 2719 base64, 2720 }; 2721 } 2722 else if (value instanceof DataView) { 2723 const name = value.constructor.name; 2724 return { 2725 rr_type: name, 2726 args: [ 2727 serializeArg(value.buffer, win, ctx), 2728 value.byteOffset, 2729 value.byteLength, 2730 ], 2731 }; 2732 } 2733 else if (value instanceof HTMLImageElement) { 2734 const name = value.constructor.name; 2735 const { src } = value; 2736 return { 2737 rr_type: name, 2738 src, 2739 }; 2740 } 2741 else if (value instanceof ImageData) { 2742 const name = value.constructor.name; 2743 return { 2744 rr_type: name, 2745 args: [serializeArg(value.data, win, ctx), value.width, value.height], 2746 }; 2747 } 2748 else if (isInstanceOfWebGLObject(value, win) || typeof value === 'object') { 2749 const name = value.constructor.name; 2750 const index = saveWebGLVar(value, win, ctx); 2751 return { 2752 rr_type: name, 2753 index: index, 2754 }; 2755 } 2756 return value; 2757 } 2758 const serializeArgs = (args, win, ctx) => { 2759 return [...args].map((arg) => serializeArg(arg, win, ctx)); 2760 }; 2761 const isInstanceOfWebGLObject = (value, win) => { 2762 const webGLConstructorNames = [ 2763 'WebGLActiveInfo', 2764 'WebGLBuffer', 2765 'WebGLFramebuffer', 2766 'WebGLProgram', 2767 'WebGLRenderbuffer', 2768 'WebGLShader', 2769 'WebGLShaderPrecisionFormat', 2770 'WebGLTexture', 2771 'WebGLUniformLocation', 2772 'WebGLVertexArrayObject', 2773 'WebGLVertexArrayObjectOES', 2774 ]; 2775 const supportedWebGLConstructorNames = webGLConstructorNames.filter((name) => typeof win[name] === 'function'); 2776 return Boolean(supportedWebGLConstructorNames.find((name) => value instanceof win[name])); 2777 }; 2778 2779 function patchGLPrototype(prototype, type, cb, blockClass, unblockSelector, blockSelector, mirror, win) { 2780 const handlers = []; 2781 const props = Object.getOwnPropertyNames(prototype); 2782 for (const prop of props) { 2783 try { 2784 if (typeof prototype[prop] !== 'function') { 2785 continue; 2786 } 2787 const restoreHandler = patch(prototype, prop, function (original) { 2788 return function (...args) { 2789 const result = original.apply(this, args); 2790 saveWebGLVar(result, win, prototype); 2791 if (!isBlocked(this.canvas, blockClass, blockSelector, unblockSelector)) { 2792 const id = mirror.getId(this.canvas); 2793 const recordArgs = serializeArgs([...args], win, prototype); 2794 const mutation = { 2795 type, 2796 property: prop, 2797 args: recordArgs, 2798 }; 2799 cb(this.canvas, mutation); 2800 } 2801 return result; 2802 }; 2803 }); 2804 handlers.push(restoreHandler); 2805 } 2806 catch (_a) { 2807 const hookHandler = hookSetter(prototype, prop, { 2808 set(v) { 2809 cb(this.canvas, { 2810 type, 2811 property: prop, 2812 args: [v], 2813 setter: true, 2814 }); 2815 }, 2816 }); 2817 handlers.push(hookHandler); 2818 } 2819 } 2820 return handlers; 2821 } 2822 function initCanvasWebGLMutationObserver(cb, win, blockClass, blockSelector, unblockSelector, mirror) { 2823 const handlers = []; 2824 handlers.push(...patchGLPrototype(win.WebGLRenderingContext.prototype, CanvasContext.WebGL, cb, blockClass, blockSelector, unblockSelector, mirror, win)); 2825 if (typeof win.WebGL2RenderingContext !== 'undefined') { 2826 handlers.push(...patchGLPrototype(win.WebGL2RenderingContext.prototype, CanvasContext.WebGL2, cb, blockClass, blockSelector, unblockSelector, mirror, win)); 2827 } 2828 return () => { 2829 handlers.forEach((h) => h()); 2830 }; 2831 } 2832 2833 class CanvasManager { 2834 reset() { 2835 this.pendingCanvasMutations.clear(); 2836 this.resetObservers && this.resetObservers(); 2837 } 2838 freeze() { 2839 this.frozen = true; 2840 } 2841 unfreeze() { 2842 this.frozen = false; 2843 } 2844 lock() { 2845 this.locked = true; 2846 } 2847 unlock() { 2848 this.locked = false; 2849 } 2850 constructor(options) { 2851 this.pendingCanvasMutations = new Map(); 2852 this.rafStamps = { latestId: 0, invokeId: null }; 2853 this.frozen = false; 2854 this.locked = false; 2855 this.processMutation = function (target, mutation) { 2856 const newFrame = this.rafStamps.invokeId && 2857 this.rafStamps.latestId !== this.rafStamps.invokeId; 2858 if (newFrame || !this.rafStamps.invokeId) 2859 this.rafStamps.invokeId = this.rafStamps.latestId; 2860 if (!this.pendingCanvasMutations.has(target)) { 2861 this.pendingCanvasMutations.set(target, []); 2862 } 2863 this.pendingCanvasMutations.get(target).push(mutation); 2864 }; 2865 this.mutationCb = options.mutationCb; 2866 this.mirror = options.mirror; 2867 if (options.recordCanvas === true) 2868 this.initCanvasMutationObserver(options.win, options.blockClass, options.blockSelector, options.unblockSelector); 2869 } 2870 initCanvasMutationObserver(win, blockClass, unblockSelector, blockSelector) { 2871 this.startRAFTimestamping(); 2872 this.startPendingCanvasMutationFlusher(); 2873 const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, unblockSelector); 2874 const canvas2DReset = initCanvas2DMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector, unblockSelector, this.mirror); 2875 const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector, unblockSelector, this.mirror); 2876 this.resetObservers = () => { 2877 canvasContextReset(); 2878 canvas2DReset(); 2879 canvasWebGL1and2Reset(); 2880 }; 2881 } 2882 startPendingCanvasMutationFlusher() { 2883 requestAnimationFrame(() => this.flushPendingCanvasMutations()); 2884 } 2885 startRAFTimestamping() { 2886 const setLatestRAFTimestamp = (timestamp) => { 2887 this.rafStamps.latestId = timestamp; 2888 requestAnimationFrame(setLatestRAFTimestamp); 2889 }; 2890 requestAnimationFrame(setLatestRAFTimestamp); 2891 } 2892 flushPendingCanvasMutations() { 2893 this.pendingCanvasMutations.forEach((values, canvas) => { 2894 const id = this.mirror.getId(canvas); 2895 this.flushPendingCanvasMutationFor(canvas, id); 2896 }); 2897 requestAnimationFrame(() => this.flushPendingCanvasMutations()); 2898 } 2899 flushPendingCanvasMutationFor(canvas, id) { 2900 if (this.frozen || this.locked) { 2901 return; 2902 } 2903 const valuesWithType = this.pendingCanvasMutations.get(canvas); 2904 if (!valuesWithType || id === -1) 2905 return; 2906 const values = valuesWithType.map((value) => { 2907 const rest = __rest(value, ["type"]); 2908 return rest; 2909 }); 2910 const { type } = valuesWithType[0]; 2911 this.mutationCb({ id, type, commands: values }); 2912 this.pendingCanvasMutations.delete(canvas); 2913 } 2914 } 2915 2916 function wrapEvent(e) { 2917 return Object.assign(Object.assign({}, e), { timestamp: Date.now() }); 2918 } 2919 let wrappedEmit; 2920 let takeFullSnapshot; 2921 const mirror = createMirror(); 2922 function record(options = {}) { 2923 const { emit, checkoutEveryNms, checkoutEveryNth, blockClass = 'rr-block', blockSelector = null, unblockSelector = null, ignoreClass = 'rr-ignore', ignoreSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, maskInputSelector = null, unmaskTextSelector = null, unmaskInputSelector = null, inlineStylesheet = true, maskAllText = false, maskAllInputs, maskInputOptions: _maskInputOptions, slimDOMOptions: _slimDOMOptions, maskInputFn, maskTextFn, hooks, packFn, sampling = {}, mousemoveWait, recordCanvas = false, userTriggeredOnInput = false, collectFonts = false, inlineImages = false, plugins, keepIframeSrcFn = () => false, onMutation, } = options; 2924 if (!emit) { 2925 throw new Error('emit function is required'); 2926 } 2927 if (mousemoveWait !== undefined && sampling.mousemove === undefined) { 2928 sampling.mousemove = mousemoveWait; 2929 } 2930 const maskInputOptions = maskAllInputs === true 2931 ? { 2932 color: true, 2933 date: true, 2934 'datetime-local': true, 2935 email: true, 2936 month: true, 2937 number: true, 2938 range: true, 2939 search: true, 2940 tel: true, 2941 text: true, 2942 time: true, 2943 url: true, 2944 week: true, 2945 textarea: true, 2946 select: true, 2947 radio: true, 2948 checkbox: true, 2949 } 2950 : _maskInputOptions !== undefined 2951 ? _maskInputOptions 2952 : {}; 2953 const slimDOMOptions = _slimDOMOptions === true || _slimDOMOptions === 'all' 2954 ? { 2955 script: true, 2956 comment: true, 2957 headFavicon: true, 2958 headWhitespace: true, 2959 headMetaSocial: true, 2960 headMetaRobots: true, 2961 headMetaHttpEquiv: true, 2962 headMetaVerification: true, 2963 headMetaAuthorship: _slimDOMOptions === 'all', 2964 headMetaDescKeywords: _slimDOMOptions === 'all', 2965 } 2966 : _slimDOMOptions 2967 ? _slimDOMOptions 2968 : {}; 2969 polyfill(); 2970 let lastFullSnapshotEvent; 2971 let incrementalSnapshotCount = 0; 2972 const eventProcessor = (e) => { 2973 for (const plugin of plugins || []) { 2974 if (plugin.eventProcessor) { 2975 e = plugin.eventProcessor(e); 2976 } 2977 } 2978 if (packFn) { 2979 e = packFn(e); 2980 } 2981 return e; 2982 }; 2983 wrappedEmit = (e, isCheckout) => { 2984 var _a; 2985 if (((_a = mutationBuffers[0]) === null || _a === void 0 ? void 0 : _a.isFrozen()) && 2986 e.type !== EventType.FullSnapshot && 2987 !(e.type === EventType.IncrementalSnapshot && 2988 e.data.source === IncrementalSource.Mutation)) { 2989 mutationBuffers.forEach((buf) => buf.unfreeze()); 2990 } 2991 emit(eventProcessor(e), isCheckout); 2992 if (e.type === EventType.FullSnapshot) { 2993 lastFullSnapshotEvent = e; 2994 incrementalSnapshotCount = 0; 2995 } 2996 else if (e.type === EventType.IncrementalSnapshot) { 2997 if (e.data.source === IncrementalSource.Mutation && 2998 e.data.isAttachIframe) { 2999 return; 3000 } 3001 incrementalSnapshotCount++; 3002 const exceedCount = checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth; 3003 const exceedTime = checkoutEveryNms && 3004 e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms; 3005 if (exceedCount || exceedTime) { 3006 takeFullSnapshot(true); 3007 } 3008 } 3009 }; 3010 const wrappedMutationEmit = (m) => { 3011 wrappedEmit(wrapEvent({ 3012 type: EventType.IncrementalSnapshot, 3013 data: Object.assign({ source: IncrementalSource.Mutation }, m), 3014 })); 3015 }; 3016 const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({ 3017 type: EventType.IncrementalSnapshot, 3018 data: Object.assign({ source: IncrementalSource.Scroll }, p), 3019 })); 3020 const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({ 3021 type: EventType.IncrementalSnapshot, 3022 data: Object.assign({ source: IncrementalSource.CanvasMutation }, p), 3023 })); 3024 const iframeManager = new IframeManager({ 3025 mutationCb: wrappedMutationEmit, 3026 }); 3027 const canvasManager = new CanvasManager({ 3028 recordCanvas, 3029 mutationCb: wrappedCanvasMutationEmit, 3030 win: window, 3031 blockClass, 3032 blockSelector, 3033 unblockSelector, 3034 mirror, 3035 }); 3036 const shadowDomManager = new ShadowDomManager({ 3037 mutationCb: wrappedMutationEmit, 3038 scrollCb: wrappedScrollEmit, 3039 bypassOptions: { 3040 onMutation, 3041 blockClass, 3042 blockSelector, 3043 unblockSelector, 3044 maskTextClass, 3045 maskTextSelector, 3046 unmaskTextSelector, 3047 maskInputSelector, 3048 unmaskInputSelector, 3049 inlineStylesheet, 3050 maskAllText, 3051 maskInputOptions, 3052 maskTextFn, 3053 maskInputFn, 3054 recordCanvas, 3055 inlineImages, 3056 sampling, 3057 slimDOMOptions, 3058 iframeManager, 3059 canvasManager, 3060 }, 3061 mirror, 3062 }); 3063 takeFullSnapshot = (isCheckout = false) => { 3064 var _a, _b, _c, _d; 3065 wrappedEmit(wrapEvent({ 3066 type: EventType.Meta, 3067 data: { 3068 href: window.location.href, 3069 width: getWindowWidth(), 3070 height: getWindowHeight(), 3071 }, 3072 }), isCheckout); 3073 mutationBuffers.forEach((buf) => buf.lock()); 3074 const [node, idNodeMap] = snapshot(document, { 3075 blockClass, 3076 blockSelector, 3077 unblockSelector, 3078 maskTextClass, 3079 maskTextSelector, 3080 unmaskTextSelector, 3081 maskInputSelector, 3082 unmaskInputSelector, 3083 inlineStylesheet, 3084 maskAllText, 3085 maskAllInputs: maskInputOptions, 3086 maskTextFn, 3087 slimDOM: slimDOMOptions, 3088 recordCanvas, 3089 inlineImages, 3090 onSerialize: (n) => { 3091 if (isIframeINode(n)) { 3092 iframeManager.addIframe(n); 3093 } 3094 if (hasShadowRoot(n)) { 3095 shadowDomManager.addShadowRoot(n.shadowRoot, document); 3096 } 3097 }, 3098 onIframeLoad: (iframe, childSn) => { 3099 iframeManager.attachIframe(iframe, childSn); 3100 shadowDomManager.observeAttachShadow(iframe); 3101 }, 3102 keepIframeSrcFn, 3103 }); 3104 if (!node) { 3105 return console.warn('Failed to snapshot the document'); 3106 } 3107 mirror.map = idNodeMap; 3108 wrappedEmit(wrapEvent({ 3109 type: EventType.FullSnapshot, 3110 data: { 3111 node, 3112 initialOffset: { 3113 left: window.pageXOffset !== undefined 3114 ? window.pageXOffset 3115 : (document === null || document === void 0 ? void 0 : document.documentElement.scrollLeft) || 3116 ((_b = (_a = document === null || document === void 0 ? void 0 : document.body) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.scrollLeft) || 3117 (document === null || document === void 0 ? void 0 : document.body.scrollLeft) || 3118 0, 3119 top: window.pageYOffset !== undefined 3120 ? window.pageYOffset 3121 : (document === null || document === void 0 ? void 0 : document.documentElement.scrollTop) || 3122 ((_d = (_c = document === null || document === void 0 ? void 0 : document.body) === null || _c === void 0 ? void 0 : _c.parentElement) === null || _d === void 0 ? void 0 : _d.scrollTop) || 3123 (document === null || document === void 0 ? void 0 : document.body.scrollTop) || 3124 0, 3125 }, 3126 }, 3127 })); 3128 mutationBuffers.forEach((buf) => buf.unlock()); 3129 }; 3130 try { 3131 const handlers = []; 3132 handlers.push(on('DOMContentLoaded', () => { 3133 wrappedEmit(wrapEvent({ 3134 type: EventType.DomContentLoaded, 3135 data: {}, 3136 })); 3137 })); 3138 const observe = (doc) => { 3139 var _a; 3140 return callbackWrapper(initObservers)({ 3141 onMutation, 3142 mutationCb: wrappedMutationEmit, 3143 mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({ 3144 type: EventType.IncrementalSnapshot, 3145 data: { 3146 source, 3147 positions, 3148 }, 3149 })), 3150 mouseInteractionCb: (d) => wrappedEmit(wrapEvent({ 3151 type: EventType.IncrementalSnapshot, 3152 data: Object.assign({ source: IncrementalSource.MouseInteraction }, d), 3153 })), 3154 scrollCb: wrappedScrollEmit, 3155 viewportResizeCb: (d) => wrappedEmit(wrapEvent({ 3156 type: EventType.IncrementalSnapshot, 3157 data: Object.assign({ source: IncrementalSource.ViewportResize }, d), 3158 })), 3159 inputCb: (v) => wrappedEmit(wrapEvent({ 3160 type: EventType.IncrementalSnapshot, 3161 data: Object.assign({ source: IncrementalSource.Input }, v), 3162 })), 3163 mediaInteractionCb: (p) => wrappedEmit(wrapEvent({ 3164 type: EventType.IncrementalSnapshot, 3165 data: Object.assign({ source: IncrementalSource.MediaInteraction }, p), 3166 })), 3167 styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({ 3168 type: EventType.IncrementalSnapshot, 3169 data: Object.assign({ source: IncrementalSource.StyleSheetRule }, r), 3170 })), 3171 styleDeclarationCb: (r) => wrappedEmit(wrapEvent({ 3172 type: EventType.IncrementalSnapshot, 3173 data: Object.assign({ source: IncrementalSource.StyleDeclaration }, r), 3174 })), 3175 canvasMutationCb: wrappedCanvasMutationEmit, 3176 fontCb: (p) => wrappedEmit(wrapEvent({ 3177 type: EventType.IncrementalSnapshot, 3178 data: Object.assign({ source: IncrementalSource.Font }, p), 3179 })), 3180 blockClass, 3181 ignoreClass, 3182 ignoreSelector, 3183 maskTextClass, 3184 maskTextSelector, 3185 unmaskTextSelector, 3186 maskInputSelector, 3187 unmaskInputSelector, 3188 maskInputOptions, 3189 inlineStylesheet, 3190 sampling, 3191 recordCanvas, 3192 inlineImages, 3193 userTriggeredOnInput, 3194 collectFonts, 3195 doc, 3196 maskAllText, 3197 maskInputFn, 3198 maskTextFn, 3199 blockSelector, 3200 unblockSelector, 3201 slimDOMOptions, 3202 mirror, 3203 iframeManager, 3204 shadowDomManager, 3205 canvasManager, 3206 plugins: ((_a = plugins === null || plugins === void 0 ? void 0 : plugins.filter((p) => p.observer)) === null || _a === void 0 ? void 0 : _a.map((p) => ({ 3207 observer: p.observer, 3208 options: p.options, 3209 callback: (payload) => wrappedEmit(wrapEvent({ 3210 type: EventType.Plugin, 3211 data: { 3212 plugin: p.name, 3213 payload, 3214 }, 3215 })), 3216 }))) || [], 3217 }, hooks); 3218 }; 3219 iframeManager.addLoadListener((iframeEl) => { 3220 try { 3221 handlers.push(observe(iframeEl.contentDocument)); 3222 } 3223 catch (error) { 3224 console.warn(error); 3225 } 3226 }); 3227 const init = () => { 3228 takeFullSnapshot(); 3229 handlers.push(observe(document)); 3230 }; 3231 if (document.readyState === 'interactive' || 3232 document.readyState === 'complete') { 3233 init(); 3234 } 3235 else { 3236 handlers.push(on('load', () => { 3237 wrappedEmit(wrapEvent({ 3238 type: EventType.Load, 3239 data: {}, 3240 })); 3241 init(); 3242 }, window)); 3243 } 3244 return () => { 3245 handlers.forEach((h) => h()); 3246 }; 3247 } 3248 catch (error) { 3249 console.warn(error); 3250 } 3251 } 3252 record.addCustomEvent = (tag, payload) => { 3253 if (!wrappedEmit) { 3254 throw new Error('please add custom event after start recording'); 3255 } 3256 wrappedEmit(wrapEvent({ 3257 type: EventType.Custom, 3258 data: { 3259 tag, 3260 payload, 3261 }, 3262 })); 3263 }; 3264 record.freezePage = () => { 3265 mutationBuffers.forEach((buf) => buf.freeze()); 3266 }; 3267 record.takeFullSnapshot = (isCheckout) => { 3268 if (!takeFullSnapshot) { 3269 throw new Error('please take full snapshot after start recording'); 3270 } 3271 takeFullSnapshot(isCheckout); 3272 }; 3273 record.mirror = mirror; 3274 3275 /** 3276 * Add a breadcrumb event to replay. 3277 */ 3278 function addBreadcrumbEvent(replay, breadcrumb) { 3279 if (breadcrumb.category === 'sentry.transaction') { 3280 return; 3281 } 3282 3283 if (['ui.click', 'ui.input'].includes(breadcrumb.category )) { 3284 replay.triggerUserActivity(); 3285 } else { 3286 replay.checkAndHandleExpiredSession(); 3287 } 3288 3289 replay.addUpdate(() => { 3290 void replay.throttledAddEvent({ 3291 type: EventType.Custom, 3292 // TODO: We were converting from ms to seconds for breadcrumbs, spans, 3293 // but maybe we should just keep them as milliseconds 3294 timestamp: (breadcrumb.timestamp || 0) * 1000, 3295 data: { 3296 tag: 'breadcrumb', 3297 // normalize to max. 10 depth and 1_000 properties per object 3298 payload: normalize(breadcrumb, 10, 1000), 3299 }, 3300 }); 3301 3302 // Do not flush after console log messages 3303 return breadcrumb.category === 'console'; 3304 }); 3305 } 3306 3307 const INTERACTIVE_SELECTOR = 'button,a'; 3308 3309 /** 3310 * For clicks, we check if the target is inside of a button or link 3311 * If so, we use this as the target instead 3312 * This is useful because if you click on the image in <button><img></button>, 3313 * The target will be the image, not the button, which we don't want here 3314 */ 3315 function getClickTargetNode(event) { 3316 const target = getTargetNode(event); 3317 3318 if (!target || !(target instanceof Element)) { 3319 return target; 3320 } 3321 3322 const closestInteractive = target.closest(INTERACTIVE_SELECTOR); 3323 return closestInteractive || target; 3324 } 3325 3326 /** Get the event target node. */ 3327 function getTargetNode(event) { 3328 if (isEventWithTarget(event)) { 3329 return event.target ; 3330 } 3331 3332 return event; 3333 } 3334 3335 function isEventWithTarget(event) { 3336 return typeof event === 'object' && !!event && 'target' in event; 3337 } 3338 3339 let handlers; 3340 3341 /** 3342 * Register a handler to be called when `window.open()` is called. 3343 * Returns a cleanup function. 3344 */ 3345 function onWindowOpen(cb) { 3346 // Ensure to only register this once 3347 if (!handlers) { 3348 handlers = []; 3349 monkeyPatchWindowOpen(); 3350 } 3351 3352 handlers.push(cb); 3353 3354 return () => { 3355 const pos = handlers ? handlers.indexOf(cb) : -1; 3356 if (pos > -1) { 3357 (handlers ).splice(pos, 1); 3358 } 3359 }; 3360 } 3361 3362 function monkeyPatchWindowOpen() { 3363 fill(WINDOW, 'open', function (originalWindowOpen) { 3364 return function (...args) { 3365 if (handlers) { 3366 try { 3367 handlers.forEach(handler => handler()); 3368 } catch (e) { 3369 // ignore errors in here 3370 } 3371 } 3372 3373 return originalWindowOpen.apply(WINDOW, args); 3374 }; 3375 }); 3376 } 3377 3378 /** Handle a click. */ 3379 function handleClick(clickDetector, clickBreadcrumb, node) { 3380 clickDetector.handleClick(clickBreadcrumb, node); 3381 } 3382 3383 /** A click detector class that can be used to detect slow or rage clicks on elements. */ 3384 class ClickDetector { 3385 // protected for testing 3386 __init() {this._lastMutation = 0;} 3387 __init2() {this._lastScroll = 0;} 3388 3389 __init3() {this._clicks = [];} 3390 3391 constructor( 3392 replay, 3393 slowClickConfig, 3394 // Just for easier testing 3395 _addBreadcrumbEvent = addBreadcrumbEvent, 3396 ) {ClickDetector.prototype.__init.call(this);ClickDetector.prototype.__init2.call(this);ClickDetector.prototype.__init3.call(this); 3397 // We want everything in s, but options are in ms 3398 this._timeout = slowClickConfig.timeout / 1000; 3399 this._multiClickTimeout = slowClickConfig.multiClickTimeout / 1000; 3400 this._threshold = slowClickConfig.threshold / 1000; 3401 this._scollTimeout = slowClickConfig.scrollTimeout / 1000; 3402 this._replay = replay; 3403 this._ignoreSelector = slowClickConfig.ignoreSelector; 3404 this._addBreadcrumbEvent = _addBreadcrumbEvent; 3405 } 3406 3407 /** Register click detection handlers on mutation or scroll. */ 3408 addListeners() { 3409 const mutationHandler = () => { 3410 this._lastMutation = nowInSeconds(); 3411 }; 3412 3413 const scrollHandler = () => { 3414 this._lastScroll = nowInSeconds(); 3415 }; 3416 3417 const cleanupWindowOpen = onWindowOpen(() => { 3418 // Treat window.open as mutation 3419 this._lastMutation = nowInSeconds(); 3420 }); 3421 3422 const clickHandler = (event) => { 3423 if (!event.target) { 3424 return; 3425 } 3426 3427 const node = getClickTargetNode(event); 3428 if (node) { 3429 this._handleMultiClick(node ); 3430 } 3431 }; 3432 3433 const obs = new MutationObserver(mutationHandler); 3434 3435 obs.observe(WINDOW.document.documentElement, { 3436 attributes: true, 3437 characterData: true, 3438 childList: true, 3439 subtree: true, 3440 }); 3441 3442 WINDOW.addEventListener('scroll', scrollHandler, { passive: true }); 3443 WINDOW.addEventListener('click', clickHandler, { passive: true }); 3444 3445 this._teardown = () => { 3446 WINDOW.removeEventListener('scroll', scrollHandler); 3447 WINDOW.removeEventListener('click', clickHandler); 3448 cleanupWindowOpen(); 3449 3450 obs.disconnect(); 3451 this._clicks = []; 3452 this._lastMutation = 0; 3453 this._lastScroll = 0; 3454 }; 3455 } 3456 3457 /** Clean up listeners. */ 3458 removeListeners() { 3459 if (this._teardown) { 3460 this._teardown(); 3461 } 3462 3463 if (this._checkClickTimeout) { 3464 clearTimeout(this._checkClickTimeout); 3465 } 3466 } 3467 3468 /** Handle a click */ 3469 handleClick(breadcrumb, node) { 3470 if (ignoreElement(node, this._ignoreSelector) || !isClickBreadcrumb(breadcrumb)) { 3471 return; 3472 } 3473 3474 const click = this._getClick(node); 3475 3476 if (click) { 3477 // this means a click on the same element was captured in the last 1s, so we consider this a multi click 3478 return; 3479 } 3480 3481 const newClick = { 3482 timestamp: breadcrumb.timestamp, 3483 clickBreadcrumb: breadcrumb, 3484 // Set this to 0 so we know it originates from the click breadcrumb 3485 clickCount: 0, 3486 node, 3487 }; 3488 this._clicks.push(newClick); 3489 3490 // If this is the first new click, set a timeout to check for multi clicks 3491 if (this._clicks.length === 1) { 3492 this._scheduleCheckClicks(); 3493 } 3494 } 3495 3496 /** Count multiple clicks on elements. */ 3497 _handleMultiClick(node) { 3498 const click = this._getClick(node); 3499 3500 if (!click) { 3501 return; 3502 } 3503 3504 click.clickCount++; 3505 } 3506 3507 /** Try to get an existing click on the given element. */ 3508 _getClick(node) { 3509 const now = nowInSeconds(); 3510 3511 // Find any click on the same element in the last second 3512 // If one exists, we consider this click as a double/triple/etc click 3513 return this._clicks.find(click => click.node === node && now - click.timestamp < this._multiClickTimeout); 3514 } 3515 3516 /** Check the clicks that happened. */ 3517 _checkClicks() { 3518 const timedOutClicks = []; 3519 3520 const now = nowInSeconds(); 3521 3522 this._clicks.forEach(click => { 3523 if (!click.mutationAfter && this._lastMutation) { 3524 click.mutationAfter = click.timestamp <= this._lastMutation ? this._lastMutation - click.timestamp : undefined; 3525 } 3526 if (!click.scrollAfter && this._lastScroll) { 3527 click.scrollAfter = click.timestamp <= this._lastScroll ? this._lastScroll - click.timestamp : undefined; 3528 } 3529 3530 // If an action happens after the multi click threshold, we can skip waiting and handle the click right away 3531 const actionTime = click.scrollAfter || click.mutationAfter || 0; 3532 if (actionTime && actionTime >= this._multiClickTimeout) { 3533 timedOutClicks.push(click); 3534 return; 3535 } 3536 3537 if (click.timestamp + this._timeout <= now) { 3538 timedOutClicks.push(click); 3539 } 3540 }); 3541 3542 // Remove "old" clicks 3543 for (const click of timedOutClicks) { 3544 this._generateBreadcrumbs(click); 3545 3546 const pos = this._clicks.indexOf(click); 3547 if (pos !== -1) { 3548 this._clicks.splice(pos, 1); 3549 } 3550 } 3551 3552 // Trigger new check, unless no clicks left 3553 if (this._clicks.length) { 3554 this._scheduleCheckClicks(); 3555 } 3556 } 3557 3558 /** Generate matching breadcrumb(s) for the click. */ 3559 _generateBreadcrumbs(click) { 3560 const replay = this._replay; 3561 const hadScroll = click.scrollAfter && click.scrollAfter <= this._scollTimeout; 3562 const hadMutation = click.mutationAfter && click.mutationAfter <= this._threshold; 3563 3564 const isSlowClick = !hadScroll && !hadMutation; 3565 const { clickCount, clickBreadcrumb } = click; 3566 3567 // Slow click 3568 if (isSlowClick) { 3569 // If `mutationAfter` is set, it means a mutation happened after the threshold, but before the timeout 3570 // If not, it means we just timed out without scroll & mutation 3571 const timeAfterClickMs = Math.min(click.mutationAfter || this._timeout, this._timeout) * 1000; 3572 const endReason = timeAfterClickMs < this._timeout * 1000 ? 'mutation' : 'timeout'; 3573 3574 const breadcrumb = { 3575 type: 'default', 3576 message: clickBreadcrumb.message, 3577 timestamp: clickBreadcrumb.timestamp, 3578 category: 'ui.slowClickDetected', 3579 data: { 3580 ...clickBreadcrumb.data, 3581 url: WINDOW.location.href, 3582 route: replay.getCurrentRoute(), 3583 timeAfterClickMs, 3584 endReason, 3585 // If clickCount === 0, it means multiClick was not correctly captured here 3586 // - we still want to send 1 in this case 3587 clickCount: clickCount || 1, 3588 }, 3589 }; 3590 3591 this._addBreadcrumbEvent(replay, breadcrumb); 3592 return; 3593 } 3594 3595 // Multi click 3596 if (clickCount > 1) { 3597 const breadcrumb = { 3598 type: 'default', 3599 message: clickBreadcrumb.message, 3600 timestamp: clickBreadcrumb.timestamp, 3601 category: 'ui.multiClick', 3602 data: { 3603 ...clickBreadcrumb.data, 3604 url: WINDOW.location.href, 3605 route: replay.getCurrentRoute(), 3606 clickCount, 3607 metric: true, 3608 }, 3609 }; 3610 3611 this._addBreadcrumbEvent(replay, breadcrumb); 3612 } 3613 } 3614 3615 /** Schedule to check current clicks. */ 3616 _scheduleCheckClicks() { 3617 this._checkClickTimeout = setTimeout(() => this._checkClicks(), 1000); 3618 } 3619 } 3620 3621 const SLOW_CLICK_TAGS = ['A', 'BUTTON', 'INPUT']; 3622 3623 /** exported for tests only */ 3624 function ignoreElement(node, ignoreSelector) { 3625 if (!SLOW_CLICK_TAGS.includes(node.tagName)) { 3626 return true; 3627 } 3628 3629 // If <input> tag, we only want to consider input[type='submit'] & input[type='button'] 3630 if (node.tagName === 'INPUT' && !['submit', 'button'].includes(node.getAttribute('type') || '')) { 3631 return true; 3632 } 3633 3634 // If <a> tag, detect special variants that may not lead to an action 3635 // If target !== _self, we may open the link somewhere else, which would lead to no action 3636 // Also, when downloading a file, we may not leave the page, but still not trigger an action 3637 if ( 3638 node.tagName === 'A' && 3639 (node.hasAttribute('download') || (node.hasAttribute('target') && node.getAttribute('target') !== '_self')) 3640 ) { 3641 return true; 3642 } 3643 3644 if (ignoreSelector && node.matches(ignoreSelector)) { 3645 return true; 3646 } 3647 3648 return false; 3649 } 3650 3651 function isClickBreadcrumb(breadcrumb) { 3652 return !!(breadcrumb.data && typeof breadcrumb.data.nodeId === 'number' && breadcrumb.timestamp); 3653 } 3654 3655 // This is good enough for us, and is easier to test/mock than `timestampInSeconds` 3656 function nowInSeconds() { 3657 return Date.now() / 1000; 3658 } 3659 3660 /** 3661 * Create a breadcrumb for a replay. 3662 */ 3663 function createBreadcrumb( 3664 breadcrumb, 3665 ) { 3666 return { 3667 timestamp: Date.now() / 1000, 3668 type: 'default', 3669 ...breadcrumb, 3670 }; 3671 } 3672 3673 var NodeType; 3674 (function (NodeType) { 3675 NodeType[NodeType["Document"] = 0] = "Document"; 3676 NodeType[NodeType["DocumentType"] = 1] = "DocumentType"; 3677 NodeType[NodeType["Element"] = 2] = "Element"; 3678 NodeType[NodeType["Text"] = 3] = "Text"; 3679 NodeType[NodeType["CDATA"] = 4] = "CDATA"; 3680 NodeType[NodeType["Comment"] = 5] = "Comment"; 3681 })(NodeType || (NodeType = {})); 3682 3683 // Note that these are the serialized attributes and not attributes directly on 3684 // the DOM Node. Attributes we are interested in: 3685 const ATTRIBUTES_TO_RECORD = new Set([ 3686 'id', 3687 'class', 3688 'aria-label', 3689 'role', 3690 'name', 3691 'alt', 3692 'title', 3693 'data-test-id', 3694 'data-testid', 3695 'disabled', 3696 'aria-disabled', 3697 ]); 3698 3699 /** 3700 * Inclusion list of attributes that we want to record from the DOM element 3701 */ 3702 function getAttributesToRecord(attributes) { 3703 const obj = {}; 3704 for (const key in attributes) { 3705 if (ATTRIBUTES_TO_RECORD.has(key)) { 3706 let normalizedKey = key; 3707 3708 if (key === 'data-testid' || key === 'data-test-id') { 3709 normalizedKey = 'testId'; 3710 } 3711 3712 obj[normalizedKey] = attributes[key]; 3713 } 3714 } 3715 3716 return obj; 3717 } 3718 3719 const handleDomListener = ( 3720 replay, 3721 ) => { 3722 return (handlerData) => { 3723 if (!replay.isEnabled()) { 3724 return; 3725 } 3726 3727 const result = handleDom(handlerData); 3728 3729 if (!result) { 3730 return; 3731 } 3732 3733 const isClick = handlerData.name === 'click'; 3734 const event = isClick && (handlerData.event ); 3735 // Ignore clicks if ctrl/alt/meta keys are held down as they alter behavior of clicks (e.g. open in new tab) 3736 if (isClick && replay.clickDetector && event && !event.altKey && !event.metaKey && !event.ctrlKey) { 3737 handleClick( 3738 replay.clickDetector, 3739 result , 3740 getClickTargetNode(handlerData.event) , 3741 ); 3742 } 3743 3744 addBreadcrumbEvent(replay, result); 3745 }; 3746 }; 3747 3748 /** Get the base DOM breadcrumb. */ 3749 function getBaseDomBreadcrumb(target, message) { 3750 // `__sn` property is the serialized node created by rrweb 3751 const serializedNode = target && isRrwebNode(target) && target.__sn.type === NodeType.Element ? target.__sn : null; 3752 3753 return { 3754 message, 3755 data: serializedNode 3756 ? { 3757 nodeId: serializedNode.id, 3758 node: { 3759 id: serializedNode.id, 3760 tagName: serializedNode.tagName, 3761 textContent: target 3762 ? Array.from(target.childNodes) 3763 .map( 3764 (node) => '__sn' in node && node.__sn.type === NodeType.Text && node.__sn.textContent, 3765 ) 3766 .filter(Boolean) // filter out empty values 3767 .map(text => (text ).trim()) 3768 .join('') 3769 : '', 3770 attributes: getAttributesToRecord(serializedNode.attributes), 3771 }, 3772 } 3773 : {}, 3774 }; 3775 } 3776 3777 /** 3778 * An event handler to react to DOM events. 3779 * Exported for tests. 3780 */ 3781 function handleDom(handlerData) { 3782 const { target, message } = getDomTarget(handlerData); 3783 3784 return createBreadcrumb({ 3785 category: `ui.${handlerData.name}`, 3786 ...getBaseDomBreadcrumb(target, message), 3787 }); 3788 } 3789 3790 function getDomTarget(handlerData) { 3791 const isClick = handlerData.name === 'click'; 3792 3793 let message; 3794 let target = null; 3795 3796 // Accessing event.target can throw (see getsentry/raven-js#838, #768) 3797 try { 3798 target = isClick ? getClickTargetNode(handlerData.event) : getTargetNode(handlerData.event); 3799 message = htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>'; 3800 } catch (e) { 3801 message = '<unknown>'; 3802 } 3803 3804 return { target, message }; 3805 } 3806 3807 function isRrwebNode(node) { 3808 return '__sn' in node; 3809 } 3810 3811 /** Handle keyboard events & create breadcrumbs. */ 3812 function handleKeyboardEvent(replay, event) { 3813 if (!replay.isEnabled()) { 3814 return; 3815 } 3816 3817 // Update user activity, but do not restart recording as it can create 3818 // noisy/low-value replays (e.g. user comes back from idle, hits alt-tab, new 3819 // session with a single "keydown" breadcrumb is created) 3820 replay.updateUserActivity(); 3821 3822 const breadcrumb = getKeyboardBreadcrumb(event); 3823 3824 if (!breadcrumb) { 3825 return; 3826 } 3827 3828 addBreadcrumbEvent(replay, breadcrumb); 3829 } 3830 3831 /** exported only for tests */ 3832 function getKeyboardBreadcrumb(event) { 3833 const { metaKey, shiftKey, ctrlKey, altKey, key, target } = event; 3834 3835 // never capture for input fields 3836 if (!target || isInputElement(target ) || !key) { 3837 return null; 3838 } 3839 3840 // Note: We do not consider shift here, as that means "uppercase" 3841 const hasModifierKey = metaKey || ctrlKey || altKey; 3842 const isCharacterKey = key.length === 1; // other keys like Escape, Tab, etc have a longer length 3843 3844 // Do not capture breadcrumb if only a word key is pressed 3845 // This could leak e.g. user input 3846 if (!hasModifierKey && isCharacterKey) { 3847 return null; 3848 } 3849 3850 const message = htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>'; 3851 const baseBreadcrumb = getBaseDomBreadcrumb(target , message); 3852 3853 return createBreadcrumb({ 3854 category: 'ui.keyDown', 3855 message, 3856 data: { 3857 ...baseBreadcrumb.data, 3858 metaKey, 3859 shiftKey, 3860 ctrlKey, 3861 altKey, 3862 key, 3863 }, 3864 }); 3865 } 3866 3867 function isInputElement(target) { 3868 return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable; 3869 } 3870 3871 const NAVIGATION_ENTRY_KEYS = [ 3872 'name', 3873 'type', 3874 'startTime', 3875 'transferSize', 3876 'duration', 3877 ]; 3878 3879 function isNavigationEntryEqual(a) { 3880 return function (b) { 3881 return NAVIGATION_ENTRY_KEYS.every(key => a[key] === b[key]); 3882 }; 3883 } 3884 3885 /** 3886 * There are some difficulties diagnosing why there are duplicate navigation 3887 * entries. We've witnessed several intermittent results: 3888 * - duplicate entries have duration = 0 3889 * - duplicate entries are the same object reference 3890 * - none of the above 3891 * 3892 * Compare the values of several keys to determine if the entries are duplicates or not. 3893 */ 3894 // TODO (high-prio): Figure out wth is returned here 3895 // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 3896 function dedupePerformanceEntries( 3897 currentList, 3898 newList, 3899 ) { 3900 // Partition `currentList` into 3 different lists based on entryType 3901 const [existingNavigationEntries, existingLcpEntries, existingEntries] = currentList.reduce( 3902 (acc, entry) => { 3903 if (entry.entryType === 'navigation') { 3904 acc[0].push(entry ); 3905 } else if (entry.entryType === 'largest-contentful-paint') { 3906 acc[1].push(entry ); 3907 } else { 3908 acc[2].push(entry); 3909 } 3910 return acc; 3911 }, 3912 [[], [], []], 3913 ); 3914 3915 const newEntries = []; 3916 const newNavigationEntries = []; 3917 let newLcpEntry = existingLcpEntries.length 3918 ? existingLcpEntries[existingLcpEntries.length - 1] // Take the last element as list is sorted 3919 : undefined; 3920 3921 newList.forEach(entry => { 3922 if (entry.entryType === 'largest-contentful-paint') { 3923 // We want the latest LCP event only 3924 if (!newLcpEntry || newLcpEntry.startTime < entry.startTime) { 3925 newLcpEntry = entry; 3926 } 3927 return; 3928 } 3929 3930 if (entry.entryType === 'navigation') { 3931 const navigationEntry = entry ; 3932 3933 // Check if the navigation entry is contained in currentList or newList 3934 if ( 3935 // Ignore any navigation entries with duration 0, as they are likely duplicates 3936 entry.duration > 0 && 3937 // Ensure new entry does not already exist in existing entries 3938 !existingNavigationEntries.find(isNavigationEntryEqual(navigationEntry)) && 3939 // Ensure new entry does not already exist in new list of navigation entries 3940 !newNavigationEntries.find(isNavigationEntryEqual(navigationEntry)) 3941 ) { 3942 newNavigationEntries.push(navigationEntry); 3943 } 3944 3945 // Otherwise this navigation entry is considered a duplicate and is thrown away 3946 return; 3947 } 3948 3949 newEntries.push(entry); 3950 }); 3951 3952 // Re-combine and sort by startTime 3953 return [ 3954 ...(newLcpEntry ? [newLcpEntry] : []), 3955 ...existingNavigationEntries, 3956 ...existingEntries, 3957 ...newEntries, 3958 ...newNavigationEntries, 3959 ].sort((a, b) => a.startTime - b.startTime); 3960 } 3961 3962 /** 3963 * Sets up a PerformanceObserver to listen to all performance entry types. 3964 */ 3965 function setupPerformanceObserver(replay) { 3966 const performanceObserverHandler = (list) => { 3967 // For whatever reason the observer was returning duplicate navigation 3968 // entries (the other entry types were not duplicated). 3969 const newPerformanceEntries = dedupePerformanceEntries( 3970 replay.performanceEvents, 3971 list.getEntries() , 3972 ); 3973 replay.performanceEvents = newPerformanceEntries; 3974 }; 3975 3976 const performanceObserver = new PerformanceObserver(performanceObserverHandler); 3977 3978 [ 3979 'element', 3980 'event', 3981 'first-input', 3982 'largest-contentful-paint', 3983 'layout-shift', 3984 'longtask', 3985 'navigation', 3986 'paint', 3987 'resource', 3988 ].forEach(type => { 3989 try { 3990 performanceObserver.observe({ 3991 type, 3992 buffered: true, 3993 }); 3994 } catch (e) { 3995 // This can throw if an entry type is not supported in the browser. 3996 // Ignore these errors. 3997 } 3998 }); 3999 4000 return performanceObserver; 4001 } 4002 4003 const r = `/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */ 4004 function t(t){let e=t.length;for(;--e>=0;)t[e]=0}const e=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),a=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),i=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),n=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),s=new Array(576);t(s);const r=new Array(60);t(r);const o=new Array(512);t(o);const l=new Array(256);t(l);const h=new Array(29);t(h);const d=new Array(30);function _(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}let f,c,u;function w(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}t(d);const m=t=>t<256?o[t]:o[256+(t>>>7)],b=(t,e)=>{t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255},g=(t,e,a)=>{t.bi_valid>16-a?(t.bi_buf|=e<<t.bi_valid&65535,b(t,t.bi_buf),t.bi_buf=e>>16-t.bi_valid,t.bi_valid+=a-16):(t.bi_buf|=e<<t.bi_valid&65535,t.bi_valid+=a)},p=(t,e,a)=>{g(t,a[2*e],a[2*e+1])},k=(t,e)=>{let a=0;do{a|=1&t,t>>>=1,a<<=1}while(--e>0);return a>>>1},v=(t,e,a)=>{const i=new Array(16);let n,s,r=0;for(n=1;n<=15;n++)r=r+a[n-1]<<1,i[n]=r;for(s=0;s<=e;s++){let e=t[2*s+1];0!==e&&(t[2*s]=k(i[e]++,e))}},y=t=>{let e;for(e=0;e<286;e++)t.dyn_ltree[2*e]=0;for(e=0;e<30;e++)t.dyn_dtree[2*e]=0;for(e=0;e<19;e++)t.bl_tree[2*e]=0;t.dyn_ltree[512]=1,t.opt_len=t.static_len=0,t.sym_next=t.matches=0},x=t=>{t.bi_valid>8?b(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0},z=(t,e,a,i)=>{const n=2*e,s=2*a;return t[n]<t[s]||t[n]===t[s]&&i[e]<=i[a]},A=(t,e,a)=>{const i=t.heap[a];let n=a<<1;for(;n<=t.heap_len&&(n<t.heap_len&&z(e,t.heap[n+1],t.heap[n],t.depth)&&n++,!z(e,i,t.heap[n],t.depth));)t.heap[a]=t.heap[n],a=n,n<<=1;t.heap[a]=i},E=(t,i,n)=>{let s,r,o,_,f=0;if(0!==t.sym_next)do{s=255&t.pending_buf[t.sym_buf+f++],s+=(255&t.pending_buf[t.sym_buf+f++])<<8,r=t.pending_buf[t.sym_buf+f++],0===s?p(t,r,i):(o=l[r],p(t,o+256+1,i),_=e[o],0!==_&&(r-=h[o],g(t,r,_)),s--,o=m(s),p(t,o,n),_=a[o],0!==_&&(s-=d[o],g(t,s,_)))}while(f<t.sym_next);p(t,256,i)},R=(t,e)=>{const a=e.dyn_tree,i=e.stat_desc.static_tree,n=e.stat_desc.has_stree,s=e.stat_desc.elems;let r,o,l,h=-1;for(t.heap_len=0,t.heap_max=573,r=0;r<s;r++)0!==a[2*r]?(t.heap[++t.heap_len]=h=r,t.depth[r]=0):a[2*r+1]=0;for(;t.heap_len<2;)l=t.heap[++t.heap_len]=h<2?++h:0,a[2*l]=1,t.depth[l]=0,t.opt_len--,n&&(t.static_len-=i[2*l+1]);for(e.max_code=h,r=t.heap_len>>1;r>=1;r--)A(t,a,r);l=s;do{r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],A(t,a,1),o=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=o,a[2*l]=a[2*r]+a[2*o],t.depth[l]=(t.depth[r]>=t.depth[o]?t.depth[r]:t.depth[o])+1,a[2*r+1]=a[2*o+1]=l,t.heap[1]=l++,A(t,a,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],((t,e)=>{const a=e.dyn_tree,i=e.max_code,n=e.stat_desc.static_tree,s=e.stat_desc.has_stree,r=e.stat_desc.extra_bits,o=e.stat_desc.extra_base,l=e.stat_desc.max_length;let h,d,_,f,c,u,w=0;for(f=0;f<=15;f++)t.bl_count[f]=0;for(a[2*t.heap[t.heap_max]+1]=0,h=t.heap_max+1;h<573;h++)d=t.heap[h],f=a[2*a[2*d+1]+1]+1,f>l&&(f=l,w++),a[2*d+1]=f,d>i||(t.bl_count[f]++,c=0,d>=o&&(c=r[d-o]),u=a[2*d],t.opt_len+=u*(f+c),s&&(t.static_len+=u*(n[2*d+1]+c)));if(0!==w){do{for(f=l-1;0===t.bl_count[f];)f--;t.bl_count[f]--,t.bl_count[f+1]+=2,t.bl_count[l]--,w-=2}while(w>0);for(f=l;0!==f;f--)for(d=t.bl_count[f];0!==d;)_=t.heap[--h],_>i||(a[2*_+1]!==f&&(t.opt_len+=(f-a[2*_+1])*a[2*_],a[2*_+1]=f),d--)}})(t,e),v(a,h,t.bl_count)},Z=(t,e,a)=>{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=r,r=e[2*(i+1)+1],++o<l&&n===r||(o<h?t.bl_tree[2*n]+=o:0!==n?(n!==s&&t.bl_tree[2*n]++,t.bl_tree[32]++):o<=10?t.bl_tree[34]++:t.bl_tree[36]++,o=0,s=n,0===r?(l=138,h=3):n===r?(l=6,h=3):(l=7,h=4))},U=(t,e,a)=>{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),i=0;i<=a;i++)if(n=r,r=e[2*(i+1)+1],!(++o<l&&n===r)){if(o<h)do{p(t,n,t.bl_tree)}while(0!=--o);else 0!==n?(n!==s&&(p(t,n,t.bl_tree),o--),p(t,16,t.bl_tree),g(t,o-3,2)):o<=10?(p(t,17,t.bl_tree),g(t,o-3,3)):(p(t,18,t.bl_tree),g(t,o-11,7));o=0,s=n,0===r?(l=138,h=3):n===r?(l=6,h=3):(l=7,h=4)}};let S=!1;const D=(t,e,a,i)=>{g(t,0+(i?1:0),3),x(t),b(t,a),b(t,~a),a&&t.pending_buf.set(t.window.subarray(e,e+a),t.pending),t.pending+=a};var T=(t,e,a,i)=>{let o,l,h=0;t.level>0?(2===t.strm.data_type&&(t.strm.data_type=(t=>{let e,a=4093624447;for(e=0;e<=31;e++,a>>>=1)if(1&a&&0!==t.dyn_ltree[2*e])return 0;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return 1;for(e=32;e<256;e++)if(0!==t.dyn_ltree[2*e])return 1;return 0})(t)),R(t,t.l_desc),R(t,t.d_desc),h=(t=>{let e;for(Z(t,t.dyn_ltree,t.l_desc.max_code),Z(t,t.dyn_dtree,t.d_desc.max_code),R(t,t.bl_desc),e=18;e>=3&&0===t.bl_tree[2*n[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e})(t),o=t.opt_len+3+7>>>3,l=t.static_len+3+7>>>3,l<=o&&(o=l)):o=l=a+5,a+4<=o&&-1!==e?D(t,e,a,i):4===t.strategy||l===o?(g(t,2+(i?1:0),3),E(t,s,r)):(g(t,4+(i?1:0),3),((t,e,a,i)=>{let s;for(g(t,e-257,5),g(t,a-1,5),g(t,i-4,4),s=0;s<i;s++)g(t,t.bl_tree[2*n[s]+1],3);U(t,t.dyn_ltree,e-1),U(t,t.dyn_dtree,a-1)})(t,t.l_desc.max_code+1,t.d_desc.max_code+1,h+1),E(t,t.dyn_ltree,t.dyn_dtree)),y(t),i&&x(t)},O={_tr_init:t=>{S||((()=>{let t,n,w,m,b;const g=new Array(16);for(w=0,m=0;m<28;m++)for(h[m]=w,t=0;t<1<<e[m];t++)l[w++]=m;for(l[w-1]=m,b=0,m=0;m<16;m++)for(d[m]=b,t=0;t<1<<a[m];t++)o[b++]=m;for(b>>=7;m<30;m++)for(d[m]=b<<7,t=0;t<1<<a[m]-7;t++)o[256+b++]=m;for(n=0;n<=15;n++)g[n]=0;for(t=0;t<=143;)s[2*t+1]=8,t++,g[8]++;for(;t<=255;)s[2*t+1]=9,t++,g[9]++;for(;t<=279;)s[2*t+1]=7,t++,g[7]++;for(;t<=287;)s[2*t+1]=8,t++,g[8]++;for(v(s,287,g),t=0;t<30;t++)r[2*t+1]=5,r[2*t]=k(t,5);f=new _(s,e,257,286,15),c=new _(r,a,0,30,15),u=new _(new Array(0),i,0,19,7)})(),S=!0),t.l_desc=new w(t.dyn_ltree,f),t.d_desc=new w(t.dyn_dtree,c),t.bl_desc=new w(t.bl_tree,u),t.bi_buf=0,t.bi_valid=0,y(t)},_tr_stored_block:D,_tr_flush_block:T,_tr_tally:(t,e,a)=>(t.pending_buf[t.sym_buf+t.sym_next++]=e,t.pending_buf[t.sym_buf+t.sym_next++]=e>>8,t.pending_buf[t.sym_buf+t.sym_next++]=a,0===e?t.dyn_ltree[2*a]++:(t.matches++,e--,t.dyn_ltree[2*(l[a]+256+1)]++,t.dyn_dtree[2*m(e)]++),t.sym_next===t.sym_end),_tr_align:t=>{g(t,2,3),p(t,256,s),(t=>{16===t.bi_valid?(b(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)})(t)}};var F=(t,e,a,i)=>{let n=65535&t|0,s=t>>>16&65535|0,r=0;for(;0!==a;){r=a>2e3?2e3:a,a-=r;do{n=n+e[i++]|0,s=s+n|0}while(--r);n%=65521,s%=65521}return n|s<<16|0};const L=new Uint32Array((()=>{let t,e=[];for(var a=0;a<256;a++){t=a;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[a]=t}return e})());var N=(t,e,a,i)=>{const n=L,s=i+a;t^=-1;for(let a=i;a<s;a++)t=t>>>8^n[255&(t^e[a])];return-1^t},I={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"},B={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};const{_tr_init:C,_tr_stored_block:H,_tr_flush_block:M,_tr_tally:j,_tr_align:K}=O,{Z_NO_FLUSH:P,Z_PARTIAL_FLUSH:Y,Z_FULL_FLUSH:G,Z_FINISH:X,Z_BLOCK:W,Z_OK:q,Z_STREAM_END:J,Z_STREAM_ERROR:Q,Z_DATA_ERROR:V,Z_BUF_ERROR:$,Z_DEFAULT_COMPRESSION:tt,Z_FILTERED:et,Z_HUFFMAN_ONLY:at,Z_RLE:it,Z_FIXED:nt,Z_DEFAULT_STRATEGY:st,Z_UNKNOWN:rt,Z_DEFLATED:ot}=B,lt=(t,e)=>(t.msg=I[e],e),ht=t=>2*t-(t>4?9:0),dt=t=>{let e=t.length;for(;--e>=0;)t[e]=0},_t=t=>{let e,a,i,n=t.w_size;e=t.hash_size,i=e;do{a=t.head[--i],t.head[i]=a>=n?a-n:0}while(--e);e=n,i=e;do{a=t.prev[--i],t.prev[i]=a>=n?a-n:0}while(--e)};let ft=(t,e,a)=>(e<<t.hash_shift^a)&t.hash_mask;const ct=t=>{const e=t.state;let a=e.pending;a>t.avail_out&&(a=t.avail_out),0!==a&&(t.output.set(e.pending_buf.subarray(e.pending_out,e.pending_out+a),t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0))},ut=(t,e)=>{M(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,ct(t.strm)},wt=(t,e)=>{t.pending_buf[t.pending++]=e},mt=(t,e)=>{t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e},bt=(t,e,a,i)=>{let n=t.avail_in;return n>i&&(n=i),0===n?0:(t.avail_in-=n,e.set(t.input.subarray(t.next_in,t.next_in+n),a),1===t.state.wrap?t.adler=F(t.adler,e,n,a):2===t.state.wrap&&(t.adler=N(t.adler,e,n,a)),t.next_in+=n,t.total_in+=n,n)},gt=(t,e)=>{let a,i,n=t.max_chain_length,s=t.strstart,r=t.prev_length,o=t.nice_match;const l=t.strstart>t.w_size-262?t.strstart-(t.w_size-262):0,h=t.window,d=t.w_mask,_=t.prev,f=t.strstart+258;let c=h[s+r-1],u=h[s+r];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(a=e,h[a+r]===u&&h[a+r-1]===c&&h[a]===h[s]&&h[++a]===h[s+1]){s+=2,a++;do{}while(h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&s<f);if(i=258-(f-s),s=f-258,i>r){if(t.match_start=e,r=i,i>=o)break;c=h[s+r-1],u=h[s+r]}}}while((e=_[e&d])>l&&0!=--n);return r<=t.lookahead?r:t.lookahead},pt=t=>{const e=t.w_size;let a,i,n;do{if(i=t.window_size-t.lookahead-t.strstart,t.strstart>=e+(e-262)&&(t.window.set(t.window.subarray(e,e+e-i),0),t.match_start-=e,t.strstart-=e,t.block_start-=e,t.insert>t.strstart&&(t.insert=t.strstart),_t(t),i+=e),0===t.strm.avail_in)break;if(a=bt(t.strm,t.window,t.strstart+t.lookahead,i),t.lookahead+=a,t.lookahead+t.insert>=3)for(n=t.strstart-t.insert,t.ins_h=t.window[n],t.ins_h=ft(t,t.ins_h,t.window[n+1]);t.insert&&(t.ins_h=ft(t,t.ins_h,t.window[n+3-1]),t.prev[n&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=n,n++,t.insert--,!(t.lookahead+t.insert<3)););}while(t.lookahead<262&&0!==t.strm.avail_in)},kt=(t,e)=>{let a,i,n,s=t.pending_buf_size-5>t.w_size?t.w_size:t.pending_buf_size-5,r=0,o=t.strm.avail_in;do{if(a=65535,n=t.bi_valid+42>>3,t.strm.avail_out<n)break;if(n=t.strm.avail_out-n,i=t.strstart-t.block_start,a>i+t.strm.avail_in&&(a=i+t.strm.avail_in),a>n&&(a=n),a<s&&(0===a&&e!==X||e===P||a!==i+t.strm.avail_in))break;r=e===X&&a===i+t.strm.avail_in?1:0,H(t,0,0,r),t.pending_buf[t.pending-4]=a,t.pending_buf[t.pending-3]=a>>8,t.pending_buf[t.pending-2]=~a,t.pending_buf[t.pending-1]=~a>>8,ct(t.strm),i&&(i>a&&(i=a),t.strm.output.set(t.window.subarray(t.block_start,t.block_start+i),t.strm.next_out),t.strm.next_out+=i,t.strm.avail_out-=i,t.strm.total_out+=i,t.block_start+=i,a-=i),a&&(bt(t.strm,t.strm.output,t.strm.next_out,a),t.strm.next_out+=a,t.strm.avail_out-=a,t.strm.total_out+=a)}while(0===r);return o-=t.strm.avail_in,o&&(o>=t.w_size?(t.matches=2,t.window.set(t.strm.input.subarray(t.strm.next_in-t.w_size,t.strm.next_in),0),t.strstart=t.w_size,t.insert=t.strstart):(t.window_size-t.strstart<=o&&(t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,t.insert>t.strstart&&(t.insert=t.strstart)),t.window.set(t.strm.input.subarray(t.strm.next_in-o,t.strm.next_in),t.strstart),t.strstart+=o,t.insert+=o>t.w_size-t.insert?t.w_size-t.insert:o),t.block_start=t.strstart),t.high_water<t.strstart&&(t.high_water=t.strstart),r?4:e!==P&&e!==X&&0===t.strm.avail_in&&t.strstart===t.block_start?2:(n=t.window_size-t.strstart,t.strm.avail_in>n&&t.block_start>=t.w_size&&(t.block_start-=t.w_size,t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,n+=t.w_size,t.insert>t.strstart&&(t.insert=t.strstart)),n>t.strm.avail_in&&(n=t.strm.avail_in),n&&(bt(t.strm,t.window,t.strstart,n),t.strstart+=n,t.insert+=n>t.w_size-t.insert?t.w_size-t.insert:n),t.high_water<t.strstart&&(t.high_water=t.strstart),n=t.bi_valid+42>>3,n=t.pending_buf_size-n>65535?65535:t.pending_buf_size-n,s=n>t.w_size?t.w_size:n,i=t.strstart-t.block_start,(i>=s||(i||e===X)&&e!==P&&0===t.strm.avail_in&&i<=n)&&(a=i>n?n:i,r=e===X&&0===t.strm.avail_in&&a===i?1:0,H(t,t.block_start,a,r),t.block_start+=a,ct(t.strm)),r?3:1)},vt=(t,e)=>{let a,i;for(;;){if(t.lookahead<262){if(pt(t),t.lookahead<262&&e===P)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==a&&t.strstart-a<=t.w_size-262&&(t.match_length=gt(t,a)),t.match_length>=3)if(i=j(t,t.strstart-t.match_start,t.match_length-3),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=3){t.match_length--;do{t.strstart++,t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart}while(0!=--t.match_length);t.strstart++}else t.strstart+=t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=ft(t,t.ins_h,t.window[t.strstart+1]);else i=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(i&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=t.strstart<2?t.strstart:2,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2},yt=(t,e)=>{let a,i,n;for(;;){if(t.lookahead<262){if(pt(t),t.lookahead<262&&e===P)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=2,0!==a&&t.prev_length<t.max_lazy_match&&t.strstart-a<=t.w_size-262&&(t.match_length=gt(t,a),t.match_length<=5&&(t.strategy===et||3===t.match_length&&t.strstart-t.match_start>4096)&&(t.match_length=2)),t.prev_length>=3&&t.match_length<=t.prev_length){n=t.strstart+t.lookahead-3,i=j(t,t.strstart-1-t.prev_match,t.prev_length-3),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=n&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart)}while(0!=--t.prev_length);if(t.match_available=0,t.match_length=2,t.strstart++,i&&(ut(t,!1),0===t.strm.avail_out))return 1}else if(t.match_available){if(i=j(t,0,t.window[t.strstart-1]),i&&ut(t,!1),t.strstart++,t.lookahead--,0===t.strm.avail_out)return 1}else t.match_available=1,t.strstart++,t.lookahead--}return t.match_available&&(i=j(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<2?t.strstart:2,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2};function xt(t,e,a,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=a,this.max_chain=i,this.func=n}const zt=[new xt(0,0,0,0,kt),new xt(4,4,8,4,vt),new xt(4,5,16,8,vt),new xt(4,6,32,32,vt),new xt(4,4,16,16,yt),new xt(8,16,32,32,yt),new xt(8,16,128,128,yt),new xt(8,32,128,256,yt),new xt(32,128,258,1024,yt),new xt(32,258,258,4096,yt)];function At(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=ot,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new Uint16Array(1146),this.dyn_dtree=new Uint16Array(122),this.bl_tree=new Uint16Array(78),dt(this.dyn_ltree),dt(this.dyn_dtree),dt(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new Uint16Array(16),this.heap=new Uint16Array(573),dt(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new Uint16Array(573),dt(this.depth),this.sym_buf=0,this.lit_bufsize=0,this.sym_next=0,this.sym_end=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}const Et=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||42!==e.status&&57!==e.status&&69!==e.status&&73!==e.status&&91!==e.status&&103!==e.status&&113!==e.status&&666!==e.status?1:0},Rt=t=>{if(Et(t))return lt(t,Q);t.total_in=t.total_out=0,t.data_type=rt;const e=t.state;return e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=2===e.wrap?57:e.wrap?42:113,t.adler=2===e.wrap?0:1,e.last_flush=-2,C(e),q},Zt=t=>{const e=Rt(t);var a;return e===q&&((a=t.state).window_size=2*a.w_size,dt(a.head),a.max_lazy_match=zt[a.level].max_lazy,a.good_match=zt[a.level].good_length,a.nice_match=zt[a.level].nice_length,a.max_chain_length=zt[a.level].max_chain,a.strstart=0,a.block_start=0,a.lookahead=0,a.insert=0,a.match_length=a.prev_length=2,a.match_available=0,a.ins_h=0),e},Ut=(t,e,a,i,n,s)=>{if(!t)return Q;let r=1;if(e===tt&&(e=6),i<0?(r=0,i=-i):i>15&&(r=2,i-=16),n<1||n>9||a!==ot||i<8||i>15||e<0||e>9||s<0||s>nt||8===i&&1!==r)return lt(t,Q);8===i&&(i=9);const o=new At;return t.state=o,o.strm=t,o.status=42,o.wrap=r,o.gzhead=null,o.w_bits=i,o.w_size=1<<o.w_bits,o.w_mask=o.w_size-1,o.hash_bits=n+7,o.hash_size=1<<o.hash_bits,o.hash_mask=o.hash_size-1,o.hash_shift=~~((o.hash_bits+3-1)/3),o.window=new Uint8Array(2*o.w_size),o.head=new Uint16Array(o.hash_size),o.prev=new Uint16Array(o.w_size),o.lit_bufsize=1<<n+6,o.pending_buf_size=4*o.lit_bufsize,o.pending_buf=new Uint8Array(o.pending_buf_size),o.sym_buf=o.lit_bufsize,o.sym_end=3*(o.lit_bufsize-1),o.level=e,o.strategy=s,o.method=a,Zt(t)};var St={deflateInit:(t,e)=>Ut(t,e,ot,15,8,st),deflateInit2:Ut,deflateReset:Zt,deflateResetKeep:Rt,deflateSetHeader:(t,e)=>Et(t)||2!==t.state.wrap?Q:(t.state.gzhead=e,q),deflate:(t,e)=>{if(Et(t)||e>W||e<0)return t?lt(t,Q):Q;const a=t.state;if(!t.output||0!==t.avail_in&&!t.input||666===a.status&&e!==X)return lt(t,0===t.avail_out?$:Q);const i=a.last_flush;if(a.last_flush=e,0!==a.pending){if(ct(t),0===t.avail_out)return a.last_flush=-1,q}else if(0===t.avail_in&&ht(e)<=ht(i)&&e!==X)return lt(t,$);if(666===a.status&&0!==t.avail_in)return lt(t,$);if(42===a.status&&0===a.wrap&&(a.status=113),42===a.status){let e=ot+(a.w_bits-8<<4)<<8,i=-1;if(i=a.strategy>=at||a.level<2?0:a.level<6?1:6===a.level?2:3,e|=i<<6,0!==a.strstart&&(e|=32),e+=31-e%31,mt(a,e),0!==a.strstart&&(mt(a,t.adler>>>16),mt(a,65535&t.adler)),t.adler=1,a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,q}if(57===a.status)if(t.adler=0,wt(a,31),wt(a,139),wt(a,8),a.gzhead)wt(a,(a.gzhead.text?1:0)+(a.gzhead.hcrc?2:0)+(a.gzhead.extra?4:0)+(a.gzhead.name?8:0)+(a.gzhead.comment?16:0)),wt(a,255&a.gzhead.time),wt(a,a.gzhead.time>>8&255),wt(a,a.gzhead.time>>16&255),wt(a,a.gzhead.time>>24&255),wt(a,9===a.level?2:a.strategy>=at||a.level<2?4:0),wt(a,255&a.gzhead.os),a.gzhead.extra&&a.gzhead.extra.length&&(wt(a,255&a.gzhead.extra.length),wt(a,a.gzhead.extra.length>>8&255)),a.gzhead.hcrc&&(t.adler=N(t.adler,a.pending_buf,a.pending,0)),a.gzindex=0,a.status=69;else if(wt(a,0),wt(a,0),wt(a,0),wt(a,0),wt(a,0),wt(a,9===a.level?2:a.strategy>=at||a.level<2?4:0),wt(a,3),a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,q;if(69===a.status){if(a.gzhead.extra){let e=a.pending,i=(65535&a.gzhead.extra.length)-a.gzindex;for(;a.pending+i>a.pending_buf_size;){let n=a.pending_buf_size-a.pending;if(a.pending_buf.set(a.gzhead.extra.subarray(a.gzindex,a.gzindex+n),a.pending),a.pending=a.pending_buf_size,a.gzhead.hcrc&&a.pending>e&&(t.adler=N(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex+=n,ct(t),0!==a.pending)return a.last_flush=-1,q;e=0,i-=n}let n=new Uint8Array(a.gzhead.extra);a.pending_buf.set(n.subarray(a.gzindex,a.gzindex+i),a.pending),a.pending+=i,a.gzhead.hcrc&&a.pending>e&&(t.adler=N(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex=0}a.status=73}if(73===a.status){if(a.gzhead.name){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=N(t.adler,a.pending_buf,a.pending-i,i)),ct(t),0!==a.pending)return a.last_flush=-1,q;i=0}e=a.gzindex<a.gzhead.name.length?255&a.gzhead.name.charCodeAt(a.gzindex++):0,wt(a,e)}while(0!==e);a.gzhead.hcrc&&a.pending>i&&(t.adler=N(t.adler,a.pending_buf,a.pending-i,i)),a.gzindex=0}a.status=91}if(91===a.status){if(a.gzhead.comment){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=N(t.adler,a.pending_buf,a.pending-i,i)),ct(t),0!==a.pending)return a.last_flush=-1,q;i=0}e=a.gzindex<a.gzhead.comment.length?255&a.gzhead.comment.charCodeAt(a.gzindex++):0,wt(a,e)}while(0!==e);a.gzhead.hcrc&&a.pending>i&&(t.adler=N(t.adler,a.pending_buf,a.pending-i,i))}a.status=103}if(103===a.status){if(a.gzhead.hcrc){if(a.pending+2>a.pending_buf_size&&(ct(t),0!==a.pending))return a.last_flush=-1,q;wt(a,255&t.adler),wt(a,t.adler>>8&255),t.adler=0}if(a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,q}if(0!==t.avail_in||0!==a.lookahead||e!==P&&666!==a.status){let i=0===a.level?kt(a,e):a.strategy===at?((t,e)=>{let a;for(;;){if(0===t.lookahead&&(pt(t),0===t.lookahead)){if(e===P)return 1;break}if(t.match_length=0,a=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2})(a,e):a.strategy===it?((t,e)=>{let a,i,n,s;const r=t.window;for(;;){if(t.lookahead<=258){if(pt(t),t.lookahead<=258&&e===P)return 1;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=3&&t.strstart>0&&(n=t.strstart-1,i=r[n],i===r[++n]&&i===r[++n]&&i===r[++n])){s=t.strstart+258;do{}while(i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&n<s);t.match_length=258-(s-n),t.match_length>t.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=3?(a=j(t,1,t.match_length-3),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2})(a,e):zt[a.level].func(a,e);if(3!==i&&4!==i||(a.status=666),1===i||3===i)return 0===t.avail_out&&(a.last_flush=-1),q;if(2===i&&(e===Y?K(a):e!==W&&(H(a,0,0,!1),e===G&&(dt(a.head),0===a.lookahead&&(a.strstart=0,a.block_start=0,a.insert=0))),ct(t),0===t.avail_out))return a.last_flush=-1,q}return e!==X?q:a.wrap<=0?J:(2===a.wrap?(wt(a,255&t.adler),wt(a,t.adler>>8&255),wt(a,t.adler>>16&255),wt(a,t.adler>>24&255),wt(a,255&t.total_in),wt(a,t.total_in>>8&255),wt(a,t.total_in>>16&255),wt(a,t.total_in>>24&255)):(mt(a,t.adler>>>16),mt(a,65535&t.adler)),ct(t),a.wrap>0&&(a.wrap=-a.wrap),0!==a.pending?q:J)},deflateEnd:t=>{if(Et(t))return Q;const e=t.state.status;return t.state=null,113===e?lt(t,V):q},deflateSetDictionary:(t,e)=>{let a=e.length;if(Et(t))return Q;const i=t.state,n=i.wrap;if(2===n||1===n&&42!==i.status||i.lookahead)return Q;if(1===n&&(t.adler=F(t.adler,e,a,0)),i.wrap=0,a>=i.w_size){0===n&&(dt(i.head),i.strstart=0,i.block_start=0,i.insert=0);let t=new Uint8Array(i.w_size);t.set(e.subarray(a-i.w_size,a),0),e=t,a=i.w_size}const s=t.avail_in,r=t.next_in,o=t.input;for(t.avail_in=a,t.next_in=0,t.input=e,pt(i);i.lookahead>=3;){let t=i.strstart,e=i.lookahead-2;do{i.ins_h=ft(i,i.ins_h,i.window[t+3-1]),i.prev[t&i.w_mask]=i.head[i.ins_h],i.head[i.ins_h]=t,t++}while(--e);i.strstart=t,i.lookahead=2,pt(i)}return i.strstart+=i.lookahead,i.block_start=i.strstart,i.insert=i.lookahead,i.lookahead=0,i.match_length=i.prev_length=2,i.match_available=0,t.next_in=r,t.input=o,t.avail_in=s,i.wrap=n,q},deflateInfo:"pako deflate (from Nodeca project)"};const Dt=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var Tt=function(t){const e=Array.prototype.slice.call(arguments,1);for(;e.length;){const a=e.shift();if(a){if("object"!=typeof a)throw new TypeError(a+"must be non-object");for(const e in a)Dt(a,e)&&(t[e]=a[e])}}return t},Ot=t=>{let e=0;for(let a=0,i=t.length;a<i;a++)e+=t[a].length;const a=new Uint8Array(e);for(let e=0,i=0,n=t.length;e<n;e++){let n=t[e];a.set(n,i),i+=n.length}return a};let Ft=!0;try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(t){Ft=!1}const Lt=new Uint8Array(256);for(let t=0;t<256;t++)Lt[t]=t>=252?6:t>=248?5:t>=240?4:t>=224?3:t>=192?2:1;Lt[254]=Lt[254]=1;var Nt=t=>{if("function"==typeof TextEncoder&&TextEncoder.prototype.encode)return(new TextEncoder).encode(t);let e,a,i,n,s,r=t.length,o=0;for(n=0;n<r;n++)a=t.charCodeAt(n),55296==(64512&a)&&n+1<r&&(i=t.charCodeAt(n+1),56320==(64512&i)&&(a=65536+(a-55296<<10)+(i-56320),n++)),o+=a<128?1:a<2048?2:a<65536?3:4;for(e=new Uint8Array(o),s=0,n=0;s<o;n++)a=t.charCodeAt(n),55296==(64512&a)&&n+1<r&&(i=t.charCodeAt(n+1),56320==(64512&i)&&(a=65536+(a-55296<<10)+(i-56320),n++)),a<128?e[s++]=a:a<2048?(e[s++]=192|a>>>6,e[s++]=128|63&a):a<65536?(e[s++]=224|a>>>12,e[s++]=128|a>>>6&63,e[s++]=128|63&a):(e[s++]=240|a>>>18,e[s++]=128|a>>>12&63,e[s++]=128|a>>>6&63,e[s++]=128|63&a);return e},It=(t,e)=>{const a=e||t.length;if("function"==typeof TextDecoder&&TextDecoder.prototype.decode)return(new TextDecoder).decode(t.subarray(0,e));let i,n;const s=new Array(2*a);for(n=0,i=0;i<a;){let e=t[i++];if(e<128){s[n++]=e;continue}let r=Lt[e];if(r>4)s[n++]=65533,i+=r-1;else{for(e&=2===r?31:3===r?15:7;r>1&&i<a;)e=e<<6|63&t[i++],r--;r>1?s[n++]=65533:e<65536?s[n++]=e:(e-=65536,s[n++]=55296|e>>10&1023,s[n++]=56320|1023&e)}}return((t,e)=>{if(e<65534&&t.subarray&&Ft)return String.fromCharCode.apply(null,t.length===e?t:t.subarray(0,e));let a="";for(let i=0;i<e;i++)a+=String.fromCharCode(t[i]);return a})(s,n)},Bt=(t,e)=>{(e=e||t.length)>t.length&&(e=t.length);let a=e-1;for(;a>=0&&128==(192&t[a]);)a--;return a<0||0===a?e:a+Lt[t[a]]>e?a:e};var Ct=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0};const Ht=Object.prototype.toString,{Z_NO_FLUSH:Mt,Z_SYNC_FLUSH:jt,Z_FULL_FLUSH:Kt,Z_FINISH:Pt,Z_OK:Yt,Z_STREAM_END:Gt,Z_DEFAULT_COMPRESSION:Xt,Z_DEFAULT_STRATEGY:Wt,Z_DEFLATED:qt}=B;function Jt(t){this.options=Tt({level:Xt,method:qt,chunkSize:16384,windowBits:15,memLevel:8,strategy:Wt},t||{});let e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Ct,this.strm.avail_out=0;let a=St.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(a!==Yt)throw new Error(I[a]);if(e.header&&St.deflateSetHeader(this.strm,e.header),e.dictionary){let t;if(t="string"==typeof e.dictionary?Nt(e.dictionary):"[object ArrayBuffer]"===Ht.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,a=St.deflateSetDictionary(this.strm,t),a!==Yt)throw new Error(I[a]);this._dict_set=!0}}function Qt(t,e){const a=new Jt(e);if(a.push(t,!0),a.err)throw a.msg||I[a.err];return a.result}Jt.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize;let n,s;if(this.ended)return!1;for(s=e===~~e?e:!0===e?Pt:Mt,"string"==typeof t?a.input=Nt(t):"[object ArrayBuffer]"===Ht.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;)if(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),(s===jt||s===Kt)&&a.avail_out<=6)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else{if(n=St.deflate(a,s),n===Gt)return a.next_out>0&&this.onData(a.output.subarray(0,a.next_out)),n=St.deflateEnd(this.strm),this.onEnd(n),this.ended=!0,n===Yt;if(0!==a.avail_out){if(s>0&&a.next_out>0)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else if(0===a.avail_in)break}else this.onData(a.output)}return!0},Jt.prototype.onData=function(t){this.chunks.push(t)},Jt.prototype.onEnd=function(t){t===Yt&&(this.result=Ot(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};var Vt={Deflate:Jt,deflate:Qt,deflateRaw:function(t,e){return(e=e||{}).raw=!0,Qt(t,e)},gzip:function(t,e){return(e=e||{}).gzip=!0,Qt(t,e)},constants:B};var $t=function(t,e){let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z,A;const E=t.state;a=t.next_in,z=t.input,i=a+(t.avail_in-5),n=t.next_out,A=t.output,s=n-(e-t.avail_out),r=n+(t.avail_out-257),o=E.dmax,l=E.wsize,h=E.whave,d=E.wnext,_=E.window,f=E.hold,c=E.bits,u=E.lencode,w=E.distcode,m=(1<<E.lenbits)-1,b=(1<<E.distbits)-1;t:do{c<15&&(f+=z[a++]<<c,c+=8,f+=z[a++]<<c,c+=8),g=u[f&m];e:for(;;){if(p=g>>>24,f>>>=p,c-=p,p=g>>>16&255,0===p)A[n++]=65535&g;else{if(!(16&p)){if(0==(64&p)){g=u[(65535&g)+(f&(1<<p)-1)];continue e}if(32&p){E.mode=16191;break t}t.msg="invalid literal/length code",E.mode=16209;break t}k=65535&g,p&=15,p&&(c<p&&(f+=z[a++]<<c,c+=8),k+=f&(1<<p)-1,f>>>=p,c-=p),c<15&&(f+=z[a++]<<c,c+=8,f+=z[a++]<<c,c+=8),g=w[f&b];a:for(;;){if(p=g>>>24,f>>>=p,c-=p,p=g>>>16&255,!(16&p)){if(0==(64&p)){g=w[(65535&g)+(f&(1<<p)-1)];continue a}t.msg="invalid distance code",E.mode=16209;break t}if(v=65535&g,p&=15,c<p&&(f+=z[a++]<<c,c+=8,c<p&&(f+=z[a++]<<c,c+=8)),v+=f&(1<<p)-1,v>o){t.msg="invalid distance too far back",E.mode=16209;break t}if(f>>>=p,c-=p,p=n-s,v>p){if(p=v-p,p>h&&E.sane){t.msg="invalid distance too far back",E.mode=16209;break t}if(y=0,x=_,0===d){if(y+=l-p,p<k){k-=p;do{A[n++]=_[y++]}while(--p);y=n-v,x=A}}else if(d<p){if(y+=l+d-p,p-=d,p<k){k-=p;do{A[n++]=_[y++]}while(--p);if(y=0,d<k){p=d,k-=p;do{A[n++]=_[y++]}while(--p);y=n-v,x=A}}}else if(y+=d-p,p<k){k-=p;do{A[n++]=_[y++]}while(--p);y=n-v,x=A}for(;k>2;)A[n++]=x[y++],A[n++]=x[y++],A[n++]=x[y++],k-=3;k&&(A[n++]=x[y++],k>1&&(A[n++]=x[y++]))}else{y=n-v;do{A[n++]=A[y++],A[n++]=A[y++],A[n++]=A[y++],k-=3}while(k>2);k&&(A[n++]=A[y++],k>1&&(A[n++]=A[y++]))}break}}break}}while(a<i&&n<r);k=c>>3,a-=k,c-=k<<3,f&=(1<<c)-1,t.next_in=a,t.next_out=n,t.avail_in=a<i?i-a+5:5-(a-i),t.avail_out=n<r?r-n+257:257-(n-r),E.hold=f,E.bits=c};const te=new Uint16Array([3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0]),ee=new Uint8Array([16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78]),ae=new Uint16Array([1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0]),ie=new Uint8Array([16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64]);var ne=(t,e,a,i,n,s,r,o)=>{const l=o.bits;let h,d,_,f,c,u,w=0,m=0,b=0,g=0,p=0,k=0,v=0,y=0,x=0,z=0,A=null;const E=new Uint16Array(16),R=new Uint16Array(16);let Z,U,S,D=null;for(w=0;w<=15;w++)E[w]=0;for(m=0;m<i;m++)E[e[a+m]]++;for(p=l,g=15;g>=1&&0===E[g];g--);if(p>g&&(p=g),0===g)return n[s++]=20971520,n[s++]=20971520,o.bits=1,0;for(b=1;b<g&&0===E[b];b++);for(p<b&&(p=b),y=1,w=1;w<=15;w++)if(y<<=1,y-=E[w],y<0)return-1;if(y>0&&(0===t||1!==g))return-1;for(R[1]=0,w=1;w<15;w++)R[w+1]=R[w]+E[w];for(m=0;m<i;m++)0!==e[a+m]&&(r[R[e[a+m]]++]=m);if(0===t?(A=D=r,u=20):1===t?(A=te,D=ee,u=257):(A=ae,D=ie,u=0),z=0,m=0,w=b,c=s,k=p,v=0,_=-1,x=1<<p,f=x-1,1===t&&x>852||2===t&&x>592)return 1;for(;;){Z=w-v,r[m]+1<u?(U=0,S=r[m]):r[m]>=u?(U=D[r[m]-u],S=A[r[m]-u]):(U=96,S=0),h=1<<w-v,d=1<<k,b=d;do{d-=h,n[c+(z>>v)+d]=Z<<24|U<<16|S|0}while(0!==d);for(h=1<<w-1;z&h;)h>>=1;if(0!==h?(z&=h-1,z+=h):z=0,m++,0==--E[w]){if(w===g)break;w=e[a+r[m]]}if(w>p&&(z&f)!==_){for(0===v&&(v=p),c+=b,k=w-v,y=1<<k;k+v<g&&(y-=E[k+v],!(y<=0));)k++,y<<=1;if(x+=1<<k,1===t&&x>852||2===t&&x>592)return 1;_=z&f,n[_]=p<<24|k<<16|c-s|0}}return 0!==z&&(n[c+z]=w-v<<24|64<<16|0),o.bits=p,0};const{Z_FINISH:se,Z_BLOCK:re,Z_TREES:oe,Z_OK:le,Z_STREAM_END:he,Z_NEED_DICT:de,Z_STREAM_ERROR:_e,Z_DATA_ERROR:fe,Z_MEM_ERROR:ce,Z_BUF_ERROR:ue,Z_DEFLATED:we}=B,me=16209,be=t=>(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24);function ge(){this.strm=null,this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}const pe=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||e.mode<16180||e.mode>16211?1:0},ke=t=>{if(pe(t))return _e;const e=t.state;return t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=16180,e.last=0,e.havedict=0,e.flags=-1,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new Int32Array(852),e.distcode=e.distdyn=new Int32Array(592),e.sane=1,e.back=-1,le},ve=t=>{if(pe(t))return _e;const e=t.state;return e.wsize=0,e.whave=0,e.wnext=0,ke(t)},ye=(t,e)=>{let a;if(pe(t))return _e;const i=t.state;return e<0?(a=0,e=-e):(a=5+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?_e:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=a,i.wbits=e,ve(t))},xe=(t,e)=>{if(!t)return _e;const a=new ge;t.state=a,a.strm=t,a.window=null,a.mode=16180;const i=ye(t,e);return i!==le&&(t.state=null),i};let ze,Ae,Ee=!0;const Re=t=>{if(Ee){ze=new Int32Array(512),Ae=new Int32Array(32);let e=0;for(;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(ne(1,t.lens,0,288,ze,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;ne(2,t.lens,0,32,Ae,0,t.work,{bits:5}),Ee=!1}t.lencode=ze,t.lenbits=9,t.distcode=Ae,t.distbits=5},Ze=(t,e,a,i)=>{let n;const s=t.state;return null===s.window&&(s.wsize=1<<s.wbits,s.wnext=0,s.whave=0,s.window=new Uint8Array(s.wsize)),i>=s.wsize?(s.window.set(e.subarray(a-s.wsize,a),0),s.wnext=0,s.whave=s.wsize):(n=s.wsize-s.wnext,n>i&&(n=i),s.window.set(e.subarray(a-i,a-i+n),s.wnext),(i-=n)?(s.window.set(e.subarray(a-i,a),0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whave<s.wsize&&(s.whave+=n))),0};var Ue={inflateReset:ve,inflateReset2:ye,inflateResetKeep:ke,inflateInit:t=>xe(t,15),inflateInit2:xe,inflate:(t,e)=>{let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z=0;const A=new Uint8Array(4);let E,R;const Z=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(pe(t)||!t.output||!t.input&&0!==t.avail_in)return _e;a=t.state,16191===a.mode&&(a.mode=16192),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,_=o,f=l,x=le;t:for(;;)switch(a.mode){case 16180:if(0===a.wrap){a.mode=16192;break}for(;d<16;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}if(2&a.wrap&&35615===h){0===a.wbits&&(a.wbits=15),a.check=0,A[0]=255&h,A[1]=h>>>8&255,a.check=N(a.check,A,2,0),h=0,d=0,a.mode=16181;break}if(a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&h)<<8)+(h>>8))%31){t.msg="incorrect header check",a.mode=me;break}if((15&h)!==we){t.msg="unknown compression method",a.mode=me;break}if(h>>>=4,d-=4,y=8+(15&h),0===a.wbits&&(a.wbits=y),y>15||y>a.wbits){t.msg="invalid window size",a.mode=me;break}a.dmax=1<<a.wbits,a.flags=0,t.adler=a.check=1,a.mode=512&h?16189:16191,h=0,d=0;break;case 16181:for(;d<16;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}if(a.flags=h,(255&a.flags)!==we){t.msg="unknown compression method",a.mode=me;break}if(57344&a.flags){t.msg="unknown header flags set",a.mode=me;break}a.head&&(a.head.text=h>>8&1),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=N(a.check,A,2,0)),h=0,d=0,a.mode=16182;case 16182:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}a.head&&(a.head.time=h),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,A[2]=h>>>16&255,A[3]=h>>>24&255,a.check=N(a.check,A,4,0)),h=0,d=0,a.mode=16183;case 16183:for(;d<16;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}a.head&&(a.head.xflags=255&h,a.head.os=h>>8),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=N(a.check,A,2,0)),h=0,d=0,a.mode=16184;case 16184:if(1024&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}a.length=h,a.head&&(a.head.extra_len=h),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=N(a.check,A,2,0)),h=0,d=0}else a.head&&(a.head.extra=null);a.mode=16185;case 16185:if(1024&a.flags&&(c=a.length,c>o&&(c=o),c&&(a.head&&(y=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Uint8Array(a.head.extra_len)),a.head.extra.set(i.subarray(s,s+c),y)),512&a.flags&&4&a.wrap&&(a.check=N(a.check,i,c,s)),o-=c,s+=c,a.length-=c),a.length))break t;a.length=0,a.mode=16186;case 16186:if(2048&a.flags){if(0===o)break t;c=0;do{y=i[s+c++],a.head&&y&&a.length<65536&&(a.head.name+=String.fromCharCode(y))}while(y&&c<o);if(512&a.flags&&4&a.wrap&&(a.check=N(a.check,i,c,s)),o-=c,s+=c,y)break t}else a.head&&(a.head.name=null);a.length=0,a.mode=16187;case 16187:if(4096&a.flags){if(0===o)break t;c=0;do{y=i[s+c++],a.head&&y&&a.length<65536&&(a.head.comment+=String.fromCharCode(y))}while(y&&c<o);if(512&a.flags&&4&a.wrap&&(a.check=N(a.check,i,c,s)),o-=c,s+=c,y)break t}else a.head&&(a.head.comment=null);a.mode=16188;case 16188:if(512&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}if(4&a.wrap&&h!==(65535&a.check)){t.msg="header crc mismatch",a.mode=me;break}h=0,d=0}a.head&&(a.head.hcrc=a.flags>>9&1,a.head.done=!0),t.adler=a.check=0,a.mode=16191;break;case 16189:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}t.adler=a.check=be(h),h=0,d=0,a.mode=16190;case 16190:if(0===a.havedict)return t.next_out=r,t.avail_out=l,t.next_in=s,t.avail_in=o,a.hold=h,a.bits=d,de;t.adler=a.check=1,a.mode=16191;case 16191:if(e===re||e===oe)break t;case 16192:if(a.last){h>>>=7&d,d-=7&d,a.mode=16206;break}for(;d<3;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}switch(a.last=1&h,h>>>=1,d-=1,3&h){case 0:a.mode=16193;break;case 1:if(Re(a),a.mode=16199,e===oe){h>>>=2,d-=2;break t}break;case 2:a.mode=16196;break;case 3:t.msg="invalid block type",a.mode=me}h>>>=2,d-=2;break;case 16193:for(h>>>=7&d,d-=7&d;d<32;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}if((65535&h)!=(h>>>16^65535)){t.msg="invalid stored block lengths",a.mode=me;break}if(a.length=65535&h,h=0,d=0,a.mode=16194,e===oe)break t;case 16194:a.mode=16195;case 16195:if(c=a.length,c){if(c>o&&(c=o),c>l&&(c=l),0===c)break t;n.set(i.subarray(s,s+c),r),o-=c,s+=c,l-=c,r+=c,a.length-=c;break}a.mode=16191;break;case 16196:for(;d<14;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}if(a.nlen=257+(31&h),h>>>=5,d-=5,a.ndist=1+(31&h),h>>>=5,d-=5,a.ncode=4+(15&h),h>>>=4,d-=4,a.nlen>286||a.ndist>30){t.msg="too many length or distance symbols",a.mode=me;break}a.have=0,a.mode=16197;case 16197:for(;a.have<a.ncode;){for(;d<3;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}a.lens[Z[a.have++]]=7&h,h>>>=3,d-=3}for(;a.have<19;)a.lens[Z[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,E={bits:a.lenbits},x=ne(0,a.lens,0,19,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid code lengths set",a.mode=me;break}a.have=0,a.mode=16198;case 16198:for(;a.have<a.nlen+a.ndist;){for(;z=a.lencode[h&(1<<a.lenbits)-1],m=z>>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}if(g<16)h>>>=m,d-=m,a.lens[a.have++]=g;else{if(16===g){for(R=m+2;d<R;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}if(h>>>=m,d-=m,0===a.have){t.msg="invalid bit length repeat",a.mode=me;break}y=a.lens[a.have-1],c=3+(3&h),h>>>=2,d-=2}else if(17===g){for(R=m+3;d<R;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}h>>>=m,d-=m,y=0,c=3+(7&h),h>>>=3,d-=3}else{for(R=m+7;d<R;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}h>>>=m,d-=m,y=0,c=11+(127&h),h>>>=7,d-=7}if(a.have+c>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=me;break}for(;c--;)a.lens[a.have++]=y}}if(a.mode===me)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=me;break}if(a.lenbits=9,E={bits:a.lenbits},x=ne(1,a.lens,0,a.nlen,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid literal/lengths set",a.mode=me;break}if(a.distbits=6,a.distcode=a.distdyn,E={bits:a.distbits},x=ne(2,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,E),a.distbits=E.bits,x){t.msg="invalid distances set",a.mode=me;break}if(a.mode=16199,e===oe)break t;case 16199:a.mode=16200;case 16200:if(o>=6&&l>=258){t.next_out=r,t.avail_out=l,t.next_in=s,t.avail_in=o,a.hold=h,a.bits=d,$t(t,f),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,16191===a.mode&&(a.back=-1);break}for(a.back=0;z=a.lencode[h&(1<<a.lenbits)-1],m=z>>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}if(b&&0==(240&b)){for(p=m,k=b,v=g;z=a.lencode[v+((h&(1<<p+k)-1)>>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}h>>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,a.length=g,0===b){a.mode=16205;break}if(32&b){a.back=-1,a.mode=16191;break}if(64&b){t.msg="invalid literal/length code",a.mode=me;break}a.extra=15&b,a.mode=16201;case 16201:if(a.extra){for(R=a.extra;d<R;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}a.length+=h&(1<<a.extra)-1,h>>>=a.extra,d-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=16202;case 16202:for(;z=a.distcode[h&(1<<a.distbits)-1],m=z>>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}if(0==(240&b)){for(p=m,k=b,v=g;z=a.distcode[v+((h&(1<<p+k)-1)>>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}h>>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,64&b){t.msg="invalid distance code",a.mode=me;break}a.offset=g,a.extra=15&b,a.mode=16203;case 16203:if(a.extra){for(R=a.extra;d<R;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}a.offset+=h&(1<<a.extra)-1,h>>>=a.extra,d-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=me;break}a.mode=16204;case 16204:if(0===l)break t;if(c=f-l,a.offset>c){if(c=a.offset-c,c>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=me;break}c>a.wnext?(c-=a.wnext,u=a.wsize-c):u=a.wnext-c,c>a.length&&(c=a.length),w=a.window}else w=n,u=r-a.offset,c=a.length;c>l&&(c=l),l-=c,a.length-=c;do{n[r++]=w[u++]}while(--c);0===a.length&&(a.mode=16200);break;case 16205:if(0===l)break t;n[r++]=a.length,l--,a.mode=16200;break;case 16206:if(a.wrap){for(;d<32;){if(0===o)break t;o--,h|=i[s++]<<d,d+=8}if(f-=l,t.total_out+=f,a.total+=f,4&a.wrap&&f&&(t.adler=a.check=a.flags?N(a.check,n,f,r-f):F(a.check,n,f,r-f)),f=l,4&a.wrap&&(a.flags?h:be(h))!==a.check){t.msg="incorrect data check",a.mode=me;break}h=0,d=0}a.mode=16207;case 16207:if(a.wrap&&a.flags){for(;d<32;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8}if(4&a.wrap&&h!==(4294967295&a.total)){t.msg="incorrect length check",a.mode=me;break}h=0,d=0}a.mode=16208;case 16208:x=he;break t;case me:x=fe;break t;case 16210:return ce;default:return _e}return t.next_out=r,t.avail_out=l,t.next_in=s,t.avail_in=o,a.hold=h,a.bits=d,(a.wsize||f!==t.avail_out&&a.mode<me&&(a.mode<16206||e!==se))&&Ze(t,t.output,t.next_out,f-t.avail_out),_-=t.avail_in,f-=t.avail_out,t.total_in+=_,t.total_out+=f,a.total+=f,4&a.wrap&&f&&(t.adler=a.check=a.flags?N(a.check,n,f,t.next_out-f):F(a.check,n,f,t.next_out-f)),t.data_type=a.bits+(a.last?64:0)+(16191===a.mode?128:0)+(16199===a.mode||16194===a.mode?256:0),(0===_&&0===f||e===se)&&x===le&&(x=ue),x},inflateEnd:t=>{if(pe(t))return _e;let e=t.state;return e.window&&(e.window=null),t.state=null,le},inflateGetHeader:(t,e)=>{if(pe(t))return _e;const a=t.state;return 0==(2&a.wrap)?_e:(a.head=e,e.done=!1,le)},inflateSetDictionary:(t,e)=>{const a=e.length;let i,n,s;return pe(t)?_e:(i=t.state,0!==i.wrap&&16190!==i.mode?_e:16190===i.mode&&(n=1,n=F(n,e,a,0),n!==i.check)?fe:(s=Ze(t,e,a,a),s?(i.mode=16210,ce):(i.havedict=1,le)))},inflateInfo:"pako inflate (from Nodeca project)"};var Se=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1};const De=Object.prototype.toString,{Z_NO_FLUSH:Te,Z_FINISH:Oe,Z_OK:Fe,Z_STREAM_END:Le,Z_NEED_DICT:Ne,Z_STREAM_ERROR:Ie,Z_DATA_ERROR:Be,Z_MEM_ERROR:Ce}=B;function He(t){this.options=Tt({chunkSize:65536,windowBits:15,to:""},t||{});const e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Ct,this.strm.avail_out=0;let a=Ue.inflateInit2(this.strm,e.windowBits);if(a!==Fe)throw new Error(I[a]);if(this.header=new Se,Ue.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=Nt(e.dictionary):"[object ArrayBuffer]"===De.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(a=Ue.inflateSetDictionary(this.strm,e.dictionary),a!==Fe)))throw new Error(I[a])}He.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize,n=this.options.dictionary;let s,r,o;if(this.ended)return!1;for(r=e===~~e?e:!0===e?Oe:Te,"[object ArrayBuffer]"===De.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;){for(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),s=Ue.inflate(a,r),s===Ne&&n&&(s=Ue.inflateSetDictionary(a,n),s===Fe?s=Ue.inflate(a,r):s===Be&&(s=Ne));a.avail_in>0&&s===Le&&a.state.wrap>0&&0!==t[a.next_in];)Ue.inflateReset(a),s=Ue.inflate(a,r);switch(s){case Ie:case Be:case Ne:case Ce:return this.onEnd(s),this.ended=!0,!1}if(o=a.avail_out,a.next_out&&(0===a.avail_out||s===Le))if("string"===this.options.to){let t=Bt(a.output,a.next_out),e=a.next_out-t,n=It(a.output,t);a.next_out=e,a.avail_out=i-e,e&&a.output.set(a.output.subarray(t,t+e),0),this.onData(n)}else this.onData(a.output.length===a.next_out?a.output:a.output.subarray(0,a.next_out));if(s!==Fe||0!==o){if(s===Le)return s=Ue.inflateEnd(this.strm),this.onEnd(s),this.ended=!0,!0;if(0===a.avail_in)break}}return!0},He.prototype.onData=function(t){this.chunks.push(t)},He.prototype.onEnd=function(t){t===Fe&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=Ot(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};const{Deflate:Me,deflate:je,deflateRaw:Ke,gzip:Pe}=Vt;var Ye=Me,Ge=je,Xe=B;const We=new class{constructor(){this._init()}clear(){this._init()}addEvent(t){if(!t)throw new Error("Adding invalid event");const e=this._hasEvents?",":"";this.deflate.push(e+t,Xe.Z_SYNC_FLUSH),this._hasEvents=!0}finish(){if(this.deflate.push("]",Xe.Z_FINISH),this.deflate.err)throw this.deflate.err;const t=this.deflate.result;return this._init(),t}_init(){this._hasEvents=!1,this.deflate=new Ye,this.deflate.push("[",Xe.Z_NO_FLUSH)}},qe={clear:()=>{We.clear()},addEvent:t=>We.addEvent(t),finish:()=>We.finish(),compress:t=>function(t){return Ge(t)}(t)};addEventListener("message",(function(t){const e=t.data.method,a=t.data.id,i=t.data.arg;if(e in qe&&"function"==typeof qe[e])try{const t=qe[e](i);postMessage({id:a,method:e,success:!0,response:t})}catch(t){postMessage({id:a,method:e,success:!1,response:t.message}),console.error(t)}})),postMessage({id:void 0,method:"init",success:!0,response:void 0});`; 4005 4006 function e(){const e=new Blob([r]);return URL.createObjectURL(e)} 4007 4008 /** 4009 * Converts a timestamp to ms, if it was in s, or keeps it as ms. 4010 */ 4011 function timestampToMs(timestamp) { 4012 const isMs = timestamp > 9999999999; 4013 return isMs ? timestamp : timestamp * 1000; 4014 } 4015 4016 /** This error indicates that the event buffer size exceeded the limit.. */ 4017 class EventBufferSizeExceededError extends Error { 4018 constructor() { 4019 super(`Event buffer exceeded maximum size of ${REPLAY_MAX_EVENT_BUFFER_SIZE}.`); 4020 } 4021 } 4022 4023 /** 4024 * A basic event buffer that does not do any compression. 4025 * Used as fallback if the compression worker cannot be loaded or is disabled. 4026 */ 4027 class EventBufferArray { 4028 /** All the events that are buffered to be sent. */ 4029 4030 __init() {this._totalSize = 0;} 4031 4032 constructor() {EventBufferArray.prototype.__init.call(this); 4033 this.events = []; 4034 } 4035 4036 /** @inheritdoc */ 4037 get hasEvents() { 4038 return this.events.length > 0; 4039 } 4040 4041 /** @inheritdoc */ 4042 get type() { 4043 return 'sync'; 4044 } 4045 4046 /** @inheritdoc */ 4047 destroy() { 4048 this.events = []; 4049 } 4050 4051 /** @inheritdoc */ 4052 async addEvent(event) { 4053 const eventSize = JSON.stringify(event).length; 4054 this._totalSize += eventSize; 4055 if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) { 4056 throw new EventBufferSizeExceededError(); 4057 } 4058 4059 this.events.push(event); 4060 } 4061 4062 /** @inheritdoc */ 4063 finish() { 4064 return new Promise(resolve => { 4065 // Make a copy of the events array reference and immediately clear the 4066 // events member so that we do not lose new events while uploading 4067 // attachment. 4068 const eventsRet = this.events; 4069 this.clear(); 4070 resolve(JSON.stringify(eventsRet)); 4071 }); 4072 } 4073 4074 /** @inheritdoc */ 4075 clear() { 4076 this.events = []; 4077 this._totalSize = 0; 4078 } 4079 4080 /** @inheritdoc */ 4081 getEarliestTimestamp() { 4082 const timestamp = this.events.map(event => event.timestamp).sort()[0]; 4083 4084 if (!timestamp) { 4085 return null; 4086 } 4087 4088 return timestampToMs(timestamp); 4089 } 4090 } 4091 4092 /** 4093 * Event buffer that uses a web worker to compress events. 4094 * Exported only for testing. 4095 */ 4096 class WorkerHandler { 4097 4098 constructor(worker) { 4099 this._worker = worker; 4100 this._id = 0; 4101 } 4102 4103 /** 4104 * Ensure the worker is ready (or not). 4105 * This will either resolve when the worker is ready, or reject if an error occured. 4106 */ 4107 ensureReady() { 4108 // Ensure we only check once 4109 if (this._ensureReadyPromise) { 4110 return this._ensureReadyPromise; 4111 } 4112 4113 this._ensureReadyPromise = new Promise((resolve, reject) => { 4114 this._worker.addEventListener( 4115 'message', 4116 ({ data }) => { 4117 if ((data ).success) { 4118 resolve(); 4119 } else { 4120 reject(); 4121 } 4122 }, 4123 { once: true }, 4124 ); 4125 4126 this._worker.addEventListener( 4127 'error', 4128 error => { 4129 reject(error); 4130 }, 4131 { once: true }, 4132 ); 4133 }); 4134 4135 return this._ensureReadyPromise; 4136 } 4137 4138 /** 4139 * Destroy the worker. 4140 */ 4141 destroy() { 4142 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Destroying compression worker'); 4143 this._worker.terminate(); 4144 } 4145 4146 /** 4147 * Post message to worker and wait for response before resolving promise. 4148 */ 4149 postMessage(method, arg) { 4150 const id = this._getAndIncrementId(); 4151 4152 return new Promise((resolve, reject) => { 4153 const listener = ({ data }) => { 4154 const response = data ; 4155 if (response.method !== method) { 4156 return; 4157 } 4158 4159 // There can be multiple listeners for a single method, the id ensures 4160 // that the response matches the caller. 4161 if (response.id !== id) { 4162 return; 4163 } 4164 4165 // At this point, we'll always want to remove listener regardless of result status 4166 this._worker.removeEventListener('message', listener); 4167 4168 if (!response.success) { 4169 // TODO: Do some error handling, not sure what 4170 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay]', response.response); 4171 4172 reject(new Error('Error in compression worker')); 4173 return; 4174 } 4175 4176 resolve(response.response ); 4177 }; 4178 4179 // Note: we can't use `once` option because it's possible it needs to 4180 // listen to multiple messages 4181 this._worker.addEventListener('message', listener); 4182 this._worker.postMessage({ id, method, arg }); 4183 }); 4184 } 4185 4186 /** Get the current ID and increment it for the next call. */ 4187 _getAndIncrementId() { 4188 return this._id++; 4189 } 4190 } 4191 4192 /** 4193 * Event buffer that uses a web worker to compress events. 4194 * Exported only for testing. 4195 */ 4196 class EventBufferCompressionWorker { 4197 4198 __init() {this._totalSize = 0;} 4199 4200 constructor(worker) {EventBufferCompressionWorker.prototype.__init.call(this); 4201 this._worker = new WorkerHandler(worker); 4202 this._earliestTimestamp = null; 4203 } 4204 4205 /** @inheritdoc */ 4206 get hasEvents() { 4207 return !!this._earliestTimestamp; 4208 } 4209 4210 /** @inheritdoc */ 4211 get type() { 4212 return 'worker'; 4213 } 4214 4215 /** 4216 * Ensure the worker is ready (or not). 4217 * This will either resolve when the worker is ready, or reject if an error occured. 4218 */ 4219 ensureReady() { 4220 return this._worker.ensureReady(); 4221 } 4222 4223 /** 4224 * Destroy the event buffer. 4225 */ 4226 destroy() { 4227 this._worker.destroy(); 4228 } 4229 4230 /** 4231 * Add an event to the event buffer. 4232 * 4233 * Returns true if event was successfuly received and processed by worker. 4234 */ 4235 addEvent(event) { 4236 const timestamp = timestampToMs(event.timestamp); 4237 if (!this._earliestTimestamp || timestamp < this._earliestTimestamp) { 4238 this._earliestTimestamp = timestamp; 4239 } 4240 4241 const data = JSON.stringify(event); 4242 this._totalSize += data.length; 4243 4244 if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) { 4245 return Promise.reject(new EventBufferSizeExceededError()); 4246 } 4247 4248 return this._sendEventToWorker(data); 4249 } 4250 4251 /** 4252 * Finish the event buffer and return the compressed data. 4253 */ 4254 finish() { 4255 return this._finishRequest(); 4256 } 4257 4258 /** @inheritdoc */ 4259 clear() { 4260 this._earliestTimestamp = null; 4261 this._totalSize = 0; 4262 // We do not wait on this, as we assume the order of messages is consistent for the worker 4263 void this._worker.postMessage('clear'); 4264 } 4265 4266 /** @inheritdoc */ 4267 getEarliestTimestamp() { 4268 return this._earliestTimestamp; 4269 } 4270 4271 /** 4272 * Send the event to the worker. 4273 */ 4274 _sendEventToWorker(data) { 4275 return this._worker.postMessage('addEvent', data); 4276 } 4277 4278 /** 4279 * Finish the request and return the compressed data from the worker. 4280 */ 4281 async _finishRequest() { 4282 const response = await this._worker.postMessage('finish'); 4283 4284 this._earliestTimestamp = null; 4285 this._totalSize = 0; 4286 4287 return response; 4288 } 4289 } 4290 4291 /** 4292 * This proxy will try to use the compression worker, and fall back to use the simple buffer if an error occurs there. 4293 * This can happen e.g. if the worker cannot be loaded. 4294 * Exported only for testing. 4295 */ 4296 class EventBufferProxy { 4297 4298 constructor(worker) { 4299 this._fallback = new EventBufferArray(); 4300 this._compression = new EventBufferCompressionWorker(worker); 4301 this._used = this._fallback; 4302 4303 this._ensureWorkerIsLoadedPromise = this._ensureWorkerIsLoaded(); 4304 } 4305 4306 /** @inheritdoc */ 4307 get type() { 4308 return this._used.type; 4309 } 4310 4311 /** @inheritDoc */ 4312 get hasEvents() { 4313 return this._used.hasEvents; 4314 } 4315 4316 /** @inheritDoc */ 4317 destroy() { 4318 this._fallback.destroy(); 4319 this._compression.destroy(); 4320 } 4321 4322 /** @inheritdoc */ 4323 clear() { 4324 return this._used.clear(); 4325 } 4326 4327 /** @inheritdoc */ 4328 getEarliestTimestamp() { 4329 return this._used.getEarliestTimestamp(); 4330 } 4331 4332 /** 4333 * Add an event to the event buffer. 4334 * 4335 * Returns true if event was successfully added. 4336 */ 4337 addEvent(event) { 4338 return this._used.addEvent(event); 4339 } 4340 4341 /** @inheritDoc */ 4342 async finish() { 4343 // Ensure the worker is loaded, so the sent event is compressed 4344 await this.ensureWorkerIsLoaded(); 4345 4346 return this._used.finish(); 4347 } 4348 4349 /** Ensure the worker has loaded. */ 4350 ensureWorkerIsLoaded() { 4351 return this._ensureWorkerIsLoadedPromise; 4352 } 4353 4354 /** Actually check if the worker has been loaded. */ 4355 async _ensureWorkerIsLoaded() { 4356 try { 4357 await this._compression.ensureReady(); 4358 } catch (error) { 4359 // If the worker fails to load, we fall back to the simple buffer. 4360 // Nothing more to do from our side here 4361 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Failed to load the compression worker, falling back to simple buffer'); 4362 return; 4363 } 4364 4365 // Now we need to switch over the array buffer to the compression worker 4366 await this._switchToCompressionWorker(); 4367 } 4368 4369 /** Switch the used buffer to the compression worker. */ 4370 async _switchToCompressionWorker() { 4371 const { events } = this._fallback; 4372 4373 const addEventPromises = []; 4374 for (const event of events) { 4375 addEventPromises.push(this._compression.addEvent(event)); 4376 } 4377 4378 // We switch over to the new buffer immediately - any further events will be added 4379 // after the previously buffered ones 4380 this._used = this._compression; 4381 4382 // Wait for original events to be re-added before resolving 4383 try { 4384 await Promise.all(addEventPromises); 4385 } catch (error) { 4386 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn('[Replay] Failed to add events when switching buffers.', error); 4387 } 4388 } 4389 } 4390 4391 /** 4392 * Create an event buffer for replays. 4393 */ 4394 function createEventBuffer({ useCompression }) { 4395 // eslint-disable-next-line no-restricted-globals 4396 if (useCompression && window.Worker) { 4397 try { 4398 const workerUrl = e(); 4399 4400 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Using compression worker'); 4401 const worker = new Worker(workerUrl); 4402 return new EventBufferProxy(worker); 4403 } catch (error) { 4404 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Failed to create compression worker'); 4405 // Fall back to use simple event buffer array 4406 } 4407 } 4408 4409 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Using simple buffer'); 4410 return new EventBufferArray(); 4411 } 4412 4413 /** If sessionStorage is available. */ 4414 function hasSessionStorage() { 4415 return 'sessionStorage' in WINDOW && !!WINDOW.sessionStorage; 4416 } 4417 4418 /** 4419 * Removes the session from Session Storage and unsets session in replay instance 4420 */ 4421 function clearSession(replay) { 4422 deleteSession(); 4423 replay.session = undefined; 4424 } 4425 4426 /** 4427 * Deletes a session from storage 4428 */ 4429 function deleteSession() { 4430 if (!hasSessionStorage()) { 4431 return; 4432 } 4433 4434 try { 4435 WINDOW.sessionStorage.removeItem(REPLAY_SESSION_KEY); 4436 } catch (e) { 4437 // Ignore potential SecurityError exceptions 4438 } 4439 } 4440 4441 /** 4442 * Given an initial timestamp and an expiry duration, checks to see if current 4443 * time should be considered as expired. 4444 */ 4445 function isExpired( 4446 initialTime, 4447 expiry, 4448 targetTime = +new Date(), 4449 ) { 4450 // Always expired if < 0 4451 if (initialTime === null || expiry === undefined || expiry < 0) { 4452 return true; 4453 } 4454 4455 // Never expires if == 0 4456 if (expiry === 0) { 4457 return false; 4458 } 4459 4460 return initialTime + expiry <= targetTime; 4461 } 4462 4463 /** 4464 * Checks to see if session is expired 4465 */ 4466 function isSessionExpired(session, timeouts, targetTime = +new Date()) { 4467 return ( 4468 // First, check that maximum session length has not been exceeded 4469 isExpired(session.started, timeouts.maxSessionLife, targetTime) || 4470 // check that the idle timeout has not been exceeded (i.e. user has 4471 // performed an action within the last `sessionIdleExpire` ms) 4472 isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime) 4473 ); 4474 } 4475 4476 /** 4477 * Given a sample rate, returns true if replay should be sampled. 4478 * 4479 * 1.0 = 100% sampling 4480 * 0.0 = 0% sampling 4481 */ 4482 function isSampled(sampleRate) { 4483 if (sampleRate === undefined) { 4484 return false; 4485 } 4486 4487 // Math.random() returns a number in range of 0 to 1 (inclusive of 0, but not 1) 4488 return Math.random() < sampleRate; 4489 } 4490 4491 /** 4492 * Save a session to session storage. 4493 */ 4494 function saveSession(session) { 4495 if (!hasSessionStorage()) { 4496 return; 4497 } 4498 4499 try { 4500 WINDOW.sessionStorage.setItem(REPLAY_SESSION_KEY, JSON.stringify(session)); 4501 } catch (e) { 4502 // Ignore potential SecurityError exceptions 4503 } 4504 } 4505 4506 /** 4507 * Get a session with defaults & applied sampling. 4508 */ 4509 function makeSession(session) { 4510 const now = Date.now(); 4511 const id = session.id || uuid4(); 4512 // Note that this means we cannot set a started/lastActivity of `0`, but this should not be relevant outside of tests. 4513 const started = session.started || now; 4514 const lastActivity = session.lastActivity || now; 4515 const segmentId = session.segmentId || 0; 4516 const sampled = session.sampled; 4517 4518 return { 4519 id, 4520 started, 4521 lastActivity, 4522 segmentId, 4523 sampled, 4524 shouldRefresh: true, 4525 }; 4526 } 4527 4528 /** 4529 * Get the sampled status for a session based on sample rates & current sampled status. 4530 */ 4531 function getSessionSampleType(sessionSampleRate, allowBuffering) { 4532 return isSampled(sessionSampleRate) ? 'session' : allowBuffering ? 'buffer' : false; 4533 } 4534 4535 /** 4536 * Create a new session, which in its current implementation is a Sentry event 4537 * that all replays will be saved to as attachments. Currently, we only expect 4538 * one of these Sentry events per "replay session". 4539 */ 4540 function createSession({ sessionSampleRate, allowBuffering, stickySession = false }) { 4541 const sampled = getSessionSampleType(sessionSampleRate, allowBuffering); 4542 const session = makeSession({ 4543 sampled, 4544 }); 4545 4546 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log(`[Replay] Creating new session: ${session.id}`); 4547 4548 if (stickySession) { 4549 saveSession(session); 4550 } 4551 4552 return session; 4553 } 4554 4555 /** 4556 * Fetches a session from storage 4557 */ 4558 function fetchSession() { 4559 if (!hasSessionStorage()) { 4560 return null; 4561 } 4562 4563 try { 4564 // This can throw if cookies are disabled 4565 const sessionStringFromStorage = WINDOW.sessionStorage.getItem(REPLAY_SESSION_KEY); 4566 4567 if (!sessionStringFromStorage) { 4568 return null; 4569 } 4570 4571 const sessionObj = JSON.parse(sessionStringFromStorage) ; 4572 4573 return makeSession(sessionObj); 4574 } catch (e) { 4575 return null; 4576 } 4577 } 4578 4579 /** 4580 * Get or create a session 4581 */ 4582 function getSession({ 4583 timeouts, 4584 currentSession, 4585 stickySession, 4586 sessionSampleRate, 4587 allowBuffering, 4588 }) { 4589 // If session exists and is passed, use it instead of always hitting session storage 4590 const session = currentSession || (stickySession && fetchSession()); 4591 4592 if (session) { 4593 // If there is a session, check if it is valid (e.g. "last activity" time 4594 // should be within the "session idle time", and "session started" time is 4595 // within "max session time"). 4596 const isExpired = isSessionExpired(session, timeouts); 4597 4598 if (!isExpired || (allowBuffering && session.shouldRefresh)) { 4599 return { type: 'saved', session }; 4600 } else if (!session.shouldRefresh) { 4601 // This is the case if we have an error session that is completed 4602 // (=triggered an error). Session will continue as session-based replay, 4603 // and when this session is expired, it will not be renewed until user 4604 // reloads. 4605 const discardedSession = makeSession({ sampled: false }); 4606 return { type: 'new', session: discardedSession }; 4607 } else { 4608 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Session has expired'); 4609 } 4610 // Otherwise continue to create a new session 4611 } 4612 4613 const newSession = createSession({ 4614 stickySession, 4615 sessionSampleRate, 4616 allowBuffering, 4617 }); 4618 4619 return { type: 'new', session: newSession }; 4620 } 4621 4622 function isCustomEvent(event) { 4623 return event.type === EventType.Custom; 4624 } 4625 4626 /** 4627 * Add an event to the event buffer. 4628 * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`. 4629 */ 4630 async function addEvent( 4631 replay, 4632 event, 4633 isCheckout, 4634 ) { 4635 if (!replay.eventBuffer) { 4636 // This implies that `_isEnabled` is false 4637 return null; 4638 } 4639 4640 if (replay.isPaused()) { 4641 // Do not add to event buffer when recording is paused 4642 return null; 4643 } 4644 4645 const timestampInMs = timestampToMs(event.timestamp); 4646 4647 // Throw out events that happen more than 5 minutes ago. This can happen if 4648 // page has been left open and idle for a long period of time and user 4649 // comes back to trigger a new session. The performance entries rely on 4650 // `performance.timeOrigin`, which is when the page first opened. 4651 if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) { 4652 return null; 4653 } 4654 4655 try { 4656 if (isCheckout) { 4657 replay.eventBuffer.clear(); 4658 } 4659 4660 const replayOptions = replay.getOptions(); 4661 4662 const eventAfterPossibleCallback = 4663 typeof replayOptions.beforeAddRecordingEvent === 'function' && isCustomEvent(event) 4664 ? replayOptions.beforeAddRecordingEvent(event) 4665 : event; 4666 4667 if (!eventAfterPossibleCallback) { 4668 return; 4669 } 4670 4671 return await replay.eventBuffer.addEvent(eventAfterPossibleCallback); 4672 } catch (error) { 4673 const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent'; 4674 4675 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(error); 4676 await replay.stop(reason); 4677 4678 const client = getCurrentHub().getClient(); 4679 4680 if (client) { 4681 client.recordDroppedEvent('internal_sdk_error', 'replay'); 4682 } 4683 } 4684 } 4685 4686 /** If the event is an error event */ 4687 function isErrorEvent(event) { 4688 return !event.type; 4689 } 4690 4691 /** If the event is a transaction event */ 4692 function isTransactionEvent(event) { 4693 return event.type === 'transaction'; 4694 } 4695 4696 /** If the event is an replay event */ 4697 function isReplayEvent(event) { 4698 return event.type === 'replay_event'; 4699 } 4700 4701 /** 4702 * Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`. 4703 */ 4704 function handleAfterSendEvent(replay) { 4705 // Custom transports may still be returning `Promise<void>`, which means we cannot expect the status code to be available there 4706 // TODO (v8): remove this check as it will no longer be necessary 4707 const enforceStatusCode = isBaseTransportSend(); 4708 4709 return (event, sendResponse) => { 4710 if (!isErrorEvent(event) && !isTransactionEvent(event)) { 4711 return; 4712 } 4713 4714 const statusCode = sendResponse && sendResponse.statusCode; 4715 4716 // We only want to do stuff on successful error sending, otherwise you get error replays without errors attached 4717 // If not using the base transport, we allow `undefined` response (as a custom transport may not implement this correctly yet) 4718 // If we do use the base transport, we skip if we encountered an non-OK status code 4719 if (enforceStatusCode && (!statusCode || statusCode < 200 || statusCode >= 300)) { 4720 return; 4721 } 4722 4723 // Collect traceIds in _context regardless of `recordingMode` 4724 // In error mode, _context gets cleared on every checkout 4725 if (isTransactionEvent(event) && event.contexts && event.contexts.trace && event.contexts.trace.trace_id) { 4726 replay.getContext().traceIds.add(event.contexts.trace.trace_id ); 4727 return; 4728 } 4729 4730 // Everything below is just for error events 4731 if (!isErrorEvent(event)) { 4732 return; 4733 } 4734 4735 // Add error to list of errorIds of replay. This is ok to do even if not 4736 // sampled because context will get reset at next checkout. 4737 // XXX: There is also a race condition where it's possible to capture an 4738 // error to Sentry before Replay SDK has loaded, but response returns after 4739 // it was loaded, and this gets called. 4740 if (event.event_id) { 4741 replay.getContext().errorIds.add(event.event_id); 4742 } 4743 4744 // If error event is tagged with replay id it means it was sampled (when in buffer mode) 4745 // Need to be very careful that this does not cause an infinite loop 4746 if (replay.recordingMode === 'buffer' && event.tags && event.tags.replayId) { 4747 setTimeout(() => { 4748 // Capture current event buffer as new replay 4749 void replay.sendBufferedReplayOrFlush(); 4750 }); 4751 } 4752 }; 4753 } 4754 4755 function isBaseTransportSend() { 4756 const client = getCurrentHub().getClient(); 4757 if (!client) { 4758 return false; 4759 } 4760 4761 const transport = client.getTransport(); 4762 if (!transport) { 4763 return false; 4764 } 4765 4766 return ( 4767 (transport.send ).__sentry__baseTransport__ || false 4768 ); 4769 } 4770 4771 /** 4772 * Returns true if we think the given event is an error originating inside of rrweb. 4773 */ 4774 function isRrwebError(event, hint) { 4775 if (event.type || !event.exception || !event.exception.values || !event.exception.values.length) { 4776 return false; 4777 } 4778 4779 // @ts-ignore this may be set by rrweb when it finds errors 4780 if (hint.originalException && hint.originalException.__rrweb__) { 4781 return true; 4782 } 4783 4784 // Check if any exception originates from rrweb 4785 return event.exception.values.some(exception => { 4786 if (!exception.stacktrace || !exception.stacktrace.frames || !exception.stacktrace.frames.length) { 4787 return false; 4788 } 4789 4790 return exception.stacktrace.frames.some(frame => frame.filename && frame.filename.includes('/rrweb/src/')); 4791 }); 4792 } 4793 4794 /** 4795 * Determine if event should be sampled (only applies in buffer mode). 4796 * When an event is captured by `hanldleGlobalEvent`, when in buffer mode 4797 * we determine if we want to sample the error or not. 4798 */ 4799 function shouldSampleForBufferEvent(replay, event) { 4800 if (replay.recordingMode !== 'buffer') { 4801 return false; 4802 } 4803 4804 // ignore this error because otherwise we could loop indefinitely with 4805 // trying to capture replay and failing 4806 if (event.message === UNABLE_TO_SEND_REPLAY) { 4807 return false; 4808 } 4809 4810 // Require the event to be an error event & to have an exception 4811 if (!event.exception || event.type) { 4812 return false; 4813 } 4814 4815 return isSampled(replay.getOptions().errorSampleRate); 4816 } 4817 4818 /** 4819 * Returns a listener to be added to `addGlobalEventProcessor(listener)`. 4820 */ 4821 function handleGlobalEventListener( 4822 replay, 4823 includeAfterSendEventHandling = false, 4824 ) { 4825 const afterSendHandler = includeAfterSendEventHandling ? handleAfterSendEvent(replay) : undefined; 4826 4827 return (event, hint) => { 4828 if (isReplayEvent(event)) { 4829 // Replays have separate set of breadcrumbs, do not include breadcrumbs 4830 // from core SDK 4831 delete event.breadcrumbs; 4832 return event; 4833 } 4834 4835 // We only want to handle errors & transactions, nothing else 4836 if (!isErrorEvent(event) && !isTransactionEvent(event)) { 4837 return event; 4838 } 4839 4840 // Unless `captureExceptions` is enabled, we want to ignore errors coming from rrweb 4841 // As there can be a bunch of stuff going wrong in internals there, that we don't want to bubble up to users 4842 if (isRrwebError(event, hint) && !replay.getOptions()._experiments.captureExceptions) { 4843 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Ignoring error from rrweb internals', event); 4844 return null; 4845 } 4846 4847 // When in buffer mode, we decide to sample here. 4848 // Later, in `handleAfterSendEvent`, if the replayId is set, we know that we sampled 4849 // And convert the buffer session to a full session 4850 const isErrorEventSampled = shouldSampleForBufferEvent(replay, event); 4851 4852 // Tag errors if it has been sampled in buffer mode, or if it is session mode 4853 // Only tag transactions if in session mode 4854 const shouldTagReplayId = isErrorEventSampled || replay.recordingMode === 'session'; 4855 4856 if (shouldTagReplayId) { 4857 event.tags = { ...event.tags, replayId: replay.getSessionId() }; 4858 } 4859 4860 // In cases where a custom client is used that does not support the new hooks (yet), 4861 // we manually call this hook method here 4862 if (afterSendHandler) { 4863 // Pretend the error had a 200 response so we always capture it 4864 afterSendHandler(event, { statusCode: 200 }); 4865 } 4866 4867 return event; 4868 }; 4869 } 4870 4871 /** 4872 * Create a "span" for each performance entry. 4873 */ 4874 function createPerformanceSpans( 4875 replay, 4876 entries, 4877 ) { 4878 return entries.map(({ type, start, end, name, data }) => { 4879 const response = replay.throttledAddEvent({ 4880 type: EventType.Custom, 4881 timestamp: start, 4882 data: { 4883 tag: 'performanceSpan', 4884 payload: { 4885 op: type, 4886 description: name, 4887 startTimestamp: start, 4888 endTimestamp: end, 4889 data, 4890 }, 4891 }, 4892 }); 4893 4894 // If response is a string, it means its either THROTTLED or SKIPPED 4895 return typeof response === 'string' ? Promise.resolve(null) : response; 4896 }); 4897 } 4898 4899 function handleHistory(handlerData) { 4900 const { from, to } = handlerData; 4901 4902 const now = Date.now() / 1000; 4903 4904 return { 4905 type: 'navigation.push', 4906 start: now, 4907 end: now, 4908 name: to, 4909 data: { 4910 previous: from, 4911 }, 4912 }; 4913 } 4914 4915 /** 4916 * Returns a listener to be added to `addInstrumentationHandler('history', listener)`. 4917 */ 4918 function handleHistorySpanListener(replay) { 4919 return (handlerData) => { 4920 if (!replay.isEnabled()) { 4921 return; 4922 } 4923 4924 const result = handleHistory(handlerData); 4925 4926 if (result === null) { 4927 return; 4928 } 4929 4930 // Need to collect visited URLs 4931 replay.getContext().urls.push(result.name); 4932 replay.triggerUserActivity(); 4933 4934 replay.addUpdate(() => { 4935 createPerformanceSpans(replay, [result]); 4936 // Returning false to flush 4937 return false; 4938 }); 4939 }; 4940 } 4941 4942 /** 4943 * Check whether a given request URL should be filtered out. This is so we 4944 * don't log Sentry ingest requests. 4945 */ 4946 function shouldFilterRequest(replay, url) { 4947 // If we enabled the `traceInternals` experiment, we want to trace everything 4948 if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && replay.getOptions()._experiments.traceInternals) { 4949 return false; 4950 } 4951 4952 return _isSentryRequest(url); 4953 } 4954 4955 /** 4956 * Checks wether a given URL belongs to the configured Sentry DSN. 4957 */ 4958 function _isSentryRequest(url) { 4959 const client = getCurrentHub().getClient(); 4960 const dsn = client && client.getDsn(); 4961 return dsn ? url.includes(dsn.host) : false; 4962 } 4963 4964 /** Add a performance entry breadcrumb */ 4965 function addNetworkBreadcrumb( 4966 replay, 4967 result, 4968 ) { 4969 if (!replay.isEnabled()) { 4970 return; 4971 } 4972 4973 if (result === null) { 4974 return; 4975 } 4976 4977 if (shouldFilterRequest(replay, result.name)) { 4978 return; 4979 } 4980 4981 replay.addUpdate(() => { 4982 createPerformanceSpans(replay, [result]); 4983 // Returning true will cause `addUpdate` to not flush 4984 // We do not want network requests to cause a flush. This will prevent 4985 // recurring/polling requests from keeping the replay session alive. 4986 return true; 4987 }); 4988 } 4989 4990 /** only exported for tests */ 4991 function handleFetch(handlerData) { 4992 const { startTimestamp, endTimestamp, fetchData, response } = handlerData; 4993 4994 if (!endTimestamp) { 4995 return null; 4996 } 4997 4998 // This is only used as a fallback, so we know the body sizes are never set here 4999 const { method, url } = fetchData; 5000 5001 return { 5002 type: 'resource.fetch', 5003 start: startTimestamp / 1000, 5004 end: endTimestamp / 1000, 5005 name: url, 5006 data: { 5007 method, 5008 statusCode: response ? (response ).status : undefined, 5009 }, 5010 }; 5011 } 5012 5013 /** 5014 * Returns a listener to be added to `addInstrumentationHandler('fetch', listener)`. 5015 */ 5016 function handleFetchSpanListener(replay) { 5017 return (handlerData) => { 5018 if (!replay.isEnabled()) { 5019 return; 5020 } 5021 5022 const result = handleFetch(handlerData); 5023 5024 addNetworkBreadcrumb(replay, result); 5025 }; 5026 } 5027 5028 /** only exported for tests */ 5029 function handleXhr(handlerData) { 5030 const { startTimestamp, endTimestamp, xhr } = handlerData; 5031 5032 const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY]; 5033 5034 if (!startTimestamp || !endTimestamp || !sentryXhrData) { 5035 return null; 5036 } 5037 5038 // This is only used as a fallback, so we know the body sizes are never set here 5039 const { method, url, status_code: statusCode } = sentryXhrData; 5040 5041 if (url === undefined) { 5042 return null; 5043 } 5044 5045 return { 5046 type: 'resource.xhr', 5047 name: url, 5048 start: startTimestamp / 1000, 5049 end: endTimestamp / 1000, 5050 data: { 5051 method, 5052 statusCode, 5053 }, 5054 }; 5055 } 5056 5057 /** 5058 * Returns a listener to be added to `addInstrumentationHandler('xhr', listener)`. 5059 */ 5060 function handleXhrSpanListener(replay) { 5061 return (handlerData) => { 5062 if (!replay.isEnabled()) { 5063 return; 5064 } 5065 5066 const result = handleXhr(handlerData); 5067 5068 addNetworkBreadcrumb(replay, result); 5069 }; 5070 } 5071 5072 const OBJ = 10; 5073 const OBJ_KEY = 11; 5074 const OBJ_KEY_STR = 12; 5075 const OBJ_VAL = 13; 5076 const OBJ_VAL_STR = 14; 5077 const OBJ_VAL_COMPLETED = 15; 5078 5079 const ARR = 20; 5080 const ARR_VAL = 21; 5081 const ARR_VAL_STR = 22; 5082 const ARR_VAL_COMPLETED = 23; 5083 5084 const ALLOWED_PRIMITIVES = ['true', 'false', 'null']; 5085 5086 /** 5087 * Complete an incomplete JSON string. 5088 * This will ensure that the last element always has a `"~~"` to indicate it was truncated. 5089 * For example, `[1,2,` will be completed to `[1,2,"~~"]` 5090 * and `{"aa":"b` will be completed to `{"aa":"b~~"}` 5091 */ 5092 function completeJson(incompleteJson, stack) { 5093 if (!stack.length) { 5094 return incompleteJson; 5095 } 5096 5097 let json = incompleteJson; 5098 5099 // Most checks are only needed for the last step in the stack 5100 const lastPos = stack.length - 1; 5101 const lastStep = stack[lastPos]; 5102 5103 json = _fixLastStep(json, lastStep); 5104 5105 // Complete remaining steps - just add closing brackets 5106 for (let i = lastPos; i >= 0; i--) { 5107 const step = stack[i]; 5108 5109 switch (step) { 5110 case OBJ: 5111 json = `${json}}`; 5112 break; 5113 case ARR: 5114 json = `${json}]`; 5115 break; 5116 } 5117 } 5118 5119 return json; 5120 } 5121 5122 function _fixLastStep(json, lastStep) { 5123 switch (lastStep) { 5124 // Object cases 5125 case OBJ: 5126 return `${json}"~~":"~~"`; 5127 case OBJ_KEY: 5128 return `${json}:"~~"`; 5129 case OBJ_KEY_STR: 5130 return `${json}~~":"~~"`; 5131 case OBJ_VAL: 5132 return _maybeFixIncompleteObjValue(json); 5133 case OBJ_VAL_STR: 5134 return `${json}~~"`; 5135 case OBJ_VAL_COMPLETED: 5136 return `${json},"~~":"~~"`; 5137 5138 // Array cases 5139 case ARR: 5140 return `${json}"~~"`; 5141 case ARR_VAL: 5142 return _maybeFixIncompleteArrValue(json); 5143 case ARR_VAL_STR: 5144 return `${json}~~"`; 5145 case ARR_VAL_COMPLETED: 5146 return `${json},"~~"`; 5147 } 5148 5149 return json; 5150 } 5151 5152 function _maybeFixIncompleteArrValue(json) { 5153 const pos = _findLastArrayDelimiter(json); 5154 5155 if (pos > -1) { 5156 const part = json.slice(pos + 1); 5157 5158 if (ALLOWED_PRIMITIVES.includes(part.trim())) { 5159 return `${json},"~~"`; 5160 } 5161 5162 // Everything else is replaced with `"~~"` 5163 return `${json.slice(0, pos + 1)}"~~"`; 5164 } 5165 5166 // fallback, this shouldn't happen, to be save 5167 return json; 5168 } 5169 5170 function _findLastArrayDelimiter(json) { 5171 for (let i = json.length - 1; i >= 0; i--) { 5172 const char = json[i]; 5173 5174 if (char === ',' || char === '[') { 5175 return i; 5176 } 5177 } 5178 5179 return -1; 5180 } 5181 5182 function _maybeFixIncompleteObjValue(json) { 5183 const startPos = json.lastIndexOf(':'); 5184 5185 const part = json.slice(startPos + 1); 5186 5187 if (ALLOWED_PRIMITIVES.includes(part.trim())) { 5188 return `${json},"~~":"~~"`; 5189 } 5190 5191 // Everything else is replaced with `"~~"` 5192 // This also means we do not have incomplete numbers, e.g `[1` is replaced with `["~~"]` 5193 return `${json.slice(0, startPos + 1)}"~~"`; 5194 } 5195 5196 /** 5197 * Evaluate an (incomplete) JSON string. 5198 */ 5199 function evaluateJson(json) { 5200 const stack = []; 5201 5202 for (let pos = 0; pos < json.length; pos++) { 5203 _evaluateJsonPos(stack, json, pos); 5204 } 5205 5206 return stack; 5207 } 5208 5209 function _evaluateJsonPos(stack, json, pos) { 5210 const curStep = stack[stack.length - 1]; 5211 5212 const char = json[pos]; 5213 5214 const whitespaceRegex = /\s/; 5215 5216 if (whitespaceRegex.test(char)) { 5217 return; 5218 } 5219 5220 if (char === '"' && !_isEscaped(json, pos)) { 5221 _handleQuote(stack, curStep); 5222 return; 5223 } 5224 5225 switch (char) { 5226 case '{': 5227 _handleObj(stack, curStep); 5228 break; 5229 case '[': 5230 _handleArr(stack, curStep); 5231 break; 5232 case ':': 5233 _handleColon(stack, curStep); 5234 break; 5235 case ',': 5236 _handleComma(stack, curStep); 5237 break; 5238 case '}': 5239 _handleObjClose(stack, curStep); 5240 break; 5241 case ']': 5242 _handleArrClose(stack, curStep); 5243 break; 5244 } 5245 } 5246 5247 function _handleQuote(stack, curStep) { 5248 // End of obj value 5249 if (curStep === OBJ_VAL_STR) { 5250 stack.pop(); 5251 stack.push(OBJ_VAL_COMPLETED); 5252 return; 5253 } 5254 5255 // End of arr value 5256 if (curStep === ARR_VAL_STR) { 5257 stack.pop(); 5258 stack.push(ARR_VAL_COMPLETED); 5259 return; 5260 } 5261 5262 // Start of obj value 5263 if (curStep === OBJ_VAL) { 5264 stack.push(OBJ_VAL_STR); 5265 return; 5266 } 5267 5268 // Start of arr value 5269 if (curStep === ARR_VAL) { 5270 stack.push(ARR_VAL_STR); 5271 return; 5272 } 5273 5274 // Start of obj key 5275 if (curStep === OBJ) { 5276 stack.push(OBJ_KEY_STR); 5277 return; 5278 } 5279 5280 // End of obj key 5281 if (curStep === OBJ_KEY_STR) { 5282 stack.pop(); 5283 stack.push(OBJ_KEY); 5284 return; 5285 } 5286 } 5287 5288 function _handleObj(stack, curStep) { 5289 // Initial object 5290 if (!curStep) { 5291 stack.push(OBJ); 5292 return; 5293 } 5294 5295 // New object as obj value 5296 if (curStep === OBJ_VAL) { 5297 stack.push(OBJ); 5298 return; 5299 } 5300 5301 // New object as array element 5302 if (curStep === ARR_VAL) { 5303 stack.push(OBJ); 5304 } 5305 5306 // New object as first array element 5307 if (curStep === ARR) { 5308 stack.push(OBJ); 5309 return; 5310 } 5311 } 5312 5313 function _handleArr(stack, curStep) { 5314 // Initial array 5315 if (!curStep) { 5316 stack.push(ARR); 5317 stack.push(ARR_VAL); 5318 return; 5319 } 5320 5321 // New array as obj value 5322 if (curStep === OBJ_VAL) { 5323 stack.push(ARR); 5324 stack.push(ARR_VAL); 5325 return; 5326 } 5327 5328 // New array as array element 5329 if (curStep === ARR_VAL) { 5330 stack.push(ARR); 5331 stack.push(ARR_VAL); 5332 } 5333 5334 // New array as first array element 5335 if (curStep === ARR) { 5336 stack.push(ARR); 5337 stack.push(ARR_VAL); 5338 return; 5339 } 5340 } 5341 5342 function _handleColon(stack, curStep) { 5343 if (curStep === OBJ_KEY) { 5344 stack.pop(); 5345 stack.push(OBJ_VAL); 5346 } 5347 } 5348 5349 function _handleComma(stack, curStep) { 5350 // Comma after obj value 5351 if (curStep === OBJ_VAL) { 5352 stack.pop(); 5353 return; 5354 } 5355 if (curStep === OBJ_VAL_COMPLETED) { 5356 // Pop OBJ_VAL_COMPLETED & OBJ_VAL 5357 stack.pop(); 5358 stack.pop(); 5359 return; 5360 } 5361 5362 // Comma after arr value 5363 if (curStep === ARR_VAL) { 5364 // do nothing - basically we'd pop ARR_VAL but add it right back 5365 return; 5366 } 5367 5368 if (curStep === ARR_VAL_COMPLETED) { 5369 // Pop ARR_VAL_COMPLETED 5370 stack.pop(); 5371 5372 // basically we'd pop ARR_VAL but add it right back 5373 return; 5374 } 5375 } 5376 5377 function _handleObjClose(stack, curStep) { 5378 // Empty object {} 5379 if (curStep === OBJ) { 5380 stack.pop(); 5381 } 5382 5383 // Object with element 5384 if (curStep === OBJ_VAL) { 5385 // Pop OBJ_VAL, OBJ 5386 stack.pop(); 5387 stack.pop(); 5388 } 5389 5390 // Obj with element 5391 if (curStep === OBJ_VAL_COMPLETED) { 5392 // Pop OBJ_VAL_COMPLETED, OBJ_VAL, OBJ 5393 stack.pop(); 5394 stack.pop(); 5395 stack.pop(); 5396 } 5397 5398 // if was obj value, complete it 5399 if (stack[stack.length - 1] === OBJ_VAL) { 5400 stack.push(OBJ_VAL_COMPLETED); 5401 } 5402 5403 // if was arr value, complete it 5404 if (stack[stack.length - 1] === ARR_VAL) { 5405 stack.push(ARR_VAL_COMPLETED); 5406 } 5407 } 5408 5409 function _handleArrClose(stack, curStep) { 5410 // Empty array [] 5411 if (curStep === ARR) { 5412 stack.pop(); 5413 } 5414 5415 // Array with element 5416 if (curStep === ARR_VAL) { 5417 // Pop ARR_VAL, ARR 5418 stack.pop(); 5419 stack.pop(); 5420 } 5421 5422 // Array with element 5423 if (curStep === ARR_VAL_COMPLETED) { 5424 // Pop ARR_VAL_COMPLETED, ARR_VAL, ARR 5425 stack.pop(); 5426 stack.pop(); 5427 stack.pop(); 5428 } 5429 5430 // if was obj value, complete it 5431 if (stack[stack.length - 1] === OBJ_VAL) { 5432 stack.push(OBJ_VAL_COMPLETED); 5433 } 5434 5435 // if was arr value, complete it 5436 if (stack[stack.length - 1] === ARR_VAL) { 5437 stack.push(ARR_VAL_COMPLETED); 5438 } 5439 } 5440 5441 function _isEscaped(str, pos) { 5442 const previousChar = str[pos - 1]; 5443 5444 return previousChar === '\\' && !_isEscaped(str, pos - 1); 5445 } 5446 5447 /* eslint-disable max-lines */ 5448 5449 /** 5450 * Takes an incomplete JSON string, and returns a hopefully valid JSON string. 5451 * Note that this _can_ fail, so you should check the return value is valid JSON. 5452 */ 5453 function fixJson(incompleteJson) { 5454 const stack = evaluateJson(incompleteJson); 5455 5456 return completeJson(incompleteJson, stack); 5457 } 5458 5459 /** Get the size of a body. */ 5460 function getBodySize( 5461 body, 5462 textEncoder, 5463 ) { 5464 if (!body) { 5465 return undefined; 5466 } 5467 5468 try { 5469 if (typeof body === 'string') { 5470 return textEncoder.encode(body).length; 5471 } 5472 5473 if (body instanceof URLSearchParams) { 5474 return textEncoder.encode(body.toString()).length; 5475 } 5476 5477 if (body instanceof FormData) { 5478 const formDataStr = _serializeFormData(body); 5479 return textEncoder.encode(formDataStr).length; 5480 } 5481 5482 if (body instanceof Blob) { 5483 return body.size; 5484 } 5485 5486 if (body instanceof ArrayBuffer) { 5487 return body.byteLength; 5488 } 5489 5490 // Currently unhandled types: ArrayBufferView, ReadableStream 5491 } catch (e) { 5492 // just return undefined 5493 } 5494 5495 return undefined; 5496 } 5497 5498 /** Convert a Content-Length header to number/undefined. */ 5499 function parseContentLengthHeader(header) { 5500 if (!header) { 5501 return undefined; 5502 } 5503 5504 const size = parseInt(header, 10); 5505 return isNaN(size) ? undefined : size; 5506 } 5507 5508 /** Get the string representation of a body. */ 5509 function getBodyString(body) { 5510 if (typeof body === 'string') { 5511 return body; 5512 } 5513 5514 if (body instanceof URLSearchParams) { 5515 return body.toString(); 5516 } 5517 5518 if (body instanceof FormData) { 5519 return _serializeFormData(body); 5520 } 5521 5522 return undefined; 5523 } 5524 5525 /** Convert ReplayNetworkRequestData to a PerformanceEntry. */ 5526 function makeNetworkReplayBreadcrumb( 5527 type, 5528 data, 5529 ) { 5530 if (!data) { 5531 return null; 5532 } 5533 5534 const { startTimestamp, endTimestamp, url, method, statusCode, request, response } = data; 5535 5536 const result = { 5537 type, 5538 start: startTimestamp / 1000, 5539 end: endTimestamp / 1000, 5540 name: url, 5541 data: dropUndefinedKeys({ 5542 method, 5543 statusCode, 5544 request, 5545 response, 5546 }), 5547 }; 5548 5549 return result; 5550 } 5551 5552 /** Build the request or response part of a replay network breadcrumb that was skipped. */ 5553 function buildSkippedNetworkRequestOrResponse(bodySize) { 5554 return { 5555 headers: {}, 5556 size: bodySize, 5557 _meta: { 5558 warnings: ['URL_SKIPPED'], 5559 }, 5560 }; 5561 } 5562 5563 /** Build the request or response part of a replay network breadcrumb. */ 5564 function buildNetworkRequestOrResponse( 5565 headers, 5566 bodySize, 5567 body, 5568 ) { 5569 if (!bodySize && Object.keys(headers).length === 0) { 5570 return undefined; 5571 } 5572 5573 if (!bodySize) { 5574 return { 5575 headers, 5576 }; 5577 } 5578 5579 if (!body) { 5580 return { 5581 headers, 5582 size: bodySize, 5583 }; 5584 } 5585 5586 const info = { 5587 headers, 5588 size: bodySize, 5589 }; 5590 5591 const { body: normalizedBody, warnings } = normalizeNetworkBody(body); 5592 info.body = normalizedBody; 5593 if (warnings.length > 0) { 5594 info._meta = { 5595 warnings, 5596 }; 5597 } 5598 5599 return info; 5600 } 5601 5602 /** Filter a set of headers */ 5603 function getAllowedHeaders(headers, allowedHeaders) { 5604 return Object.keys(headers).reduce((filteredHeaders, key) => { 5605 const normalizedKey = key.toLowerCase(); 5606 // Avoid putting empty strings into the headers 5607 if (allowedHeaders.includes(normalizedKey) && headers[key]) { 5608 filteredHeaders[normalizedKey] = headers[key]; 5609 } 5610 return filteredHeaders; 5611 }, {}); 5612 } 5613 5614 function _serializeFormData(formData) { 5615 // This is a bit simplified, but gives us a decent estimate 5616 // This converts e.g. { name: 'Anne Smith', age: 13 } to 'name=Anne+Smith&age=13' 5617 // @ts-ignore passing FormData to URLSearchParams actually works 5618 return new URLSearchParams(formData).toString(); 5619 } 5620 5621 function normalizeNetworkBody(body) 5622 5623 { 5624 if (!body || typeof body !== 'string') { 5625 return { 5626 body, 5627 warnings: [], 5628 }; 5629 } 5630 5631 const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE; 5632 5633 if (_strIsProbablyJson(body)) { 5634 try { 5635 const json = exceedsSizeLimit ? fixJson(body.slice(0, NETWORK_BODY_MAX_SIZE)) : body; 5636 const normalizedBody = JSON.parse(json); 5637 return { 5638 body: normalizedBody, 5639 warnings: exceedsSizeLimit ? ['JSON_TRUNCATED'] : [], 5640 }; 5641 } catch (e3) { 5642 return { 5643 body: exceedsSizeLimit ? `${body.slice(0, NETWORK_BODY_MAX_SIZE)}…` : body, 5644 warnings: exceedsSizeLimit ? ['INVALID_JSON', 'TEXT_TRUNCATED'] : ['INVALID_JSON'], 5645 }; 5646 } 5647 } 5648 5649 return { 5650 body: exceedsSizeLimit ? `${body.slice(0, NETWORK_BODY_MAX_SIZE)}…` : body, 5651 warnings: exceedsSizeLimit ? ['TEXT_TRUNCATED'] : [], 5652 }; 5653 } 5654 5655 function _strIsProbablyJson(str) { 5656 const first = str[0]; 5657 const last = str[str.length - 1]; 5658 5659 // Simple check: If this does not start & end with {} or [], it's not JSON 5660 return (first === '[' && last === ']') || (first === '{' && last === '}'); 5661 } 5662 5663 /** Match an URL against a list of strings/Regex. */ 5664 function urlMatches(url, urls) { 5665 const fullUrl = getFullUrl(url); 5666 5667 return stringMatchesSomePattern(fullUrl, urls); 5668 } 5669 5670 /** exported for tests */ 5671 function getFullUrl(url, baseURI = WINDOW.document.baseURI) { 5672 // Short circuit for common cases: 5673 if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith(WINDOW.location.origin)) { 5674 return url; 5675 } 5676 const fixedUrl = new URL(url, baseURI); 5677 5678 // If these do not match, we are not dealing with a relative URL, so just return it 5679 if (fixedUrl.origin !== new URL(baseURI).origin) { 5680 return url; 5681 } 5682 5683 const fullUrl = fixedUrl.href; 5684 5685 // Remove trailing slashes, if they don't match the original URL 5686 if (!url.endsWith('/') && fullUrl.endsWith('/')) { 5687 return fullUrl.slice(0, -1); 5688 } 5689 5690 return fullUrl; 5691 } 5692 5693 /** 5694 * Capture a fetch breadcrumb to a replay. 5695 * This adds additional data (where approriate). 5696 */ 5697 async function captureFetchBreadcrumbToReplay( 5698 breadcrumb, 5699 hint, 5700 options 5701 5702 , 5703 ) { 5704 try { 5705 const data = await _prepareFetchData(breadcrumb, hint, options); 5706 5707 // Create a replay performance entry from this breadcrumb 5708 const result = makeNetworkReplayBreadcrumb('resource.fetch', data); 5709 addNetworkBreadcrumb(options.replay, result); 5710 } catch (error) { 5711 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] Failed to capture fetch breadcrumb', error); 5712 } 5713 } 5714 5715 /** 5716 * Enrich a breadcrumb with additional data. 5717 * This has to be sync & mutate the given breadcrumb, 5718 * as the breadcrumb is afterwards consumed by other handlers. 5719 */ 5720 function enrichFetchBreadcrumb( 5721 breadcrumb, 5722 hint, 5723 options, 5724 ) { 5725 const { input, response } = hint; 5726 5727 const body = _getFetchRequestArgBody(input); 5728 const reqSize = getBodySize(body, options.textEncoder); 5729 5730 const resSize = response ? parseContentLengthHeader(response.headers.get('content-length')) : undefined; 5731 5732 if (reqSize !== undefined) { 5733 breadcrumb.data.request_body_size = reqSize; 5734 } 5735 if (resSize !== undefined) { 5736 breadcrumb.data.response_body_size = resSize; 5737 } 5738 } 5739 5740 async function _prepareFetchData( 5741 breadcrumb, 5742 hint, 5743 options 5744 5745 , 5746 ) { 5747 const { startTimestamp, endTimestamp } = hint; 5748 5749 const { 5750 url, 5751 method, 5752 status_code: statusCode = 0, 5753 request_body_size: requestBodySize, 5754 response_body_size: responseBodySize, 5755 } = breadcrumb.data; 5756 5757 const captureDetails = urlMatches(url, options.networkDetailAllowUrls); 5758 5759 const request = captureDetails 5760 ? _getRequestInfo(options, hint.input, requestBodySize) 5761 : buildSkippedNetworkRequestOrResponse(requestBodySize); 5762 const response = await _getResponseInfo(captureDetails, options, hint.response, responseBodySize); 5763 5764 return { 5765 startTimestamp, 5766 endTimestamp, 5767 url, 5768 method, 5769 statusCode, 5770 request, 5771 response, 5772 }; 5773 } 5774 5775 function _getRequestInfo( 5776 { networkCaptureBodies, networkRequestHeaders }, 5777 input, 5778 requestBodySize, 5779 ) { 5780 const headers = getRequestHeaders(input, networkRequestHeaders); 5781 5782 if (!networkCaptureBodies) { 5783 return buildNetworkRequestOrResponse(headers, requestBodySize, undefined); 5784 } 5785 5786 // We only want to transmit string or string-like bodies 5787 const requestBody = _getFetchRequestArgBody(input); 5788 const bodyStr = getBodyString(requestBody); 5789 return buildNetworkRequestOrResponse(headers, requestBodySize, bodyStr); 5790 } 5791 5792 async function _getResponseInfo( 5793 captureDetails, 5794 { 5795 networkCaptureBodies, 5796 textEncoder, 5797 networkResponseHeaders, 5798 } 5799 5800 , 5801 response, 5802 responseBodySize, 5803 ) { 5804 if (!captureDetails && responseBodySize !== undefined) { 5805 return buildSkippedNetworkRequestOrResponse(responseBodySize); 5806 } 5807 5808 const headers = getAllHeaders(response.headers, networkResponseHeaders); 5809 5810 if (!networkCaptureBodies && responseBodySize !== undefined) { 5811 return buildNetworkRequestOrResponse(headers, responseBodySize, undefined); 5812 } 5813 5814 // Only clone the response if we need to 5815 try { 5816 // We have to clone this, as the body can only be read once 5817 const res = response.clone(); 5818 const bodyText = await _parseFetchBody(res); 5819 5820 const size = 5821 bodyText && bodyText.length && responseBodySize === undefined 5822 ? getBodySize(bodyText, textEncoder) 5823 : responseBodySize; 5824 5825 if (!captureDetails) { 5826 return buildSkippedNetworkRequestOrResponse(size); 5827 } 5828 5829 if (networkCaptureBodies) { 5830 return buildNetworkRequestOrResponse(headers, size, bodyText); 5831 } 5832 5833 return buildNetworkRequestOrResponse(headers, size, undefined); 5834 } catch (e) { 5835 // fallback 5836 return buildNetworkRequestOrResponse(headers, responseBodySize, undefined); 5837 } 5838 } 5839 5840 async function _parseFetchBody(response) { 5841 try { 5842 return await response.text(); 5843 } catch (e2) { 5844 return undefined; 5845 } 5846 } 5847 5848 function _getFetchRequestArgBody(fetchArgs = []) { 5849 // We only support getting the body from the fetch options 5850 if (fetchArgs.length !== 2 || typeof fetchArgs[1] !== 'object') { 5851 return undefined; 5852 } 5853 5854 return (fetchArgs[1] ).body; 5855 } 5856 5857 function getAllHeaders(headers, allowedHeaders) { 5858 const allHeaders = {}; 5859 5860 allowedHeaders.forEach(header => { 5861 if (headers.get(header)) { 5862 allHeaders[header] = headers.get(header) ; 5863 } 5864 }); 5865 5866 return allHeaders; 5867 } 5868 5869 function getRequestHeaders(fetchArgs, allowedHeaders) { 5870 if (fetchArgs.length === 1 && typeof fetchArgs[0] !== 'string') { 5871 return getHeadersFromOptions(fetchArgs[0] , allowedHeaders); 5872 } 5873 5874 if (fetchArgs.length === 2) { 5875 return getHeadersFromOptions(fetchArgs[1] , allowedHeaders); 5876 } 5877 5878 return {}; 5879 } 5880 5881 function getHeadersFromOptions( 5882 input, 5883 allowedHeaders, 5884 ) { 5885 if (!input) { 5886 return {}; 5887 } 5888 5889 const headers = input.headers; 5890 5891 if (!headers) { 5892 return {}; 5893 } 5894 5895 if (headers instanceof Headers) { 5896 return getAllHeaders(headers, allowedHeaders); 5897 } 5898 5899 // We do not support this, as it is not really documented (anymore?) 5900 if (Array.isArray(headers)) { 5901 return {}; 5902 } 5903 5904 return getAllowedHeaders(headers, allowedHeaders); 5905 } 5906 5907 /** 5908 * Capture an XHR breadcrumb to a replay. 5909 * This adds additional data (where approriate). 5910 */ 5911 async function captureXhrBreadcrumbToReplay( 5912 breadcrumb, 5913 hint, 5914 options, 5915 ) { 5916 try { 5917 const data = _prepareXhrData(breadcrumb, hint, options); 5918 5919 // Create a replay performance entry from this breadcrumb 5920 const result = makeNetworkReplayBreadcrumb('resource.xhr', data); 5921 addNetworkBreadcrumb(options.replay, result); 5922 } catch (error) { 5923 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] Failed to capture fetch breadcrumb', error); 5924 } 5925 } 5926 5927 /** 5928 * Enrich a breadcrumb with additional data. 5929 * This has to be sync & mutate the given breadcrumb, 5930 * as the breadcrumb is afterwards consumed by other handlers. 5931 */ 5932 function enrichXhrBreadcrumb( 5933 breadcrumb, 5934 hint, 5935 options, 5936 ) { 5937 const { xhr, input } = hint; 5938 5939 const reqSize = getBodySize(input, options.textEncoder); 5940 const resSize = xhr.getResponseHeader('content-length') 5941 ? parseContentLengthHeader(xhr.getResponseHeader('content-length')) 5942 : getBodySize(xhr.response, options.textEncoder); 5943 5944 if (reqSize !== undefined) { 5945 breadcrumb.data.request_body_size = reqSize; 5946 } 5947 if (resSize !== undefined) { 5948 breadcrumb.data.response_body_size = resSize; 5949 } 5950 } 5951 5952 function _prepareXhrData( 5953 breadcrumb, 5954 hint, 5955 options, 5956 ) { 5957 const { startTimestamp, endTimestamp, input, xhr } = hint; 5958 5959 const { 5960 url, 5961 method, 5962 status_code: statusCode = 0, 5963 request_body_size: requestBodySize, 5964 response_body_size: responseBodySize, 5965 } = breadcrumb.data; 5966 5967 if (!url) { 5968 return null; 5969 } 5970 5971 if (!urlMatches(url, options.networkDetailAllowUrls)) { 5972 const request = buildSkippedNetworkRequestOrResponse(requestBodySize); 5973 const response = buildSkippedNetworkRequestOrResponse(responseBodySize); 5974 return { 5975 startTimestamp, 5976 endTimestamp, 5977 url, 5978 method, 5979 statusCode, 5980 request, 5981 response, 5982 }; 5983 } 5984 5985 const xhrInfo = xhr[SENTRY_XHR_DATA_KEY]; 5986 const networkRequestHeaders = xhrInfo 5987 ? getAllowedHeaders(xhrInfo.request_headers, options.networkRequestHeaders) 5988 : {}; 5989 const networkResponseHeaders = getAllowedHeaders(getResponseHeaders(xhr), options.networkResponseHeaders); 5990 5991 const request = buildNetworkRequestOrResponse( 5992 networkRequestHeaders, 5993 requestBodySize, 5994 options.networkCaptureBodies ? getBodyString(input) : undefined, 5995 ); 5996 const response = buildNetworkRequestOrResponse( 5997 networkResponseHeaders, 5998 responseBodySize, 5999 options.networkCaptureBodies ? hint.xhr.responseText : undefined, 6000 ); 6001 6002 return { 6003 startTimestamp, 6004 endTimestamp, 6005 url, 6006 method, 6007 statusCode, 6008 request, 6009 response, 6010 }; 6011 } 6012 6013 function getResponseHeaders(xhr) { 6014 const headers = xhr.getAllResponseHeaders(); 6015 6016 if (!headers) { 6017 return {}; 6018 } 6019 6020 return headers.split('\r\n').reduce((acc, line) => { 6021 const [key, value] = line.split(': '); 6022 acc[key.toLowerCase()] = value; 6023 return acc; 6024 }, {}); 6025 } 6026 6027 /** 6028 * This method does two things: 6029 * - It enriches the regular XHR/fetch breadcrumbs with request/response size data 6030 * - It captures the XHR/fetch breadcrumbs to the replay 6031 * (enriching it with further data that is _not_ added to the regular breadcrumbs) 6032 */ 6033 function handleNetworkBreadcrumbs(replay) { 6034 const client = getCurrentHub().getClient(); 6035 6036 try { 6037 const textEncoder = new TextEncoder(); 6038 6039 const { networkDetailAllowUrls, networkCaptureBodies, networkRequestHeaders, networkResponseHeaders } = 6040 replay.getOptions(); 6041 6042 const options = { 6043 replay, 6044 textEncoder, 6045 networkDetailAllowUrls, 6046 networkCaptureBodies, 6047 networkRequestHeaders, 6048 networkResponseHeaders, 6049 }; 6050 6051 if (client && client.on) { 6052 client.on('beforeAddBreadcrumb', (breadcrumb, hint) => beforeAddNetworkBreadcrumb(options, breadcrumb, hint)); 6053 } else { 6054 // Fallback behavior 6055 addInstrumentationHandler('fetch', handleFetchSpanListener(replay)); 6056 addInstrumentationHandler('xhr', handleXhrSpanListener(replay)); 6057 } 6058 } catch (e2) { 6059 // Do nothing 6060 } 6061 } 6062 6063 /** just exported for tests */ 6064 function beforeAddNetworkBreadcrumb( 6065 options, 6066 breadcrumb, 6067 hint, 6068 ) { 6069 if (!breadcrumb.data) { 6070 return; 6071 } 6072 6073 try { 6074 if (_isXhrBreadcrumb(breadcrumb) && _isXhrHint(hint)) { 6075 // This has to be sync, as we need to ensure the breadcrumb is enriched in the same tick 6076 // Because the hook runs synchronously, and the breadcrumb is afterwards passed on 6077 // So any async mutations to it will not be reflected in the final breadcrumb 6078 enrichXhrBreadcrumb(breadcrumb, hint, options); 6079 6080 void captureXhrBreadcrumbToReplay(breadcrumb, hint, options); 6081 } 6082 6083 if (_isFetchBreadcrumb(breadcrumb) && _isFetchHint(hint)) { 6084 // This has to be sync, as we need to ensure the breadcrumb is enriched in the same tick 6085 // Because the hook runs synchronously, and the breadcrumb is afterwards passed on 6086 // So any async mutations to it will not be reflected in the final breadcrumb 6087 enrichFetchBreadcrumb(breadcrumb, hint, options); 6088 6089 void captureFetchBreadcrumbToReplay(breadcrumb, hint, options); 6090 } 6091 } catch (e) { 6092 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn('Error when enriching network breadcrumb'); 6093 } 6094 } 6095 6096 function _isXhrBreadcrumb(breadcrumb) { 6097 return breadcrumb.category === 'xhr'; 6098 } 6099 6100 function _isFetchBreadcrumb(breadcrumb) { 6101 return breadcrumb.category === 'fetch'; 6102 } 6103 6104 function _isXhrHint(hint) { 6105 return hint && hint.xhr; 6106 } 6107 6108 function _isFetchHint(hint) { 6109 return hint && hint.response; 6110 } 6111 6112 let _LAST_BREADCRUMB = null; 6113 6114 function isBreadcrumbWithCategory(breadcrumb) { 6115 return !!breadcrumb.category; 6116 } 6117 6118 const handleScopeListener = 6119 (replay) => 6120 (scope) => { 6121 if (!replay.isEnabled()) { 6122 return; 6123 } 6124 6125 const result = handleScope(scope); 6126 6127 if (!result) { 6128 return; 6129 } 6130 6131 addBreadcrumbEvent(replay, result); 6132 }; 6133 6134 /** 6135 * An event handler to handle scope changes. 6136 */ 6137 function handleScope(scope) { 6138 // TODO (v8): Remove this guard. This was put in place because we introduced 6139 // Scope.getLastBreadcrumb mid-v7 which caused incompatibilities with older SDKs. 6140 // For now, we'll just return null if the method doesn't exist but we should eventually 6141 // get rid of this guard. 6142 const newBreadcrumb = scope.getLastBreadcrumb && scope.getLastBreadcrumb(); 6143 6144 // Listener can be called when breadcrumbs have not changed, so we store the 6145 // reference to the last crumb and only return a crumb if it has changed 6146 if (_LAST_BREADCRUMB === newBreadcrumb || !newBreadcrumb) { 6147 return null; 6148 } 6149 6150 _LAST_BREADCRUMB = newBreadcrumb; 6151 6152 if ( 6153 !isBreadcrumbWithCategory(newBreadcrumb) || 6154 ['fetch', 'xhr', 'sentry.event', 'sentry.transaction'].includes(newBreadcrumb.category) || 6155 newBreadcrumb.category.startsWith('ui.') 6156 ) { 6157 return null; 6158 } 6159 6160 if (newBreadcrumb.category === 'console') { 6161 return normalizeConsoleBreadcrumb(newBreadcrumb); 6162 } 6163 6164 return createBreadcrumb(newBreadcrumb); 6165 } 6166 6167 /** exported for tests only */ 6168 function normalizeConsoleBreadcrumb( 6169 breadcrumb, 6170 ) { 6171 const args = breadcrumb.data && breadcrumb.data.arguments; 6172 6173 if (!Array.isArray(args) || args.length === 0) { 6174 return createBreadcrumb(breadcrumb); 6175 } 6176 6177 let isTruncated = false; 6178 6179 // Avoid giant args captures 6180 const normalizedArgs = args.map(arg => { 6181 if (!arg) { 6182 return arg; 6183 } 6184 if (typeof arg === 'string') { 6185 if (arg.length > CONSOLE_ARG_MAX_SIZE) { 6186 isTruncated = true; 6187 return `${arg.slice(0, CONSOLE_ARG_MAX_SIZE)}…`; 6188 } 6189 6190 return arg; 6191 } 6192 if (typeof arg === 'object') { 6193 try { 6194 const normalizedArg = normalize(arg, 7); 6195 const stringified = JSON.stringify(normalizedArg); 6196 if (stringified.length > CONSOLE_ARG_MAX_SIZE) { 6197 const fixedJson = fixJson(stringified.slice(0, CONSOLE_ARG_MAX_SIZE)); 6198 const json = JSON.parse(fixedJson); 6199 // We only set this after JSON.parse() was successfull, so we know we didn't run into `catch` 6200 isTruncated = true; 6201 return json; 6202 } 6203 return normalizedArg; 6204 } catch (e) { 6205 // fall back to default 6206 } 6207 } 6208 6209 return arg; 6210 }); 6211 6212 return createBreadcrumb({ 6213 ...breadcrumb, 6214 data: { 6215 ...breadcrumb.data, 6216 arguments: normalizedArgs, 6217 ...(isTruncated ? { _meta: { warnings: ['CONSOLE_ARG_TRUNCATED'] } } : {}), 6218 }, 6219 }); 6220 } 6221 6222 /** 6223 * Add global listeners that cannot be removed. 6224 */ 6225 function addGlobalListeners(replay) { 6226 // Listeners from core SDK // 6227 const scope = getCurrentHub().getScope(); 6228 const client = getCurrentHub().getClient(); 6229 6230 if (scope) { 6231 scope.addScopeListener(handleScopeListener(replay)); 6232 } 6233 addInstrumentationHandler('dom', handleDomListener(replay)); 6234 addInstrumentationHandler('history', handleHistorySpanListener(replay)); 6235 handleNetworkBreadcrumbs(replay); 6236 6237 // Tag all (non replay) events that get sent to Sentry with the current 6238 // replay ID so that we can reference them later in the UI 6239 addGlobalEventProcessor(handleGlobalEventListener(replay, !hasHooks(client))); 6240 6241 // If a custom client has no hooks yet, we continue to use the "old" implementation 6242 if (hasHooks(client)) { 6243 client.on('afterSendEvent', handleAfterSendEvent(replay)); 6244 client.on('createDsc', (dsc) => { 6245 const replayId = replay.getSessionId(); 6246 // We do not want to set the DSC when in buffer mode, as that means the replay has not been sent (yet) 6247 if (replayId && replay.isEnabled() && replay.recordingMode === 'session') { 6248 dsc.replay_id = replayId; 6249 } 6250 }); 6251 6252 client.on('startTransaction', transaction => { 6253 replay.lastTransaction = transaction; 6254 }); 6255 6256 // We may be missing the initial startTransaction due to timing issues, 6257 // so we capture it on finish again. 6258 client.on('finishTransaction', transaction => { 6259 replay.lastTransaction = transaction; 6260 }); 6261 } 6262 } 6263 6264 // eslint-disable-next-line @typescript-eslint/no-explicit-any 6265 function hasHooks(client) { 6266 return !!(client && client.on); 6267 } 6268 6269 /** 6270 * Create a "span" for the total amount of memory being used by JS objects 6271 * (including v8 internal objects). 6272 */ 6273 async function addMemoryEntry(replay) { 6274 // window.performance.memory is a non-standard API and doesn't work on all browsers, so we try-catch this 6275 try { 6276 return Promise.all( 6277 createPerformanceSpans(replay, [ 6278 // @ts-ignore memory doesn't exist on type Performance as the API is non-standard (we check that it exists above) 6279 createMemoryEntry(WINDOW.performance.memory), 6280 ]), 6281 ); 6282 } catch (error) { 6283 // Do nothing 6284 return []; 6285 } 6286 } 6287 6288 function createMemoryEntry(memoryEntry) { 6289 const { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize } = memoryEntry; 6290 // we don't want to use `getAbsoluteTime` because it adds the event time to the 6291 // time origin, so we get the current timestamp instead 6292 const time = Date.now() / 1000; 6293 return { 6294 type: 'memory', 6295 name: 'memory', 6296 start: time, 6297 end: time, 6298 data: { 6299 memory: { 6300 jsHeapSizeLimit, 6301 totalJSHeapSize, 6302 usedJSHeapSize, 6303 }, 6304 }, 6305 }; 6306 } 6307 6308 // Map entryType -> function to normalize data for event 6309 // @ts-ignore TODO: entry type does not fit the create* functions entry type 6310 const ENTRY_TYPES 6311 6312 = { 6313 // @ts-ignore TODO: entry type does not fit the create* functions entry type 6314 resource: createResourceEntry, 6315 paint: createPaintEntry, 6316 // @ts-ignore TODO: entry type does not fit the create* functions entry type 6317 navigation: createNavigationEntry, 6318 // @ts-ignore TODO: entry type does not fit the create* functions entry type 6319 ['largest-contentful-paint']: createLargestContentfulPaint, 6320 }; 6321 6322 /** 6323 * Create replay performance entries from the browser performance entries. 6324 */ 6325 function createPerformanceEntries( 6326 entries, 6327 ) { 6328 return entries.map(createPerformanceEntry).filter(Boolean) ; 6329 } 6330 6331 function createPerformanceEntry(entry) { 6332 if (ENTRY_TYPES[entry.entryType] === undefined) { 6333 return null; 6334 } 6335 6336 return ENTRY_TYPES[entry.entryType](entry); 6337 } 6338 6339 function getAbsoluteTime(time) { 6340 // browserPerformanceTimeOrigin can be undefined if `performance` or 6341 // `performance.now` doesn't exist, but this is already checked by this integration 6342 return ((browserPerformanceTimeOrigin || WINDOW.performance.timeOrigin) + time) / 1000; 6343 } 6344 6345 function createPaintEntry(entry) { 6346 const { duration, entryType, name, startTime } = entry; 6347 6348 const start = getAbsoluteTime(startTime); 6349 return { 6350 type: entryType, 6351 name, 6352 start, 6353 end: start + duration, 6354 data: undefined, 6355 }; 6356 } 6357 6358 function createNavigationEntry(entry) { 6359 const { 6360 entryType, 6361 name, 6362 decodedBodySize, 6363 duration, 6364 domComplete, 6365 encodedBodySize, 6366 domContentLoadedEventStart, 6367 domContentLoadedEventEnd, 6368 domInteractive, 6369 loadEventStart, 6370 loadEventEnd, 6371 redirectCount, 6372 startTime, 6373 transferSize, 6374 type, 6375 } = entry; 6376 6377 // Ignore entries with no duration, they do not seem to be useful and cause dupes 6378 if (duration === 0) { 6379 return null; 6380 } 6381 6382 return { 6383 type: `${entryType}.${type}`, 6384 start: getAbsoluteTime(startTime), 6385 end: getAbsoluteTime(domComplete), 6386 name, 6387 data: { 6388 size: transferSize, 6389 decodedBodySize, 6390 encodedBodySize, 6391 duration, 6392 domInteractive, 6393 domContentLoadedEventStart, 6394 domContentLoadedEventEnd, 6395 loadEventStart, 6396 loadEventEnd, 6397 domComplete, 6398 redirectCount, 6399 }, 6400 }; 6401 } 6402 6403 function createResourceEntry( 6404 entry, 6405 ) { 6406 const { 6407 entryType, 6408 initiatorType, 6409 name, 6410 responseEnd, 6411 startTime, 6412 decodedBodySize, 6413 encodedBodySize, 6414 responseStatus, 6415 transferSize, 6416 } = entry; 6417 6418 // Core SDK handles these 6419 if (['fetch', 'xmlhttprequest'].includes(initiatorType)) { 6420 return null; 6421 } 6422 6423 return { 6424 type: `${entryType}.${initiatorType}`, 6425 start: getAbsoluteTime(startTime), 6426 end: getAbsoluteTime(responseEnd), 6427 name, 6428 data: { 6429 size: transferSize, 6430 statusCode: responseStatus, 6431 decodedBodySize, 6432 encodedBodySize, 6433 }, 6434 }; 6435 } 6436 6437 function createLargestContentfulPaint( 6438 entry, 6439 ) { 6440 const { entryType, startTime, size } = entry; 6441 6442 let startTimeOrNavigationActivation = 0; 6443 6444 if (WINDOW.performance) { 6445 const navEntry = WINDOW.performance.getEntriesByType('navigation')[0] 6446 6447 ; 6448 6449 // See https://github.com/GoogleChrome/web-vitals/blob/9f11c4c6578fb4c5ee6fa4e32b9d1d756475f135/src/lib/getActivationStart.ts#L21 6450 startTimeOrNavigationActivation = (navEntry && navEntry.activationStart) || 0; 6451 } 6452 6453 // value is in ms 6454 const value = Math.max(startTime - startTimeOrNavigationActivation, 0); 6455 // LCP doesn't have a "duration", it just happens at a single point in time. 6456 // But the UI expects both, so use end (in seconds) for both timestamps. 6457 const end = getAbsoluteTime(startTimeOrNavigationActivation) + value / 1000; 6458 6459 return { 6460 type: entryType, 6461 name: entryType, 6462 start: end, 6463 end, 6464 data: { 6465 value, // LCP "duration" in ms 6466 size, 6467 // Not sure why this errors, Node should be correct (Argument of type 'Node' is not assignable to parameter of type 'INode') 6468 // eslint-disable-next-line @typescript-eslint/no-explicit-any 6469 nodeId: record.mirror.getId(entry.element ), 6470 }, 6471 }; 6472 } 6473 6474 /** 6475 * Heavily simplified debounce function based on lodash.debounce. 6476 * 6477 * This function takes a callback function (@param fun) and delays its invocation 6478 * by @param wait milliseconds. Optionally, a maxWait can be specified in @param options, 6479 * which ensures that the callback is invoked at least once after the specified max. wait time. 6480 * 6481 * @param func the function whose invocation is to be debounced 6482 * @param wait the minimum time until the function is invoked after it was called once 6483 * @param options the options object, which can contain the `maxWait` property 6484 * 6485 * @returns the debounced version of the function, which needs to be called at least once to start the 6486 * debouncing process. Subsequent calls will reset the debouncing timer and, in case @paramfunc 6487 * was already invoked in the meantime, return @param func's return value. 6488 * The debounced function has two additional properties: 6489 * - `flush`: Invokes the debounced function immediately and returns its return value 6490 * - `cancel`: Cancels the debouncing process and resets the debouncing timer 6491 */ 6492 function debounce(func, wait, options) { 6493 let callbackReturnValue; 6494 6495 let timerId; 6496 let maxTimerId; 6497 6498 const maxWait = options && options.maxWait ? Math.max(options.maxWait, wait) : 0; 6499 6500 function invokeFunc() { 6501 cancelTimers(); 6502 callbackReturnValue = func(); 6503 return callbackReturnValue; 6504 } 6505 6506 function cancelTimers() { 6507 timerId !== undefined && clearTimeout(timerId); 6508 maxTimerId !== undefined && clearTimeout(maxTimerId); 6509 timerId = maxTimerId = undefined; 6510 } 6511 6512 function flush() { 6513 if (timerId !== undefined || maxTimerId !== undefined) { 6514 return invokeFunc(); 6515 } 6516 return callbackReturnValue; 6517 } 6518 6519 function debounced() { 6520 if (timerId) { 6521 clearTimeout(timerId); 6522 } 6523 timerId = setTimeout(invokeFunc, wait); 6524 6525 if (maxWait && maxTimerId === undefined) { 6526 maxTimerId = setTimeout(invokeFunc, maxWait); 6527 } 6528 6529 return callbackReturnValue; 6530 } 6531 6532 debounced.cancel = cancelTimers; 6533 debounced.flush = flush; 6534 return debounced; 6535 } 6536 6537 /** 6538 * Handler for recording events. 6539 * 6540 * Adds to event buffer, and has varying flushing behaviors if the event was a checkout. 6541 */ 6542 function getHandleRecordingEmit(replay) { 6543 let hadFirstEvent = false; 6544 6545 return (event, _isCheckout) => { 6546 // If this is false, it means session is expired, create and a new session and wait for checkout 6547 if (!replay.checkAndHandleExpiredSession()) { 6548 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn('[Replay] Received replay event after session expired.'); 6549 6550 return; 6551 } 6552 6553 // `_isCheckout` is only set when the checkout is due to `checkoutEveryNms` 6554 // We also want to treat the first event as a checkout, so we handle this specifically here 6555 const isCheckout = _isCheckout || !hadFirstEvent; 6556 hadFirstEvent = true; 6557 6558 // The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush. 6559 replay.addUpdate(() => { 6560 // The session is always started immediately on pageload/init, but for 6561 // error-only replays, it should reflect the most recent checkout 6562 // when an error occurs. Clear any state that happens before this current 6563 // checkout. This needs to happen before `addEvent()` which updates state 6564 // dependent on this reset. 6565 if (replay.recordingMode === 'buffer' && isCheckout) { 6566 replay.setInitialState(); 6567 } 6568 6569 // We need to clear existing events on a checkout, otherwise they are 6570 // incremental event updates and should be appended 6571 void addEvent(replay, event, isCheckout); 6572 6573 // Different behavior for full snapshots (type=2), ignore other event types 6574 // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16 6575 if (!isCheckout) { 6576 return false; 6577 } 6578 6579 // Additionally, create a meta event that will capture certain SDK settings. 6580 // In order to handle buffer mode, this needs to either be done when we 6581 // receive checkout events or at flush time. 6582 // 6583 // `isCheckout` is always true, but want to be explicit that it should 6584 // only be added for checkouts 6585 void addSettingsEvent(replay, isCheckout); 6586 6587 // If there is a previousSessionId after a full snapshot occurs, then 6588 // the replay session was started due to session expiration. The new session 6589 // is started before triggering a new checkout and contains the id 6590 // of the previous session. Do not immediately flush in this case 6591 // to avoid capturing only the checkout and instead the replay will 6592 // be captured if they perform any follow-up actions. 6593 if (replay.session && replay.session.previousSessionId) { 6594 return true; 6595 } 6596 6597 // When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer 6598 // this should usually be the timestamp of the checkout event, but to be safe... 6599 if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) { 6600 const earliestEvent = replay.eventBuffer.getEarliestTimestamp(); 6601 if (earliestEvent) { 6602 replay.session.started = earliestEvent; 6603 6604 if (replay.getOptions().stickySession) { 6605 saveSession(replay.session); 6606 } 6607 } 6608 } 6609 6610 if (replay.recordingMode === 'session') { 6611 // If the full snapshot is due to an initial load, we will not have 6612 // a previous session ID. In this case, we want to buffer events 6613 // for a set amount of time before flushing. This can help avoid 6614 // capturing replays of users that immediately close the window. 6615 void replay.flush(); 6616 } 6617 6618 return true; 6619 }); 6620 }; 6621 } 6622 6623 /** 6624 * Exported for tests 6625 */ 6626 function createOptionsEvent(replay) { 6627 const options = replay.getOptions(); 6628 return { 6629 type: EventType.Custom, 6630 timestamp: Date.now(), 6631 data: { 6632 tag: 'options', 6633 payload: { 6634 sessionSampleRate: options.sessionSampleRate, 6635 errorSampleRate: options.errorSampleRate, 6636 useCompressionOption: options.useCompression, 6637 blockAllMedia: options.blockAllMedia, 6638 maskAllText: options.maskAllText, 6639 maskAllInputs: options.maskAllInputs, 6640 useCompression: replay.eventBuffer ? replay.eventBuffer.type === 'worker' : false, 6641 networkDetailHasUrls: options.networkDetailAllowUrls.length > 0, 6642 networkCaptureBodies: options.networkCaptureBodies, 6643 networkRequestHasHeaders: options.networkRequestHeaders.length > 0, 6644 networkResponseHasHeaders: options.networkResponseHeaders.length > 0, 6645 }, 6646 }, 6647 }; 6648 } 6649 6650 /** 6651 * Add a "meta" event that contains a simplified view on current configuration 6652 * options. This should only be included on the first segment of a recording. 6653 */ 6654 function addSettingsEvent(replay, isCheckout) { 6655 // Only need to add this event when sending the first segment 6656 if (!isCheckout || !replay.session || replay.session.segmentId !== 0) { 6657 return Promise.resolve(null); 6658 } 6659 6660 return addEvent(replay, createOptionsEvent(replay), false); 6661 } 6662 6663 /** 6664 * Create a replay envelope ready to be sent. 6665 * This includes both the replay event, as well as the recording data. 6666 */ 6667 function createReplayEnvelope( 6668 replayEvent, 6669 recordingData, 6670 dsn, 6671 tunnel, 6672 ) { 6673 return createEnvelope( 6674 createEventEnvelopeHeaders(replayEvent, getSdkMetadataForEnvelopeHeader(replayEvent), tunnel, dsn), 6675 [ 6676 [{ type: 'replay_event' }, replayEvent], 6677 [ 6678 { 6679 type: 'replay_recording', 6680 // If string then we need to encode to UTF8, otherwise will have 6681 // wrong size. TextEncoder has similar browser support to 6682 // MutationObserver, although it does not accept IE11. 6683 length: 6684 typeof recordingData === 'string' ? new TextEncoder().encode(recordingData).length : recordingData.length, 6685 }, 6686 recordingData, 6687 ], 6688 ], 6689 ); 6690 } 6691 6692 /** 6693 * Prepare the recording data ready to be sent. 6694 */ 6695 function prepareRecordingData({ 6696 recordingData, 6697 headers, 6698 } 6699 6700 ) { 6701 let payloadWithSequence; 6702 6703 // XXX: newline is needed to separate sequence id from events 6704 const replayHeaders = `${JSON.stringify(headers)} 6705 `; 6706 6707 if (typeof recordingData === 'string') { 6708 payloadWithSequence = `${replayHeaders}${recordingData}`; 6709 } else { 6710 const enc = new TextEncoder(); 6711 // XXX: newline is needed to separate sequence id from events 6712 const sequence = enc.encode(replayHeaders); 6713 // Merge the two Uint8Arrays 6714 payloadWithSequence = new Uint8Array(sequence.length + recordingData.length); 6715 payloadWithSequence.set(sequence); 6716 payloadWithSequence.set(recordingData, sequence.length); 6717 } 6718 6719 return payloadWithSequence; 6720 } 6721 6722 /** 6723 * Prepare a replay event & enrich it with the SDK metadata. 6724 */ 6725 async function prepareReplayEvent({ 6726 client, 6727 scope, 6728 replayId: event_id, 6729 event, 6730 } 6731 6732 ) { 6733 const integrations = 6734 typeof client._integrations === 'object' && client._integrations !== null && !Array.isArray(client._integrations) 6735 ? Object.keys(client._integrations) 6736 : undefined; 6737 const preparedEvent = (await prepareEvent( 6738 client.getOptions(), 6739 event, 6740 { event_id, integrations }, 6741 scope, 6742 )) ; 6743 6744 // If e.g. a global event processor returned null 6745 if (!preparedEvent) { 6746 return null; 6747 } 6748 6749 // This normally happens in browser client "_prepareEvent" 6750 // but since we do not use this private method from the client, but rather the plain import 6751 // we need to do this manually. 6752 preparedEvent.platform = preparedEvent.platform || 'javascript'; 6753 6754 // extract the SDK name because `client._prepareEvent` doesn't add it to the event 6755 const metadata = client.getSdkMetadata && client.getSdkMetadata(); 6756 const { name, version } = (metadata && metadata.sdk) || {}; 6757 6758 preparedEvent.sdk = { 6759 ...preparedEvent.sdk, 6760 name: name || 'sentry.javascript.unknown', 6761 version: version || '0.0.0', 6762 }; 6763 6764 return preparedEvent; 6765 } 6766 6767 /** 6768 * Send replay attachment using `fetch()` 6769 */ 6770 async function sendReplayRequest({ 6771 recordingData, 6772 replayId, 6773 segmentId: segment_id, 6774 eventContext, 6775 timestamp, 6776 session, 6777 }) { 6778 const preparedRecordingData = prepareRecordingData({ 6779 recordingData, 6780 headers: { 6781 segment_id, 6782 }, 6783 }); 6784 6785 const { urls, errorIds, traceIds, initialTimestamp } = eventContext; 6786 6787 const hub = getCurrentHub(); 6788 const client = hub.getClient(); 6789 const scope = hub.getScope(); 6790 const transport = client && client.getTransport(); 6791 const dsn = client && client.getDsn(); 6792 6793 if (!client || !transport || !dsn || !session.sampled) { 6794 return; 6795 } 6796 6797 const baseEvent = { 6798 type: REPLAY_EVENT_NAME, 6799 replay_start_timestamp: initialTimestamp / 1000, 6800 timestamp: timestamp / 1000, 6801 error_ids: errorIds, 6802 trace_ids: traceIds, 6803 urls, 6804 replay_id: replayId, 6805 segment_id, 6806 replay_type: session.sampled, 6807 }; 6808 6809 const replayEvent = await prepareReplayEvent({ scope, client, replayId, event: baseEvent }); 6810 6811 if (!replayEvent) { 6812 // Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions 6813 client.recordDroppedEvent('event_processor', 'replay', baseEvent); 6814 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('An event processor returned `null`, will not send event.'); 6815 return; 6816 } 6817 6818 /* 6819 For reference, the fully built event looks something like this: 6820 { 6821 "type": "replay_event", 6822 "timestamp": 1670837008.634, 6823 "error_ids": [ 6824 "errorId" 6825 ], 6826 "trace_ids": [ 6827 "traceId" 6828 ], 6829 "urls": [ 6830 "https://example.com" 6831 ], 6832 "replay_id": "eventId", 6833 "segment_id": 3, 6834 "replay_type": "error", 6835 "platform": "javascript", 6836 "event_id": "eventId", 6837 "environment": "production", 6838 "sdk": { 6839 "integrations": [ 6840 "BrowserTracing", 6841 "Replay" 6842 ], 6843 "name": "sentry.javascript.browser", 6844 "version": "7.25.0" 6845 }, 6846 "sdkProcessingMetadata": {}, 6847 "contexts": { 6848 }, 6849 } 6850 */ 6851 6852 const envelope = createReplayEnvelope(replayEvent, preparedRecordingData, dsn, client.getOptions().tunnel); 6853 6854 let response; 6855 6856 try { 6857 response = await transport.send(envelope); 6858 } catch (err) { 6859 const error = new Error(UNABLE_TO_SEND_REPLAY); 6860 6861 try { 6862 // In case browsers don't allow this property to be writable 6863 // @ts-ignore This needs lib es2022 and newer 6864 error.cause = err; 6865 } catch (e) { 6866 // nothing to do 6867 } 6868 throw error; 6869 } 6870 6871 // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore 6872 if (!response) { 6873 return response; 6874 } 6875 6876 // If the status code is invalid, we want to immediately stop & not retry 6877 if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) { 6878 throw new TransportStatusCodeError(response.statusCode); 6879 } 6880 6881 return response; 6882 } 6883 6884 /** 6885 * This error indicates that the transport returned an invalid status code. 6886 */ 6887 class TransportStatusCodeError extends Error { 6888 constructor(statusCode) { 6889 super(`Transport returned status code ${statusCode}`); 6890 } 6891 } 6892 6893 /** 6894 * Finalize and send the current replay event to Sentry 6895 */ 6896 async function sendReplay( 6897 replayData, 6898 retryConfig = { 6899 count: 0, 6900 interval: RETRY_BASE_INTERVAL, 6901 }, 6902 ) { 6903 const { recordingData, options } = replayData; 6904 6905 // short circuit if there's no events to upload (this shouldn't happen as _runFlush makes this check) 6906 if (!recordingData.length) { 6907 return; 6908 } 6909 6910 try { 6911 await sendReplayRequest(replayData); 6912 return true; 6913 } catch (err) { 6914 if (err instanceof TransportStatusCodeError) { 6915 throw err; 6916 } 6917 6918 // Capture error for every failed replay 6919 setContext('Replays', { 6920 _retryCount: retryConfig.count, 6921 }); 6922 6923 if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && options._experiments && options._experiments.captureExceptions) { 6924 captureException(err); 6925 } 6926 6927 // If an error happened here, it's likely that uploading the attachment 6928 // failed, we'll can retry with the same events payload 6929 if (retryConfig.count >= RETRY_MAX_COUNT) { 6930 const error = new Error(`${UNABLE_TO_SEND_REPLAY} - max retries exceeded`); 6931 6932 try { 6933 // In case browsers don't allow this property to be writable 6934 // @ts-ignore This needs lib es2022 and newer 6935 error.cause = err; 6936 } catch (e) { 6937 // nothing to do 6938 } 6939 6940 throw error; 6941 } 6942 6943 // will retry in intervals of 5, 10, 30 6944 retryConfig.interval *= ++retryConfig.count; 6945 6946 return new Promise((resolve, reject) => { 6947 setTimeout(async () => { 6948 try { 6949 await sendReplay(replayData, retryConfig); 6950 resolve(true); 6951 } catch (err) { 6952 reject(err); 6953 } 6954 }, retryConfig.interval); 6955 }); 6956 } 6957 } 6958 6959 const THROTTLED = '__THROTTLED'; 6960 const SKIPPED = '__SKIPPED'; 6961 6962 /** 6963 * Create a throttled function off a given function. 6964 * When calling the throttled function, it will call the original function only 6965 * if it hasn't been called more than `maxCount` times in the last `durationSeconds`. 6966 * 6967 * Returns `THROTTLED` if throttled for the first time, after that `SKIPPED`, 6968 * or else the return value of the original function. 6969 */ 6970 // eslint-disable-next-line @typescript-eslint/no-explicit-any 6971 function throttle( 6972 fn, 6973 maxCount, 6974 durationSeconds, 6975 ) { 6976 const counter = new Map(); 6977 6978 const _cleanup = (now) => { 6979 const threshold = now - durationSeconds; 6980 counter.forEach((_value, key) => { 6981 if (key < threshold) { 6982 counter.delete(key); 6983 } 6984 }); 6985 }; 6986 6987 const _getTotalCount = () => { 6988 return [...counter.values()].reduce((a, b) => a + b, 0); 6989 }; 6990 6991 let isThrottled = false; 6992 6993 return (...rest) => { 6994 // Date in second-precision, which we use as basis for the throttling 6995 const now = Math.floor(Date.now() / 1000); 6996 6997 // First, make sure to delete any old entries 6998 _cleanup(now); 6999 7000 // If already over limit, do nothing 7001 if (_getTotalCount() >= maxCount) { 7002 const wasThrottled = isThrottled; 7003 isThrottled = true; 7004 return wasThrottled ? SKIPPED : THROTTLED; 7005 } 7006 7007 isThrottled = false; 7008 const count = counter.get(now) || 0; 7009 counter.set(now, count + 1); 7010 7011 return fn(...rest); 7012 }; 7013 } 7014 7015 /* eslint-disable max-lines */ // TODO: We might want to split this file up 7016 7017 /** 7018 * The main replay container class, which holds all the state and methods for recording and sending replays. 7019 */ 7020 class ReplayContainer { 7021 __init() {this.eventBuffer = null;} 7022 7023 /** 7024 * List of PerformanceEntry from PerformanceObserver 7025 */ 7026 __init2() {this.performanceEvents = [];} 7027 7028 /** 7029 * Recording can happen in one of three modes: 7030 * - session: Record the whole session, sending it continuously 7031 * - buffer: Always keep the last 60s of recording, requires: 7032 * - having replaysOnErrorSampleRate > 0 to capture replay when an error occurs 7033 * - or calling `flush()` to send the replay 7034 */ 7035 __init3() {this.recordingMode = 'session';} 7036 7037 /** 7038 * The current or last active transcation. 7039 * This is only available when performance is enabled. 7040 */ 7041 7042 /** 7043 * These are here so we can overwrite them in tests etc. 7044 * @hidden 7045 */ 7046 __init4() {this.timeouts = { 7047 sessionIdlePause: SESSION_IDLE_PAUSE_DURATION, 7048 sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION, 7049 maxSessionLife: MAX_SESSION_LIFE, 7050 }; } 7051 7052 /** 7053 * Options to pass to `rrweb.record()` 7054 */ 7055 7056 __init5() {this._performanceObserver = null;} 7057 7058 __init6() {this._flushLock = null;} 7059 7060 /** 7061 * Timestamp of the last user activity. This lives across sessions. 7062 */ 7063 __init7() {this._lastActivity = Date.now();} 7064 7065 /** 7066 * Is the integration currently active? 7067 */ 7068 __init8() {this._isEnabled = false;} 7069 7070 /** 7071 * Paused is a state where: 7072 * - DOM Recording is not listening at all 7073 * - Nothing will be added to event buffer (e.g. core SDK events) 7074 */ 7075 __init9() {this._isPaused = false;} 7076 7077 /** 7078 * Have we attached listeners to the core SDK? 7079 * Note we have to track this as there is no way to remove instrumentation handlers. 7080 */ 7081 __init10() {this._hasInitializedCoreListeners = false;} 7082 7083 /** 7084 * Function to stop recording 7085 */ 7086 __init11() {this._stopRecording = null;} 7087 7088 __init12() {this._context = { 7089 errorIds: new Set(), 7090 traceIds: new Set(), 7091 urls: [], 7092 initialTimestamp: Date.now(), 7093 initialUrl: '', 7094 };} 7095 7096 constructor({ 7097 options, 7098 recordingOptions, 7099 } 7100 7101 ) {ReplayContainer.prototype.__init.call(this);ReplayContainer.prototype.__init2.call(this);ReplayContainer.prototype.__init3.call(this);ReplayContainer.prototype.__init4.call(this);ReplayContainer.prototype.__init5.call(this);ReplayContainer.prototype.__init6.call(this);ReplayContainer.prototype.__init7.call(this);ReplayContainer.prototype.__init8.call(this);ReplayContainer.prototype.__init9.call(this);ReplayContainer.prototype.__init10.call(this);ReplayContainer.prototype.__init11.call(this);ReplayContainer.prototype.__init12.call(this);ReplayContainer.prototype.__init13.call(this);ReplayContainer.prototype.__init14.call(this);ReplayContainer.prototype.__init15.call(this);ReplayContainer.prototype.__init16.call(this);ReplayContainer.prototype.__init17.call(this);ReplayContainer.prototype.__init18.call(this); 7102 this._recordingOptions = recordingOptions; 7103 this._options = options; 7104 7105 this._debouncedFlush = debounce(() => this._flush(), this._options.flushMinDelay, { 7106 maxWait: this._options.flushMaxDelay, 7107 }); 7108 7109 this._throttledAddEvent = throttle( 7110 (event, isCheckout) => addEvent(this, event, isCheckout), 7111 // Max 300 events... 7112 300, 7113 // ... per 5s 7114 5, 7115 ); 7116 7117 const { slowClickTimeout, slowClickIgnoreSelectors } = this.getOptions(); 7118 7119 const slowClickConfig = slowClickTimeout 7120 ? { 7121 threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout), 7122 timeout: slowClickTimeout, 7123 scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT, 7124 ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '', 7125 multiClickTimeout: MULTI_CLICK_TIMEOUT, 7126 } 7127 : undefined; 7128 7129 if (slowClickConfig) { 7130 this.clickDetector = new ClickDetector(this, slowClickConfig); 7131 } 7132 } 7133 7134 /** Get the event context. */ 7135 getContext() { 7136 return this._context; 7137 } 7138 7139 /** If recording is currently enabled. */ 7140 isEnabled() { 7141 return this._isEnabled; 7142 } 7143 7144 /** If recording is currently paused. */ 7145 isPaused() { 7146 return this._isPaused; 7147 } 7148 7149 /** Get the replay integration options. */ 7150 getOptions() { 7151 return this._options; 7152 } 7153 7154 /** 7155 * Initializes the plugin based on sampling configuration. Should not be 7156 * called outside of constructor. 7157 */ 7158 initializeSampling() { 7159 const { errorSampleRate, sessionSampleRate } = this._options; 7160 7161 // If neither sample rate is > 0, then do nothing - user will need to call one of 7162 // `start()` or `startBuffering` themselves. 7163 if (errorSampleRate <= 0 && sessionSampleRate <= 0) { 7164 return; 7165 } 7166 7167 // Otherwise if there is _any_ sample rate set, try to load an existing 7168 // session, or create a new one. 7169 const isSessionSampled = this._loadAndCheckSession(); 7170 7171 if (!isSessionSampled) { 7172 // This should only occur if `errorSampleRate` is 0 and was unsampled for 7173 // session-based replay. In this case there is nothing to do. 7174 return; 7175 } 7176 7177 if (!this.session) { 7178 // This should not happen, something wrong has occurred 7179 this._handleException(new Error('Unable to initialize and create session')); 7180 return; 7181 } 7182 7183 if (this.session.sampled && this.session.sampled !== 'session') { 7184 // If not sampled as session-based, then recording mode will be `buffer` 7185 // Note that we don't explicitly check if `sampled === 'buffer'` because we 7186 // could have sessions from Session storage that are still `error` from 7187 // prior SDK version. 7188 this.recordingMode = 'buffer'; 7189 } 7190 7191 this._initializeRecording(); 7192 } 7193 7194 /** 7195 * Start a replay regardless of sampling rate. Calling this will always 7196 * create a new session. Will throw an error if replay is already in progress. 7197 * 7198 * Creates or loads a session, attaches listeners to varying events (DOM, 7199 * _performanceObserver, Recording, Sentry SDK, etc) 7200 */ 7201 start() { 7202 if (this._isEnabled && this.recordingMode === 'session') { 7203 throw new Error('Replay recording is already in progress'); 7204 } 7205 7206 if (this._isEnabled && this.recordingMode === 'buffer') { 7207 throw new Error('Replay buffering is in progress, call `flush()` to save the replay'); 7208 } 7209 7210 const previousSessionId = this.session && this.session.id; 7211 7212 const { session } = getSession({ 7213 timeouts: this.timeouts, 7214 stickySession: Boolean(this._options.stickySession), 7215 currentSession: this.session, 7216 // This is intentional: create a new session-based replay when calling `start()` 7217 sessionSampleRate: 1, 7218 allowBuffering: false, 7219 }); 7220 7221 session.previousSessionId = previousSessionId; 7222 this.session = session; 7223 7224 this._initializeRecording(); 7225 } 7226 7227 /** 7228 * Start replay buffering. Buffers until `flush()` is called or, if 7229 * `replaysOnErrorSampleRate` > 0, an error occurs. 7230 */ 7231 startBuffering() { 7232 if (this._isEnabled) { 7233 throw new Error('Replay recording is already in progress'); 7234 } 7235 7236 const previousSessionId = this.session && this.session.id; 7237 7238 const { session } = getSession({ 7239 timeouts: this.timeouts, 7240 stickySession: Boolean(this._options.stickySession), 7241 currentSession: this.session, 7242 sessionSampleRate: 0, 7243 allowBuffering: true, 7244 }); 7245 7246 session.previousSessionId = previousSessionId; 7247 this.session = session; 7248 7249 this.recordingMode = 'buffer'; 7250 this._initializeRecording(); 7251 } 7252 7253 /** 7254 * Start recording. 7255 * 7256 * Note that this will cause a new DOM checkout 7257 */ 7258 startRecording() { 7259 try { 7260 this._stopRecording = record({ 7261 ...this._recordingOptions, 7262 // When running in error sampling mode, we need to overwrite `checkoutEveryNms` 7263 // Without this, it would record forever, until an error happens, which we don't want 7264 // instead, we'll always keep the last 60 seconds of replay before an error happened 7265 ...(this.recordingMode === 'buffer' && { checkoutEveryNms: BUFFER_CHECKOUT_TIME }), 7266 emit: getHandleRecordingEmit(this), 7267 onMutation: this._onMutationHandler, 7268 }); 7269 } catch (err) { 7270 this._handleException(err); 7271 } 7272 } 7273 7274 /** 7275 * Stops the recording, if it was running. 7276 * 7277 * Returns true if it was previously stopped, or is now stopped, 7278 * otherwise false. 7279 */ 7280 stopRecording() { 7281 try { 7282 if (this._stopRecording) { 7283 this._stopRecording(); 7284 this._stopRecording = undefined; 7285 } 7286 7287 return true; 7288 } catch (err) { 7289 this._handleException(err); 7290 return false; 7291 } 7292 } 7293 7294 /** 7295 * Currently, this needs to be manually called (e.g. for tests). Sentry SDK 7296 * does not support a teardown 7297 */ 7298 async stop(reason) { 7299 if (!this._isEnabled) { 7300 return; 7301 } 7302 7303 try { 7304 if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { 7305 const msg = `[Replay] Stopping Replay${reason ? ` triggered by ${reason}` : ''}`; 7306 7307 // When `traceInternals` is enabled, we want to log this to the console 7308 // Else, use the regular debug output 7309 // eslint-disable-next-line 7310 const log = this.getOptions()._experiments.traceInternals ? console.warn : logger.log; 7311 log(msg); 7312 } 7313 7314 // We can't move `_isEnabled` after awaiting a flush, otherwise we can 7315 // enter into an infinite loop when `stop()` is called while flushing. 7316 this._isEnabled = false; 7317 this._removeListeners(); 7318 this.stopRecording(); 7319 7320 this._debouncedFlush.cancel(); 7321 // See comment above re: `_isEnabled`, we "force" a flush, ignoring the 7322 // `_isEnabled` state of the plugin since it was disabled above. 7323 if (this.recordingMode === 'session') { 7324 await this._flush({ force: true }); 7325 } 7326 7327 // After flush, destroy event buffer 7328 this.eventBuffer && this.eventBuffer.destroy(); 7329 this.eventBuffer = null; 7330 7331 // Clear session from session storage, note this means if a new session 7332 // is started after, it will not have `previousSessionId` 7333 clearSession(this); 7334 } catch (err) { 7335 this._handleException(err); 7336 } 7337 } 7338 7339 /** 7340 * Pause some replay functionality. See comments for `_isPaused`. 7341 * This differs from stop as this only stops DOM recording, it is 7342 * not as thorough of a shutdown as `stop()`. 7343 */ 7344 pause() { 7345 this._isPaused = true; 7346 this.stopRecording(); 7347 } 7348 7349 /** 7350 * Resumes recording, see notes for `pause(). 7351 * 7352 * Note that calling `startRecording()` here will cause a 7353 * new DOM checkout.` 7354 */ 7355 resume() { 7356 if (!this._loadAndCheckSession()) { 7357 return; 7358 } 7359 7360 this._isPaused = false; 7361 this.startRecording(); 7362 } 7363 7364 /** 7365 * If not in "session" recording mode, flush event buffer which will create a new replay. 7366 * Unless `continueRecording` is false, the replay will continue to record and 7367 * behave as a "session"-based replay. 7368 * 7369 * Otherwise, queue up a flush. 7370 */ 7371 async sendBufferedReplayOrFlush({ continueRecording = true } = {}) { 7372 if (this.recordingMode === 'session') { 7373 return this.flushImmediate(); 7374 } 7375 7376 const activityTime = Date.now(); 7377 7378 // Allow flush to complete before resuming as a session recording, otherwise 7379 // the checkout from `startRecording` may be included in the payload. 7380 // Prefer to keep the error replay as a separate (and smaller) segment 7381 // than the session replay. 7382 await this.flushImmediate(); 7383 7384 const hasStoppedRecording = this.stopRecording(); 7385 7386 if (!continueRecording || !hasStoppedRecording) { 7387 return; 7388 } 7389 7390 // Re-start recording, but in "session" recording mode 7391 7392 // Reset all "capture on error" configuration before 7393 // starting a new recording 7394 this.recordingMode = 'session'; 7395 7396 // Once this session ends, we do not want to refresh it 7397 if (this.session) { 7398 this.session.shouldRefresh = false; 7399 7400 // It's possible that the session lifespan is > max session lifespan 7401 // because we have been buffering beyond max session lifespan (we ignore 7402 // expiration given that `shouldRefresh` is true). Since we flip 7403 // `shouldRefresh`, the session could be considered expired due to 7404 // lifespan, which is not what we want. Update session start date to be 7405 // the current timestamp, so that session is not considered to be 7406 // expired. This means that max replay duration can be MAX_SESSION_LIFE + 7407 // (length of buffer), which we are ok with. 7408 this._updateUserActivity(activityTime); 7409 this._updateSessionActivity(activityTime); 7410 this.session.started = activityTime; 7411 this._maybeSaveSession(); 7412 } 7413 7414 this.startRecording(); 7415 } 7416 7417 /** 7418 * We want to batch uploads of replay events. Save events only if 7419 * `<flushMinDelay>` milliseconds have elapsed since the last event 7420 * *OR* if `<flushMaxDelay>` milliseconds have elapsed. 7421 * 7422 * Accepts a callback to perform side-effects and returns true to stop batch 7423 * processing and hand back control to caller. 7424 */ 7425 addUpdate(cb) { 7426 // We need to always run `cb` (e.g. in the case of `this.recordingMode == 'buffer'`) 7427 const cbResult = cb(); 7428 7429 // If this option is turned on then we will only want to call `flush` 7430 // explicitly 7431 if (this.recordingMode === 'buffer') { 7432 return; 7433 } 7434 7435 // If callback is true, we do not want to continue with flushing -- the 7436 // caller will need to handle it. 7437 if (cbResult === true) { 7438 return; 7439 } 7440 7441 // addUpdate is called quite frequently - use _debouncedFlush so that it 7442 // respects the flush delays and does not flush immediately 7443 this._debouncedFlush(); 7444 } 7445 7446 /** 7447 * Updates the user activity timestamp and resumes recording. This should be 7448 * called in an event handler for a user action that we consider as the user 7449 * being "active" (e.g. a mouse click). 7450 */ 7451 triggerUserActivity() { 7452 this._updateUserActivity(); 7453 7454 // This case means that recording was once stopped due to inactivity. 7455 // Ensure that recording is resumed. 7456 if (!this._stopRecording) { 7457 // Create a new session, otherwise when the user action is flushed, it 7458 // will get rejected due to an expired session. 7459 if (!this._loadAndCheckSession()) { 7460 return; 7461 } 7462 7463 // Note: This will cause a new DOM checkout 7464 this.resume(); 7465 return; 7466 } 7467 7468 // Otherwise... recording was never suspended, continue as normalish 7469 this.checkAndHandleExpiredSession(); 7470 7471 this._updateSessionActivity(); 7472 } 7473 7474 /** 7475 * Updates the user activity timestamp *without* resuming 7476 * recording. Some user events (e.g. keydown) can be create 7477 * low-value replays that only contain the keypress as a 7478 * breadcrumb. Instead this would require other events to 7479 * create a new replay after a session has expired. 7480 */ 7481 updateUserActivity() { 7482 this._updateUserActivity(); 7483 this._updateSessionActivity(); 7484 } 7485 7486 /** 7487 * Only flush if `this.recordingMode === 'session'` 7488 */ 7489 conditionalFlush() { 7490 if (this.recordingMode === 'buffer') { 7491 return Promise.resolve(); 7492 } 7493 7494 return this.flushImmediate(); 7495 } 7496 7497 /** 7498 * Flush using debounce flush 7499 */ 7500 flush() { 7501 return this._debouncedFlush() ; 7502 } 7503 7504 /** 7505 * Always flush via `_debouncedFlush` so that we do not have flushes triggered 7506 * from calling both `flush` and `_debouncedFlush`. Otherwise, there could be 7507 * cases of mulitple flushes happening closely together. 7508 */ 7509 flushImmediate() { 7510 this._debouncedFlush(); 7511 // `.flush` is provided by the debounced function, analogously to lodash.debounce 7512 return this._debouncedFlush.flush() ; 7513 } 7514 7515 /** 7516 * Cancels queued up flushes. 7517 */ 7518 cancelFlush() { 7519 this._debouncedFlush.cancel(); 7520 } 7521 7522 /** Get the current sesion (=replay) ID */ 7523 getSessionId() { 7524 return this.session && this.session.id; 7525 } 7526 7527 /** 7528 * Checks if recording should be stopped due to user inactivity. Otherwise 7529 * check if session is expired and create a new session if so. Triggers a new 7530 * full snapshot on new session. 7531 * 7532 * Returns true if session is not expired, false otherwise. 7533 * @hidden 7534 */ 7535 checkAndHandleExpiredSession() { 7536 const oldSessionId = this.getSessionId(); 7537 7538 // Prevent starting a new session if the last user activity is older than 7539 // SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new 7540 // session+recording. This creates noisy replays that do not have much 7541 // content in them. 7542 if ( 7543 this._lastActivity && 7544 isExpired(this._lastActivity, this.timeouts.sessionIdlePause) && 7545 this.session && 7546 this.session.sampled === 'session' 7547 ) { 7548 // Pause recording only for session-based replays. Otherwise, resuming 7549 // will create a new replay and will conflict with users who only choose 7550 // to record error-based replays only. (e.g. the resumed replay will not 7551 // contain a reference to an error) 7552 this.pause(); 7553 return; 7554 } 7555 7556 // --- There is recent user activity --- // 7557 // This will create a new session if expired, based on expiry length 7558 if (!this._loadAndCheckSession()) { 7559 return; 7560 } 7561 7562 // Session was expired if session ids do not match 7563 const expired = oldSessionId !== this.getSessionId(); 7564 7565 if (!expired) { 7566 return true; 7567 } 7568 7569 // Session is expired, trigger a full snapshot (which will create a new session) 7570 this._triggerFullSnapshot(); 7571 7572 return false; 7573 } 7574 7575 /** 7576 * Capture some initial state that can change throughout the lifespan of the 7577 * replay. This is required because otherwise they would be captured at the 7578 * first flush. 7579 */ 7580 setInitialState() { 7581 const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`; 7582 const url = `${WINDOW.location.origin}${urlPath}`; 7583 7584 this.performanceEvents = []; 7585 7586 // Reset _context as well 7587 this._clearContext(); 7588 7589 this._context.initialUrl = url; 7590 this._context.initialTimestamp = Date.now(); 7591 this._context.urls.push(url); 7592 } 7593 7594 /** 7595 * Add a breadcrumb event, that may be throttled. 7596 * If it was throttled, we add a custom breadcrumb to indicate that. 7597 */ 7598 throttledAddEvent( 7599 event, 7600 isCheckout, 7601 ) { 7602 const res = this._throttledAddEvent(event, isCheckout); 7603 7604 // If this is THROTTLED, it means we have throttled the event for the first time 7605 // In this case, we want to add a breadcrumb indicating that something was skipped 7606 if (res === THROTTLED) { 7607 const breadcrumb = createBreadcrumb({ 7608 category: 'replay.throttled', 7609 }); 7610 7611 this.addUpdate(() => { 7612 void addEvent(this, { 7613 type: EventType.Custom, 7614 timestamp: breadcrumb.timestamp || 0, 7615 data: { 7616 tag: 'breadcrumb', 7617 payload: breadcrumb, 7618 metric: true, 7619 }, 7620 }); 7621 }); 7622 } 7623 7624 return res; 7625 } 7626 7627 /** 7628 * This will get the parametrized route name of the current page. 7629 * This is only available if performance is enabled, and if an instrumented router is used. 7630 */ 7631 getCurrentRoute() { 7632 const lastTransaction = this.lastTransaction || getCurrentHub().getScope().getTransaction(); 7633 if (!lastTransaction || !['route', 'custom'].includes(lastTransaction.metadata.source)) { 7634 return undefined; 7635 } 7636 7637 return lastTransaction.name; 7638 } 7639 7640 /** 7641 * Initialize and start all listeners to varying events (DOM, 7642 * Performance Observer, Recording, Sentry SDK, etc) 7643 */ 7644 _initializeRecording() { 7645 this.setInitialState(); 7646 7647 // this method is generally called on page load or manually - in both cases 7648 // we should treat it as an activity 7649 this._updateSessionActivity(); 7650 7651 this.eventBuffer = createEventBuffer({ 7652 useCompression: this._options.useCompression, 7653 }); 7654 7655 this._removeListeners(); 7656 this._addListeners(); 7657 7658 // Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout 7659 this._isEnabled = true; 7660 7661 this.startRecording(); 7662 } 7663 7664 /** A wrapper to conditionally capture exceptions. */ 7665 _handleException(error) { 7666 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay]', error); 7667 7668 if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && this._options._experiments && this._options._experiments.captureExceptions) { 7669 captureException(error); 7670 } 7671 } 7672 7673 /** 7674 * Loads (or refreshes) the current session. 7675 * Returns false if session is not recorded. 7676 */ 7677 _loadAndCheckSession() { 7678 const { type, session } = getSession({ 7679 timeouts: this.timeouts, 7680 stickySession: Boolean(this._options.stickySession), 7681 currentSession: this.session, 7682 sessionSampleRate: this._options.sessionSampleRate, 7683 allowBuffering: this._options.errorSampleRate > 0 || this.recordingMode === 'buffer', 7684 }); 7685 7686 // If session was newly created (i.e. was not loaded from storage), then 7687 // enable flag to create the root replay 7688 if (type === 'new') { 7689 this.setInitialState(); 7690 } 7691 7692 const currentSessionId = this.getSessionId(); 7693 if (session.id !== currentSessionId) { 7694 session.previousSessionId = currentSessionId; 7695 } 7696 7697 this.session = session; 7698 7699 if (!this.session.sampled) { 7700 void this.stop('session unsampled'); 7701 return false; 7702 } 7703 7704 return true; 7705 } 7706 7707 /** 7708 * Adds listeners to record events for the replay 7709 */ 7710 _addListeners() { 7711 try { 7712 WINDOW.document.addEventListener('visibilitychange', this._handleVisibilityChange); 7713 WINDOW.addEventListener('blur', this._handleWindowBlur); 7714 WINDOW.addEventListener('focus', this._handleWindowFocus); 7715 WINDOW.addEventListener('keydown', this._handleKeyboardEvent); 7716 7717 if (this.clickDetector) { 7718 this.clickDetector.addListeners(); 7719 } 7720 7721 // There is no way to remove these listeners, so ensure they are only added once 7722 if (!this._hasInitializedCoreListeners) { 7723 addGlobalListeners(this); 7724 7725 this._hasInitializedCoreListeners = true; 7726 } 7727 } catch (err) { 7728 this._handleException(err); 7729 } 7730 7731 // PerformanceObserver // 7732 if (!('PerformanceObserver' in WINDOW)) { 7733 return; 7734 } 7735 7736 this._performanceObserver = setupPerformanceObserver(this); 7737 } 7738 7739 /** 7740 * Cleans up listeners that were created in `_addListeners` 7741 */ 7742 _removeListeners() { 7743 try { 7744 WINDOW.document.removeEventListener('visibilitychange', this._handleVisibilityChange); 7745 7746 WINDOW.removeEventListener('blur', this._handleWindowBlur); 7747 WINDOW.removeEventListener('focus', this._handleWindowFocus); 7748 WINDOW.removeEventListener('keydown', this._handleKeyboardEvent); 7749 7750 if (this.clickDetector) { 7751 this.clickDetector.removeListeners(); 7752 } 7753 7754 if (this._performanceObserver) { 7755 this._performanceObserver.disconnect(); 7756 this._performanceObserver = null; 7757 } 7758 } catch (err) { 7759 this._handleException(err); 7760 } 7761 } 7762 7763 /** 7764 * Handle when visibility of the page content changes. Opening a new tab will 7765 * cause the state to change to hidden because of content of current page will 7766 * be hidden. Likewise, moving a different window to cover the contents of the 7767 * page will also trigger a change to a hidden state. 7768 */ 7769 __init13() {this._handleVisibilityChange = () => { 7770 if (WINDOW.document.visibilityState === 'visible') { 7771 this._doChangeToForegroundTasks(); 7772 } else { 7773 this._doChangeToBackgroundTasks(); 7774 } 7775 };} 7776 7777 /** 7778 * Handle when page is blurred 7779 */ 7780 __init14() {this._handleWindowBlur = () => { 7781 const breadcrumb = createBreadcrumb({ 7782 category: 'ui.blur', 7783 }); 7784 7785 // Do not count blur as a user action -- it's part of the process of them 7786 // leaving the page 7787 this._doChangeToBackgroundTasks(breadcrumb); 7788 };} 7789 7790 /** 7791 * Handle when page is focused 7792 */ 7793 __init15() {this._handleWindowFocus = () => { 7794 const breadcrumb = createBreadcrumb({ 7795 category: 'ui.focus', 7796 }); 7797 7798 // Do not count focus as a user action -- instead wait until they focus and 7799 // interactive with page 7800 this._doChangeToForegroundTasks(breadcrumb); 7801 };} 7802 7803 /** Ensure page remains active when a key is pressed. */ 7804 __init16() {this._handleKeyboardEvent = (event) => { 7805 handleKeyboardEvent(this, event); 7806 };} 7807 7808 /** 7809 * Tasks to run when we consider a page to be hidden (via blurring and/or visibility) 7810 */ 7811 _doChangeToBackgroundTasks(breadcrumb) { 7812 if (!this.session) { 7813 return; 7814 } 7815 7816 const expired = isSessionExpired(this.session, this.timeouts); 7817 7818 if (breadcrumb && !expired) { 7819 this._createCustomBreadcrumb(breadcrumb); 7820 } 7821 7822 // Send replay when the page/tab becomes hidden. There is no reason to send 7823 // replay if it becomes visible, since no actions we care about were done 7824 // while it was hidden 7825 void this.conditionalFlush(); 7826 } 7827 7828 /** 7829 * Tasks to run when we consider a page to be visible (via focus and/or visibility) 7830 */ 7831 _doChangeToForegroundTasks(breadcrumb) { 7832 if (!this.session) { 7833 return; 7834 } 7835 7836 const isSessionActive = this.checkAndHandleExpiredSession(); 7837 7838 if (!isSessionActive) { 7839 // If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION 7840 // ms, we will re-use the existing session, otherwise create a new 7841 // session 7842 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Document has become active, but session has expired'); 7843 return; 7844 } 7845 7846 if (breadcrumb) { 7847 this._createCustomBreadcrumb(breadcrumb); 7848 } 7849 } 7850 7851 /** 7852 * Trigger rrweb to take a full snapshot which will cause this plugin to 7853 * create a new Replay event. 7854 */ 7855 _triggerFullSnapshot(checkout = true) { 7856 try { 7857 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Taking full rrweb snapshot'); 7858 record.takeFullSnapshot(checkout); 7859 } catch (err) { 7860 this._handleException(err); 7861 } 7862 } 7863 7864 /** 7865 * Update user activity (across session lifespans) 7866 */ 7867 _updateUserActivity(_lastActivity = Date.now()) { 7868 this._lastActivity = _lastActivity; 7869 } 7870 7871 /** 7872 * Updates the session's last activity timestamp 7873 */ 7874 _updateSessionActivity(_lastActivity = Date.now()) { 7875 if (this.session) { 7876 this.session.lastActivity = _lastActivity; 7877 this._maybeSaveSession(); 7878 } 7879 } 7880 7881 /** 7882 * Helper to create (and buffer) a replay breadcrumb from a core SDK breadcrumb 7883 */ 7884 _createCustomBreadcrumb(breadcrumb) { 7885 this.addUpdate(() => { 7886 void this.throttledAddEvent({ 7887 type: EventType.Custom, 7888 timestamp: breadcrumb.timestamp || 0, 7889 data: { 7890 tag: 'breadcrumb', 7891 payload: breadcrumb, 7892 }, 7893 }); 7894 }); 7895 } 7896 7897 /** 7898 * Observed performance events are added to `this.performanceEvents`. These 7899 * are included in the replay event before it is finished and sent to Sentry. 7900 */ 7901 _addPerformanceEntries() { 7902 // Copy and reset entries before processing 7903 const entries = [...this.performanceEvents]; 7904 this.performanceEvents = []; 7905 7906 return Promise.all(createPerformanceSpans(this, createPerformanceEntries(entries))); 7907 } 7908 7909 /** 7910 * Clear _context 7911 */ 7912 _clearContext() { 7913 // XXX: `initialTimestamp` and `initialUrl` do not get cleared 7914 this._context.errorIds.clear(); 7915 this._context.traceIds.clear(); 7916 this._context.urls = []; 7917 } 7918 7919 /** Update the initial timestamp based on the buffer content. */ 7920 _updateInitialTimestampFromEventBuffer() { 7921 const { session, eventBuffer } = this; 7922 if (!session || !eventBuffer) { 7923 return; 7924 } 7925 7926 // we only ever update this on the initial segment 7927 if (session.segmentId) { 7928 return; 7929 } 7930 7931 const earliestEvent = eventBuffer.getEarliestTimestamp(); 7932 if (earliestEvent && earliestEvent < this._context.initialTimestamp) { 7933 this._context.initialTimestamp = earliestEvent; 7934 } 7935 } 7936 7937 /** 7938 * Return and clear _context 7939 */ 7940 _popEventContext() { 7941 const _context = { 7942 initialTimestamp: this._context.initialTimestamp, 7943 initialUrl: this._context.initialUrl, 7944 errorIds: Array.from(this._context.errorIds), 7945 traceIds: Array.from(this._context.traceIds), 7946 urls: this._context.urls, 7947 }; 7948 7949 this._clearContext(); 7950 7951 return _context; 7952 } 7953 7954 /** 7955 * Flushes replay event buffer to Sentry. 7956 * 7957 * Performance events are only added right before flushing - this is 7958 * due to the buffered performance observer events. 7959 * 7960 * Should never be called directly, only by `flush` 7961 */ 7962 async _runFlush() { 7963 if (!this.session || !this.eventBuffer) { 7964 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] No session or eventBuffer found to flush.'); 7965 return; 7966 } 7967 7968 await this._addPerformanceEntries(); 7969 7970 // Check eventBuffer again, as it could have been stopped in the meanwhile 7971 if (!this.eventBuffer || !this.eventBuffer.hasEvents) { 7972 return; 7973 } 7974 7975 // Only attach memory event if eventBuffer is not empty 7976 await addMemoryEntry(this); 7977 7978 // Check eventBuffer again, as it could have been stopped in the meanwhile 7979 if (!this.eventBuffer) { 7980 return; 7981 } 7982 7983 try { 7984 // This uses the data from the eventBuffer, so we need to call this before `finish() 7985 this._updateInitialTimestampFromEventBuffer(); 7986 7987 // Note this empties the event buffer regardless of outcome of sending replay 7988 const recordingData = await this.eventBuffer.finish(); 7989 7990 // NOTE: Copy values from instance members, as it's possible they could 7991 // change before the flush finishes. 7992 const replayId = this.session.id; 7993 const eventContext = this._popEventContext(); 7994 // Always increment segmentId regardless of outcome of sending replay 7995 const segmentId = this.session.segmentId++; 7996 this._maybeSaveSession(); 7997 7998 await sendReplay({ 7999 replayId, 8000 recordingData, 8001 segmentId, 8002 eventContext, 8003 session: this.session, 8004 options: this.getOptions(), 8005 timestamp: Date.now(), 8006 }); 8007 } catch (err) { 8008 this._handleException(err); 8009 8010 // This means we retried 3 times and all of them failed, 8011 // or we ran into a problem we don't want to retry, like rate limiting. 8012 // In this case, we want to completely stop the replay - otherwise, we may get inconsistent segments 8013 void this.stop('sendReplay'); 8014 8015 const client = getCurrentHub().getClient(); 8016 8017 if (client) { 8018 client.recordDroppedEvent('send_error', 'replay'); 8019 } 8020 } 8021 } 8022 8023 /** 8024 * Flush recording data to Sentry. Creates a lock so that only a single flush 8025 * can be active at a time. Do not call this directly. 8026 */ 8027 __init17() {this._flush = async ({ 8028 force = false, 8029 } 8030 8031 = {}) => { 8032 if (!this._isEnabled && !force) { 8033 // This can happen if e.g. the replay was stopped because of exceeding the retry limit 8034 return; 8035 } 8036 8037 if (!this.checkAndHandleExpiredSession()) { 8038 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] Attempting to finish replay event after session expired.'); 8039 return; 8040 } 8041 8042 if (!this.session) { 8043 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] No session found to flush.'); 8044 return; 8045 } 8046 8047 // A flush is about to happen, cancel any queued flushes 8048 this._debouncedFlush.cancel(); 8049 8050 // this._flushLock acts as a lock so that future calls to `_flush()` 8051 // will be blocked until this promise resolves 8052 if (!this._flushLock) { 8053 this._flushLock = this._runFlush(); 8054 await this._flushLock; 8055 this._flushLock = null; 8056 return; 8057 } 8058 8059 // Wait for previous flush to finish, then call the debounced `_flush()`. 8060 // It's possible there are other flush requests queued and waiting for it 8061 // to resolve. We want to reduce all outstanding requests (as well as any 8062 // new flush requests that occur within a second of the locked flush 8063 // completing) into a single flush. 8064 8065 try { 8066 await this._flushLock; 8067 } catch (err) { 8068 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(err); 8069 } finally { 8070 this._debouncedFlush(); 8071 } 8072 };} 8073 8074 /** Save the session, if it is sticky */ 8075 _maybeSaveSession() { 8076 if (this.session && this._options.stickySession) { 8077 saveSession(this.session); 8078 } 8079 } 8080 8081 /** Handler for rrweb.record.onMutation */ 8082 __init18() {this._onMutationHandler = (mutations) => { 8083 const count = mutations.length; 8084 8085 const mutationLimit = this._options.mutationLimit; 8086 const mutationBreadcrumbLimit = this._options.mutationBreadcrumbLimit; 8087 const overMutationLimit = mutationLimit && count > mutationLimit; 8088 8089 // Create a breadcrumb if a lot of mutations happen at the same time 8090 // We can show this in the UI as an information with potential performance improvements 8091 if (count > mutationBreadcrumbLimit || overMutationLimit) { 8092 const breadcrumb = createBreadcrumb({ 8093 category: 'replay.mutations', 8094 data: { 8095 count, 8096 limit: overMutationLimit, 8097 }, 8098 }); 8099 this._createCustomBreadcrumb(breadcrumb); 8100 } 8101 8102 // Stop replay if over the mutation limit 8103 if (overMutationLimit) { 8104 void this.stop('mutationLimit'); 8105 return false; 8106 } 8107 8108 // `true` means we use the regular mutation handling by rrweb 8109 return true; 8110 };} 8111 } 8112 8113 function getOption( 8114 selectors, 8115 defaultSelectors, 8116 deprecatedClassOption, 8117 deprecatedSelectorOption, 8118 ) { 8119 const deprecatedSelectors = typeof deprecatedSelectorOption === 'string' ? deprecatedSelectorOption.split(',') : []; 8120 8121 const allSelectors = [ 8122 ...selectors, 8123 // @deprecated 8124 ...deprecatedSelectors, 8125 8126 // sentry defaults 8127 ...defaultSelectors, 8128 ]; 8129 8130 // @deprecated 8131 if (typeof deprecatedClassOption !== 'undefined') { 8132 // NOTE: No support for RegExp 8133 if (typeof deprecatedClassOption === 'string') { 8134 allSelectors.push(`.${deprecatedClassOption}`); 8135 } 8136 8137 // eslint-disable-next-line no-console 8138 console.warn( 8139 '[Replay] You are using a deprecated configuration item for privacy. Read the documentation on how to use the new privacy configuration.', 8140 ); 8141 } 8142 8143 return allSelectors.join(','); 8144 } 8145 8146 /** 8147 * Returns privacy related configuration for use in rrweb 8148 */ 8149 function getPrivacyOptions({ 8150 mask, 8151 unmask, 8152 block, 8153 unblock, 8154 ignore, 8155 8156 // eslint-disable-next-line deprecation/deprecation 8157 blockClass, 8158 // eslint-disable-next-line deprecation/deprecation 8159 blockSelector, 8160 // eslint-disable-next-line deprecation/deprecation 8161 maskTextClass, 8162 // eslint-disable-next-line deprecation/deprecation 8163 maskTextSelector, 8164 // eslint-disable-next-line deprecation/deprecation 8165 ignoreClass, 8166 }) { 8167 const defaultBlockedElements = ['base[href="/"]']; 8168 8169 const maskSelector = getOption(mask, ['.sentry-mask', '[data-sentry-mask]'], maskTextClass, maskTextSelector); 8170 const unmaskSelector = getOption(unmask, ['.sentry-unmask', '[data-sentry-unmask]']); 8171 8172 const options = { 8173 // We are making the decision to make text and input selectors the same 8174 maskTextSelector: maskSelector, 8175 unmaskTextSelector: unmaskSelector, 8176 maskInputSelector: maskSelector, 8177 unmaskInputSelector: unmaskSelector, 8178 8179 blockSelector: getOption( 8180 block, 8181 ['.sentry-block', '[data-sentry-block]', ...defaultBlockedElements], 8182 blockClass, 8183 blockSelector, 8184 ), 8185 unblockSelector: getOption(unblock, ['.sentry-unblock', '[data-sentry-unblock]']), 8186 ignoreSelector: getOption(ignore, ['.sentry-ignore', '[data-sentry-ignore]', 'input[type="file"]'], ignoreClass), 8187 }; 8188 8189 if (blockClass instanceof RegExp) { 8190 options.blockClass = blockClass; 8191 } 8192 8193 if (maskTextClass instanceof RegExp) { 8194 options.maskTextClass = maskTextClass; 8195 } 8196 8197 return options; 8198 } 8199 8200 /** 8201 * Returns true if we are in the browser. 8202 */ 8203 function isBrowser() { 8204 // eslint-disable-next-line no-restricted-globals 8205 return typeof window !== 'undefined' && (!isNodeEnv() || isElectronNodeRenderer()); 8206 } 8207 8208 // Electron renderers with nodeIntegration enabled are detected as Node.js so we specifically test for them 8209 function isElectronNodeRenderer() { 8210 return typeof process !== 'undefined' && (process ).type === 'renderer'; 8211 } 8212 8213 const MEDIA_SELECTORS = 8214 'img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]'; 8215 8216 const DEFAULT_NETWORK_HEADERS = ['content-length', 'content-type', 'accept']; 8217 8218 let _initialized = false; 8219 8220 /** 8221 * The main replay integration class, to be passed to `init({ integrations: [] })`. 8222 */ 8223 class Replay { 8224 /** 8225 * @inheritDoc 8226 */ 8227 static __initStatic() {this.id = 'Replay';} 8228 8229 /** 8230 * @inheritDoc 8231 */ 8232 __init() {this.name = Replay.id;} 8233 8234 /** 8235 * Options to pass to `rrweb.record()` 8236 */ 8237 8238 /** 8239 * Initial options passed to the replay integration, merged with default values. 8240 * Note: `sessionSampleRate` and `errorSampleRate` are not required here, as they 8241 * can only be finally set when setupOnce() is called. 8242 * 8243 * @private 8244 */ 8245 8246 constructor({ 8247 flushMinDelay = DEFAULT_FLUSH_MIN_DELAY, 8248 flushMaxDelay = DEFAULT_FLUSH_MAX_DELAY, 8249 stickySession = true, 8250 useCompression = true, 8251 _experiments = {}, 8252 sessionSampleRate, 8253 errorSampleRate, 8254 maskAllText = true, 8255 maskAllInputs = true, 8256 blockAllMedia = true, 8257 8258 mutationBreadcrumbLimit = 750, 8259 mutationLimit = 10000, 8260 8261 slowClickTimeout = 7000, 8262 slowClickIgnoreSelectors = [], 8263 8264 networkDetailAllowUrls = [], 8265 networkCaptureBodies = true, 8266 networkRequestHeaders = [], 8267 networkResponseHeaders = [], 8268 8269 mask = [], 8270 unmask = [], 8271 block = [], 8272 unblock = [], 8273 ignore = [], 8274 maskFn, 8275 8276 beforeAddRecordingEvent, 8277 8278 // eslint-disable-next-line deprecation/deprecation 8279 blockClass, 8280 // eslint-disable-next-line deprecation/deprecation 8281 blockSelector, 8282 // eslint-disable-next-line deprecation/deprecation 8283 maskInputOptions, 8284 // eslint-disable-next-line deprecation/deprecation 8285 maskTextClass, 8286 // eslint-disable-next-line deprecation/deprecation 8287 maskTextSelector, 8288 // eslint-disable-next-line deprecation/deprecation 8289 ignoreClass, 8290 } = {}) {Replay.prototype.__init.call(this); 8291 this._recordingOptions = { 8292 maskAllInputs, 8293 maskAllText, 8294 maskInputOptions: { ...(maskInputOptions || {}), password: true }, 8295 maskTextFn: maskFn, 8296 maskInputFn: maskFn, 8297 8298 ...getPrivacyOptions({ 8299 mask, 8300 unmask, 8301 block, 8302 unblock, 8303 ignore, 8304 blockClass, 8305 blockSelector, 8306 maskTextClass, 8307 maskTextSelector, 8308 ignoreClass, 8309 }), 8310 8311 // Our defaults 8312 slimDOMOptions: 'all', 8313 inlineStylesheet: true, 8314 // Disable inline images as it will increase segment/replay size 8315 inlineImages: false, 8316 // collect fonts, but be aware that `sentry.io` needs to be an allowed 8317 // origin for playback 8318 collectFonts: true, 8319 }; 8320 8321 this._initialOptions = { 8322 flushMinDelay, 8323 flushMaxDelay, 8324 stickySession, 8325 sessionSampleRate, 8326 errorSampleRate, 8327 useCompression, 8328 blockAllMedia, 8329 maskAllInputs, 8330 maskAllText, 8331 mutationBreadcrumbLimit, 8332 mutationLimit, 8333 slowClickTimeout, 8334 slowClickIgnoreSelectors, 8335 networkDetailAllowUrls, 8336 networkCaptureBodies, 8337 networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders), 8338 networkResponseHeaders: _getMergedNetworkHeaders(networkResponseHeaders), 8339 beforeAddRecordingEvent, 8340 8341 _experiments, 8342 }; 8343 8344 if (typeof sessionSampleRate === 'number') { 8345 // eslint-disable-next-line 8346 console.warn( 8347 `[Replay] You are passing \`sessionSampleRate\` to the Replay integration. 8348 This option is deprecated and will be removed soon. 8349 Instead, configure \`replaysSessionSampleRate\` directly in the SDK init options, e.g.: 8350 Sentry.init({ replaysSessionSampleRate: ${sessionSampleRate} })`, 8351 ); 8352 8353 this._initialOptions.sessionSampleRate = sessionSampleRate; 8354 } 8355 8356 if (typeof errorSampleRate === 'number') { 8357 // eslint-disable-next-line 8358 console.warn( 8359 `[Replay] You are passing \`errorSampleRate\` to the Replay integration. 8360 This option is deprecated and will be removed soon. 8361 Instead, configure \`replaysOnErrorSampleRate\` directly in the SDK init options, e.g.: 8362 Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, 8363 ); 8364 8365 this._initialOptions.errorSampleRate = errorSampleRate; 8366 } 8367 8368 if (this._initialOptions.blockAllMedia) { 8369 // `blockAllMedia` is a more user friendly option to configure blocking 8370 // embedded media elements 8371 this._recordingOptions.blockSelector = !this._recordingOptions.blockSelector 8372 ? MEDIA_SELECTORS 8373 : `${this._recordingOptions.blockSelector},${MEDIA_SELECTORS}`; 8374 } 8375 8376 if (this._isInitialized && isBrowser()) { 8377 throw new Error('Multiple Sentry Session Replay instances are not supported'); 8378 } 8379 8380 this._isInitialized = true; 8381 } 8382 8383 /** If replay has already been initialized */ 8384 get _isInitialized() { 8385 return _initialized; 8386 } 8387 8388 /** Update _isInitialized */ 8389 set _isInitialized(value) { 8390 _initialized = value; 8391 } 8392 8393 /** 8394 * Setup and initialize replay container 8395 */ 8396 setupOnce() { 8397 if (!isBrowser()) { 8398 return; 8399 } 8400 8401 this._setup(); 8402 8403 // Once upon a time, we tried to create a transaction in `setupOnce` and it would 8404 // potentially create a transaction before some native SDK integrations have run 8405 // and applied their own global event processor. An example is: 8406 // https://github.com/getsentry/sentry-javascript/blob/b47ceafbdac7f8b99093ce6023726ad4687edc48/packages/browser/src/integrations/useragent.ts 8407 // 8408 // So we call `this._initialize()` in next event loop as a workaround to wait for other 8409 // global event processors to finish. This is no longer needed, but keeping it 8410 // here to avoid any future issues. 8411 setTimeout(() => this._initialize()); 8412 } 8413 8414 /** 8415 * Start a replay regardless of sampling rate. Calling this will always 8416 * create a new session. Will throw an error if replay is already in progress. 8417 * 8418 * Creates or loads a session, attaches listeners to varying events (DOM, 8419 * PerformanceObserver, Recording, Sentry SDK, etc) 8420 */ 8421 start() { 8422 if (!this._replay) { 8423 return; 8424 } 8425 8426 this._replay.start(); 8427 } 8428 8429 /** 8430 * Start replay buffering. Buffers until `flush()` is called or, if 8431 * `replaysOnErrorSampleRate` > 0, until an error occurs. 8432 */ 8433 startBuffering() { 8434 if (!this._replay) { 8435 return; 8436 } 8437 8438 this._replay.startBuffering(); 8439 } 8440 8441 /** 8442 * Currently, this needs to be manually called (e.g. for tests). Sentry SDK 8443 * does not support a teardown 8444 */ 8445 stop() { 8446 if (!this._replay) { 8447 return Promise.resolve(); 8448 } 8449 8450 return this._replay.stop(); 8451 } 8452 8453 /** 8454 * If not in "session" recording mode, flush event buffer which will create a new replay. 8455 * Unless `continueRecording` is false, the replay will continue to record and 8456 * behave as a "session"-based replay. 8457 * 8458 * Otherwise, queue up a flush. 8459 */ 8460 flush(options) { 8461 if (!this._replay || !this._replay.isEnabled()) { 8462 return Promise.resolve(); 8463 } 8464 8465 return this._replay.sendBufferedReplayOrFlush(options); 8466 } 8467 8468 /** 8469 * Get the current session ID. 8470 */ 8471 getReplayId() { 8472 if (!this._replay || !this._replay.isEnabled()) { 8473 return; 8474 } 8475 8476 return this._replay.getSessionId(); 8477 } 8478 /** 8479 * Initializes replay. 8480 */ 8481 _initialize() { 8482 if (!this._replay) { 8483 return; 8484 } 8485 8486 this._replay.initializeSampling(); 8487 } 8488 8489 /** Setup the integration. */ 8490 _setup() { 8491 // Client is not available in constructor, so we need to wait until setupOnce 8492 const finalOptions = loadReplayOptionsFromClient(this._initialOptions); 8493 8494 this._replay = new ReplayContainer({ 8495 options: finalOptions, 8496 recordingOptions: this._recordingOptions, 8497 }); 8498 } 8499 } Replay.__initStatic(); 8500 8501 /** Parse Replay-related options from SDK options */ 8502 function loadReplayOptionsFromClient(initialOptions) { 8503 const client = getCurrentHub().getClient(); 8504 const opt = client && (client.getOptions() ); 8505 8506 const finalOptions = { sessionSampleRate: 0, errorSampleRate: 0, ...dropUndefinedKeys(initialOptions) }; 8507 8508 if (!opt) { 8509 // eslint-disable-next-line no-console 8510 console.warn('SDK client is not available.'); 8511 return finalOptions; 8512 } 8513 8514 if ( 8515 initialOptions.sessionSampleRate == null && // TODO remove once deprecated rates are removed 8516 initialOptions.errorSampleRate == null && // TODO remove once deprecated rates are removed 8517 opt.replaysSessionSampleRate == null && 8518 opt.replaysOnErrorSampleRate == null 8519 ) { 8520 // eslint-disable-next-line no-console 8521 console.warn( 8522 'Replay is disabled because neither `replaysSessionSampleRate` nor `replaysOnErrorSampleRate` are set.', 8523 ); 8524 } 8525 8526 if (typeof opt.replaysSessionSampleRate === 'number') { 8527 finalOptions.sessionSampleRate = opt.replaysSessionSampleRate; 8528 } 8529 8530 if (typeof opt.replaysOnErrorSampleRate === 'number') { 8531 finalOptions.errorSampleRate = opt.replaysOnErrorSampleRate; 8532 } 8533 8534 return finalOptions; 8535 } 8536 8537 function _getMergedNetworkHeaders(headers) { 8538 return [...DEFAULT_NETWORK_HEADERS, ...headers.map(header => header.toLowerCase())]; 8539 } 8540 8541 export { Replay }; 8542 //# sourceMappingURL=index.js.map