From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../node_modules/@sentry/replay/esm/index.js | 8542 ++++++++++++++++++++ 1 file changed, 8542 insertions(+) create mode 100644 shared/logger/node_modules/@sentry/replay/esm/index.js (limited to 'shared/logger/node_modules/@sentry/replay') diff --git a/shared/logger/node_modules/@sentry/replay/esm/index.js b/shared/logger/node_modules/@sentry/replay/esm/index.js new file mode 100644 index 0000000..84a600d --- /dev/null +++ b/shared/logger/node_modules/@sentry/replay/esm/index.js @@ -0,0 +1,8542 @@ +import { getCurrentHub, addGlobalEventProcessor, prepareEvent, setContext, captureException } from '@sentry/core'; +import { GLOBAL_OBJ, normalize, fill, htmlTreeAsString, logger, uuid4, SENTRY_XHR_DATA_KEY, dropUndefinedKeys, stringMatchesSomePattern, addInstrumentationHandler, browserPerformanceTimeOrigin, createEnvelope, createEventEnvelopeHeaders, getSdkMetadataForEnvelopeHeader, isNodeEnv } from '@sentry/utils'; + +// exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser` +// prevents the browser package from being bundled in the CDN bundle, and avoids a +// circular dependency between the browser and replay packages should `@sentry/browser` import +// from `@sentry/replay` in the future +const WINDOW = GLOBAL_OBJ ; + +const REPLAY_SESSION_KEY = 'sentryReplaySession'; +const REPLAY_EVENT_NAME = 'replay_event'; +const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay'; + +// The idle limit for a session after which recording is paused. +const SESSION_IDLE_PAUSE_DURATION = 300000; // 5 minutes in ms + +// The idle limit for a session after which the session expires. +const SESSION_IDLE_EXPIRE_DURATION = 900000; // 15 minutes in ms + +// The maximum length of a session +const MAX_SESSION_LIFE = 3600000; // 60 minutes in ms + +/** Default flush delays */ +const DEFAULT_FLUSH_MIN_DELAY = 5000; +// XXX: Temp fix for our debounce logic where `maxWait` would never occur if it +// was the same as `wait` +const DEFAULT_FLUSH_MAX_DELAY = 5500; + +/* How long to wait for error checkouts */ +const BUFFER_CHECKOUT_TIME = 60000; + +const RETRY_BASE_INTERVAL = 5000; +const RETRY_MAX_COUNT = 3; + +/* The max (uncompressed) size in bytes of a network body. Any body larger than this will be truncated. */ +const NETWORK_BODY_MAX_SIZE = 150000; + +/* The max size of a single console arg that is captured. Any arg larger than this will be truncated. */ +const CONSOLE_ARG_MAX_SIZE = 5000; + +/* Min. time to wait before we consider something a slow click. */ +const SLOW_CLICK_THRESHOLD = 3000; +/* For scroll actions after a click, we only look for a very short time period to detect programmatic scrolling. */ +const SLOW_CLICK_SCROLL_TIMEOUT = 300; +/* Clicks in this time period are considered e.g. double/triple clicks. */ +const MULTI_CLICK_TIMEOUT = 1000; + +/** When encountering a total segment size exceeding this size, stop the replay (as we cannot properly ingest it). */ +const REPLAY_MAX_EVENT_BUFFER_SIZE = 20000000; // ~20MB + +var NodeType$1; +(function (NodeType) { + NodeType[NodeType["Document"] = 0] = "Document"; + NodeType[NodeType["DocumentType"] = 1] = "DocumentType"; + NodeType[NodeType["Element"] = 2] = "Element"; + NodeType[NodeType["Text"] = 3] = "Text"; + NodeType[NodeType["CDATA"] = 4] = "CDATA"; + NodeType[NodeType["Comment"] = 5] = "Comment"; +})(NodeType$1 || (NodeType$1 = {})); + +function isElement(n) { + return n.nodeType === n.ELEMENT_NODE; +} +function isShadowRoot(n) { + const host = n === null || n === void 0 ? void 0 : n.host; + return Boolean(host && host.shadowRoot && host.shadowRoot === n); +} +function isInputTypeMasked({ maskInputOptions, tagName, type, }) { + if (tagName.toLowerCase() === 'option') { + tagName = 'select'; + } + const actualType = typeof type === 'string' ? type.toLowerCase() : undefined; + return (maskInputOptions[tagName.toLowerCase()] || + (actualType && maskInputOptions[actualType]) || + actualType === 'password' || + (tagName === 'input' && !type && maskInputOptions['text'])); +} +function hasInputMaskOptions({ tagName, type, maskInputOptions, maskInputSelector, }) { + return (maskInputSelector || isInputTypeMasked({ maskInputOptions, tagName, type })); +} +function maskInputValue({ input, maskInputSelector, unmaskInputSelector, maskInputOptions, tagName, type, value, maskInputFn, }) { + let text = value || ''; + if (unmaskInputSelector && input.matches(unmaskInputSelector)) { + return text; + } + if (input.hasAttribute('data-rr-is-password')) { + type = 'password'; + } + if (isInputTypeMasked({ maskInputOptions, tagName, type }) || + (maskInputSelector && input.matches(maskInputSelector))) { + if (maskInputFn) { + text = maskInputFn(text); + } + else { + text = '*'.repeat(text.length); + } + } + return text; +} +const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; +function is2DCanvasBlank(canvas) { + const ctx = canvas.getContext('2d'); + if (!ctx) + return true; + const chunkSize = 50; + for (let x = 0; x < canvas.width; x += chunkSize) { + for (let y = 0; y < canvas.height; y += chunkSize) { + const getImageData = ctx.getImageData; + const originalGetImageData = ORIGINAL_ATTRIBUTE_NAME in getImageData + ? getImageData[ORIGINAL_ATTRIBUTE_NAME] + : getImageData; + const pixelBuffer = new Uint32Array(originalGetImageData.call(ctx, x, y, Math.min(chunkSize, canvas.width - x), Math.min(chunkSize, canvas.height - y)).data.buffer); + if (pixelBuffer.some((pixel) => pixel !== 0)) + return false; + } + } + return true; +} +function getInputType(element) { + const type = element.type; + return element.hasAttribute('data-rr-is-password') + ? 'password' + : type + ? type.toLowerCase() + : null; +} +function getInputValue(el, tagName, type) { + typeof type === 'string' ? type.toLowerCase() : ''; + if (tagName === 'INPUT' && (type === 'radio' || type === 'checkbox')) { + return el.getAttribute('value') || ''; + } + return el.value; +} + +let _id = 1; +const tagNameRegex = new RegExp('[^a-z0-9-_:]'); +const IGNORED_NODE = -2; +function defaultMaskFn(str) { + return str ? str.replace(/[\S]/g, '*') : ''; +} +function genId() { + return _id++; +} +function getValidTagName(element) { + if (element instanceof HTMLFormElement) { + return 'form'; + } + const processedTagName = element.tagName.toLowerCase().trim(); + if (tagNameRegex.test(processedTagName)) { + return 'div'; + } + return processedTagName; +} +function getCssRulesString(s) { + try { + const rules = s.rules || s.cssRules; + return rules ? Array.from(rules).map(getCssRuleString).join('') : null; + } + catch (error) { + return null; + } +} +function getCssRuleString(rule) { + let cssStringified = rule.cssText; + if (isCSSImportRule(rule)) { + try { + cssStringified = getCssRulesString(rule.styleSheet) || cssStringified; + } + catch (_a) { + } + } + return validateStringifiedCssRule(cssStringified); +} +function validateStringifiedCssRule(cssStringified) { + if (cssStringified.indexOf(':') > -1) { + const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm; + return cssStringified.replace(regex, '$1\\$2'); + } + return cssStringified; +} +function isCSSImportRule(rule) { + return 'styleSheet' in rule; +} +function stringifyStyleSheet(sheet) { + return sheet.cssRules + ? Array.from(sheet.cssRules) + .map((rule) => rule.cssText ? validateStringifiedCssRule(rule.cssText) : '') + .join('') + : ''; +} +function extractOrigin(url) { + let origin = ''; + if (url.indexOf('//') > -1) { + origin = url.split('/').slice(0, 3).join('/'); + } + else { + origin = url.split('/')[0]; + } + origin = origin.split('?')[0]; + return origin; +} +let canvasService; +let canvasCtx; +const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; +const RELATIVE_PATH = /^(?!www\.|(?:http|ftp)s?:\/\/|[A-Za-z]:\\|\/\/|#).*/; +const DATA_URI = /^(data:)([^,]*),(.*)/i; +function absoluteToStylesheet(cssText, href) { + return (cssText || '').replace(URL_IN_CSS_REF, (origin, quote1, path1, quote2, path2, path3) => { + const filePath = path1 || path2 || path3; + const maybeQuote = quote1 || quote2 || ''; + if (!filePath) { + return origin; + } + if (!RELATIVE_PATH.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (DATA_URI.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (filePath[0] === '/') { + return `url(${maybeQuote}${extractOrigin(href) + filePath}${maybeQuote})`; + } + const stack = href.split('/'); + const parts = filePath.split('/'); + stack.pop(); + for (const part of parts) { + if (part === '.') { + continue; + } + else if (part === '..') { + stack.pop(); + } + else { + stack.push(part); + } + } + return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; + }); +} +const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; +const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/; +function getAbsoluteSrcsetString(doc, attributeValue) { + if (attributeValue.trim() === '') { + return attributeValue; + } + let pos = 0; + function collectCharacters(regEx) { + let chars; + let match = regEx.exec(attributeValue.substring(pos)); + if (match) { + chars = match[0]; + pos += chars.length; + return chars; + } + return ''; + } + let output = []; + while (true) { + collectCharacters(SRCSET_COMMAS_OR_SPACES); + if (pos >= attributeValue.length) { + break; + } + let url = collectCharacters(SRCSET_NOT_SPACES); + if (url.slice(-1) === ',') { + url = absoluteToDoc(doc, url.substring(0, url.length - 1)); + output.push(url); + } + else { + let descriptorsStr = ''; + url = absoluteToDoc(doc, url); + let inParens = false; + while (true) { + let c = attributeValue.charAt(pos); + if (c === '') { + output.push((url + descriptorsStr).trim()); + break; + } + else if (!inParens) { + if (c === ',') { + pos += 1; + output.push((url + descriptorsStr).trim()); + break; + } + else if (c === '(') { + inParens = true; + } + } + else { + if (c === ')') { + inParens = false; + } + } + descriptorsStr += c; + pos += 1; + } + } + } + return output.join(', '); +} +function absoluteToDoc(doc, attributeValue) { + if (!attributeValue || attributeValue.trim() === '') { + return attributeValue; + } + const a = doc.createElement('a'); + a.href = attributeValue; + return a.href; +} +function isSVGElement(el) { + return Boolean(el.tagName === 'svg' || el.ownerSVGElement); +} +function getHref() { + const a = document.createElement('a'); + a.href = ''; + return a.href; +} +function transformAttribute(doc, element, _tagName, _name, value, maskAllText, unmaskTextSelector, maskTextFn) { + if (!value) { + return value; + } + const name = _name.toLowerCase(); + const tagName = _tagName.toLowerCase(); + if (name === 'src' || name === 'href') { + return absoluteToDoc(doc, value); + } + else if (name === 'xlink:href' && value[0] !== '#') { + return absoluteToDoc(doc, value); + } + else if (name === 'background' && + (tagName === 'table' || tagName === 'td' || tagName === 'th')) { + return absoluteToDoc(doc, value); + } + else if (name === 'srcset') { + return getAbsoluteSrcsetString(doc, value); + } + else if (name === 'style') { + return absoluteToStylesheet(value, getHref()); + } + else if (tagName === 'object' && name === 'data') { + return absoluteToDoc(doc, value); + } + else if (maskAllText && + _shouldMaskAttribute(element, name, tagName, unmaskTextSelector)) { + return maskTextFn ? maskTextFn(value) : defaultMaskFn(value); + } + return value; +} +function _shouldMaskAttribute(element, attribute, tagName, unmaskTextSelector) { + if (unmaskTextSelector && element.matches(unmaskTextSelector)) { + return false; + } + return (['placeholder', 'title', 'aria-label'].indexOf(attribute) > -1 || + (tagName === 'input' && + attribute === 'value' && + element.hasAttribute('type') && + ['submit', 'button'].indexOf(element.getAttribute('type').toLowerCase()) > -1)); +} +function _isBlockedElement(element, blockClass, blockSelector, unblockSelector) { + if (unblockSelector && element.matches(unblockSelector)) { + return false; + } + if (typeof blockClass === 'string') { + if (element.classList.contains(blockClass)) { + return true; + } + } + else { + for (let eIndex = 0; eIndex < element.classList.length; eIndex++) { + const className = element.classList[eIndex]; + if (blockClass.test(className)) { + return true; + } + } + } + if (blockSelector) { + return element.matches(blockSelector); + } + return false; +} +function needMaskingText(node, maskTextClass, maskTextSelector, unmaskTextSelector, maskAllText) { + if (!node) { + return false; + } + if (node.nodeType !== node.ELEMENT_NODE) { + return needMaskingText(node.parentNode, maskTextClass, maskTextSelector, unmaskTextSelector, maskAllText); + } + if (unmaskTextSelector) { + if (node.matches(unmaskTextSelector) || + node.closest(unmaskTextSelector)) { + return false; + } + } + if (maskAllText) { + return true; + } + if (typeof maskTextClass === 'string') { + if (node.classList.contains(maskTextClass)) { + return true; + } + } + else { + for (let eIndex = 0; eIndex < node.classList.length; eIndex++) { + const className = node.classList[eIndex]; + if (maskTextClass.test(className)) { + return true; + } + } + } + if (maskTextSelector) { + if (node.matches(maskTextSelector)) { + return true; + } + } + return needMaskingText(node.parentNode, maskTextClass, maskTextSelector, unmaskTextSelector, maskAllText); +} +function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) { + const win = iframeEl.contentWindow; + if (!win) { + return; + } + let fired = false; + let readyState; + try { + readyState = win.document.readyState; + } + catch (error) { + return; + } + if (readyState !== 'complete') { + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, iframeLoadTimeout); + iframeEl.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + return; + } + const blankUrl = 'about:blank'; + if (win.location.href !== blankUrl || + iframeEl.src === blankUrl || + iframeEl.src === '') { + setTimeout(listener, 0); + return; + } + iframeEl.addEventListener('load', listener); +} +function serializeNode(n, options) { + var _a; + const { doc, blockClass, blockSelector, unblockSelector, maskTextClass, maskTextSelector, unmaskTextSelector, inlineStylesheet, maskInputSelector, unmaskInputSelector, maskAllText, maskInputOptions = {}, maskTextFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, } = options; + let rootId; + if (doc.__sn) { + const docId = doc.__sn.id; + rootId = docId === 1 ? undefined : docId; + } + switch (n.nodeType) { + case n.DOCUMENT_NODE: + if (n.compatMode !== 'CSS1Compat') { + return { + type: NodeType$1.Document, + childNodes: [], + compatMode: n.compatMode, + rootId, + }; + } + else { + return { + type: NodeType$1.Document, + childNodes: [], + rootId, + }; + } + case n.DOCUMENT_TYPE_NODE: + return { + type: NodeType$1.DocumentType, + name: n.name, + publicId: n.publicId, + systemId: n.systemId, + rootId, + }; + case n.ELEMENT_NODE: + const needBlock = _isBlockedElement(n, blockClass, blockSelector, unblockSelector); + const tagName = getValidTagName(n); + let attributes = {}; + for (const { name, value } of Array.from(n.attributes)) { + if (!skipAttribute(tagName, name)) { + attributes[name] = transformAttribute(doc, n, tagName, name, value, maskAllText, unmaskTextSelector, maskTextFn); + } + } + if (tagName === 'link' && inlineStylesheet) { + const stylesheet = Array.from(doc.styleSheets).find((s) => { + return s.href === n.href; + }); + let cssText = null; + if (stylesheet) { + cssText = getCssRulesString(stylesheet); + } + if (cssText) { + delete attributes.rel; + delete attributes.href; + attributes._cssText = absoluteToStylesheet(cssText, stylesheet.href); + } + } + if (tagName === 'style' && + n.sheet && + !(n.innerText || + n.textContent || + '').trim().length) { + const cssText = getCssRulesString(n.sheet); + if (cssText) { + attributes._cssText = absoluteToStylesheet(cssText, getHref()); + } + } + if (tagName === 'input' || + tagName === 'textarea' || + tagName === 'select' || + tagName === 'option') { + const el = n; + const type = getInputType(el); + const value = getInputValue(el, tagName.toUpperCase(), type); + const checked = n.checked; + if (type !== 'submit' && + type !== 'button' && + value) { + attributes.value = maskInputValue({ + input: el, + type, + tagName, + value, + maskInputSelector, + unmaskInputSelector, + maskInputOptions, + maskInputFn, + }); + } + if (checked) { + attributes.checked = checked; + } + } + if (tagName === 'option') { + if (n.selected && !maskInputOptions['select']) { + attributes.selected = true; + } + else { + delete attributes.selected; + } + } + if (tagName === 'canvas' && recordCanvas) { + if (n.__context === '2d') { + if (!is2DCanvasBlank(n)) { + attributes.rr_dataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + } + else if (!('__context' in n)) { + const canvasDataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + const blankCanvas = document.createElement('canvas'); + blankCanvas.width = n.width; + blankCanvas.height = n.height; + const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality); + if (canvasDataURL !== blankCanvasDataURL) { + attributes.rr_dataURL = canvasDataURL; + } + } + } + if (tagName === 'img' && inlineImages) { + if (!canvasService) { + canvasService = doc.createElement('canvas'); + canvasCtx = canvasService.getContext('2d'); + } + const image = n; + const oldValue = image.crossOrigin; + image.crossOrigin = 'anonymous'; + const recordInlineImage = () => { + try { + canvasService.width = image.naturalWidth; + canvasService.height = image.naturalHeight; + canvasCtx.drawImage(image, 0, 0); + attributes.rr_dataURL = canvasService.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + catch (err) { + console.warn(`Cannot inline img src=${image.currentSrc}! Error: ${err}`); + } + oldValue + ? (attributes.crossOrigin = oldValue) + : delete attributes.crossOrigin; + }; + if (image.complete && image.naturalWidth !== 0) + recordInlineImage(); + else + image.onload = recordInlineImage; + } + if (tagName === 'audio' || tagName === 'video') { + attributes.rr_mediaState = n.paused + ? 'paused' + : 'played'; + attributes.rr_mediaCurrentTime = n.currentTime; + } + if (n.scrollLeft) { + attributes.rr_scrollLeft = n.scrollLeft; + } + if (n.scrollTop) { + attributes.rr_scrollTop = n.scrollTop; + } + if (needBlock) { + const { width, height } = n.getBoundingClientRect(); + attributes = { + class: attributes.class, + rr_width: `${width}px`, + rr_height: `${height}px`, + }; + } + if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src)) { + if (!n.contentDocument) { + attributes.rr_src = attributes.src; + } + delete attributes.src; + } + return { + type: NodeType$1.Element, + tagName, + attributes, + childNodes: [], + isSVG: isSVGElement(n) || undefined, + needBlock, + rootId, + }; + case n.TEXT_NODE: + const parentTagName = n.parentNode && n.parentNode.tagName; + let textContent = n.textContent; + const isStyle = parentTagName === 'STYLE' ? true : undefined; + const isScript = parentTagName === 'SCRIPT' ? true : undefined; + if (isStyle && textContent) { + try { + if (n.nextSibling || n.previousSibling) { + } + else if ((_a = n.parentNode.sheet) === null || _a === void 0 ? void 0 : _a.cssRules) { + textContent = stringifyStyleSheet(n.parentNode.sheet); + } + } + catch (err) { + console.warn(`Cannot get CSS styles from text's parentNode. Error: ${err}`, n); + } + textContent = absoluteToStylesheet(textContent, getHref()); + } + if (isScript) { + textContent = 'SCRIPT_PLACEHOLDER'; + } + if (parentTagName === 'TEXTAREA' && textContent) { + textContent = ''; + } + else if (parentTagName === 'OPTION' && textContent) { + const option = n.parentNode; + textContent = maskInputValue({ + input: option, + type: null, + tagName: parentTagName, + value: textContent, + maskInputSelector, + unmaskInputSelector, + maskInputOptions, + maskInputFn, + }); + } + else if (!isStyle && + !isScript && + needMaskingText(n, maskTextClass, maskTextSelector, unmaskTextSelector, maskAllText) && + textContent) { + textContent = maskTextFn + ? maskTextFn(textContent) + : defaultMaskFn(textContent); + } + return { + type: NodeType$1.Text, + textContent: textContent || '', + isStyle, + rootId, + }; + case n.CDATA_SECTION_NODE: + return { + type: NodeType$1.CDATA, + textContent: '', + rootId, + }; + case n.COMMENT_NODE: + return { + type: NodeType$1.Comment, + textContent: n.textContent || '', + rootId, + }; + default: + return false; + } +} +function lowerIfExists(maybeAttr) { + if (maybeAttr === undefined || maybeAttr === null) { + return ''; + } + else { + return maybeAttr.toLowerCase(); + } +} +function slimDOMExcluded(sn, slimDOMOptions) { + if (slimDOMOptions.comment && sn.type === NodeType$1.Comment) { + return true; + } + else if (sn.type === NodeType$1.Element) { + if (slimDOMOptions.script && + (sn.tagName === 'script' || + (sn.tagName === 'link' && + (sn.attributes.rel === 'preload' || + sn.attributes.rel === 'modulepreload') && + sn.attributes.as === 'script') || + (sn.tagName === 'link' && + sn.attributes.rel === 'prefetch' && + typeof sn.attributes.href === 'string' && + sn.attributes.href.endsWith('.js')))) { + return true; + } + else if (slimDOMOptions.headFavicon && + ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') || + (sn.tagName === 'meta' && + (lowerIfExists(sn.attributes.name).match(/^msapplication-tile(image|color)$/) || + lowerIfExists(sn.attributes.name) === 'application-name' || + lowerIfExists(sn.attributes.rel) === 'icon' || + lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' || + lowerIfExists(sn.attributes.rel) === 'shortcut icon')))) { + return true; + } + else if (sn.tagName === 'meta') { + if (slimDOMOptions.headMetaDescKeywords && + lowerIfExists(sn.attributes.name).match(/^description|keywords$/)) { + return true; + } + else if (slimDOMOptions.headMetaSocial && + (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) || + lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) || + lowerIfExists(sn.attributes.name) === 'pinterest')) { + return true; + } + else if (slimDOMOptions.headMetaRobots && + (lowerIfExists(sn.attributes.name) === 'robots' || + lowerIfExists(sn.attributes.name) === 'googlebot' || + lowerIfExists(sn.attributes.name) === 'bingbot')) { + return true; + } + else if (slimDOMOptions.headMetaHttpEquiv && + sn.attributes['http-equiv'] !== undefined) { + return true; + } + else if (slimDOMOptions.headMetaAuthorship && + (lowerIfExists(sn.attributes.name) === 'author' || + lowerIfExists(sn.attributes.name) === 'generator' || + lowerIfExists(sn.attributes.name) === 'framework' || + lowerIfExists(sn.attributes.name) === 'publisher' || + lowerIfExists(sn.attributes.name) === 'progid' || + lowerIfExists(sn.attributes.property).match(/^article:/) || + lowerIfExists(sn.attributes.property).match(/^product:/))) { + return true; + } + else if (slimDOMOptions.headMetaVerification && + (lowerIfExists(sn.attributes.name) === 'google-site-verification' || + lowerIfExists(sn.attributes.name) === 'yandex-verification' || + lowerIfExists(sn.attributes.name) === 'csrf-token' || + lowerIfExists(sn.attributes.name) === 'p:domain_verify' || + lowerIfExists(sn.attributes.name) === 'verify-v1' || + lowerIfExists(sn.attributes.name) === 'verification' || + lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token')) { + return true; + } + } + } + return false; +} +function serializeNodeWithId(n, options) { + 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; + let { preserveWhiteSpace = true } = options; + const _serializedNode = serializeNode(n, { + doc, + blockClass, + blockSelector, + unblockSelector, + maskTextClass, + maskTextSelector, + unmaskTextSelector, + inlineStylesheet, + maskInputSelector, + unmaskInputSelector, + maskAllText, + maskInputOptions, + maskTextFn, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + }); + if (!_serializedNode) { + console.warn(n, 'not serialized'); + return null; + } + let id; + if ('__sn' in n) { + id = n.__sn.id; + } + else if (slimDOMExcluded(_serializedNode, slimDOMOptions) || + (!preserveWhiteSpace && + _serializedNode.type === NodeType$1.Text && + !_serializedNode.isStyle && + !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)) { + id = IGNORED_NODE; + } + else { + id = genId(); + } + const serializedNode = Object.assign(_serializedNode, { id }); + n.__sn = serializedNode; + if (id === IGNORED_NODE) { + return null; + } + map[id] = n; + if (onSerialize) { + onSerialize(n); + } + let recordChild = !skipChild; + if (serializedNode.type === NodeType$1.Element) { + recordChild = recordChild && !serializedNode.needBlock; + delete serializedNode.needBlock; + if (n.shadowRoot) + serializedNode.isShadowHost = true; + } + if ((serializedNode.type === NodeType$1.Document || + serializedNode.type === NodeType$1.Element) && + recordChild) { + if (slimDOMOptions.headWhitespace && + _serializedNode.type === NodeType$1.Element && + _serializedNode.tagName === 'head') { + preserveWhiteSpace = false; + } + const bypassOptions = { + doc, + map, + blockClass, + blockSelector, + unblockSelector, + maskTextClass, + maskTextSelector, + unmaskTextSelector, + skipChild, + inlineStylesheet, + maskInputSelector, + unmaskInputSelector, + maskAllText, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + keepIframeSrcFn, + }; + for (const childN of Array.from(n.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + serializedNode.childNodes.push(serializedChildNode); + } + } + if (isElement(n) && n.shadowRoot) { + for (const childN of Array.from(n.shadowRoot.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + serializedChildNode.isShadow = true; + serializedNode.childNodes.push(serializedChildNode); + } + } + } + } + if (n.parentNode && isShadowRoot(n.parentNode)) { + serializedNode.isShadow = true; + } + if (serializedNode.type === NodeType$1.Element && + serializedNode.tagName === 'iframe') { + onceIframeLoaded(n, () => { + const iframeDoc = n.contentDocument; + if (iframeDoc && onIframeLoad) { + const serializedIframeNode = serializeNodeWithId(iframeDoc, { + doc: iframeDoc, + map, + blockClass, + blockSelector, + unblockSelector, + maskTextClass, + maskTextSelector, + unmaskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputSelector, + unmaskInputSelector, + maskAllText, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + keepIframeSrcFn, + }); + if (serializedIframeNode) { + onIframeLoad(n, serializedIframeNode); + } + } + }, iframeLoadTimeout); + } + return serializedNode; +} +function snapshot(n, options) { + 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 || {}; + const idNodeMap = {}; + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + } + : maskAllInputs === false + ? {} + : maskAllInputs; + const slimDOMOptions = slimDOM === true || slimDOM === 'all' + ? + { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaDescKeywords: slimDOM === 'all', + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaAuthorship: true, + headMetaVerification: true, + } + : slimDOM === false + ? {} + : slimDOM; + return [ + serializeNodeWithId(n, { + doc: n, + map: idNodeMap, + blockClass, + blockSelector, + unblockSelector, + maskTextClass, + maskTextSelector, + unmaskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputSelector, + unmaskInputSelector, + maskAllText, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + keepIframeSrcFn, + }), + idNodeMap, + ]; +} +function skipAttribute(tagName, attributeName, value) { + return ((tagName === 'video' || tagName === 'audio') && attributeName === 'autoplay'); +} + +var EventType; +(function (EventType) { + EventType[EventType["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType[EventType["Load"] = 1] = "Load"; + EventType[EventType["FullSnapshot"] = 2] = "FullSnapshot"; + EventType[EventType["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType[EventType["Meta"] = 4] = "Meta"; + EventType[EventType["Custom"] = 5] = "Custom"; + EventType[EventType["Plugin"] = 6] = "Plugin"; +})(EventType || (EventType = {})); +var IncrementalSource; +(function (IncrementalSource) { + IncrementalSource[IncrementalSource["Mutation"] = 0] = "Mutation"; + IncrementalSource[IncrementalSource["MouseMove"] = 1] = "MouseMove"; + IncrementalSource[IncrementalSource["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource[IncrementalSource["Scroll"] = 3] = "Scroll"; + IncrementalSource[IncrementalSource["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource[IncrementalSource["Input"] = 5] = "Input"; + IncrementalSource[IncrementalSource["TouchMove"] = 6] = "TouchMove"; + IncrementalSource[IncrementalSource["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource[IncrementalSource["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource[IncrementalSource["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource[IncrementalSource["Font"] = 10] = "Font"; + IncrementalSource[IncrementalSource["Log"] = 11] = "Log"; + IncrementalSource[IncrementalSource["Drag"] = 12] = "Drag"; + IncrementalSource[IncrementalSource["StyleDeclaration"] = 13] = "StyleDeclaration"; +})(IncrementalSource || (IncrementalSource = {})); +var MouseInteractions; +(function (MouseInteractions) { + MouseInteractions[MouseInteractions["MouseUp"] = 0] = "MouseUp"; + MouseInteractions[MouseInteractions["MouseDown"] = 1] = "MouseDown"; + MouseInteractions[MouseInteractions["Click"] = 2] = "Click"; + MouseInteractions[MouseInteractions["ContextMenu"] = 3] = "ContextMenu"; + MouseInteractions[MouseInteractions["DblClick"] = 4] = "DblClick"; + MouseInteractions[MouseInteractions["Focus"] = 5] = "Focus"; + MouseInteractions[MouseInteractions["Blur"] = 6] = "Blur"; + MouseInteractions[MouseInteractions["TouchStart"] = 7] = "TouchStart"; + MouseInteractions[MouseInteractions["TouchMove_Departed"] = 8] = "TouchMove_Departed"; + MouseInteractions[MouseInteractions["TouchEnd"] = 9] = "TouchEnd"; + MouseInteractions[MouseInteractions["TouchCancel"] = 10] = "TouchCancel"; +})(MouseInteractions || (MouseInteractions = {})); +var CanvasContext; +(function (CanvasContext) { + CanvasContext[CanvasContext["2D"] = 0] = "2D"; + CanvasContext[CanvasContext["WebGL"] = 1] = "WebGL"; + CanvasContext[CanvasContext["WebGL2"] = 2] = "WebGL2"; +})(CanvasContext || (CanvasContext = {})); +var MediaInteractions; +(function (MediaInteractions) { + MediaInteractions[MediaInteractions["Play"] = 0] = "Play"; + MediaInteractions[MediaInteractions["Pause"] = 1] = "Pause"; + MediaInteractions[MediaInteractions["Seeked"] = 2] = "Seeked"; + MediaInteractions[MediaInteractions["VolumeChange"] = 3] = "VolumeChange"; +})(MediaInteractions || (MediaInteractions = {})); +var ReplayerEvents; +(function (ReplayerEvents) { + ReplayerEvents["Start"] = "start"; + ReplayerEvents["Pause"] = "pause"; + ReplayerEvents["Resume"] = "resume"; + ReplayerEvents["Resize"] = "resize"; + ReplayerEvents["Finish"] = "finish"; + ReplayerEvents["FullsnapshotRebuilded"] = "fullsnapshot-rebuilded"; + ReplayerEvents["LoadStylesheetStart"] = "load-stylesheet-start"; + ReplayerEvents["LoadStylesheetEnd"] = "load-stylesheet-end"; + ReplayerEvents["SkipStart"] = "skip-start"; + ReplayerEvents["SkipEnd"] = "skip-end"; + ReplayerEvents["MouseInteraction"] = "mouse-interaction"; + ReplayerEvents["EventCast"] = "event-cast"; + ReplayerEvents["CustomEvent"] = "custom-event"; + ReplayerEvents["Flush"] = "flush"; + ReplayerEvents["StateChange"] = "state-change"; + ReplayerEvents["PlayBack"] = "play-back"; +})(ReplayerEvents || (ReplayerEvents = {})); + +function on(type, fn, target = document) { + const options = { capture: true, passive: true }; + target.addEventListener(type, fn, options); + return () => target.removeEventListener(type, fn, options); +} +function createMirror() { + return { + map: {}, + getId(n) { + if (!n || !n.__sn) { + return -1; + } + return n.__sn.id; + }, + getNode(id) { + return this.map[id] || null; + }, + removeNodeFromMap(n) { + const id = n.__sn && n.__sn.id; + delete this.map[id]; + if (n.childNodes) { + n.childNodes.forEach((child) => this.removeNodeFromMap(child)); + } + }, + has(id) { + return this.map.hasOwnProperty(id); + }, + reset() { + this.map = {}; + }, + }; +} +const DEPARTED_MIRROR_ACCESS_WARNING = 'Please stop import mirror directly. Instead of that,' + + '\r\n' + + 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' + + '\r\n' + + 'or you can use record.mirror to access the mirror instance during recording.'; +let _mirror = { + map: {}, + getId() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return -1; + }, + getNode() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return null; + }, + removeNodeFromMap() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, + has() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return false; + }, + reset() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, +}; +if (typeof window !== 'undefined' && window.Proxy && window.Reflect) { + _mirror = new Proxy(_mirror, { + get(target, prop, receiver) { + if (prop === 'map') { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + } + return Reflect.get(target, prop, receiver); + }, + }); +} +function throttle$1(func, wait, options = {}) { + let timeout = null; + let previous = 0; + return function (arg) { + let now = Date.now(); + if (!previous && options.leading === false) { + previous = now; + } + let remaining = wait - (now - previous); + let context = this; + let args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + func.apply(context, args); + } + else if (!timeout && options.trailing !== false) { + timeout = setTimeout(() => { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + func.apply(context, args); + }, remaining); + } + }; +} +function hookSetter(target, key, d, isRevoked, win = window) { + const original = win.Object.getOwnPropertyDescriptor(target, key); + win.Object.defineProperty(target, key, isRevoked + ? d + : { + set(value) { + setTimeout(() => { + d.set.call(this, value); + }, 0); + if (original && original.set) { + original.set.call(this, value); + } + }, + }); + return () => hookSetter(target, key, original || {}, true); +} +function patch(source, name, replacement) { + try { + if (!(name in source)) { + return () => { }; + } + const original = source[name]; + const wrapped = replacement(original); + if (typeof wrapped === 'function') { + wrapped.prototype = wrapped.prototype || {}; + Object.defineProperties(wrapped, { + __rrweb_original__: { + enumerable: false, + value: original, + }, + }); + } + source[name] = wrapped; + return () => { + source[name] = original; + }; + } + catch (_a) { + return () => { }; + } +} +function getWindowHeight() { + return (window.innerHeight || + (document.documentElement && document.documentElement.clientHeight) || + (document.body && document.body.clientHeight)); +} +function getWindowWidth() { + return (window.innerWidth || + (document.documentElement && document.documentElement.clientWidth) || + (document.body && document.body.clientWidth)); +} +function isBlocked(node, blockClass, blockSelector, unblockSelector) { + if (!node) { + return false; + } + if (node.nodeType === node.ELEMENT_NODE) { + let needBlock = false; + const needUnblock = unblockSelector && node.matches(unblockSelector); + if (typeof blockClass === 'string') { + if (node.closest !== undefined) { + needBlock = + !needUnblock && + node.closest('.' + blockClass) !== null; + } + else { + needBlock = + !needUnblock && node.classList.contains(blockClass); + } + } + else { + !needUnblock && + node.classList.forEach((className) => { + if (blockClass.test(className)) { + needBlock = true; + } + }); + } + if (!needBlock && blockSelector) { + needBlock = node.matches(blockSelector); + } + return ((!needUnblock && needBlock) || + isBlocked(node.parentNode, blockClass, blockSelector, unblockSelector)); + } + if (node.nodeType === node.TEXT_NODE) { + return isBlocked(node.parentNode, blockClass, blockSelector, unblockSelector); + } + return isBlocked(node.parentNode, blockClass, blockSelector, unblockSelector); +} +function isIgnored(n) { + if ('__sn' in n) { + return n.__sn.id === IGNORED_NODE; + } + return false; +} +function isAncestorRemoved(target, mirror) { + if (isShadowRoot(target)) { + return false; + } + const id = mirror.getId(target); + if (!mirror.has(id)) { + return true; + } + if (target.parentNode && + target.parentNode.nodeType === target.DOCUMENT_NODE) { + return false; + } + if (!target.parentNode) { + return true; + } + return isAncestorRemoved(target.parentNode, mirror); +} +function isTouchEvent(event) { + return Boolean(event.changedTouches); +} +function polyfill(win = window) { + if ('NodeList' in win && !win.NodeList.prototype.forEach) { + win.NodeList.prototype.forEach = Array.prototype + .forEach; + } + if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) { + win.DOMTokenList.prototype.forEach = Array.prototype + .forEach; + } + if (!Node.prototype.contains) { + Node.prototype.contains = function contains(node) { + if (!(0 in arguments)) { + throw new TypeError('1 argument is required'); + } + do { + if (this === node) { + return true; + } + } while ((node = node && node.parentNode)); + return false; + }; + } +} +function isIframeINode(node) { + if ('__sn' in node) { + return (node.__sn.type === NodeType$1.Element && node.__sn.tagName === 'iframe'); + } + return false; +} +function hasShadowRoot(n) { + return Boolean(n === null || n === void 0 ? void 0 : n.shadowRoot); +} + +function isNodeInLinkedList(n) { + return '__ln' in n; +} +class DoubleLinkedList { + constructor() { + this.length = 0; + this.head = null; + } + get(position) { + if (position >= this.length) { + throw new Error('Position outside of list range'); + } + let current = this.head; + for (let index = 0; index < position; index++) { + current = (current === null || current === void 0 ? void 0 : current.next) || null; + } + return current; + } + addNode(n) { + const node = { + value: n, + previous: null, + next: null, + }; + n.__ln = node; + if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) { + const current = n.previousSibling.__ln.next; + node.next = current; + node.previous = n.previousSibling.__ln; + n.previousSibling.__ln.next = node; + if (current) { + current.previous = node; + } + } + else if (n.nextSibling && + isNodeInLinkedList(n.nextSibling) && + n.nextSibling.__ln.previous) { + const current = n.nextSibling.__ln.previous; + node.previous = current; + node.next = n.nextSibling.__ln; + n.nextSibling.__ln.previous = node; + if (current) { + current.next = node; + } + } + else { + if (this.head) { + this.head.previous = node; + } + node.next = this.head; + this.head = node; + } + this.length++; + } + removeNode(n) { + const current = n.__ln; + if (!this.head) { + return; + } + if (!current.previous) { + this.head = current.next; + if (this.head) { + this.head.previous = null; + } + } + else { + current.previous.next = current.next; + if (current.next) { + current.next.previous = current.previous; + } + } + if (n.__ln) { + delete n.__ln; + } + this.length--; + } +} +const moveKey = (id, parentId) => `${id}@${parentId}`; +function isINode(n) { + return '__sn' in n; +} +class MutationBuffer { + constructor() { + this.frozen = false; + this.locked = false; + this.texts = []; + this.attributes = []; + this.removes = []; + this.mapRemoves = []; + this.movedMap = {}; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.processMutations = (mutations) => { + mutations.forEach(this.processMutation); + this.emit(); + }; + this.emit = () => { + if (this.frozen || this.locked) { + return; + } + const adds = []; + const addList = new DoubleLinkedList(); + const getNextId = (n) => { + let ns = n; + let nextId = IGNORED_NODE; + while (nextId === IGNORED_NODE) { + ns = ns && ns.nextSibling; + nextId = ns && this.mirror.getId(ns); + } + return nextId; + }; + const pushAdd = (n) => { + var _a, _b, _c, _d, _e; + const shadowHost = n.getRootNode + ? (_a = n.getRootNode()) === null || _a === void 0 ? void 0 : _a.host + : null; + let rootShadowHost = shadowHost; + 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) + rootShadowHost = + ((_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) || + null; + const notInDoc = !this.doc.contains(n) && + (!rootShadowHost || !this.doc.contains(rootShadowHost)); + if (!n.parentNode || notInDoc) { + return; + } + const parentId = isShadowRoot(n.parentNode) + ? this.mirror.getId(shadowHost) + : this.mirror.getId(n.parentNode); + const nextId = getNextId(n); + if (parentId === -1 || nextId === -1) { + return addList.addNode(n); + } + let sn = serializeNodeWithId(n, { + doc: this.doc, + map: this.mirror.map, + blockClass: this.blockClass, + blockSelector: this.blockSelector, + unblockSelector: this.unblockSelector, + maskTextClass: this.maskTextClass, + maskTextSelector: this.maskTextSelector, + unmaskTextSelector: this.unmaskTextSelector, + maskInputSelector: this.maskInputSelector, + unmaskInputSelector: this.unmaskInputSelector, + skipChild: true, + inlineStylesheet: this.inlineStylesheet, + maskAllText: this.maskAllText, + maskInputOptions: this.maskInputOptions, + maskTextFn: this.maskTextFn, + maskInputFn: this.maskInputFn, + slimDOMOptions: this.slimDOMOptions, + recordCanvas: this.recordCanvas, + inlineImages: this.inlineImages, + onSerialize: (currentN) => { + if (isIframeINode(currentN)) { + this.iframeManager.addIframe(currentN); + } + if (hasShadowRoot(n)) { + this.shadowDomManager.addShadowRoot(n.shadowRoot, document); + } + }, + onIframeLoad: (iframe, childSn) => { + this.iframeManager.attachIframe(iframe, childSn); + this.shadowDomManager.observeAttachShadow(iframe); + }, + }); + if (sn) { + adds.push({ + parentId, + nextId, + node: sn, + }); + } + }; + while (this.mapRemoves.length) { + this.mirror.removeNodeFromMap(this.mapRemoves.shift()); + } + for (const n of this.movedSet) { + if (isParentRemoved(this.removes, n, this.mirror) && + !this.movedSet.has(n.parentNode)) { + continue; + } + pushAdd(n); + } + for (const n of this.addedSet) { + if (!isAncestorInSet(this.droppedSet, n) && + !isParentRemoved(this.removes, n, this.mirror)) { + pushAdd(n); + } + else if (isAncestorInSet(this.movedSet, n)) { + pushAdd(n); + } + else { + this.droppedSet.add(n); + } + } + let candidate = null; + while (addList.length) { + let node = null; + if (candidate) { + const parentId = this.mirror.getId(candidate.value.parentNode); + const nextId = getNextId(candidate.value); + if (parentId !== -1 && nextId !== -1) { + node = candidate; + } + } + if (!node) { + for (let index = addList.length - 1; index >= 0; index--) { + const _node = addList.get(index); + if (_node) { + const parentId = this.mirror.getId(_node.value.parentNode); + const nextId = getNextId(_node.value); + if (parentId !== -1 && nextId !== -1) { + node = _node; + break; + } + } + } + } + if (!node) { + while (addList.head) { + addList.removeNode(addList.head.value); + } + break; + } + candidate = node.previous; + addList.removeNode(node.value); + pushAdd(node.value); + } + const payload = { + texts: this.texts + .map((text) => ({ + id: this.mirror.getId(text.node), + value: text.value, + })) + .filter((text) => this.mirror.has(text.id)), + attributes: this.attributes + .map((attribute) => ({ + id: this.mirror.getId(attribute.node), + attributes: attribute.attributes, + })) + .filter((attribute) => this.mirror.has(attribute.id)), + removes: this.removes, + adds, + }; + if (!payload.texts.length && + !payload.attributes.length && + !payload.removes.length && + !payload.adds.length) { + return; + } + this.texts = []; + this.attributes = []; + this.removes = []; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.movedMap = {}; + this.mutationCb(payload); + }; + this.processMutation = (m) => { + if (isIgnored(m.target)) { + return; + } + switch (m.type) { + case 'characterData': { + const value = m.target.textContent; + if (!isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector) && value !== m.oldValue) { + this.texts.push({ + value: needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, this.unmaskTextSelector, this.maskAllText) && value + ? this.maskTextFn + ? this.maskTextFn(value) + : value.replace(/[\S]/g, '*') + : value, + node: m.target, + }); + } + break; + } + case 'attributes': { + const target = m.target; + let value = target.getAttribute(m.attributeName); + if (m.attributeName === 'value') { + value = maskInputValue({ + input: target, + maskInputSelector: this.maskInputSelector, + unmaskInputSelector: this.unmaskInputSelector, + maskInputOptions: this.maskInputOptions, + tagName: target.tagName, + type: target.getAttribute('type'), + value, + maskInputFn: this.maskInputFn, + }); + } + if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector) || value === m.oldValue) { + return; + } + let item = this.attributes.find((a) => a.node === m.target); + if (!item) { + item = { + node: m.target, + attributes: {}, + }; + this.attributes.push(item); + } + if (m.attributeName === 'type' && + target.tagName === 'INPUT' && + (m.oldValue || '').toLowerCase() === 'password') { + target.setAttribute('data-rr-is-password', 'true'); + } + if (m.attributeName === 'style') { + const old = this.doc.createElement('span'); + if (m.oldValue) { + old.setAttribute('style', m.oldValue); + } + if (item.attributes.style === undefined || + item.attributes.style === null) { + item.attributes.style = {}; + } + try { + const styleObj = item.attributes.style; + for (const pname of Array.from(target.style)) { + const newValue = target.style.getPropertyValue(pname); + const newPriority = target.style.getPropertyPriority(pname); + if (newValue !== old.style.getPropertyValue(pname) || + newPriority !== old.style.getPropertyPriority(pname)) { + if (newPriority === '') { + styleObj[pname] = newValue; + } + else { + styleObj[pname] = [newValue, newPriority]; + } + } + } + for (const pname of Array.from(old.style)) { + if (target.style.getPropertyValue(pname) === '') { + styleObj[pname] = false; + } + } + } + catch (error) { + console.warn('[rrweb] Error when parsing update to style attribute:', error); + } + } + else { + const element = m.target; + item.attributes[m.attributeName] = transformAttribute(this.doc, element, element.tagName, m.attributeName, value, this.maskAllText, this.unmaskTextSelector, this.maskTextFn); + } + break; + } + case 'childList': { + m.addedNodes.forEach((n) => this.genAdds(n, m.target)); + m.removedNodes.forEach((n) => { + const nodeId = this.mirror.getId(n); + const parentId = isShadowRoot(m.target) + ? this.mirror.getId(m.target.host) + : this.mirror.getId(m.target); + if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector) || isIgnored(n)) { + return; + } + if (this.addedSet.has(n)) { + deepDelete(this.addedSet, n); + this.droppedSet.add(n); + } + else if (this.addedSet.has(m.target) && nodeId === -1) ; + else if (isAncestorRemoved(m.target, this.mirror)) ; + else if (this.movedSet.has(n) && + this.movedMap[moveKey(nodeId, parentId)]) { + deepDelete(this.movedSet, n); + } + else { + this.removes.push({ + parentId, + id: nodeId, + isShadow: isShadowRoot(m.target) ? true : undefined, + }); + } + this.mapRemoves.push(n); + }); + break; + } + } + }; + this.genAdds = (n, target) => { + if (target && isBlocked(target, this.blockClass, this.blockSelector, this.unblockSelector)) { + return; + } + if (isINode(n)) { + if (isIgnored(n)) { + return; + } + this.movedSet.add(n); + let targetId = null; + if (target && isINode(target)) { + targetId = target.__sn.id; + } + if (targetId) { + this.movedMap[moveKey(n.__sn.id, targetId)] = true; + } + } + else { + this.addedSet.add(n); + this.droppedSet.delete(n); + } + if (!isBlocked(n, this.blockClass, this.blockSelector, this.unblockSelector)) + n.childNodes.forEach((childN) => this.genAdds(childN)); + }; + } + init(options) { + [ + 'mutationCb', + 'blockClass', + 'blockSelector', + 'unblockSelector', + 'maskTextClass', + 'maskTextSelector', + 'unmaskTextSelector', + 'maskInputSelector', + 'unmaskInputSelector', + 'inlineStylesheet', + 'maskAllText', + 'maskInputOptions', + 'maskTextFn', + 'maskInputFn', + 'recordCanvas', + 'inlineImages', + 'slimDOMOptions', + 'doc', + 'mirror', + 'iframeManager', + 'shadowDomManager', + 'canvasManager', + ].forEach((key) => { + this[key] = options[key]; + }); + } + freeze() { + this.frozen = true; + this.canvasManager.freeze(); + } + unfreeze() { + this.frozen = false; + this.canvasManager.unfreeze(); + this.emit(); + } + isFrozen() { + return this.frozen; + } + lock() { + this.locked = true; + this.canvasManager.lock(); + } + unlock() { + this.locked = false; + this.canvasManager.unlock(); + this.emit(); + } + reset() { + this.shadowDomManager.reset(); + this.canvasManager.reset(); + } +} +function deepDelete(addsSet, n) { + addsSet.delete(n); + n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); +} +function isParentRemoved(removes, n, mirror) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + const parentId = mirror.getId(parentNode); + if (removes.some((r) => r.id === parentId)) { + return true; + } + return isParentRemoved(removes, parentNode, mirror); +} +function isAncestorInSet(set, n) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + if (set.has(parentNode)) { + return true; + } + return isAncestorInSet(set, parentNode); +} + +const callbackWrapper = (cb) => { + const rrwebWrapped = (...rest) => { + try { + return cb(...rest); + } + catch (error) { + try { + error.__rrweb__ = true; + } + catch (_a) { + } + throw error; + } + }; + return rrwebWrapped; +}; + +const mutationBuffers = []; +function getEventTarget(event) { + try { + if ('composedPath' in event) { + const path = event.composedPath(); + if (path.length) { + return path[0]; + } + } + else if ('path' in event && event.path.length) { + return event.path[0]; + } + } + catch (_a) { } + return event && event.target; +} +function initMutationObserver(options, rootEl) { + var _a, _b; + const mutationBuffer = new MutationBuffer(); + mutationBuffers.push(mutationBuffer); + mutationBuffer.init(options); + let mutationObserverCtor = window.MutationObserver || + window.__rrMutationObserver; + 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'); + if (angularZoneSymbol && + window[angularZoneSymbol]) { + mutationObserverCtor = window[angularZoneSymbol]; + } + const observer = new mutationObserverCtor(callbackWrapper((mutations) => { + if (options.onMutation && options.onMutation(mutations) === false) { + return; + } + mutationBuffer.processMutations(mutations); + })); + observer.observe(rootEl, { + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }); + return observer; +} +function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) { + if (sampling.mousemove === false) { + return () => { }; + } + const threshold = typeof sampling.mousemove === 'number' ? sampling.mousemove : 50; + const callbackThreshold = typeof sampling.mousemoveCallback === 'number' + ? sampling.mousemoveCallback + : 500; + let positions = []; + let timeBaseline; + const wrappedCb = throttle$1((source) => { + const totalOffset = Date.now() - timeBaseline; + callbackWrapper(mousemoveCb)(positions.map((p) => { + p.timeOffset -= totalOffset; + return p; + }), source); + positions = []; + timeBaseline = null; + }, callbackThreshold); + const updatePosition = throttle$1((evt) => { + const target = getEventTarget(evt); + const { clientX, clientY } = isTouchEvent(evt) + ? evt.changedTouches[0] + : evt; + if (!timeBaseline) { + timeBaseline = Date.now(); + } + positions.push({ + x: clientX, + y: clientY, + id: mirror.getId(target), + timeOffset: Date.now() - timeBaseline, + }); + wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent + ? IncrementalSource.Drag + : evt instanceof MouseEvent + ? IncrementalSource.MouseMove + : IncrementalSource.TouchMove); + }, threshold, { + trailing: false, + }); + const handlers = [ + on('mousemove', callbackWrapper(updatePosition), doc), + on('touchmove', callbackWrapper(updatePosition), doc), + on('drag', callbackWrapper(updatePosition), doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initMouseInteractionObserver({ mouseInteractionCb, doc, mirror, blockClass, blockSelector, unblockSelector, sampling, }) { + if (sampling.mouseInteraction === false) { + return () => { }; + } + const disableMap = sampling.mouseInteraction === true || + sampling.mouseInteraction === undefined + ? {} + : sampling.mouseInteraction; + const handlers = []; + const getHandler = (eventKey) => { + return (event) => { + const target = getEventTarget(event); + if (isBlocked(target, blockClass, blockSelector, unblockSelector)) { + return; + } + const e = isTouchEvent(event) ? event.changedTouches[0] : event; + if (!e) { + return; + } + const id = mirror.getId(target); + const { clientX, clientY } = e; + callbackWrapper(mouseInteractionCb)({ + type: MouseInteractions[eventKey], + id, + x: clientX, + y: clientY, + }); + }; + }; + Object.keys(MouseInteractions) + .filter((key) => Number.isNaN(Number(key)) && + !key.endsWith('_Departed') && + disableMap[key] !== false) + .forEach((eventKey) => { + const eventName = eventKey.toLowerCase(); + const handler = callbackWrapper(getHandler(eventKey)); + handlers.push(on(eventName, handler, doc)); + }); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, unblockSelector, sampling, }) { + const updatePosition = throttle$1((evt) => { + const target = getEventTarget(evt); + if (!target || + isBlocked(target, blockClass, blockSelector, unblockSelector)) { + return; + } + const id = mirror.getId(target); + if (target === doc) { + const scrollEl = (doc.scrollingElement || doc.documentElement); + callbackWrapper(scrollCb)({ + id, + x: scrollEl.scrollLeft, + y: scrollEl.scrollTop, + }); + } + else { + callbackWrapper(scrollCb)({ + id, + x: target.scrollLeft, + y: target.scrollTop, + }); + } + }, sampling.scroll || 100); + return on('scroll', callbackWrapper(updatePosition), doc); +} +function initViewportResizeObserver({ viewportResizeCb, }) { + let lastH = -1; + let lastW = -1; + const updateDimension = throttle$1(() => { + const height = getWindowHeight(); + const width = getWindowWidth(); + if (lastH !== height || lastW !== width) { + callbackWrapper(viewportResizeCb)({ + width: Number(width), + height: Number(height), + }); + lastH = height; + lastW = width; + } + }, 200); + return on('resize', callbackWrapper(updateDimension), window); +} +function wrapEventWithUserTriggeredFlag(v, enable) { + const value = Object.assign({}, v); + if (!enable) + delete value.userTriggered; + return value; +} +const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; +const lastInputValueMap = new WeakMap(); +function initInputObserver({ inputCb, doc, mirror, blockClass, blockSelector, unblockSelector, ignoreClass, ignoreSelector, maskInputSelector, unmaskInputSelector, maskInputOptions, maskInputFn, sampling, userTriggeredOnInput, }) { + function eventHandler(event) { + let target = getEventTarget(event); + const tagName = target && target.tagName; + const userTriggered = event.isTrusted; + if (tagName === 'OPTION') + target = target.parentElement; + if (!target || + !tagName || + INPUT_TAGS.indexOf(tagName) < 0 || + isBlocked(target, blockClass, blockSelector, unblockSelector)) { + return; + } + const el = target; + const type = getInputType(el); + if (el.classList.contains(ignoreClass) || + (ignoreSelector && el.matches(ignoreSelector))) { + return; + } + let text = getInputValue(el, tagName, type); + let isChecked = false; + if (type === 'radio' || type === 'checkbox') { + isChecked = target.checked; + } + if (hasInputMaskOptions({ + maskInputOptions, + maskInputSelector, + tagName, + type, + })) { + text = maskInputValue({ + input: el, + maskInputOptions, + maskInputSelector, + unmaskInputSelector, + tagName, + type, + value: text, + maskInputFn, + }); + } + cbWithDedup(target, callbackWrapper(wrapEventWithUserTriggeredFlag)({ text, isChecked, userTriggered }, userTriggeredOnInput)); + const name = target.name; + if (type === 'radio' && name && isChecked) { + doc + .querySelectorAll(`input[type="radio"][name="${name}"]`) + .forEach((el) => { + if (el !== target) { + const text = maskInputValue({ + input: el, + maskInputOptions, + maskInputSelector, + unmaskInputSelector, + tagName, + type, + value: getInputValue(el, tagName, type), + maskInputFn, + }); + cbWithDedup(el, callbackWrapper(wrapEventWithUserTriggeredFlag)({ + text, + isChecked: !isChecked, + userTriggered: false, + }, userTriggeredOnInput)); + } + }); + } + } + function cbWithDedup(target, v) { + const lastInputValue = lastInputValueMap.get(target); + if (!lastInputValue || + lastInputValue.text !== v.text || + lastInputValue.isChecked !== v.isChecked) { + lastInputValueMap.set(target, v); + const id = mirror.getId(target); + inputCb(Object.assign(Object.assign({}, v), { id })); + } + } + const events = sampling.input === 'last' ? ['change'] : ['input', 'change']; + const handlers = events.map((eventName) => on(eventName, callbackWrapper(eventHandler), doc)); + const propertyDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + const hookProperties = [ + [HTMLInputElement.prototype, 'value'], + [HTMLInputElement.prototype, 'checked'], + [HTMLSelectElement.prototype, 'value'], + [HTMLTextAreaElement.prototype, 'value'], + [HTMLSelectElement.prototype, 'selectedIndex'], + [HTMLOptionElement.prototype, 'selected'], + ]; + if (propertyDescriptor && propertyDescriptor.set) { + handlers.push(...hookProperties.map((p) => hookSetter(p[0], p[1], { + set() { + callbackWrapper(eventHandler)({ target: this }); + }, + }))); + } + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function getNestedCSSRulePositions(rule) { + const positions = []; + function recurse(childRule, pos) { + if ((hasNestedCSSRule('CSSGroupingRule') && + childRule.parentRule instanceof CSSGroupingRule) || + (hasNestedCSSRule('CSSMediaRule') && + childRule.parentRule instanceof CSSMediaRule) || + (hasNestedCSSRule('CSSSupportsRule') && + childRule.parentRule instanceof CSSSupportsRule) || + (hasNestedCSSRule('CSSConditionRule') && + childRule.parentRule instanceof CSSConditionRule)) { + const rules = Array.from(childRule.parentRule.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + else { + const rules = Array.from(childRule.parentStyleSheet.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + return pos; + } + return recurse(rule, positions); +} +function initStyleSheetObserver({ styleSheetRuleCb, mirror }, { win }) { + if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) { + return () => { }; + } + const insertRule = win.CSSStyleSheet.prototype.insertRule; + win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const id = mirror.getId(thisArg.ownerNode); + if (id !== -1) { + styleSheetRuleCb({ + id, + adds: [{ rule, index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const deleteRule = win.CSSStyleSheet.prototype.deleteRule; + win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const id = mirror.getId(thisArg.ownerNode); + if (id !== -1) { + styleSheetRuleCb({ + id, + removes: [{ index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const supportedNestedCSSRuleTypes = {}; + if (canMonkeyPatchNestedCSSRule('CSSGroupingRule')) { + supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule; + } + else { + if (canMonkeyPatchNestedCSSRule('CSSMediaRule')) { + supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule; + } + if (canMonkeyPatchNestedCSSRule('CSSConditionRule')) { + supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule; + } + if (canMonkeyPatchNestedCSSRule('CSSSupportsRule')) { + supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule; + } + } + const unmodifiedFunctions = {}; + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + unmodifiedFunctions[typeKey] = { + insertRule: type.prototype.insertRule, + deleteRule: type.prototype.deleteRule, + }; + type.prototype.insertRule = new Proxy(unmodifiedFunctions[typeKey].insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const id = mirror.getId(thisArg.parentStyleSheet.ownerNode); + if (id !== -1) { + styleSheetRuleCb({ + id, + adds: [ + { + rule, + index: [ + ...getNestedCSSRulePositions(thisArg), + index || 0, + ], + }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + type.prototype.deleteRule = new Proxy(unmodifiedFunctions[typeKey].deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const id = mirror.getId(thisArg.parentStyleSheet.ownerNode); + if (id !== -1) { + styleSheetRuleCb({ + id, + removes: [ + { index: [...getNestedCSSRulePositions(thisArg), index] }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + }); + return callbackWrapper(() => { + win.CSSStyleSheet.prototype.insertRule = insertRule; + win.CSSStyleSheet.prototype.deleteRule = deleteRule; + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; + type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; + }); + }); +} +function initStyleDeclarationObserver({ styleDeclarationCb, mirror }, { win }) { + const setProperty = win.CSSStyleDeclaration.prototype.setProperty; + win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a, _b; + const [property, value, priority] = argumentsList; + const id = mirror.getId((_b = (_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet) === null || _b === void 0 ? void 0 : _b.ownerNode); + if (id !== -1) { + styleDeclarationCb({ + id, + set: { + property, + value, + priority, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty; + win.CSSStyleDeclaration.prototype.removeProperty = new Proxy(removeProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a, _b; + const [property] = argumentsList; + const id = mirror.getId((_b = (_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet) === null || _b === void 0 ? void 0 : _b.ownerNode); + if (id !== -1) { + styleDeclarationCb({ + id, + remove: { + property, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + return callbackWrapper(() => { + win.CSSStyleDeclaration.prototype.setProperty = setProperty; + win.CSSStyleDeclaration.prototype.removeProperty = removeProperty; + }); +} +function initMediaInteractionObserver({ mediaInteractionCb, blockClass, blockSelector, unblockSelector, mirror, sampling, }) { + const handler = (type) => throttle$1(callbackWrapper((event) => { + const target = getEventTarget(event); + if (!target || + isBlocked(target, blockClass, blockSelector, unblockSelector)) { + return; + } + const { currentTime, volume, muted } = target; + mediaInteractionCb({ + type, + id: mirror.getId(target), + currentTime, + volume, + muted, + }); + }), sampling.media || 500); + const handlers = [ + on('play', handler(0)), + on('pause', handler(1)), + on('seeked', handler(2)), + on('volumechange', handler(3)), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initFontObserver({ fontCb, doc }) { + const win = doc.defaultView; + if (!win) { + return () => { }; + } + const handlers = []; + const fontMap = new WeakMap(); + const originalFontFace = win.FontFace; + win.FontFace = function FontFace(family, source, descriptors) { + const fontFace = new originalFontFace(family, source, descriptors); + fontMap.set(fontFace, { + family, + buffer: typeof source !== 'string', + descriptors, + fontSource: typeof source === 'string' + ? source + : + JSON.stringify(Array.from(new Uint8Array(source))), + }); + return fontFace; + }; + const restoreHandler = patch(doc.fonts, 'add', function (original) { + return function (fontFace) { + setTimeout(() => { + const p = fontMap.get(fontFace); + if (p) { + fontCb(p); + fontMap.delete(fontFace); + } + }, 0); + return original.apply(this, [fontFace]); + }; + }); + handlers.push(() => { + win.FontFace = originalFontFace; + }); + handlers.push(restoreHandler); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function mergeHooks(o, hooks) { + const { mutationCb, mousemoveCb, mouseInteractionCb, scrollCb, viewportResizeCb, inputCb, mediaInteractionCb, styleSheetRuleCb, styleDeclarationCb, canvasMutationCb, fontCb, } = o; + o.mutationCb = (...p) => { + if (hooks.mutation) { + hooks.mutation(...p); + } + mutationCb(...p); + }; + o.mousemoveCb = (...p) => { + if (hooks.mousemove) { + hooks.mousemove(...p); + } + mousemoveCb(...p); + }; + o.mouseInteractionCb = (...p) => { + if (hooks.mouseInteraction) { + hooks.mouseInteraction(...p); + } + mouseInteractionCb(...p); + }; + o.scrollCb = (...p) => { + if (hooks.scroll) { + hooks.scroll(...p); + } + scrollCb(...p); + }; + o.viewportResizeCb = (...p) => { + if (hooks.viewportResize) { + hooks.viewportResize(...p); + } + viewportResizeCb(...p); + }; + o.inputCb = (...p) => { + if (hooks.input) { + hooks.input(...p); + } + inputCb(...p); + }; + o.mediaInteractionCb = (...p) => { + if (hooks.mediaInteaction) { + hooks.mediaInteaction(...p); + } + mediaInteractionCb(...p); + }; + o.styleSheetRuleCb = (...p) => { + if (hooks.styleSheetRule) { + hooks.styleSheetRule(...p); + } + styleSheetRuleCb(...p); + }; + o.styleDeclarationCb = (...p) => { + if (hooks.styleDeclaration) { + hooks.styleDeclaration(...p); + } + styleDeclarationCb(...p); + }; + o.canvasMutationCb = (...p) => { + if (hooks.canvasMutation) { + hooks.canvasMutation(...p); + } + canvasMutationCb(...p); + }; + o.fontCb = (...p) => { + if (hooks.font) { + hooks.font(...p); + } + fontCb(...p); + }; +} +function initObservers(o, hooks = {}) { + const currentWindow = o.doc.defaultView; + if (!currentWindow) { + return () => { }; + } + mergeHooks(o, hooks); + const mutationObserver = initMutationObserver(o, o.doc); + const mousemoveHandler = initMoveObserver(o); + const mouseInteractionHandler = initMouseInteractionObserver(o); + const scrollHandler = initScrollObserver(o); + const viewportResizeHandler = initViewportResizeObserver(o); + const inputHandler = initInputObserver(o); + const mediaInteractionHandler = initMediaInteractionObserver(o); + const styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow }); + const styleDeclarationObserver = initStyleDeclarationObserver(o, { + win: currentWindow, + }); + const fontObserver = o.collectFonts ? initFontObserver(o) : () => { }; + const pluginHandlers = []; + for (const plugin of o.plugins) { + pluginHandlers.push(plugin.observer(plugin.callback, currentWindow, plugin.options)); + } + return callbackWrapper(() => { + mutationBuffers.forEach((b) => b.reset()); + mutationObserver.disconnect(); + mousemoveHandler(); + mouseInteractionHandler(); + scrollHandler(); + viewportResizeHandler(); + inputHandler(); + mediaInteractionHandler(); + try { + styleSheetObserver(); + styleDeclarationObserver(); + } + catch (e) { + } + fontObserver(); + pluginHandlers.forEach((h) => h()); + }); +} +function hasNestedCSSRule(prop) { + return typeof window[prop] !== 'undefined'; +} +function canMonkeyPatchNestedCSSRule(prop) { + return Boolean(typeof window[prop] !== 'undefined' && + window[prop].prototype && + 'insertRule' in window[prop].prototype && + 'deleteRule' in window[prop].prototype); +} + +class IframeManager { + constructor(options) { + this.iframes = new WeakMap(); + this.mutationCb = options.mutationCb; + } + addIframe(iframeEl) { + this.iframes.set(iframeEl, true); + } + addLoadListener(cb) { + this.loadListener = cb; + } + attachIframe(iframeEl, childSn) { + var _a; + this.mutationCb({ + adds: [ + { + parentId: iframeEl.__sn.id, + nextId: null, + node: childSn, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }); + (_a = this.loadListener) === null || _a === void 0 ? void 0 : _a.call(this, iframeEl); + } +} + +class ShadowDomManager { + constructor(options) { + this.restorePatches = []; + this.mutationCb = options.mutationCb; + this.scrollCb = options.scrollCb; + this.bypassOptions = options.bypassOptions; + this.mirror = options.mirror; + const manager = this; + this.restorePatches.push(patch(HTMLElement.prototype, 'attachShadow', function (original) { + return function () { + const shadowRoot = original.apply(this, arguments); + if (this.shadowRoot) + manager.addShadowRoot(this.shadowRoot, this.ownerDocument); + return shadowRoot; + }; + })); + } + addShadowRoot(shadowRoot, doc) { + initMutationObserver(Object.assign(Object.assign({}, this.bypassOptions), { doc, mutationCb: this.mutationCb, mirror: this.mirror, shadowDomManager: this }), shadowRoot); + initScrollObserver(Object.assign(Object.assign({}, this.bypassOptions), { scrollCb: this.scrollCb, doc: shadowRoot, mirror: this.mirror })); + } + observeAttachShadow(iframeElement) { + if (iframeElement.contentWindow) { + const manager = this; + this.restorePatches.push(patch(iframeElement.contentWindow.HTMLElement.prototype, 'attachShadow', function (original) { + return function () { + const shadowRoot = original.apply(this, arguments); + if (this.shadowRoot) + manager.addShadowRoot(this.shadowRoot, iframeElement.contentDocument); + return shadowRoot; + }; + })); + } + } + reset() { + this.restorePatches.forEach((restorePatch) => restorePatch()); + } +} + +/****************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ + +function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +} + +function initCanvas2DMutationObserver(cb, win, blockClass, unblockSelector, blockSelector, mirror) { + const handlers = []; + const props2D = Object.getOwnPropertyNames(win.CanvasRenderingContext2D.prototype); + for (const prop of props2D) { + try { + if (typeof win.CanvasRenderingContext2D.prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(win.CanvasRenderingContext2D.prototype, prop, function (original) { + return function (...args) { + if (!isBlocked(this.canvas, blockClass, blockSelector, unblockSelector)) { + setTimeout(() => { + const recordArgs = [...args]; + if (prop === 'drawImage') { + if (recordArgs[0] && + recordArgs[0] instanceof HTMLCanvasElement) { + const canvas = recordArgs[0]; + const ctx = canvas.getContext('2d'); + let imgd = ctx === null || ctx === void 0 ? void 0 : ctx.getImageData(0, 0, canvas.width, canvas.height); + let pix = imgd === null || imgd === void 0 ? void 0 : imgd.data; + recordArgs[0] = JSON.stringify(pix); + } + } + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: recordArgs, + }); + }, 0); + } + return original.apply(this, args); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(win.CanvasRenderingContext2D.prototype, prop, { + set(v) { + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return () => { + handlers.forEach((h) => h()); + }; +} + +function initCanvasContextObserver(win, blockClass, blockSelector, unblockSelector) { + const handlers = []; + try { + const restoreHandler = patch(win.HTMLCanvasElement.prototype, 'getContext', function (original) { + return function (contextType, ...args) { + if (!isBlocked(this, blockClass, blockSelector, unblockSelector)) { + if (!('__context' in this)) + this.__context = contextType; + } + return original.apply(this, [contextType, ...args]); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + console.error('failed to patch HTMLCanvasElement.prototype.getContext'); + } + return () => { + handlers.forEach((h) => h()); + }; +} + +/* + * base64-arraybuffer 1.0.2 + * Copyright (c) 2022 Niklas von Hertzen + * Released under MIT License + */ +var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +// Use a lookup table to find the index. +var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); +for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; +} +var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; +}; + +const webGLVarMap = new Map(); +function variableListFor(ctx, ctor) { + let contextMap = webGLVarMap.get(ctx); + if (!contextMap) { + contextMap = new Map(); + webGLVarMap.set(ctx, contextMap); + } + if (!contextMap.has(ctor)) { + contextMap.set(ctor, []); + } + return contextMap.get(ctor); +} +const saveWebGLVar = (value, win, ctx) => { + if (!value || + !(isInstanceOfWebGLObject(value, win) || typeof value === 'object')) + return; + const name = value.constructor.name; + const list = variableListFor(ctx, name); + let index = list.indexOf(value); + if (index === -1) { + index = list.length; + list.push(value); + } + return index; +}; +function serializeArg(value, win, ctx) { + if (value instanceof Array) { + return value.map((arg) => serializeArg(arg, win, ctx)); + } + else if (value === null) { + return value; + } + else if (value instanceof Float32Array || + value instanceof Float64Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Uint8Array || + value instanceof Uint16Array || + value instanceof Int16Array || + value instanceof Int8Array || + value instanceof Uint8ClampedArray) { + const name = value.constructor.name; + return { + rr_type: name, + args: [Object.values(value)], + }; + } + else if (value instanceof ArrayBuffer) { + const name = value.constructor.name; + const base64 = encode(value); + return { + rr_type: name, + base64, + }; + } + else if (value instanceof DataView) { + const name = value.constructor.name; + return { + rr_type: name, + args: [ + serializeArg(value.buffer, win, ctx), + value.byteOffset, + value.byteLength, + ], + }; + } + else if (value instanceof HTMLImageElement) { + const name = value.constructor.name; + const { src } = value; + return { + rr_type: name, + src, + }; + } + else if (value instanceof ImageData) { + const name = value.constructor.name; + return { + rr_type: name, + args: [serializeArg(value.data, win, ctx), value.width, value.height], + }; + } + else if (isInstanceOfWebGLObject(value, win) || typeof value === 'object') { + const name = value.constructor.name; + const index = saveWebGLVar(value, win, ctx); + return { + rr_type: name, + index: index, + }; + } + return value; +} +const serializeArgs = (args, win, ctx) => { + return [...args].map((arg) => serializeArg(arg, win, ctx)); +}; +const isInstanceOfWebGLObject = (value, win) => { + const webGLConstructorNames = [ + 'WebGLActiveInfo', + 'WebGLBuffer', + 'WebGLFramebuffer', + 'WebGLProgram', + 'WebGLRenderbuffer', + 'WebGLShader', + 'WebGLShaderPrecisionFormat', + 'WebGLTexture', + 'WebGLUniformLocation', + 'WebGLVertexArrayObject', + 'WebGLVertexArrayObjectOES', + ]; + const supportedWebGLConstructorNames = webGLConstructorNames.filter((name) => typeof win[name] === 'function'); + return Boolean(supportedWebGLConstructorNames.find((name) => value instanceof win[name])); +}; + +function patchGLPrototype(prototype, type, cb, blockClass, unblockSelector, blockSelector, mirror, win) { + const handlers = []; + const props = Object.getOwnPropertyNames(prototype); + for (const prop of props) { + try { + if (typeof prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(prototype, prop, function (original) { + return function (...args) { + const result = original.apply(this, args); + saveWebGLVar(result, win, prototype); + if (!isBlocked(this.canvas, blockClass, blockSelector, unblockSelector)) { + const id = mirror.getId(this.canvas); + const recordArgs = serializeArgs([...args], win, prototype); + const mutation = { + type, + property: prop, + args: recordArgs, + }; + cb(this.canvas, mutation); + } + return result; + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(prototype, prop, { + set(v) { + cb(this.canvas, { + type, + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return handlers; +} +function initCanvasWebGLMutationObserver(cb, win, blockClass, blockSelector, unblockSelector, mirror) { + const handlers = []; + handlers.push(...patchGLPrototype(win.WebGLRenderingContext.prototype, CanvasContext.WebGL, cb, blockClass, blockSelector, unblockSelector, mirror, win)); + if (typeof win.WebGL2RenderingContext !== 'undefined') { + handlers.push(...patchGLPrototype(win.WebGL2RenderingContext.prototype, CanvasContext.WebGL2, cb, blockClass, blockSelector, unblockSelector, mirror, win)); + } + return () => { + handlers.forEach((h) => h()); + }; +} + +class CanvasManager { + reset() { + this.pendingCanvasMutations.clear(); + this.resetObservers && this.resetObservers(); + } + freeze() { + this.frozen = true; + } + unfreeze() { + this.frozen = false; + } + lock() { + this.locked = true; + } + unlock() { + this.locked = false; + } + constructor(options) { + this.pendingCanvasMutations = new Map(); + this.rafStamps = { latestId: 0, invokeId: null }; + this.frozen = false; + this.locked = false; + this.processMutation = function (target, mutation) { + const newFrame = this.rafStamps.invokeId && + this.rafStamps.latestId !== this.rafStamps.invokeId; + if (newFrame || !this.rafStamps.invokeId) + this.rafStamps.invokeId = this.rafStamps.latestId; + if (!this.pendingCanvasMutations.has(target)) { + this.pendingCanvasMutations.set(target, []); + } + this.pendingCanvasMutations.get(target).push(mutation); + }; + this.mutationCb = options.mutationCb; + this.mirror = options.mirror; + if (options.recordCanvas === true) + this.initCanvasMutationObserver(options.win, options.blockClass, options.blockSelector, options.unblockSelector); + } + initCanvasMutationObserver(win, blockClass, unblockSelector, blockSelector) { + this.startRAFTimestamping(); + this.startPendingCanvasMutationFlusher(); + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, unblockSelector); + const canvas2DReset = initCanvas2DMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector, unblockSelector, this.mirror); + const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector, unblockSelector, this.mirror); + this.resetObservers = () => { + canvasContextReset(); + canvas2DReset(); + canvasWebGL1and2Reset(); + }; + } + startPendingCanvasMutationFlusher() { + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + startRAFTimestamping() { + const setLatestRAFTimestamp = (timestamp) => { + this.rafStamps.latestId = timestamp; + requestAnimationFrame(setLatestRAFTimestamp); + }; + requestAnimationFrame(setLatestRAFTimestamp); + } + flushPendingCanvasMutations() { + this.pendingCanvasMutations.forEach((values, canvas) => { + const id = this.mirror.getId(canvas); + this.flushPendingCanvasMutationFor(canvas, id); + }); + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + flushPendingCanvasMutationFor(canvas, id) { + if (this.frozen || this.locked) { + return; + } + const valuesWithType = this.pendingCanvasMutations.get(canvas); + if (!valuesWithType || id === -1) + return; + const values = valuesWithType.map((value) => { + const rest = __rest(value, ["type"]); + return rest; + }); + const { type } = valuesWithType[0]; + this.mutationCb({ id, type, commands: values }); + this.pendingCanvasMutations.delete(canvas); + } +} + +function wrapEvent(e) { + return Object.assign(Object.assign({}, e), { timestamp: Date.now() }); +} +let wrappedEmit; +let takeFullSnapshot; +const mirror = createMirror(); +function record(options = {}) { + 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; + if (!emit) { + throw new Error('emit function is required'); + } + if (mousemoveWait !== undefined && sampling.mousemove === undefined) { + sampling.mousemove = mousemoveWait; + } + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + radio: true, + checkbox: true, + } + : _maskInputOptions !== undefined + ? _maskInputOptions + : {}; + const slimDOMOptions = _slimDOMOptions === true || _slimDOMOptions === 'all' + ? { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaVerification: true, + headMetaAuthorship: _slimDOMOptions === 'all', + headMetaDescKeywords: _slimDOMOptions === 'all', + } + : _slimDOMOptions + ? _slimDOMOptions + : {}; + polyfill(); + let lastFullSnapshotEvent; + let incrementalSnapshotCount = 0; + const eventProcessor = (e) => { + for (const plugin of plugins || []) { + if (plugin.eventProcessor) { + e = plugin.eventProcessor(e); + } + } + if (packFn) { + e = packFn(e); + } + return e; + }; + wrappedEmit = (e, isCheckout) => { + var _a; + if (((_a = mutationBuffers[0]) === null || _a === void 0 ? void 0 : _a.isFrozen()) && + e.type !== EventType.FullSnapshot && + !(e.type === EventType.IncrementalSnapshot && + e.data.source === IncrementalSource.Mutation)) { + mutationBuffers.forEach((buf) => buf.unfreeze()); + } + emit(eventProcessor(e), isCheckout); + if (e.type === EventType.FullSnapshot) { + lastFullSnapshotEvent = e; + incrementalSnapshotCount = 0; + } + else if (e.type === EventType.IncrementalSnapshot) { + if (e.data.source === IncrementalSource.Mutation && + e.data.isAttachIframe) { + return; + } + incrementalSnapshotCount++; + const exceedCount = checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth; + const exceedTime = checkoutEveryNms && + e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms; + if (exceedCount || exceedTime) { + takeFullSnapshot(true); + } + } + }; + const wrappedMutationEmit = (m) => { + wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.Mutation }, m), + })); + }; + const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.Scroll }, p), + })); + const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.CanvasMutation }, p), + })); + const iframeManager = new IframeManager({ + mutationCb: wrappedMutationEmit, + }); + const canvasManager = new CanvasManager({ + recordCanvas, + mutationCb: wrappedCanvasMutationEmit, + win: window, + blockClass, + blockSelector, + unblockSelector, + mirror, + }); + const shadowDomManager = new ShadowDomManager({ + mutationCb: wrappedMutationEmit, + scrollCb: wrappedScrollEmit, + bypassOptions: { + onMutation, + blockClass, + blockSelector, + unblockSelector, + maskTextClass, + maskTextSelector, + unmaskTextSelector, + maskInputSelector, + unmaskInputSelector, + inlineStylesheet, + maskAllText, + maskInputOptions, + maskTextFn, + maskInputFn, + recordCanvas, + inlineImages, + sampling, + slimDOMOptions, + iframeManager, + canvasManager, + }, + mirror, + }); + takeFullSnapshot = (isCheckout = false) => { + var _a, _b, _c, _d; + wrappedEmit(wrapEvent({ + type: EventType.Meta, + data: { + href: window.location.href, + width: getWindowWidth(), + height: getWindowHeight(), + }, + }), isCheckout); + mutationBuffers.forEach((buf) => buf.lock()); + const [node, idNodeMap] = snapshot(document, { + blockClass, + blockSelector, + unblockSelector, + maskTextClass, + maskTextSelector, + unmaskTextSelector, + maskInputSelector, + unmaskInputSelector, + inlineStylesheet, + maskAllText, + maskAllInputs: maskInputOptions, + maskTextFn, + slimDOM: slimDOMOptions, + recordCanvas, + inlineImages, + onSerialize: (n) => { + if (isIframeINode(n)) { + iframeManager.addIframe(n); + } + if (hasShadowRoot(n)) { + shadowDomManager.addShadowRoot(n.shadowRoot, document); + } + }, + onIframeLoad: (iframe, childSn) => { + iframeManager.attachIframe(iframe, childSn); + shadowDomManager.observeAttachShadow(iframe); + }, + keepIframeSrcFn, + }); + if (!node) { + return console.warn('Failed to snapshot the document'); + } + mirror.map = idNodeMap; + wrappedEmit(wrapEvent({ + type: EventType.FullSnapshot, + data: { + node, + initialOffset: { + left: window.pageXOffset !== undefined + ? window.pageXOffset + : (document === null || document === void 0 ? void 0 : document.documentElement.scrollLeft) || + ((_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) || + (document === null || document === void 0 ? void 0 : document.body.scrollLeft) || + 0, + top: window.pageYOffset !== undefined + ? window.pageYOffset + : (document === null || document === void 0 ? void 0 : document.documentElement.scrollTop) || + ((_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) || + (document === null || document === void 0 ? void 0 : document.body.scrollTop) || + 0, + }, + }, + })); + mutationBuffers.forEach((buf) => buf.unlock()); + }; + try { + const handlers = []; + handlers.push(on('DOMContentLoaded', () => { + wrappedEmit(wrapEvent({ + type: EventType.DomContentLoaded, + data: {}, + })); + })); + const observe = (doc) => { + var _a; + return callbackWrapper(initObservers)({ + onMutation, + mutationCb: wrappedMutationEmit, + mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source, + positions, + }, + })), + mouseInteractionCb: (d) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.MouseInteraction }, d), + })), + scrollCb: wrappedScrollEmit, + viewportResizeCb: (d) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.ViewportResize }, d), + })), + inputCb: (v) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.Input }, v), + })), + mediaInteractionCb: (p) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.MediaInteraction }, p), + })), + styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.StyleSheetRule }, r), + })), + styleDeclarationCb: (r) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.StyleDeclaration }, r), + })), + canvasMutationCb: wrappedCanvasMutationEmit, + fontCb: (p) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.Font }, p), + })), + blockClass, + ignoreClass, + ignoreSelector, + maskTextClass, + maskTextSelector, + unmaskTextSelector, + maskInputSelector, + unmaskInputSelector, + maskInputOptions, + inlineStylesheet, + sampling, + recordCanvas, + inlineImages, + userTriggeredOnInput, + collectFonts, + doc, + maskAllText, + maskInputFn, + maskTextFn, + blockSelector, + unblockSelector, + slimDOMOptions, + mirror, + iframeManager, + shadowDomManager, + canvasManager, + plugins: ((_a = plugins === null || plugins === void 0 ? void 0 : plugins.filter((p) => p.observer)) === null || _a === void 0 ? void 0 : _a.map((p) => ({ + observer: p.observer, + options: p.options, + callback: (payload) => wrappedEmit(wrapEvent({ + type: EventType.Plugin, + data: { + plugin: p.name, + payload, + }, + })), + }))) || [], + }, hooks); + }; + iframeManager.addLoadListener((iframeEl) => { + try { + handlers.push(observe(iframeEl.contentDocument)); + } + catch (error) { + console.warn(error); + } + }); + const init = () => { + takeFullSnapshot(); + handlers.push(observe(document)); + }; + if (document.readyState === 'interactive' || + document.readyState === 'complete') { + init(); + } + else { + handlers.push(on('load', () => { + wrappedEmit(wrapEvent({ + type: EventType.Load, + data: {}, + })); + init(); + }, window)); + } + return () => { + handlers.forEach((h) => h()); + }; + } + catch (error) { + console.warn(error); + } +} +record.addCustomEvent = (tag, payload) => { + if (!wrappedEmit) { + throw new Error('please add custom event after start recording'); + } + wrappedEmit(wrapEvent({ + type: EventType.Custom, + data: { + tag, + payload, + }, + })); +}; +record.freezePage = () => { + mutationBuffers.forEach((buf) => buf.freeze()); +}; +record.takeFullSnapshot = (isCheckout) => { + if (!takeFullSnapshot) { + throw new Error('please take full snapshot after start recording'); + } + takeFullSnapshot(isCheckout); +}; +record.mirror = mirror; + +/** + * Add a breadcrumb event to replay. + */ +function addBreadcrumbEvent(replay, breadcrumb) { + if (breadcrumb.category === 'sentry.transaction') { + return; + } + + if (['ui.click', 'ui.input'].includes(breadcrumb.category )) { + replay.triggerUserActivity(); + } else { + replay.checkAndHandleExpiredSession(); + } + + replay.addUpdate(() => { + void replay.throttledAddEvent({ + type: EventType.Custom, + // TODO: We were converting from ms to seconds for breadcrumbs, spans, + // but maybe we should just keep them as milliseconds + timestamp: (breadcrumb.timestamp || 0) * 1000, + data: { + tag: 'breadcrumb', + // normalize to max. 10 depth and 1_000 properties per object + payload: normalize(breadcrumb, 10, 1000), + }, + }); + + // Do not flush after console log messages + return breadcrumb.category === 'console'; + }); +} + +const INTERACTIVE_SELECTOR = 'button,a'; + +/** + * For clicks, we check if the target is inside of a button or link + * If so, we use this as the target instead + * This is useful because if you click on the image in , + * The target will be the image, not the button, which we don't want here + */ +function getClickTargetNode(event) { + const target = getTargetNode(event); + + if (!target || !(target instanceof Element)) { + return target; + } + + const closestInteractive = target.closest(INTERACTIVE_SELECTOR); + return closestInteractive || target; +} + +/** Get the event target node. */ +function getTargetNode(event) { + if (isEventWithTarget(event)) { + return event.target ; + } + + return event; +} + +function isEventWithTarget(event) { + return typeof event === 'object' && !!event && 'target' in event; +} + +let handlers; + +/** + * Register a handler to be called when `window.open()` is called. + * Returns a cleanup function. + */ +function onWindowOpen(cb) { + // Ensure to only register this once + if (!handlers) { + handlers = []; + monkeyPatchWindowOpen(); + } + + handlers.push(cb); + + return () => { + const pos = handlers ? handlers.indexOf(cb) : -1; + if (pos > -1) { + (handlers ).splice(pos, 1); + } + }; +} + +function monkeyPatchWindowOpen() { + fill(WINDOW, 'open', function (originalWindowOpen) { + return function (...args) { + if (handlers) { + try { + handlers.forEach(handler => handler()); + } catch (e) { + // ignore errors in here + } + } + + return originalWindowOpen.apply(WINDOW, args); + }; + }); +} + +/** Handle a click. */ +function handleClick(clickDetector, clickBreadcrumb, node) { + clickDetector.handleClick(clickBreadcrumb, node); +} + +/** A click detector class that can be used to detect slow or rage clicks on elements. */ +class ClickDetector { + // protected for testing + __init() {this._lastMutation = 0;} + __init2() {this._lastScroll = 0;} + + __init3() {this._clicks = [];} + + constructor( + replay, + slowClickConfig, + // Just for easier testing + _addBreadcrumbEvent = addBreadcrumbEvent, + ) {ClickDetector.prototype.__init.call(this);ClickDetector.prototype.__init2.call(this);ClickDetector.prototype.__init3.call(this); + // We want everything in s, but options are in ms + this._timeout = slowClickConfig.timeout / 1000; + this._multiClickTimeout = slowClickConfig.multiClickTimeout / 1000; + this._threshold = slowClickConfig.threshold / 1000; + this._scollTimeout = slowClickConfig.scrollTimeout / 1000; + this._replay = replay; + this._ignoreSelector = slowClickConfig.ignoreSelector; + this._addBreadcrumbEvent = _addBreadcrumbEvent; + } + + /** Register click detection handlers on mutation or scroll. */ + addListeners() { + const mutationHandler = () => { + this._lastMutation = nowInSeconds(); + }; + + const scrollHandler = () => { + this._lastScroll = nowInSeconds(); + }; + + const cleanupWindowOpen = onWindowOpen(() => { + // Treat window.open as mutation + this._lastMutation = nowInSeconds(); + }); + + const clickHandler = (event) => { + if (!event.target) { + return; + } + + const node = getClickTargetNode(event); + if (node) { + this._handleMultiClick(node ); + } + }; + + const obs = new MutationObserver(mutationHandler); + + obs.observe(WINDOW.document.documentElement, { + attributes: true, + characterData: true, + childList: true, + subtree: true, + }); + + WINDOW.addEventListener('scroll', scrollHandler, { passive: true }); + WINDOW.addEventListener('click', clickHandler, { passive: true }); + + this._teardown = () => { + WINDOW.removeEventListener('scroll', scrollHandler); + WINDOW.removeEventListener('click', clickHandler); + cleanupWindowOpen(); + + obs.disconnect(); + this._clicks = []; + this._lastMutation = 0; + this._lastScroll = 0; + }; + } + + /** Clean up listeners. */ + removeListeners() { + if (this._teardown) { + this._teardown(); + } + + if (this._checkClickTimeout) { + clearTimeout(this._checkClickTimeout); + } + } + + /** Handle a click */ + handleClick(breadcrumb, node) { + if (ignoreElement(node, this._ignoreSelector) || !isClickBreadcrumb(breadcrumb)) { + return; + } + + const click = this._getClick(node); + + if (click) { + // this means a click on the same element was captured in the last 1s, so we consider this a multi click + return; + } + + const newClick = { + timestamp: breadcrumb.timestamp, + clickBreadcrumb: breadcrumb, + // Set this to 0 so we know it originates from the click breadcrumb + clickCount: 0, + node, + }; + this._clicks.push(newClick); + + // If this is the first new click, set a timeout to check for multi clicks + if (this._clicks.length === 1) { + this._scheduleCheckClicks(); + } + } + + /** Count multiple clicks on elements. */ + _handleMultiClick(node) { + const click = this._getClick(node); + + if (!click) { + return; + } + + click.clickCount++; + } + + /** Try to get an existing click on the given element. */ + _getClick(node) { + const now = nowInSeconds(); + + // Find any click on the same element in the last second + // If one exists, we consider this click as a double/triple/etc click + return this._clicks.find(click => click.node === node && now - click.timestamp < this._multiClickTimeout); + } + + /** Check the clicks that happened. */ + _checkClicks() { + const timedOutClicks = []; + + const now = nowInSeconds(); + + this._clicks.forEach(click => { + if (!click.mutationAfter && this._lastMutation) { + click.mutationAfter = click.timestamp <= this._lastMutation ? this._lastMutation - click.timestamp : undefined; + } + if (!click.scrollAfter && this._lastScroll) { + click.scrollAfter = click.timestamp <= this._lastScroll ? this._lastScroll - click.timestamp : undefined; + } + + // If an action happens after the multi click threshold, we can skip waiting and handle the click right away + const actionTime = click.scrollAfter || click.mutationAfter || 0; + if (actionTime && actionTime >= this._multiClickTimeout) { + timedOutClicks.push(click); + return; + } + + if (click.timestamp + this._timeout <= now) { + timedOutClicks.push(click); + } + }); + + // Remove "old" clicks + for (const click of timedOutClicks) { + this._generateBreadcrumbs(click); + + const pos = this._clicks.indexOf(click); + if (pos !== -1) { + this._clicks.splice(pos, 1); + } + } + + // Trigger new check, unless no clicks left + if (this._clicks.length) { + this._scheduleCheckClicks(); + } + } + + /** Generate matching breadcrumb(s) for the click. */ + _generateBreadcrumbs(click) { + const replay = this._replay; + const hadScroll = click.scrollAfter && click.scrollAfter <= this._scollTimeout; + const hadMutation = click.mutationAfter && click.mutationAfter <= this._threshold; + + const isSlowClick = !hadScroll && !hadMutation; + const { clickCount, clickBreadcrumb } = click; + + // Slow click + if (isSlowClick) { + // If `mutationAfter` is set, it means a mutation happened after the threshold, but before the timeout + // If not, it means we just timed out without scroll & mutation + const timeAfterClickMs = Math.min(click.mutationAfter || this._timeout, this._timeout) * 1000; + const endReason = timeAfterClickMs < this._timeout * 1000 ? 'mutation' : 'timeout'; + + const breadcrumb = { + type: 'default', + message: clickBreadcrumb.message, + timestamp: clickBreadcrumb.timestamp, + category: 'ui.slowClickDetected', + data: { + ...clickBreadcrumb.data, + url: WINDOW.location.href, + route: replay.getCurrentRoute(), + timeAfterClickMs, + endReason, + // If clickCount === 0, it means multiClick was not correctly captured here + // - we still want to send 1 in this case + clickCount: clickCount || 1, + }, + }; + + this._addBreadcrumbEvent(replay, breadcrumb); + return; + } + + // Multi click + if (clickCount > 1) { + const breadcrumb = { + type: 'default', + message: clickBreadcrumb.message, + timestamp: clickBreadcrumb.timestamp, + category: 'ui.multiClick', + data: { + ...clickBreadcrumb.data, + url: WINDOW.location.href, + route: replay.getCurrentRoute(), + clickCount, + metric: true, + }, + }; + + this._addBreadcrumbEvent(replay, breadcrumb); + } + } + + /** Schedule to check current clicks. */ + _scheduleCheckClicks() { + this._checkClickTimeout = setTimeout(() => this._checkClicks(), 1000); + } +} + +const SLOW_CLICK_TAGS = ['A', 'BUTTON', 'INPUT']; + +/** exported for tests only */ +function ignoreElement(node, ignoreSelector) { + if (!SLOW_CLICK_TAGS.includes(node.tagName)) { + return true; + } + + // If tag, we only want to consider input[type='submit'] & input[type='button'] + if (node.tagName === 'INPUT' && !['submit', 'button'].includes(node.getAttribute('type') || '')) { + return true; + } + + // If tag, detect special variants that may not lead to an action + // If target !== _self, we may open the link somewhere else, which would lead to no action + // Also, when downloading a file, we may not leave the page, but still not trigger an action + if ( + node.tagName === 'A' && + (node.hasAttribute('download') || (node.hasAttribute('target') && node.getAttribute('target') !== '_self')) + ) { + return true; + } + + if (ignoreSelector && node.matches(ignoreSelector)) { + return true; + } + + return false; +} + +function isClickBreadcrumb(breadcrumb) { + return !!(breadcrumb.data && typeof breadcrumb.data.nodeId === 'number' && breadcrumb.timestamp); +} + +// This is good enough for us, and is easier to test/mock than `timestampInSeconds` +function nowInSeconds() { + return Date.now() / 1000; +} + +/** + * Create a breadcrumb for a replay. + */ +function createBreadcrumb( + breadcrumb, +) { + return { + timestamp: Date.now() / 1000, + type: 'default', + ...breadcrumb, + }; +} + +var NodeType; +(function (NodeType) { + NodeType[NodeType["Document"] = 0] = "Document"; + NodeType[NodeType["DocumentType"] = 1] = "DocumentType"; + NodeType[NodeType["Element"] = 2] = "Element"; + NodeType[NodeType["Text"] = 3] = "Text"; + NodeType[NodeType["CDATA"] = 4] = "CDATA"; + NodeType[NodeType["Comment"] = 5] = "Comment"; +})(NodeType || (NodeType = {})); + +// Note that these are the serialized attributes and not attributes directly on +// the DOM Node. Attributes we are interested in: +const ATTRIBUTES_TO_RECORD = new Set([ + 'id', + 'class', + 'aria-label', + 'role', + 'name', + 'alt', + 'title', + 'data-test-id', + 'data-testid', + 'disabled', + 'aria-disabled', +]); + +/** + * Inclusion list of attributes that we want to record from the DOM element + */ +function getAttributesToRecord(attributes) { + const obj = {}; + for (const key in attributes) { + if (ATTRIBUTES_TO_RECORD.has(key)) { + let normalizedKey = key; + + if (key === 'data-testid' || key === 'data-test-id') { + normalizedKey = 'testId'; + } + + obj[normalizedKey] = attributes[key]; + } + } + + return obj; +} + +const handleDomListener = ( + replay, +) => { + return (handlerData) => { + if (!replay.isEnabled()) { + return; + } + + const result = handleDom(handlerData); + + if (!result) { + return; + } + + const isClick = handlerData.name === 'click'; + const event = isClick && (handlerData.event ); + // Ignore clicks if ctrl/alt/meta keys are held down as they alter behavior of clicks (e.g. open in new tab) + if (isClick && replay.clickDetector && event && !event.altKey && !event.metaKey && !event.ctrlKey) { + handleClick( + replay.clickDetector, + result , + getClickTargetNode(handlerData.event) , + ); + } + + addBreadcrumbEvent(replay, result); + }; +}; + +/** Get the base DOM breadcrumb. */ +function getBaseDomBreadcrumb(target, message) { + // `__sn` property is the serialized node created by rrweb + const serializedNode = target && isRrwebNode(target) && target.__sn.type === NodeType.Element ? target.__sn : null; + + return { + message, + data: serializedNode + ? { + nodeId: serializedNode.id, + node: { + id: serializedNode.id, + tagName: serializedNode.tagName, + textContent: target + ? Array.from(target.childNodes) + .map( + (node) => '__sn' in node && node.__sn.type === NodeType.Text && node.__sn.textContent, + ) + .filter(Boolean) // filter out empty values + .map(text => (text ).trim()) + .join('') + : '', + attributes: getAttributesToRecord(serializedNode.attributes), + }, + } + : {}, + }; +} + +/** + * An event handler to react to DOM events. + * Exported for tests. + */ +function handleDom(handlerData) { + const { target, message } = getDomTarget(handlerData); + + return createBreadcrumb({ + category: `ui.${handlerData.name}`, + ...getBaseDomBreadcrumb(target, message), + }); +} + +function getDomTarget(handlerData) { + const isClick = handlerData.name === 'click'; + + let message; + let target = null; + + // Accessing event.target can throw (see getsentry/raven-js#838, #768) + try { + target = isClick ? getClickTargetNode(handlerData.event) : getTargetNode(handlerData.event); + message = htmlTreeAsString(target, { maxStringLength: 200 }) || ''; + } catch (e) { + message = ''; + } + + return { target, message }; +} + +function isRrwebNode(node) { + return '__sn' in node; +} + +/** Handle keyboard events & create breadcrumbs. */ +function handleKeyboardEvent(replay, event) { + if (!replay.isEnabled()) { + return; + } + + // Update user activity, but do not restart recording as it can create + // noisy/low-value replays (e.g. user comes back from idle, hits alt-tab, new + // session with a single "keydown" breadcrumb is created) + replay.updateUserActivity(); + + const breadcrumb = getKeyboardBreadcrumb(event); + + if (!breadcrumb) { + return; + } + + addBreadcrumbEvent(replay, breadcrumb); +} + +/** exported only for tests */ +function getKeyboardBreadcrumb(event) { + const { metaKey, shiftKey, ctrlKey, altKey, key, target } = event; + + // never capture for input fields + if (!target || isInputElement(target ) || !key) { + return null; + } + + // Note: We do not consider shift here, as that means "uppercase" + const hasModifierKey = metaKey || ctrlKey || altKey; + const isCharacterKey = key.length === 1; // other keys like Escape, Tab, etc have a longer length + + // Do not capture breadcrumb if only a word key is pressed + // This could leak e.g. user input + if (!hasModifierKey && isCharacterKey) { + return null; + } + + const message = htmlTreeAsString(target, { maxStringLength: 200 }) || ''; + const baseBreadcrumb = getBaseDomBreadcrumb(target , message); + + return createBreadcrumb({ + category: 'ui.keyDown', + message, + data: { + ...baseBreadcrumb.data, + metaKey, + shiftKey, + ctrlKey, + altKey, + key, + }, + }); +} + +function isInputElement(target) { + return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable; +} + +const NAVIGATION_ENTRY_KEYS = [ + 'name', + 'type', + 'startTime', + 'transferSize', + 'duration', +]; + +function isNavigationEntryEqual(a) { + return function (b) { + return NAVIGATION_ENTRY_KEYS.every(key => a[key] === b[key]); + }; +} + +/** + * There are some difficulties diagnosing why there are duplicate navigation + * entries. We've witnessed several intermittent results: + * - duplicate entries have duration = 0 + * - duplicate entries are the same object reference + * - none of the above + * + * Compare the values of several keys to determine if the entries are duplicates or not. + */ +// TODO (high-prio): Figure out wth is returned here +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function dedupePerformanceEntries( + currentList, + newList, +) { + // Partition `currentList` into 3 different lists based on entryType + const [existingNavigationEntries, existingLcpEntries, existingEntries] = currentList.reduce( + (acc, entry) => { + if (entry.entryType === 'navigation') { + acc[0].push(entry ); + } else if (entry.entryType === 'largest-contentful-paint') { + acc[1].push(entry ); + } else { + acc[2].push(entry); + } + return acc; + }, + [[], [], []], + ); + + const newEntries = []; + const newNavigationEntries = []; + let newLcpEntry = existingLcpEntries.length + ? existingLcpEntries[existingLcpEntries.length - 1] // Take the last element as list is sorted + : undefined; + + newList.forEach(entry => { + if (entry.entryType === 'largest-contentful-paint') { + // We want the latest LCP event only + if (!newLcpEntry || newLcpEntry.startTime < entry.startTime) { + newLcpEntry = entry; + } + return; + } + + if (entry.entryType === 'navigation') { + const navigationEntry = entry ; + + // Check if the navigation entry is contained in currentList or newList + if ( + // Ignore any navigation entries with duration 0, as they are likely duplicates + entry.duration > 0 && + // Ensure new entry does not already exist in existing entries + !existingNavigationEntries.find(isNavigationEntryEqual(navigationEntry)) && + // Ensure new entry does not already exist in new list of navigation entries + !newNavigationEntries.find(isNavigationEntryEqual(navigationEntry)) + ) { + newNavigationEntries.push(navigationEntry); + } + + // Otherwise this navigation entry is considered a duplicate and is thrown away + return; + } + + newEntries.push(entry); + }); + + // Re-combine and sort by startTime + return [ + ...(newLcpEntry ? [newLcpEntry] : []), + ...existingNavigationEntries, + ...existingEntries, + ...newEntries, + ...newNavigationEntries, + ].sort((a, b) => a.startTime - b.startTime); +} + +/** + * Sets up a PerformanceObserver to listen to all performance entry types. + */ +function setupPerformanceObserver(replay) { + const performanceObserverHandler = (list) => { + // For whatever reason the observer was returning duplicate navigation + // entries (the other entry types were not duplicated). + const newPerformanceEntries = dedupePerformanceEntries( + replay.performanceEvents, + list.getEntries() , + ); + replay.performanceEvents = newPerformanceEntries; + }; + + const performanceObserver = new PerformanceObserver(performanceObserverHandler); + + [ + 'element', + 'event', + 'first-input', + 'largest-contentful-paint', + 'layout-shift', + 'longtask', + 'navigation', + 'paint', + 'resource', + ].forEach(type => { + try { + performanceObserver.observe({ + type, + buffered: true, + }); + } catch (e) { + // This can throw if an entry type is not supported in the browser. + // Ignore these errors. + } + }); + + return performanceObserver; +} + +const r = `/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */ +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<>16-t.bi_valid,t.bi_valid+=a-16):(t.bi_buf|=e<{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]{const i=t.heap[a];let n=a<<1;for(;n<=t.heap_len&&(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{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>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{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{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{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<>=7;m<30;m++)for(d[m]=b<<7,t=0;t<1<(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>>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<{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]&&sr){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_outi+t.strm.avail_in&&(a=i+t.strm.avail_in),a>n&&(a=n),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_watern&&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>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_length4096)&&(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<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.gzindexi&&(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.gzindexi&&(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]&&nt.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=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>>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;i4)s[n++]=65533,i+=r-1;else{for(e&=2===r?31:3===r?15:7;r>1&&i1?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=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<>>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,c-=p),c<15&&(f+=z[a++]<>>24,f>>>=p,c-=p,p=g>>>16&255,!(16&p)){if(0==(64&p)){g=w[(65535&g)+(f&(1<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,p2;)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>3,a-=k,c-=k<<3,f&=(1<{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=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;b0&&(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;m852||2===t&&x>592)return 1;for(;;){Z=w-v,r[m]+1=u?(U=D[r[m]-u],S=A[r[m]-u]):(U=96,S=0),h=1<>v)+d]=Z<<24|U<<16|S|0}while(0!==d);for(h=1<>=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<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.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.whavexe(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++]<>>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<>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++]<>>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++]<>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++]<>>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>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++]<>>=7&d,d-=7&d,a.mode=16206;break}for(;d<3;){if(0===o)break t;o--,h+=i[s++]<>>=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++]<>>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++]<>>=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>>=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>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=m,d-=m,a.lens[a.have++]=g;else{if(16===g){for(R=m+2;d>>=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>>=m,d-=m,y=0,c=3+(7&h),h>>>=3,d-=3}else{for(R=m+7;d>>=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<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=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>>=a.extra,d-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=16202;case 16202:for(;z=a.distcode[h&(1<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=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>>=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++]<{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});`; + +function e(){const e=new Blob([r]);return URL.createObjectURL(e)} + +/** + * Converts a timestamp to ms, if it was in s, or keeps it as ms. + */ +function timestampToMs(timestamp) { + const isMs = timestamp > 9999999999; + return isMs ? timestamp : timestamp * 1000; +} + +/** This error indicates that the event buffer size exceeded the limit.. */ +class EventBufferSizeExceededError extends Error { + constructor() { + super(`Event buffer exceeded maximum size of ${REPLAY_MAX_EVENT_BUFFER_SIZE}.`); + } +} + +/** + * A basic event buffer that does not do any compression. + * Used as fallback if the compression worker cannot be loaded or is disabled. + */ +class EventBufferArray { + /** All the events that are buffered to be sent. */ + + __init() {this._totalSize = 0;} + + constructor() {EventBufferArray.prototype.__init.call(this); + this.events = []; + } + + /** @inheritdoc */ + get hasEvents() { + return this.events.length > 0; + } + + /** @inheritdoc */ + get type() { + return 'sync'; + } + + /** @inheritdoc */ + destroy() { + this.events = []; + } + + /** @inheritdoc */ + async addEvent(event) { + const eventSize = JSON.stringify(event).length; + this._totalSize += eventSize; + if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) { + throw new EventBufferSizeExceededError(); + } + + this.events.push(event); + } + + /** @inheritdoc */ + finish() { + return new Promise(resolve => { + // Make a copy of the events array reference and immediately clear the + // events member so that we do not lose new events while uploading + // attachment. + const eventsRet = this.events; + this.clear(); + resolve(JSON.stringify(eventsRet)); + }); + } + + /** @inheritdoc */ + clear() { + this.events = []; + this._totalSize = 0; + } + + /** @inheritdoc */ + getEarliestTimestamp() { + const timestamp = this.events.map(event => event.timestamp).sort()[0]; + + if (!timestamp) { + return null; + } + + return timestampToMs(timestamp); + } +} + +/** + * Event buffer that uses a web worker to compress events. + * Exported only for testing. + */ +class WorkerHandler { + + constructor(worker) { + this._worker = worker; + this._id = 0; + } + + /** + * Ensure the worker is ready (or not). + * This will either resolve when the worker is ready, or reject if an error occured. + */ + ensureReady() { + // Ensure we only check once + if (this._ensureReadyPromise) { + return this._ensureReadyPromise; + } + + this._ensureReadyPromise = new Promise((resolve, reject) => { + this._worker.addEventListener( + 'message', + ({ data }) => { + if ((data ).success) { + resolve(); + } else { + reject(); + } + }, + { once: true }, + ); + + this._worker.addEventListener( + 'error', + error => { + reject(error); + }, + { once: true }, + ); + }); + + return this._ensureReadyPromise; + } + + /** + * Destroy the worker. + */ + destroy() { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Destroying compression worker'); + this._worker.terminate(); + } + + /** + * Post message to worker and wait for response before resolving promise. + */ + postMessage(method, arg) { + const id = this._getAndIncrementId(); + + return new Promise((resolve, reject) => { + const listener = ({ data }) => { + const response = data ; + if (response.method !== method) { + return; + } + + // There can be multiple listeners for a single method, the id ensures + // that the response matches the caller. + if (response.id !== id) { + return; + } + + // At this point, we'll always want to remove listener regardless of result status + this._worker.removeEventListener('message', listener); + + if (!response.success) { + // TODO: Do some error handling, not sure what + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay]', response.response); + + reject(new Error('Error in compression worker')); + return; + } + + resolve(response.response ); + }; + + // Note: we can't use `once` option because it's possible it needs to + // listen to multiple messages + this._worker.addEventListener('message', listener); + this._worker.postMessage({ id, method, arg }); + }); + } + + /** Get the current ID and increment it for the next call. */ + _getAndIncrementId() { + return this._id++; + } +} + +/** + * Event buffer that uses a web worker to compress events. + * Exported only for testing. + */ +class EventBufferCompressionWorker { + + __init() {this._totalSize = 0;} + + constructor(worker) {EventBufferCompressionWorker.prototype.__init.call(this); + this._worker = new WorkerHandler(worker); + this._earliestTimestamp = null; + } + + /** @inheritdoc */ + get hasEvents() { + return !!this._earliestTimestamp; + } + + /** @inheritdoc */ + get type() { + return 'worker'; + } + + /** + * Ensure the worker is ready (or not). + * This will either resolve when the worker is ready, or reject if an error occured. + */ + ensureReady() { + return this._worker.ensureReady(); + } + + /** + * Destroy the event buffer. + */ + destroy() { + this._worker.destroy(); + } + + /** + * Add an event to the event buffer. + * + * Returns true if event was successfuly received and processed by worker. + */ + addEvent(event) { + const timestamp = timestampToMs(event.timestamp); + if (!this._earliestTimestamp || timestamp < this._earliestTimestamp) { + this._earliestTimestamp = timestamp; + } + + const data = JSON.stringify(event); + this._totalSize += data.length; + + if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) { + return Promise.reject(new EventBufferSizeExceededError()); + } + + return this._sendEventToWorker(data); + } + + /** + * Finish the event buffer and return the compressed data. + */ + finish() { + return this._finishRequest(); + } + + /** @inheritdoc */ + clear() { + this._earliestTimestamp = null; + this._totalSize = 0; + // We do not wait on this, as we assume the order of messages is consistent for the worker + void this._worker.postMessage('clear'); + } + + /** @inheritdoc */ + getEarliestTimestamp() { + return this._earliestTimestamp; + } + + /** + * Send the event to the worker. + */ + _sendEventToWorker(data) { + return this._worker.postMessage('addEvent', data); + } + + /** + * Finish the request and return the compressed data from the worker. + */ + async _finishRequest() { + const response = await this._worker.postMessage('finish'); + + this._earliestTimestamp = null; + this._totalSize = 0; + + return response; + } +} + +/** + * This proxy will try to use the compression worker, and fall back to use the simple buffer if an error occurs there. + * This can happen e.g. if the worker cannot be loaded. + * Exported only for testing. + */ +class EventBufferProxy { + + constructor(worker) { + this._fallback = new EventBufferArray(); + this._compression = new EventBufferCompressionWorker(worker); + this._used = this._fallback; + + this._ensureWorkerIsLoadedPromise = this._ensureWorkerIsLoaded(); + } + + /** @inheritdoc */ + get type() { + return this._used.type; + } + + /** @inheritDoc */ + get hasEvents() { + return this._used.hasEvents; + } + + /** @inheritDoc */ + destroy() { + this._fallback.destroy(); + this._compression.destroy(); + } + + /** @inheritdoc */ + clear() { + return this._used.clear(); + } + + /** @inheritdoc */ + getEarliestTimestamp() { + return this._used.getEarliestTimestamp(); + } + + /** + * Add an event to the event buffer. + * + * Returns true if event was successfully added. + */ + addEvent(event) { + return this._used.addEvent(event); + } + + /** @inheritDoc */ + async finish() { + // Ensure the worker is loaded, so the sent event is compressed + await this.ensureWorkerIsLoaded(); + + return this._used.finish(); + } + + /** Ensure the worker has loaded. */ + ensureWorkerIsLoaded() { + return this._ensureWorkerIsLoadedPromise; + } + + /** Actually check if the worker has been loaded. */ + async _ensureWorkerIsLoaded() { + try { + await this._compression.ensureReady(); + } catch (error) { + // If the worker fails to load, we fall back to the simple buffer. + // Nothing more to do from our side here + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Failed to load the compression worker, falling back to simple buffer'); + return; + } + + // Now we need to switch over the array buffer to the compression worker + await this._switchToCompressionWorker(); + } + + /** Switch the used buffer to the compression worker. */ + async _switchToCompressionWorker() { + const { events } = this._fallback; + + const addEventPromises = []; + for (const event of events) { + addEventPromises.push(this._compression.addEvent(event)); + } + + // We switch over to the new buffer immediately - any further events will be added + // after the previously buffered ones + this._used = this._compression; + + // Wait for original events to be re-added before resolving + try { + await Promise.all(addEventPromises); + } catch (error) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn('[Replay] Failed to add events when switching buffers.', error); + } + } +} + +/** + * Create an event buffer for replays. + */ +function createEventBuffer({ useCompression }) { + // eslint-disable-next-line no-restricted-globals + if (useCompression && window.Worker) { + try { + const workerUrl = e(); + + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Using compression worker'); + const worker = new Worker(workerUrl); + return new EventBufferProxy(worker); + } catch (error) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Failed to create compression worker'); + // Fall back to use simple event buffer array + } + } + + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Using simple buffer'); + return new EventBufferArray(); +} + +/** If sessionStorage is available. */ +function hasSessionStorage() { + return 'sessionStorage' in WINDOW && !!WINDOW.sessionStorage; +} + +/** + * Removes the session from Session Storage and unsets session in replay instance + */ +function clearSession(replay) { + deleteSession(); + replay.session = undefined; +} + +/** + * Deletes a session from storage + */ +function deleteSession() { + if (!hasSessionStorage()) { + return; + } + + try { + WINDOW.sessionStorage.removeItem(REPLAY_SESSION_KEY); + } catch (e) { + // Ignore potential SecurityError exceptions + } +} + +/** + * Given an initial timestamp and an expiry duration, checks to see if current + * time should be considered as expired. + */ +function isExpired( + initialTime, + expiry, + targetTime = +new Date(), +) { + // Always expired if < 0 + if (initialTime === null || expiry === undefined || expiry < 0) { + return true; + } + + // Never expires if == 0 + if (expiry === 0) { + return false; + } + + return initialTime + expiry <= targetTime; +} + +/** + * Checks to see if session is expired + */ +function isSessionExpired(session, timeouts, targetTime = +new Date()) { + return ( + // First, check that maximum session length has not been exceeded + isExpired(session.started, timeouts.maxSessionLife, targetTime) || + // check that the idle timeout has not been exceeded (i.e. user has + // performed an action within the last `sessionIdleExpire` ms) + isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime) + ); +} + +/** + * Given a sample rate, returns true if replay should be sampled. + * + * 1.0 = 100% sampling + * 0.0 = 0% sampling + */ +function isSampled(sampleRate) { + if (sampleRate === undefined) { + return false; + } + + // Math.random() returns a number in range of 0 to 1 (inclusive of 0, but not 1) + return Math.random() < sampleRate; +} + +/** + * Save a session to session storage. + */ +function saveSession(session) { + if (!hasSessionStorage()) { + return; + } + + try { + WINDOW.sessionStorage.setItem(REPLAY_SESSION_KEY, JSON.stringify(session)); + } catch (e) { + // Ignore potential SecurityError exceptions + } +} + +/** + * Get a session with defaults & applied sampling. + */ +function makeSession(session) { + const now = Date.now(); + const id = session.id || uuid4(); + // Note that this means we cannot set a started/lastActivity of `0`, but this should not be relevant outside of tests. + const started = session.started || now; + const lastActivity = session.lastActivity || now; + const segmentId = session.segmentId || 0; + const sampled = session.sampled; + + return { + id, + started, + lastActivity, + segmentId, + sampled, + shouldRefresh: true, + }; +} + +/** + * Get the sampled status for a session based on sample rates & current sampled status. + */ +function getSessionSampleType(sessionSampleRate, allowBuffering) { + return isSampled(sessionSampleRate) ? 'session' : allowBuffering ? 'buffer' : false; +} + +/** + * Create a new session, which in its current implementation is a Sentry event + * that all replays will be saved to as attachments. Currently, we only expect + * one of these Sentry events per "replay session". + */ +function createSession({ sessionSampleRate, allowBuffering, stickySession = false }) { + const sampled = getSessionSampleType(sessionSampleRate, allowBuffering); + const session = makeSession({ + sampled, + }); + + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log(`[Replay] Creating new session: ${session.id}`); + + if (stickySession) { + saveSession(session); + } + + return session; +} + +/** + * Fetches a session from storage + */ +function fetchSession() { + if (!hasSessionStorage()) { + return null; + } + + try { + // This can throw if cookies are disabled + const sessionStringFromStorage = WINDOW.sessionStorage.getItem(REPLAY_SESSION_KEY); + + if (!sessionStringFromStorage) { + return null; + } + + const sessionObj = JSON.parse(sessionStringFromStorage) ; + + return makeSession(sessionObj); + } catch (e) { + return null; + } +} + +/** + * Get or create a session + */ +function getSession({ + timeouts, + currentSession, + stickySession, + sessionSampleRate, + allowBuffering, +}) { + // If session exists and is passed, use it instead of always hitting session storage + const session = currentSession || (stickySession && fetchSession()); + + if (session) { + // If there is a session, check if it is valid (e.g. "last activity" time + // should be within the "session idle time", and "session started" time is + // within "max session time"). + const isExpired = isSessionExpired(session, timeouts); + + if (!isExpired || (allowBuffering && session.shouldRefresh)) { + return { type: 'saved', session }; + } else if (!session.shouldRefresh) { + // This is the case if we have an error session that is completed + // (=triggered an error). Session will continue as session-based replay, + // and when this session is expired, it will not be renewed until user + // reloads. + const discardedSession = makeSession({ sampled: false }); + return { type: 'new', session: discardedSession }; + } else { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Session has expired'); + } + // Otherwise continue to create a new session + } + + const newSession = createSession({ + stickySession, + sessionSampleRate, + allowBuffering, + }); + + return { type: 'new', session: newSession }; +} + +function isCustomEvent(event) { + return event.type === EventType.Custom; +} + +/** + * Add an event to the event buffer. + * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`. + */ +async function addEvent( + replay, + event, + isCheckout, +) { + if (!replay.eventBuffer) { + // This implies that `_isEnabled` is false + return null; + } + + if (replay.isPaused()) { + // Do not add to event buffer when recording is paused + return null; + } + + const timestampInMs = timestampToMs(event.timestamp); + + // Throw out events that happen more than 5 minutes ago. This can happen if + // page has been left open and idle for a long period of time and user + // comes back to trigger a new session. The performance entries rely on + // `performance.timeOrigin`, which is when the page first opened. + if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) { + return null; + } + + try { + if (isCheckout) { + replay.eventBuffer.clear(); + } + + const replayOptions = replay.getOptions(); + + const eventAfterPossibleCallback = + typeof replayOptions.beforeAddRecordingEvent === 'function' && isCustomEvent(event) + ? replayOptions.beforeAddRecordingEvent(event) + : event; + + if (!eventAfterPossibleCallback) { + return; + } + + return await replay.eventBuffer.addEvent(eventAfterPossibleCallback); + } catch (error) { + const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent'; + + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(error); + await replay.stop(reason); + + const client = getCurrentHub().getClient(); + + if (client) { + client.recordDroppedEvent('internal_sdk_error', 'replay'); + } + } +} + +/** If the event is an error event */ +function isErrorEvent(event) { + return !event.type; +} + +/** If the event is a transaction event */ +function isTransactionEvent(event) { + return event.type === 'transaction'; +} + +/** If the event is an replay event */ +function isReplayEvent(event) { + return event.type === 'replay_event'; +} + +/** + * Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`. + */ +function handleAfterSendEvent(replay) { + // Custom transports may still be returning `Promise`, which means we cannot expect the status code to be available there + // TODO (v8): remove this check as it will no longer be necessary + const enforceStatusCode = isBaseTransportSend(); + + return (event, sendResponse) => { + if (!isErrorEvent(event) && !isTransactionEvent(event)) { + return; + } + + const statusCode = sendResponse && sendResponse.statusCode; + + // We only want to do stuff on successful error sending, otherwise you get error replays without errors attached + // If not using the base transport, we allow `undefined` response (as a custom transport may not implement this correctly yet) + // If we do use the base transport, we skip if we encountered an non-OK status code + if (enforceStatusCode && (!statusCode || statusCode < 200 || statusCode >= 300)) { + return; + } + + // Collect traceIds in _context regardless of `recordingMode` + // In error mode, _context gets cleared on every checkout + if (isTransactionEvent(event) && event.contexts && event.contexts.trace && event.contexts.trace.trace_id) { + replay.getContext().traceIds.add(event.contexts.trace.trace_id ); + return; + } + + // Everything below is just for error events + if (!isErrorEvent(event)) { + return; + } + + // Add error to list of errorIds of replay. This is ok to do even if not + // sampled because context will get reset at next checkout. + // XXX: There is also a race condition where it's possible to capture an + // error to Sentry before Replay SDK has loaded, but response returns after + // it was loaded, and this gets called. + if (event.event_id) { + replay.getContext().errorIds.add(event.event_id); + } + + // If error event is tagged with replay id it means it was sampled (when in buffer mode) + // Need to be very careful that this does not cause an infinite loop + if (replay.recordingMode === 'buffer' && event.tags && event.tags.replayId) { + setTimeout(() => { + // Capture current event buffer as new replay + void replay.sendBufferedReplayOrFlush(); + }); + } + }; +} + +function isBaseTransportSend() { + const client = getCurrentHub().getClient(); + if (!client) { + return false; + } + + const transport = client.getTransport(); + if (!transport) { + return false; + } + + return ( + (transport.send ).__sentry__baseTransport__ || false + ); +} + +/** + * Returns true if we think the given event is an error originating inside of rrweb. + */ +function isRrwebError(event, hint) { + if (event.type || !event.exception || !event.exception.values || !event.exception.values.length) { + return false; + } + + // @ts-ignore this may be set by rrweb when it finds errors + if (hint.originalException && hint.originalException.__rrweb__) { + return true; + } + + // Check if any exception originates from rrweb + return event.exception.values.some(exception => { + if (!exception.stacktrace || !exception.stacktrace.frames || !exception.stacktrace.frames.length) { + return false; + } + + return exception.stacktrace.frames.some(frame => frame.filename && frame.filename.includes('/rrweb/src/')); + }); +} + +/** + * Determine if event should be sampled (only applies in buffer mode). + * When an event is captured by `hanldleGlobalEvent`, when in buffer mode + * we determine if we want to sample the error or not. + */ +function shouldSampleForBufferEvent(replay, event) { + if (replay.recordingMode !== 'buffer') { + return false; + } + + // ignore this error because otherwise we could loop indefinitely with + // trying to capture replay and failing + if (event.message === UNABLE_TO_SEND_REPLAY) { + return false; + } + + // Require the event to be an error event & to have an exception + if (!event.exception || event.type) { + return false; + } + + return isSampled(replay.getOptions().errorSampleRate); +} + +/** + * Returns a listener to be added to `addGlobalEventProcessor(listener)`. + */ +function handleGlobalEventListener( + replay, + includeAfterSendEventHandling = false, +) { + const afterSendHandler = includeAfterSendEventHandling ? handleAfterSendEvent(replay) : undefined; + + return (event, hint) => { + if (isReplayEvent(event)) { + // Replays have separate set of breadcrumbs, do not include breadcrumbs + // from core SDK + delete event.breadcrumbs; + return event; + } + + // We only want to handle errors & transactions, nothing else + if (!isErrorEvent(event) && !isTransactionEvent(event)) { + return event; + } + + // Unless `captureExceptions` is enabled, we want to ignore errors coming from rrweb + // As there can be a bunch of stuff going wrong in internals there, that we don't want to bubble up to users + if (isRrwebError(event, hint) && !replay.getOptions()._experiments.captureExceptions) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Ignoring error from rrweb internals', event); + return null; + } + + // When in buffer mode, we decide to sample here. + // Later, in `handleAfterSendEvent`, if the replayId is set, we know that we sampled + // And convert the buffer session to a full session + const isErrorEventSampled = shouldSampleForBufferEvent(replay, event); + + // Tag errors if it has been sampled in buffer mode, or if it is session mode + // Only tag transactions if in session mode + const shouldTagReplayId = isErrorEventSampled || replay.recordingMode === 'session'; + + if (shouldTagReplayId) { + event.tags = { ...event.tags, replayId: replay.getSessionId() }; + } + + // In cases where a custom client is used that does not support the new hooks (yet), + // we manually call this hook method here + if (afterSendHandler) { + // Pretend the error had a 200 response so we always capture it + afterSendHandler(event, { statusCode: 200 }); + } + + return event; + }; +} + +/** + * Create a "span" for each performance entry. + */ +function createPerformanceSpans( + replay, + entries, +) { + return entries.map(({ type, start, end, name, data }) => { + const response = replay.throttledAddEvent({ + type: EventType.Custom, + timestamp: start, + data: { + tag: 'performanceSpan', + payload: { + op: type, + description: name, + startTimestamp: start, + endTimestamp: end, + data, + }, + }, + }); + + // If response is a string, it means its either THROTTLED or SKIPPED + return typeof response === 'string' ? Promise.resolve(null) : response; + }); +} + +function handleHistory(handlerData) { + const { from, to } = handlerData; + + const now = Date.now() / 1000; + + return { + type: 'navigation.push', + start: now, + end: now, + name: to, + data: { + previous: from, + }, + }; +} + +/** + * Returns a listener to be added to `addInstrumentationHandler('history', listener)`. + */ +function handleHistorySpanListener(replay) { + return (handlerData) => { + if (!replay.isEnabled()) { + return; + } + + const result = handleHistory(handlerData); + + if (result === null) { + return; + } + + // Need to collect visited URLs + replay.getContext().urls.push(result.name); + replay.triggerUserActivity(); + + replay.addUpdate(() => { + createPerformanceSpans(replay, [result]); + // Returning false to flush + return false; + }); + }; +} + +/** + * Check whether a given request URL should be filtered out. This is so we + * don't log Sentry ingest requests. + */ +function shouldFilterRequest(replay, url) { + // If we enabled the `traceInternals` experiment, we want to trace everything + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && replay.getOptions()._experiments.traceInternals) { + return false; + } + + return _isSentryRequest(url); +} + +/** + * Checks wether a given URL belongs to the configured Sentry DSN. + */ +function _isSentryRequest(url) { + const client = getCurrentHub().getClient(); + const dsn = client && client.getDsn(); + return dsn ? url.includes(dsn.host) : false; +} + +/** Add a performance entry breadcrumb */ +function addNetworkBreadcrumb( + replay, + result, +) { + if (!replay.isEnabled()) { + return; + } + + if (result === null) { + return; + } + + if (shouldFilterRequest(replay, result.name)) { + return; + } + + replay.addUpdate(() => { + createPerformanceSpans(replay, [result]); + // Returning true will cause `addUpdate` to not flush + // We do not want network requests to cause a flush. This will prevent + // recurring/polling requests from keeping the replay session alive. + return true; + }); +} + +/** only exported for tests */ +function handleFetch(handlerData) { + const { startTimestamp, endTimestamp, fetchData, response } = handlerData; + + if (!endTimestamp) { + return null; + } + + // This is only used as a fallback, so we know the body sizes are never set here + const { method, url } = fetchData; + + return { + type: 'resource.fetch', + start: startTimestamp / 1000, + end: endTimestamp / 1000, + name: url, + data: { + method, + statusCode: response ? (response ).status : undefined, + }, + }; +} + +/** + * Returns a listener to be added to `addInstrumentationHandler('fetch', listener)`. + */ +function handleFetchSpanListener(replay) { + return (handlerData) => { + if (!replay.isEnabled()) { + return; + } + + const result = handleFetch(handlerData); + + addNetworkBreadcrumb(replay, result); + }; +} + +/** only exported for tests */ +function handleXhr(handlerData) { + const { startTimestamp, endTimestamp, xhr } = handlerData; + + const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY]; + + if (!startTimestamp || !endTimestamp || !sentryXhrData) { + return null; + } + + // This is only used as a fallback, so we know the body sizes are never set here + const { method, url, status_code: statusCode } = sentryXhrData; + + if (url === undefined) { + return null; + } + + return { + type: 'resource.xhr', + name: url, + start: startTimestamp / 1000, + end: endTimestamp / 1000, + data: { + method, + statusCode, + }, + }; +} + +/** + * Returns a listener to be added to `addInstrumentationHandler('xhr', listener)`. + */ +function handleXhrSpanListener(replay) { + return (handlerData) => { + if (!replay.isEnabled()) { + return; + } + + const result = handleXhr(handlerData); + + addNetworkBreadcrumb(replay, result); + }; +} + +const OBJ = 10; +const OBJ_KEY = 11; +const OBJ_KEY_STR = 12; +const OBJ_VAL = 13; +const OBJ_VAL_STR = 14; +const OBJ_VAL_COMPLETED = 15; + +const ARR = 20; +const ARR_VAL = 21; +const ARR_VAL_STR = 22; +const ARR_VAL_COMPLETED = 23; + +const ALLOWED_PRIMITIVES = ['true', 'false', 'null']; + +/** + * Complete an incomplete JSON string. + * This will ensure that the last element always has a `"~~"` to indicate it was truncated. + * For example, `[1,2,` will be completed to `[1,2,"~~"]` + * and `{"aa":"b` will be completed to `{"aa":"b~~"}` + */ +function completeJson(incompleteJson, stack) { + if (!stack.length) { + return incompleteJson; + } + + let json = incompleteJson; + + // Most checks are only needed for the last step in the stack + const lastPos = stack.length - 1; + const lastStep = stack[lastPos]; + + json = _fixLastStep(json, lastStep); + + // Complete remaining steps - just add closing brackets + for (let i = lastPos; i >= 0; i--) { + const step = stack[i]; + + switch (step) { + case OBJ: + json = `${json}}`; + break; + case ARR: + json = `${json}]`; + break; + } + } + + return json; +} + +function _fixLastStep(json, lastStep) { + switch (lastStep) { + // Object cases + case OBJ: + return `${json}"~~":"~~"`; + case OBJ_KEY: + return `${json}:"~~"`; + case OBJ_KEY_STR: + return `${json}~~":"~~"`; + case OBJ_VAL: + return _maybeFixIncompleteObjValue(json); + case OBJ_VAL_STR: + return `${json}~~"`; + case OBJ_VAL_COMPLETED: + return `${json},"~~":"~~"`; + + // Array cases + case ARR: + return `${json}"~~"`; + case ARR_VAL: + return _maybeFixIncompleteArrValue(json); + case ARR_VAL_STR: + return `${json}~~"`; + case ARR_VAL_COMPLETED: + return `${json},"~~"`; + } + + return json; +} + +function _maybeFixIncompleteArrValue(json) { + const pos = _findLastArrayDelimiter(json); + + if (pos > -1) { + const part = json.slice(pos + 1); + + if (ALLOWED_PRIMITIVES.includes(part.trim())) { + return `${json},"~~"`; + } + + // Everything else is replaced with `"~~"` + return `${json.slice(0, pos + 1)}"~~"`; + } + + // fallback, this shouldn't happen, to be save + return json; +} + +function _findLastArrayDelimiter(json) { + for (let i = json.length - 1; i >= 0; i--) { + const char = json[i]; + + if (char === ',' || char === '[') { + return i; + } + } + + return -1; +} + +function _maybeFixIncompleteObjValue(json) { + const startPos = json.lastIndexOf(':'); + + const part = json.slice(startPos + 1); + + if (ALLOWED_PRIMITIVES.includes(part.trim())) { + return `${json},"~~":"~~"`; + } + + // Everything else is replaced with `"~~"` + // This also means we do not have incomplete numbers, e.g `[1` is replaced with `["~~"]` + return `${json.slice(0, startPos + 1)}"~~"`; +} + +/** + * Evaluate an (incomplete) JSON string. + */ +function evaluateJson(json) { + const stack = []; + + for (let pos = 0; pos < json.length; pos++) { + _evaluateJsonPos(stack, json, pos); + } + + return stack; +} + +function _evaluateJsonPos(stack, json, pos) { + const curStep = stack[stack.length - 1]; + + const char = json[pos]; + + const whitespaceRegex = /\s/; + + if (whitespaceRegex.test(char)) { + return; + } + + if (char === '"' && !_isEscaped(json, pos)) { + _handleQuote(stack, curStep); + return; + } + + switch (char) { + case '{': + _handleObj(stack, curStep); + break; + case '[': + _handleArr(stack, curStep); + break; + case ':': + _handleColon(stack, curStep); + break; + case ',': + _handleComma(stack, curStep); + break; + case '}': + _handleObjClose(stack, curStep); + break; + case ']': + _handleArrClose(stack, curStep); + break; + } +} + +function _handleQuote(stack, curStep) { + // End of obj value + if (curStep === OBJ_VAL_STR) { + stack.pop(); + stack.push(OBJ_VAL_COMPLETED); + return; + } + + // End of arr value + if (curStep === ARR_VAL_STR) { + stack.pop(); + stack.push(ARR_VAL_COMPLETED); + return; + } + + // Start of obj value + if (curStep === OBJ_VAL) { + stack.push(OBJ_VAL_STR); + return; + } + + // Start of arr value + if (curStep === ARR_VAL) { + stack.push(ARR_VAL_STR); + return; + } + + // Start of obj key + if (curStep === OBJ) { + stack.push(OBJ_KEY_STR); + return; + } + + // End of obj key + if (curStep === OBJ_KEY_STR) { + stack.pop(); + stack.push(OBJ_KEY); + return; + } +} + +function _handleObj(stack, curStep) { + // Initial object + if (!curStep) { + stack.push(OBJ); + return; + } + + // New object as obj value + if (curStep === OBJ_VAL) { + stack.push(OBJ); + return; + } + + // New object as array element + if (curStep === ARR_VAL) { + stack.push(OBJ); + } + + // New object as first array element + if (curStep === ARR) { + stack.push(OBJ); + return; + } +} + +function _handleArr(stack, curStep) { + // Initial array + if (!curStep) { + stack.push(ARR); + stack.push(ARR_VAL); + return; + } + + // New array as obj value + if (curStep === OBJ_VAL) { + stack.push(ARR); + stack.push(ARR_VAL); + return; + } + + // New array as array element + if (curStep === ARR_VAL) { + stack.push(ARR); + stack.push(ARR_VAL); + } + + // New array as first array element + if (curStep === ARR) { + stack.push(ARR); + stack.push(ARR_VAL); + return; + } +} + +function _handleColon(stack, curStep) { + if (curStep === OBJ_KEY) { + stack.pop(); + stack.push(OBJ_VAL); + } +} + +function _handleComma(stack, curStep) { + // Comma after obj value + if (curStep === OBJ_VAL) { + stack.pop(); + return; + } + if (curStep === OBJ_VAL_COMPLETED) { + // Pop OBJ_VAL_COMPLETED & OBJ_VAL + stack.pop(); + stack.pop(); + return; + } + + // Comma after arr value + if (curStep === ARR_VAL) { + // do nothing - basically we'd pop ARR_VAL but add it right back + return; + } + + if (curStep === ARR_VAL_COMPLETED) { + // Pop ARR_VAL_COMPLETED + stack.pop(); + + // basically we'd pop ARR_VAL but add it right back + return; + } +} + +function _handleObjClose(stack, curStep) { + // Empty object {} + if (curStep === OBJ) { + stack.pop(); + } + + // Object with element + if (curStep === OBJ_VAL) { + // Pop OBJ_VAL, OBJ + stack.pop(); + stack.pop(); + } + + // Obj with element + if (curStep === OBJ_VAL_COMPLETED) { + // Pop OBJ_VAL_COMPLETED, OBJ_VAL, OBJ + stack.pop(); + stack.pop(); + stack.pop(); + } + + // if was obj value, complete it + if (stack[stack.length - 1] === OBJ_VAL) { + stack.push(OBJ_VAL_COMPLETED); + } + + // if was arr value, complete it + if (stack[stack.length - 1] === ARR_VAL) { + stack.push(ARR_VAL_COMPLETED); + } +} + +function _handleArrClose(stack, curStep) { + // Empty array [] + if (curStep === ARR) { + stack.pop(); + } + + // Array with element + if (curStep === ARR_VAL) { + // Pop ARR_VAL, ARR + stack.pop(); + stack.pop(); + } + + // Array with element + if (curStep === ARR_VAL_COMPLETED) { + // Pop ARR_VAL_COMPLETED, ARR_VAL, ARR + stack.pop(); + stack.pop(); + stack.pop(); + } + + // if was obj value, complete it + if (stack[stack.length - 1] === OBJ_VAL) { + stack.push(OBJ_VAL_COMPLETED); + } + + // if was arr value, complete it + if (stack[stack.length - 1] === ARR_VAL) { + stack.push(ARR_VAL_COMPLETED); + } +} + +function _isEscaped(str, pos) { + const previousChar = str[pos - 1]; + + return previousChar === '\\' && !_isEscaped(str, pos - 1); +} + +/* eslint-disable max-lines */ + +/** + * Takes an incomplete JSON string, and returns a hopefully valid JSON string. + * Note that this _can_ fail, so you should check the return value is valid JSON. + */ +function fixJson(incompleteJson) { + const stack = evaluateJson(incompleteJson); + + return completeJson(incompleteJson, stack); +} + +/** Get the size of a body. */ +function getBodySize( + body, + textEncoder, +) { + if (!body) { + return undefined; + } + + try { + if (typeof body === 'string') { + return textEncoder.encode(body).length; + } + + if (body instanceof URLSearchParams) { + return textEncoder.encode(body.toString()).length; + } + + if (body instanceof FormData) { + const formDataStr = _serializeFormData(body); + return textEncoder.encode(formDataStr).length; + } + + if (body instanceof Blob) { + return body.size; + } + + if (body instanceof ArrayBuffer) { + return body.byteLength; + } + + // Currently unhandled types: ArrayBufferView, ReadableStream + } catch (e) { + // just return undefined + } + + return undefined; +} + +/** Convert a Content-Length header to number/undefined. */ +function parseContentLengthHeader(header) { + if (!header) { + return undefined; + } + + const size = parseInt(header, 10); + return isNaN(size) ? undefined : size; +} + +/** Get the string representation of a body. */ +function getBodyString(body) { + if (typeof body === 'string') { + return body; + } + + if (body instanceof URLSearchParams) { + return body.toString(); + } + + if (body instanceof FormData) { + return _serializeFormData(body); + } + + return undefined; +} + +/** Convert ReplayNetworkRequestData to a PerformanceEntry. */ +function makeNetworkReplayBreadcrumb( + type, + data, +) { + if (!data) { + return null; + } + + const { startTimestamp, endTimestamp, url, method, statusCode, request, response } = data; + + const result = { + type, + start: startTimestamp / 1000, + end: endTimestamp / 1000, + name: url, + data: dropUndefinedKeys({ + method, + statusCode, + request, + response, + }), + }; + + return result; +} + +/** Build the request or response part of a replay network breadcrumb that was skipped. */ +function buildSkippedNetworkRequestOrResponse(bodySize) { + return { + headers: {}, + size: bodySize, + _meta: { + warnings: ['URL_SKIPPED'], + }, + }; +} + +/** Build the request or response part of a replay network breadcrumb. */ +function buildNetworkRequestOrResponse( + headers, + bodySize, + body, +) { + if (!bodySize && Object.keys(headers).length === 0) { + return undefined; + } + + if (!bodySize) { + return { + headers, + }; + } + + if (!body) { + return { + headers, + size: bodySize, + }; + } + + const info = { + headers, + size: bodySize, + }; + + const { body: normalizedBody, warnings } = normalizeNetworkBody(body); + info.body = normalizedBody; + if (warnings.length > 0) { + info._meta = { + warnings, + }; + } + + return info; +} + +/** Filter a set of headers */ +function getAllowedHeaders(headers, allowedHeaders) { + return Object.keys(headers).reduce((filteredHeaders, key) => { + const normalizedKey = key.toLowerCase(); + // Avoid putting empty strings into the headers + if (allowedHeaders.includes(normalizedKey) && headers[key]) { + filteredHeaders[normalizedKey] = headers[key]; + } + return filteredHeaders; + }, {}); +} + +function _serializeFormData(formData) { + // This is a bit simplified, but gives us a decent estimate + // This converts e.g. { name: 'Anne Smith', age: 13 } to 'name=Anne+Smith&age=13' + // @ts-ignore passing FormData to URLSearchParams actually works + return new URLSearchParams(formData).toString(); +} + +function normalizeNetworkBody(body) + + { + if (!body || typeof body !== 'string') { + return { + body, + warnings: [], + }; + } + + const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE; + + if (_strIsProbablyJson(body)) { + try { + const json = exceedsSizeLimit ? fixJson(body.slice(0, NETWORK_BODY_MAX_SIZE)) : body; + const normalizedBody = JSON.parse(json); + return { + body: normalizedBody, + warnings: exceedsSizeLimit ? ['JSON_TRUNCATED'] : [], + }; + } catch (e3) { + return { + body: exceedsSizeLimit ? `${body.slice(0, NETWORK_BODY_MAX_SIZE)}…` : body, + warnings: exceedsSizeLimit ? ['INVALID_JSON', 'TEXT_TRUNCATED'] : ['INVALID_JSON'], + }; + } + } + + return { + body: exceedsSizeLimit ? `${body.slice(0, NETWORK_BODY_MAX_SIZE)}…` : body, + warnings: exceedsSizeLimit ? ['TEXT_TRUNCATED'] : [], + }; +} + +function _strIsProbablyJson(str) { + const first = str[0]; + const last = str[str.length - 1]; + + // Simple check: If this does not start & end with {} or [], it's not JSON + return (first === '[' && last === ']') || (first === '{' && last === '}'); +} + +/** Match an URL against a list of strings/Regex. */ +function urlMatches(url, urls) { + const fullUrl = getFullUrl(url); + + return stringMatchesSomePattern(fullUrl, urls); +} + +/** exported for tests */ +function getFullUrl(url, baseURI = WINDOW.document.baseURI) { + // Short circuit for common cases: + if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith(WINDOW.location.origin)) { + return url; + } + const fixedUrl = new URL(url, baseURI); + + // If these do not match, we are not dealing with a relative URL, so just return it + if (fixedUrl.origin !== new URL(baseURI).origin) { + return url; + } + + const fullUrl = fixedUrl.href; + + // Remove trailing slashes, if they don't match the original URL + if (!url.endsWith('/') && fullUrl.endsWith('/')) { + return fullUrl.slice(0, -1); + } + + return fullUrl; +} + +/** + * Capture a fetch breadcrumb to a replay. + * This adds additional data (where approriate). + */ +async function captureFetchBreadcrumbToReplay( + breadcrumb, + hint, + options + +, +) { + try { + const data = await _prepareFetchData(breadcrumb, hint, options); + + // Create a replay performance entry from this breadcrumb + const result = makeNetworkReplayBreadcrumb('resource.fetch', data); + addNetworkBreadcrumb(options.replay, result); + } catch (error) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] Failed to capture fetch breadcrumb', error); + } +} + +/** + * Enrich a breadcrumb with additional data. + * This has to be sync & mutate the given breadcrumb, + * as the breadcrumb is afterwards consumed by other handlers. + */ +function enrichFetchBreadcrumb( + breadcrumb, + hint, + options, +) { + const { input, response } = hint; + + const body = _getFetchRequestArgBody(input); + const reqSize = getBodySize(body, options.textEncoder); + + const resSize = response ? parseContentLengthHeader(response.headers.get('content-length')) : undefined; + + if (reqSize !== undefined) { + breadcrumb.data.request_body_size = reqSize; + } + if (resSize !== undefined) { + breadcrumb.data.response_body_size = resSize; + } +} + +async function _prepareFetchData( + breadcrumb, + hint, + options + +, +) { + const { startTimestamp, endTimestamp } = hint; + + const { + url, + method, + status_code: statusCode = 0, + request_body_size: requestBodySize, + response_body_size: responseBodySize, + } = breadcrumb.data; + + const captureDetails = urlMatches(url, options.networkDetailAllowUrls); + + const request = captureDetails + ? _getRequestInfo(options, hint.input, requestBodySize) + : buildSkippedNetworkRequestOrResponse(requestBodySize); + const response = await _getResponseInfo(captureDetails, options, hint.response, responseBodySize); + + return { + startTimestamp, + endTimestamp, + url, + method, + statusCode, + request, + response, + }; +} + +function _getRequestInfo( + { networkCaptureBodies, networkRequestHeaders }, + input, + requestBodySize, +) { + const headers = getRequestHeaders(input, networkRequestHeaders); + + if (!networkCaptureBodies) { + return buildNetworkRequestOrResponse(headers, requestBodySize, undefined); + } + + // We only want to transmit string or string-like bodies + const requestBody = _getFetchRequestArgBody(input); + const bodyStr = getBodyString(requestBody); + return buildNetworkRequestOrResponse(headers, requestBodySize, bodyStr); +} + +async function _getResponseInfo( + captureDetails, + { + networkCaptureBodies, + textEncoder, + networkResponseHeaders, + } + +, + response, + responseBodySize, +) { + if (!captureDetails && responseBodySize !== undefined) { + return buildSkippedNetworkRequestOrResponse(responseBodySize); + } + + const headers = getAllHeaders(response.headers, networkResponseHeaders); + + if (!networkCaptureBodies && responseBodySize !== undefined) { + return buildNetworkRequestOrResponse(headers, responseBodySize, undefined); + } + + // Only clone the response if we need to + try { + // We have to clone this, as the body can only be read once + const res = response.clone(); + const bodyText = await _parseFetchBody(res); + + const size = + bodyText && bodyText.length && responseBodySize === undefined + ? getBodySize(bodyText, textEncoder) + : responseBodySize; + + if (!captureDetails) { + return buildSkippedNetworkRequestOrResponse(size); + } + + if (networkCaptureBodies) { + return buildNetworkRequestOrResponse(headers, size, bodyText); + } + + return buildNetworkRequestOrResponse(headers, size, undefined); + } catch (e) { + // fallback + return buildNetworkRequestOrResponse(headers, responseBodySize, undefined); + } +} + +async function _parseFetchBody(response) { + try { + return await response.text(); + } catch (e2) { + return undefined; + } +} + +function _getFetchRequestArgBody(fetchArgs = []) { + // We only support getting the body from the fetch options + if (fetchArgs.length !== 2 || typeof fetchArgs[1] !== 'object') { + return undefined; + } + + return (fetchArgs[1] ).body; +} + +function getAllHeaders(headers, allowedHeaders) { + const allHeaders = {}; + + allowedHeaders.forEach(header => { + if (headers.get(header)) { + allHeaders[header] = headers.get(header) ; + } + }); + + return allHeaders; +} + +function getRequestHeaders(fetchArgs, allowedHeaders) { + if (fetchArgs.length === 1 && typeof fetchArgs[0] !== 'string') { + return getHeadersFromOptions(fetchArgs[0] , allowedHeaders); + } + + if (fetchArgs.length === 2) { + return getHeadersFromOptions(fetchArgs[1] , allowedHeaders); + } + + return {}; +} + +function getHeadersFromOptions( + input, + allowedHeaders, +) { + if (!input) { + return {}; + } + + const headers = input.headers; + + if (!headers) { + return {}; + } + + if (headers instanceof Headers) { + return getAllHeaders(headers, allowedHeaders); + } + + // We do not support this, as it is not really documented (anymore?) + if (Array.isArray(headers)) { + return {}; + } + + return getAllowedHeaders(headers, allowedHeaders); +} + +/** + * Capture an XHR breadcrumb to a replay. + * This adds additional data (where approriate). + */ +async function captureXhrBreadcrumbToReplay( + breadcrumb, + hint, + options, +) { + try { + const data = _prepareXhrData(breadcrumb, hint, options); + + // Create a replay performance entry from this breadcrumb + const result = makeNetworkReplayBreadcrumb('resource.xhr', data); + addNetworkBreadcrumb(options.replay, result); + } catch (error) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] Failed to capture fetch breadcrumb', error); + } +} + +/** + * Enrich a breadcrumb with additional data. + * This has to be sync & mutate the given breadcrumb, + * as the breadcrumb is afterwards consumed by other handlers. + */ +function enrichXhrBreadcrumb( + breadcrumb, + hint, + options, +) { + const { xhr, input } = hint; + + const reqSize = getBodySize(input, options.textEncoder); + const resSize = xhr.getResponseHeader('content-length') + ? parseContentLengthHeader(xhr.getResponseHeader('content-length')) + : getBodySize(xhr.response, options.textEncoder); + + if (reqSize !== undefined) { + breadcrumb.data.request_body_size = reqSize; + } + if (resSize !== undefined) { + breadcrumb.data.response_body_size = resSize; + } +} + +function _prepareXhrData( + breadcrumb, + hint, + options, +) { + const { startTimestamp, endTimestamp, input, xhr } = hint; + + const { + url, + method, + status_code: statusCode = 0, + request_body_size: requestBodySize, + response_body_size: responseBodySize, + } = breadcrumb.data; + + if (!url) { + return null; + } + + if (!urlMatches(url, options.networkDetailAllowUrls)) { + const request = buildSkippedNetworkRequestOrResponse(requestBodySize); + const response = buildSkippedNetworkRequestOrResponse(responseBodySize); + return { + startTimestamp, + endTimestamp, + url, + method, + statusCode, + request, + response, + }; + } + + const xhrInfo = xhr[SENTRY_XHR_DATA_KEY]; + const networkRequestHeaders = xhrInfo + ? getAllowedHeaders(xhrInfo.request_headers, options.networkRequestHeaders) + : {}; + const networkResponseHeaders = getAllowedHeaders(getResponseHeaders(xhr), options.networkResponseHeaders); + + const request = buildNetworkRequestOrResponse( + networkRequestHeaders, + requestBodySize, + options.networkCaptureBodies ? getBodyString(input) : undefined, + ); + const response = buildNetworkRequestOrResponse( + networkResponseHeaders, + responseBodySize, + options.networkCaptureBodies ? hint.xhr.responseText : undefined, + ); + + return { + startTimestamp, + endTimestamp, + url, + method, + statusCode, + request, + response, + }; +} + +function getResponseHeaders(xhr) { + const headers = xhr.getAllResponseHeaders(); + + if (!headers) { + return {}; + } + + return headers.split('\r\n').reduce((acc, line) => { + const [key, value] = line.split(': '); + acc[key.toLowerCase()] = value; + return acc; + }, {}); +} + +/** + * This method does two things: + * - It enriches the regular XHR/fetch breadcrumbs with request/response size data + * - It captures the XHR/fetch breadcrumbs to the replay + * (enriching it with further data that is _not_ added to the regular breadcrumbs) + */ +function handleNetworkBreadcrumbs(replay) { + const client = getCurrentHub().getClient(); + + try { + const textEncoder = new TextEncoder(); + + const { networkDetailAllowUrls, networkCaptureBodies, networkRequestHeaders, networkResponseHeaders } = + replay.getOptions(); + + const options = { + replay, + textEncoder, + networkDetailAllowUrls, + networkCaptureBodies, + networkRequestHeaders, + networkResponseHeaders, + }; + + if (client && client.on) { + client.on('beforeAddBreadcrumb', (breadcrumb, hint) => beforeAddNetworkBreadcrumb(options, breadcrumb, hint)); + } else { + // Fallback behavior + addInstrumentationHandler('fetch', handleFetchSpanListener(replay)); + addInstrumentationHandler('xhr', handleXhrSpanListener(replay)); + } + } catch (e2) { + // Do nothing + } +} + +/** just exported for tests */ +function beforeAddNetworkBreadcrumb( + options, + breadcrumb, + hint, +) { + if (!breadcrumb.data) { + return; + } + + try { + if (_isXhrBreadcrumb(breadcrumb) && _isXhrHint(hint)) { + // This has to be sync, as we need to ensure the breadcrumb is enriched in the same tick + // Because the hook runs synchronously, and the breadcrumb is afterwards passed on + // So any async mutations to it will not be reflected in the final breadcrumb + enrichXhrBreadcrumb(breadcrumb, hint, options); + + void captureXhrBreadcrumbToReplay(breadcrumb, hint, options); + } + + if (_isFetchBreadcrumb(breadcrumb) && _isFetchHint(hint)) { + // This has to be sync, as we need to ensure the breadcrumb is enriched in the same tick + // Because the hook runs synchronously, and the breadcrumb is afterwards passed on + // So any async mutations to it will not be reflected in the final breadcrumb + enrichFetchBreadcrumb(breadcrumb, hint, options); + + void captureFetchBreadcrumbToReplay(breadcrumb, hint, options); + } + } catch (e) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn('Error when enriching network breadcrumb'); + } +} + +function _isXhrBreadcrumb(breadcrumb) { + return breadcrumb.category === 'xhr'; +} + +function _isFetchBreadcrumb(breadcrumb) { + return breadcrumb.category === 'fetch'; +} + +function _isXhrHint(hint) { + return hint && hint.xhr; +} + +function _isFetchHint(hint) { + return hint && hint.response; +} + +let _LAST_BREADCRUMB = null; + +function isBreadcrumbWithCategory(breadcrumb) { + return !!breadcrumb.category; +} + +const handleScopeListener = + (replay) => + (scope) => { + if (!replay.isEnabled()) { + return; + } + + const result = handleScope(scope); + + if (!result) { + return; + } + + addBreadcrumbEvent(replay, result); + }; + +/** + * An event handler to handle scope changes. + */ +function handleScope(scope) { + // TODO (v8): Remove this guard. This was put in place because we introduced + // Scope.getLastBreadcrumb mid-v7 which caused incompatibilities with older SDKs. + // For now, we'll just return null if the method doesn't exist but we should eventually + // get rid of this guard. + const newBreadcrumb = scope.getLastBreadcrumb && scope.getLastBreadcrumb(); + + // Listener can be called when breadcrumbs have not changed, so we store the + // reference to the last crumb and only return a crumb if it has changed + if (_LAST_BREADCRUMB === newBreadcrumb || !newBreadcrumb) { + return null; + } + + _LAST_BREADCRUMB = newBreadcrumb; + + if ( + !isBreadcrumbWithCategory(newBreadcrumb) || + ['fetch', 'xhr', 'sentry.event', 'sentry.transaction'].includes(newBreadcrumb.category) || + newBreadcrumb.category.startsWith('ui.') + ) { + return null; + } + + if (newBreadcrumb.category === 'console') { + return normalizeConsoleBreadcrumb(newBreadcrumb); + } + + return createBreadcrumb(newBreadcrumb); +} + +/** exported for tests only */ +function normalizeConsoleBreadcrumb( + breadcrumb, +) { + const args = breadcrumb.data && breadcrumb.data.arguments; + + if (!Array.isArray(args) || args.length === 0) { + return createBreadcrumb(breadcrumb); + } + + let isTruncated = false; + + // Avoid giant args captures + const normalizedArgs = args.map(arg => { + if (!arg) { + return arg; + } + if (typeof arg === 'string') { + if (arg.length > CONSOLE_ARG_MAX_SIZE) { + isTruncated = true; + return `${arg.slice(0, CONSOLE_ARG_MAX_SIZE)}…`; + } + + return arg; + } + if (typeof arg === 'object') { + try { + const normalizedArg = normalize(arg, 7); + const stringified = JSON.stringify(normalizedArg); + if (stringified.length > CONSOLE_ARG_MAX_SIZE) { + const fixedJson = fixJson(stringified.slice(0, CONSOLE_ARG_MAX_SIZE)); + const json = JSON.parse(fixedJson); + // We only set this after JSON.parse() was successfull, so we know we didn't run into `catch` + isTruncated = true; + return json; + } + return normalizedArg; + } catch (e) { + // fall back to default + } + } + + return arg; + }); + + return createBreadcrumb({ + ...breadcrumb, + data: { + ...breadcrumb.data, + arguments: normalizedArgs, + ...(isTruncated ? { _meta: { warnings: ['CONSOLE_ARG_TRUNCATED'] } } : {}), + }, + }); +} + +/** + * Add global listeners that cannot be removed. + */ +function addGlobalListeners(replay) { + // Listeners from core SDK // + const scope = getCurrentHub().getScope(); + const client = getCurrentHub().getClient(); + + if (scope) { + scope.addScopeListener(handleScopeListener(replay)); + } + addInstrumentationHandler('dom', handleDomListener(replay)); + addInstrumentationHandler('history', handleHistorySpanListener(replay)); + handleNetworkBreadcrumbs(replay); + + // Tag all (non replay) events that get sent to Sentry with the current + // replay ID so that we can reference them later in the UI + addGlobalEventProcessor(handleGlobalEventListener(replay, !hasHooks(client))); + + // If a custom client has no hooks yet, we continue to use the "old" implementation + if (hasHooks(client)) { + client.on('afterSendEvent', handleAfterSendEvent(replay)); + client.on('createDsc', (dsc) => { + const replayId = replay.getSessionId(); + // We do not want to set the DSC when in buffer mode, as that means the replay has not been sent (yet) + if (replayId && replay.isEnabled() && replay.recordingMode === 'session') { + dsc.replay_id = replayId; + } + }); + + client.on('startTransaction', transaction => { + replay.lastTransaction = transaction; + }); + + // We may be missing the initial startTransaction due to timing issues, + // so we capture it on finish again. + client.on('finishTransaction', transaction => { + replay.lastTransaction = transaction; + }); + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function hasHooks(client) { + return !!(client && client.on); +} + +/** + * Create a "span" for the total amount of memory being used by JS objects + * (including v8 internal objects). + */ +async function addMemoryEntry(replay) { + // window.performance.memory is a non-standard API and doesn't work on all browsers, so we try-catch this + try { + return Promise.all( + createPerformanceSpans(replay, [ + // @ts-ignore memory doesn't exist on type Performance as the API is non-standard (we check that it exists above) + createMemoryEntry(WINDOW.performance.memory), + ]), + ); + } catch (error) { + // Do nothing + return []; + } +} + +function createMemoryEntry(memoryEntry) { + const { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize } = memoryEntry; + // we don't want to use `getAbsoluteTime` because it adds the event time to the + // time origin, so we get the current timestamp instead + const time = Date.now() / 1000; + return { + type: 'memory', + name: 'memory', + start: time, + end: time, + data: { + memory: { + jsHeapSizeLimit, + totalJSHeapSize, + usedJSHeapSize, + }, + }, + }; +} + +// Map entryType -> function to normalize data for event +// @ts-ignore TODO: entry type does not fit the create* functions entry type +const ENTRY_TYPES + + = { + // @ts-ignore TODO: entry type does not fit the create* functions entry type + resource: createResourceEntry, + paint: createPaintEntry, + // @ts-ignore TODO: entry type does not fit the create* functions entry type + navigation: createNavigationEntry, + // @ts-ignore TODO: entry type does not fit the create* functions entry type + ['largest-contentful-paint']: createLargestContentfulPaint, +}; + +/** + * Create replay performance entries from the browser performance entries. + */ +function createPerformanceEntries( + entries, +) { + return entries.map(createPerformanceEntry).filter(Boolean) ; +} + +function createPerformanceEntry(entry) { + if (ENTRY_TYPES[entry.entryType] === undefined) { + return null; + } + + return ENTRY_TYPES[entry.entryType](entry); +} + +function getAbsoluteTime(time) { + // browserPerformanceTimeOrigin can be undefined if `performance` or + // `performance.now` doesn't exist, but this is already checked by this integration + return ((browserPerformanceTimeOrigin || WINDOW.performance.timeOrigin) + time) / 1000; +} + +function createPaintEntry(entry) { + const { duration, entryType, name, startTime } = entry; + + const start = getAbsoluteTime(startTime); + return { + type: entryType, + name, + start, + end: start + duration, + data: undefined, + }; +} + +function createNavigationEntry(entry) { + const { + entryType, + name, + decodedBodySize, + duration, + domComplete, + encodedBodySize, + domContentLoadedEventStart, + domContentLoadedEventEnd, + domInteractive, + loadEventStart, + loadEventEnd, + redirectCount, + startTime, + transferSize, + type, + } = entry; + + // Ignore entries with no duration, they do not seem to be useful and cause dupes + if (duration === 0) { + return null; + } + + return { + type: `${entryType}.${type}`, + start: getAbsoluteTime(startTime), + end: getAbsoluteTime(domComplete), + name, + data: { + size: transferSize, + decodedBodySize, + encodedBodySize, + duration, + domInteractive, + domContentLoadedEventStart, + domContentLoadedEventEnd, + loadEventStart, + loadEventEnd, + domComplete, + redirectCount, + }, + }; +} + +function createResourceEntry( + entry, +) { + const { + entryType, + initiatorType, + name, + responseEnd, + startTime, + decodedBodySize, + encodedBodySize, + responseStatus, + transferSize, + } = entry; + + // Core SDK handles these + if (['fetch', 'xmlhttprequest'].includes(initiatorType)) { + return null; + } + + return { + type: `${entryType}.${initiatorType}`, + start: getAbsoluteTime(startTime), + end: getAbsoluteTime(responseEnd), + name, + data: { + size: transferSize, + statusCode: responseStatus, + decodedBodySize, + encodedBodySize, + }, + }; +} + +function createLargestContentfulPaint( + entry, +) { + const { entryType, startTime, size } = entry; + + let startTimeOrNavigationActivation = 0; + + if (WINDOW.performance) { + const navEntry = WINDOW.performance.getEntriesByType('navigation')[0] + +; + + // See https://github.com/GoogleChrome/web-vitals/blob/9f11c4c6578fb4c5ee6fa4e32b9d1d756475f135/src/lib/getActivationStart.ts#L21 + startTimeOrNavigationActivation = (navEntry && navEntry.activationStart) || 0; + } + + // value is in ms + const value = Math.max(startTime - startTimeOrNavigationActivation, 0); + // LCP doesn't have a "duration", it just happens at a single point in time. + // But the UI expects both, so use end (in seconds) for both timestamps. + const end = getAbsoluteTime(startTimeOrNavigationActivation) + value / 1000; + + return { + type: entryType, + name: entryType, + start: end, + end, + data: { + value, // LCP "duration" in ms + size, + // Not sure why this errors, Node should be correct (Argument of type 'Node' is not assignable to parameter of type 'INode') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + nodeId: record.mirror.getId(entry.element ), + }, + }; +} + +/** + * Heavily simplified debounce function based on lodash.debounce. + * + * This function takes a callback function (@param fun) and delays its invocation + * by @param wait milliseconds. Optionally, a maxWait can be specified in @param options, + * which ensures that the callback is invoked at least once after the specified max. wait time. + * + * @param func the function whose invocation is to be debounced + * @param wait the minimum time until the function is invoked after it was called once + * @param options the options object, which can contain the `maxWait` property + * + * @returns the debounced version of the function, which needs to be called at least once to start the + * debouncing process. Subsequent calls will reset the debouncing timer and, in case @paramfunc + * was already invoked in the meantime, return @param func's return value. + * The debounced function has two additional properties: + * - `flush`: Invokes the debounced function immediately and returns its return value + * - `cancel`: Cancels the debouncing process and resets the debouncing timer + */ +function debounce(func, wait, options) { + let callbackReturnValue; + + let timerId; + let maxTimerId; + + const maxWait = options && options.maxWait ? Math.max(options.maxWait, wait) : 0; + + function invokeFunc() { + cancelTimers(); + callbackReturnValue = func(); + return callbackReturnValue; + } + + function cancelTimers() { + timerId !== undefined && clearTimeout(timerId); + maxTimerId !== undefined && clearTimeout(maxTimerId); + timerId = maxTimerId = undefined; + } + + function flush() { + if (timerId !== undefined || maxTimerId !== undefined) { + return invokeFunc(); + } + return callbackReturnValue; + } + + function debounced() { + if (timerId) { + clearTimeout(timerId); + } + timerId = setTimeout(invokeFunc, wait); + + if (maxWait && maxTimerId === undefined) { + maxTimerId = setTimeout(invokeFunc, maxWait); + } + + return callbackReturnValue; + } + + debounced.cancel = cancelTimers; + debounced.flush = flush; + return debounced; +} + +/** + * Handler for recording events. + * + * Adds to event buffer, and has varying flushing behaviors if the event was a checkout. + */ +function getHandleRecordingEmit(replay) { + let hadFirstEvent = false; + + return (event, _isCheckout) => { + // If this is false, it means session is expired, create and a new session and wait for checkout + if (!replay.checkAndHandleExpiredSession()) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn('[Replay] Received replay event after session expired.'); + + return; + } + + // `_isCheckout` is only set when the checkout is due to `checkoutEveryNms` + // We also want to treat the first event as a checkout, so we handle this specifically here + const isCheckout = _isCheckout || !hadFirstEvent; + hadFirstEvent = true; + + // The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush. + replay.addUpdate(() => { + // The session is always started immediately on pageload/init, but for + // error-only replays, it should reflect the most recent checkout + // when an error occurs. Clear any state that happens before this current + // checkout. This needs to happen before `addEvent()` which updates state + // dependent on this reset. + if (replay.recordingMode === 'buffer' && isCheckout) { + replay.setInitialState(); + } + + // We need to clear existing events on a checkout, otherwise they are + // incremental event updates and should be appended + void addEvent(replay, event, isCheckout); + + // Different behavior for full snapshots (type=2), ignore other event types + // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16 + if (!isCheckout) { + return false; + } + + // Additionally, create a meta event that will capture certain SDK settings. + // In order to handle buffer mode, this needs to either be done when we + // receive checkout events or at flush time. + // + // `isCheckout` is always true, but want to be explicit that it should + // only be added for checkouts + void addSettingsEvent(replay, isCheckout); + + // If there is a previousSessionId after a full snapshot occurs, then + // the replay session was started due to session expiration. The new session + // is started before triggering a new checkout and contains the id + // of the previous session. Do not immediately flush in this case + // to avoid capturing only the checkout and instead the replay will + // be captured if they perform any follow-up actions. + if (replay.session && replay.session.previousSessionId) { + return true; + } + + // When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer + // this should usually be the timestamp of the checkout event, but to be safe... + if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) { + const earliestEvent = replay.eventBuffer.getEarliestTimestamp(); + if (earliestEvent) { + replay.session.started = earliestEvent; + + if (replay.getOptions().stickySession) { + saveSession(replay.session); + } + } + } + + if (replay.recordingMode === 'session') { + // If the full snapshot is due to an initial load, we will not have + // a previous session ID. In this case, we want to buffer events + // for a set amount of time before flushing. This can help avoid + // capturing replays of users that immediately close the window. + void replay.flush(); + } + + return true; + }); + }; +} + +/** + * Exported for tests + */ +function createOptionsEvent(replay) { + const options = replay.getOptions(); + return { + type: EventType.Custom, + timestamp: Date.now(), + data: { + tag: 'options', + payload: { + sessionSampleRate: options.sessionSampleRate, + errorSampleRate: options.errorSampleRate, + useCompressionOption: options.useCompression, + blockAllMedia: options.blockAllMedia, + maskAllText: options.maskAllText, + maskAllInputs: options.maskAllInputs, + useCompression: replay.eventBuffer ? replay.eventBuffer.type === 'worker' : false, + networkDetailHasUrls: options.networkDetailAllowUrls.length > 0, + networkCaptureBodies: options.networkCaptureBodies, + networkRequestHasHeaders: options.networkRequestHeaders.length > 0, + networkResponseHasHeaders: options.networkResponseHeaders.length > 0, + }, + }, + }; +} + +/** + * Add a "meta" event that contains a simplified view on current configuration + * options. This should only be included on the first segment of a recording. + */ +function addSettingsEvent(replay, isCheckout) { + // Only need to add this event when sending the first segment + if (!isCheckout || !replay.session || replay.session.segmentId !== 0) { + return Promise.resolve(null); + } + + return addEvent(replay, createOptionsEvent(replay), false); +} + +/** + * Create a replay envelope ready to be sent. + * This includes both the replay event, as well as the recording data. + */ +function createReplayEnvelope( + replayEvent, + recordingData, + dsn, + tunnel, +) { + return createEnvelope( + createEventEnvelopeHeaders(replayEvent, getSdkMetadataForEnvelopeHeader(replayEvent), tunnel, dsn), + [ + [{ type: 'replay_event' }, replayEvent], + [ + { + type: 'replay_recording', + // If string then we need to encode to UTF8, otherwise will have + // wrong size. TextEncoder has similar browser support to + // MutationObserver, although it does not accept IE11. + length: + typeof recordingData === 'string' ? new TextEncoder().encode(recordingData).length : recordingData.length, + }, + recordingData, + ], + ], + ); +} + +/** + * Prepare the recording data ready to be sent. + */ +function prepareRecordingData({ + recordingData, + headers, +} + +) { + let payloadWithSequence; + + // XXX: newline is needed to separate sequence id from events + const replayHeaders = `${JSON.stringify(headers)} +`; + + if (typeof recordingData === 'string') { + payloadWithSequence = `${replayHeaders}${recordingData}`; + } else { + const enc = new TextEncoder(); + // XXX: newline is needed to separate sequence id from events + const sequence = enc.encode(replayHeaders); + // Merge the two Uint8Arrays + payloadWithSequence = new Uint8Array(sequence.length + recordingData.length); + payloadWithSequence.set(sequence); + payloadWithSequence.set(recordingData, sequence.length); + } + + return payloadWithSequence; +} + +/** + * Prepare a replay event & enrich it with the SDK metadata. + */ +async function prepareReplayEvent({ + client, + scope, + replayId: event_id, + event, +} + +) { + const integrations = + typeof client._integrations === 'object' && client._integrations !== null && !Array.isArray(client._integrations) + ? Object.keys(client._integrations) + : undefined; + const preparedEvent = (await prepareEvent( + client.getOptions(), + event, + { event_id, integrations }, + scope, + )) ; + + // If e.g. a global event processor returned null + if (!preparedEvent) { + return null; + } + + // This normally happens in browser client "_prepareEvent" + // but since we do not use this private method from the client, but rather the plain import + // we need to do this manually. + preparedEvent.platform = preparedEvent.platform || 'javascript'; + + // extract the SDK name because `client._prepareEvent` doesn't add it to the event + const metadata = client.getSdkMetadata && client.getSdkMetadata(); + const { name, version } = (metadata && metadata.sdk) || {}; + + preparedEvent.sdk = { + ...preparedEvent.sdk, + name: name || 'sentry.javascript.unknown', + version: version || '0.0.0', + }; + + return preparedEvent; +} + +/** + * Send replay attachment using `fetch()` + */ +async function sendReplayRequest({ + recordingData, + replayId, + segmentId: segment_id, + eventContext, + timestamp, + session, +}) { + const preparedRecordingData = prepareRecordingData({ + recordingData, + headers: { + segment_id, + }, + }); + + const { urls, errorIds, traceIds, initialTimestamp } = eventContext; + + const hub = getCurrentHub(); + const client = hub.getClient(); + const scope = hub.getScope(); + const transport = client && client.getTransport(); + const dsn = client && client.getDsn(); + + if (!client || !transport || !dsn || !session.sampled) { + return; + } + + const baseEvent = { + type: REPLAY_EVENT_NAME, + replay_start_timestamp: initialTimestamp / 1000, + timestamp: timestamp / 1000, + error_ids: errorIds, + trace_ids: traceIds, + urls, + replay_id: replayId, + segment_id, + replay_type: session.sampled, + }; + + const replayEvent = await prepareReplayEvent({ scope, client, replayId, event: baseEvent }); + + if (!replayEvent) { + // Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions + client.recordDroppedEvent('event_processor', 'replay', baseEvent); + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('An event processor returned `null`, will not send event.'); + return; + } + + /* + For reference, the fully built event looks something like this: + { + "type": "replay_event", + "timestamp": 1670837008.634, + "error_ids": [ + "errorId" + ], + "trace_ids": [ + "traceId" + ], + "urls": [ + "https://example.com" + ], + "replay_id": "eventId", + "segment_id": 3, + "replay_type": "error", + "platform": "javascript", + "event_id": "eventId", + "environment": "production", + "sdk": { + "integrations": [ + "BrowserTracing", + "Replay" + ], + "name": "sentry.javascript.browser", + "version": "7.25.0" + }, + "sdkProcessingMetadata": {}, + "contexts": { + }, + } + */ + + const envelope = createReplayEnvelope(replayEvent, preparedRecordingData, dsn, client.getOptions().tunnel); + + let response; + + try { + response = await transport.send(envelope); + } catch (err) { + const error = new Error(UNABLE_TO_SEND_REPLAY); + + try { + // In case browsers don't allow this property to be writable + // @ts-ignore This needs lib es2022 and newer + error.cause = err; + } catch (e) { + // nothing to do + } + throw error; + } + + // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore + if (!response) { + return response; + } + + // If the status code is invalid, we want to immediately stop & not retry + if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) { + throw new TransportStatusCodeError(response.statusCode); + } + + return response; +} + +/** + * This error indicates that the transport returned an invalid status code. + */ +class TransportStatusCodeError extends Error { + constructor(statusCode) { + super(`Transport returned status code ${statusCode}`); + } +} + +/** + * Finalize and send the current replay event to Sentry + */ +async function sendReplay( + replayData, + retryConfig = { + count: 0, + interval: RETRY_BASE_INTERVAL, + }, +) { + const { recordingData, options } = replayData; + + // short circuit if there's no events to upload (this shouldn't happen as _runFlush makes this check) + if (!recordingData.length) { + return; + } + + try { + await sendReplayRequest(replayData); + return true; + } catch (err) { + if (err instanceof TransportStatusCodeError) { + throw err; + } + + // Capture error for every failed replay + setContext('Replays', { + _retryCount: retryConfig.count, + }); + + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && options._experiments && options._experiments.captureExceptions) { + captureException(err); + } + + // If an error happened here, it's likely that uploading the attachment + // failed, we'll can retry with the same events payload + if (retryConfig.count >= RETRY_MAX_COUNT) { + const error = new Error(`${UNABLE_TO_SEND_REPLAY} - max retries exceeded`); + + try { + // In case browsers don't allow this property to be writable + // @ts-ignore This needs lib es2022 and newer + error.cause = err; + } catch (e) { + // nothing to do + } + + throw error; + } + + // will retry in intervals of 5, 10, 30 + retryConfig.interval *= ++retryConfig.count; + + return new Promise((resolve, reject) => { + setTimeout(async () => { + try { + await sendReplay(replayData, retryConfig); + resolve(true); + } catch (err) { + reject(err); + } + }, retryConfig.interval); + }); + } +} + +const THROTTLED = '__THROTTLED'; +const SKIPPED = '__SKIPPED'; + +/** + * Create a throttled function off a given function. + * When calling the throttled function, it will call the original function only + * if it hasn't been called more than `maxCount` times in the last `durationSeconds`. + * + * Returns `THROTTLED` if throttled for the first time, after that `SKIPPED`, + * or else the return value of the original function. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function throttle( + fn, + maxCount, + durationSeconds, +) { + const counter = new Map(); + + const _cleanup = (now) => { + const threshold = now - durationSeconds; + counter.forEach((_value, key) => { + if (key < threshold) { + counter.delete(key); + } + }); + }; + + const _getTotalCount = () => { + return [...counter.values()].reduce((a, b) => a + b, 0); + }; + + let isThrottled = false; + + return (...rest) => { + // Date in second-precision, which we use as basis for the throttling + const now = Math.floor(Date.now() / 1000); + + // First, make sure to delete any old entries + _cleanup(now); + + // If already over limit, do nothing + if (_getTotalCount() >= maxCount) { + const wasThrottled = isThrottled; + isThrottled = true; + return wasThrottled ? SKIPPED : THROTTLED; + } + + isThrottled = false; + const count = counter.get(now) || 0; + counter.set(now, count + 1); + + return fn(...rest); + }; +} + +/* eslint-disable max-lines */ // TODO: We might want to split this file up + +/** + * The main replay container class, which holds all the state and methods for recording and sending replays. + */ +class ReplayContainer { + __init() {this.eventBuffer = null;} + + /** + * List of PerformanceEntry from PerformanceObserver + */ + __init2() {this.performanceEvents = [];} + + /** + * Recording can happen in one of three modes: + * - session: Record the whole session, sending it continuously + * - buffer: Always keep the last 60s of recording, requires: + * - having replaysOnErrorSampleRate > 0 to capture replay when an error occurs + * - or calling `flush()` to send the replay + */ + __init3() {this.recordingMode = 'session';} + + /** + * The current or last active transcation. + * This is only available when performance is enabled. + */ + + /** + * These are here so we can overwrite them in tests etc. + * @hidden + */ + __init4() {this.timeouts = { + sessionIdlePause: SESSION_IDLE_PAUSE_DURATION, + sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION, + maxSessionLife: MAX_SESSION_LIFE, + }; } + + /** + * Options to pass to `rrweb.record()` + */ + + __init5() {this._performanceObserver = null;} + + __init6() {this._flushLock = null;} + + /** + * Timestamp of the last user activity. This lives across sessions. + */ + __init7() {this._lastActivity = Date.now();} + + /** + * Is the integration currently active? + */ + __init8() {this._isEnabled = false;} + + /** + * Paused is a state where: + * - DOM Recording is not listening at all + * - Nothing will be added to event buffer (e.g. core SDK events) + */ + __init9() {this._isPaused = false;} + + /** + * Have we attached listeners to the core SDK? + * Note we have to track this as there is no way to remove instrumentation handlers. + */ + __init10() {this._hasInitializedCoreListeners = false;} + + /** + * Function to stop recording + */ + __init11() {this._stopRecording = null;} + + __init12() {this._context = { + errorIds: new Set(), + traceIds: new Set(), + urls: [], + initialTimestamp: Date.now(), + initialUrl: '', + };} + + constructor({ + options, + recordingOptions, + } + +) {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); + this._recordingOptions = recordingOptions; + this._options = options; + + this._debouncedFlush = debounce(() => this._flush(), this._options.flushMinDelay, { + maxWait: this._options.flushMaxDelay, + }); + + this._throttledAddEvent = throttle( + (event, isCheckout) => addEvent(this, event, isCheckout), + // Max 300 events... + 300, + // ... per 5s + 5, + ); + + const { slowClickTimeout, slowClickIgnoreSelectors } = this.getOptions(); + + const slowClickConfig = slowClickTimeout + ? { + threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout), + timeout: slowClickTimeout, + scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT, + ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '', + multiClickTimeout: MULTI_CLICK_TIMEOUT, + } + : undefined; + + if (slowClickConfig) { + this.clickDetector = new ClickDetector(this, slowClickConfig); + } + } + + /** Get the event context. */ + getContext() { + return this._context; + } + + /** If recording is currently enabled. */ + isEnabled() { + return this._isEnabled; + } + + /** If recording is currently paused. */ + isPaused() { + return this._isPaused; + } + + /** Get the replay integration options. */ + getOptions() { + return this._options; + } + + /** + * Initializes the plugin based on sampling configuration. Should not be + * called outside of constructor. + */ + initializeSampling() { + const { errorSampleRate, sessionSampleRate } = this._options; + + // If neither sample rate is > 0, then do nothing - user will need to call one of + // `start()` or `startBuffering` themselves. + if (errorSampleRate <= 0 && sessionSampleRate <= 0) { + return; + } + + // Otherwise if there is _any_ sample rate set, try to load an existing + // session, or create a new one. + const isSessionSampled = this._loadAndCheckSession(); + + if (!isSessionSampled) { + // This should only occur if `errorSampleRate` is 0 and was unsampled for + // session-based replay. In this case there is nothing to do. + return; + } + + if (!this.session) { + // This should not happen, something wrong has occurred + this._handleException(new Error('Unable to initialize and create session')); + return; + } + + if (this.session.sampled && this.session.sampled !== 'session') { + // If not sampled as session-based, then recording mode will be `buffer` + // Note that we don't explicitly check if `sampled === 'buffer'` because we + // could have sessions from Session storage that are still `error` from + // prior SDK version. + this.recordingMode = 'buffer'; + } + + this._initializeRecording(); + } + + /** + * Start a replay regardless of sampling rate. Calling this will always + * create a new session. Will throw an error if replay is already in progress. + * + * Creates or loads a session, attaches listeners to varying events (DOM, + * _performanceObserver, Recording, Sentry SDK, etc) + */ + start() { + if (this._isEnabled && this.recordingMode === 'session') { + throw new Error('Replay recording is already in progress'); + } + + if (this._isEnabled && this.recordingMode === 'buffer') { + throw new Error('Replay buffering is in progress, call `flush()` to save the replay'); + } + + const previousSessionId = this.session && this.session.id; + + const { session } = getSession({ + timeouts: this.timeouts, + stickySession: Boolean(this._options.stickySession), + currentSession: this.session, + // This is intentional: create a new session-based replay when calling `start()` + sessionSampleRate: 1, + allowBuffering: false, + }); + + session.previousSessionId = previousSessionId; + this.session = session; + + this._initializeRecording(); + } + + /** + * Start replay buffering. Buffers until `flush()` is called or, if + * `replaysOnErrorSampleRate` > 0, an error occurs. + */ + startBuffering() { + if (this._isEnabled) { + throw new Error('Replay recording is already in progress'); + } + + const previousSessionId = this.session && this.session.id; + + const { session } = getSession({ + timeouts: this.timeouts, + stickySession: Boolean(this._options.stickySession), + currentSession: this.session, + sessionSampleRate: 0, + allowBuffering: true, + }); + + session.previousSessionId = previousSessionId; + this.session = session; + + this.recordingMode = 'buffer'; + this._initializeRecording(); + } + + /** + * Start recording. + * + * Note that this will cause a new DOM checkout + */ + startRecording() { + try { + this._stopRecording = record({ + ...this._recordingOptions, + // When running in error sampling mode, we need to overwrite `checkoutEveryNms` + // Without this, it would record forever, until an error happens, which we don't want + // instead, we'll always keep the last 60 seconds of replay before an error happened + ...(this.recordingMode === 'buffer' && { checkoutEveryNms: BUFFER_CHECKOUT_TIME }), + emit: getHandleRecordingEmit(this), + onMutation: this._onMutationHandler, + }); + } catch (err) { + this._handleException(err); + } + } + + /** + * Stops the recording, if it was running. + * + * Returns true if it was previously stopped, or is now stopped, + * otherwise false. + */ + stopRecording() { + try { + if (this._stopRecording) { + this._stopRecording(); + this._stopRecording = undefined; + } + + return true; + } catch (err) { + this._handleException(err); + return false; + } + } + + /** + * Currently, this needs to be manually called (e.g. for tests). Sentry SDK + * does not support a teardown + */ + async stop(reason) { + if (!this._isEnabled) { + return; + } + + try { + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) { + const msg = `[Replay] Stopping Replay${reason ? ` triggered by ${reason}` : ''}`; + + // When `traceInternals` is enabled, we want to log this to the console + // Else, use the regular debug output + // eslint-disable-next-line + const log = this.getOptions()._experiments.traceInternals ? console.warn : logger.log; + log(msg); + } + + // We can't move `_isEnabled` after awaiting a flush, otherwise we can + // enter into an infinite loop when `stop()` is called while flushing. + this._isEnabled = false; + this._removeListeners(); + this.stopRecording(); + + this._debouncedFlush.cancel(); + // See comment above re: `_isEnabled`, we "force" a flush, ignoring the + // `_isEnabled` state of the plugin since it was disabled above. + if (this.recordingMode === 'session') { + await this._flush({ force: true }); + } + + // After flush, destroy event buffer + this.eventBuffer && this.eventBuffer.destroy(); + this.eventBuffer = null; + + // Clear session from session storage, note this means if a new session + // is started after, it will not have `previousSessionId` + clearSession(this); + } catch (err) { + this._handleException(err); + } + } + + /** + * Pause some replay functionality. See comments for `_isPaused`. + * This differs from stop as this only stops DOM recording, it is + * not as thorough of a shutdown as `stop()`. + */ + pause() { + this._isPaused = true; + this.stopRecording(); + } + + /** + * Resumes recording, see notes for `pause(). + * + * Note that calling `startRecording()` here will cause a + * new DOM checkout.` + */ + resume() { + if (!this._loadAndCheckSession()) { + return; + } + + this._isPaused = false; + this.startRecording(); + } + + /** + * If not in "session" recording mode, flush event buffer which will create a new replay. + * Unless `continueRecording` is false, the replay will continue to record and + * behave as a "session"-based replay. + * + * Otherwise, queue up a flush. + */ + async sendBufferedReplayOrFlush({ continueRecording = true } = {}) { + if (this.recordingMode === 'session') { + return this.flushImmediate(); + } + + const activityTime = Date.now(); + + // Allow flush to complete before resuming as a session recording, otherwise + // the checkout from `startRecording` may be included in the payload. + // Prefer to keep the error replay as a separate (and smaller) segment + // than the session replay. + await this.flushImmediate(); + + const hasStoppedRecording = this.stopRecording(); + + if (!continueRecording || !hasStoppedRecording) { + return; + } + + // Re-start recording, but in "session" recording mode + + // Reset all "capture on error" configuration before + // starting a new recording + this.recordingMode = 'session'; + + // Once this session ends, we do not want to refresh it + if (this.session) { + this.session.shouldRefresh = false; + + // It's possible that the session lifespan is > max session lifespan + // because we have been buffering beyond max session lifespan (we ignore + // expiration given that `shouldRefresh` is true). Since we flip + // `shouldRefresh`, the session could be considered expired due to + // lifespan, which is not what we want. Update session start date to be + // the current timestamp, so that session is not considered to be + // expired. This means that max replay duration can be MAX_SESSION_LIFE + + // (length of buffer), which we are ok with. + this._updateUserActivity(activityTime); + this._updateSessionActivity(activityTime); + this.session.started = activityTime; + this._maybeSaveSession(); + } + + this.startRecording(); + } + + /** + * We want to batch uploads of replay events. Save events only if + * `` milliseconds have elapsed since the last event + * *OR* if `` milliseconds have elapsed. + * + * Accepts a callback to perform side-effects and returns true to stop batch + * processing and hand back control to caller. + */ + addUpdate(cb) { + // We need to always run `cb` (e.g. in the case of `this.recordingMode == 'buffer'`) + const cbResult = cb(); + + // If this option is turned on then we will only want to call `flush` + // explicitly + if (this.recordingMode === 'buffer') { + return; + } + + // If callback is true, we do not want to continue with flushing -- the + // caller will need to handle it. + if (cbResult === true) { + return; + } + + // addUpdate is called quite frequently - use _debouncedFlush so that it + // respects the flush delays and does not flush immediately + this._debouncedFlush(); + } + + /** + * Updates the user activity timestamp and resumes recording. This should be + * called in an event handler for a user action that we consider as the user + * being "active" (e.g. a mouse click). + */ + triggerUserActivity() { + this._updateUserActivity(); + + // This case means that recording was once stopped due to inactivity. + // Ensure that recording is resumed. + if (!this._stopRecording) { + // Create a new session, otherwise when the user action is flushed, it + // will get rejected due to an expired session. + if (!this._loadAndCheckSession()) { + return; + } + + // Note: This will cause a new DOM checkout + this.resume(); + return; + } + + // Otherwise... recording was never suspended, continue as normalish + this.checkAndHandleExpiredSession(); + + this._updateSessionActivity(); + } + + /** + * Updates the user activity timestamp *without* resuming + * recording. Some user events (e.g. keydown) can be create + * low-value replays that only contain the keypress as a + * breadcrumb. Instead this would require other events to + * create a new replay after a session has expired. + */ + updateUserActivity() { + this._updateUserActivity(); + this._updateSessionActivity(); + } + + /** + * Only flush if `this.recordingMode === 'session'` + */ + conditionalFlush() { + if (this.recordingMode === 'buffer') { + return Promise.resolve(); + } + + return this.flushImmediate(); + } + + /** + * Flush using debounce flush + */ + flush() { + return this._debouncedFlush() ; + } + + /** + * Always flush via `_debouncedFlush` so that we do not have flushes triggered + * from calling both `flush` and `_debouncedFlush`. Otherwise, there could be + * cases of mulitple flushes happening closely together. + */ + flushImmediate() { + this._debouncedFlush(); + // `.flush` is provided by the debounced function, analogously to lodash.debounce + return this._debouncedFlush.flush() ; + } + + /** + * Cancels queued up flushes. + */ + cancelFlush() { + this._debouncedFlush.cancel(); + } + + /** Get the current sesion (=replay) ID */ + getSessionId() { + return this.session && this.session.id; + } + + /** + * Checks if recording should be stopped due to user inactivity. Otherwise + * check if session is expired and create a new session if so. Triggers a new + * full snapshot on new session. + * + * Returns true if session is not expired, false otherwise. + * @hidden + */ + checkAndHandleExpiredSession() { + const oldSessionId = this.getSessionId(); + + // Prevent starting a new session if the last user activity is older than + // SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new + // session+recording. This creates noisy replays that do not have much + // content in them. + if ( + this._lastActivity && + isExpired(this._lastActivity, this.timeouts.sessionIdlePause) && + this.session && + this.session.sampled === 'session' + ) { + // Pause recording only for session-based replays. Otherwise, resuming + // will create a new replay and will conflict with users who only choose + // to record error-based replays only. (e.g. the resumed replay will not + // contain a reference to an error) + this.pause(); + return; + } + + // --- There is recent user activity --- // + // This will create a new session if expired, based on expiry length + if (!this._loadAndCheckSession()) { + return; + } + + // Session was expired if session ids do not match + const expired = oldSessionId !== this.getSessionId(); + + if (!expired) { + return true; + } + + // Session is expired, trigger a full snapshot (which will create a new session) + this._triggerFullSnapshot(); + + return false; + } + + /** + * Capture some initial state that can change throughout the lifespan of the + * replay. This is required because otherwise they would be captured at the + * first flush. + */ + setInitialState() { + const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`; + const url = `${WINDOW.location.origin}${urlPath}`; + + this.performanceEvents = []; + + // Reset _context as well + this._clearContext(); + + this._context.initialUrl = url; + this._context.initialTimestamp = Date.now(); + this._context.urls.push(url); + } + + /** + * Add a breadcrumb event, that may be throttled. + * If it was throttled, we add a custom breadcrumb to indicate that. + */ + throttledAddEvent( + event, + isCheckout, + ) { + const res = this._throttledAddEvent(event, isCheckout); + + // If this is THROTTLED, it means we have throttled the event for the first time + // In this case, we want to add a breadcrumb indicating that something was skipped + if (res === THROTTLED) { + const breadcrumb = createBreadcrumb({ + category: 'replay.throttled', + }); + + this.addUpdate(() => { + void addEvent(this, { + type: EventType.Custom, + timestamp: breadcrumb.timestamp || 0, + data: { + tag: 'breadcrumb', + payload: breadcrumb, + metric: true, + }, + }); + }); + } + + return res; + } + + /** + * This will get the parametrized route name of the current page. + * This is only available if performance is enabled, and if an instrumented router is used. + */ + getCurrentRoute() { + const lastTransaction = this.lastTransaction || getCurrentHub().getScope().getTransaction(); + if (!lastTransaction || !['route', 'custom'].includes(lastTransaction.metadata.source)) { + return undefined; + } + + return lastTransaction.name; + } + + /** + * Initialize and start all listeners to varying events (DOM, + * Performance Observer, Recording, Sentry SDK, etc) + */ + _initializeRecording() { + this.setInitialState(); + + // this method is generally called on page load or manually - in both cases + // we should treat it as an activity + this._updateSessionActivity(); + + this.eventBuffer = createEventBuffer({ + useCompression: this._options.useCompression, + }); + + this._removeListeners(); + this._addListeners(); + + // Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout + this._isEnabled = true; + + this.startRecording(); + } + + /** A wrapper to conditionally capture exceptions. */ + _handleException(error) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay]', error); + + if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && this._options._experiments && this._options._experiments.captureExceptions) { + captureException(error); + } + } + + /** + * Loads (or refreshes) the current session. + * Returns false if session is not recorded. + */ + _loadAndCheckSession() { + const { type, session } = getSession({ + timeouts: this.timeouts, + stickySession: Boolean(this._options.stickySession), + currentSession: this.session, + sessionSampleRate: this._options.sessionSampleRate, + allowBuffering: this._options.errorSampleRate > 0 || this.recordingMode === 'buffer', + }); + + // If session was newly created (i.e. was not loaded from storage), then + // enable flag to create the root replay + if (type === 'new') { + this.setInitialState(); + } + + const currentSessionId = this.getSessionId(); + if (session.id !== currentSessionId) { + session.previousSessionId = currentSessionId; + } + + this.session = session; + + if (!this.session.sampled) { + void this.stop('session unsampled'); + return false; + } + + return true; + } + + /** + * Adds listeners to record events for the replay + */ + _addListeners() { + try { + WINDOW.document.addEventListener('visibilitychange', this._handleVisibilityChange); + WINDOW.addEventListener('blur', this._handleWindowBlur); + WINDOW.addEventListener('focus', this._handleWindowFocus); + WINDOW.addEventListener('keydown', this._handleKeyboardEvent); + + if (this.clickDetector) { + this.clickDetector.addListeners(); + } + + // There is no way to remove these listeners, so ensure they are only added once + if (!this._hasInitializedCoreListeners) { + addGlobalListeners(this); + + this._hasInitializedCoreListeners = true; + } + } catch (err) { + this._handleException(err); + } + + // PerformanceObserver // + if (!('PerformanceObserver' in WINDOW)) { + return; + } + + this._performanceObserver = setupPerformanceObserver(this); + } + + /** + * Cleans up listeners that were created in `_addListeners` + */ + _removeListeners() { + try { + WINDOW.document.removeEventListener('visibilitychange', this._handleVisibilityChange); + + WINDOW.removeEventListener('blur', this._handleWindowBlur); + WINDOW.removeEventListener('focus', this._handleWindowFocus); + WINDOW.removeEventListener('keydown', this._handleKeyboardEvent); + + if (this.clickDetector) { + this.clickDetector.removeListeners(); + } + + if (this._performanceObserver) { + this._performanceObserver.disconnect(); + this._performanceObserver = null; + } + } catch (err) { + this._handleException(err); + } + } + + /** + * Handle when visibility of the page content changes. Opening a new tab will + * cause the state to change to hidden because of content of current page will + * be hidden. Likewise, moving a different window to cover the contents of the + * page will also trigger a change to a hidden state. + */ + __init13() {this._handleVisibilityChange = () => { + if (WINDOW.document.visibilityState === 'visible') { + this._doChangeToForegroundTasks(); + } else { + this._doChangeToBackgroundTasks(); + } + };} + + /** + * Handle when page is blurred + */ + __init14() {this._handleWindowBlur = () => { + const breadcrumb = createBreadcrumb({ + category: 'ui.blur', + }); + + // Do not count blur as a user action -- it's part of the process of them + // leaving the page + this._doChangeToBackgroundTasks(breadcrumb); + };} + + /** + * Handle when page is focused + */ + __init15() {this._handleWindowFocus = () => { + const breadcrumb = createBreadcrumb({ + category: 'ui.focus', + }); + + // Do not count focus as a user action -- instead wait until they focus and + // interactive with page + this._doChangeToForegroundTasks(breadcrumb); + };} + + /** Ensure page remains active when a key is pressed. */ + __init16() {this._handleKeyboardEvent = (event) => { + handleKeyboardEvent(this, event); + };} + + /** + * Tasks to run when we consider a page to be hidden (via blurring and/or visibility) + */ + _doChangeToBackgroundTasks(breadcrumb) { + if (!this.session) { + return; + } + + const expired = isSessionExpired(this.session, this.timeouts); + + if (breadcrumb && !expired) { + this._createCustomBreadcrumb(breadcrumb); + } + + // Send replay when the page/tab becomes hidden. There is no reason to send + // replay if it becomes visible, since no actions we care about were done + // while it was hidden + void this.conditionalFlush(); + } + + /** + * Tasks to run when we consider a page to be visible (via focus and/or visibility) + */ + _doChangeToForegroundTasks(breadcrumb) { + if (!this.session) { + return; + } + + const isSessionActive = this.checkAndHandleExpiredSession(); + + if (!isSessionActive) { + // If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION + // ms, we will re-use the existing session, otherwise create a new + // session + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Document has become active, but session has expired'); + return; + } + + if (breadcrumb) { + this._createCustomBreadcrumb(breadcrumb); + } + } + + /** + * Trigger rrweb to take a full snapshot which will cause this plugin to + * create a new Replay event. + */ + _triggerFullSnapshot(checkout = true) { + try { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Taking full rrweb snapshot'); + record.takeFullSnapshot(checkout); + } catch (err) { + this._handleException(err); + } + } + + /** + * Update user activity (across session lifespans) + */ + _updateUserActivity(_lastActivity = Date.now()) { + this._lastActivity = _lastActivity; + } + + /** + * Updates the session's last activity timestamp + */ + _updateSessionActivity(_lastActivity = Date.now()) { + if (this.session) { + this.session.lastActivity = _lastActivity; + this._maybeSaveSession(); + } + } + + /** + * Helper to create (and buffer) a replay breadcrumb from a core SDK breadcrumb + */ + _createCustomBreadcrumb(breadcrumb) { + this.addUpdate(() => { + void this.throttledAddEvent({ + type: EventType.Custom, + timestamp: breadcrumb.timestamp || 0, + data: { + tag: 'breadcrumb', + payload: breadcrumb, + }, + }); + }); + } + + /** + * Observed performance events are added to `this.performanceEvents`. These + * are included in the replay event before it is finished and sent to Sentry. + */ + _addPerformanceEntries() { + // Copy and reset entries before processing + const entries = [...this.performanceEvents]; + this.performanceEvents = []; + + return Promise.all(createPerformanceSpans(this, createPerformanceEntries(entries))); + } + + /** + * Clear _context + */ + _clearContext() { + // XXX: `initialTimestamp` and `initialUrl` do not get cleared + this._context.errorIds.clear(); + this._context.traceIds.clear(); + this._context.urls = []; + } + + /** Update the initial timestamp based on the buffer content. */ + _updateInitialTimestampFromEventBuffer() { + const { session, eventBuffer } = this; + if (!session || !eventBuffer) { + return; + } + + // we only ever update this on the initial segment + if (session.segmentId) { + return; + } + + const earliestEvent = eventBuffer.getEarliestTimestamp(); + if (earliestEvent && earliestEvent < this._context.initialTimestamp) { + this._context.initialTimestamp = earliestEvent; + } + } + + /** + * Return and clear _context + */ + _popEventContext() { + const _context = { + initialTimestamp: this._context.initialTimestamp, + initialUrl: this._context.initialUrl, + errorIds: Array.from(this._context.errorIds), + traceIds: Array.from(this._context.traceIds), + urls: this._context.urls, + }; + + this._clearContext(); + + return _context; + } + + /** + * Flushes replay event buffer to Sentry. + * + * Performance events are only added right before flushing - this is + * due to the buffered performance observer events. + * + * Should never be called directly, only by `flush` + */ + async _runFlush() { + if (!this.session || !this.eventBuffer) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] No session or eventBuffer found to flush.'); + return; + } + + await this._addPerformanceEntries(); + + // Check eventBuffer again, as it could have been stopped in the meanwhile + if (!this.eventBuffer || !this.eventBuffer.hasEvents) { + return; + } + + // Only attach memory event if eventBuffer is not empty + await addMemoryEntry(this); + + // Check eventBuffer again, as it could have been stopped in the meanwhile + if (!this.eventBuffer) { + return; + } + + try { + // This uses the data from the eventBuffer, so we need to call this before `finish() + this._updateInitialTimestampFromEventBuffer(); + + // Note this empties the event buffer regardless of outcome of sending replay + const recordingData = await this.eventBuffer.finish(); + + // NOTE: Copy values from instance members, as it's possible they could + // change before the flush finishes. + const replayId = this.session.id; + const eventContext = this._popEventContext(); + // Always increment segmentId regardless of outcome of sending replay + const segmentId = this.session.segmentId++; + this._maybeSaveSession(); + + await sendReplay({ + replayId, + recordingData, + segmentId, + eventContext, + session: this.session, + options: this.getOptions(), + timestamp: Date.now(), + }); + } catch (err) { + this._handleException(err); + + // This means we retried 3 times and all of them failed, + // or we ran into a problem we don't want to retry, like rate limiting. + // In this case, we want to completely stop the replay - otherwise, we may get inconsistent segments + void this.stop('sendReplay'); + + const client = getCurrentHub().getClient(); + + if (client) { + client.recordDroppedEvent('send_error', 'replay'); + } + } + } + + /** + * Flush recording data to Sentry. Creates a lock so that only a single flush + * can be active at a time. Do not call this directly. + */ + __init17() {this._flush = async ({ + force = false, + } + + = {}) => { + if (!this._isEnabled && !force) { + // This can happen if e.g. the replay was stopped because of exceeding the retry limit + return; + } + + if (!this.checkAndHandleExpiredSession()) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] Attempting to finish replay event after session expired.'); + return; + } + + if (!this.session) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] No session found to flush.'); + return; + } + + // A flush is about to happen, cancel any queued flushes + this._debouncedFlush.cancel(); + + // this._flushLock acts as a lock so that future calls to `_flush()` + // will be blocked until this promise resolves + if (!this._flushLock) { + this._flushLock = this._runFlush(); + await this._flushLock; + this._flushLock = null; + return; + } + + // Wait for previous flush to finish, then call the debounced `_flush()`. + // It's possible there are other flush requests queued and waiting for it + // to resolve. We want to reduce all outstanding requests (as well as any + // new flush requests that occur within a second of the locked flush + // completing) into a single flush. + + try { + await this._flushLock; + } catch (err) { + (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(err); + } finally { + this._debouncedFlush(); + } + };} + + /** Save the session, if it is sticky */ + _maybeSaveSession() { + if (this.session && this._options.stickySession) { + saveSession(this.session); + } + } + + /** Handler for rrweb.record.onMutation */ + __init18() {this._onMutationHandler = (mutations) => { + const count = mutations.length; + + const mutationLimit = this._options.mutationLimit; + const mutationBreadcrumbLimit = this._options.mutationBreadcrumbLimit; + const overMutationLimit = mutationLimit && count > mutationLimit; + + // Create a breadcrumb if a lot of mutations happen at the same time + // We can show this in the UI as an information with potential performance improvements + if (count > mutationBreadcrumbLimit || overMutationLimit) { + const breadcrumb = createBreadcrumb({ + category: 'replay.mutations', + data: { + count, + limit: overMutationLimit, + }, + }); + this._createCustomBreadcrumb(breadcrumb); + } + + // Stop replay if over the mutation limit + if (overMutationLimit) { + void this.stop('mutationLimit'); + return false; + } + + // `true` means we use the regular mutation handling by rrweb + return true; + };} +} + +function getOption( + selectors, + defaultSelectors, + deprecatedClassOption, + deprecatedSelectorOption, +) { + const deprecatedSelectors = typeof deprecatedSelectorOption === 'string' ? deprecatedSelectorOption.split(',') : []; + + const allSelectors = [ + ...selectors, + // @deprecated + ...deprecatedSelectors, + + // sentry defaults + ...defaultSelectors, + ]; + + // @deprecated + if (typeof deprecatedClassOption !== 'undefined') { + // NOTE: No support for RegExp + if (typeof deprecatedClassOption === 'string') { + allSelectors.push(`.${deprecatedClassOption}`); + } + + // eslint-disable-next-line no-console + console.warn( + '[Replay] You are using a deprecated configuration item for privacy. Read the documentation on how to use the new privacy configuration.', + ); + } + + return allSelectors.join(','); +} + +/** + * Returns privacy related configuration for use in rrweb + */ +function getPrivacyOptions({ + mask, + unmask, + block, + unblock, + ignore, + + // eslint-disable-next-line deprecation/deprecation + blockClass, + // eslint-disable-next-line deprecation/deprecation + blockSelector, + // eslint-disable-next-line deprecation/deprecation + maskTextClass, + // eslint-disable-next-line deprecation/deprecation + maskTextSelector, + // eslint-disable-next-line deprecation/deprecation + ignoreClass, +}) { + const defaultBlockedElements = ['base[href="/"]']; + + const maskSelector = getOption(mask, ['.sentry-mask', '[data-sentry-mask]'], maskTextClass, maskTextSelector); + const unmaskSelector = getOption(unmask, ['.sentry-unmask', '[data-sentry-unmask]']); + + const options = { + // We are making the decision to make text and input selectors the same + maskTextSelector: maskSelector, + unmaskTextSelector: unmaskSelector, + maskInputSelector: maskSelector, + unmaskInputSelector: unmaskSelector, + + blockSelector: getOption( + block, + ['.sentry-block', '[data-sentry-block]', ...defaultBlockedElements], + blockClass, + blockSelector, + ), + unblockSelector: getOption(unblock, ['.sentry-unblock', '[data-sentry-unblock]']), + ignoreSelector: getOption(ignore, ['.sentry-ignore', '[data-sentry-ignore]', 'input[type="file"]'], ignoreClass), + }; + + if (blockClass instanceof RegExp) { + options.blockClass = blockClass; + } + + if (maskTextClass instanceof RegExp) { + options.maskTextClass = maskTextClass; + } + + return options; +} + +/** + * Returns true if we are in the browser. + */ +function isBrowser() { + // eslint-disable-next-line no-restricted-globals + return typeof window !== 'undefined' && (!isNodeEnv() || isElectronNodeRenderer()); +} + +// Electron renderers with nodeIntegration enabled are detected as Node.js so we specifically test for them +function isElectronNodeRenderer() { + return typeof process !== 'undefined' && (process ).type === 'renderer'; +} + +const MEDIA_SELECTORS = + 'img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]'; + +const DEFAULT_NETWORK_HEADERS = ['content-length', 'content-type', 'accept']; + +let _initialized = false; + +/** + * The main replay integration class, to be passed to `init({ integrations: [] })`. + */ +class Replay { + /** + * @inheritDoc + */ + static __initStatic() {this.id = 'Replay';} + + /** + * @inheritDoc + */ + __init() {this.name = Replay.id;} + + /** + * Options to pass to `rrweb.record()` + */ + + /** + * Initial options passed to the replay integration, merged with default values. + * Note: `sessionSampleRate` and `errorSampleRate` are not required here, as they + * can only be finally set when setupOnce() is called. + * + * @private + */ + + constructor({ + flushMinDelay = DEFAULT_FLUSH_MIN_DELAY, + flushMaxDelay = DEFAULT_FLUSH_MAX_DELAY, + stickySession = true, + useCompression = true, + _experiments = {}, + sessionSampleRate, + errorSampleRate, + maskAllText = true, + maskAllInputs = true, + blockAllMedia = true, + + mutationBreadcrumbLimit = 750, + mutationLimit = 10000, + + slowClickTimeout = 7000, + slowClickIgnoreSelectors = [], + + networkDetailAllowUrls = [], + networkCaptureBodies = true, + networkRequestHeaders = [], + networkResponseHeaders = [], + + mask = [], + unmask = [], + block = [], + unblock = [], + ignore = [], + maskFn, + + beforeAddRecordingEvent, + + // eslint-disable-next-line deprecation/deprecation + blockClass, + // eslint-disable-next-line deprecation/deprecation + blockSelector, + // eslint-disable-next-line deprecation/deprecation + maskInputOptions, + // eslint-disable-next-line deprecation/deprecation + maskTextClass, + // eslint-disable-next-line deprecation/deprecation + maskTextSelector, + // eslint-disable-next-line deprecation/deprecation + ignoreClass, + } = {}) {Replay.prototype.__init.call(this); + this._recordingOptions = { + maskAllInputs, + maskAllText, + maskInputOptions: { ...(maskInputOptions || {}), password: true }, + maskTextFn: maskFn, + maskInputFn: maskFn, + + ...getPrivacyOptions({ + mask, + unmask, + block, + unblock, + ignore, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + ignoreClass, + }), + + // Our defaults + slimDOMOptions: 'all', + inlineStylesheet: true, + // Disable inline images as it will increase segment/replay size + inlineImages: false, + // collect fonts, but be aware that `sentry.io` needs to be an allowed + // origin for playback + collectFonts: true, + }; + + this._initialOptions = { + flushMinDelay, + flushMaxDelay, + stickySession, + sessionSampleRate, + errorSampleRate, + useCompression, + blockAllMedia, + maskAllInputs, + maskAllText, + mutationBreadcrumbLimit, + mutationLimit, + slowClickTimeout, + slowClickIgnoreSelectors, + networkDetailAllowUrls, + networkCaptureBodies, + networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders), + networkResponseHeaders: _getMergedNetworkHeaders(networkResponseHeaders), + beforeAddRecordingEvent, + + _experiments, + }; + + if (typeof sessionSampleRate === 'number') { + // eslint-disable-next-line + console.warn( + `[Replay] You are passing \`sessionSampleRate\` to the Replay integration. +This option is deprecated and will be removed soon. +Instead, configure \`replaysSessionSampleRate\` directly in the SDK init options, e.g.: +Sentry.init({ replaysSessionSampleRate: ${sessionSampleRate} })`, + ); + + this._initialOptions.sessionSampleRate = sessionSampleRate; + } + + if (typeof errorSampleRate === 'number') { + // eslint-disable-next-line + console.warn( + `[Replay] You are passing \`errorSampleRate\` to the Replay integration. +This option is deprecated and will be removed soon. +Instead, configure \`replaysOnErrorSampleRate\` directly in the SDK init options, e.g.: +Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, + ); + + this._initialOptions.errorSampleRate = errorSampleRate; + } + + if (this._initialOptions.blockAllMedia) { + // `blockAllMedia` is a more user friendly option to configure blocking + // embedded media elements + this._recordingOptions.blockSelector = !this._recordingOptions.blockSelector + ? MEDIA_SELECTORS + : `${this._recordingOptions.blockSelector},${MEDIA_SELECTORS}`; + } + + if (this._isInitialized && isBrowser()) { + throw new Error('Multiple Sentry Session Replay instances are not supported'); + } + + this._isInitialized = true; + } + + /** If replay has already been initialized */ + get _isInitialized() { + return _initialized; + } + + /** Update _isInitialized */ + set _isInitialized(value) { + _initialized = value; + } + + /** + * Setup and initialize replay container + */ + setupOnce() { + if (!isBrowser()) { + return; + } + + this._setup(); + + // Once upon a time, we tried to create a transaction in `setupOnce` and it would + // potentially create a transaction before some native SDK integrations have run + // and applied their own global event processor. An example is: + // https://github.com/getsentry/sentry-javascript/blob/b47ceafbdac7f8b99093ce6023726ad4687edc48/packages/browser/src/integrations/useragent.ts + // + // So we call `this._initialize()` in next event loop as a workaround to wait for other + // global event processors to finish. This is no longer needed, but keeping it + // here to avoid any future issues. + setTimeout(() => this._initialize()); + } + + /** + * Start a replay regardless of sampling rate. Calling this will always + * create a new session. Will throw an error if replay is already in progress. + * + * Creates or loads a session, attaches listeners to varying events (DOM, + * PerformanceObserver, Recording, Sentry SDK, etc) + */ + start() { + if (!this._replay) { + return; + } + + this._replay.start(); + } + + /** + * Start replay buffering. Buffers until `flush()` is called or, if + * `replaysOnErrorSampleRate` > 0, until an error occurs. + */ + startBuffering() { + if (!this._replay) { + return; + } + + this._replay.startBuffering(); + } + + /** + * Currently, this needs to be manually called (e.g. for tests). Sentry SDK + * does not support a teardown + */ + stop() { + if (!this._replay) { + return Promise.resolve(); + } + + return this._replay.stop(); + } + + /** + * If not in "session" recording mode, flush event buffer which will create a new replay. + * Unless `continueRecording` is false, the replay will continue to record and + * behave as a "session"-based replay. + * + * Otherwise, queue up a flush. + */ + flush(options) { + if (!this._replay || !this._replay.isEnabled()) { + return Promise.resolve(); + } + + return this._replay.sendBufferedReplayOrFlush(options); + } + + /** + * Get the current session ID. + */ + getReplayId() { + if (!this._replay || !this._replay.isEnabled()) { + return; + } + + return this._replay.getSessionId(); + } + /** + * Initializes replay. + */ + _initialize() { + if (!this._replay) { + return; + } + + this._replay.initializeSampling(); + } + + /** Setup the integration. */ + _setup() { + // Client is not available in constructor, so we need to wait until setupOnce + const finalOptions = loadReplayOptionsFromClient(this._initialOptions); + + this._replay = new ReplayContainer({ + options: finalOptions, + recordingOptions: this._recordingOptions, + }); + } +} Replay.__initStatic(); + +/** Parse Replay-related options from SDK options */ +function loadReplayOptionsFromClient(initialOptions) { + const client = getCurrentHub().getClient(); + const opt = client && (client.getOptions() ); + + const finalOptions = { sessionSampleRate: 0, errorSampleRate: 0, ...dropUndefinedKeys(initialOptions) }; + + if (!opt) { + // eslint-disable-next-line no-console + console.warn('SDK client is not available.'); + return finalOptions; + } + + if ( + initialOptions.sessionSampleRate == null && // TODO remove once deprecated rates are removed + initialOptions.errorSampleRate == null && // TODO remove once deprecated rates are removed + opt.replaysSessionSampleRate == null && + opt.replaysOnErrorSampleRate == null + ) { + // eslint-disable-next-line no-console + console.warn( + 'Replay is disabled because neither `replaysSessionSampleRate` nor `replaysOnErrorSampleRate` are set.', + ); + } + + if (typeof opt.replaysSessionSampleRate === 'number') { + finalOptions.sessionSampleRate = opt.replaysSessionSampleRate; + } + + if (typeof opt.replaysOnErrorSampleRate === 'number') { + finalOptions.errorSampleRate = opt.replaysOnErrorSampleRate; + } + + return finalOptions; +} + +function _getMergedNetworkHeaders(headers) { + return [...DEFAULT_NETWORK_HEADERS, ...headers.map(header => header.toLowerCase())]; +} + +export { Replay }; +//# sourceMappingURL=index.js.map -- cgit v1.2.3