import { _nullishCoalesce, _optionalChain } from '@sentry/utils';
import { addBreadcrumb, getClient, isSentryRequestUrl, getCurrentScope, addEventProcessor, prepareEvent, getIsolationScope, setContext, captureException, spanToJSON, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
import { GLOBAL_OBJ, normalize, fill, htmlTreeAsString, browserPerformanceTimeOrigin, logger, uuid4, SENTRY_XHR_DATA_KEY, dropUndefinedKeys, stringMatchesSomePattern, addFetchInstrumentationHandler, addXhrInstrumentationHandler, addClickKeypressInstrumentationHandler, addHistoryInstrumentationHandler, createEnvelope, createEventEnvelopeHeaders, getSdkMetadataForEnvelopeHeader, updateRateLimits, isRateLimited, consoleSandbox, isBrowser } from '@sentry/utils';
import { addPerformanceInstrumentationHandler, addLcpInstrumentationHandler } from '@sentry-internal/tracing';
// 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
/** 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;
/** 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
/** Replays must be min. 5s long before we send them. */
const MIN_REPLAY_DURATION = 4999;
/* The max. allowed value that the minReplayDuration can be set to. */
const MIN_REPLAY_DURATION_LIMIT = 15000;
/** The max. length of a replay. */
const MAX_REPLAY_DURATION = 3600000; // 60 minutes in ms;
function _nullishCoalesce$1(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }function _optionalChain$5(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }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$1(n) {
return n.nodeType === n.ELEMENT_NODE;
}
function isShadowRoot(n) {
const host = _optionalChain$5([n, 'optionalAccess', _ => _.host]);
return Boolean(_optionalChain$5([host, 'optionalAccess', _2 => _2.shadowRoot]) === n);
}
function isNativeShadowDom(shadowRoot) {
return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]';
}
function fixBrowserCompatibilityIssuesInCSS(cssText) {
if (cssText.includes(' background-clip: text;') &&
!cssText.includes(' -webkit-background-clip: text;')) {
cssText = cssText.replace(' background-clip: text;', ' -webkit-background-clip: text; background-clip: text;');
}
return cssText;
}
function escapeImportStatement(rule) {
const { cssText } = rule;
if (cssText.split('"').length < 3)
return cssText;
const statement = ['@import', `url(${JSON.stringify(rule.href)})`];
if (rule.layerName === '') {
statement.push(`layer`);
}
else if (rule.layerName) {
statement.push(`layer(${rule.layerName})`);
}
if (rule.supportsText) {
statement.push(`supports(${rule.supportsText})`);
}
if (rule.media.length) {
statement.push(rule.media.mediaText);
}
return statement.join(' ') + ';';
}
function stringifyStylesheet(s) {
try {
const rules = s.rules || s.cssRules;
return rules
? fixBrowserCompatibilityIssuesInCSS(Array.from(rules, stringifyRule).join(''))
: null;
}
catch (error) {
return null;
}
}
function stringifyRule(rule) {
let importStringified;
if (isCSSImportRule(rule)) {
try {
importStringified =
stringifyStylesheet(rule.styleSheet) ||
escapeImportStatement(rule);
}
catch (error) {
}
}
else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) {
return fixSafariColons(rule.cssText);
}
return importStringified || rule.cssText;
}
function fixSafariColons(cssStringified) {
const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm;
return cssStringified.replace(regex, '$1\\$2');
}
function isCSSImportRule(rule) {
return 'styleSheet' in rule;
}
function isCSSStyleRule(rule) {
return 'selectorText' in rule;
}
class Mirror {
constructor() {
this.idNodeMap = new Map();
this.nodeMetaMap = new WeakMap();
}
getId(n) {
if (!n)
return -1;
const id = _optionalChain$5([this, 'access', _3 => _3.getMeta, 'call', _4 => _4(n), 'optionalAccess', _5 => _5.id]);
return _nullishCoalesce$1(id, () => ( -1));
}
getNode(id) {
return this.idNodeMap.get(id) || null;
}
getIds() {
return Array.from(this.idNodeMap.keys());
}
getMeta(n) {
return this.nodeMetaMap.get(n) || null;
}
removeNodeFromMap(n) {
const id = this.getId(n);
this.idNodeMap.delete(id);
if (n.childNodes) {
n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode));
}
}
has(id) {
return this.idNodeMap.has(id);
}
hasNode(node) {
return this.nodeMetaMap.has(node);
}
add(n, meta) {
const id = meta.id;
this.idNodeMap.set(id, n);
this.nodeMetaMap.set(n, meta);
}
replace(id, n) {
const oldNode = this.getNode(id);
if (oldNode) {
const meta = this.nodeMetaMap.get(oldNode);
if (meta)
this.nodeMetaMap.set(n, meta);
}
this.idNodeMap.set(id, n);
}
reset() {
this.idNodeMap = new Map();
this.nodeMetaMap = new WeakMap();
}
}
function createMirror() {
return new Mirror();
}
function shouldMaskInput({ maskInputOptions, tagName, type, }) {
if (tagName === 'OPTION') {
tagName = 'SELECT';
}
return Boolean(maskInputOptions[tagName.toLowerCase()] ||
(type && maskInputOptions[type]) ||
type === 'password' ||
(tagName === 'INPUT' && !type && maskInputOptions['text']));
}
function maskInputValue({ isMasked, element, value, maskInputFn, }) {
let text = value || '';
if (!isMasked) {
return text;
}
if (maskInputFn) {
text = maskInputFn(text, element);
}
return '*'.repeat(text.length);
}
function toLowerCase(str) {
return str.toLowerCase();
}
function toUpperCase(str) {
return str.toUpperCase();
}
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
?
toLowerCase(type)
: null;
}
function getInputValue(el, tagName, type) {
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 genId() {
return _id++;
}
function getValidTagName(element) {
if (element instanceof HTMLFormElement) {
return 'form';
}
const processedTagName = toLowerCase(element.tagName);
if (tagNameRegex.test(processedTagName)) {
return 'div';
}
return processedTagName;
}
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 URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i;
const URL_WWW_MATCH = /^www\..*/i;
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 (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.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;
const match = regEx.exec(attributeValue.substring(pos));
if (match) {
chars = match[0];
pos += chars.length;
return chars;
}
return '';
}
const 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) {
const 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, tagName, name, value, element, maskAttributeFn) {
if (!value) {
return value;
}
if (name === 'src' ||
(name === 'href' && !(tagName === 'use' && value[0] === '#'))) {
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);
}
if (typeof maskAttributeFn === 'function') {
return maskAttributeFn(name, value, element);
}
return value;
}
function ignoreAttribute(tagName, name, _value) {
return (tagName === 'video' || tagName === 'audio') && name === 'autoplay';
}
function _isBlockedElement(element, blockClass, blockSelector, unblockSelector) {
try {
if (unblockSelector && element.matches(unblockSelector)) {
return false;
}
if (typeof blockClass === 'string') {
if (element.classList.contains(blockClass)) {
return true;
}
}
else {
for (let eIndex = element.classList.length; eIndex--;) {
const className = element.classList[eIndex];
if (blockClass.test(className)) {
return true;
}
}
}
if (blockSelector) {
return element.matches(blockSelector);
}
}
catch (e) {
}
return false;
}
function elementClassMatchesRegex(el, regex) {
for (let eIndex = el.classList.length; eIndex--;) {
const className = el.classList[eIndex];
if (regex.test(className)) {
return true;
}
}
return false;
}
function distanceToMatch(node, matchPredicate, limit = Infinity, distance = 0) {
if (!node)
return -1;
if (node.nodeType !== node.ELEMENT_NODE)
return -1;
if (distance > limit)
return -1;
if (matchPredicate(node))
return distance;
return distanceToMatch(node.parentNode, matchPredicate, limit, distance + 1);
}
function createMatchPredicate(className, selector) {
return (node) => {
const el = node;
if (el === null)
return false;
try {
if (className) {
if (typeof className === 'string') {
if (el.matches(`.${className}`))
return true;
}
else if (elementClassMatchesRegex(el, className)) {
return true;
}
}
if (selector && el.matches(selector))
return true;
return false;
}
catch (e2) {
return false;
}
};
}
function needMaskingText(node, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, maskAllText) {
try {
const el = node.nodeType === node.ELEMENT_NODE
? node
: node.parentElement;
if (el === null)
return false;
if (el.tagName === 'INPUT') {
const autocomplete = el.getAttribute('autocomplete');
const disallowedAutocompleteValues = [
'current-password',
'new-password',
'cc-number',
'cc-exp',
'cc-exp-month',
'cc-exp-year',
'cc-csc',
];
if (disallowedAutocompleteValues.includes(autocomplete)) {
return true;
}
}
let maskDistance = -1;
let unmaskDistance = -1;
if (maskAllText) {
unmaskDistance = distanceToMatch(el, createMatchPredicate(unmaskTextClass, unmaskTextSelector));
if (unmaskDistance < 0) {
return true;
}
maskDistance = distanceToMatch(el, createMatchPredicate(maskTextClass, maskTextSelector), unmaskDistance >= 0 ? unmaskDistance : Infinity);
}
else {
maskDistance = distanceToMatch(el, createMatchPredicate(maskTextClass, maskTextSelector));
if (maskDistance < 0) {
return false;
}
unmaskDistance = distanceToMatch(el, createMatchPredicate(unmaskTextClass, unmaskTextSelector), maskDistance >= 0 ? maskDistance : Infinity);
}
return maskDistance >= 0
? unmaskDistance >= 0
? maskDistance <= unmaskDistance
: true
: unmaskDistance >= 0
? false
: !!maskAllText;
}
catch (e) {
}
return !!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);
}
iframeEl.addEventListener('load', listener);
}
function onceStylesheetLoaded(link, listener, styleSheetLoadTimeout) {
let fired = false;
let styleSheetLoaded;
try {
styleSheetLoaded = link.sheet;
}
catch (error) {
return;
}
if (styleSheetLoaded)
return;
const timer = setTimeout(() => {
if (!fired) {
listener();
fired = true;
}
}, styleSheetLoadTimeout);
link.addEventListener('load', () => {
clearTimeout(timer);
fired = true;
listener();
});
}
function serializeNode(n, options) {
const { doc, mirror, blockClass, blockSelector, unblockSelector, maskAllText, maskAttributeFn, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, inlineStylesheet, maskInputOptions = {}, maskTextFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, } = options;
const rootId = getRootId(doc, mirror);
switch (n.nodeType) {
case n.DOCUMENT_NODE:
if (n.compatMode !== 'CSS1Compat') {
return {
type: NodeType$1.Document,
childNodes: [],
compatMode: n.compatMode,
};
}
else {
return {
type: NodeType$1.Document,
childNodes: [],
};
}
case n.DOCUMENT_TYPE_NODE:
return {
type: NodeType$1.DocumentType,
name: n.name,
publicId: n.publicId,
systemId: n.systemId,
rootId,
};
case n.ELEMENT_NODE:
return serializeElementNode(n, {
doc,
blockClass,
blockSelector,
unblockSelector,
inlineStylesheet,
maskAttributeFn,
maskInputOptions,
maskInputFn,
dataURLOptions,
inlineImages,
recordCanvas,
keepIframeSrcFn,
newlyAddedElement,
rootId,
maskAllText,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
});
case n.TEXT_NODE:
return serializeTextNode(n, {
maskAllText,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
maskTextFn,
maskInputOptions,
maskInputFn,
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 getRootId(doc, mirror) {
if (!mirror.hasNode(doc))
return undefined;
const docId = mirror.getId(doc);
return docId === 1 ? undefined : docId;
}
function serializeTextNode(n, options) {
const { maskAllText, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, maskTextFn, maskInputOptions, maskInputFn, rootId, } = options;
const parentTagName = n.parentNode && n.parentNode.tagName;
let textContent = n.textContent;
const isStyle = parentTagName === 'STYLE' ? true : undefined;
const isScript = parentTagName === 'SCRIPT' ? true : undefined;
const isTextarea = parentTagName === 'TEXTAREA' ? true : undefined;
if (isStyle && textContent) {
try {
if (n.nextSibling || n.previousSibling) {
}
else if (_optionalChain$5([n, 'access', _6 => _6.parentNode, 'access', _7 => _7.sheet, 'optionalAccess', _8 => _8.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';
}
const forceMask = needMaskingText(n, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, maskAllText);
if (!isStyle && !isScript && !isTextarea && textContent && forceMask) {
textContent = maskTextFn
? maskTextFn(textContent)
: textContent.replace(/[\S]/g, '*');
}
if (isTextarea && textContent && (maskInputOptions.textarea || forceMask)) {
textContent = maskInputFn
? maskInputFn(textContent, n.parentNode)
: textContent.replace(/[\S]/g, '*');
}
if (parentTagName === 'OPTION' && textContent) {
const isInputMasked = shouldMaskInput({
type: null,
tagName: parentTagName,
maskInputOptions,
});
textContent = maskInputValue({
isMasked: needMaskingText(n, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, isInputMasked),
element: n,
value: textContent,
maskInputFn,
});
}
return {
type: NodeType$1.Text,
textContent: textContent || '',
isStyle,
rootId,
};
}
function serializeElementNode(n, options) {
const { doc, blockClass, blockSelector, unblockSelector, inlineStylesheet, maskInputOptions = {}, maskAttributeFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, rootId, maskAllText, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, } = options;
const needBlock = _isBlockedElement(n, blockClass, blockSelector, unblockSelector);
const tagName = getValidTagName(n);
let attributes = {};
const len = n.attributes.length;
for (let i = 0; i < len; i++) {
const attr = n.attributes[i];
if (attr.name && !ignoreAttribute(tagName, attr.name, attr.value)) {
attributes[attr.name] = transformAttribute(doc, tagName, toLowerCase(attr.name), attr.value, n, maskAttributeFn);
}
}
if (tagName === 'link' && inlineStylesheet) {
const stylesheet = Array.from(doc.styleSheets).find((s) => {
return s.href === n.href;
});
let cssText = null;
if (stylesheet) {
cssText = stringifyStylesheet(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 = stringifyStylesheet(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, toUpperCase(tagName), type);
const checked = el.checked;
if (type !== 'submit' && type !== 'button' && value) {
const forceMask = needMaskingText(el, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, shouldMaskInput({
type,
tagName: toUpperCase(tagName),
maskInputOptions,
}));
attributes.value = maskInputValue({
isMasked: forceMask,
element: el,
value,
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 = () => {
image.removeEventListener('load', 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)
: image.removeAttribute('crossorigin');
};
if (image.complete && image.naturalWidth !== 0)
recordInlineImage();
else
image.addEventListener('load', recordInlineImage);
}
if (tagName === 'audio' || tagName === 'video') {
attributes.rr_mediaState = n.paused
? 'paused'
: 'played';
attributes.rr_mediaCurrentTime = n.currentTime;
}
if (!newlyAddedElement) {
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;
}
let isCustomElement;
try {
if (customElements.get(tagName))
isCustomElement = true;
}
catch (e) {
}
return {
type: NodeType$1.Element,
tagName,
attributes,
childNodes: [],
isSVG: isSVGElement(n) || undefined,
needBlock,
rootId,
isCustom: isCustomElement,
};
}
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, mirror, blockClass, blockSelector, unblockSelector, maskAllText, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, skipChild = false, inlineStylesheet = true, maskInputOptions = {}, maskAttributeFn, maskTextFn, maskInputFn, slimDOMOptions, dataURLOptions = {}, inlineImages = false, recordCanvas = false, onSerialize, onIframeLoad, iframeLoadTimeout = 5000, onStylesheetLoad, stylesheetLoadTimeout = 5000, keepIframeSrcFn = () => false, newlyAddedElement = false, } = options;
let { preserveWhiteSpace = true } = options;
const _serializedNode = serializeNode(n, {
doc,
mirror,
blockClass,
blockSelector,
maskAllText,
unblockSelector,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
dataURLOptions,
inlineImages,
recordCanvas,
keepIframeSrcFn,
newlyAddedElement,
});
if (!_serializedNode) {
console.warn(n, 'not serialized');
return null;
}
let id;
if (mirror.hasNode(n)) {
id = mirror.getId(n);
}
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 });
mirror.add(n, serializedNode);
if (id === IGNORED_NODE) {
return null;
}
if (onSerialize) {
onSerialize(n);
}
let recordChild = !skipChild;
if (serializedNode.type === NodeType$1.Element) {
recordChild = recordChild && !serializedNode.needBlock;
delete serializedNode.needBlock;
const shadowRoot = n.shadowRoot;
if (shadowRoot && isNativeShadowDom(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,
mirror,
blockClass,
blockSelector,
maskAllText,
unblockSelector,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
skipChild,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
dataURLOptions,
inlineImages,
recordCanvas,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
iframeLoadTimeout,
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn,
};
for (const childN of Array.from(n.childNodes)) {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
serializedNode.childNodes.push(serializedChildNode);
}
}
if (isElement$1(n) && n.shadowRoot) {
for (const childN of Array.from(n.shadowRoot.childNodes)) {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
isNativeShadowDom(n.shadowRoot) &&
(serializedChildNode.isShadow = true);
serializedNode.childNodes.push(serializedChildNode);
}
}
}
}
if (n.parentNode &&
isShadowRoot(n.parentNode) &&
isNativeShadowDom(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,
mirror,
blockClass,
blockSelector,
unblockSelector,
maskAllText,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
skipChild: false,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
dataURLOptions,
inlineImages,
recordCanvas,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
iframeLoadTimeout,
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn,
});
if (serializedIframeNode) {
onIframeLoad(n, serializedIframeNode);
}
}
}, iframeLoadTimeout);
}
if (serializedNode.type === NodeType$1.Element &&
serializedNode.tagName === 'link' &&
serializedNode.attributes.rel === 'stylesheet') {
onceStylesheetLoaded(n, () => {
if (onStylesheetLoad) {
const serializedLinkNode = serializeNodeWithId(n, {
doc,
mirror,
blockClass,
blockSelector,
unblockSelector,
maskAllText,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
skipChild: false,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
dataURLOptions,
inlineImages,
recordCanvas,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
iframeLoadTimeout,
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn,
});
if (serializedLinkNode) {
onStylesheetLoad(n, serializedLinkNode);
}
}
}, stylesheetLoadTimeout);
}
return serializedNode;
}
function snapshot(n, options) {
const { mirror = new Mirror(), blockClass = 'rr-block', blockSelector = null, unblockSelector = null, maskAllText = false, maskTextClass = 'rr-mask', unmaskTextClass = null, maskTextSelector = null, unmaskTextSelector = null, inlineStylesheet = true, inlineImages = false, recordCanvas = false, maskAllInputs = false, maskAttributeFn, maskTextFn, maskInputFn, slimDOM = false, dataURLOptions, preserveWhiteSpace, onSerialize, onIframeLoad, iframeLoadTimeout, onStylesheetLoad, stylesheetLoadTimeout, keepIframeSrcFn = () => false, } = options || {};
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,
mirror,
blockClass,
blockSelector,
unblockSelector,
maskAllText,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
skipChild: false,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
dataURLOptions,
inlineImages,
recordCanvas,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
iframeLoadTimeout,
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn,
newlyAddedElement: false,
});
}
function _optionalChain$4(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
function on(type, fn, target = document) {
const options = { capture: true, passive: true };
target.addEventListener(type, fn, options);
return () => target.removeEventListener(type, fn, options);
}
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 (...args) {
const now = Date.now();
if (!previous && options.leading === false) {
previous = now;
}
const remaining = wait - (now - previous);
const context = this;
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 (e2) {
return () => {
};
}
}
let nowTimestamp = Date.now;
if (!(/[1-9][0-9]{12}/.test(Date.now().toString()))) {
nowTimestamp = () => new Date().getTime();
}
function getWindowScroll(win) {
const doc = win.document;
return {
left: doc.scrollingElement
? doc.scrollingElement.scrollLeft
: win.pageXOffset !== undefined
? win.pageXOffset
: _optionalChain$4([doc, 'optionalAccess', _ => _.documentElement, 'access', _2 => _2.scrollLeft]) ||
_optionalChain$4([doc, 'optionalAccess', _3 => _3.body, 'optionalAccess', _4 => _4.parentElement, 'optionalAccess', _5 => _5.scrollLeft]) ||
_optionalChain$4([doc, 'optionalAccess', _6 => _6.body, 'optionalAccess', _7 => _7.scrollLeft]) ||
0,
top: doc.scrollingElement
? doc.scrollingElement.scrollTop
: win.pageYOffset !== undefined
? win.pageYOffset
: _optionalChain$4([doc, 'optionalAccess', _8 => _8.documentElement, 'access', _9 => _9.scrollTop]) ||
_optionalChain$4([doc, 'optionalAccess', _10 => _10.body, 'optionalAccess', _11 => _11.parentElement, 'optionalAccess', _12 => _12.scrollTop]) ||
_optionalChain$4([doc, 'optionalAccess', _13 => _13.body, 'optionalAccess', _14 => _14.scrollTop]) ||
0,
};
}
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, checkAncestors) {
if (!node) {
return false;
}
const el = node.nodeType === node.ELEMENT_NODE
? node
: node.parentElement;
if (!el)
return false;
const blockedPredicate = createMatchPredicate(blockClass, blockSelector);
if (!checkAncestors) {
const isUnblocked = unblockSelector && el.matches(unblockSelector);
return blockedPredicate(el) && !isUnblocked;
}
const blockDistance = distanceToMatch(el, blockedPredicate);
let unblockDistance = -1;
if (blockDistance < 0) {
return false;
}
if (unblockSelector) {
unblockDistance = distanceToMatch(el, createMatchPredicate(null, unblockSelector));
}
if (blockDistance > -1 && unblockDistance < 0) {
return true;
}
return blockDistance < unblockDistance;
}
function isSerialized(n, mirror) {
return mirror.getId(n) !== -1;
}
function isIgnored(n, mirror) {
return mirror.getId(n) === IGNORED_NODE;
}
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 legacy_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 = (...args) => {
let node = args[0];
if (!(0 in args)) {
throw new TypeError('1 argument is required');
}
do {
if (this === node) {
return true;
}
} while ((node = node && node.parentNode));
return false;
};
}
}
function isSerializedIframe(n, mirror) {
return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n));
}
function isSerializedStylesheet(n, mirror) {
return Boolean(n.nodeName === 'LINK' &&
n.nodeType === n.ELEMENT_NODE &&
n.getAttribute &&
n.getAttribute('rel') === 'stylesheet' &&
mirror.getMeta(n));
}
function hasShadowRoot(n) {
return Boolean(_optionalChain$4([n, 'optionalAccess', _18 => _18.shadowRoot]));
}
class StyleSheetMirror {
constructor() {
this.id = 1;
this.styleIDMap = new WeakMap();
this.idStyleMap = new Map();
}
getId(stylesheet) {
return _nullishCoalesce(this.styleIDMap.get(stylesheet), () => ( -1));
}
has(stylesheet) {
return this.styleIDMap.has(stylesheet);
}
add(stylesheet, id) {
if (this.has(stylesheet))
return this.getId(stylesheet);
let newId;
if (id === undefined) {
newId = this.id++;
}
else
newId = id;
this.styleIDMap.set(stylesheet, newId);
this.idStyleMap.set(newId, stylesheet);
return newId;
}
getStyle(id) {
return this.idStyleMap.get(id) || null;
}
reset() {
this.styleIDMap = new WeakMap();
this.idStyleMap = new Map();
this.id = 1;
}
generateId() {
return this.id++;
}
}
function getShadowHost(n) {
let shadowHost = null;
if (_optionalChain$4([n, 'access', _19 => _19.getRootNode, 'optionalCall', _20 => _20(), 'optionalAccess', _21 => _21.nodeType]) === Node.DOCUMENT_FRAGMENT_NODE &&
n.getRootNode().host)
shadowHost = n.getRootNode().host;
return shadowHost;
}
function getRootShadowHost(n) {
let rootShadowHost = n;
let shadowHost;
while ((shadowHost = getShadowHost(rootShadowHost)))
rootShadowHost = shadowHost;
return rootShadowHost;
}
function shadowHostInDom(n) {
const doc = n.ownerDocument;
if (!doc)
return false;
const shadowHost = getRootShadowHost(n);
return doc.contains(shadowHost);
}
function inDom(n) {
const doc = n.ownerDocument;
if (!doc)
return false;
return doc.contains(n) || shadowHostInDom(n);
}
let cachedRequestAnimationFrameImplementation;
function getRequestAnimationFrameImplementation() {
if (cachedRequestAnimationFrameImplementation) {
return cachedRequestAnimationFrameImplementation;
}
const document = window.document;
let requestAnimationFrameImplementation = window.requestAnimationFrame;
if (document && typeof document.createElement === 'function') {
try {
const sandbox = document.createElement('iframe');
sandbox.hidden = true;
document.head.appendChild(sandbox);
const contentWindow = sandbox.contentWindow;
if (contentWindow && contentWindow.requestAnimationFrame) {
requestAnimationFrameImplementation =
contentWindow.requestAnimationFrame;
}
document.head.removeChild(sandbox);
}
catch (e) {
}
}
return (cachedRequestAnimationFrameImplementation =
requestAnimationFrameImplementation.bind(window));
}
function onRequestAnimationFrame(...rest) {
return getRequestAnimationFrameImplementation()(...rest);
}
var EventType = /* @__PURE__ */ ((EventType2) => {
EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
EventType2[EventType2["Load"] = 1] = "Load";
EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
EventType2[EventType2["Meta"] = 4] = "Meta";
EventType2[EventType2["Custom"] = 5] = "Custom";
EventType2[EventType2["Plugin"] = 6] = "Plugin";
return EventType2;
})(EventType || {});
var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
return IncrementalSource2;
})(IncrementalSource || {});
var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => {
MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp";
MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown";
MouseInteractions2[MouseInteractions2["Click"] = 2] = "Click";
MouseInteractions2[MouseInteractions2["ContextMenu"] = 3] = "ContextMenu";
MouseInteractions2[MouseInteractions2["DblClick"] = 4] = "DblClick";
MouseInteractions2[MouseInteractions2["Focus"] = 5] = "Focus";
MouseInteractions2[MouseInteractions2["Blur"] = 6] = "Blur";
MouseInteractions2[MouseInteractions2["TouchStart"] = 7] = "TouchStart";
MouseInteractions2[MouseInteractions2["TouchMove_Departed"] = 8] = "TouchMove_Departed";
MouseInteractions2[MouseInteractions2["TouchEnd"] = 9] = "TouchEnd";
MouseInteractions2[MouseInteractions2["TouchCancel"] = 10] = "TouchCancel";
return MouseInteractions2;
})(MouseInteractions || {});
var PointerTypes = /* @__PURE__ */ ((PointerTypes2) => {
PointerTypes2[PointerTypes2["Mouse"] = 0] = "Mouse";
PointerTypes2[PointerTypes2["Pen"] = 1] = "Pen";
PointerTypes2[PointerTypes2["Touch"] = 2] = "Touch";
return PointerTypes2;
})(PointerTypes || {});
function _optionalChain$3(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
function isNodeInLinkedList(n) {
return '__ln' in n;
}
class DoubleLinkedList {
constructor() {
this.length = 0;
this.head = null;
this.tail = 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 = _optionalChain$3([current, 'optionalAccess', _ => _.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;
}
if (node.next === null) {
this.tail = 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 {
this.tail = null;
}
}
else {
current.previous.next = current.next;
if (current.next) {
current.next.previous = current.previous;
}
else {
this.tail = current.previous;
}
}
if (n.__ln) {
delete n.__ln;
}
this.length--;
}
}
const moveKey = (id, parentId) => `${id}@${parentId}`;
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 addedIds = new Set();
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) => {
if (!n.parentNode || !inDom(n)) {
return;
}
const parentId = isShadowRoot(n.parentNode)
? this.mirror.getId(getShadowHost(n))
: this.mirror.getId(n.parentNode);
const nextId = getNextId(n);
if (parentId === -1 || nextId === -1) {
return addList.addNode(n);
}
const sn = serializeNodeWithId(n, {
doc: this.doc,
mirror: this.mirror,
blockClass: this.blockClass,
blockSelector: this.blockSelector,
maskAllText: this.maskAllText,
unblockSelector: this.unblockSelector,
maskTextClass: this.maskTextClass,
unmaskTextClass: this.unmaskTextClass,
maskTextSelector: this.maskTextSelector,
unmaskTextSelector: this.unmaskTextSelector,
skipChild: true,
newlyAddedElement: true,
inlineStylesheet: this.inlineStylesheet,
maskInputOptions: this.maskInputOptions,
maskAttributeFn: this.maskAttributeFn,
maskTextFn: this.maskTextFn,
maskInputFn: this.maskInputFn,
slimDOMOptions: this.slimDOMOptions,
dataURLOptions: this.dataURLOptions,
recordCanvas: this.recordCanvas,
inlineImages: this.inlineImages,
onSerialize: (currentN) => {
if (isSerializedIframe(currentN, this.mirror)) {
this.iframeManager.addIframe(currentN);
}
if (isSerializedStylesheet(currentN, this.mirror)) {
this.stylesheetManager.trackLinkElement(currentN);
}
if (hasShadowRoot(n)) {
this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc);
}
},
onIframeLoad: (iframe, childSn) => {
this.iframeManager.attachIframe(iframe, childSn);
this.shadowDomManager.observeAttachShadow(iframe);
},
onStylesheetLoad: (link, childSn) => {
this.stylesheetManager.attachLinkElement(link, childSn);
},
});
if (sn) {
adds.push({
parentId,
nextId,
node: sn,
});
addedIds.add(sn.id);
}
};
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) {
let tailNode = addList.tail;
while (tailNode) {
const _node = tailNode;
tailNode = tailNode.previous;
if (_node) {
const parentId = this.mirror.getId(_node.value.parentNode);
const nextId = getNextId(_node.value);
if (nextId === -1)
continue;
else if (parentId !== -1) {
node = _node;
break;
}
else {
const unhandledNode = _node.value;
if (unhandledNode.parentNode &&
unhandledNode.parentNode.nodeType ===
Node.DOCUMENT_FRAGMENT_NODE) {
const shadowHost = unhandledNode.parentNode
.host;
const parentId = this.mirror.getId(shadowHost);
if (parentId !== -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) => !addedIds.has(text.id))
.filter((text) => this.mirror.has(text.id)),
attributes: this.attributes
.map((attribute) => {
const { attributes } = attribute;
if (typeof attributes.style === 'string') {
const diffAsStr = JSON.stringify(attribute.styleDiff);
const unchangedAsStr = JSON.stringify(attribute._unchangedStyles);
if (diffAsStr.length < attributes.style.length) {
if ((diffAsStr + unchangedAsStr).split('var(').length ===
attributes.style.split('var(').length) {
attributes.style = attribute.styleDiff;
}
}
}
return {
id: this.mirror.getId(attribute.node),
attributes: attributes,
};
})
.filter((attribute) => !addedIds.has(attribute.id))
.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, this.mirror)) {
return;
}
let unattachedDoc;
try {
unattachedDoc = document.implementation.createHTMLDocument();
}
catch (e) {
unattachedDoc = this.doc;
}
switch (m.type) {
case 'characterData': {
const value = m.target.textContent;
if (!isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector, false) &&
value !== m.oldValue) {
this.texts.push({
value: needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, this.unmaskTextClass, 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 attributeName = m.attributeName;
let value = m.target.getAttribute(attributeName);
if (attributeName === 'value') {
const type = getInputType(target);
const tagName = target.tagName;
value = getInputValue(target, tagName, type);
const isInputMasked = shouldMaskInput({
maskInputOptions: this.maskInputOptions,
tagName,
type,
});
const forceMask = needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, this.unmaskTextClass, this.unmaskTextSelector, isInputMasked);
value = maskInputValue({
isMasked: forceMask,
element: target,
value,
maskInputFn: this.maskInputFn,
});
}
if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector, false) ||
value === m.oldValue) {
return;
}
let item = this.attributes.find((a) => a.node === m.target);
if (target.tagName === 'IFRAME' &&
attributeName === 'src' &&
!this.keepIframeSrcFn(value)) {
if (!target.contentDocument) {
attributeName = 'rr_src';
}
else {
return;
}
}
if (!item) {
item = {
node: m.target,
attributes: {},
styleDiff: {},
_unchangedStyles: {},
};
this.attributes.push(item);
}
if (attributeName === 'type' &&
target.tagName === 'INPUT' &&
(m.oldValue || '').toLowerCase() === 'password') {
target.setAttribute('data-rr-is-password', 'true');
}
if (!ignoreAttribute(target.tagName, attributeName)) {
item.attributes[attributeName] = transformAttribute(this.doc, toLowerCase(target.tagName), toLowerCase(attributeName), value, target, this.maskAttributeFn);
if (attributeName === 'style') {
const old = unattachedDoc.createElement('span');
if (m.oldValue) {
old.setAttribute('style', m.oldValue);
}
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 === '') {
item.styleDiff[pname] = newValue;
}
else {
item.styleDiff[pname] = [newValue, newPriority];
}
}
else {
item._unchangedStyles[pname] = [newValue, newPriority];
}
}
for (const pname of Array.from(old.style)) {
if (target.style.getPropertyValue(pname) === '') {
item.styleDiff[pname] = false;
}
}
}
}
break;
}
case 'childList': {
if (isBlocked(m.target, this.blockClass, this.blockSelector, this.unblockSelector, true)) {
return;
}
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, false) ||
isIgnored(n, this.mirror) ||
!isSerialized(n, this.mirror)) {
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) && isNativeShadowDom(m.target)
? true
: undefined,
});
}
this.mapRemoves.push(n);
});
break;
}
}
};
this.genAdds = (n, target) => {
if (this.processedNodeManager.inOtherBuffer(n, this))
return;
if (this.addedSet.has(n) || this.movedSet.has(n))
return;
if (this.mirror.hasNode(n)) {
if (isIgnored(n, this.mirror)) {
return;
}
this.movedSet.add(n);
let targetId = null;
if (target && this.mirror.hasNode(target)) {
targetId = this.mirror.getId(target);
}
if (targetId && targetId !== -1) {
this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true;
}
}
else {
this.addedSet.add(n);
this.droppedSet.delete(n);
}
if (!isBlocked(n, this.blockClass, this.blockSelector, this.unblockSelector, false)) {
n.childNodes.forEach((childN) => this.genAdds(childN));
if (hasShadowRoot(n)) {
n.shadowRoot.childNodes.forEach((childN) => {
this.processedNodeManager.add(childN, this);
this.genAdds(childN, n);
});
}
}
};
}
init(options) {
[
'mutationCb',
'blockClass',
'blockSelector',
'unblockSelector',
'maskAllText',
'maskTextClass',
'unmaskTextClass',
'maskTextSelector',
'unmaskTextSelector',
'inlineStylesheet',
'maskInputOptions',
'maskAttributeFn',
'maskTextFn',
'maskInputFn',
'keepIframeSrcFn',
'recordCanvas',
'inlineImages',
'slimDOMOptions',
'dataURLOptions',
'doc',
'mirror',
'iframeManager',
'stylesheetManager',
'shadowDomManager',
'canvasManager',
'processedNodeManager',
].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) {
if (removes.length === 0)
return false;
return _isParentRemoved(removes, n, mirror);
}
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) {
if (set.size === 0)
return false;
return _isAncestorInSet(set, n);
}
function _isAncestorInSet(set, n) {
const { parentNode } = n;
if (!parentNode) {
return false;
}
if (set.has(parentNode)) {
return true;
}
return _isAncestorInSet(set, parentNode);
}
let errorHandler;
function registerErrorHandler(handler) {
errorHandler = handler;
}
function unregisterErrorHandler() {
errorHandler = undefined;
}
const callbackWrapper = (cb) => {
if (!errorHandler) {
return cb;
}
const rrwebWrapped = ((...rest) => {
try {
return cb(...rest);
}
catch (error) {
if (errorHandler && errorHandler(error) === true) {
return () => {
};
}
throw error;
}
});
return rrwebWrapped;
};
function _optionalChain$2(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
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 (e2) {
}
return event && event.target;
}
function initMutationObserver(options, rootEl) {
const mutationBuffer = new MutationBuffer();
mutationBuffers.push(mutationBuffer);
mutationBuffer.init(options);
let mutationObserverCtor = window.MutationObserver ||
window.__rrMutationObserver;
const angularZoneSymbol = _optionalChain$2([window, 'optionalAccess', _ => _.Zone, 'optionalAccess', _2 => _2.__symbol__, 'optionalCall', _3 => _3('MutationObserver')]);
if (angularZoneSymbol &&
window[angularZoneSymbol]) {
mutationObserverCtor = window[angularZoneSymbol];
}
const observer = new mutationObserverCtor(callbackWrapper((mutations) => {
if (options.onMutation && options.onMutation(mutations) === false) {
return;
}
mutationBuffer.processMutations.bind(mutationBuffer)(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(callbackWrapper((source) => {
const totalOffset = Date.now() - timeBaseline;
mousemoveCb(positions.map((p) => {
p.timeOffset -= totalOffset;
return p;
}), source);
positions = [];
timeBaseline = null;
}), callbackThreshold);
const updatePosition = callbackWrapper(throttle$1(callbackWrapper((evt) => {
const target = getEventTarget(evt);
const { clientX, clientY } = legacy_isTouchEvent(evt)
? evt.changedTouches[0]
: evt;
if (!timeBaseline) {
timeBaseline = nowTimestamp();
}
positions.push({
x: clientX,
y: clientY,
id: mirror.getId(target),
timeOffset: nowTimestamp() - timeBaseline,
});
wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent
? IncrementalSource.Drag
: evt instanceof MouseEvent
? IncrementalSource.MouseMove
: IncrementalSource.TouchMove);
}), threshold, {
trailing: false,
}));
const handlers = [
on('mousemove', updatePosition, doc),
on('touchmove', updatePosition, doc),
on('drag', 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 = [];
let currentPointerType = null;
const getHandler = (eventKey) => {
return (event) => {
const target = getEventTarget(event);
if (isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
return;
}
let pointerType = null;
let thisEventKey = eventKey;
if ('pointerType' in event) {
switch (event.pointerType) {
case 'mouse':
pointerType = PointerTypes.Mouse;
break;
case 'touch':
pointerType = PointerTypes.Touch;
break;
case 'pen':
pointerType = PointerTypes.Pen;
break;
}
if (pointerType === PointerTypes.Touch) {
if (MouseInteractions[eventKey] === MouseInteractions.MouseDown) {
thisEventKey = 'TouchStart';
}
else if (MouseInteractions[eventKey] === MouseInteractions.MouseUp) {
thisEventKey = 'TouchEnd';
}
}
else if (pointerType === PointerTypes.Pen) ;
}
else if (legacy_isTouchEvent(event)) {
pointerType = PointerTypes.Touch;
}
if (pointerType !== null) {
currentPointerType = pointerType;
if ((thisEventKey.startsWith('Touch') &&
pointerType === PointerTypes.Touch) ||
(thisEventKey.startsWith('Mouse') &&
pointerType === PointerTypes.Mouse)) {
pointerType = null;
}
}
else if (MouseInteractions[eventKey] === MouseInteractions.Click) {
pointerType = currentPointerType;
currentPointerType = null;
}
const e = legacy_isTouchEvent(event) ? event.changedTouches[0] : event;
if (!e) {
return;
}
const id = mirror.getId(target);
const { clientX, clientY } = e;
callbackWrapper(mouseInteractionCb)({
type: MouseInteractions[thisEventKey],
id,
x: clientX,
y: clientY,
...(pointerType !== null && { pointerType }),
});
};
};
Object.keys(MouseInteractions)
.filter((key) => Number.isNaN(Number(key)) &&
!key.endsWith('_Departed') &&
disableMap[key] !== false)
.forEach((eventKey) => {
let eventName = toLowerCase(eventKey);
const handler = getHandler(eventKey);
if (window.PointerEvent) {
switch (MouseInteractions[eventKey]) {
case MouseInteractions.MouseDown:
case MouseInteractions.MouseUp:
eventName = eventName.replace('mouse', 'pointer');
break;
case MouseInteractions.TouchStart:
case MouseInteractions.TouchEnd:
return;
}
}
handlers.push(on(eventName, handler, doc));
});
return callbackWrapper(() => {
handlers.forEach((h) => h());
});
}
function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, unblockSelector, sampling, }) {
const updatePosition = callbackWrapper(throttle$1(callbackWrapper((evt) => {
const target = getEventTarget(evt);
if (!target ||
isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
return;
}
const id = mirror.getId(target);
if (target === doc && doc.defaultView) {
const scrollLeftTop = getWindowScroll(doc.defaultView);
scrollCb({
id,
x: scrollLeftTop.left,
y: scrollLeftTop.top,
});
}
else {
scrollCb({
id,
x: target.scrollLeft,
y: target.scrollTop,
});
}
}), sampling.scroll || 100));
return on('scroll', updatePosition, doc);
}
function initViewportResizeObserver({ viewportResizeCb }, { win }) {
let lastH = -1;
let lastW = -1;
const updateDimension = callbackWrapper(throttle$1(callbackWrapper(() => {
const height = getWindowHeight();
const width = getWindowWidth();
if (lastH !== height || lastW !== width) {
viewportResizeCb({
width: Number(width),
height: Number(height),
});
lastH = height;
lastW = width;
}
}), 200));
return on('resize', updateDimension, win);
}
const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
const lastInputValueMap = new WeakMap();
function initInputObserver({ inputCb, doc, mirror, blockClass, blockSelector, unblockSelector, ignoreClass, ignoreSelector, maskInputOptions, maskInputFn, sampling, userTriggeredOnInput, maskTextClass, unmaskTextClass, maskTextSelector, unmaskTextSelector, }) {
function eventHandler(event) {
let target = getEventTarget(event);
const userTriggered = event.isTrusted;
const tagName = target && toUpperCase(target.tagName);
if (tagName === 'OPTION')
target = target.parentElement;
if (!target ||
!tagName ||
INPUT_TAGS.indexOf(tagName) < 0 ||
isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
return;
}
const el = target;
if (el.classList.contains(ignoreClass) ||
(ignoreSelector && el.matches(ignoreSelector))) {
return;
}
const type = getInputType(target);
let text = getInputValue(el, tagName, type);
let isChecked = false;
const isInputMasked = shouldMaskInput({
maskInputOptions,
tagName,
type,
});
const forceMask = needMaskingText(target, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, isInputMasked);
if (type === 'radio' || type === 'checkbox') {
isChecked = target.checked;
}
text = maskInputValue({
isMasked: forceMask,
element: target,
value: text,
maskInputFn,
});
cbWithDedup(target, userTriggeredOnInput
? { text, isChecked, userTriggered }
: { text, isChecked });
const name = target.name;
if (type === 'radio' && name && isChecked) {
doc
.querySelectorAll(`input[type="radio"][name="${name}"]`)
.forEach((el) => {
if (el !== target) {
const text = maskInputValue({
isMasked: forceMask,
element: el,
value: getInputValue(el, tagName, type),
maskInputFn,
});
cbWithDedup(el, userTriggeredOnInput
? { text, isChecked: !isChecked, userTriggered: false }
: { text, isChecked: !isChecked });
}
});
}
}
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);
callbackWrapper(inputCb)({
...v,
id,
});
}
}
const events = sampling.input === 'last' ? ['change'] : ['input', 'change'];
const handlers = events.map((eventName) => on(eventName, callbackWrapper(eventHandler), doc));
const currentWindow = doc.defaultView;
if (!currentWindow) {
return () => {
handlers.forEach((h) => h());
};
}
const propertyDescriptor = currentWindow.Object.getOwnPropertyDescriptor(currentWindow.HTMLInputElement.prototype, 'value');
const hookProperties = [
[currentWindow.HTMLInputElement.prototype, 'value'],
[currentWindow.HTMLInputElement.prototype, 'checked'],
[currentWindow.HTMLSelectElement.prototype, 'value'],
[currentWindow.HTMLTextAreaElement.prototype, 'value'],
[currentWindow.HTMLSelectElement.prototype, 'selectedIndex'],
[currentWindow.HTMLOptionElement.prototype, 'selected'],
];
if (propertyDescriptor && propertyDescriptor.set) {
handlers.push(...hookProperties.map((p) => hookSetter(p[0], p[1], {
set() {
callbackWrapper(eventHandler)({
target: this,
isTrusted: false,
});
},
}, false, currentWindow)));
}
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 if (childRule.parentStyleSheet) {
const rules = Array.from(childRule.parentStyleSheet.cssRules);
const index = rules.indexOf(childRule);
pos.unshift(index);
}
return pos;
}
return recurse(rule, positions);
}
function getIdAndStyleId(sheet, mirror, styleMirror) {
let id, styleId;
if (!sheet)
return {};
if (sheet.ownerNode)
id = mirror.getId(sheet.ownerNode);
else
styleId = styleMirror.getId(sheet);
return {
styleId,
id,
};
}
function initStyleSheetObserver({ styleSheetRuleCb, mirror, stylesheetManager }, { 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, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
if ((id && id !== -1) || (styleId && styleId !== -1)) {
styleSheetRuleCb({
id,
styleId,
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, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
if ((id && id !== -1) || (styleId && styleId !== -1)) {
styleSheetRuleCb({
id,
styleId,
removes: [{ index }],
});
}
return target.apply(thisArg, argumentsList);
}),
});
let replace;
if (win.CSSStyleSheet.prototype.replace) {
replace = win.CSSStyleSheet.prototype.replace;
win.CSSStyleSheet.prototype.replace = new Proxy(replace, {
apply: callbackWrapper((target, thisArg, argumentsList) => {
const [text] = argumentsList;
const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
if ((id && id !== -1) || (styleId && styleId !== -1)) {
styleSheetRuleCb({
id,
styleId,
replace: text,
});
}
return target.apply(thisArg, argumentsList);
}),
});
}
let replaceSync;
if (win.CSSStyleSheet.prototype.replaceSync) {
replaceSync = win.CSSStyleSheet.prototype.replaceSync;
win.CSSStyleSheet.prototype.replaceSync = new Proxy(replaceSync, {
apply: callbackWrapper((target, thisArg, argumentsList) => {
const [text] = argumentsList;
const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror);
if ((id && id !== -1) || (styleId && styleId !== -1)) {
styleSheetRuleCb({
id,
styleId,
replaceSync: text,
});
}
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, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror);
if ((id && id !== -1) || (styleId && styleId !== -1)) {
styleSheetRuleCb({
id,
styleId,
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, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror);
if ((id && id !== -1) || (styleId && styleId !== -1)) {
styleSheetRuleCb({
id,
styleId,
removes: [
{ index: [...getNestedCSSRulePositions(thisArg), index] },
],
});
}
return target.apply(thisArg, argumentsList);
}),
});
});
return callbackWrapper(() => {
win.CSSStyleSheet.prototype.insertRule = insertRule;
win.CSSStyleSheet.prototype.deleteRule = deleteRule;
replace && (win.CSSStyleSheet.prototype.replace = replace);
replaceSync && (win.CSSStyleSheet.prototype.replaceSync = replaceSync);
Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => {
type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule;
type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule;
});
});
}
function initAdoptedStyleSheetObserver({ mirror, stylesheetManager, }, host) {
let hostId = null;
if (host.nodeName === '#document')
hostId = mirror.getId(host);
else
hostId = mirror.getId(host.host);
const patchTarget = host.nodeName === '#document'
? _optionalChain$2([host, 'access', _4 => _4.defaultView, 'optionalAccess', _5 => _5.Document])
: _optionalChain$2([host, 'access', _6 => _6.ownerDocument, 'optionalAccess', _7 => _7.defaultView, 'optionalAccess', _8 => _8.ShadowRoot]);
const originalPropertyDescriptor = _optionalChain$2([patchTarget, 'optionalAccess', _9 => _9.prototype])
? Object.getOwnPropertyDescriptor(_optionalChain$2([patchTarget, 'optionalAccess', _10 => _10.prototype]), 'adoptedStyleSheets')
: undefined;
if (hostId === null ||
hostId === -1 ||
!patchTarget ||
!originalPropertyDescriptor)
return () => {
};
Object.defineProperty(host, 'adoptedStyleSheets', {
configurable: originalPropertyDescriptor.configurable,
enumerable: originalPropertyDescriptor.enumerable,
get() {
return _optionalChain$2([originalPropertyDescriptor, 'access', _11 => _11.get, 'optionalAccess', _12 => _12.call, 'call', _13 => _13(this)]);
},
set(sheets) {
const result = _optionalChain$2([originalPropertyDescriptor, 'access', _14 => _14.set, 'optionalAccess', _15 => _15.call, 'call', _16 => _16(this, sheets)]);
if (hostId !== null && hostId !== -1) {
try {
stylesheetManager.adoptStyleSheets(sheets, hostId);
}
catch (e) {
}
}
return result;
},
});
return callbackWrapper(() => {
Object.defineProperty(host, 'adoptedStyleSheets', {
configurable: originalPropertyDescriptor.configurable,
enumerable: originalPropertyDescriptor.enumerable,
get: originalPropertyDescriptor.get,
set: originalPropertyDescriptor.set,
});
});
}
function initStyleDeclarationObserver({ styleDeclarationCb, mirror, ignoreCSSAttributes, stylesheetManager, }, { win }) {
const setProperty = win.CSSStyleDeclaration.prototype.setProperty;
win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, {
apply: callbackWrapper((target, thisArg, argumentsList) => {
const [property, value, priority] = argumentsList;
if (ignoreCSSAttributes.has(property)) {
return setProperty.apply(thisArg, [property, value, priority]);
}
const { id, styleId } = getIdAndStyleId(_optionalChain$2([thisArg, 'access', _17 => _17.parentRule, 'optionalAccess', _18 => _18.parentStyleSheet]), mirror, stylesheetManager.styleMirror);
if ((id && id !== -1) || (styleId && styleId !== -1)) {
styleDeclarationCb({
id,
styleId,
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) => {
const [property] = argumentsList;
if (ignoreCSSAttributes.has(property)) {
return removeProperty.apply(thisArg, [property]);
}
const { id, styleId } = getIdAndStyleId(_optionalChain$2([thisArg, 'access', _19 => _19.parentRule, 'optionalAccess', _20 => _20.parentStyleSheet]), mirror, stylesheetManager.styleMirror);
if ((id && id !== -1) || (styleId && styleId !== -1)) {
styleDeclarationCb({
id,
styleId,
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, doc, }) {
const handler = callbackWrapper((type) => throttle$1(callbackWrapper((event) => {
const target = getEventTarget(event);
if (!target ||
isBlocked(target, blockClass, blockSelector, unblockSelector, true)) {
return;
}
const { currentTime, volume, muted, playbackRate } = target;
mediaInteractionCb({
type,
id: mirror.getId(target),
currentTime,
volume,
muted,
playbackRate,
});
}), sampling.media || 500));
const handlers = [
on('play', handler(0), doc),
on('pause', handler(1), doc),
on('seeked', handler(2), doc),
on('volumechange', handler(3), doc),
on('ratechange', handler(4), doc),
];
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(callbackWrapper(() => {
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 initSelectionObserver(param) {
const { doc, mirror, blockClass, blockSelector, unblockSelector, selectionCb, } = param;
let collapsed = true;
const updateSelection = callbackWrapper(() => {
const selection = doc.getSelection();
if (!selection || (collapsed && _optionalChain$2([selection, 'optionalAccess', _21 => _21.isCollapsed])))
return;
collapsed = selection.isCollapsed || false;
const ranges = [];
const count = selection.rangeCount || 0;
for (let i = 0; i < count; i++) {
const range = selection.getRangeAt(i);
const { startContainer, startOffset, endContainer, endOffset } = range;
const blocked = isBlocked(startContainer, blockClass, blockSelector, unblockSelector, true) ||
isBlocked(endContainer, blockClass, blockSelector, unblockSelector, true);
if (blocked)
continue;
ranges.push({
start: mirror.getId(startContainer),
startOffset,
end: mirror.getId(endContainer),
endOffset,
});
}
selectionCb({ ranges });
});
updateSelection();
return on('selectionchange', updateSelection);
}
function initCustomElementObserver({ doc, customElementCb, }) {
const win = doc.defaultView;
if (!win || !win.customElements)
return () => { };
const restoreHandler = patch(win.customElements, 'define', function (original) {
return function (name, constructor, options) {
try {
customElementCb({
define: {
name,
},
});
}
catch (e) {
}
return original.apply(this, [name, constructor, options]);
};
});
return restoreHandler;
}
function initObservers(o, _hooks = {}) {
const currentWindow = o.doc.defaultView;
if (!currentWindow) {
return () => {
};
}
const mutationObserver = initMutationObserver(o, o.doc);
const mousemoveHandler = initMoveObserver(o);
const mouseInteractionHandler = initMouseInteractionObserver(o);
const scrollHandler = initScrollObserver(o);
const viewportResizeHandler = initViewportResizeObserver(o, {
win: currentWindow,
});
const inputHandler = initInputObserver(o);
const mediaInteractionHandler = initMediaInteractionObserver(o);
const styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow });
const adoptedStyleSheetObserver = initAdoptedStyleSheetObserver(o, o.doc);
const styleDeclarationObserver = initStyleDeclarationObserver(o, {
win: currentWindow,
});
const fontObserver = o.collectFonts
? initFontObserver(o)
: () => {
};
const selectionObserver = initSelectionObserver(o);
const customElementObserver = initCustomElementObserver(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();
styleSheetObserver();
adoptedStyleSheetObserver();
styleDeclarationObserver();
fontObserver();
selectionObserver();
customElementObserver();
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 CrossOriginIframeMirror {
constructor(generateIdFn) {
this.generateIdFn = generateIdFn;
this.iframeIdToRemoteIdMap = new WeakMap();
this.iframeRemoteIdToIdMap = new WeakMap();
}
getId(iframe, remoteId, idToRemoteMap, remoteToIdMap) {
const idToRemoteIdMap = idToRemoteMap || this.getIdToRemoteIdMap(iframe);
const remoteIdToIdMap = remoteToIdMap || this.getRemoteIdToIdMap(iframe);
let id = idToRemoteIdMap.get(remoteId);
if (!id) {
id = this.generateIdFn();
idToRemoteIdMap.set(remoteId, id);
remoteIdToIdMap.set(id, remoteId);
}
return id;
}
getIds(iframe, remoteId) {
const idToRemoteIdMap = this.getIdToRemoteIdMap(iframe);
const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe);
return remoteId.map((id) => this.getId(iframe, id, idToRemoteIdMap, remoteIdToIdMap));
}
getRemoteId(iframe, id, map) {
const remoteIdToIdMap = map || this.getRemoteIdToIdMap(iframe);
if (typeof id !== 'number')
return id;
const remoteId = remoteIdToIdMap.get(id);
if (!remoteId)
return -1;
return remoteId;
}
getRemoteIds(iframe, ids) {
const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe);
return ids.map((id) => this.getRemoteId(iframe, id, remoteIdToIdMap));
}
reset(iframe) {
if (!iframe) {
this.iframeIdToRemoteIdMap = new WeakMap();
this.iframeRemoteIdToIdMap = new WeakMap();
return;
}
this.iframeIdToRemoteIdMap.delete(iframe);
this.iframeRemoteIdToIdMap.delete(iframe);
}
getIdToRemoteIdMap(iframe) {
let idToRemoteIdMap = this.iframeIdToRemoteIdMap.get(iframe);
if (!idToRemoteIdMap) {
idToRemoteIdMap = new Map();
this.iframeIdToRemoteIdMap.set(iframe, idToRemoteIdMap);
}
return idToRemoteIdMap;
}
getRemoteIdToIdMap(iframe) {
let remoteIdToIdMap = this.iframeRemoteIdToIdMap.get(iframe);
if (!remoteIdToIdMap) {
remoteIdToIdMap = new Map();
this.iframeRemoteIdToIdMap.set(iframe, remoteIdToIdMap);
}
return remoteIdToIdMap;
}
}
function _optionalChain$1(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
class IframeManagerNoop {
constructor() {
this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId);
this.crossOriginIframeRootIdMap = new WeakMap();
}
addIframe() {
}
addLoadListener() {
}
attachIframe() {
}
}
class IframeManager {
constructor(options) {
this.iframes = new WeakMap();
this.crossOriginIframeMap = new WeakMap();
this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId);
this.crossOriginIframeRootIdMap = new WeakMap();
this.mutationCb = options.mutationCb;
this.wrappedEmit = options.wrappedEmit;
this.stylesheetManager = options.stylesheetManager;
this.recordCrossOriginIframes = options.recordCrossOriginIframes;
this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror));
this.mirror = options.mirror;
if (this.recordCrossOriginIframes) {
window.addEventListener('message', this.handleMessage.bind(this));
}
}
addIframe(iframeEl) {
this.iframes.set(iframeEl, true);
if (iframeEl.contentWindow)
this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl);
}
addLoadListener(cb) {
this.loadListener = cb;
}
attachIframe(iframeEl, childSn) {
this.mutationCb({
adds: [
{
parentId: this.mirror.getId(iframeEl),
nextId: null,
node: childSn,
},
],
removes: [],
texts: [],
attributes: [],
isAttachIframe: true,
});
_optionalChain$1([this, 'access', _ => _.loadListener, 'optionalCall', _2 => _2(iframeEl)]);
if (iframeEl.contentDocument &&
iframeEl.contentDocument.adoptedStyleSheets &&
iframeEl.contentDocument.adoptedStyleSheets.length > 0)
this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument));
}
handleMessage(message) {
const crossOriginMessageEvent = message;
if (crossOriginMessageEvent.data.type !== 'rrweb' ||
crossOriginMessageEvent.origin !== crossOriginMessageEvent.data.origin)
return;
const iframeSourceWindow = message.source;
if (!iframeSourceWindow)
return;
const iframeEl = this.crossOriginIframeMap.get(message.source);
if (!iframeEl)
return;
const transformedEvent = this.transformCrossOriginEvent(iframeEl, crossOriginMessageEvent.data.event);
if (transformedEvent)
this.wrappedEmit(transformedEvent, crossOriginMessageEvent.data.isCheckout);
}
transformCrossOriginEvent(iframeEl, e) {
switch (e.type) {
case EventType.FullSnapshot: {
this.crossOriginIframeMirror.reset(iframeEl);
this.crossOriginIframeStyleMirror.reset(iframeEl);
this.replaceIdOnNode(e.data.node, iframeEl);
const rootId = e.data.node.id;
this.crossOriginIframeRootIdMap.set(iframeEl, rootId);
this.patchRootIdOnNode(e.data.node, rootId);
return {
timestamp: e.timestamp,
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
adds: [
{
parentId: this.mirror.getId(iframeEl),
nextId: null,
node: e.data.node,
},
],
removes: [],
texts: [],
attributes: [],
isAttachIframe: true,
},
};
}
case EventType.Meta:
case EventType.Load:
case EventType.DomContentLoaded: {
return false;
}
case EventType.Plugin: {
return e;
}
case EventType.Custom: {
this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']);
return e;
}
case EventType.IncrementalSnapshot: {
switch (e.data.source) {
case IncrementalSource.Mutation: {
e.data.adds.forEach((n) => {
this.replaceIds(n, iframeEl, [
'parentId',
'nextId',
'previousId',
]);
this.replaceIdOnNode(n.node, iframeEl);
const rootId = this.crossOriginIframeRootIdMap.get(iframeEl);
rootId && this.patchRootIdOnNode(n.node, rootId);
});
e.data.removes.forEach((n) => {
this.replaceIds(n, iframeEl, ['parentId', 'id']);
});
e.data.attributes.forEach((n) => {
this.replaceIds(n, iframeEl, ['id']);
});
e.data.texts.forEach((n) => {
this.replaceIds(n, iframeEl, ['id']);
});
return e;
}
case IncrementalSource.Drag:
case IncrementalSource.TouchMove:
case IncrementalSource.MouseMove: {
e.data.positions.forEach((p) => {
this.replaceIds(p, iframeEl, ['id']);
});
return e;
}
case IncrementalSource.ViewportResize: {
return false;
}
case IncrementalSource.MediaInteraction:
case IncrementalSource.MouseInteraction:
case IncrementalSource.Scroll:
case IncrementalSource.CanvasMutation:
case IncrementalSource.Input: {
this.replaceIds(e.data, iframeEl, ['id']);
return e;
}
case IncrementalSource.StyleSheetRule:
case IncrementalSource.StyleDeclaration: {
this.replaceIds(e.data, iframeEl, ['id']);
this.replaceStyleIds(e.data, iframeEl, ['styleId']);
return e;
}
case IncrementalSource.Font: {
return e;
}
case IncrementalSource.Selection: {
e.data.ranges.forEach((range) => {
this.replaceIds(range, iframeEl, ['start', 'end']);
});
return e;
}
case IncrementalSource.AdoptedStyleSheet: {
this.replaceIds(e.data, iframeEl, ['id']);
this.replaceStyleIds(e.data, iframeEl, ['styleIds']);
_optionalChain$1([e, 'access', _3 => _3.data, 'access', _4 => _4.styles, 'optionalAccess', _5 => _5.forEach, 'call', _6 => _6((style) => {
this.replaceStyleIds(style, iframeEl, ['styleId']);
})]);
return e;
}
}
}
}
return false;
}
replace(iframeMirror, obj, iframeEl, keys) {
for (const key of keys) {
if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number')
continue;
if (Array.isArray(obj[key])) {
obj[key] = iframeMirror.getIds(iframeEl, obj[key]);
}
else {
obj[key] = iframeMirror.getId(iframeEl, obj[key]);
}
}
return obj;
}
replaceIds(obj, iframeEl, keys) {
return this.replace(this.crossOriginIframeMirror, obj, iframeEl, keys);
}
replaceStyleIds(obj, iframeEl, keys) {
return this.replace(this.crossOriginIframeStyleMirror, obj, iframeEl, keys);
}
replaceIdOnNode(node, iframeEl) {
this.replaceIds(node, iframeEl, ['id', 'rootId']);
if ('childNodes' in node) {
node.childNodes.forEach((child) => {
this.replaceIdOnNode(child, iframeEl);
});
}
}
patchRootIdOnNode(node, rootId) {
if (node.type !== NodeType$1.Document && !node.rootId)
node.rootId = rootId;
if ('childNodes' in node) {
node.childNodes.forEach((child) => {
this.patchRootIdOnNode(child, rootId);
});
}
}
}
class ShadowDomManagerNoop {
init() {
}
addShadowRoot() {
}
observeAttachShadow() {
}
reset() {
}
}
class ShadowDomManager {
constructor(options) {
this.shadowDoms = new WeakSet();
this.restoreHandlers = [];
this.mutationCb = options.mutationCb;
this.scrollCb = options.scrollCb;
this.bypassOptions = options.bypassOptions;
this.mirror = options.mirror;
this.init();
}
init() {
this.reset();
this.patchAttachShadow(Element, document);
}
addShadowRoot(shadowRoot, doc) {
if (!isNativeShadowDom(shadowRoot))
return;
if (this.shadowDoms.has(shadowRoot))
return;
this.shadowDoms.add(shadowRoot);
const observer = initMutationObserver({
...this.bypassOptions,
doc,
mutationCb: this.mutationCb,
mirror: this.mirror,
shadowDomManager: this,
}, shadowRoot);
this.restoreHandlers.push(() => observer.disconnect());
this.restoreHandlers.push(initScrollObserver({
...this.bypassOptions,
scrollCb: this.scrollCb,
doc: shadowRoot,
mirror: this.mirror,
}));
setTimeout(() => {
if (shadowRoot.adoptedStyleSheets &&
shadowRoot.adoptedStyleSheets.length > 0)
this.bypassOptions.stylesheetManager.adoptStyleSheets(shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host));
this.restoreHandlers.push(initAdoptedStyleSheetObserver({
mirror: this.mirror,
stylesheetManager: this.bypassOptions.stylesheetManager,
}, shadowRoot));
}, 0);
}
observeAttachShadow(iframeElement) {
if (!iframeElement.contentWindow || !iframeElement.contentDocument)
return;
this.patchAttachShadow(iframeElement.contentWindow.Element, iframeElement.contentDocument);
}
patchAttachShadow(element, doc) {
const manager = this;
this.restoreHandlers.push(patch(element.prototype, 'attachShadow', function (original) {
return function (option) {
const shadowRoot = original.call(this, option);
if (this.shadowRoot && inDom(this))
manager.addShadowRoot(this.shadowRoot, doc);
return shadowRoot;
};
}));
}
reset() {
this.restoreHandlers.forEach((handler) => {
try {
handler();
}
catch (e) {
}
});
this.restoreHandlers = [];
this.shadowDoms = new WeakSet();
}
}
class CanvasManagerNoop {
reset() {
}
freeze() {
}
unfreeze() {
}
lock() {
}
unlock() {
}
snapshot() {
}
}
class StylesheetManager {
constructor(options) {
this.trackedLinkElements = new WeakSet();
this.styleMirror = new StyleSheetMirror();
this.mutationCb = options.mutationCb;
this.adoptedStyleSheetCb = options.adoptedStyleSheetCb;
}
attachLinkElement(linkEl, childSn) {
if ('_cssText' in childSn.attributes)
this.mutationCb({
adds: [],
removes: [],
texts: [],
attributes: [
{
id: childSn.id,
attributes: childSn
.attributes,
},
],
});
this.trackLinkElement(linkEl);
}
trackLinkElement(linkEl) {
if (this.trackedLinkElements.has(linkEl))
return;
this.trackedLinkElements.add(linkEl);
this.trackStylesheetInLinkElement(linkEl);
}
adoptStyleSheets(sheets, hostId) {
if (sheets.length === 0)
return;
const adoptedStyleSheetData = {
id: hostId,
styleIds: [],
};
const styles = [];
for (const sheet of sheets) {
let styleId;
if (!this.styleMirror.has(sheet)) {
styleId = this.styleMirror.add(sheet);
styles.push({
styleId,
rules: Array.from(sheet.rules || CSSRule, (r, index) => ({
rule: stringifyRule(r),
index,
})),
});
}
else
styleId = this.styleMirror.getId(sheet);
adoptedStyleSheetData.styleIds.push(styleId);
}
if (styles.length > 0)
adoptedStyleSheetData.styles = styles;
this.adoptedStyleSheetCb(adoptedStyleSheetData);
}
reset() {
this.styleMirror.reset();
this.trackedLinkElements = new WeakSet();
}
trackStylesheetInLinkElement(linkEl) {
}
}
class ProcessedNodeManager {
constructor() {
this.nodeMap = new WeakMap();
this.loop = true;
this.periodicallyClear();
}
periodicallyClear() {
onRequestAnimationFrame(() => {
this.clear();
if (this.loop)
this.periodicallyClear();
});
}
inOtherBuffer(node, thisBuffer) {
const buffers = this.nodeMap.get(node);
return (buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer));
}
add(node, buffer) {
this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer));
}
clear() {
this.nodeMap = new WeakMap();
}
destroy() {
this.loop = false;
}
}
function wrapEvent(e) {
const eWithTime = e;
eWithTime.timestamp = nowTimestamp();
return eWithTime;
}
let _takeFullSnapshot;
const mirror = createMirror();
function record(options = {}) {
const { emit, checkoutEveryNms, checkoutEveryNth, blockClass = 'rr-block', blockSelector = null, unblockSelector = null, ignoreClass = 'rr-ignore', ignoreSelector = null, maskAllText = false, maskTextClass = 'rr-mask', unmaskTextClass = null, maskTextSelector = null, unmaskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, slimDOMOptions: _slimDOMOptions, maskAttributeFn, maskInputFn, maskTextFn, packFn, sampling = {}, dataURLOptions = {}, mousemoveWait, recordCanvas = false, recordCrossOriginIframes = false, recordAfter = options.recordAfter === 'DOMContentLoaded'
? options.recordAfter
: 'load', userTriggeredOnInput = false, collectFonts = false, inlineImages = false, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), errorHandler, onMutation, getCanvasManager, } = options;
registerErrorHandler(errorHandler);
const inEmittingFrame = recordCrossOriginIframes
? window.parent === window
: true;
let passEmitsToParent = false;
if (!inEmittingFrame) {
try {
if (window.parent.document) {
passEmitsToParent = false;
}
}
catch (e) {
passEmitsToParent = true;
}
}
if (inEmittingFrame && !emit) {
throw new Error('emit function is required');
}
if (mousemoveWait !== undefined && sampling.mousemove === undefined) {
sampling.mousemove = mousemoveWait;
}
mirror.reset();
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 &&
!passEmitsToParent) {
e = packFn(e);
}
return e;
};
const wrappedEmit = (e, isCheckout) => {
if (_optionalChain([mutationBuffers, 'access', _ => _[0], 'optionalAccess', _2 => _2.isFrozen, 'call', _3 => _3()]) &&
e.type !== EventType.FullSnapshot &&
!(e.type === EventType.IncrementalSnapshot &&
e.data.source === IncrementalSource.Mutation)) {
mutationBuffers.forEach((buf) => buf.unfreeze());
}
if (inEmittingFrame) {
_optionalChain([emit, 'optionalCall', _4 => _4(eventProcessor(e), isCheckout)]);
}
else if (passEmitsToParent) {
const message = {
type: 'rrweb',
event: eventProcessor(e),
origin: window.location.origin,
isCheckout,
};
window.parent.postMessage(message, '*');
}
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: {
source: IncrementalSource.Mutation,
...m,
},
}));
};
const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Scroll,
...p,
},
}));
const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.CanvasMutation,
...p,
},
}));
const wrappedAdoptedStyleSheetEmit = (a) => wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.AdoptedStyleSheet,
...a,
},
}));
const stylesheetManager = new StylesheetManager({
mutationCb: wrappedMutationEmit,
adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit,
});
const iframeManager = typeof __RRWEB_EXCLUDE_IFRAME__ === 'boolean' && __RRWEB_EXCLUDE_IFRAME__
? new IframeManagerNoop()
: new IframeManager({
mirror,
mutationCb: wrappedMutationEmit,
stylesheetManager: stylesheetManager,
recordCrossOriginIframes,
wrappedEmit,
});
for (const plugin of plugins || []) {
if (plugin.getMirror)
plugin.getMirror({
nodeMirror: mirror,
crossOriginIframeMirror: iframeManager.crossOriginIframeMirror,
crossOriginIframeStyleMirror: iframeManager.crossOriginIframeStyleMirror,
});
}
const processedNodeManager = new ProcessedNodeManager();
const canvasManager = _getCanvasManager(getCanvasManager, {
mirror,
win: window,
mutationCb: (p) => wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.CanvasMutation,
...p,
},
})),
recordCanvas,
blockClass,
blockSelector,
unblockSelector,
sampling: sampling['canvas'],
dataURLOptions,
errorHandler,
});
const shadowDomManager = typeof __RRWEB_EXCLUDE_SHADOW_DOM__ === 'boolean' &&
__RRWEB_EXCLUDE_SHADOW_DOM__
? new ShadowDomManagerNoop()
: new ShadowDomManager({
mutationCb: wrappedMutationEmit,
scrollCb: wrappedScrollEmit,
bypassOptions: {
onMutation,
blockClass,
blockSelector,
unblockSelector,
maskAllText,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
inlineStylesheet,
maskInputOptions,
dataURLOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
recordCanvas,
inlineImages,
sampling,
slimDOMOptions,
iframeManager,
stylesheetManager,
canvasManager,
keepIframeSrcFn,
processedNodeManager,
},
mirror,
});
const takeFullSnapshot = (isCheckout = false) => {
wrappedEmit(wrapEvent({
type: EventType.Meta,
data: {
href: window.location.href,
width: getWindowWidth(),
height: getWindowHeight(),
},
}), isCheckout);
stylesheetManager.reset();
shadowDomManager.init();
mutationBuffers.forEach((buf) => buf.lock());
const node = snapshot(document, {
mirror,
blockClass,
blockSelector,
unblockSelector,
maskAllText,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
inlineStylesheet,
maskAllInputs: maskInputOptions,
maskAttributeFn,
maskInputFn,
maskTextFn,
slimDOM: slimDOMOptions,
dataURLOptions,
recordCanvas,
inlineImages,
onSerialize: (n) => {
if (isSerializedIframe(n, mirror)) {
iframeManager.addIframe(n);
}
if (isSerializedStylesheet(n, mirror)) {
stylesheetManager.trackLinkElement(n);
}
if (hasShadowRoot(n)) {
shadowDomManager.addShadowRoot(n.shadowRoot, document);
}
},
onIframeLoad: (iframe, childSn) => {
iframeManager.attachIframe(iframe, childSn);
shadowDomManager.observeAttachShadow(iframe);
},
onStylesheetLoad: (linkEl, childSn) => {
stylesheetManager.attachLinkElement(linkEl, childSn);
},
keepIframeSrcFn,
});
if (!node) {
return console.warn('Failed to snapshot the document');
}
wrappedEmit(wrapEvent({
type: EventType.FullSnapshot,
data: {
node,
initialOffset: getWindowScroll(window),
},
}));
mutationBuffers.forEach((buf) => buf.unlock());
if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0)
stylesheetManager.adoptStyleSheets(document.adoptedStyleSheets, mirror.getId(document));
};
_takeFullSnapshot = takeFullSnapshot;
try {
const handlers = [];
const observe = (doc) => {
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: {
source: IncrementalSource.MouseInteraction,
...d,
},
})),
scrollCb: wrappedScrollEmit,
viewportResizeCb: (d) => wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.ViewportResize,
...d,
},
})),
inputCb: (v) => wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Input,
...v,
},
})),
mediaInteractionCb: (p) => wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.MediaInteraction,
...p,
},
})),
styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.StyleSheetRule,
...r,
},
})),
styleDeclarationCb: (r) => wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.StyleDeclaration,
...r,
},
})),
canvasMutationCb: wrappedCanvasMutationEmit,
fontCb: (p) => wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Font,
...p,
},
})),
selectionCb: (p) => {
wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Selection,
...p,
},
}));
},
customElementCb: (c) => {
wrappedEmit(wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.CustomElement,
...c,
},
}));
},
blockClass,
ignoreClass,
ignoreSelector,
maskAllText,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
maskInputOptions,
inlineStylesheet,
sampling,
recordCanvas,
inlineImages,
userTriggeredOnInput,
collectFonts,
doc,
maskAttributeFn,
maskInputFn,
maskTextFn,
keepIframeSrcFn,
blockSelector,
unblockSelector,
slimDOMOptions,
dataURLOptions,
mirror,
iframeManager,
stylesheetManager,
shadowDomManager,
processedNodeManager,
canvasManager,
ignoreCSSAttributes,
plugins: _optionalChain([plugins
, 'optionalAccess', _5 => _5.filter, 'call', _6 => _6((p) => p.observer)
, 'optionalAccess', _7 => _7.map, 'call', _8 => _8((p) => ({
observer: p.observer,
options: p.options,
callback: (payload) => wrappedEmit(wrapEvent({
type: EventType.Plugin,
data: {
plugin: p.name,
payload,
},
})),
}))]) || [],
}, {});
};
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('DOMContentLoaded', () => {
wrappedEmit(wrapEvent({
type: EventType.DomContentLoaded,
data: {},
}));
if (recordAfter === 'DOMContentLoaded')
init();
}));
handlers.push(on('load', () => {
wrappedEmit(wrapEvent({
type: EventType.Load,
data: {},
}));
if (recordAfter === 'load')
init();
}, window));
}
return () => {
handlers.forEach((h) => h());
processedNodeManager.destroy();
_takeFullSnapshot = undefined;
unregisterErrorHandler();
};
}
catch (error) {
console.warn(error);
}
}
function takeFullSnapshot(isCheckout) {
if (!_takeFullSnapshot) {
throw new Error('please take full snapshot after start recording');
}
_takeFullSnapshot(isCheckout);
}
record.mirror = mirror;
record.takeFullSnapshot = takeFullSnapshot;
function _getCanvasManager(getCanvasManagerFn, options) {
try {
return getCanvasManagerFn
? getCanvasManagerFn(options)
: new CanvasManagerNoop();
}
catch (e2) {
console.warn('Unable to initialize CanvasManager');
return new CanvasManagerNoop();
}
}
const ReplayEventTypeIncrementalSnapshot = 3;
const ReplayEventTypeCustom = 5;
/**
* 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;
}
/**
* Converts a timestamp to s, if it was in ms, or keeps it as s.
*/
function timestampToS(timestamp) {
const isMs = timestamp > 9999999999;
return isMs ? timestamp / 1000 : timestamp;
}
/**
* 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(() => {
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
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';
/** Get the closest interactive parent element, or else return the given element. */
function getClosestInteractive(element) {
const closestInteractive = element.closest(INTERACTIVE_SELECTOR);
return closestInteractive || element;
}
/**
* 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;
}
return getClosestInteractive(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
constructor(
replay,
slowClickConfig,
// Just for easier testing
_addBreadcrumbEvent = addBreadcrumbEvent,
) {
this._lastMutation = 0;
this._lastScroll = 0;
this._clicks = [];
// We want everything in s, but options are in ms
this._timeout = slowClickConfig.timeout / 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 cleanupWindowOpen = onWindowOpen(() => {
// Treat window.open as mutation
this._lastMutation = nowInSeconds();
});
this._teardown = () => {
cleanupWindowOpen();
this._clicks = [];
this._lastMutation = 0;
this._lastScroll = 0;
};
}
/** Clean up listeners. */
removeListeners() {
if (this._teardown) {
this._teardown();
}
if (this._checkClickTimeout) {
clearTimeout(this._checkClickTimeout);
}
}
/** @inheritDoc */
handleClick(breadcrumb, node) {
if (ignoreElement(node, this._ignoreSelector) || !isClickBreadcrumb(breadcrumb)) {
return;
}
const newClick = {
timestamp: timestampToS(breadcrumb.timestamp),
clickBreadcrumb: breadcrumb,
// Set this to 0 so we know it originates from the click breadcrumb
clickCount: 0,
node,
};
// If there was a click in the last 1s on the same element, ignore it - only keep a single reference per second
if (
this._clicks.some(click => click.node === newClick.node && Math.abs(click.timestamp - newClick.timestamp) < 1)
) {
return;
}
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();
}
}
/** @inheritDoc */
registerMutation(timestamp = Date.now()) {
this._lastMutation = timestampToS(timestamp);
}
/** @inheritDoc */
registerScroll(timestamp = Date.now()) {
this._lastScroll = timestampToS(timestamp);
}
/** @inheritDoc */
registerClick(element) {
const node = getClosestInteractive(element);
this._handleMultiClick(node );
}
/** Count multiple clicks on elements. */
_handleMultiClick(node) {
this._getClicks(node).forEach(click => {
click.clickCount++;
});
}
/** Get all pending clicks for a given node. */
_getClicks(node) {
return this._clicks.filter(click => click.node === node);
}
/** 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;
}
// All of these are in seconds!
if (click.timestamp + this._timeout <= now) {
timedOutClicks.push(click);
}
});
// Remove "old" clicks
for (const click of timedOutClicks) {
const pos = this._clicks.indexOf(click);
if (pos > -1) {
this._generateBreadcrumbs(click);
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() {
if (this._checkClickTimeout) {
clearTimeout(this._checkClickTimeout);
}
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;
}
/** Update the click detector based on a recording event of rrweb. */
function updateClickDetectorForRecordingEvent(clickDetector, event) {
try {
// note: We only consider incremental snapshots here
// This means that any full snapshot is ignored for mutation detection - the reason is that we simply cannot know if a mutation happened here.
// E.g. think that we are buffering, an error happens and we take a full snapshot because we switched to session mode -
// in this scenario, we would not know if a dead click happened because of the error, which is a key dead click scenario.
// Instead, by ignoring full snapshots, we have the risk that we generate a false positive
// (if a mutation _did_ happen but was "swallowed" by the full snapshot)
// But this should be more unlikely as we'd generally capture the incremental snapshot right away
if (!isIncrementalEvent(event)) {
return;
}
const { source } = event.data;
if (source === IncrementalSource.Mutation) {
clickDetector.registerMutation(event.timestamp);
}
if (source === IncrementalSource.Scroll) {
clickDetector.registerScroll(event.timestamp);
}
if (isIncrementalMouseInteraction(event)) {
const { type, id } = event.data;
const node = record.mirror.getNode(id);
if (node instanceof HTMLElement && type === MouseInteractions.Click) {
clickDetector.registerClick(node);
}
}
} catch (e) {
// ignore errors here, e.g. if accessing something that does not exist
}
}
function isIncrementalEvent(event) {
return event.type === ReplayEventTypeIncrementalSnapshot;
}
function isIncrementalMouseInteraction(
event,
) {
return event.data.source === IncrementalSource.MouseInteraction;
}
/**
* 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',
'data-sentry-component',
]);
/**
* 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 ) : undefined;
// Ignore clicks if ctrl/alt/meta/shift keys are held down as they alter behavior of clicks (e.g. open in new tab)
if (
isClick &&
replay.clickDetector &&
event &&
event.target &&
!event.altKey &&
!event.metaKey &&
!event.ctrlKey &&
!event.shiftKey
) {
handleClick(
replay.clickDetector,
result ,
getClickTargetNode(handlerData.event ) ,
);
}
addBreadcrumbEvent(replay, result);
};
};
/** Get the base DOM breadcrumb. */
function getBaseDomBreadcrumb(target, message) {
const nodeId = record.mirror.getId(target);
const node = nodeId && record.mirror.getNode(nodeId);
const meta = node && record.mirror.getMeta(node);
const element = meta && isElement(meta) ? meta : null;
return {
message,
data: element
? {
nodeId,
node: {
id: nodeId,
tagName: element.tagName,
textContent: Array.from(element.childNodes)
.map((node) => node.type === NodeType.Text && node.textContent)
.filter(Boolean) // filter out empty values
.map(text => (text ).trim())
.join(''),
attributes: getAttributesToRecord(element.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 isElement(node) {
return node.type === NodeType.Element;
}
/** 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;
}
// Map entryType -> function to normalize data for event
const ENTRY_TYPES
= {
// @ts-expect-error TODO: entry type does not fit the create* functions entry type
resource: createResourceEntry,
paint: createPaintEntry,
// @ts-expect-error TODO: entry type does not fit the create* functions entry type
navigation: createNavigationEntry,
};
/**
* 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]) {
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,
},
};
}
/**
* Add a LCP event to the replay based on an LCP metric.
*/
function getLargestContentfulPaint(metric
) {
const entries = metric.entries;
const lastEntry = entries[entries.length - 1] ;
const element = lastEntry ? lastEntry.element : undefined;
const value = metric.value;
const end = getAbsoluteTime(value);
const data = {
type: 'largest-contentful-paint',
name: 'largest-contentful-paint',
start: end,
end,
data: {
value,
size: value,
nodeId: element ? record.mirror.getId(element) : undefined,
},
};
return data;
}
/**
* Sets up a PerformanceObserver to listen to all performance entry types.
* Returns a callback to stop observing.
*/
function setupPerformanceObserver(replay) {
function addPerformanceEntry(entry) {
// It is possible for entries to come up multiple times
if (!replay.performanceEntries.includes(entry)) {
replay.performanceEntries.push(entry);
}
}
function onEntries({ entries }) {
entries.forEach(addPerformanceEntry);
}
const clearCallbacks = [];
(['navigation', 'paint', 'resource'] ).forEach(type => {
clearCallbacks.push(addPerformanceInstrumentationHandler(type, onEntries));
});
clearCallbacks.push(
addLcpInstrumentationHandler(({ metric }) => {
replay.replayPerformanceEntries.push(getLargestContentfulPaint(metric));
}),
);
// A callback to cleanup all handlers
return () => {
clearCallbacks.forEach(clearCallback => clearCallback());
};
}
/**
* This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.
*
* ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
*/
const DEBUG_BUILD = (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__);
const r = `var t=Uint8Array,n=Uint16Array,r=Int32Array,e=new t([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,0,0,0]),i=new t([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,0,0]),a=new t([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),s=function(t,e){for(var i=new n(31),a=0;a<31;++a)i[a]=e+=1<>1|(21845&c)<<1;v=(61680&(v=(52428&v)>>2|(13107&v)<<2))>>4|(3855&v)<<4,u[c]=((65280&v)>>8|(255&v)<<8)>>1}var d=function(t,r,e){for(var i=t.length,a=0,s=new n(r);a>h]=l}else for(o=new n(i),a=0;a>15-t[a]);return o},g=new t(288);for(c=0;c<144;++c)g[c]=8;for(c=144;c<256;++c)g[c]=9;for(c=256;c<280;++c)g[c]=7;for(c=280;c<288;++c)g[c]=8;var w=new t(32);for(c=0;c<32;++c)w[c]=5;var p=d(g,9,0),y=d(w,5,0),m=function(t){return(t+7)/8|0},b=function(n,r,e){return(null==r||r<0)&&(r=0),(null==e||e>n.length)&&(e=n.length),new t(n.subarray(r,e))},M=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],E=function(t,n,r){var e=new Error(n||M[t]);if(e.code=t,Error.captureStackTrace&&Error.captureStackTrace(e,E),!r)throw e;return e},z=function(t,n,r){r<<=7&n;var e=n/8|0;t[e]|=r,t[e+1]|=r>>8},A=function(t,n,r){r<<=7&n;var e=n/8|0;t[e]|=r,t[e+1]|=r>>8,t[e+2]|=r>>16},_=function(r,e){for(var i=[],a=0;ad&&(d=o[a].s);var g=new n(d+1),w=x(i[c-1],g,0);if(w>e){a=0;var p=0,y=w-e,m=1<e))break;p+=m-(1<>=y;p>0;){var M=o[a].s;g[M]=0&&p;--a){var E=o[a].s;g[E]==e&&(--g[E],++p)}w=e}return{t:new t(g),l:w}},x=function(t,n,r){return-1==t.s?Math.max(x(t.l,n,r+1),x(t.r,n,r+1)):n[t.s]=r},D=function(t){for(var r=t.length;r&&!t[--r];);for(var e=new n(++r),i=0,a=t[0],s=1,o=function(t){e[i++]=t},f=1;f<=r;++f)if(t[f]==a&&f!=r)++s;else{if(!a&&s>2){for(;s>138;s-=138)o(32754);s>2&&(o(s>10?s-11<<5|28690:s-3<<5|12305),s=0)}else if(s>3){for(o(a),--s;s>6;s-=6)o(8304);s>2&&(o(s-3<<5|8208),s=0)}for(;s--;)o(a);s=1,a=t[f]}return{c:e.subarray(0,i),n:r}},T=function(t,n){for(var r=0,e=0;e>8,t[i+2]=255^t[i],t[i+3]=255^t[i+1];for(var a=0;a4&&!H[a[K-1]];--K);var N,P,Q,R,V=v+5<<3,W=T(f,g)+T(h,w)+l,X=T(f,M)+T(h,C)+l+14+3*K+T(q,H)+2*q[16]+3*q[17]+7*q[18];if(c>=0&&V<=W&&V<=X)return k(r,m,t.subarray(c,c+v));if(z(r,m,1+(X15&&(z(r,m,tt[B]>>5&127),m+=tt[B]>>12)}}}else N=p,P=g,Q=y,R=w;for(B=0;B255){A(r,m,N[(nt=rt>>18&31)+257]),m+=P[nt+257],nt>7&&(z(r,m,rt>>23&31),m+=e[nt]);var et=31&rt;A(r,m,Q[et]),m+=R[et],et>3&&(A(r,m,rt>>5&8191),m+=i[et])}else A(r,m,N[rt]),m+=P[rt]}return A(r,m,N[256]),m+P[256]},U=new r([65540,131080,131088,131104,262176,1048704,1048832,2114560,2117632]),F=new t(0),I=function(){for(var t=new Int32Array(256),n=0;n<256;++n){for(var r=n,e=9;--e;)r=(1&r&&-306674912)^r>>>1;t[n]=r}return t}(),S=function(){var t=1,n=0;return{p:function(r){for(var e=t,i=n,a=0|r.length,s=0;s!=a;){for(var o=Math.min(s+2655,a);s>16),i=(65535&i)+15*(i>>16)}t=e,n=i},d:function(){return(255&(t%=65521))<<24|(65280&t)<<8|(255&(n%=65521))<<8|n>>8}}},L=function(a,s,o,f,u){if(!u&&(u={l:1},s.dictionary)){var c=s.dictionary.subarray(-32768),v=new t(c.length+a.length);v.set(c),v.set(a,c.length),a=v,u.w=c.length}return function(a,s,o,f,u,c){var v=c.z||a.length,d=new t(f+v+5*(1+Math.ceil(v/7e3))+u),g=d.subarray(f,d.length-u),w=c.l,p=7&(c.r||0);if(s){p&&(g[0]=c.r>>3);for(var y=U[s-1],M=y>>13,E=8191&y,z=(1<7e3||q>24576)&&(N>423||!w)){p=C(a,g,0,F,I,S,O,q,G,j-G,p),q=L=O=0,G=j;for(var P=0;P<286;++P)I[P]=0;for(P=0;P<30;++P)S[P]=0}var Q=2,R=0,V=E,W=J-K&32767;if(N>2&&H==T(j-W))for(var X=Math.min(M,N)-1,Y=Math.min(32767,j),Z=Math.min(258,N);W<=Y&&--V&&J!=K;){if(a[j+Q]==a[j+Q-W]){for(var $=0;$Q){if(Q=$,R=W,$>X)break;var tt=Math.min(W,$-2),nt=0;for(P=0;Pnt&&(nt=et,K=rt)}}}W+=(J=K)-(K=A[J])&32767}if(R){F[q++]=268435456|h[Q]<<18|l[R];var it=31&h[Q],at=31&l[R];O+=e[it]+i[at],++I[257+it],++S[at],B=j+Q,++L}else F[q++]=a[j],++I[a[j]]}}for(j=Math.max(j,B);j=v&&(g[p/8|0]=w,st=v),p=k(g,p+1,a.subarray(j,st))}c.i=v}return b(d,0,f+m(p)+u)}(a,null==s.level?6:s.level,null==s.mem?Math.ceil(1.5*Math.max(8,Math.min(13,Math.log(a.length)))):12+s.mem,o,f,u)},O=function(t,n,r){for(;r;++n)t[n]=r,r>>>=8},j=function(){function n(n,r){if("function"==typeof n&&(r=n,n={}),this.ondata=r,this.o=n||{},this.s={l:0,i:32768,w:32768,z:32768},this.b=new t(98304),this.o.dictionary){var e=this.o.dictionary.subarray(-32768);this.b.set(e,32768-e.length),this.s.i=32768-e.length}}return n.prototype.p=function(t,n){this.ondata(L(t,this.o,0,0,this.s),n)},n.prototype.push=function(n,r){this.ondata||E(5),this.s.l&&E(4);var e=n.length+this.s.z;if(e>this.b.length){if(e>2*this.b.length-32768){var i=new t(-32768&e);i.set(this.b.subarray(0,this.s.z)),this.b=i}var a=this.b.length-this.s.z;a&&(this.b.set(n.subarray(0,a),this.s.z),this.s.z=this.b.length,this.p(this.b,!1)),this.b.set(this.b.subarray(-32768)),this.b.set(n.subarray(a),32768),this.s.z=n.length-a+32768,this.s.i=32766,this.s.w=32768}else this.b.set(n,this.s.z),this.s.z+=n.length;this.s.l=1&r,(this.s.z>this.s.w+8191||r)&&(this.p(this.b,r||!1),this.s.w=this.s.i,this.s.i-=2)},n}();function q(t,n){n||(n={});var r=function(){var t=-1;return{p:function(n){for(var r=t,e=0;e>>8;t=r},d:function(){return~t}}}(),e=t.length;r.p(t);var i,a=L(t,n,10+((i=n).filename?i.filename.length+1:0),8),s=a.length;return function(t,n){var r=n.filename;if(t[0]=31,t[1]=139,t[2]=8,t[8]=n.level<2?4:9==n.level?2:0,t[9]=3,0!=n.mtime&&O(t,4,Math.floor(new Date(n.mtime||Date.now())/1e3)),r){t[3]=8;for(var e=0;e<=r.length;++e)t[e+10]=r.charCodeAt(e)}}(a,n),O(a,s-8,r.d()),O(a,s-4,e),a}var B=function(){function t(t,n){this.c=S(),this.v=1,j.call(this,t,n)}return t.prototype.push=function(t,n){this.c.p(t),j.prototype.push.call(this,t,n)},t.prototype.p=function(t,n){var r=L(t,this.o,this.v&&(this.o.dictionary?6:2),n&&4,this.s);this.v&&(function(t,n){var r=n.level,e=0==r?0:r<6?1:9==r?3:2;if(t[0]=120,t[1]=e<<6|(n.dictionary&&32),t[1]|=31-(t[0]<<8|t[1])%31,n.dictionary){var i=S();i.p(n.dictionary),O(t,2,i.d())}}(r,this.o),this.v=0),n&&O(r,r.length-4,this.c.d()),this.ondata(r,n)},t}(),G="undefined"!=typeof TextEncoder&&new TextEncoder,H="undefined"!=typeof TextDecoder&&new TextDecoder;try{H.decode(F,{stream:!0})}catch(t){}var J=function(){function t(t){this.ondata=t}return t.prototype.push=function(t,n){this.ondata||E(5),this.d&&E(4),this.ondata(K(t),this.d=n||!1)},t}();function K(n,r){if(r){for(var e=new t(n.length),i=0;i>1)),o=0,f=function(t){s[o++]=t};for(i=0;is.length){var h=new t(o+8+(a-i<<1));h.set(s),s=h}var l=n.charCodeAt(i);l<128||r?f(l):l<2048?(f(192|l>>6),f(128|63&l)):l>55295&&l<57344?(f(240|(l=65536+(1047552&l)|1023&n.charCodeAt(++i))>>18),f(128|l>>12&63),f(128|l>>6&63),f(128|63&l)):(f(224|l>>12),f(128|l>>6&63),f(128|63&l))}return b(s,0,o)}const N=new class{constructor(){this._init()}clear(){this._init()}addEvent(t){if(!t)throw new Error("Adding invalid event");const n=this._hasEvents?",":"";this.stream.push(n+t),this._hasEvents=!0}finish(){this.stream.push("]",!0);const t=function(t){let n=0;for(let r=0,e=t.length;r{this._deflatedData.push(t)},this.stream=new J(((t,n)=>{this.deflate.push(t,n)})),this.stream.push("[")}},P={clear:()=>{N.clear()},addEvent:t=>N.addEvent(t),finish:()=>N.finish(),compress:t=>function(t){return q(K(t))}(t)};addEventListener("message",(function(t){const n=t.data.method,r=t.data.id,e=t.data.arg;if(n in P&&"function"==typeof P[n])try{const t=P[n](e);postMessage({id:r,method:n,success:!0,response:t})}catch(t){postMessage({id:r,method:n,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)}
/**
* Log a message in debug mode, and add a breadcrumb when _experiment.traceInternals is enabled.
*/
function logInfo(message, shouldAddBreadcrumb) {
if (!DEBUG_BUILD) {
return;
}
logger.info(message);
if (shouldAddBreadcrumb) {
addLogBreadcrumb(message);
}
}
/**
* Log a message, and add a breadcrumb in the next tick.
* This is necessary when the breadcrumb may be added before the replay is initialized.
*/
function logInfoNextTick(message, shouldAddBreadcrumb) {
if (!DEBUG_BUILD) {
return;
}
logger.info(message);
if (shouldAddBreadcrumb) {
// Wait a tick here to avoid race conditions for some initial logs
// which may be added before replay is initialized
setTimeout(() => {
addLogBreadcrumb(message);
}, 0);
}
}
function addLogBreadcrumb(message) {
addBreadcrumb(
{
category: 'console',
data: {
logger: 'replay',
},
level: 'info',
message,
},
{ level: 'info' },
);
}
/** 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. */
/** @inheritdoc */
constructor() {
this.events = [];
this._totalSize = 0;
this.hasCheckout = false;
}
/** @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;
this.hasCheckout = false;
}
/** @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() {
logInfo('[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
DEBUG_BUILD && 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 {
/** @inheritdoc */
constructor(worker) {
this._worker = new WorkerHandler(worker);
this._earliestTimestamp = null;
this._totalSize = 0;
this.hasCheckout = false;
}
/** @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;
this.hasCheckout = false;
// We do not wait on this, as we assume the order of messages is consistent for the worker
this._worker.postMessage('clear').then(null, e => {
DEBUG_BUILD && logger.warn('[Replay] Sending "clear" message to worker failed', e);
});
}
/** @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 */
get hasCheckout() {
return this._used.hasCheckout;
}
/** @inheritdoc */
set hasCheckout(value) {
this._used.hasCheckout = value;
}
/** @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
logInfo('[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, hasCheckout } = this._fallback;
const addEventPromises = [];
for (const event of events) {
addEventPromises.push(this._compression.addEvent(event));
}
this._compression.hasCheckout = hasCheckout;
// 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) {
DEBUG_BUILD && logger.warn('[Replay] Failed to add events when switching buffers.', error);
}
}
}
/**
* Create an event buffer for replays.
*/
function createEventBuffer({
useCompression,
workerUrl: customWorkerUrl,
}) {
if (
useCompression &&
// eslint-disable-next-line no-restricted-globals
window.Worker
) {
const worker = _loadWorker(customWorkerUrl);
if (worker) {
return worker;
}
}
logInfo('[Replay] Using simple buffer');
return new EventBufferArray();
}
function _loadWorker(customWorkerUrl) {
try {
const workerUrl = customWorkerUrl || _getWorkerUrl();
if (!workerUrl) {
return;
}
logInfo(`[Replay] Using compression worker${customWorkerUrl ? ` from ${customWorkerUrl}` : ''}`);
const worker = new Worker(workerUrl);
return new EventBufferProxy(worker);
} catch (error) {
logInfo('[Replay] Failed to create compression worker');
// Fall back to use simple event buffer array
}
}
function _getWorkerUrl() {
if (typeof __SENTRY_EXCLUDE_REPLAY_WORKER__ === 'undefined' || !__SENTRY_EXCLUDE_REPLAY_WORKER__) {
return e();
}
return '';
}
/** If sessionStorage is available. */
function hasSessionStorage() {
try {
// This can throw, e.g. when being accessed in a sandboxed iframe
return 'sessionStorage' in WINDOW && !!WINDOW.sessionStorage;
} catch (e) {
return false;
}
}
/**
* 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 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;
}
/**
* 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;
const previousSessionId = session.previousSessionId;
return {
id,
started,
lastActivity,
segmentId,
sampled,
previousSessionId,
};
}
/**
* 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 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 },
{ previousSessionId } = {},
) {
const sampled = getSessionSampleType(sessionSampleRate, allowBuffering);
const session = makeSession({
sampled,
previousSessionId,
});
if (stickySession) {
saveSession(session);
}
return session;
}
/**
* Fetches a session from storage
*/
function fetchSession(traceInternals) {
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) ;
logInfoNextTick('[Replay] Loading existing session', traceInternals);
return makeSession(sessionObj);
} catch (e) {
return null;
}
}
/**
* 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,
{
maxReplayDuration,
sessionIdleExpire,
targetTime = Date.now(),
},
) {
return (
// First, check that maximum session length has not been exceeded
isExpired(session.started, maxReplayDuration, 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, sessionIdleExpire, targetTime)
);
}
/** If the session should be refreshed or not. */
function shouldRefreshSession(
session,
{ sessionIdleExpire, maxReplayDuration },
) {
// If not expired, all good, just keep the session
if (!isSessionExpired(session, { sessionIdleExpire, maxReplayDuration })) {
return false;
}
// If we are buffering & haven't ever flushed yet, always continue
if (session.sampled === 'buffer' && session.segmentId === 0) {
return false;
}
return true;
}
/**
* Get or create a session, when initializing the replay.
* Returns a session that may be unsampled.
*/
function loadOrCreateSession(
{
traceInternals,
sessionIdleExpire,
maxReplayDuration,
previousSessionId,
}
,
sessionOptions,
) {
const existingSession = sessionOptions.stickySession && fetchSession(traceInternals);
// No session exists yet, just create a new one
if (!existingSession) {
logInfoNextTick('[Replay] Creating new session', traceInternals);
return createSession(sessionOptions, { previousSessionId });
}
if (!shouldRefreshSession(existingSession, { sessionIdleExpire, maxReplayDuration })) {
return existingSession;
}
logInfoNextTick('[Replay] Session in sessionStorage is expired, creating new one...');
return createSession(sessionOptions, { previousSessionId: existingSession.id });
}
function isCustomEvent(event) {
return event.type === EventType.Custom;
}
/**
* Add an event to the event buffer.
* In contrast to `addEvent`, this does not return a promise & does not wait for the adding of the event to succeed/fail.
* Instead this returns `true` if we tried to add the event, else false.
* It returns `false` e.g. if we are paused, disabled, or out of the max replay duration.
*
* `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
*/
function addEventSync(replay, event, isCheckout) {
if (!shouldAddEvent(replay, event)) {
return false;
}
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
_addEvent(replay, event, isCheckout);
return true;
}
/**
* Add an event to the event buffer.
* Resolves to `null` if no event was added, else to `void`.
*
* `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
*/
function addEvent(
replay,
event,
isCheckout,
) {
if (!shouldAddEvent(replay, event)) {
return Promise.resolve(null);
}
return _addEvent(replay, event, isCheckout);
}
async function _addEvent(
replay,
event,
isCheckout,
) {
if (!replay.eventBuffer) {
return null;
}
try {
if (isCheckout && replay.recordingMode === 'buffer') {
replay.eventBuffer.clear();
}
if (isCheckout) {
replay.eventBuffer.hasCheckout = true;
}
const replayOptions = replay.getOptions();
const eventAfterPossibleCallback = maybeApplyCallback(event, replayOptions.beforeAddRecordingEvent);
if (!eventAfterPossibleCallback) {
return;
}
return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
} catch (error) {
const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent';
DEBUG_BUILD && logger.error(error);
await replay.stop({ reason });
const client = getClient();
if (client) {
client.recordDroppedEvent('internal_sdk_error', 'replay');
}
}
}
/** Exported only for tests. */
function shouldAddEvent(replay, event) {
if (!replay.eventBuffer || replay.isPaused() || !replay.isEnabled()) {
return false;
}
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 false;
}
// Throw out events that are +60min from the initial timestamp
if (timestampInMs > replay.getContext().initialTimestamp + replay.getOptions().maxReplayDuration) {
logInfo(
`[Replay] Skipping event with timestamp ${timestampInMs} because it is after maxReplayDuration`,
replay.getOptions()._experiments.traceInternals,
);
return false;
}
return true;
}
function maybeApplyCallback(
event,
callback,
) {
try {
if (typeof callback === 'function' && isCustomEvent(event)) {
return callback(event);
}
} catch (error) {
DEBUG_BUILD &&
logger.error('[Replay] An error occured in the `beforeAddRecordingEvent` callback, skipping the event...', error);
return null;
}
return event;
}
/** 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';
}
/** If the event is a feedback event */
function isFeedbackEvent(event) {
return event.type === 'feedback';
}
/**
* 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 (!replay.isEnabled() || (!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;
}
if (isTransactionEvent(event)) {
handleTransactionEvent(replay, event);
return;
}
handleErrorEvent(replay, event);
};
}
function handleTransactionEvent(replay, event) {
const replayContext = replay.getContext();
// Collect traceIds in _context regardless of `recordingMode`
// In error mode, _context gets cleared on every checkout
// We limit to max. 100 transactions linked
if (event.contexts && event.contexts.trace && event.contexts.trace.trace_id && replayContext.traceIds.size < 100) {
replayContext.traceIds.add(event.contexts.trace.trace_id );
}
}
function handleErrorEvent(replay, event) {
const replayContext = replay.getContext();
// 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.
// We limit to max. 100 errors linked
if (event.event_id && replayContext.errorIds.size < 100) {
replayContext.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) {
return;
}
const { beforeErrorSampling } = replay.getOptions();
if (typeof beforeErrorSampling === 'function' && !beforeErrorSampling(event)) {
return;
}
setTimeout(() => {
// Capture current event buffer as new replay
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
replay.sendBufferedReplayOrFlush();
});
}
function isBaseTransportSend() {
const client = getClient();
if (!client) {
return false;
}
const transport = client.getTransport();
if (!transport) {
return false;
}
return (
(transport.send ).__sentry__baseTransport__ || false
);
}
/**
* Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`.
*/
function handleBeforeSendEvent(replay) {
return (event) => {
if (!replay.isEnabled() || !isErrorEvent(event)) {
return;
}
handleHydrationError(replay, event);
};
}
function handleHydrationError(replay, event) {
const exceptionValue = event.exception && event.exception.values && event.exception.values[0].value;
if (typeof exceptionValue !== 'string') {
return;
}
if (
// Only matches errors in production builds of react-dom
// Example https://reactjs.org/docs/error-decoder.html?invariant=423
exceptionValue.match(/reactjs\.org\/docs\/error-decoder\.html\?invariant=(418|419|422|423|425)/) ||
// Development builds of react-dom
// Error 1: Hydration failed because the initial UI does not match what was rendered on the server.
// Error 2: Text content does not match server-rendered HTML. Warning: Text content did not match.
exceptionValue.match(/(does not match server-rendered HTML|Hydration failed because)/i)
) {
const breadcrumb = createBreadcrumb({
category: 'replay.hydrate-error',
});
addBreadcrumbEvent(replay, breadcrumb);
}
}
/**
* 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-expect-error this may be set by rrweb when it finds errors
if (hint.originalException && hint.originalException.__rrweb__) {
return true;
}
return false;
}
/**
* Add a feedback breadcrumb event to replay.
*/
function addFeedbackBreadcrumb(replay, event) {
replay.triggerUserActivity();
replay.addUpdate(() => {
if (!event.timestamp) {
// Ignore events that don't have timestamps (this shouldn't happen, more of a typing issue)
// Return true here so that we don't flush
return true;
}
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
replay.throttledAddEvent({
type: EventType.Custom,
timestamp: event.timestamp * 1000,
data: {
tag: 'breadcrumb',
payload: {
timestamp: event.timestamp,
type: 'default',
category: 'sentry.feedback',
data: {
feedbackId: event.event_id,
},
},
},
} );
return false;
});
}
/**
* 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 `addEventProcessor(listener)`.
*/
function handleGlobalEventListener(
replay,
includeAfterSendEventHandling = false,
) {
const afterSendHandler = includeAfterSendEventHandling ? handleAfterSendEvent(replay) : undefined;
return Object.assign(
(event, hint) => {
// Do nothing if replay has been disabled
if (!replay.isEnabled()) {
return event;
}
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, and feedbacks, nothing else
if (!isErrorEvent(event) && !isTransactionEvent(event) && !isFeedbackEvent(event)) {
return event;
}
// Ensure we do not add replay_id if the session is expired
const isSessionActive = replay.checkAndHandleExpiredSession();
if (!isSessionActive) {
return event;
}
if (isFeedbackEvent(event)) {
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
replay.flush();
event.contexts.feedback.replay_id = replay.getSessionId();
// Add a replay breadcrumb for this piece of feedback
addFeedbackBreadcrumb(replay, 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) {
DEBUG_BUILD && 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;
},
{ id: 'Replay' },
);
}
/**
* 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 `addHistoryInstrumentationHandler(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 (DEBUG_BUILD && replay.getOptions()._experiments.traceInternals) {
return false;
}
return isSentryRequestUrl(url, getClient());
}
/** 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 `addFetchInstrumentationHandler(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 `addXhrInstrumentationHandler(listener)`.
*/
function handleXhrSpanListener(replay) {
return (handlerData) => {
if (!replay.isEnabled()) {
return;
}
const result = handleXhr(handlerData);
addNetworkBreadcrumb(replay, result);
};
}
/** 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) {
try {
if (typeof body === 'string') {
return [body];
}
if (body instanceof URLSearchParams) {
return [body.toString()];
}
if (body instanceof FormData) {
return [_serializeFormData(body)];
}
if (!body) {
return [undefined];
}
} catch (e2) {
DEBUG_BUILD && logger.warn('[Replay] Failed to serialize body', body);
return [undefined, 'BODY_PARSE_ERROR'];
}
DEBUG_BUILD && logger.info('[Replay] Skipping network body because of body type', body);
return [undefined, 'UNPARSEABLE_BODY_TYPE'];
}
/** Merge a warning into an existing network request/response. */
function mergeWarning(
info,
warning,
) {
if (!info) {
return {
headers: {},
size: undefined,
_meta: {
warnings: [warning],
},
};
}
const newMeta = { ...info._meta };
const existingWarnings = newMeta.warnings || [];
newMeta.warnings = [...existingWarnings, warning];
info._meta = newMeta;
return info;
}
/** 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 && 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-expect-error passing FormData to URLSearchParams actually works
return new URLSearchParams(formData).toString();
}
function normalizeNetworkBody(body)
{
if (!body || typeof body !== 'string') {
return {
body,
};
}
const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE;
const isProbablyJson = _strIsProbablyJson(body);
if (exceedsSizeLimit) {
const truncatedBody = body.slice(0, NETWORK_BODY_MAX_SIZE);
if (isProbablyJson) {
return {
body: truncatedBody,
warnings: ['MAYBE_JSON_TRUNCATED'],
};
}
return {
body: `${truncatedBody}…`,
warnings: ['TEXT_TRUNCATED'],
};
}
if (isProbablyJson) {
try {
const jsonBody = JSON.parse(body);
return {
body: jsonBody,
};
} catch (e3) {
// fall back to just send the body as string
}
}
return {
body,
};
}
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) {
DEBUG_BUILD && 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 = input ? _getFetchRequestArgBody(input) : undefined;
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 now = Date.now();
const { startTimestamp = now, endTimestamp = now } = hint;
const {
url,
method,
status_code: statusCode = 0,
request_body_size: requestBodySize,
response_body_size: responseBodySize,
} = breadcrumb.data;
const captureDetails =
urlMatches(url, options.networkDetailAllowUrls) && !urlMatches(url, options.networkDetailDenyUrls);
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 = input ? 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, warning] = getBodyString(requestBody);
const data = buildNetworkRequestOrResponse(headers, requestBodySize, bodyStr);
if (warning) {
return mergeWarning(data, warning);
}
return data;
}
/** Exported only for tests. */
async function _getResponseInfo(
captureDetails,
{
networkCaptureBodies,
textEncoder,
networkResponseHeaders,
}
,
response,
responseBodySize,
) {
if (!captureDetails && responseBodySize !== undefined) {
return buildSkippedNetworkRequestOrResponse(responseBodySize);
}
const headers = response ? getAllHeaders(response.headers, networkResponseHeaders) : {};
if (!response || (!networkCaptureBodies && responseBodySize !== undefined)) {
return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
}
const [bodyText, warning] = await _parseFetchResponseBody(response);
const result = getResponseData(bodyText, {
networkCaptureBodies,
textEncoder,
responseBodySize,
captureDetails,
headers,
});
if (warning) {
return mergeWarning(result, warning);
}
return result;
}
function getResponseData(
bodyText,
{
networkCaptureBodies,
textEncoder,
responseBodySize,
captureDetails,
headers,
}
,
) {
try {
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 (error) {
DEBUG_BUILD && logger.warn('[Replay] Failed to serialize response body', error);
// fallback
return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
}
}
async function _parseFetchResponseBody(response) {
const res = _tryCloneResponse(response);
if (!res) {
return [undefined, 'BODY_PARSE_ERROR'];
}
try {
const text = await _tryGetResponseText(res);
return [text];
} catch (error) {
DEBUG_BUILD && logger.warn('[Replay] Failed to get text body from response', error);
return [undefined, 'BODY_PARSE_ERROR'];
}
}
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);
}
function _tryCloneResponse(response) {
try {
// We have to clone this, as the body can only be read once
return response.clone();
} catch (error) {
// this can throw if the response was already consumed before
DEBUG_BUILD && logger.warn('[Replay] Failed to clone response body', error);
}
}
/**
* Get the response body of a fetch request, or timeout after 500ms.
* Fetch can return a streaming body, that may not resolve (or not for a long time).
* If that happens, we rather abort after a short time than keep waiting for this.
*/
function _tryGetResponseText(response) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timeout while trying to read response body')), 500);
_getResponseText(response)
.then(
txt => resolve(txt),
reason => reject(reason),
)
.finally(() => clearTimeout(timeout));
});
}
async function _getResponseText(response) {
// Force this to be a promise, just to be safe
// eslint-disable-next-line no-return-await
return await response.text();
}
/**
* 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) {
DEBUG_BUILD && logger.error('[Replay] Failed to capture xhr 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;
if (!xhr) {
return;
}
const reqSize = getBodySize(input, options.textEncoder);
const resSize = xhr.getResponseHeader('content-length')
? parseContentLengthHeader(xhr.getResponseHeader('content-length'))
: _getBodySize(xhr.response, xhr.responseType, 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 now = Date.now();
const { startTimestamp = now, endTimestamp = now, 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 (!xhr || !urlMatches(url, options.networkDetailAllowUrls) || urlMatches(url, options.networkDetailDenyUrls)) {
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 [requestBody, requestWarning] = options.networkCaptureBodies ? getBodyString(input) : [undefined];
const [responseBody, responseWarning] = options.networkCaptureBodies ? _getXhrResponseBody(xhr) : [undefined];
const request = buildNetworkRequestOrResponse(networkRequestHeaders, requestBodySize, requestBody);
const response = buildNetworkRequestOrResponse(networkResponseHeaders, responseBodySize, responseBody);
return {
startTimestamp,
endTimestamp,
url,
method,
statusCode,
request: requestWarning ? mergeWarning(request, requestWarning) : request,
response: responseWarning ? mergeWarning(response, responseWarning) : 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;
}, {});
}
function _getXhrResponseBody(xhr) {
// We collect errors that happen, but only log them if we can't get any response body
const errors = [];
try {
return [xhr.responseText];
} catch (e) {
errors.push(e);
}
// Try to manually parse the response body, if responseText fails
try {
return _parseXhrResponse(xhr.response, xhr.responseType);
} catch (e) {
errors.push(e);
}
DEBUG_BUILD && logger.warn('[Replay] Failed to get xhr response body', ...errors);
return [undefined];
}
/**
* Get the string representation of the XHR response.
* Based on MDN, these are the possible types of the response:
* string
* ArrayBuffer
* Blob
* Document
* POJO
*
* Exported only for tests.
*/
function _parseXhrResponse(
body,
responseType,
) {
try {
if (typeof body === 'string') {
return [body];
}
if (body instanceof Document) {
return [body.body.outerHTML];
}
if (responseType === 'json' && body && typeof body === 'object') {
return [JSON.stringify(body)];
}
if (!body) {
return [undefined];
}
} catch (e2) {
DEBUG_BUILD && logger.warn('[Replay] Failed to serialize body', body);
return [undefined, 'BODY_PARSE_ERROR'];
}
DEBUG_BUILD && logger.info('[Replay] Skipping network body because of body type', body);
return [undefined, 'UNPARSEABLE_BODY_TYPE'];
}
function _getBodySize(
body,
responseType,
textEncoder,
) {
try {
const bodyStr = responseType === 'json' && body && typeof body === 'object' ? JSON.stringify(body) : body;
return getBodySize(bodyStr, textEncoder);
} catch (e3) {
return undefined;
}
}
/**
* 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 = getClient();
try {
const textEncoder = new TextEncoder();
const {
networkDetailAllowUrls,
networkDetailDenyUrls,
networkCaptureBodies,
networkRequestHeaders,
networkResponseHeaders,
} = replay.getOptions();
const options = {
replay,
textEncoder,
networkDetailAllowUrls,
networkDetailDenyUrls,
networkCaptureBodies,
networkRequestHeaders,
networkResponseHeaders,
};
if (client && client.on) {
client.on('beforeAddBreadcrumb', (breadcrumb, hint) => beforeAddNetworkBreadcrumb(options, breadcrumb, hint));
} else {
// Fallback behavior
addFetchInstrumentationHandler(handleFetchSpanListener(replay));
addXhrInstrumentationHandler(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);
// This call should not reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
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);
// This call should not reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
captureFetchBreadcrumbToReplay(breadcrumb, hint, options);
}
} catch (e) {
DEBUG_BUILD && 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) {
isTruncated = true;
// We use the pretty printed JSON string here as a base
return `${JSON.stringify(normalizedArg, null, 2).slice(0, CONSOLE_ARG_MAX_SIZE)}…`;
}
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 = getCurrentScope();
const client = getClient();
scope.addScopeListener(handleScopeListener(replay));
addClickKeypressInstrumentationHandler(handleDomListener(replay));
addHistoryInstrumentationHandler(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
const eventProcessor = handleGlobalEventListener(replay, !hasHooks(client));
if (client && client.addEventProcessor) {
client.addEventProcessor(eventProcessor);
} else {
addEventProcessor(eventProcessor);
}
// If a custom client has no hooks yet, we continue to use the "old" implementation
if (hasHooks(client)) {
client.on('beforeSendEvent', handleBeforeSendEvent(replay));
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') {
// Ensure to check that the session is still active - it could have expired in the meanwhile
const isSessionActive = replay.checkAndHandleExpiredSession();
if (isSessionActive) {
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;
});
// We want to flush replay
client.on('beforeSendFeedback', (feedbackEvent, options) => {
const replayId = replay.getSessionId();
if (options && options.includeReplay && replay.isEnabled() && replayId) {
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
replay.flush();
if (feedbackEvent.contexts && feedbackEvent.contexts.feedback) {
feedbackEvent.contexts.feedback.replay_id = replayId;
}
}
});
}
}
// 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-expect-error 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,
},
},
};
}
/**
* 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()) {
DEBUG_BUILD && 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;
if (replay.clickDetector) {
updateClickDetectorForRecordingEvent(replay.clickDetector, event);
}
// 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();
}
// If the event is not added (e.g. due to being paused, disabled, or out of the max replay duration),
// Skip all further steps
if (!addEventSync(replay, event, isCheckout)) {
// Return true to skip scheduling a debounced flush
return true;
}
// 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
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) {
logInfo(
`[Replay] Updating session start time to earliest event in buffer to ${new Date(earliestEvent)}`,
replay.getOptions()._experiments.traceInternals,
);
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.
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
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: {
shouldRecordCanvas: replay.isRecordingCanvas(),
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;
}
addEventSync(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 eventHint = { event_id, integrations };
if (client.emit) {
client.emit('preprocessEvent', event, eventHint);
}
const preparedEvent = (await prepareEvent(
client.getOptions(),
event,
eventHint,
scope,
client,
getIsolationScope(),
)) ;
// 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 client = getClient();
const scope = getCurrentScope();
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);
logInfo('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": {
},
}
*/
// Prevent this data (which, if it exists, was used in earlier steps in the processing pipeline) from being sent to
// sentry. (Note: Our use of this property comes and goes with whatever we might be debugging, whatever hacks we may
// have temporarily added, etc. Even if we don't happen to be using it at some point in the future, let's not get rid
// of this `delete`, lest we miss putting it back in the next time the property is in use.)
delete replayEvent.sdkProcessingMetadata;
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-expect-error 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);
}
const rateLimits = updateRateLimits({}, response);
if (isRateLimited(rateLimits, 'replay')) {
throw new RateLimitError(rateLimits);
}
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}`);
}
}
/**
* This error indicates that we hit a rate limit API error.
*/
class RateLimitError extends Error {
constructor(rateLimits) {
super('Rate limit hit');
this.rateLimits = rateLimits;
}
}
/**
* 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 || err instanceof RateLimitError) {
throw err;
}
// Capture error for every failed replay
setContext('Replays', {
_retryCount: retryConfig.count,
});
if (DEBUG_BUILD && 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-expect-error 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 {
/**
* 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
*/
/**
* 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
*/
/**
* Options to pass to `rrweb.record()`
*/
/**
* Timestamp of the last user activity. This lives across sessions.
*/
/**
* Is the integration currently active?
*/
/**
* Paused is a state where:
* - DOM Recording is not listening at all
* - Nothing will be added to event buffer (e.g. core SDK events)
*/
/**
* Have we attached listeners to the core SDK?
* Note we have to track this as there is no way to remove instrumentation handlers.
*/
/**
* Function to stop recording
*/
/**
* Internal use for canvas recording options
*/
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);
this.eventBuffer = null;
this.performanceEntries = [];
this.replayPerformanceEntries = [];
this.recordingMode = 'session';
this.timeouts = {
sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
} ;
this._lastActivity = Date.now();
this._isEnabled = false;
this._isPaused = false;
this._hasInitializedCoreListeners = false;
this._context = {
errorIds: new Set(),
traceIds: new Set(),
urls: [],
initialTimestamp: Date.now(),
initialUrl: '',
};
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(',') : '',
}
: 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;
}
/**
* Determine if canvas recording is enabled
*/
isRecordingCanvas() {
return Boolean(this._canvas);
}
/** Get the replay integration options. */
getOptions() {
return this._options;
}
/**
* Initializes the plugin based on sampling configuration. Should not be
* called outside of constructor.
*/
initializeSampling(previousSessionId) {
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.
this._initializeSessionForSampling(previousSessionId);
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 === false) {
// 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 segmentId > 0, it means we've previously already captured this session
// In this case, we still want to continue in `session` recording mode
this.recordingMode = this.session.sampled === 'buffer' && this.session.segmentId === 0 ? 'buffer' : 'session';
logInfoNextTick(
`[Replay] Starting replay in ${this.recordingMode} mode`,
this._options._experiments.traceInternals,
);
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');
}
logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals);
const session = loadOrCreateSession(
{
maxReplayDuration: this._options.maxReplayDuration,
sessionIdleExpire: this.timeouts.sessionIdleExpire,
traceInternals: this._options._experiments.traceInternals,
},
{
stickySession: this._options.stickySession,
// This is intentional: create a new session-based replay when calling `start()`
sessionSampleRate: 1,
allowBuffering: false,
},
);
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');
}
logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals);
const session = loadOrCreateSession(
{
sessionIdleExpire: this.timeouts.sessionIdleExpire,
maxReplayDuration: this._options.maxReplayDuration,
traceInternals: this._options._experiments.traceInternals,
},
{
stickySession: this._options.stickySession,
sessionSampleRate: 0,
allowBuffering: true,
},
);
this.session = session;
this.recordingMode = 'buffer';
this._initializeRecording();
}
/**
* Start recording.
*
* Note that this will cause a new DOM checkout
*/
startRecording() {
try {
const canvasOptions = this._canvas;
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,
...(canvasOptions
? {
recordCanvas: canvasOptions.recordCanvas,
getCanvasManager: canvasOptions.getCanvasManager,
sampling: canvasOptions.sampling,
dataURLOptions: canvasOptions.dataURLOptions,
}
: {}),
});
} 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({ forceFlush = false, reason } = {}) {
if (!this._isEnabled) {
return;
}
// 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;
try {
logInfo(
`[Replay] Stopping Replay${reason ? ` triggered by ${reason}` : ''}`,
this._options._experiments.traceInternals,
);
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 (forceFlush) {
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() {
if (this._isPaused) {
return;
}
this._isPaused = true;
this.stopRecording();
logInfo('[Replay] Pausing replay', this._options._experiments.traceInternals);
}
/**
* Resumes recording, see notes for `pause().
*
* Note that calling `startRecording()` here will cause a
* new DOM checkout.`
*/
resume() {
if (!this._isPaused || !this._checkSession()) {
return;
}
this._isPaused = false;
this.startRecording();
logInfo('[Replay] Resuming replay', this._options._experiments.traceInternals);
}
/**
* 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();
logInfo('[Replay] Converting buffer to session', this._options._experiments.traceInternals);
// 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;
}
// To avoid race conditions where this is called multiple times, we check here again that we are still buffering
if ((this.recordingMode ) === 'session') {
return;
}
// Re-start recording in session-mode
this.recordingMode = 'session';
// Once this session ends, we do not want to refresh it
if (this.session) {
this._updateUserActivity(activityTime);
this._updateSessionActivity(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._checkSession()) {
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() {
// 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._checkSession()) {
// Check session handles the refreshing itself
return false;
}
return true;
}
/**
* 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.performanceEntries = [];
this.replayPerformanceEntries = [];
// 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(() => {
// Return `false` if the event _was_ added, as that means we schedule a flush
return !addEventSync(this, {
type: ReplayEventTypeCustom,
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() {
// eslint-disable-next-line deprecation/deprecation
const lastTransaction = this.lastTransaction || getCurrentScope().getTransaction();
const attributes = (lastTransaction && spanToJSON(lastTransaction).data) || {};
const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
if (!lastTransaction || !source || !['route', 'custom'].includes(source)) {
return undefined;
}
return spanToJSON(lastTransaction).description;
}
/**
* 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,
workerUrl: this._options.workerUrl,
});
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._isPaused = false;
this.startRecording();
}
/** A wrapper to conditionally capture exceptions. */
_handleException(error) {
DEBUG_BUILD && logger.error('[Replay]', error);
if (DEBUG_BUILD && this._options._experiments && this._options._experiments.captureExceptions) {
captureException(error);
}
}
/**
* Loads (or refreshes) the current session.
*/
_initializeSessionForSampling(previousSessionId) {
// Whenever there is _any_ error sample rate, we always allow buffering
// Because we decide on sampling when an error occurs, we need to buffer at all times if sampling for errors
const allowBuffering = this._options.errorSampleRate > 0;
const session = loadOrCreateSession(
{
sessionIdleExpire: this.timeouts.sessionIdleExpire,
maxReplayDuration: this._options.maxReplayDuration,
traceInternals: this._options._experiments.traceInternals,
previousSessionId,
},
{
stickySession: this._options.stickySession,
sessionSampleRate: this._options.sessionSampleRate,
allowBuffering,
},
);
this.session = session;
}
/**
* Checks and potentially refreshes the current session.
* Returns false if session is not recorded.
*/
_checkSession() {
// If there is no session yet, we do not want to refresh anything
// This should generally not happen, but to be safe....
if (!this.session) {
return false;
}
const currentSession = this.session;
if (
shouldRefreshSession(currentSession, {
sessionIdleExpire: this.timeouts.sessionIdleExpire,
maxReplayDuration: this._options.maxReplayDuration,
})
) {
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this._refreshSession(currentSession);
return false;
}
return true;
}
/**
* Refresh a session with a new one.
* This stops the current session (without forcing a flush, as that would never work since we are expired),
* and then does a new sampling based on the refreshed session.
*/
async _refreshSession(session) {
if (!this._isEnabled) {
return;
}
await this.stop({ reason: 'refresh session' });
this.initializeSampling(session.id);
}
/**
* 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);
}
this._performanceCleanupCallback = 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._performanceCleanupCallback) {
this._performanceCleanupCallback();
}
} 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.
*/
__init() {this._handleVisibilityChange = () => {
if (WINDOW.document.visibilityState === 'visible') {
this._doChangeToForegroundTasks();
} else {
this._doChangeToBackgroundTasks();
}
};}
/**
* Handle when page is blurred
*/
__init2() {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
*/
__init3() {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. */
__init4() {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, {
maxReplayDuration: this._options.maxReplayDuration,
sessionIdleExpire: this.timeouts.sessionIdleExpire,
});
if (expired) {
return;
}
if (breadcrumb) {
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
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
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
logInfo('[Replay] Document has become active, but session has expired');
return;
}
if (breadcrumb) {
this._createCustomBreadcrumb(breadcrumb);
}
}
/**
* 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(() => {
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.throttledAddEvent({
type: EventType.Custom,
timestamp: breadcrumb.timestamp || 0,
data: {
tag: 'breadcrumb',
payload: breadcrumb,
},
});
});
}
/**
* Observed performance events are added to `this.performanceEntries`. These
* are included in the replay event before it is finished and sent to Sentry.
*/
_addPerformanceEntries() {
const performanceEntries = createPerformanceEntries(this.performanceEntries).concat(this.replayPerformanceEntries);
this.performanceEntries = [];
this.replayPerformanceEntries = [];
return Promise.all(createPerformanceSpans(this, performanceEntries));
}
/**
* 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() {
const replayId = this.getSessionId();
if (!this.session || !this.eventBuffer || !replayId) {
DEBUG_BUILD && 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;
}
// if this changed in the meanwhile, e.g. because the session was refreshed or similar, we abort here
if (replayId !== this.getSessionId()) {
return;
}
try {
// This uses the data from the eventBuffer, so we need to call this before `finish()
this._updateInitialTimestampFromEventBuffer();
const timestamp = Date.now();
// Check total duration again, to avoid sending outdated stuff
// We leave 30s wiggle room to accomodate late flushing etc.
// This _could_ happen when the browser is suspended during flushing, in which case we just want to stop
if (timestamp - this._context.initialTimestamp > this._options.maxReplayDuration + 30000) {
throw new Error('Session is too long, not sending replay');
}
const eventContext = this._popEventContext();
// Always increment segmentId regardless of outcome of sending replay
const segmentId = this.session.segmentId++;
this._maybeSaveSession();
// Note this empties the event buffer regardless of outcome of sending replay
const recordingData = await this.eventBuffer.finish();
await sendReplay({
replayId,
recordingData,
segmentId,
eventContext,
session: this.session,
options: this.getOptions(),
timestamp,
});
} 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
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stop({ reason: 'sendReplay' });
const client = 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.
*/
__init5() {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()) {
DEBUG_BUILD && logger.error('[Replay] Attempting to finish replay event after session expired.');
return;
}
if (!this.session) {
// should never happen, as we would have bailed out before
return;
}
const start = this.session.started;
const now = Date.now();
const duration = now - start;
// A flush is about to happen, cancel any queued flushes
this._debouncedFlush.cancel();
// If session is too short, or too long (allow some wiggle room over maxReplayDuration), do not send it
// This _should_ not happen, but it may happen if flush is triggered due to a page activity change or similar
const tooShort = duration < this._options.minReplayDuration;
const tooLong = duration > this._options.maxReplayDuration + 5000;
if (tooShort || tooLong) {
logInfo(
`[Replay] Session duration (${Math.floor(duration / 1000)}s) is too ${
tooShort ? 'short' : 'long'
}, not sending replay.`,
this._options._experiments.traceInternals,
);
if (tooShort) {
this._debouncedFlush();
}
return;
}
const eventBuffer = this.eventBuffer;
if (eventBuffer && this.session.segmentId === 0 && !eventBuffer.hasCheckout) {
logInfo('[Replay] Flushing initial segment without checkout.', this._options._experiments.traceInternals);
// TODO FN: Evaluate if we want to stop here, or remove this again?
}
// 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 = undefined;
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) {
DEBUG_BUILD && 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 */
__init6() {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) {
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stop({ reason: 'mutationLimit', forceFlush: this.recordingMode === 'session' });
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}`);
}
consoleSandbox(() => {
// 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,
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;
}
/**
* Masks an attribute if necessary, otherwise return attribute value as-is.
*/
function maskAttribute({
el,
key,
maskAttributes,
maskAllText,
privacyOptions,
value,
}) {
// We only mask attributes if `maskAllText` is true
if (!maskAllText) {
return value;
}
// unmaskTextSelector takes precendence
if (privacyOptions.unmaskTextSelector && el.matches(privacyOptions.unmaskTextSelector)) {
return value;
}
if (
maskAttributes.includes(key) ||
// Need to mask `value` attribute for `` if it's a button-like
// type
(key === 'value' && el.tagName === 'INPUT' && ['submit', 'button'].includes(el.getAttribute('type') || ''))
) {
return value.replace(/[\S]/g, '*');
}
return value;
}
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;
const replayIntegration = ((options) => {
// eslint-disable-next-line deprecation/deprecation
return new Replay(options);
}) ;
/**
* The main replay integration class, to be passed to `init({ integrations: [] })`.
* @deprecated Use `replayIntegration()` instead.
*/
class Replay {
/**
* @inheritDoc
*/
static __initStatic() {this.id = 'Replay';}
/**
* @inheritDoc
*/
/**
* 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,
minReplayDuration = MIN_REPLAY_DURATION,
maxReplayDuration = MAX_REPLAY_DURATION,
stickySession = true,
useCompression = true,
workerUrl,
_experiments = {},
sessionSampleRate,
errorSampleRate,
maskAllText = true,
maskAllInputs = true,
blockAllMedia = true,
mutationBreadcrumbLimit = 750,
mutationLimit = 10000,
slowClickTimeout = 7000,
slowClickIgnoreSelectors = [],
networkDetailAllowUrls = [],
networkDetailDenyUrls = [],
networkCaptureBodies = true,
networkRequestHeaders = [],
networkResponseHeaders = [],
mask = [],
maskAttributes = ['title', 'placeholder'],
unmask = [],
block = [],
unblock = [],
ignore = [],
maskFn,
beforeAddRecordingEvent,
beforeErrorSampling,
// 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,
} = {}) {
// eslint-disable-next-line deprecation/deprecation
this.name = Replay.id;
const privacyOptions = getPrivacyOptions({
mask,
unmask,
block,
unblock,
ignore,
blockClass,
blockSelector,
maskTextClass,
maskTextSelector,
ignoreClass,
});
this._recordingOptions = {
maskAllInputs,
maskAllText,
maskInputOptions: { ...(maskInputOptions || {}), password: true },
maskTextFn: maskFn,
maskInputFn: maskFn,
maskAttributeFn: (key, value, el) =>
maskAttribute({
maskAttributes,
maskAllText,
privacyOptions,
key,
value,
el,
}),
...privacyOptions,
// 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,
errorHandler: (err) => {
try {
err.__rrweb__ = true;
} catch (error) {
// ignore errors here
// this can happen if the error is frozen or does not allow mutation for other reasons
}
},
};
this._initialOptions = {
flushMinDelay,
flushMaxDelay,
minReplayDuration: Math.min(minReplayDuration, MIN_REPLAY_DURATION_LIMIT),
maxReplayDuration: Math.min(maxReplayDuration, MAX_REPLAY_DURATION),
stickySession,
sessionSampleRate,
errorSampleRate,
useCompression,
workerUrl,
blockAllMedia,
maskAllInputs,
maskAllText,
mutationBreadcrumbLimit,
mutationLimit,
slowClickTimeout,
slowClickIgnoreSelectors,
networkDetailAllowUrls,
networkDetailDenyUrls,
networkCaptureBodies,
networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders),
networkResponseHeaders: _getMergedNetworkHeaders(networkResponseHeaders),
beforeAddRecordingEvent,
beforeErrorSampling,
_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({ forceFlush: this._replay.recordingMode === 'session' });
}
/**
* 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;
}
// We have to run this in _initialize, because this runs in setTimeout
// So when this runs all integrations have been added
// Before this, we cannot access integrations on the client,
// so we need to mutate the options here
this._maybeLoadFromReplayCanvasIntegration();
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,
});
}
/** Get canvas options from ReplayCanvas integration, if it is also added. */
_maybeLoadFromReplayCanvasIntegration() {
// To save bundle size, we skip checking for stuff here
// and instead just try-catch everything - as generally this should all be defined
/* eslint-disable @typescript-eslint/no-non-null-assertion */
try {
const client = getClient();
const canvasIntegration = client.getIntegrationByName('ReplayCanvas')
;
if (!canvasIntegration) {
return;
}
this._replay['_canvas'] = canvasIntegration.getOptions();
} catch (e) {
// ignore errors here
}
/* eslint-enable @typescript-eslint/no-non-null-assertion */
}
}Replay.__initStatic();
/** Parse Replay-related options from SDK options */
function loadReplayOptionsFromClient(initialOptions) {
const client = getClient();
const opt = client && (client.getOptions() );
const finalOptions = { sessionSampleRate: 0, errorSampleRate: 0, ...dropUndefinedKeys(initialOptions) };
if (!opt) {
consoleSandbox(() => {
// 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
) {
consoleSandbox(() => {
// 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())];
}
/**
* This is a small utility to get a type-safe instance of the Replay integration.
*/
// eslint-disable-next-line deprecation/deprecation
function getReplay() {
const client = getClient();
return (
client && client.getIntegrationByName && client.getIntegrationByName('Replay')
);
}
export { Replay, getReplay, replayIntegration };
//# sourceMappingURL=index.js.map