From c46d75a37418c4614d5b385dbaacb39f7d4a6eca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:50:33 +0200 Subject: [PATCH] chore(deps): update dependency @sentry/browser to v7.112.0 (#11465) * chore(deps): update dependency @sentry/browser to v7.112.0 * js: Update vendored libraries --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] --- scripts/yarn/package.json | 2 +- scripts/yarn/yarn.lock | 170 +- weblate/static/vendor/sentry.js | 31126 +++++++++++++++++------------- 3 files changed, 17825 insertions(+), 13473 deletions(-) diff --git a/scripts/yarn/package.json b/scripts/yarn/package.json index 9cf132193f40..d16aa398611a 100644 --- a/scripts/yarn/package.json +++ b/scripts/yarn/package.json @@ -4,7 +4,7 @@ "main": "index.js", "license": "GPL-3.0+", "dependencies": { - "@sentry/browser": "7.111.0", + "@sentry/browser": "7.112.0", "@tarekraafat/autocomplete.js": "^10.2.7", "autosize": "6.0.1", "bootstrap-rtl": "3.3.4", diff --git a/scripts/yarn/yarn.lock b/scripts/yarn/yarn.lock index f1dcf269b65f..31d823f57a44 100644 --- a/scripts/yarn/yarn.lock +++ b/scripts/yarn/yarn.lock @@ -2,76 +2,87 @@ # yarn lockfile v1 -"@sentry-internal/feedback@7.111.0": - version "7.111.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.111.0.tgz#c715e7e6a1877b60cd1f4dff85969660e0deff3f" - integrity sha512-xaKgPPDEirOan7c9HwzYA1KK87kRp/qfIx9ZKLOEtxwy6nqoMuSByGqSwm1Oqfcjpbd7y6/y+7Bw+69ZKNVLDQ== - dependencies: - "@sentry/core" "7.111.0" - "@sentry/types" "7.111.0" - "@sentry/utils" "7.111.0" - -"@sentry-internal/replay-canvas@7.111.0": - version "7.111.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.111.0.tgz#aa3cba0477f312cbf40eff4eabeaeda6221a55b6" - integrity sha512-3KPBIpiegTYmuVw9gA2aKuliAQONS3Ny1kJc9x5kz6XQGuLFxqlh6KzoCVaKfQJeq2WJqRNeR4KFFuNGuB3H8w== - dependencies: - "@sentry/core" "7.111.0" - "@sentry/replay" "7.111.0" - "@sentry/types" "7.111.0" - "@sentry/utils" "7.111.0" - -"@sentry-internal/tracing@7.111.0": - version "7.111.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.111.0.tgz#b352df9f38009c5d306308a829a1dd9a57f084fd" - integrity sha512-CgXly8rsdu4loWVKi2RqpInH3C2cVBuaYsx4ZP5IJpzSinsUAMyyr3Pc0PZzCyoVpBBXGBGj/4HhFsY3q6Z0Vg== - dependencies: - "@sentry/core" "7.111.0" - "@sentry/types" "7.111.0" - "@sentry/utils" "7.111.0" - -"@sentry/browser@7.111.0": - version "7.111.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.111.0.tgz#29da73e7192eb5643d101c47922d7374e4cc88ed" - integrity sha512-x7S9XoJh+TbMnur4eBhPpCVo+p7udABBV2gQk+Iw6LP9e8EFKmGmNyl76vSsT6GeFJ7mwxDEKfuwbVoLBjIvHw== - dependencies: - "@sentry-internal/feedback" "7.111.0" - "@sentry-internal/replay-canvas" "7.111.0" - "@sentry-internal/tracing" "7.111.0" - "@sentry/core" "7.111.0" - "@sentry/replay" "7.111.0" - "@sentry/types" "7.111.0" - "@sentry/utils" "7.111.0" - -"@sentry/core@7.111.0": - version "7.111.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.111.0.tgz#54c9037a3b79b3623377dce1887b69b40670e201" - integrity sha512-/ljeMjZu8CSrLGrseBi/7S2zRIFsqMcvfyG6Nwgfc07J9nbHt8/MqouE1bXZfiaILqDBpK7BK9MLAAph4mkAWg== - dependencies: - "@sentry/types" "7.111.0" - "@sentry/utils" "7.111.0" - -"@sentry/replay@7.111.0": - version "7.111.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.111.0.tgz#6d21bddf2ec245db6eb2c471e81efd94364107ae" - integrity sha512-cSbI4A4hrO0sZ0ynvLQauPg8YyaDOQkhGkyvbws8W9WgfxR8X827bY9S0f1TPfgaFiVcKb0iRaAwyXHg3pyzOg== - dependencies: - "@sentry-internal/tracing" "7.111.0" - "@sentry/core" "7.111.0" - "@sentry/types" "7.111.0" - "@sentry/utils" "7.111.0" - -"@sentry/types@7.111.0": - version "7.111.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.111.0.tgz#9c869c3c51d606041916765ba58f29de915707ac" - integrity sha512-Oti4pgQ55+FBHKKcHGu51ZUxO1u52G5iVNK4mbtAN+5ArSCy/2s1H8IDJiOMswn3acfUnCR0oB/QsbEgAPZ26g== - -"@sentry/utils@7.111.0": - version "7.111.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.111.0.tgz#e006cc1e751b30ff5cf914c34eb143102e2e8c2d" - integrity sha512-CB5rz1EgCSwj3xoXogsCZ5pQtfERrURc/ItcCuoaijUhkD0iMq5MCNWMHW3mBsBrqx/Oba+XGvDu0t/5+SWwBg== - dependencies: - "@sentry/types" "7.111.0" +"@sentry-internal/feedback@7.112.0": + version "7.112.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.112.0.tgz#3dd9ccf7a57fe6a0d643a4ae534dcd0f346d607c" + integrity sha512-aqndxnTvZnqo/uUhuWLNWY/0W3zOxNs9FofLYi1SK5+QzMqDIyFY1dc9+ZqQH3/9GIlEGao+zveGAHeUEtpE8g== + dependencies: + "@sentry/core" "7.112.0" + "@sentry/types" "7.112.0" + "@sentry/utils" "7.112.0" + +"@sentry-internal/replay-canvas@7.112.0": + version "7.112.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.112.0.tgz#339d4cbf97c7a1573402459c700331b3375b0de7" + integrity sha512-DwpGY5oZf0ab4Jm9HtM8fB3xqnpAcxBKORqiVHZizz7eo0arrb1n9HCXcxsRNNOAuMRBS8aEHKberfdL6rYpyw== + dependencies: + "@sentry/core" "7.112.0" + "@sentry/replay" "7.112.0" + "@sentry/types" "7.112.0" + "@sentry/utils" "7.112.0" + +"@sentry-internal/tracing@7.112.0": + version "7.112.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.112.0.tgz#bf77cbf613a95379e6ae4dfe0f53bfe445cd32b6" + integrity sha512-PkA3NaSg4nTWp9pwVsV9x0EBiY0pEAnIboIpMuLGE5MJ/FL10NC5Fn1GPebcxNnOou62dM7P/z7Wtcm8czAn6A== + dependencies: + "@sentry/core" "7.112.0" + "@sentry/types" "7.112.0" + "@sentry/utils" "7.112.0" + +"@sentry/browser@7.112.0": + version "7.112.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.112.0.tgz#9d20bb62f848738c0711cdd2c01a122bac917b37" + integrity sha512-xqxtlQ/GMHxYcJYAhWR0ELO4kCnQV9GuIcBUEHlU/mYbPBDPxNYFzXkoz3514DBKxRVTHDkVle6vLuG0yKvXsg== + dependencies: + "@sentry-internal/feedback" "7.112.0" + "@sentry-internal/replay-canvas" "7.112.0" + "@sentry-internal/tracing" "7.112.0" + "@sentry/core" "7.112.0" + "@sentry/integrations" "7.112.0" + "@sentry/replay" "7.112.0" + "@sentry/types" "7.112.0" + "@sentry/utils" "7.112.0" + +"@sentry/core@7.112.0": + version "7.112.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.112.0.tgz#081516c160c1e35ebb09d2110cee84a4c84a23d5" + integrity sha512-q4K0fTULXMf9vb0Je6qFwQyVjfMvuPiKRRvRHcpWvWudV7oTcfPixlbbIQaj3OiY3nrjk5q86hktqboI/Z6ISw== + dependencies: + "@sentry/types" "7.112.0" + "@sentry/utils" "7.112.0" + +"@sentry/integrations@7.112.0": + version "7.112.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.112.0.tgz#bfe2ec6bac6228ef1b2f31e6484131283e654599" + integrity sha512-brN6eZkXuz1e/OKhMGJsAZjc0cUU+5G+LQWet+gGXWVGM2v7uY7mKDHr5Yl/c8WxeJBurjJzJn7YmtmR9++ZKQ== + dependencies: + "@sentry/core" "7.112.0" + "@sentry/types" "7.112.0" + "@sentry/utils" "7.112.0" + localforage "^1.8.1" + +"@sentry/replay@7.112.0": + version "7.112.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.112.0.tgz#ddd0a3c1d93456fbb189ffe625cbb07456a84b27" + integrity sha512-uP38yQpYKdU9onJEl77nSJslajXMbTLp3j+8EK4tNnXDMv+yDnSouODEdHQyX9fajKHsFi/FjjOIrVujR0Qd7w== + dependencies: + "@sentry-internal/tracing" "7.112.0" + "@sentry/core" "7.112.0" + "@sentry/types" "7.112.0" + "@sentry/utils" "7.112.0" + +"@sentry/types@7.112.0": + version "7.112.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.112.0.tgz#1bd482e21db854e0235875bc57d9649e2bd07ba1" + integrity sha512-ASonavVCSrgDjMyWjuNMSytKMGYJq7d/1+IoBJsQFLgLe1gLIXuDNbhfUAM4A+muQUGZepV9iRX4ZYhiROMHVQ== + +"@sentry/utils@7.112.0": + version "7.112.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.112.0.tgz#e9f09d63148f30a452905c9079de78fb52b3af9c" + integrity sha512-dNGcNWKoJE9VwIAZxQsqC6/7BC+8wn1rT7Km9S8xltcjhRvaK4n3QZwXoNLHjNWT0mS2lZaFyRx2hsHjblQqLg== + dependencies: + "@sentry/types" "7.112.0" "@tarekraafat/autocomplete.js@^10.2.7": version "10.2.7" @@ -648,6 +659,11 @@ ieee754@^1.1.4: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -760,6 +776,20 @@ labeled-stream-splicer@^2.0.0: inherits "^2.0.1" stream-splicer "^2.0.0" +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== + dependencies: + immediate "~3.0.5" + +localforage@^1.8.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + lodash.memoize@~3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" diff --git a/weblate/static/vendor/sentry.js b/weblate/static/vendor/sentry.js index 86b7dfa0a6d3..496e771605da 100644 --- a/weblate/static/vendor/sentry.js +++ b/weblate/static/vendor/sentry.js @@ -1976,7 +1976,7 @@ exports.feedbackIntegration = feedbackIntegration; exports.sendFeedback = sendFeedback; -},{"@sentry/core":70,"@sentry/utils":139}],2:[function(require,module,exports){ +},{"@sentry/core":70,"@sentry/utils":152}],2:[function(require,module,exports){ var { _optionalChain } = require('@sentry/utils'); @@ -2499,7 +2499,7 @@ function initCanvasWebGLMutationObserver(cb, win, blockClass, blockSelector, unb }; } -var r = `for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",e="undefined"==typeof Uint8Array?[]:new Uint8Array(256),n=0;n<64;n++)e[t.charCodeAt(n)]=n;var a=function(e){var n,a=new Uint8Array(e),s=a.length,r="";for(n=0;n>2],r+=t[(3&a[n])<<4|a[n+1]>>4],r+=t[(15&a[n+1])<<2|a[n+2]>>6],r+=t[63&a[n+2]];return s%3==2?r=r.substring(0,r.length-1)+"=":s%3==1&&(r=r.substring(0,r.length-2)+"=="),r};const s=new Map,r=new Map;const i=self;i.onmessage=async function(t){if(!("OffscreenCanvas"in globalThis))return i.postMessage({id:t.data.id});{const{id:e,bitmap:n,width:o,height:f,dataURLOptions:c}=t.data,g=async function(t,e,n){const s=t+"-"+e;if("OffscreenCanvas"in globalThis){if(r.has(s))return r.get(s);const i=new OffscreenCanvas(t,e);i.getContext("2d");const o=await i.convertToBlob(n),f=await o.arrayBuffer(),c=a(f);return r.set(s,c),c}return""}(o,f,c),d=new OffscreenCanvas(o,f);d.getContext("2d").drawImage(n,0,0),n.close();const u=await d.convertToBlob(c),h=u.type,w=await u.arrayBuffer(),l=a(w);if(!s.has(e)&&await g===l)return s.set(e,l),i.postMessage({id:e});if(s.get(e)===l)return i.postMessage({id:e});i.postMessage({id:e,type:h,base64:l,width:o,height:f}),s.set(e,l)}};`; +var r = `for(var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",t="undefined"==typeof Uint8Array?[]:new Uint8Array(256),a=0;a<64;a++)t[e.charCodeAt(a)]=a;var n=function(t){var a,n=new Uint8Array(t),r=n.length,s="";for(a=0;a>2],s+=e[(3&n[a])<<4|n[a+1]>>4],s+=e[(15&n[a+1])<<2|n[a+2]>>6],s+=e[63&n[a+2]];return r%3==2?s=s.substring(0,s.length-1)+"=":r%3==1&&(s=s.substring(0,s.length-2)+"=="),s};const r=new Map,s=new Map;const i=self;i.onmessage=async function(e){if(!("OffscreenCanvas"in globalThis))return i.postMessage({id:e.data.id});{const{id:t,bitmap:a,width:o,height:f,maxCanvasSize:c,dataURLOptions:g}=e.data,u=async function(e,t,a){const r=e+"-"+t;if("OffscreenCanvas"in globalThis){if(s.has(r))return s.get(r);const i=new OffscreenCanvas(e,t);i.getContext("2d");const o=await i.convertToBlob(a),f=await o.arrayBuffer(),c=n(f);return s.set(r,c),c}return""}(o,f,g),[h,d]=function(e,t,a){if(!a)return[e,t];const[n,r]=a;if(e<=n&&t<=r)return[e,t];let s=e,i=t;return s>n&&(i=Math.floor(n*t/e),s=n),i>r&&(s=Math.floor(r*e/t),i=r),[s,i]}(o,f,c),l=new OffscreenCanvas(h,d),w=l.getContext("bitmaprenderer"),p=h===o&&d===f?a:await createImageBitmap(a,{resizeWidth:h,resizeHeight:d,resizeQuality:"low"});w.transferFromImageBitmap(p),a.close();const y=await l.convertToBlob(g),v=y.type,b=await y.arrayBuffer(),m=n(b);if(p.close(),!r.has(t)&&await u===m)return r.set(t,m),i.postMessage({id:t});if(r.get(t)===m)return i.postMessage({id:t});i.postMessage({id:t,type:v,base64:m,width:o,height:f}),r.set(t,m)}};`; function t(){const t=new Blob([r]);return URL.createObjectURL(t)} @@ -2535,7 +2535,7 @@ class CanvasManager { } this.pendingCanvasMutations.get(target).push(mutation); }; - const { sampling = 'all', win, blockClass, blockSelector, unblockSelector, recordCanvas, dataURLOptions, errorHandler, } = options; + const { sampling = 'all', win, blockClass, blockSelector, unblockSelector, maxCanvasSize, recordCanvas, dataURLOptions, errorHandler, } = options; this.mutationCb = options.mutationCb; this.mirror = options.mirror; this.options = options; @@ -2549,14 +2549,14 @@ class CanvasManager { if (recordCanvas && sampling === 'all') this.initCanvasMutationObserver(win, blockClass, blockSelector, unblockSelector); if (recordCanvas && typeof sampling === 'number') - this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector, unblockSelector, { + this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector, unblockSelector, maxCanvasSize, { dataURLOptions, }); })(); } - initCanvasFPSObserver(fps, win, blockClass, blockSelector, unblockSelector, options) { + initCanvasFPSObserver(fps, win, blockClass, blockSelector, unblockSelector, maxCanvasSize, options) { const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, unblockSelector, true); - const rafId = this.takeSnapshot(false, fps, win, blockClass, blockSelector, unblockSelector, options.dataURLOptions); + const rafId = this.takeSnapshot(false, fps, win, blockClass, blockSelector, unblockSelector, maxCanvasSize, options.dataURLOptions); this.resetObservers = () => { canvasContextReset(); cancelAnimationFrame(rafId); @@ -2576,12 +2576,12 @@ class CanvasManager { } snapshot(canvasElement) { const { options } = this; - const rafId = this.takeSnapshot(true, options.sampling === 'all' ? 2 : options.sampling || 2, options.win, options.blockClass, options.blockSelector, options.unblockSelector, options.dataURLOptions, canvasElement); + const rafId = this.takeSnapshot(true, options.sampling === 'all' ? 2 : options.sampling || 2, options.win, options.blockClass, options.blockSelector, options.unblockSelector, options.maxCanvasSize, options.dataURLOptions, canvasElement); this.resetObservers = () => { cancelAnimationFrame(rafId); }; } - takeSnapshot(isManualSnapshot, fps, win, blockClass, blockSelector, unblockSelector, dataURLOptions, canvasElement) { + takeSnapshot(isManualSnapshot, fps, win, blockClass, blockSelector, unblockSelector, maxCanvasSize, dataURLOptions, canvasElement) { const snapshotInProgressMap = new Map(); const worker = new Worker(t()); worker.onmessage = (e) => { @@ -2614,6 +2614,8 @@ class CanvasManager { }, 0, 0, + width, + height, ], }, ], @@ -2663,6 +2665,7 @@ class CanvasManager { width: canvas.width, height: canvas.height, dataURLOptions, + maxCanvasSize, }, [bitmap]); }) .catch((error) => { @@ -2741,12 +2744,18 @@ const CANVAS_QUALITY = { }; const INTEGRATION_NAME = 'ReplayCanvas'; +const DEFAULT_MAX_CANVAS_SIZE = 1280; /** Exported only for type safe tests. */ const _replayCanvasIntegration = ((options = {}) => { + const [maxCanvasWidth, maxCanvasHeight] = options.maxCanvasSize || []; const _canvasOptions = { quality: options.quality || 'medium', enableManualSnapshot: options.enableManualSnapshot, + maxCanvasSize: [ + maxCanvasWidth ? Math.min(maxCanvasWidth, DEFAULT_MAX_CANVAS_SIZE) : DEFAULT_MAX_CANVAS_SIZE, + maxCanvasHeight ? Math.min(maxCanvasHeight, DEFAULT_MAX_CANVAS_SIZE) : DEFAULT_MAX_CANVAS_SIZE, + ] , }; let canvasManagerResolve; @@ -2757,15 +2766,16 @@ const _replayCanvasIntegration = ((options = {}) => { // eslint-disable-next-line @typescript-eslint/no-empty-function setupOnce() {}, getOptions() { - const { quality, enableManualSnapshot } = _canvasOptions; + const { quality, enableManualSnapshot, maxCanvasSize } = _canvasOptions; return { enableManualSnapshot, recordCanvas: true, - getCanvasManager: (options) => { + getCanvasManager: (getCanvasManagerOptions) => { const manager = new CanvasManager({ - ...options, + ...getCanvasManagerOptions, enableManualSnapshot, + maxCanvasSize, errorHandler: (err) => { try { if (typeof err === 'object') { @@ -2807,7 +2817,7 @@ exports.ReplayCanvas = ReplayCanvas; exports.replayCanvasIntegration = replayCanvasIntegration; -},{"@sentry/core":70,"@sentry/utils":139}],3:[function(require,module,exports){ +},{"@sentry/core":70,"@sentry/utils":152}],3:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -2850,7 +2860,7 @@ function registerBackgroundTabDetection() { exports.registerBackgroundTabDetection = registerBackgroundTabDetection; -},{"../common/debug-build.js":26,"./types.js":11,"@sentry/core":70,"@sentry/utils":139}],4:[function(require,module,exports){ +},{"../common/debug-build.js":26,"./types.js":11,"@sentry/core":70,"@sentry/utils":152}],4:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -3364,7 +3374,7 @@ exports.startBrowserTracingNavigationSpan = startBrowserTracingNavigationSpan; exports.startBrowserTracingPageLoadSpan = startBrowserTracingPageLoadSpan; -},{"../common/debug-build.js":26,"./backgroundtab.js":3,"./instrument.js":6,"./metrics/index.js":7,"./request.js":9,"./types.js":11,"@sentry/core":70,"@sentry/utils":139}],5:[function(require,module,exports){ +},{"../common/debug-build.js":26,"./backgroundtab.js":3,"./instrument.js":6,"./metrics/index.js":7,"./request.js":9,"./types.js":11,"@sentry/core":70,"@sentry/utils":152}],5:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -3827,7 +3837,7 @@ exports.BrowserTracing = BrowserTracing; exports.getMetaContent = getMetaContent; -},{"../common/debug-build.js":26,"./backgroundtab.js":3,"./instrument.js":6,"./metrics/index.js":7,"./request.js":9,"./router.js":10,"./types.js":11,"@sentry/core":70,"@sentry/utils":139}],6:[function(require,module,exports){ +},{"../common/debug-build.js":26,"./backgroundtab.js":3,"./instrument.js":6,"./metrics/index.js":7,"./request.js":9,"./router.js":10,"./types.js":11,"@sentry/core":70,"@sentry/utils":152}],6:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -4063,7 +4073,7 @@ exports.addPerformanceInstrumentationHandler = addPerformanceInstrumentationHand exports.addTtfbInstrumentationHandler = addTtfbInstrumentationHandler; -},{"../common/debug-build.js":26,"./web-vitals/getCLS.js":12,"./web-vitals/getFID.js":13,"./web-vitals/getINP.js":14,"./web-vitals/getLCP.js":15,"./web-vitals/lib/observe.js":22,"./web-vitals/onTTFB.js":25,"@sentry/utils":139}],7:[function(require,module,exports){ +},{"../common/debug-build.js":26,"./web-vitals/getCLS.js":12,"./web-vitals/getFID.js":13,"./web-vitals/getINP.js":14,"./web-vitals/getLCP.js":15,"./web-vitals/lib/observe.js":22,"./web-vitals/onTTFB.js":25,"@sentry/utils":152}],7:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -4779,7 +4789,7 @@ exports.startTrackingLongTasks = startTrackingLongTasks; exports.startTrackingWebVitals = startTrackingWebVitals; -},{"../../common/debug-build.js":26,"../instrument.js":6,"../types.js":11,"../web-vitals/lib/getNavigationEntry.js":19,"../web-vitals/lib/getVisibilityWatcher.js":20,"./utils.js":8,"@sentry/core":70,"@sentry/utils":139}],8:[function(require,module,exports){ +},{"../../common/debug-build.js":26,"../instrument.js":6,"../types.js":11,"../web-vitals/lib/getNavigationEntry.js":19,"../web-vitals/lib/getVisibilityWatcher.js":20,"./utils.js":8,"@sentry/core":70,"@sentry/utils":152}],8:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); /** @@ -5126,7 +5136,7 @@ exports.shouldAttachHeaders = shouldAttachHeaders; exports.xhrCallback = xhrCallback; -},{"../common/fetch.js":27,"./instrument.js":6,"./types.js":11,"@sentry/core":70,"@sentry/utils":139}],10:[function(require,module,exports){ +},{"../common/fetch.js":27,"./instrument.js":6,"./types.js":11,"@sentry/core":70,"@sentry/utils":152}],10:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -5197,7 +5207,7 @@ function instrumentRoutingWithDefaults( exports.instrumentRoutingWithDefaults = instrumentRoutingWithDefaults; -},{"../common/debug-build.js":26,"./types.js":11,"@sentry/utils":139}],11:[function(require,module,exports){ +},{"../common/debug-build.js":26,"./types.js":11,"@sentry/utils":152}],11:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -5209,7 +5219,7 @@ const WINDOW = utils.GLOBAL_OBJ exports.WINDOW = WINDOW; -},{"@sentry/utils":139}],12:[function(require,module,exports){ +},{"@sentry/utils":152}],12:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const bindReporter = require('./lib/bindReporter.js'); @@ -6383,7 +6393,7 @@ exports.addTracingHeadersToFetchRequest = addTracingHeadersToFetchRequest; exports.instrumentFetchRequest = instrumentFetchRequest; -},{"@sentry/core":70,"@sentry/utils":139}],28:[function(require,module,exports){ +},{"@sentry/core":70,"@sentry/utils":152}],28:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -6456,7 +6466,7 @@ function addExtensionMethods() { exports.addExtensionMethods = addExtensionMethods; -},{"@sentry/core":70,"@sentry/utils":139}],29:[function(require,module,exports){ +},{"@sentry/core":70,"@sentry/utils":152}],29:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -6513,7 +6523,7 @@ exports.instrumentFetchRequest = fetch.instrumentFetchRequest; exports.addExtensionMethods = extensions.addExtensionMethods; -},{"./browser/browserTracingIntegration.js":4,"./browser/browsertracing.js":5,"./browser/instrument.js":6,"./browser/request.js":9,"./common/fetch.js":27,"./extensions.js":28,"./node/integrations/apollo.js":30,"./node/integrations/express.js":31,"./node/integrations/graphql.js":32,"./node/integrations/lazy.js":33,"./node/integrations/mongo.js":34,"./node/integrations/mysql.js":35,"./node/integrations/postgres.js":36,"./node/integrations/prisma.js":37,"@sentry/core":70,"@sentry/utils":139}],30:[function(require,module,exports){ +},{"./browser/browserTracingIntegration.js":4,"./browser/browsertracing.js":5,"./browser/instrument.js":6,"./browser/request.js":9,"./common/fetch.js":27,"./extensions.js":28,"./node/integrations/apollo.js":30,"./node/integrations/express.js":31,"./node/integrations/graphql.js":32,"./node/integrations/lazy.js":33,"./node/integrations/mongo.js":34,"./node/integrations/mysql.js":35,"./node/integrations/postgres.js":36,"./node/integrations/prisma.js":37,"@sentry/core":70,"@sentry/utils":152}],30:[function(require,module,exports){ var { _optionalChain } = require('@sentry/utils'); @@ -6706,7 +6716,7 @@ function wrapResolver( exports.Apollo = Apollo; -},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/utils":139}],31:[function(require,module,exports){ +},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/utils":152}],31:[function(require,module,exports){ var { _optionalChain } = require('@sentry/utils'); @@ -7203,7 +7213,7 @@ exports.extractOriginalRoute = extractOriginalRoute; exports.preventDuplicateSegments = preventDuplicateSegments; -},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/core":70,"@sentry/utils":139}],32:[function(require,module,exports){ +},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/core":70,"@sentry/utils":152}],32:[function(require,module,exports){ var { _optionalChain } = require('@sentry/utils'); @@ -7292,7 +7302,7 @@ class GraphQL { exports.GraphQL = GraphQL; -},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/utils":139}],33:[function(require,module,exports){ +},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/utils":152}],33:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -7345,7 +7355,7 @@ const lazyLoadedNodePerformanceMonitoringIntegrations = [ exports.lazyLoadedNodePerformanceMonitoringIntegrations = lazyLoadedNodePerformanceMonitoringIntegrations; -},{"@sentry/utils":139}],34:[function(require,module,exports){ +},{"@sentry/utils":152}],34:[function(require,module,exports){ var { _optionalChain } = require('@sentry/utils'); @@ -7612,7 +7622,7 @@ class Mongo { exports.Mongo = Mongo; -},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/utils":139}],35:[function(require,module,exports){ +},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/utils":152}],35:[function(require,module,exports){ var { _optionalChain } = require('@sentry/utils'); @@ -7750,7 +7760,7 @@ class Mysql { exports.Mysql = Mysql; -},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/utils":139}],36:[function(require,module,exports){ +},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/utils":152}],36:[function(require,module,exports){ var { _optionalChain } = require('@sentry/utils'); @@ -7883,7 +7893,7 @@ class Postgres { exports.Postgres = Postgres; -},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/utils":139}],37:[function(require,module,exports){ +},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/utils":152}],37:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -7976,7 +7986,7 @@ class Prisma { exports.Prisma = Prisma; -},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/core":70,"@sentry/utils":139}],38:[function(require,module,exports){ +},{"../../common/debug-build.js":26,"./utils/node-utils.js":38,"@sentry/core":70,"@sentry/utils":152}],38:[function(require,module,exports){ var { _optionalChain } = require('@sentry/utils'); @@ -8001,7 +8011,7 @@ function shouldDisableAutoInstrumentation(getCurrentHub) { exports.shouldDisableAutoInstrumentation = shouldDisableAutoInstrumentation; -},{"@sentry/utils":139}],39:[function(require,module,exports){ +},{"@sentry/utils":152}],39:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -8120,7 +8130,7 @@ class BrowserClient extends core.BaseClient { exports.BrowserClient = BrowserClient; -},{"./debug-build.js":40,"./eventbuilder.js":41,"./helpers.js":42,"./userfeedback.js":60,"@sentry/core":70,"@sentry/utils":139}],40:[function(require,module,exports){ +},{"./debug-build.js":40,"./eventbuilder.js":41,"./helpers.js":42,"./userfeedback.js":60,"@sentry/core":70,"@sentry/utils":152}],40:[function(require,module,exports){ arguments[4][26][0].apply(exports,arguments) },{"dup":26}],41:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); @@ -8445,7 +8455,7 @@ exports.exceptionFromError = exceptionFromError; exports.parseStackFrames = parseStackFrames; -},{"@sentry/core":70,"@sentry/utils":139}],42:[function(require,module,exports){ +},{"@sentry/core":70,"@sentry/utils":152}],42:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); require('@sentry-internal/tracing'); @@ -8603,7 +8613,7 @@ exports.shouldIgnoreOnError = shouldIgnoreOnError; exports.wrap = wrap; -},{"@sentry-internal/tracing":29,"@sentry/core":70,"@sentry/utils":139}],43:[function(require,module,exports){ +},{"@sentry-internal/tracing":29,"@sentry/core":70,"@sentry/utils":152}],43:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -8625,6 +8635,7 @@ const index = require('./integrations/index.js'); const replay = require('@sentry/replay'); const replayCanvas = require('@sentry-internal/replay-canvas'); const feedback = require('@sentry-internal/feedback'); +const integrations = require('@sentry/integrations'); const tracing = require('@sentry-internal/tracing'); const offline = require('./transports/offline.js'); const hubextensions = require('./profiling/hubextensions.js'); @@ -8732,7 +8743,6 @@ exports.wrap = sdk.wrap; exports.Breadcrumbs = breadcrumbs.Breadcrumbs; exports.breadcrumbsIntegration = breadcrumbs.breadcrumbsIntegration; exports.Dedupe = dedupe.Dedupe; -exports.dedupeIntegration = dedupe.dedupeIntegration; exports.GlobalHandlers = globalhandlers.GlobalHandlers; exports.globalHandlersIntegration = globalhandlers.globalHandlersIntegration; exports.HttpContext = httpcontext.HttpContext; @@ -8749,6 +8759,15 @@ exports.replayCanvasIntegration = replayCanvas.replayCanvasIntegration; exports.Feedback = feedback.Feedback; exports.feedbackIntegration = feedback.feedbackIntegration; exports.sendFeedback = feedback.sendFeedback; +exports.captureConsoleIntegration = integrations.captureConsoleIntegration; +exports.contextLinesIntegration = integrations.contextLinesIntegration; +exports.debugIntegration = integrations.debugIntegration; +exports.dedupeIntegration = integrations.dedupeIntegration; +exports.extraErrorDataIntegration = integrations.extraErrorDataIntegration; +exports.httpClientIntegration = integrations.httpClientIntegration; +exports.reportingObserverIntegration = integrations.reportingObserverIntegration; +exports.rewriteFramesIntegration = integrations.rewriteFramesIntegration; +exports.sessionTimingIntegration = integrations.sessionTimingIntegration; exports.BrowserTracing = tracing.BrowserTracing; exports.browserTracingIntegration = tracing.browserTracingIntegration; exports.defaultRequestInstrumentationOptions = tracing.defaultRequestInstrumentationOptions; @@ -8762,7 +8781,7 @@ exports.browserProfilingIntegration = integration.browserProfilingIntegration; exports.Integrations = INTEGRATIONS; -},{"./client.js":39,"./eventbuilder.js":41,"./helpers.js":42,"./integrations/breadcrumbs.js":44,"./integrations/dedupe.js":45,"./integrations/globalhandlers.js":46,"./integrations/httpcontext.js":47,"./integrations/index.js":48,"./integrations/linkederrors.js":49,"./integrations/trycatch.js":50,"./profiling/hubextensions.js":51,"./profiling/integration.js":52,"./sdk.js":54,"./stack-parsers.js":55,"./transports/fetch.js":56,"./transports/offline.js":57,"./transports/xhr.js":59,"./userfeedback.js":60,"@sentry-internal/feedback":1,"@sentry-internal/replay-canvas":2,"@sentry-internal/tracing":29,"@sentry/core":70,"@sentry/replay":119}],44:[function(require,module,exports){ +},{"./client.js":39,"./eventbuilder.js":41,"./helpers.js":42,"./integrations/breadcrumbs.js":44,"./integrations/dedupe.js":45,"./integrations/globalhandlers.js":46,"./integrations/httpcontext.js":47,"./integrations/index.js":48,"./integrations/linkederrors.js":49,"./integrations/trycatch.js":50,"./profiling/hubextensions.js":51,"./profiling/integration.js":52,"./sdk.js":54,"./stack-parsers.js":55,"./transports/fetch.js":56,"./transports/offline.js":57,"./transports/xhr.js":59,"./userfeedback.js":60,"@sentry-internal/feedback":1,"@sentry-internal/replay-canvas":2,"@sentry-internal/tracing":29,"@sentry/core":70,"@sentry/integrations":126,"@sentry/replay":132}],44:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -9103,7 +9122,7 @@ exports.Breadcrumbs = Breadcrumbs; exports.breadcrumbsIntegration = breadcrumbsIntegration; -},{"../debug-build.js":40,"../helpers.js":42,"@sentry/core":70,"@sentry/utils":139}],45:[function(require,module,exports){ +},{"../debug-build.js":40,"../helpers.js":42,"@sentry/core":70,"@sentry/utils":152}],45:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -9305,7 +9324,7 @@ exports.Dedupe = Dedupe; exports.dedupeIntegration = dedupeIntegration; -},{"../debug-build.js":40,"@sentry/core":70,"@sentry/utils":139}],46:[function(require,module,exports){ +},{"../debug-build.js":40,"@sentry/core":70,"@sentry/utils":152}],46:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -9544,7 +9563,7 @@ exports.GlobalHandlers = GlobalHandlers; exports.globalHandlersIntegration = globalHandlersIntegration; -},{"../debug-build.js":40,"../eventbuilder.js":41,"../helpers.js":42,"@sentry/core":70,"@sentry/utils":139}],47:[function(require,module,exports){ +},{"../debug-build.js":40,"../eventbuilder.js":41,"../helpers.js":42,"@sentry/core":70,"@sentry/utils":152}],47:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -9667,7 +9686,7 @@ exports.LinkedErrors = LinkedErrors; exports.linkedErrorsIntegration = linkedErrorsIntegration; -},{"../eventbuilder.js":41,"@sentry/core":70,"@sentry/utils":139}],50:[function(require,module,exports){ +},{"../eventbuilder.js":41,"@sentry/core":70,"@sentry/utils":152}],50:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -9951,7 +9970,7 @@ exports.TryCatch = TryCatch; exports.browserApiErrorsIntegration = browserApiErrorsIntegration; -},{"../helpers.js":42,"@sentry/core":70,"@sentry/utils":139}],51:[function(require,module,exports){ +},{"../helpers.js":42,"@sentry/core":70,"@sentry/utils":152}],51:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -10111,7 +10130,7 @@ exports.onProfilingStartRouteTransaction = onProfilingStartRouteTransaction; exports.startProfileForTransaction = startProfileForTransaction; -},{"../debug-build.js":40,"../helpers.js":42,"./utils.js":53,"@sentry/core":70,"@sentry/utils":139}],52:[function(require,module,exports){ +},{"../debug-build.js":40,"../helpers.js":42,"./utils.js":53,"@sentry/core":70,"@sentry/utils":152}],52:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -10230,7 +10249,7 @@ exports.BrowserProfilingIntegration = BrowserProfilingIntegration; exports.browserProfilingIntegration = browserProfilingIntegration; -},{"../debug-build.js":40,"./hubextensions.js":51,"./utils.js":53,"@sentry/core":70,"@sentry/utils":139}],53:[function(require,module,exports){ +},{"../debug-build.js":40,"./hubextensions.js":51,"./utils.js":53,"@sentry/core":70,"@sentry/utils":152}],53:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -10837,7 +10856,7 @@ exports.startJSSelfProfile = startJSSelfProfile; exports.takeProfileFromGlobalCache = takeProfileFromGlobalCache; -},{"../debug-build.js":40,"../helpers.js":42,"@sentry/core":70,"@sentry/utils":139}],54:[function(require,module,exports){ +},{"../debug-build.js":40,"../helpers.js":42,"@sentry/core":70,"@sentry/utils":152}],54:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -11117,7 +11136,7 @@ exports.showReportDialog = showReportDialog; exports.wrap = wrap; -},{"./client.js":39,"./debug-build.js":40,"./helpers.js":42,"./integrations/breadcrumbs.js":44,"./integrations/dedupe.js":45,"./integrations/globalhandlers.js":46,"./integrations/httpcontext.js":47,"./integrations/linkederrors.js":49,"./integrations/trycatch.js":50,"./stack-parsers.js":55,"./transports/fetch.js":56,"./transports/xhr.js":59,"@sentry/core":70,"@sentry/utils":139}],55:[function(require,module,exports){ +},{"./client.js":39,"./debug-build.js":40,"./helpers.js":42,"./integrations/breadcrumbs.js":44,"./integrations/dedupe.js":45,"./integrations/globalhandlers.js":46,"./integrations/httpcontext.js":47,"./integrations/linkederrors.js":49,"./integrations/trycatch.js":50,"./stack-parsers.js":55,"./transports/fetch.js":56,"./transports/xhr.js":59,"@sentry/core":70,"@sentry/utils":152}],55:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -11297,7 +11316,7 @@ exports.opera11StackLineParser = opera11StackLineParser; exports.winjsStackLineParser = winjsStackLineParser; -},{"@sentry/utils":139}],56:[function(require,module,exports){ +},{"@sentry/utils":152}],56:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -11365,7 +11384,7 @@ function makeFetchTransport( exports.makeFetchTransport = makeFetchTransport; -},{"./utils.js":58,"@sentry/core":70,"@sentry/utils":139}],57:[function(require,module,exports){ +},{"./utils.js":58,"@sentry/core":70,"@sentry/utils":152}],57:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -11505,7 +11524,7 @@ exports.makeBrowserOfflineTransport = makeBrowserOfflineTransport; exports.pop = pop; -},{"@sentry/core":70,"@sentry/utils":139}],58:[function(require,module,exports){ +},{"@sentry/core":70,"@sentry/utils":152}],58:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -11595,7 +11614,7 @@ exports.clearCachedFetchImplementation = clearCachedFetchImplementation; exports.getNativeFetchImplementation = getNativeFetchImplementation; -},{"../debug-build.js":40,"../helpers.js":42,"@sentry/utils":139}],59:[function(require,module,exports){ +},{"../debug-build.js":40,"../helpers.js":42,"@sentry/utils":152}],59:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); @@ -11651,7 +11670,7 @@ function makeXHRTransport(options) { exports.makeXHRTransport = makeXHRTransport; -},{"@sentry/core":70,"@sentry/utils":139}],60:[function(require,module,exports){ +},{"@sentry/core":70,"@sentry/utils":152}],60:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -11696,7 +11715,7 @@ function createUserFeedbackEnvelopeItem(feedback) { exports.createUserFeedbackEnvelope = createUserFeedbackEnvelope; -},{"@sentry/utils":139}],61:[function(require,module,exports){ +},{"@sentry/utils":152}],61:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -11795,7 +11814,7 @@ exports.getEnvelopeEndpointWithUrlEncodedAuth = getEnvelopeEndpointWithUrlEncode exports.getReportDialogEndpoint = getReportDialogEndpoint; -},{"@sentry/utils":139}],62:[function(require,module,exports){ +},{"@sentry/utils":152}],62:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -12608,7 +12627,7 @@ exports.BaseClient = BaseClient; exports.addEventProcessor = addEventProcessor; -},{"./api.js":61,"./debug-build.js":65,"./envelope.js":66,"./exports.js":68,"./hub.js":69,"./integration.js":71,"./metrics/envelope.js":82,"./session.js":92,"./tracing/dynamicSamplingContext.js":95,"./utils/prepareEvent.js":115,"@sentry/utils":139}],63:[function(require,module,exports){ +},{"./api.js":61,"./debug-build.js":65,"./envelope.js":66,"./exports.js":68,"./hub.js":69,"./integration.js":71,"./metrics/envelope.js":82,"./session.js":92,"./tracing/dynamicSamplingContext.js":95,"./utils/prepareEvent.js":115,"@sentry/utils":152}],63:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -12656,7 +12675,7 @@ function createCheckInEnvelopeItem(checkIn) { exports.createCheckInEnvelope = createCheckInEnvelope; -},{"@sentry/utils":139}],64:[function(require,module,exports){ +},{"@sentry/utils":152}],64:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const DEFAULT_ENVIRONMENT = 'production'; @@ -12745,7 +12764,7 @@ exports.createEventEnvelope = createEventEnvelope; exports.createSessionEnvelope = createSessionEnvelope; -},{"@sentry/utils":139}],67:[function(require,module,exports){ +},{"@sentry/utils":152}],67:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -12804,7 +12823,7 @@ exports.getGlobalEventProcessors = getGlobalEventProcessors; exports.notifyEventProcessors = notifyEventProcessors; -},{"./debug-build.js":65,"@sentry/utils":139}],68:[function(require,module,exports){ +},{"./debug-build.js":65,"@sentry/utils":152}],68:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -13318,7 +13337,7 @@ exports.withMonitor = withMonitor; exports.withScope = withScope; -},{"./constants.js":64,"./debug-build.js":65,"./hub.js":69,"./session.js":92,"./utils/prepareEvent.js":115,"@sentry/utils":139}],69:[function(require,module,exports){ +},{"./constants.js":64,"./debug-build.js":65,"./hub.js":69,"./session.js":92,"./utils/prepareEvent.js":115,"@sentry/utils":152}],69:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -14149,7 +14168,7 @@ exports.setAsyncContextStrategy = setAsyncContextStrategy; exports.setHubOnCarrier = setHubOnCarrier; -},{"./constants.js":64,"./debug-build.js":65,"./scope.js":88,"./session.js":92,"./version.js":118,"@sentry/utils":139}],70:[function(require,module,exports){ +},{"./constants.js":64,"./debug-build.js":65,"./scope.js":88,"./session.js":92,"./version.js":118,"@sentry/utils":152}],70:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const hubextensions = require('./tracing/hubextensions.js'); @@ -14527,7 +14546,7 @@ exports.setupIntegration = setupIntegration; exports.setupIntegrations = setupIntegrations; -},{"./debug-build.js":65,"./eventProcessors.js":67,"./exports.js":68,"./hub.js":69,"@sentry/utils":139}],72:[function(require,module,exports){ +},{"./debug-build.js":65,"./eventProcessors.js":67,"./exports.js":68,"./hub.js":69,"@sentry/utils":152}],72:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -14597,7 +14616,7 @@ exports.FunctionToString = FunctionToString; exports.functionToStringIntegration = functionToStringIntegration; -},{"../exports.js":68,"../integration.js":71,"@sentry/utils":139}],73:[function(require,module,exports){ +},{"../exports.js":68,"../integration.js":71,"@sentry/utils":152}],73:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -14826,7 +14845,7 @@ exports.InboundFilters = InboundFilters; exports.inboundFiltersIntegration = inboundFiltersIntegration; -},{"../debug-build.js":65,"../integration.js":71,"@sentry/utils":139}],74:[function(require,module,exports){ +},{"../debug-build.js":65,"../integration.js":71,"@sentry/utils":152}],74:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const functiontostring = require('./functiontostring.js'); @@ -14890,7 +14909,7 @@ exports.LinkedErrors = LinkedErrors; exports.linkedErrorsIntegration = linkedErrorsIntegration; -},{"../integration.js":71,"@sentry/utils":139}],76:[function(require,module,exports){ +},{"../integration.js":71,"@sentry/utils":152}],76:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -14957,7 +14976,7 @@ exports.ModuleMetadata = ModuleMetadata; exports.moduleMetadataIntegration = moduleMetadataIntegration; -},{"../integration.js":71,"../metadata.js":78,"@sentry/utils":139}],77:[function(require,module,exports){ +},{"../integration.js":71,"../metadata.js":78,"@sentry/utils":152}],77:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -15137,7 +15156,7 @@ exports.RequestData = RequestData; exports.requestDataIntegration = requestDataIntegration; -},{"../integration.js":71,"../utils/spanUtils.js":117,"@sentry/utils":139}],78:[function(require,module,exports){ +},{"../integration.js":71,"../utils/spanUtils.js":117,"@sentry/utils":152}],78:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -15242,7 +15261,7 @@ exports.getMetadataForUrl = getMetadataForUrl; exports.stripMetadataFromStackFrames = stripMetadataFromStackFrames; -},{"@sentry/utils":139}],79:[function(require,module,exports){ +},{"@sentry/utils":152}],79:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils$1 = require('@sentry/utils'); @@ -15418,7 +15437,7 @@ class MetricsAggregator { exports.MetricsAggregator = MetricsAggregator; -},{"./constants.js":81,"./instance.js":84,"./metric-summary.js":86,"./utils.js":87,"@sentry/utils":139}],80:[function(require,module,exports){ +},{"./constants.js":81,"./instance.js":84,"./metric-summary.js":86,"./utils.js":87,"@sentry/utils":152}],80:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils$1 = require('@sentry/utils'); @@ -15519,7 +15538,7 @@ class BrowserMetricsAggregator { exports.BrowserMetricsAggregator = BrowserMetricsAggregator; -},{"./constants.js":81,"./instance.js":84,"./metric-summary.js":86,"./utils.js":87,"@sentry/utils":139}],81:[function(require,module,exports){ +},{"./constants.js":81,"./instance.js":84,"./metric-summary.js":86,"./utils.js":87,"@sentry/utils":152}],81:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const COUNTER_METRIC_TYPE = 'c' ; @@ -15599,7 +15618,7 @@ function createMetricEnvelopeItem(metricBucketItems) { exports.createMetricEnvelope = createMetricEnvelope; -},{"./utils.js":87,"@sentry/utils":139}],83:[function(require,module,exports){ +},{"./utils.js":87,"@sentry/utils":152}],83:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -15697,7 +15716,7 @@ exports.metrics = metrics; exports.set = set; -},{"../debug-build.js":65,"../exports.js":68,"../utils/spanUtils.js":117,"./constants.js":81,"./integration.js":85,"@sentry/utils":139}],84:[function(require,module,exports){ +},{"../debug-build.js":65,"../exports.js":68,"../utils/spanUtils.js":117,"./constants.js":81,"./integration.js":85,"@sentry/utils":152}],84:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const constants = require('./constants.js'); @@ -15962,7 +15981,7 @@ exports.getMetricSummaryJsonForSpan = getMetricSummaryJsonForSpan; exports.updateMetricSummaryOnActiveSpan = updateMetricSummaryOnActiveSpan; -},{"../debug-build.js":65,"../tracing/errors.js":96,"../tracing/spanstatus.js":102,"../tracing/trace.js":103,"@sentry/utils":139}],87:[function(require,module,exports){ +},{"../debug-build.js":65,"../tracing/errors.js":96,"../tracing/spanstatus.js":102,"../tracing/trace.js":103,"@sentry/utils":152}],87:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -16077,7 +16096,7 @@ exports.serializeMetricBuckets = serializeMetricBuckets; exports.simpleHash = simpleHash; -},{"@sentry/utils":139}],88:[function(require,module,exports){ +},{"@sentry/utils":152}],88:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -16769,7 +16788,7 @@ exports.getGlobalScope = getGlobalScope; exports.setGlobalScope = setGlobalScope; -},{"./eventProcessors.js":67,"./session.js":92,"./utils/applyScopeDataToEvent.js":109,"@sentry/utils":139}],89:[function(require,module,exports){ +},{"./eventProcessors.js":67,"./session.js":92,"./utils/applyScopeDataToEvent.js":109,"@sentry/utils":152}],89:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -16840,7 +16859,7 @@ exports.initAndBind = initAndBind; exports.setCurrentClient = setCurrentClient; -},{"./debug-build.js":65,"./exports.js":68,"./hub.js":69,"@sentry/utils":139}],90:[function(require,module,exports){ +},{"./debug-build.js":65,"./exports.js":68,"./hub.js":69,"@sentry/utils":152}],90:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); /** @@ -17141,7 +17160,7 @@ class ServerRuntimeClient exports.ServerRuntimeClient = ServerRuntimeClient; -},{"./baseclient.js":62,"./checkin.js":63,"./debug-build.js":65,"./exports.js":68,"./metrics/aggregator.js":79,"./sessionflusher.js":93,"./tracing/dynamicSamplingContext.js":95,"./tracing/hubextensions.js":97,"./tracing/spanstatus.js":102,"./utils/getRootSpan.js":110,"./utils/spanUtils.js":117,"@sentry/utils":139}],92:[function(require,module,exports){ +},{"./baseclient.js":62,"./checkin.js":63,"./debug-build.js":65,"./exports.js":68,"./metrics/aggregator.js":79,"./sessionflusher.js":93,"./tracing/dynamicSamplingContext.js":95,"./tracing/hubextensions.js":97,"./tracing/spanstatus.js":102,"./utils/getRootSpan.js":110,"./utils/spanUtils.js":117,"@sentry/utils":152}],92:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -17307,7 +17326,7 @@ exports.makeSession = makeSession; exports.updateSession = updateSession; -},{"@sentry/utils":139}],93:[function(require,module,exports){ +},{"@sentry/utils":152}],93:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -17421,7 +17440,7 @@ class SessionFlusher { exports.SessionFlusher = SessionFlusher; -},{"./exports.js":68,"@sentry/utils":139}],94:[function(require,module,exports){ +},{"./exports.js":68,"@sentry/utils":152}],94:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -17452,7 +17471,7 @@ function createSpanItem(span) { exports.createSpanEnvelope = createSpanEnvelope; -},{"@sentry/utils":139}],95:[function(require,module,exports){ +},{"@sentry/utils":152}],95:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -17552,7 +17571,7 @@ exports.getDynamicSamplingContextFromClient = getDynamicSamplingContextFromClien exports.getDynamicSamplingContextFromSpan = getDynamicSamplingContextFromSpan; -},{"../constants.js":64,"../exports.js":68,"../utils/getRootSpan.js":110,"../utils/spanUtils.js":117,"@sentry/utils":139}],96:[function(require,module,exports){ +},{"../constants.js":64,"../exports.js":68,"../utils/getRootSpan.js":110,"../utils/spanUtils.js":117,"@sentry/utils":152}],96:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -17594,7 +17613,7 @@ errorCallback.tag = 'sentry_tracingErrorCallback'; exports.registerErrorInstrumentation = registerErrorInstrumentation; -},{"../debug-build.js":65,"./utils.js":105,"@sentry/utils":139}],97:[function(require,module,exports){ +},{"../debug-build.js":65,"./utils.js":105,"@sentry/utils":152}],97:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -17753,7 +17772,7 @@ exports.addTracingExtensions = addTracingExtensions; exports.startIdleTransaction = startIdleTransaction; -},{"../debug-build.js":65,"../hub.js":69,"../utils/spanUtils.js":117,"./errors.js":96,"./idletransaction.js":98,"./sampling.js":100,"./transaction.js":104,"@sentry/utils":139}],98:[function(require,module,exports){ +},{"../debug-build.js":65,"../hub.js":69,"../utils/spanUtils.js":117,"./errors.js":96,"./idletransaction.js":98,"./sampling.js":100,"./transaction.js":104,"@sentry/utils":152}],98:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -18162,7 +18181,7 @@ exports.IdleTransactionSpanRecorder = IdleTransactionSpanRecorder; exports.TRACING_DEFAULTS = TRACING_DEFAULTS; -},{"../debug-build.js":65,"../utils/spanUtils.js":117,"./span.js":101,"./transaction.js":104,"@sentry/utils":139}],99:[function(require,module,exports){ +},{"../debug-build.js":65,"../utils/spanUtils.js":117,"./span.js":101,"./transaction.js":104,"@sentry/utils":152}],99:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('./utils.js'); @@ -18313,7 +18332,7 @@ exports.isValidSampleRate = isValidSampleRate; exports.sampleTransaction = sampleTransaction; -},{"../debug-build.js":65,"../semanticAttributes.js":90,"../utils/hasTracingEnabled.js":112,"../utils/spanUtils.js":117,"@sentry/utils":139}],101:[function(require,module,exports){ +},{"../debug-build.js":65,"../semanticAttributes.js":90,"../utils/hasTracingEnabled.js":112,"../utils/spanUtils.js":117,"@sentry/utils":152}],101:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -18959,7 +18978,7 @@ exports.Span = Span; exports.SpanRecorder = SpanRecorder; -},{"../debug-build.js":65,"../metrics/metric-summary.js":86,"../semanticAttributes.js":90,"../utils/getRootSpan.js":110,"../utils/spanUtils.js":117,"./spanstatus.js":102,"@sentry/utils":139}],102:[function(require,module,exports){ +},{"../debug-build.js":65,"../metrics/metric-summary.js":86,"../semanticAttributes.js":90,"../utils/getRootSpan.js":110,"../utils/spanUtils.js":117,"./spanstatus.js":102,"@sentry/utils":152}],102:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); /** The status of an Span. @@ -19482,7 +19501,7 @@ exports.startSpanManual = startSpanManual; exports.trace = trace; -},{"../debug-build.js":65,"../exports.js":68,"../hub.js":69,"../utils/handleCallbackErrors.js":111,"../utils/hasTracingEnabled.js":112,"../utils/spanUtils.js":117,"./dynamicSamplingContext.js":95,"./errors.js":96,"./spanstatus.js":102,"@sentry/utils":139}],104:[function(require,module,exports){ +},{"../debug-build.js":65,"../exports.js":68,"../hub.js":69,"../utils/handleCallbackErrors.js":111,"../utils/hasTracingEnabled.js":112,"../utils/spanUtils.js":117,"./dynamicSamplingContext.js":95,"./errors.js":96,"./spanstatus.js":102,"@sentry/utils":152}],104:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -19836,7 +19855,7 @@ class Transaction extends span.Span { exports.Transaction = Transaction; -},{"../debug-build.js":65,"../hub.js":69,"../metrics/metric-summary.js":86,"../semanticAttributes.js":90,"../utils/spanUtils.js":117,"./dynamicSamplingContext.js":95,"./span.js":101,"./trace.js":103,"@sentry/utils":139}],105:[function(require,module,exports){ +},{"../debug-build.js":65,"../hub.js":69,"../metrics/metric-summary.js":86,"../semanticAttributes.js":90,"../utils/spanUtils.js":117,"./dynamicSamplingContext.js":95,"./span.js":101,"./trace.js":103,"@sentry/utils":152}],105:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -19876,7 +19895,7 @@ exports.extractTraceparentData = extractTraceparentData; exports.getActiveTransaction = getActiveTransaction; -},{"../hub.js":69,"@sentry/utils":139}],106:[function(require,module,exports){ +},{"../hub.js":69,"@sentry/utils":152}],106:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -19983,7 +20002,7 @@ exports.DEFAULT_TRANSPORT_BUFFER_SIZE = DEFAULT_TRANSPORT_BUFFER_SIZE; exports.createTransport = createTransport; -},{"../debug-build.js":65,"@sentry/utils":139}],107:[function(require,module,exports){ +},{"../debug-build.js":65,"@sentry/utils":152}],107:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -20106,7 +20125,7 @@ exports.eventFromEnvelope = eventFromEnvelope; exports.makeMultiplexedTransport = makeMultiplexedTransport; -},{"../api.js":61,"@sentry/utils":139}],108:[function(require,module,exports){ +},{"../api.js":61,"@sentry/utils":152}],108:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -20235,7 +20254,7 @@ exports.START_DELAY = START_DELAY; exports.makeOfflineTransport = makeOfflineTransport; -},{"../debug-build.js":65,"@sentry/utils":139}],109:[function(require,module,exports){ +},{"../debug-build.js":65,"@sentry/utils":152}],109:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const utils = require('@sentry/utils'); @@ -20430,7 +20449,7 @@ exports.mergeAndOverwriteScopeData = mergeAndOverwriteScopeData; exports.mergeScopeData = mergeScopeData; -},{"../tracing/dynamicSamplingContext.js":95,"./getRootSpan.js":110,"./spanUtils.js":117,"@sentry/utils":139}],110:[function(require,module,exports){ +},{"../tracing/dynamicSamplingContext.js":95,"./getRootSpan.js":110,"./spanUtils.js":117,"@sentry/utils":152}],110:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); /** @@ -20519,7 +20538,7 @@ function maybeHandlePromiseRejection( exports.handleCallbackErrors = handleCallbackErrors; -},{"@sentry/utils":139}],112:[function(require,module,exports){ +},{"@sentry/utils":152}],112:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const exports$1 = require('../exports.js'); @@ -21009,7 +21028,7 @@ exports.parseEventHintOrCaptureContext = parseEventHintOrCaptureContext; exports.prepareEvent = prepareEvent; -},{"../constants.js":64,"../eventProcessors.js":67,"../scope.js":88,"./applyScopeDataToEvent.js":109,"./spanUtils.js":117,"@sentry/utils":139}],116:[function(require,module,exports){ +},{"../constants.js":64,"../eventProcessors.js":67,"../scope.js":88,"./applyScopeDataToEvent.js":109,"./spanUtils.js":117,"@sentry/utils":152}],116:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const version = require('../version.js'); @@ -21167,15995 +21186,20298 @@ exports.spanToTraceContext = spanToTraceContext; exports.spanToTraceHeader = spanToTraceHeader; -},{"@sentry/utils":139}],118:[function(require,module,exports){ +},{"@sentry/utils":152}],118:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const SDK_VERSION = '7.111.0'; +const SDK_VERSION = '7.112.0'; exports.SDK_VERSION = SDK_VERSION; },{}],119:[function(require,module,exports){ -var { - _nullishCoalesce, - _optionalChain -} = require('@sentry/utils'); +Object.defineProperty(exports, '__esModule', { value: true }); + +const core = require('@sentry/core'); +const utils = require('@sentry/utils'); + +const INTEGRATION_NAME = 'CaptureConsole'; + +const _captureConsoleIntegration = ((options = {}) => { + const levels = options.levels || utils.CONSOLE_LEVELS; + + return { + name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function + setup(client) { + if (!('console' in utils.GLOBAL_OBJ)) { + return; + } + + utils.addConsoleInstrumentationHandler(({ args, level }) => { + if (core.getClient() !== client || !levels.includes(level)) { + return; + } + + consoleHandler(args, level); + }); + }, + }; +}) ; + +const captureConsoleIntegration = core.defineIntegration(_captureConsoleIntegration); + +/** + * Send Console API calls as Sentry Events. + * @deprecated Use `captureConsoleIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +const CaptureConsole = core.convertIntegrationFnToClass( + INTEGRATION_NAME, + captureConsoleIntegration, +) + +; + +function consoleHandler(args, level) { + const captureContext = { + level: utils.severityLevelFromString(level), + extra: { + arguments: args, + }, + }; + + core.withScope(scope => { + scope.addEventProcessor(event => { + event.logger = 'console'; + + utils.addExceptionMechanism(event, { + handled: false, + type: 'console', + }); + + return event; + }); + + if (level === 'assert' && args[0] === false) { + const message = `Assertion failed: ${utils.safeJoin(args.slice(1), ' ') || 'console.assert'}`; + scope.setExtra('arguments', args.slice(1)); + core.captureMessage(message, captureContext); + return; + } + + const error = args.find(arg => arg instanceof Error); + if (level === 'error' && error) { + core.captureException(error, captureContext); + return; + } + + const message = utils.safeJoin(args, ' '); + core.captureMessage(message, captureContext); + }); +} +exports.CaptureConsole = CaptureConsole; +exports.captureConsoleIntegration = captureConsoleIntegration; + + +},{"@sentry/core":70,"@sentry/utils":152}],120:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); const utils = require('@sentry/utils'); -const tracing = require('@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 = utils.GLOBAL_OBJ ; -const REPLAY_SESSION_KEY = 'sentryReplaySession'; -const REPLAY_EVENT_NAME = 'replay_event'; -const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay'; +const DEFAULT_LINES_OF_CONTEXT = 7; -// The idle limit for a session after which recording is paused. -const SESSION_IDLE_PAUSE_DURATION = 300000; // 5 minutes in ms +const INTEGRATION_NAME = 'ContextLines'; -// The idle limit for a session after which the session expires. -const SESSION_IDLE_EXPIRE_DURATION = 900000; // 15 minutes in ms +const _contextLinesIntegration = ((options = {}) => { + const contextLines = options.frameContextLines != null ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; -/** 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; + return { + name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function + processEvent(event) { + return addSourceContext(event, contextLines); + }, + }; +}) ; -/* How long to wait for error checkouts */ -const BUFFER_CHECKOUT_TIME = 60000; +const contextLinesIntegration = core.defineIntegration(_contextLinesIntegration); -const RETRY_BASE_INTERVAL = 5000; -const RETRY_MAX_COUNT = 3; +/** + * Collects source context lines around the lines of stackframes pointing to JS embedded in + * the current page's HTML. + * + * This integration DOES NOT work for stack frames pointing to JS files that are loaded by the browser. + * For frames pointing to files, context lines are added during ingestion and symbolication + * by attempting to download the JS files to the Sentry backend. + * + * Use this integration if you have inline JS code in HTML pages that can't be accessed + * by our backend (e.g. due to a login-protected page). + * + * @deprecated Use `contextLinesIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +const ContextLines = core.convertIntegrationFnToClass(INTEGRATION_NAME, contextLinesIntegration) -/* 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; +/** + * Processes an event and adds context lines. + */ +function addSourceContext(event, contextLines) { + const doc = WINDOW.document; + const htmlFilename = WINDOW.location && utils.stripUrlQueryAndFragment(WINDOW.location.href); + if (!doc || !htmlFilename) { + return event; + } -/* 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; + const exceptions = event.exception && event.exception.values; + if (!exceptions || !exceptions.length) { + return event; + } -/** 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 + const html = doc.documentElement.innerHTML; + if (!html) { + return event; + } -/** 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; + const htmlLines = ['', '', ...html.split('\n'), '']; -/** The max. length of a replay. */ -const MAX_REPLAY_DURATION = 3600000; // 60 minutes in ms; + exceptions.forEach(exception => { + const stacktrace = exception.stacktrace; + if (stacktrace && stacktrace.frames) { + stacktrace.frames = stacktrace.frames.map(frame => + applySourceContextToFrame(frame, htmlLines, htmlFilename, contextLines), + ); + } + }); -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 = {})); + return event; +} -function isElement$1(n) { - return n.nodeType === n.ELEMENT_NODE; +/** + * Only exported for testing + */ +function applySourceContextToFrame( + frame, + htmlLines, + htmlFilename, + linesOfContext, +) { + if (frame.filename !== htmlFilename || !frame.lineno || !htmlLines.length) { + return frame; + } + + utils.addContextToFrame(htmlLines, frame, linesOfContext); + + return frame; } -function isShadowRoot(n) { - const host = _optionalChain$5([n, 'optionalAccess', _ => _.host]); - return Boolean(_optionalChain$5([host, 'optionalAccess', _2 => _2.shadowRoot]) === n); + +exports.ContextLines = ContextLines; +exports.applySourceContextToFrame = applySourceContextToFrame; +exports.contextLinesIntegration = contextLinesIntegration; + + +},{"@sentry/core":70,"@sentry/utils":152}],121:[function(require,module,exports){ +arguments[4][26][0].apply(exports,arguments) +},{"dup":26}],122:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const core = require('@sentry/core'); +const utils = require('@sentry/utils'); + +const INTEGRATION_NAME = 'Debug'; + +const _debugIntegration = ((options = {}) => { + const _options = { + debugger: false, + stringify: false, + ...options, + }; + + return { + name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function + setup(client) { + if (!client.on) { + return; + } + + client.on('beforeSendEvent', (event, hint) => { + if (_options.debugger) { + // eslint-disable-next-line no-debugger + debugger; + } + + /* eslint-disable no-console */ + utils.consoleSandbox(() => { + if (_options.stringify) { + console.log(JSON.stringify(event, null, 2)); + if (hint && Object.keys(hint).length) { + console.log(JSON.stringify(hint, null, 2)); + } + } else { + console.log(event); + if (hint && Object.keys(hint).length) { + console.log(hint); + } + } + }); + /* eslint-enable no-console */ + }); + }, + }; +}) ; + +const debugIntegration = core.defineIntegration(_debugIntegration); + +/** + * Integration to debug sent Sentry events. + * This integration should not be used in production. + * + * @deprecated Use `debugIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +const Debug = core.convertIntegrationFnToClass(INTEGRATION_NAME, debugIntegration) + +; + +exports.Debug = Debug; +exports.debugIntegration = debugIntegration; + + +},{"@sentry/core":70,"@sentry/utils":152}],123:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const core = require('@sentry/core'); +const utils = require('@sentry/utils'); +const debugBuild = require('./debug-build.js'); + +const INTEGRATION_NAME = 'Dedupe'; + +const _dedupeIntegration = (() => { + let previousEvent; + + return { + name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function + processEvent(currentEvent) { + // We want to ignore any non-error type events, e.g. transactions or replays + // These should never be deduped, and also not be compared against as _previousEvent. + if (currentEvent.type) { + return currentEvent; + } + + // Juuust in case something goes wrong + try { + if (_shouldDropEvent(currentEvent, previousEvent)) { + debugBuild.DEBUG_BUILD && utils.logger.warn('Event dropped due to being a duplicate of previously captured event.'); + return null; + } + } catch (_oO) {} // eslint-disable-line no-empty + + return (previousEvent = currentEvent); + }, + }; +}) ; + +const dedupeIntegration = core.defineIntegration(_dedupeIntegration); + +/** + * Deduplication filter. + * @deprecated Use `dedupeIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +const Dedupe = core.convertIntegrationFnToClass(INTEGRATION_NAME, dedupeIntegration) + +; + +/** only exported for tests. */ +function _shouldDropEvent(currentEvent, previousEvent) { + if (!previousEvent) { + return false; + } + + if (_isSameMessageEvent(currentEvent, previousEvent)) { + return true; + } + + if (_isSameExceptionEvent(currentEvent, previousEvent)) { + return true; + } + + return false; } -function isNativeShadowDom(shadowRoot) { - return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]'; + +function _isSameMessageEvent(currentEvent, previousEvent) { + const currentMessage = currentEvent.message; + const previousMessage = previousEvent.message; + + // If neither event has a message property, they were both exceptions, so bail out + if (!currentMessage && !previousMessage) { + return false; + } + + // If only one event has a stacktrace, but not the other one, they are not the same + if ((currentMessage && !previousMessage) || (!currentMessage && previousMessage)) { + return false; + } + + if (currentMessage !== previousMessage) { + return false; + } + + if (!_isSameFingerprint(currentEvent, previousEvent)) { + return false; + } + + if (!_isSameStacktrace(currentEvent, previousEvent)) { + return false; + } + + return true; } -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 _isSameExceptionEvent(currentEvent, previousEvent) { + const previousException = _getExceptionFromEvent(previousEvent); + const currentException = _getExceptionFromEvent(currentEvent); + + if (!previousException || !currentException) { + return false; + } + + if (previousException.type !== currentException.type || previousException.value !== currentException.value) { + return false; + } + + if (!_isSameFingerprint(currentEvent, previousEvent)) { + return false; + } + + if (!_isSameStacktrace(currentEvent, previousEvent)) { + return false; + } + + return true; } -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); + +function _isSameStacktrace(currentEvent, previousEvent) { + let currentFrames = _getFramesFromEvent(currentEvent); + let previousFrames = _getFramesFromEvent(previousEvent); + + // If neither event has a stacktrace, they are assumed to be the same + if (!currentFrames && !previousFrames) { + return true; + } + + // If only one event has a stacktrace, but not the other one, they are not the same + if ((currentFrames && !previousFrames) || (!currentFrames && previousFrames)) { + return false; + } + + currentFrames = currentFrames ; + previousFrames = previousFrames ; + + // If number of frames differ, they are not the same + if (previousFrames.length !== currentFrames.length) { + return false; + } + + // Otherwise, compare the two + for (let i = 0; i < previousFrames.length; i++) { + const frameA = previousFrames[i]; + const frameB = currentFrames[i]; + + if ( + frameA.filename !== frameB.filename || + frameA.lineno !== frameB.lineno || + frameA.colno !== frameB.colno || + frameA.function !== frameB.function + ) { + return false; } - return statement.join(' ') + ';'; + } + + return true; } -function stringifyStylesheet(s) { + +function _isSameFingerprint(currentEvent, previousEvent) { + let currentFingerprint = currentEvent.fingerprint; + let previousFingerprint = previousEvent.fingerprint; + + // If neither event has a fingerprint, they are assumed to be the same + if (!currentFingerprint && !previousFingerprint) { + return true; + } + + // If only one event has a fingerprint, but not the other one, they are not the same + if ((currentFingerprint && !previousFingerprint) || (!currentFingerprint && previousFingerprint)) { + return false; + } + + currentFingerprint = currentFingerprint ; + previousFingerprint = previousFingerprint ; + + // Otherwise, compare the two + try { + return !!(currentFingerprint.join('') === previousFingerprint.join('')); + } catch (_oO) { + return false; + } +} + +function _getExceptionFromEvent(event) { + return event.exception && event.exception.values && event.exception.values[0]; +} + +function _getFramesFromEvent(event) { + const exception = event.exception; + + if (exception) { try { - const rules = s.rules || s.cssRules; - return rules - ? fixBrowserCompatibilityIssuesInCSS(Array.from(rules, stringifyRule).join('')) - : null; - } - catch (error) { - return null; + // @ts-expect-error Object could be undefined + return exception.values[0].stacktrace.frames; + } catch (_oO) { + return undefined; } + } + return undefined; } -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); + +exports.Dedupe = Dedupe; +exports._shouldDropEvent = _shouldDropEvent; +exports.dedupeIntegration = dedupeIntegration; + + +},{"./debug-build.js":121,"@sentry/core":70,"@sentry/utils":152}],124:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const core = require('@sentry/core'); +const utils = require('@sentry/utils'); +const debugBuild = require('./debug-build.js'); + +const INTEGRATION_NAME = 'ExtraErrorData'; + +const _extraErrorDataIntegration = ((options = {}) => { + const depth = options.depth || 3; + + // TODO(v8): Flip the default for this option to true + const captureErrorCause = options.captureErrorCause || false; + + return { + name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function + processEvent(event, hint) { + return _enhanceEventWithErrorData(event, hint, depth, captureErrorCause); + }, + }; +}) ; + +const extraErrorDataIntegration = core.defineIntegration(_extraErrorDataIntegration); + +/** + * Extract additional data for from original exceptions. + * @deprecated Use `extraErrorDataIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +const ExtraErrorData = core.convertIntegrationFnToClass( + INTEGRATION_NAME, + extraErrorDataIntegration, +) + +; + +function _enhanceEventWithErrorData( + event, + hint = {}, + depth, + captureErrorCause, +) { + if (!hint.originalException || !utils.isError(hint.originalException)) { + return event; + } + const exceptionName = (hint.originalException ).name || hint.originalException.constructor.name; + + const errorData = _extractErrorData(hint.originalException , captureErrorCause); + + if (errorData) { + const contexts = { + ...event.contexts, + }; + + const normalizedErrorData = utils.normalize(errorData, depth); + + if (utils.isPlainObject(normalizedErrorData)) { + // We mark the error data as "already normalized" here, because we don't want other normalization procedures to + // potentially truncate the data we just already normalized, with a certain depth setting. + utils.addNonEnumerableProperty(normalizedErrorData, '__sentry_skip_normalization__', true); + contexts[exceptionName] = normalizedErrorData; } - 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; + + return { + ...event, + contexts, + }; + } + + return event; } -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; + +/** + * Extract extra information from the Error object + */ +function _extractErrorData(error, captureErrorCause) { + // We are trying to enhance already existing event, so no harm done if it won't succeed + try { + const nativeKeys = [ + 'name', + 'message', + 'stack', + 'line', + 'column', + 'fileName', + 'lineNumber', + 'columnNumber', + 'toJSON', + ]; + + const extraErrorInfo = {}; + + // We want only enumerable properties, thus `getOwnPropertyNames` is redundant here, as we filter keys anyway. + for (const key of Object.keys(error)) { + if (nativeKeys.indexOf(key) !== -1) { + continue; + } + const value = error[key]; + extraErrorInfo[key] = utils.isError(value) ? value.toString() : value; } - getIds() { - return Array.from(this.idNodeMap.keys()); + + // Error.cause is a standard property that is non enumerable, we therefore need to access it separately. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause + if (captureErrorCause && error.cause !== undefined) { + extraErrorInfo.cause = utils.isError(error.cause) ? error.cause.toString() : error.cause; } - getMeta(n) { - return this.nodeMetaMap.get(n) || null; + + // Check if someone attached `toJSON` method to grab even more properties (eg. axios is doing that) + if (typeof error.toJSON === 'function') { + const serializedError = error.toJSON() ; + + for (const key of Object.keys(serializedError)) { + const value = serializedError[key]; + extraErrorInfo[key] = utils.isError(value) ? value.toString() : value; + } } - 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); + + return extraErrorInfo; + } catch (oO) { + debugBuild.DEBUG_BUILD && utils.logger.error('Unable to extract extra data from the Error object:', oO); + } + + return null; +} + +exports.ExtraErrorData = ExtraErrorData; +exports.extraErrorDataIntegration = extraErrorDataIntegration; + + +},{"./debug-build.js":121,"@sentry/core":70,"@sentry/utils":152}],125:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const core = require('@sentry/core'); +const utils = require('@sentry/utils'); +const debugBuild = require('./debug-build.js'); + +const INTEGRATION_NAME = 'HttpClient'; + +const _httpClientIntegration = ((options = {}) => { + const _options = { + failedRequestStatusCodes: [[500, 599]], + failedRequestTargets: [/.*/], + ...options, + }; + + return { + name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function + setup(client) { + _wrapFetch(client, _options); + _wrapXHR(client, _options); + }, + }; +}) ; + +const httpClientIntegration = core.defineIntegration(_httpClientIntegration); + +/** + * Create events for failed client side HTTP requests. + * @deprecated Use `httpClientIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +const HttpClient = core.convertIntegrationFnToClass(INTEGRATION_NAME, httpClientIntegration) + +; + +/** + * Interceptor function for fetch requests + * + * @param requestInfo The Fetch API request info + * @param response The Fetch API response + * @param requestInit The request init object + */ +function _fetchResponseHandler( + options, + requestInfo, + response, + requestInit, +) { + if (_shouldCaptureResponse(options, response.status, response.url)) { + const request = _getRequest(requestInfo, requestInit); + + let requestHeaders, responseHeaders, requestCookies, responseCookies; + + if (_shouldSendDefaultPii()) { + [{ headers: requestHeaders, cookies: requestCookies }, { headers: responseHeaders, cookies: responseCookies }] = [ + { cookieHeader: 'Cookie', obj: request }, + { cookieHeader: 'Set-Cookie', obj: response }, + ].map(({ cookieHeader, obj }) => { + const headers = _extractFetchHeaders(obj.headers); + let cookies; + + try { + const cookieString = headers[cookieHeader] || headers[cookieHeader.toLowerCase()] || undefined; + + if (cookieString) { + cookies = _parseCookieString(cookieString); + } + } catch (e) { + debugBuild.DEBUG_BUILD && utils.logger.log(`Could not extract cookies from header ${cookieHeader}`); } - this.idNodeMap.set(id, n); - } - reset() { - this.idNodeMap = new Map(); - this.nodeMetaMap = new WeakMap(); + + return { + headers, + cookies, + }; + }); } + + const event = _createEvent({ + url: request.url, + method: request.method, + status: response.status, + requestHeaders, + responseHeaders, + requestCookies, + responseCookies, + }); + + core.captureEvent(event); + } } -function createMirror() { - return new Mirror(); -} -function shouldMaskInput({ maskInputOptions, tagName, type, }) { - if (tagName === 'OPTION') { - tagName = 'SELECT'; + +/** + * Interceptor function for XHR requests + * + * @param xhr The XHR request + * @param method The HTTP method + * @param headers The HTTP headers + */ +function _xhrResponseHandler( + options, + xhr, + method, + headers, +) { + if (_shouldCaptureResponse(options, xhr.status, xhr.responseURL)) { + let requestHeaders, responseCookies, responseHeaders; + + if (_shouldSendDefaultPii()) { + try { + const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined; + + if (cookieString) { + responseCookies = _parseCookieString(cookieString); + } + } catch (e) { + debugBuild.DEBUG_BUILD && utils.logger.log('Could not extract cookies from response headers'); + } + + try { + responseHeaders = _getXHRResponseHeaders(xhr); + } catch (e) { + debugBuild.DEBUG_BUILD && utils.logger.log('Could not extract headers from response'); + } + + requestHeaders = headers; } - return Boolean(maskInputOptions[tagName.toLowerCase()] || - (type && maskInputOptions[type]) || - type === 'password' || - (tagName === 'INPUT' && !type && maskInputOptions['text'])); + + const event = _createEvent({ + url: xhr.responseURL, + method, + status: xhr.status, + requestHeaders, + // Can't access request cookies from XHR + responseHeaders, + responseCookies, + }); + + core.captureEvent(event); + } } -function maskInputValue({ isMasked, element, value, maskInputFn, }) { - let text = value || ''; - if (!isMasked) { - return text; - } - if (maskInputFn) { - text = maskInputFn(text, element); + +/** + * Extracts response size from `Content-Length` header when possible + * + * @param headers + * @returns The response size in bytes or undefined + */ +function _getResponseSizeFromHeaders(headers) { + if (headers) { + const contentLength = headers['Content-Length'] || headers['content-length']; + + if (contentLength) { + return parseInt(contentLength, 10); } - return '*'.repeat(text.length); -} -function toLowerCase(str) { - return str.toLowerCase(); + } + + return undefined; } -function toUpperCase(str) { - return str.toUpperCase(); + +/** + * Creates an object containing cookies from the given cookie string + * + * @param cookieString The cookie string to parse + * @returns The parsed cookies + */ +function _parseCookieString(cookieString) { + return cookieString.split('; ').reduce((acc, cookie) => { + const [key, value] = cookie.split('='); + acc[key] = value; + return acc; + }, {}); } -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; + +/** + * Extracts the headers as an object from the given Fetch API request or response object + * + * @param headers The headers to extract + * @returns The extracted headers as an object + */ +function _extractFetchHeaders(headers) { + const result = {}; + + headers.forEach((value, key) => { + result[key] = value; + }); + + return result; } -function getInputType(element) { - const type = element.type; - return element.hasAttribute('data-rr-is-password') - ? 'password' - : type - ? - toLowerCase(type) - : null; + +/** + * Extracts the response headers as an object from the given XHR object + * + * @param xhr The XHR object to extract the response headers from + * @returns The response headers as an object + */ +function _getXHRResponseHeaders(xhr) { + const headers = xhr.getAllResponseHeaders(); + + if (!headers) { + return {}; + } + + return headers.split('\r\n').reduce((acc, line) => { + const [key, value] = line.split(': '); + acc[key] = value; + return acc; + }, {}); } -function getInputValue(el, tagName, type) { - if (tagName === 'INPUT' && (type === 'radio' || type === 'checkbox')) { - return el.getAttribute('value') || ''; + +/** + * Checks if the given target url is in the given list of targets + * + * @param target The target url to check + * @returns true if the target url is in the given list of targets, false otherwise + */ +function _isInGivenRequestTargets( + failedRequestTargets, + target, +) { + return failedRequestTargets.some((givenRequestTarget) => { + if (typeof givenRequestTarget === 'string') { + return target.includes(givenRequestTarget); } - return el.value; -} -let _id = 1; -const tagNameRegex = new RegExp('[^a-z0-9-_:]'); -const IGNORED_NODE = -2; -function genId() { - return _id++; + return givenRequestTarget.test(target); + }); } -function getValidTagName(element) { - if (element instanceof HTMLFormElement) { - return 'form'; - } - const processedTagName = toLowerCase(element.tagName); - if (tagNameRegex.test(processedTagName)) { - return 'div'; + +/** + * Checks if the given status code is in the given range + * + * @param status The status code to check + * @returns true if the status code is in the given range, false otherwise + */ +function _isInGivenStatusRanges( + failedRequestStatusCodes, + status, +) { + return failedRequestStatusCodes.some((range) => { + if (typeof range === 'number') { + return range === status; } - return processedTagName; + + return status >= range[0] && status <= range[1]; + }); } -function extractOrigin(url) { - let origin = ''; - if (url.indexOf('//') > -1) { - origin = url.split('/').slice(0, 3).join('/'); + +/** + * Wraps `fetch` function to capture request and response data + */ +function _wrapFetch(client, options) { + if (!utils.supportsNativeFetch()) { + return; + } + + utils.addFetchInstrumentationHandler(handlerData => { + if (core.getClient() !== client) { + return; } - else { - origin = url.split('/')[0]; + + const { response, args } = handlerData; + const [requestInfo, requestInit] = args ; + + if (!response) { + return; } - 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})`; - }); + + _fetchResponseHandler(options, requestInfo, response , requestInit); + }); } -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 ''; + +/** + * Wraps XMLHttpRequest to capture request and response data + */ +function _wrapXHR(client, options) { + if (!('XMLHttpRequest' in utils.GLOBAL_OBJ)) { + return; + } + + utils.addXhrInstrumentationHandler(handlerData => { + if (core.getClient() !== client) { + 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; - } - } + + const xhr = handlerData.xhr ; + + const sentryXhrData = xhr[utils.SENTRY_XHR_DATA_KEY]; + + if (!sentryXhrData) { + return; } - return output.join(', '); -} -function absoluteToDoc(doc, attributeValue) { - if (!attributeValue || attributeValue.trim() === '') { - return attributeValue; + + const { method, request_headers: headers } = sentryXhrData; + + try { + _xhrResponseHandler(options, xhr, method, headers); + } catch (e) { + debugBuild.DEBUG_BUILD && utils.logger.warn('Error while extracting response event form XHR response', e); } - const a = doc.createElement('a'); - a.href = attributeValue; - return a.href; + }); } -function isSVGElement(el) { - return Boolean(el.tagName === 'svg' || el.ownerSVGElement); + +/** + * Checks whether to capture given response as an event + * + * @param status response status code + * @param url response url + */ +function _shouldCaptureResponse(options, status, url) { + return ( + _isInGivenStatusRanges(options.failedRequestStatusCodes, status) && + _isInGivenRequestTargets(options.failedRequestTargets, url) && + !core.isSentryRequestUrl(url, core.getClient()) + ); } -function getHref() { - const a = document.createElement('a'); - a.href = ''; - return a.href; + +/** + * Creates a synthetic Sentry event from given response data + * + * @param data response data + * @returns event + */ +function _createEvent(data + +) { + const message = `HTTP Client Error with status code: ${data.status}`; + + const event = { + message, + exception: { + values: [ + { + type: 'Error', + value: message, + }, + ], + }, + request: { + url: data.url, + method: data.method, + headers: data.requestHeaders, + cookies: data.requestCookies, + }, + contexts: { + response: { + status_code: data.status, + headers: data.responseHeaders, + cookies: data.responseCookies, + body_size: _getResponseSizeFromHeaders(data.responseHeaders), + }, + }, + }; + + utils.addExceptionMechanism(event, { + type: 'http.client', + handled: false, + }); + + return event; } -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 _getRequest(requestInfo, requestInit) { + if (!requestInit && requestInfo instanceof Request) { + return requestInfo; + } + + // If both are set, we try to construct a new Request with the given arguments + // However, if e.g. the original request has a `body`, this will throw an error because it was already accessed + // In this case, as a fallback, we just use the original request - using both is rather an edge case + if (requestInfo instanceof Request && requestInfo.bodyUsed) { + return requestInfo; + } + + return new Request(requestInfo, requestInit); } -function ignoreAttribute(tagName, name, _value) { - return (tagName === 'video' || tagName === 'audio') && name === 'autoplay'; + +function _shouldSendDefaultPii() { + const client = core.getClient(); + return client ? Boolean(client.getOptions().sendDefaultPii) : false; } -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); + +exports.HttpClient = HttpClient; +exports.httpClientIntegration = httpClientIntegration; + + +},{"./debug-build.js":121,"@sentry/core":70,"@sentry/utils":152}],126:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const captureconsole = require('./captureconsole.js'); +const debug = require('./debug.js'); +const dedupe = require('./dedupe.js'); +const extraerrordata = require('./extraerrordata.js'); +const offline = require('./offline.js'); +const reportingobserver = require('./reportingobserver.js'); +const rewriteframes = require('./rewriteframes.js'); +const sessiontiming = require('./sessiontiming.js'); +const transaction = require('./transaction.js'); +const httpclient = require('./httpclient.js'); +const contextlines = require('./contextlines.js'); + + + +exports.CaptureConsole = captureconsole.CaptureConsole; +exports.captureConsoleIntegration = captureconsole.captureConsoleIntegration; +exports.Debug = debug.Debug; +exports.debugIntegration = debug.debugIntegration; +exports.Dedupe = dedupe.Dedupe; +exports.dedupeIntegration = dedupe.dedupeIntegration; +exports.ExtraErrorData = extraerrordata.ExtraErrorData; +exports.extraErrorDataIntegration = extraerrordata.extraErrorDataIntegration; +exports.Offline = offline.Offline; +exports.ReportingObserver = reportingobserver.ReportingObserver; +exports.reportingObserverIntegration = reportingobserver.reportingObserverIntegration; +exports.RewriteFrames = rewriteframes.RewriteFrames; +exports.rewriteFramesIntegration = rewriteframes.rewriteFramesIntegration; +exports.SessionTiming = sessiontiming.SessionTiming; +exports.sessionTimingIntegration = sessiontiming.sessionTimingIntegration; +exports.Transaction = transaction.Transaction; +exports.HttpClient = httpclient.HttpClient; +exports.httpClientIntegration = httpclient.httpClientIntegration; +exports.ContextLines = contextlines.ContextLines; +exports.contextLinesIntegration = contextlines.contextLinesIntegration; + + +},{"./captureconsole.js":119,"./contextlines.js":120,"./debug.js":122,"./dedupe.js":123,"./extraerrordata.js":124,"./httpclient.js":125,"./offline.js":127,"./reportingobserver.js":128,"./rewriteframes.js":129,"./sessiontiming.js":130,"./transaction.js":131}],127:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const utils = require('@sentry/utils'); +const localForage = require('localforage'); +const debugBuild = require('./debug-build.js'); + +const WINDOW = utils.GLOBAL_OBJ ; + +/** + * cache offline errors and send when connected + * @deprecated The offline integration has been deprecated in favor of the offline transport wrapper. + * + * http://docs.sentry.io/platforms/javascript/configuration/transports/#offline-caching + */ +class Offline { + /** + * @inheritDoc + */ + static __initStatic() {this.id = 'Offline';} + + /** + * @inheritDoc + */ + + /** + * the current hub instance + */ + + /** + * maximum number of events to store while offline + */ + + /** + * event cache + */ + + /** + * @inheritDoc + */ + constructor(options = {}) { + this.name = Offline.id; + + this.maxStoredEvents = options.maxStoredEvents || 30; // set a reasonable default + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + this.offlineEventStore = localForage.createInstance({ + name: 'sentry/offlineEventStore', + }); + } + + /** + * @inheritDoc + */ + setupOnce(addGlobalEventProcessor, getCurrentHub) { + this.hub = getCurrentHub(); + + if ('addEventListener' in WINDOW) { + WINDOW.addEventListener('online', () => { + void this._sendEvents().catch(() => { + debugBuild.DEBUG_BUILD && utils.logger.warn('could not send cached events'); + }); + }); + } + + const eventProcessor = event => { + // eslint-disable-next-line deprecation/deprecation + if (this.hub && this.hub.getIntegration(Offline)) { + // cache if we are positively offline + if ('navigator' in WINDOW && 'onLine' in WINDOW.navigator && !WINDOW.navigator.onLine) { + debugBuild.DEBUG_BUILD && utils.logger.log('Event dropped due to being a offline - caching instead'); + + void this._cacheEvent(event) + .then((_event) => this._enforceMaxEvents()) + .catch((_error) => { + debugBuild.DEBUG_BUILD && utils.logger.warn('could not cache event while offline'); + }); + + // return null on success or failure, because being offline will still result in an error + return null; } + } + + return event; + }; + + eventProcessor.id = this.name; + addGlobalEventProcessor(eventProcessor); + + // if online now, send any events stored in a previous offline session + if ('navigator' in WINDOW && 'onLine' in WINDOW.navigator && WINDOW.navigator.onLine) { + void this._sendEvents().catch(() => { + debugBuild.DEBUG_BUILD && utils.logger.warn('could not send cached events'); + }); } - catch (e) { + } + + /** + * cache an event to send later + * @param event an event + */ + async _cacheEvent(event) { + return this.offlineEventStore.setItem(utils.uuid4(), utils.normalize(event)); + } + + /** + * purge excess events if necessary + */ + async _enforceMaxEvents() { + const events = []; + + return this.offlineEventStore + .iterate((event, cacheKey, _index) => { + // aggregate events + events.push({ cacheKey, event }); + }) + .then( + () => + // this promise resolves when the iteration is finished + this._purgeEvents( + // purge all events past maxStoredEvents in reverse chronological order + events + .sort((a, b) => (b.event.timestamp || 0) - (a.event.timestamp || 0)) + .slice(this.maxStoredEvents < events.length ? this.maxStoredEvents : events.length) + .map(event => event.cacheKey), + ), + ) + .catch((_error) => { + debugBuild.DEBUG_BUILD && utils.logger.warn('could not enforce max events'); + }); + } + + /** + * purge event from cache + */ + async _purgeEvent(cacheKey) { + return this.offlineEventStore.removeItem(cacheKey); + } + + /** + * purge events from cache + */ + async _purgeEvents(cacheKeys) { + // trail with .then to ensure the return type as void and not void|void[] + return Promise.all(cacheKeys.map(cacheKey => this._purgeEvent(cacheKey))).then(); + } + + /** + * send all events + */ + async _sendEvents() { + return this.offlineEventStore.iterate((event, cacheKey, _index) => { + if (this.hub) { + this.hub.captureEvent(event); + + void this._purgeEvent(cacheKey).catch((_error) => { + debugBuild.DEBUG_BUILD && utils.logger.warn('could not purge event from cache'); + }); + } else { + debugBuild.DEBUG_BUILD && utils.logger.warn('no hub found - could not send cached event'); + } + }); + } +} Offline.__initStatic(); + +exports.Offline = Offline; + + +},{"./debug-build.js":121,"@sentry/utils":152,"localforage":188}],128:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const core = require('@sentry/core'); +const utils = require('@sentry/utils'); + +const WINDOW = utils.GLOBAL_OBJ ; + +const INTEGRATION_NAME = 'ReportingObserver'; + +const SETUP_CLIENTS = new WeakMap(); + +const _reportingObserverIntegration = ((options = {}) => { + const types = options.types || ['crash', 'deprecation', 'intervention']; + + /** Handler for the reporting observer. */ + function handler(reports) { + if (!SETUP_CLIENTS.has(core.getClient() )) { + return; } - return false; -} -function elementClassMatchesRegex(el, regex) { - for (let eIndex = el.classList.length; eIndex--;) { - const className = el.classList[eIndex]; - if (regex.test(className)) { - return true; + + for (const report of reports) { + core.withScope(scope => { + scope.setExtra('url', report.url); + + const label = `ReportingObserver [${report.type}]`; + let details = 'No details available'; + + if (report.body) { + // Object.keys doesn't work on ReportBody, as all properties are inheirted + const plainBody + + = {}; + + // eslint-disable-next-line guard-for-in + for (const prop in report.body) { + plainBody[prop] = report.body[prop]; + } + + scope.setExtra('body', plainBody); + + if (report.type === 'crash') { + const body = report.body ; + // A fancy way to create a message out of crashId OR reason OR both OR fallback + details = [body.crashId || '', body.reason || ''].join(' ').trim() || details; + } else { + const body = report.body ; + details = body.message || details; + } } + + core.captureMessage(`${label}: ${details}`); + }); } - return false; + } + + return { + name: INTEGRATION_NAME, + setupOnce() { + if (!utils.supportsReportingObserver()) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + const observer = new (WINDOW ).ReportingObserver(handler, { + buffered: true, + types, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + observer.observe(); + }, + + setup(client) { + SETUP_CLIENTS.set(client, true); + }, + }; +}) ; + +const reportingObserverIntegration = core.defineIntegration(_reportingObserverIntegration); + +/** + * Reporting API integration - https://w3c.github.io/reporting/ + * @deprecated Use `reportingObserverIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +const ReportingObserver = core.convertIntegrationFnToClass( + INTEGRATION_NAME, + reportingObserverIntegration, +) + +; + +exports.ReportingObserver = ReportingObserver; +exports.reportingObserverIntegration = reportingObserverIntegration; + + +},{"@sentry/core":70,"@sentry/utils":152}],129:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const core = require('@sentry/core'); +const utils = require('@sentry/utils'); + +const INTEGRATION_NAME = 'RewriteFrames'; + +const _rewriteFramesIntegration = ((options = {}) => { + const root = options.root; + const prefix = options.prefix || 'app:///'; + + const iteratee = + options.iteratee || + ((frame) => { + if (!frame.filename) { + return frame; + } + // Determine if this is a Windows frame by checking for a Windows-style prefix such as `C:\` + const isWindowsFrame = + /^[a-zA-Z]:\\/.test(frame.filename) || + // or the presence of a backslash without a forward slash (which are not allowed on Windows) + (frame.filename.includes('\\') && !frame.filename.includes('/')); + // Check if the frame filename begins with `/` + const startsWithSlash = /^\//.test(frame.filename); + if (isWindowsFrame || startsWithSlash) { + const filename = isWindowsFrame + ? frame.filename + .replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix + .replace(/\\/g, '/') // replace all `\\` instances with `/` + : frame.filename; + const base = root ? utils.relative(root, filename) : utils.basename(filename); + frame.filename = `${prefix}${base}`; + } + return frame; + }); + + /** Process an exception event. */ + function _processExceptionsEvent(event) { + try { + return { + ...event, + exception: { + ...event.exception, + // The check for this is performed inside `process` call itself, safe to skip here + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + values: event.exception.values.map(value => ({ + ...value, + ...(value.stacktrace && { stacktrace: _processStacktrace(value.stacktrace) }), + })), + }, + }; + } catch (_oO) { + return event; + } + } + + /** Process a stack trace. */ + function _processStacktrace(stacktrace) { + return { + ...stacktrace, + frames: stacktrace && stacktrace.frames && stacktrace.frames.map(f => iteratee(f)), + }; + } + + return { + name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function + processEvent(originalEvent) { + let processedEvent = originalEvent; + + if (originalEvent.exception && Array.isArray(originalEvent.exception.values)) { + processedEvent = _processExceptionsEvent(processedEvent); + } + + return processedEvent; + }, + }; +}) ; + +const rewriteFramesIntegration = core.defineIntegration(_rewriteFramesIntegration); + +/** + * Rewrite event frames paths. + * @deprecated Use `rewriteFramesIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +const RewriteFrames = core.convertIntegrationFnToClass( + INTEGRATION_NAME, + rewriteFramesIntegration, +) + +; + +exports.RewriteFrames = RewriteFrames; +exports.rewriteFramesIntegration = rewriteFramesIntegration; + + +},{"@sentry/core":70,"@sentry/utils":152}],130:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const core = require('@sentry/core'); + +const INTEGRATION_NAME = 'SessionTiming'; + +const _sessionTimingIntegration = (() => { + const startTime = Date.now(); + + return { + name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function + processEvent(event) { + const now = Date.now(); + + return { + ...event, + extra: { + ...event.extra, + ['session:start']: startTime, + ['session:duration']: now - startTime, + ['session:end']: now, + }, + }; + }, + }; +}) ; + +const sessionTimingIntegration = core.defineIntegration(_sessionTimingIntegration); + +/** + * This function adds duration since Sentry was initialized till the time event was sent. + * @deprecated Use `sessionTimingIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +const SessionTiming = core.convertIntegrationFnToClass( + INTEGRATION_NAME, + sessionTimingIntegration, +) ; + +exports.SessionTiming = SessionTiming; +exports.sessionTimingIntegration = sessionTimingIntegration; + + +},{"@sentry/core":70}],131:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const core = require('@sentry/core'); + +const INTEGRATION_NAME = 'Transaction'; + +const transactionIntegration = (() => { + return { + name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function + processEvent(event) { + const frames = _getFramesFromEvent(event); + + // use for loop so we don't have to reverse whole frames array + for (let i = frames.length - 1; i >= 0; i--) { + const frame = frames[i]; + + if (frame.in_app === true) { + event.transaction = _getTransaction(frame); + break; + } + } + + return event; + }, + }; +}) ; + +/** + * Add node transaction to the event. + * @deprecated This integration will be removed in v8. + */ +// eslint-disable-next-line deprecation/deprecation +const Transaction = core.convertIntegrationFnToClass(INTEGRATION_NAME, transactionIntegration) + +; + +function _getFramesFromEvent(event) { + const exception = event.exception && event.exception.values && event.exception.values[0]; + return (exception && exception.stacktrace && exception.stacktrace.frames) || []; } -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 _getTransaction(frame) { + return frame.module || frame.function ? `${frame.module || '?'}/${frame.function || '?'}` : ''; } -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; - } - }; + +exports.Transaction = Transaction; + + +},{"@sentry/core":70}],132:[function(require,module,exports){ +var { + _nullishCoalesce, + _optionalChain +} = require('@sentry/utils'); + +Object.defineProperty(exports, '__esModule', { value: true }); + +const core = require('@sentry/core'); +const utils = require('@sentry/utils'); +const tracing = require('@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 = utils.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 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 isShadowRoot(n) { + const host = _optionalChain$5([n, 'optionalAccess', _ => _.host]); + return Boolean(_optionalChain$5([host, 'optionalAccess', _2 => _2.shadowRoot]) === n); } -function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) { - const win = iframeEl.contentWindow; - if (!win) { - return; +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;'); } - let fired = false; - let readyState; - try { - readyState = win.document.readyState; + 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`); } - catch (error) { - return; + else if (rule.layerName) { + statement.push(`layer(${rule.layerName})`); } - if (readyState !== 'complete') { - const timer = setTimeout(() => { - if (!fired) { - listener(); - fired = true; - } - }, iframeLoadTimeout); - iframeEl.addEventListener('load', () => { - clearTimeout(timer); - fired = true; - listener(); - }); - return; + if (rule.supportsText) { + statement.push(`supports(${rule.supportsText})`); } - const blankUrl = 'about:blank'; - if (win.location.href !== blankUrl || - iframeEl.src === blankUrl || - iframeEl.src === '') { - setTimeout(listener, 0); - return iframeEl.addEventListener('load', listener); + if (rule.media.length) { + statement.push(rule.media.mediaText); } - iframeEl.addEventListener('load', listener); + return statement.join(' ') + ';'; } -function onceStylesheetLoaded(link, listener, styleSheetLoadTimeout) { - let fired = false; - let styleSheetLoaded; +function stringifyStylesheet(s) { try { - styleSheetLoaded = link.sheet; + const rules = s.rules || s.cssRules; + return rules + ? fixBrowserCompatibilityIssuesInCSS(Array.from(rules, stringifyRule).join('')) + : null; } catch (error) { - return; + return null; } - if (styleSheetLoaded) - return; - const timer = setTimeout(() => { - if (!fired) { - listener(); - fired = true; +} +function stringifyRule(rule) { + let importStringified; + if (isCSSImportRule(rule)) { + try { + importStringified = + stringifyStylesheet(rule.styleSheet) || + escapeImportStatement(rule); } - }, styleSheetLoadTimeout); - link.addEventListener('load', () => { - clearTimeout(timer); - fired = true; - listener(); - }); + catch (error) { + } + } + else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) { + return fixSafariColons(rule.cssText); + } + return importStringified || rule.cssText; } -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 fixSafariColons(cssStringified) { + const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm; + return cssStringified.replace(regex, '$1\\$2'); } -function getRootId(doc, mirror) { - if (!mirror.hasNode(doc)) - return undefined; - const docId = mirror.getId(doc); - return docId === 1 ? undefined : docId; +function isCSSImportRule(rule) { + return 'styleSheet' in rule; } -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'; +function isCSSStyleRule(rule) { + return 'selectorText' in rule; +} +class Mirror { + constructor() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); } - const forceMask = needMaskingText(n, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, maskAllText); - if (!isStyle && !isScript && !isTextarea && textContent && forceMask) { - textContent = maskTextFn - ? maskTextFn(textContent) - : textContent.replace(/[\S]/g, '*'); + 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)); } - if (isTextarea && textContent && (maskInputOptions.textarea || forceMask)) { - textContent = maskInputFn - ? maskInputFn(textContent, n.parentNode) - : textContent.replace(/[\S]/g, '*'); + getNode(id) { + return this.idNodeMap.get(id) || null; } - 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, - }); + getIds() { + return Array.from(this.idNodeMap.keys()); } - 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); - } + getMeta(n) { + return this.nodeMetaMap.get(n) || null; } - 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); + removeNodeFromMap(n) { + const id = this.getId(n); + this.idNodeMap.delete(id); + if (n.childNodes) { + n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); } } - if (tagName === 'style' && - n.sheet && - !(n.innerText || n.textContent || '').trim().length) { - const cssText = stringifyStylesheet(n.sheet); - if (cssText) { - attributes._cssText = absoluteToStylesheet(cssText, getHref()); - } + has(id) { + return this.idNodeMap.has(id); } - 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; - } + hasNode(node) { + return this.nodeMetaMap.has(node); } - if (tagName === 'option') { - if (n.selected && !maskInputOptions['select']) { - attributes.selected = true; - } - else { - delete attributes.selected; - } + add(n, meta) { + const id = meta.id; + this.idNodeMap.set(id, n); + this.nodeMetaMap.set(n, meta); } - 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; - } + 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); } - 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); + reset() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); } - if (tagName === 'audio' || tagName === 'video') { - attributes.rr_mediaState = n.paused - ? 'paused' - : 'played'; - attributes.rr_mediaCurrentTime = n.currentTime; +} +function createMirror() { + return new Mirror(); +} +function shouldMaskInput({ maskInputOptions, tagName, type, }) { + if (tagName === 'OPTION') { + tagName = 'SELECT'; } - if (!newlyAddedElement) { - if (n.scrollLeft) { - attributes.rr_scrollLeft = n.scrollLeft; - } - if (n.scrollTop) { - attributes.rr_scrollTop = n.scrollTop; - } + 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 (needBlock) { - const { width, height } = n.getBoundingClientRect(); - attributes = { - class: attributes.class, - rr_width: `${width}px`, - rr_height: `${height}px`, - }; + if (maskInputFn) { + text = maskInputFn(text, element); } - if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src)) { - if (!n.contentDocument) { - attributes.rr_src = attributes.src; + 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; } - delete attributes.src; } - let isCustomElement; - try { - if (customElements.get(tagName)) - isCustomElement = true; + 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') || ''; } - catch (e) { + 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'; } - return { - type: NodeType$1.Element, - tagName, - attributes, - childNodes: [], - isSVG: isSVGElement(n) || undefined, - needBlock, - rootId, - isCustom: isCustomElement, - }; + const processedTagName = toLowerCase(element.tagName); + if (tagNameRegex.test(processedTagName)) { + return 'div'; + } + return processedTagName; } -function lowerIfExists(maybeAttr) { - if (maybeAttr === undefined || maybeAttr === null) { - return ''; +function extractOrigin(url) { + let origin = ''; + if (url.indexOf('//') > -1) { + origin = url.split('/').slice(0, 3).join('/'); } else { - return maybeAttr.toLowerCase(); + origin = url.split('/')[0]; } + origin = origin.split('?')[0]; + return origin; } -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; +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; } - 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; + if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; } - 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; + 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 (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 (part === '..') { + stack.pop(); } - 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; + else { + stack.push(part); } } - } - 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, + return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; }); - if (!_serializedNode) { - console.warn(n, 'not serialized'); - return null; +} +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 id; - if (mirror.hasNode(n)) { - id = mirror.getId(n); + 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 ''; } - else if (slimDOMExcluded(_serializedNode, slimDOMOptions) || - (!preserveWhiteSpace && - _serializedNode.type === NodeType$1.Text && - !_serializedNode.isStyle && - !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)) { - id = IGNORED_NODE; + 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; + } + } } - else { - id = genId(); + return output.join(', '); +} +function absoluteToDoc(doc, attributeValue) { + if (!attributeValue || attributeValue.trim() === '') { + return attributeValue; } - const serializedNode = Object.assign(_serializedNode, { id }); - mirror.add(n, serializedNode); - if (id === IGNORED_NODE) { - return null; + 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 (onSerialize) { - onSerialize(n); + if (name === 'src' || + (name === 'href' && !(tagName === 'use' && value[0] === '#'))) { + return absoluteToDoc(doc, value); } - 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; + else if (name === 'xlink:href' && value[0] !== '#') { + return absoluteToDoc(doc, value); } - 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; + 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; } - 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 (typeof blockClass === 'string') { + if (element.classList.contains(blockClass)) { + return true; } } - 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); + 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); + } } - 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); + catch (e) { } - return serializedNode; + return false; } -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, +function elementClassMatchesRegex(el, regex) { + for (let eIndex = el.classList.length; eIndex--;) { + const className = el.classList[eIndex]; + if (regex.test(className)) { + return 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); + } + return false; } -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); +function distanceToMatch(node, matchPredicate, limit = Infinity, distance = 0) { + if (!node) 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); - }, - }); + 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 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; +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; + } } - previous = now; - func.apply(context, args); + if (selector && el.matches(selector)) + return true; + return false; } - else if (!timeout && options.trailing !== false) { - timeout = setTimeout(() => { - previous = options.leading === false ? 0 : Date.now(); - timeout = null; - func.apply(context, args); - }, remaining); + catch (e2) { + return false; } }; } -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) { +function needMaskingText(node, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, maskAllText) { try { - if (!(name in source)) { - return () => { - }; + 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; + } } - 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, - }, - }); + 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); } - source[name] = wrapped; - return () => { - source[name] = original; - }; + 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 (e2) { - return () => { - }; + catch (e) { } + return !!maskAllText; } -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; +function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) { + const win = iframeEl.contentWindow; + if (!win) { + return; } - 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; + let fired = false; + let readyState; + try { + readyState = win.document.readyState; } - const blockDistance = distanceToMatch(el, blockedPredicate); - let unblockDistance = -1; - if (blockDistance < 0) { - return false; + catch (error) { + return; } - if (unblockSelector) { - unblockDistance = distanceToMatch(el, createMatchPredicate(null, unblockSelector)); + if (readyState !== 'complete') { + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, iframeLoadTimeout); + iframeEl.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + return; } - if (blockDistance > -1 && unblockDistance < 0) { - return true; + const blankUrl = 'about:blank'; + if (win.location.href !== blankUrl || + iframeEl.src === blankUrl || + iframeEl.src === '') { + setTimeout(listener, 0); + return iframeEl.addEventListener('load', listener); } - return blockDistance < unblockDistance; -} -function isSerialized(n, mirror) { - return mirror.getId(n) !== -1; -} -function isIgnored(n, mirror) { - return mirror.getId(n) === IGNORED_NODE; + iframeEl.addEventListener('load', listener); } -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; +function onceStylesheetLoaded(link, listener, styleSheetLoadTimeout) { + let fired = false; + let styleSheetLoaded; + try { + styleSheetLoaded = link.sheet; } - if (!target.parentNode) { - return true; + catch (error) { + return; } - return isAncestorRemoved(target.parentNode, mirror); -} -function legacy_isTouchEvent(event) { - return Boolean(event.changedTouches); + if (styleSheetLoaded) + return; + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, styleSheetLoadTimeout); + link.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); } -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'); +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, + }; } - do { - if (this === node) { - return true; - } - } while ((node = node && node.parentNode)); + 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 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])); +function getRootId(doc, mirror) { + if (!mirror.hasNode(doc)) + return undefined; + const docId = mirror.getId(doc); + return docId === 1 ? undefined : docId; } -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++; +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); + } } - else - newId = id; - this.styleIDMap.set(stylesheet, newId); - this.idStyleMap.set(newId, stylesheet); - return newId; + catch (err) { + console.warn(`Cannot get CSS styles from text's parentNode. Error: ${err}`, n); + } + textContent = absoluteToStylesheet(textContent, getHref()); } - getStyle(id) { - return this.idStyleMap.get(id) || null; + if (isScript) { + textContent = 'SCRIPT_PLACEHOLDER'; } - reset() { - this.styleIDMap = new WeakMap(); - this.idStyleMap = new Map(); - this.id = 1; + const forceMask = needMaskingText(n, maskTextClass, maskTextSelector, unmaskTextClass, unmaskTextSelector, maskAllText); + if (!isStyle && !isScript && !isTextarea && textContent && forceMask) { + textContent = maskTextFn + ? maskTextFn(textContent) + : textContent.replace(/[\S]/g, '*'); } - generateId() { - return this.id++; + 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 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; +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); + } } - 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); + 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); } - catch (e) { + if (cssText) { + delete attributes.rel; + delete attributes.href; + attributes._cssText = absoluteToStylesheet(cssText, stylesheet.href); } } - 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; + if (tagName === 'style' && + n.sheet && + !(n.innerText || n.textContent || '').trim().length) { + const cssText = stringifyStylesheet(n.sheet); + if (cssText) { + attributes._cssText = absoluteToStylesheet(cssText, getHref()); + } } - get(position) { - if (position >= this.length) { - throw new Error('Position outside of list range'); + 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, + }); } - let current = this.head; - for (let index = 0; index < position; index++) { - current = _optionalChain$3([current, 'optionalAccess', _ => _.next]) || null; + if (checked) { + attributes.checked = checked; } - 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; - } + if (tagName === 'option') { + if (n.selected && !maskInputOptions['select']) { + attributes.selected = true; } else { - if (this.head) { - this.head.previous = node; - } - node.next = this.head; - this.head = node; - } - if (node.next === null) { - this.tail = node; + delete attributes.selected; } - 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; + if (tagName === 'canvas' && recordCanvas) { + if (n.__context === '2d') { + if (!is2DCanvasBlank(n)) { + attributes.rr_dataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); } - else { - this.tail = null; + } + 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; } } - else { - current.previous.next = current.next; - if (current.next) { - current.next.previous = current.previous; + } + 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); } - else { - this.tail = current.previous; + 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.__ln) { - delete n.__ln; + if (n.scrollTop) { + attributes.rr_scrollTop = n.scrollTop; } - 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(); + if (needBlock) { + const { width, height } = n.getBoundingClientRect(); + attributes = { + class: attributes.class, + rr_width: `${width}px`, + rr_height: `${height}px`, }; - this.emit = () => { - if (this.frozen || this.locked) { - return; + } + 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; } - 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()); + 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; } - for (const n of this.movedSet) { - if (isParentRemoved(this.removes, n, this.mirror) && - !this.movedSet.has(n.parentNode)) { - continue; - } - pushAdd(n); + else if (slimDOMOptions.headMetaRobots && + (lowerIfExists(sn.attributes.name) === 'robots' || + lowerIfExists(sn.attributes.name) === 'googlebot' || + lowerIfExists(sn.attributes.name) === 'bingbot')) { + return true; } - 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); - } + else if (slimDOMOptions.headMetaHttpEquiv && + sn.attributes['http-equiv'] !== undefined) { + return true; } - 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); + 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; } - 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; + 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; } - this.texts = []; - this.attributes = []; - this.removes = []; - this.addedSet = new Set(); - this.movedSet = new Set(); - this.droppedSet = new Set(); - this.movedMap = {}; - this.mutationCb(payload); + } + } + 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, }; - this.processMutation = (m) => { - if (isIgnored(m.target, this.mirror)) { - return; - } - let unattachedDoc; - try { - unattachedDoc = document.implementation.createHTMLDocument(); + for (const childN of Array.from(n.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + serializedNode.childNodes.push(serializedChildNode); } - catch (e) { - unattachedDoc = this.doc; + } + 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); + } } - 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; + } + } + 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); } } - 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); - }); + }, 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); } } - }; - } - 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; + }, stylesheetLoadTimeout); } - return _isParentRemoved(removes, parentNode, mirror); -} -function isAncestorInSet(set, n) { - if (set.size === 0) - return false; - return _isAncestorInSet(set, n); + return serializedNode; } -function _isAncestorInSet(set, n) { - const { parentNode } = n; - if (!parentNode) { - return false; - } - if (set.has(parentNode)) { - return true; - } - return _isAncestorInSet(set, parentNode); +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, + }); } -let errorHandler; -function registerErrorHandler(handler) { - errorHandler = handler; -} -function unregisterErrorHandler() { - errorHandler = undefined; +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 callbackWrapper = (cb) => { - if (!errorHandler) { - return cb; - } - const rrwebWrapped = ((...rest) => { - try { - return cb(...rest); - } - catch (error) { - if (errorHandler && errorHandler(error) === true) { - return () => { - }; +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); } - throw error; - } + return Reflect.get(target, prop, receiver); + }, }); - 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]; +} +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 ('path' in event && event.path.length) { - return event.path[0]; + else if (!timeout && options.trailing !== false) { + timeout = setTimeout(() => { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + func.apply(context, args); + }, remaining); } - } - 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 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 initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) { - if (sampling.mousemove === false) { +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; }; } - 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) { + catch (e2) { 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 }), - }); - }; +} +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, }; - 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 getWindowHeight() { + return (window.innerHeight || + (document.documentElement && document.documentElement.clientHeight) || + (document.body && document.body.clientHeight)); } -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); +function getWindowWidth() { + return (window.innerWidth || + (document.documentElement && document.documentElement.clientWidth) || + (document.body && document.body.clientWidth)); } -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 isBlocked(node, blockClass, blockSelector, unblockSelector, checkAncestors) { + if (!node) { + return false; } - 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 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 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 blockDistance = distanceToMatch(el, blockedPredicate); + let unblockDistance = -1; + if (blockDistance < 0) { + return false; } - 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))); + if (unblockSelector) { + unblockDistance = distanceToMatch(el, createMatchPredicate(null, unblockSelector)); } - return callbackWrapper(() => { - handlers.forEach((h) => h()); - }); + if (blockDistance > -1 && unblockDistance < 0) { + return true; + } + return blockDistance < unblockDistance; } -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; +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; } - return recurse(rule, positions); + 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 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 legacy_isTouchEvent(event) { + return Boolean(event.changedTouches); } -function initStyleSheetObserver({ styleSheetRuleCb, mirror, stylesheetManager }, { win }) { - if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) { - return () => { - }; +function polyfill(win = window) { + if ('NodeList' in win && !win.NodeList.prototype.forEach) { + win.NodeList.prototype.forEach = Array.prototype + .forEach; } - 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 }], - }); + 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'); } - 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, - }); + do { + if (this === node) { + return true; } - return target.apply(thisArg, argumentsList); - }), - }); + } while ((node = node && node.parentNode)); + return false; + }; } - 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 isSerializedIframe(n, mirror) { + return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n)); } -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 isSerializedStylesheet(n, mirror) { + return Boolean(n.nodeName === 'LINK' && + n.nodeType === n.ELEMENT_NODE && + n.getAttribute && + n.getAttribute('rel') === 'stylesheet' && + mirror.getMeta(n)); } -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 hasShadowRoot(n) { + return Boolean(_optionalChain$4([n, 'optionalAccess', _18 => _18.shadowRoot])); } -function initObservers(o, _hooks = {}) { - const currentWindow = o.doc.defaultView; - if (!currentWindow) { - return () => { - }; +class StyleSheetMirror { + constructor() { + this.id = 1; + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); } - 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)); + getId(stylesheet) { + return _nullishCoalesce(this.styleIDMap.get(stylesheet), () => ( -1)); } - 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(); + has(stylesheet) { + return this.styleIDMap.has(stylesheet); } - 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); + add(stylesheet, id) { + if (this.has(stylesheet)) + return this.getId(stylesheet); + let newId; + if (id === undefined) { + newId = this.id++; } - return id; + else + newId = id; + this.styleIDMap.set(stylesheet, newId); + this.idStyleMap.set(newId, stylesheet); + return newId; } - getIds(iframe, remoteId) { - const idToRemoteIdMap = this.getIdToRemoteIdMap(iframe); - const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); - return remoteId.map((id) => this.getId(iframe, id, idToRemoteIdMap, remoteIdToIdMap)); + getStyle(id) { + return this.idStyleMap.get(id) || null; } - 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; + reset() { + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + this.id = 1; } - getRemoteIds(iframe, ids) { - const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); - return ids.map((id) => this.getRemoteId(iframe, id, remoteIdToIdMap)); + generateId() { + return this.id++; } - reset(iframe) { - if (!iframe) { - this.iframeIdToRemoteIdMap = new WeakMap(); - this.iframeRemoteIdToIdMap = new WeakMap(); - return; - } - this.iframeIdToRemoteIdMap.delete(iframe); - this.iframeRemoteIdToIdMap.delete(iframe); +} +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; } - getIdToRemoteIdMap(iframe) { - let idToRemoteIdMap = this.iframeIdToRemoteIdMap.get(iframe); - if (!idToRemoteIdMap) { - idToRemoteIdMap = new Map(); - this.iframeIdToRemoteIdMap.set(iframe, idToRemoteIdMap); + 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); } - return idToRemoteIdMap; - } - getRemoteIdToIdMap(iframe) { - let remoteIdToIdMap = this.iframeRemoteIdToIdMap.get(iframe); - if (!remoteIdToIdMap) { - remoteIdToIdMap = new Map(); - this.iframeRemoteIdToIdMap.set(iframe, remoteIdToIdMap); + catch (e) { } - return remoteIdToIdMap; } + return (cachedRequestAnimationFrameImplementation = + requestAnimationFrameImplementation.bind(window)); } - -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() { - } +function onRequestAnimationFrame(...rest) { + return getRequestAnimationFrameImplementation()(...rest); } -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)); + +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; } - 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); + 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; } - 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; + 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; } - case EventType.Custom: { - this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']); - return e; + } + 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; } - 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; - } - } + } + else { + if (this.head) { + this.head.previous = node; } + node.next = this.head; + this.head = node; } - return false; + if (node.next === null) { + this.tail = node; + } + this.length++; } - 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]); + 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 { - obj[key] = iframeMirror.getId(iframeEl, obj[key]); + this.tail = null; } } - 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); - }); + else { + current.previous.next = current.next; + if (current.next) { + current.next.previous = current.previous; + } + else { + this.tail = current.previous; + } } - } - 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); - }); + if (n.__ln) { + delete n.__ln; } + this.length--; } } - -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; +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; }; - })); - } - reset() { - this.restoreHandlers.forEach((handler) => { - try { - handler(); + 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()); } - catch (e) { + for (const n of this.movedSet) { + if (isParentRemoved(this.removes, n, this.mirror) && + !this.movedSet.has(n.parentNode)) { + continue; + } + pushAdd(n); } - }); - 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: [], + 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); }; - 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, - })), - }); + this.processMutation = (m) => { + if (isIgnored(m.target, this.mirror)) { + return; } - else - styleId = this.styleMirror.getId(sheet); - adoptedStyleSheetData.styleIds.push(styleId); - } - if (styles.length > 0) - adoptedStyleSheetData.styles = styles; - this.adoptedStyleSheetCb(adoptedStyleSheetData); + 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); + }); + } + } + }; } - reset() { - this.styleMirror.reset(); - this.trackedLinkElements = new WeakSet(); + 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]; + }); } - trackStylesheetInLinkElement(linkEl) { + freeze() { + this.frozen = true; + this.canvasManager.freeze(); } -} - -class ProcessedNodeManager { - constructor() { - this.nodeMap = new WeakMap(); - this.loop = true; - this.periodicallyClear(); + unfreeze() { + this.frozen = false; + this.canvasManager.unfreeze(); + this.emit(); } - periodicallyClear() { - onRequestAnimationFrame(() => { - this.clear(); - if (this.loop) - this.periodicallyClear(); - }); + isFrozen() { + return this.frozen; } - inOtherBuffer(node, thisBuffer) { - const buffers = this.nodeMap.get(node); - return (buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer)); + lock() { + this.locked = true; + this.canvasManager.lock(); } - add(node, buffer) { - this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer)); + unlock() { + this.locked = false; + this.canvasManager.unlock(); + this.emit(); } - clear() { - this.nodeMap = new WeakMap(); + reset() { + this.shadowDomManager.reset(); + this.canvasManager.reset(); } - destroy() { - this.loop = false; +} +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); } -function wrapEvent(e) { - const eWithTime = e; - eWithTime.timestamp = nowTimestamp(); - return eWithTime; +let errorHandler; +function registerErrorHandler(handler) { + errorHandler = handler; } -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) { +function unregisterErrorHandler() { + errorHandler = undefined; +} +const callbackWrapper = (cb) => { + if (!errorHandler) { + return cb; + } + const rrwebWrapped = ((...rest) => { try { - if (window.parent.document) { - passEmitsToParent = false; + return cb(...rest); + } + catch (error) { + if (errorHandler && errorHandler(error) === true) { + return () => { + }; } + throw error; } - catch (e) { - passEmitsToParent = true; + }); + 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]; } } - if (inEmittingFrame && !emit) { - throw new Error('emit function is required'); + catch (e2) { } - if (mousemoveWait !== undefined && sampling.mousemove === undefined) { - sampling.mousemove = mousemoveWait; + 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]; } - 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, + const observer = new mutationObserverCtor(callbackWrapper((mutations) => { + if (options.onMutation && options.onMutation(mutations) === false) { + return; } - : _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', + 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(); } - : _slimDOMOptions - ? _slimDOMOptions - : {}; - polyfill(); - let lastFullSnapshotEvent; - let incrementalSnapshotCount = 0; - const eventProcessor = (e) => { - for (const plugin of plugins || []) { - if (plugin.eventProcessor) { - e = plugin.eventProcessor(e); + 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; } } - if (packFn && - !passEmitsToParent) { - e = packFn(e); + 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; } - 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()); + const id = mirror.getId(target); + if (target === doc && doc.defaultView) { + const scrollLeftTop = getWindowScroll(doc.defaultView); + scrollCb({ + id, + x: scrollLeftTop.left, + y: scrollLeftTop.top, + }); } - if (inEmittingFrame) { - _optionalChain([emit, 'optionalCall', _4 => _4(eventProcessor(e), isCheckout)]); + else { + scrollCb({ + id, + x: target.scrollLeft, + y: target.scrollTop, + }); } - else if (passEmitsToParent) { - const message = { - type: 'rrweb', - event: eventProcessor(e), - origin: window.location.origin, - isCheckout, - }; - window.parent.postMessage(message, '*'); + }), 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; } - if (e.type === EventType.FullSnapshot) { - lastFullSnapshotEvent = e; - incrementalSnapshotCount = 0; + }), 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; } - 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 && - lastFullSnapshotEvent && - e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms; - if (exceedCount || exceedTime) { - takeFullSnapshot(true); - } + const el = target; + if (el.classList.contains(ignoreClass) || + (ignoreSelector && el.matches(ignoreSelector))) { + return; } - }; - 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, + const type = getInputType(target); + let text = getInputValue(el, tagName, type); + let isChecked = false; + const isInputMasked = shouldMaskInput({ + maskInputOptions, + tagName, + type, }); - for (const plugin of plugins || []) { - if (plugin.getMirror) - plugin.getMirror({ - nodeMirror: mirror, - crossOriginIframeMirror: iframeManager.crossOriginIframeMirror, - crossOriginIframeStyleMirror: iframeManager.crossOriginIframeStyleMirror, + 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 }); + } }); + } } - const processedNodeManager = new ProcessedNodeManager(); - const canvasManager = _getCanvasManager(getCanvasManager, { - mirror, - win: window, - mutationCb: (p) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.CanvasMutation, - ...p, + 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, + }); }, - })), - recordCanvas, - blockClass, - blockSelector, - unblockSelector, - sampling: sampling['canvas'], - dataURLOptions, - errorHandler, + }, false, currentWindow))); + } + return callbackWrapper(() => { + handlers.forEach((h) => h()); }); - 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'); +} +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); } - 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)); + 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, }; - _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, - }, - })), - }))]) || [], - }, {}); +} +function initStyleSheetObserver({ styleSheetRuleCb, mirror, stylesheetManager }, { win }) { + if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) { + return () => { }; - iframeManager.addLoadListener((iframeEl) => { - try { - handlers.push(observe(iframeEl.contentDocument)); + } + 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 }], + }); } - catch (error) { - console.warn(error); + 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); + }), }); - const init = () => { - takeFullSnapshot(); - handlers.push(observe(document)); - }; - if (document.readyState === 'interactive' || - document.readyState === 'complete') { - init(); + } + 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; } - 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)); + 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 () => { - handlers.forEach((h) => h()); - processedNodeManager.destroy(); - _takeFullSnapshot = undefined; - unregisterErrorHandler(); }; } - catch (error) { - console.warn(error); - } + 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, maxCanvasSize = null, 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 && + lastFullSnapshotEvent && + 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, + maxCanvasSize, + 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: utils.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() { + utils.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 = utils.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 = utils.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 ((utils.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(tracing.addPerformanceInstrumentationHandler(type, onEntries)); + }); + + clearCallbacks.push( + tracing.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; + } + + utils.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; + } + + utils.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) { + core.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 && utils.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 && utils.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 && utils.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 || utils.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 && utils.logger.error(error); + await replay.stop({ reason }); + + const client = core.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 && + utils.logger.error('[Replay] An error occured in the `beforeAddRecordingEvent` callback, skipping the event...', error); + return null; + } + + return event; } -function takeFullSnapshot(isCheckout) { - if (!_takeFullSnapshot) { - throw new Error('please take full snapshot after start recording'); - } - _takeFullSnapshot(isCheckout); + +/** If the event is an error event */ +function isErrorEvent(event) { + return !event.type; } -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(); - } + +/** If the event is a transaction event */ +function isTransactionEvent(event) { + return event.type === 'transaction'; } -const ReplayEventTypeIncrementalSnapshot = 3; -const ReplayEventTypeCustom = 5; +/** 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'; +} /** - * Converts a timestamp to ms, if it was in s, or keeps it as ms. + * Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`. */ -function timestampToMs(timestamp) { - const isMs = timestamp > 9999999999; - return isMs ? timestamp : timestamp * 1000; +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 = core.getClient(); + if (!client) { + return false; + } + + const transport = client.getTransport(); + if (!transport) { + return false; + } + + return ( + (transport.send ).__sentry__baseTransport__ || false + ); } /** - * Converts a timestamp to s, if it was in ms, or keeps it as s. + * Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`. */ -function timestampToS(timestamp) { - const isMs = timestamp > 9999999999; - return isMs ? timestamp / 1000 : timestamp; +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); + } } /** - * Add a breadcrumb event to replay. + * Returns true if we think the given event is an error originating inside of rrweb. */ -function addBreadcrumbEvent(replay, breadcrumb) { - if (breadcrumb.category === 'sentry.transaction') { - return; +function isRrwebError(event, hint) { + if (event.type || !event.exception || !event.exception.values || !event.exception.values.length) { + return false; } - if (['ui.click', 'ui.input'].includes(breadcrumb.category )) { - replay.triggerUserActivity(); - } else { - replay.checkAndHandleExpiredSession(); + // @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, - // 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, + timestamp: event.timestamp * 1000, data: { tag: 'breadcrumb', - // normalize to max. 10 depth and 1_000 properties per object - payload: utils.normalize(breadcrumb, 10, 1000), + payload: { + timestamp: event.timestamp, + type: 'default', + category: 'sentry.feedback', + data: { + feedbackId: event.event_id, + }, + }, }, - }); + } ); - // Do not flush after console log messages - return breadcrumb.category === 'console'; + return false; }); } -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 + * 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 getClickTargetNode(event) { - const target = getTargetNode(event); - - if (!target || !(target instanceof Element)) { - return target; +function shouldSampleForBufferEvent(replay, event) { + if (replay.recordingMode !== 'buffer') { + return false; } - return getClosestInteractive(target); -} - -/** Get the event target node. */ -function getTargetNode(event) { - if (isEventWithTarget(event)) { - return event.target ; + // 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; } - return event; -} + // Require the event to be an error event & to have an exception + if (!event.exception || event.type) { + return false; + } -function isEventWithTarget(event) { - return typeof event === 'object' && !!event && 'target' in event; + return isSampled(replay.getOptions().errorSampleRate); } -let handlers; - /** - * Register a handler to be called when `window.open()` is called. - * Returns a cleanup function. + * Returns a listener to be added to `addEventProcessor(listener)`. */ -function onWindowOpen(cb) { - // Ensure to only register this once - if (!handlers) { - handlers = []; - monkeyPatchWindowOpen(); - } +function handleGlobalEventListener( + replay, + includeAfterSendEventHandling = false, +) { + const afterSendHandler = includeAfterSendEventHandling ? handleAfterSendEvent(replay) : undefined; - handlers.push(cb); + return Object.assign( + (event, hint) => { + // Do nothing if replay has been disabled + if (!replay.isEnabled()) { + return event; + } - return () => { - const pos = handlers ? handlers.indexOf(cb) : -1; - if (pos > -1) { - (handlers ).splice(pos, 1); - } - }; -} + if (isReplayEvent(event)) { + // Replays have separate set of breadcrumbs, do not include breadcrumbs + // from core SDK + delete event.breadcrumbs; + return event; + } -function monkeyPatchWindowOpen() { - utils.fill(WINDOW, 'open', function (originalWindowOpen) { - return function (...args) { - if (handlers) { - try { - handlers.forEach(handler => handler()); - } catch (e) { - // ignore errors in here - } + // We only want to handle errors, transactions, and feedbacks, nothing else + if (!isErrorEvent(event) && !isTransactionEvent(event) && !isFeedbackEvent(event)) { + return event; } - return originalWindowOpen.apply(WINDOW, args); - }; - }); -} + // Ensure we do not add replay_id if the session is expired + const isSessionActive = replay.checkAndHandleExpiredSession(); + if (!isSessionActive) { + return event; + } -/** Handle a click. */ -function handleClick(clickDetector, clickBreadcrumb, node) { - clickDetector.handleClick(clickBreadcrumb, node); -} + 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; + } -/** A click detector class that can be used to detect slow or rage clicks on elements. */ -class ClickDetector { - // protected for testing + // 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 && utils.logger.log('[Replay] Ignoring error from rrweb internals', event); + return null; + } - constructor( - replay, - slowClickConfig, - // Just for easier testing - _addBreadcrumbEvent = addBreadcrumbEvent, - ) { - this._lastMutation = 0; - this._lastScroll = 0; - this._clicks = []; + // 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); - // 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; - } + // 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'; - /** Register click detection handlers on mutation or scroll. */ - addListeners() { - const cleanupWindowOpen = onWindowOpen(() => { - // Treat window.open as mutation - this._lastMutation = nowInSeconds(); + 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, + }, + }, }); - this._teardown = () => { - cleanupWindowOpen(); + // If response is a string, it means its either THROTTLED or SKIPPED + return typeof response === 'string' ? Promise.resolve(null) : response; + }); +} - this._clicks = []; - this._lastMutation = 0; - this._lastScroll = 0; - }; - } +function handleHistory(handlerData) { + const { from, to } = handlerData; - /** Clean up listeners. */ - removeListeners() { - if (this._teardown) { - this._teardown(); - } + const now = Date.now() / 1000; - if (this._checkClickTimeout) { - clearTimeout(this._checkClickTimeout); - } - } + return { + type: 'navigation.push', + start: now, + end: now, + name: to, + data: { + previous: from, + }, + }; +} - /** @inheritDoc */ - handleClick(breadcrumb, node) { - if (ignoreElement(node, this._ignoreSelector) || !isClickBreadcrumb(breadcrumb)) { +/** + * Returns a listener to be added to `addHistoryInstrumentationHandler(listener)`. + */ +function handleHistorySpanListener(replay) { + return (handlerData) => { + if (!replay.isEnabled()) { 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, - }; + const result = handleHistory(handlerData); - // 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) - ) { + if (result === null) { return; } - this._clicks.push(newClick); + // Need to collect visited URLs + replay.getContext().urls.push(result.name); + replay.triggerUserActivity(); - // If this is the first new click, set a timeout to check for multi clicks - if (this._clicks.length === 1) { - this._scheduleCheckClicks(); - } + 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; } - /** @inheritDoc */ - registerMutation(timestamp = Date.now()) { - this._lastMutation = timestampToS(timestamp); + return core.isSentryRequestUrl(url, core.getClient()); +} + +/** Add a performance entry breadcrumb */ +function addNetworkBreadcrumb( + replay, + result, +) { + if (!replay.isEnabled()) { + return; } - /** @inheritDoc */ - registerScroll(timestamp = Date.now()) { - this._lastScroll = timestampToS(timestamp); + if (result === null) { + return; } - /** @inheritDoc */ - registerClick(element) { - const node = getClosestInteractive(element); - this._handleMultiClick(node ); + if (shouldFilterRequest(replay, result.name)) { + return; } - /** Count multiple clicks on elements. */ - _handleMultiClick(node) { - this._getClicks(node).forEach(click => { - click.clickCount++; - }); + 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; } - /** Get all pending clicks for a given node. */ - _getClicks(node) { - return this._clicks.filter(click => click.node === node); - } + // This is only used as a fallback, so we know the body sizes are never set here + const { method, url } = fetchData; - /** Check the clicks that happened. */ - _checkClicks() { - const timedOutClicks = []; + return { + type: 'resource.fetch', + start: startTimestamp / 1000, + end: endTimestamp / 1000, + name: url, + data: { + method, + statusCode: response ? (response ).status : undefined, + }, + }; +} - const now = nowInSeconds(); +/** + * Returns a listener to be added to `addFetchInstrumentationHandler(listener)`. + */ +function handleFetchSpanListener(replay) { + return (handlerData) => { + if (!replay.isEnabled()) { + return; + } - 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; - } + const result = handleFetch(handlerData); - // All of these are in seconds! - if (click.timestamp + this._timeout <= now) { - timedOutClicks.push(click); - } - }); + addNetworkBreadcrumb(replay, result); + }; +} - // Remove "old" clicks - for (const click of timedOutClicks) { - const pos = this._clicks.indexOf(click); +/** only exported for tests */ +function handleXhr(handlerData) { + const { startTimestamp, endTimestamp, xhr } = handlerData; - if (pos > -1) { - this._generateBreadcrumbs(click); - this._clicks.splice(pos, 1); - } - } + const sentryXhrData = xhr[utils.SENTRY_XHR_DATA_KEY]; - // Trigger new check, unless no clicks left - if (this._clicks.length) { - this._scheduleCheckClicks(); - } + if (!startTimestamp || !endTimestamp || !sentryXhrData) { + return null; } - /** 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; + // This is only used as a fallback, so we know the body sizes are never set here + const { method, url, status_code: statusCode } = sentryXhrData; - // 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'; + if (url === undefined) { + return null; + } - 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, - }, - }; + return { + type: 'resource.xhr', + name: url, + start: startTimestamp / 1000, + end: endTimestamp / 1000, + data: { + method, + statusCode, + }, + }; +} - this._addBreadcrumbEvent(replay, breadcrumb); +/** + * Returns a listener to be added to `addXhrInstrumentationHandler(listener)`. + */ +function handleXhrSpanListener(replay) { + return (handlerData) => { + if (!replay.isEnabled()) { 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, - }, - }; + const result = handleXhr(handlerData); - this._addBreadcrumbEvent(replay, breadcrumb); - } + addNetworkBreadcrumb(replay, result); + }; +} + +/** Get the size of a body. */ +function getBodySize( + body, + textEncoder, +) { + if (!body) { + return undefined; } - /** Schedule to check current clicks. */ - _scheduleCheckClicks() { - if (this._checkClickTimeout) { - clearTimeout(this._checkClickTimeout); + try { + if (typeof body === 'string') { + return textEncoder.encode(body).length; } - this._checkClickTimeout = setTimeout(() => this._checkClicks(), 1000); - } -} - -const SLOW_CLICK_TAGS = ['A', 'BUTTON', 'INPUT']; + if (body instanceof URLSearchParams) { + return textEncoder.encode(body.toString()).length; + } -/** exported for tests only */ -function ignoreElement(node, ignoreSelector) { - if (!SLOW_CLICK_TAGS.includes(node.tagName)) { - return true; - } + if (body instanceof FormData) { + const formDataStr = _serializeFormData(body); + return textEncoder.encode(formDataStr).length; + } - // 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 (body instanceof Blob) { + return body.size; + } - // 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 (body instanceof ArrayBuffer) { + return body.byteLength; + } - if (ignoreSelector && node.matches(ignoreSelector)) { - return true; + // Currently unhandled types: ArrayBufferView, ReadableStream + } catch (e) { + // just return undefined } - return false; + return undefined; } -function isClickBreadcrumb(breadcrumb) { - return !!(breadcrumb.data && typeof breadcrumb.data.nodeId === 'number' && breadcrumb.timestamp); -} +/** Convert a Content-Length header to number/undefined. */ +function parseContentLengthHeader(header) { + if (!header) { + return undefined; + } -// This is good enough for us, and is easier to test/mock than `timestampInSeconds` -function nowInSeconds() { - return Date.now() / 1000; + const size = parseInt(header, 10); + return isNaN(size) ? undefined : size; } -/** Update the click detector based on a recording event of rrweb. */ -function updateClickDetectorForRecordingEvent(clickDetector, event) { +/** Get the string representation of a body. */ +function getBodyString(body) { 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; + if (typeof body === 'string') { + return [body]; } - const { source } = event.data; - if (source === IncrementalSource.Mutation) { - clickDetector.registerMutation(event.timestamp); + if (body instanceof URLSearchParams) { + return [body.toString()]; } - if (source === IncrementalSource.Scroll) { - clickDetector.registerScroll(event.timestamp); + if (body instanceof FormData) { + return [_serializeFormData(body)]; } - if (isIncrementalMouseInteraction(event)) { - const { type, id } = event.data; - const node = record.mirror.getNode(id); - - if (node instanceof HTMLElement && type === MouseInteractions.Click) { - clickDetector.registerClick(node); - } + if (!body) { + return [undefined]; } - } catch (e) { - // ignore errors here, e.g. if accessing something that does not exist + } catch (e2) { + DEBUG_BUILD && utils.logger.warn('[Replay] Failed to serialize body', body); + return [undefined, 'BODY_PARSE_ERROR']; } -} -function isIncrementalEvent(event) { - return event.type === ReplayEventTypeIncrementalSnapshot; -} + DEBUG_BUILD && utils.logger.info('[Replay] Skipping network body because of body type', body); -function isIncrementalMouseInteraction( - event, -) { - return event.data.source === IncrementalSource.MouseInteraction; + return [undefined, 'UNPARSEABLE_BODY_TYPE']; } -/** - * Create a breadcrumb for a replay. - */ -function createBreadcrumb( - breadcrumb, +/** Merge a warning into an existing network request/response. */ +function mergeWarning( + info, + warning, ) { - return { - timestamp: Date.now() / 1000, - type: 'default', - ...breadcrumb, - }; -} + if (!info) { + return { + headers: {}, + size: undefined, + _meta: { + warnings: [warning], + }, + }; + } -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 = {})); + const newMeta = { ...info._meta }; + const existingWarnings = newMeta.warnings || []; + newMeta.warnings = [...existingWarnings, warning]; -// 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', -]); + info._meta = newMeta; + return info; +} -/** - * 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; +/** Convert ReplayNetworkRequestData to a PerformanceEntry. */ +function makeNetworkReplayBreadcrumb( + type, + data, +) { + if (!data) { + return null; + } - if (key === 'data-testid' || key === 'data-test-id') { - normalizedKey = 'testId'; - } + const { startTimestamp, endTimestamp, url, method, statusCode, request, response } = data; - obj[normalizedKey] = attributes[key]; - } - } + const result = { + type, + start: startTimestamp / 1000, + end: endTimestamp / 1000, + name: url, + data: utils.dropUndefinedKeys({ + method, + statusCode, + request, + response, + }), + }; - return obj; + return result; } -const handleDomListener = ( - replay, -) => { - return (handlerData) => { - if (!replay.isEnabled()) { - return; - } +/** 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'], + }, + }; +} - const result = handleDom(handlerData); +/** 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 (!result) { - return; - } + if (!bodySize) { + return { + headers, + }; + } - 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 ) , - ); - } + if (!body) { + return { + headers, + size: bodySize, + }; + } - addBreadcrumbEvent(replay, result); + const info = { + headers, + size: bodySize, }; -}; -/** 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; + const { body: normalizedBody, warnings } = normalizeNetworkBody(body); + info.body = normalizedBody; + if (warnings && warnings.length > 0) { + info._meta = { + warnings, + }; + } - 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), - }, - } - : {}, - }; + return info; } -/** - * An event handler to react to DOM events. - * Exported for tests. - */ -function handleDom(handlerData) { - const { target, message } = getDomTarget(handlerData); +/** 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; + }, {}); +} - return createBreadcrumb({ - category: `ui.${handlerData.name}`, - ...getBaseDomBreadcrumb(target, message), - }); +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 getDomTarget(handlerData) { - const isClick = handlerData.name === 'click'; +function normalizeNetworkBody(body) - let message; - let target = null; + { + if (!body || typeof body !== 'string') { + return { + body, + }; + } - // Accessing event.target can throw (see getsentry/raven-js#838, #768) - try { - target = isClick ? getClickTargetNode(handlerData.event ) : getTargetNode(handlerData.event ); - message = utils.htmlTreeAsString(target, { maxStringLength: 200 }) || ''; - } catch (e) { - message = ''; + 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'], + }; } - return { target, message }; -} + if (isProbablyJson) { + try { + const jsonBody = JSON.parse(body); + return { + body: jsonBody, + }; + } catch (e3) { + // fall back to just send the body as string + } + } -function isElement(node) { - return node.type === NodeType.Element; + return { + body, + }; } -/** 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(); +function _strIsProbablyJson(str) { + const first = str[0]; + const last = str[str.length - 1]; - const breadcrumb = getKeyboardBreadcrumb(event); + // Simple check: If this does not start & end with {} or [], it's not JSON + return (first === '[' && last === ']') || (first === '{' && last === '}'); +} - if (!breadcrumb) { - return; - } +/** Match an URL against a list of strings/Regex. */ +function urlMatches(url, urls) { + const fullUrl = getFullUrl(url); - addBreadcrumbEvent(replay, breadcrumb); + return utils.stringMatchesSomePattern(fullUrl, urls); } -/** 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; +/** 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); - // 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; + // 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 message = utils.htmlTreeAsString(target, { maxStringLength: 200 }) || ''; - const baseBreadcrumb = getBaseDomBreadcrumb(target , message); + const fullUrl = fixedUrl.href; - return createBreadcrumb({ - category: 'ui.keyDown', - message, - data: { - ...baseBreadcrumb.data, - metaKey, - shiftKey, - ctrlKey, - altKey, - key, - }, - }); -} + // Remove trailing slashes, if they don't match the original URL + if (!url.endsWith('/') && fullUrl.endsWith('/')) { + return fullUrl.slice(0, -1); + } -function isInputElement(target) { - return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable; + return fullUrl; } -// 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. + * Capture a fetch breadcrumb to a replay. + * This adds additional data (where approriate). */ -function createPerformanceEntries( - entries, +async function captureFetchBreadcrumbToReplay( + breadcrumb, + hint, + options + +, ) { - return entries.map(createPerformanceEntry).filter(Boolean) ; -} + try { + const data = await _prepareFetchData(breadcrumb, hint, options); -function createPerformanceEntry(entry) { - if (!ENTRY_TYPES[entry.entryType]) { - return null; + // Create a replay performance entry from this breadcrumb + const result = makeNetworkReplayBreadcrumb('resource.fetch', data); + addNetworkBreadcrumb(options.replay, result); + } catch (error) { + DEBUG_BUILD && utils.logger.error('[Replay] Failed to capture fetch breadcrumb', error); } - - 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 ((utils.browserPerformanceTimeOrigin || WINDOW.performance.timeOrigin) + time) / 1000; -} - -function createPaintEntry(entry) { - const { duration, entryType, name, startTime } = entry; +/** + * 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 start = getAbsoluteTime(startTime); - return { - type: entryType, - name, - start, - end: start + duration, - data: undefined, - }; -} + const body = input ? _getFetchRequestArgBody(input) : undefined; + const reqSize = getBodySize(body, options.textEncoder); -function createNavigationEntry(entry) { - const { - entryType, - name, - decodedBodySize, - duration, - domComplete, - encodedBodySize, - domContentLoadedEventStart, - domContentLoadedEventEnd, - domInteractive, - loadEventStart, - loadEventEnd, - redirectCount, - startTime, - transferSize, - type, - } = entry; + const resSize = response ? parseContentLengthHeader(response.headers.get('content-length')) : undefined; - // Ignore entries with no duration, they do not seem to be useful and cause dupes - if (duration === 0) { - return null; + if (reqSize !== undefined) { + breadcrumb.data.request_body_size = reqSize; + } + if (resSize !== undefined) { + breadcrumb.data.response_body_size = resSize; } - - 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, +async function _prepareFetchData( + breadcrumb, + hint, + options + +, ) { + const now = Date.now(); + const { startTimestamp = now, endTimestamp = now } = hint; + const { - entryType, - initiatorType, - name, - responseEnd, - startTime, - decodedBodySize, - encodedBodySize, - responseStatus, - transferSize, - } = entry; + url, + method, + status_code: statusCode = 0, + request_body_size: requestBodySize, + response_body_size: responseBodySize, + } = breadcrumb.data; - // Core SDK handles these - if (['fetch', 'xmlhttprequest'].includes(initiatorType)) { - return null; - } + 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 { - type: `${entryType}.${initiatorType}`, - start: getAbsoluteTime(startTime), - end: getAbsoluteTime(responseEnd), - name, - data: { - size: transferSize, - statusCode: responseStatus, - decodedBodySize, - encodedBodySize, - }, + startTimestamp, + endTimestamp, + url, + method, + statusCode, + request, + response, }; } -/** - * Add a LCP event to the replay based on an LCP metric. - */ -function getLargestContentfulPaint(metric - +function _getRequestInfo( + { networkCaptureBodies, networkRequestHeaders }, + input, + requestBodySize, ) { - const entries = metric.entries; - const lastEntry = entries[entries.length - 1] ; - const element = lastEntry ? lastEntry.element : undefined; + const headers = input ? getRequestHeaders(input, networkRequestHeaders) : {}; - const value = metric.value; + if (!networkCaptureBodies) { + return buildNetworkRequestOrResponse(headers, requestBodySize, undefined); + } - const end = getAbsoluteTime(value); + // 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); - const data = { - type: 'largest-contentful-paint', - name: 'largest-contentful-paint', - start: end, - end, - data: { - value, - size: value, - nodeId: element ? record.mirror.getId(element) : undefined, - }, - }; + if (warning) { + return mergeWarning(data, warning); + } 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); - } +/** Exported only for tests. */ +async function _getResponseInfo( + captureDetails, + { + networkCaptureBodies, + textEncoder, + networkResponseHeaders, } - function onEntries({ entries }) { - entries.forEach(addPerformanceEntry); +, + response, + responseBodySize, +) { + if (!captureDetails && responseBodySize !== undefined) { + return buildSkippedNetworkRequestOrResponse(responseBodySize); } - const clearCallbacks = []; + const headers = response ? getAllHeaders(response.headers, networkResponseHeaders) : {}; - (['navigation', 'paint', 'resource'] ).forEach(type => { - clearCallbacks.push(tracing.addPerformanceInstrumentationHandler(type, onEntries)); + 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, }); - clearCallbacks.push( - tracing.addLcpInstrumentationHandler(({ metric }) => { - replay.replayPerformanceEntries.push(getLargestContentfulPaint(metric)); - }), - ); + if (warning) { + return mergeWarning(result, warning); + } - // A callback to cleanup all handlers - return () => { - clearCallbacks.forEach(clearCallback => clearCallback()); - }; + return result; } -/** - * 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 getResponseData( + bodyText, + { + networkCaptureBodies, + textEncoder, + responseBodySize, + captureDetails, + headers, + } -function e(){const e=new Blob([r]);return URL.createObjectURL(e)} +, +) { + try { + const size = + bodyText && bodyText.length && responseBodySize === undefined + ? getBodySize(bodyText, textEncoder) + : responseBodySize; -/** - * Log a message in debug mode, and add a breadcrumb when _experiment.traceInternals is enabled. - */ -function logInfo(message, shouldAddBreadcrumb) { - if (!DEBUG_BUILD) { - return; - } + if (!captureDetails) { + return buildSkippedNetworkRequestOrResponse(size); + } - utils.logger.info(message); + if (networkCaptureBodies) { + return buildNetworkRequestOrResponse(headers, size, bodyText); + } - if (shouldAddBreadcrumb) { - addLogBreadcrumb(message); + return buildNetworkRequestOrResponse(headers, size, undefined); + } catch (error) { + DEBUG_BUILD && utils.logger.warn('[Replay] Failed to serialize response body', error); + // fallback + return buildNetworkRequestOrResponse(headers, responseBodySize, undefined); } } -/** - * 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; - } - - utils.logger.info(message); +async function _parseFetchResponseBody(response) { + const res = _tryCloneResponse(response); - 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); + if (!res) { + return [undefined, 'BODY_PARSE_ERROR']; } -} -function addLogBreadcrumb(message) { - core.addBreadcrumb( - { - category: 'console', - data: { - logger: 'replay', - }, - level: 'info', - message, - }, - { level: 'info' }, - ); + try { + const text = await _tryGetResponseText(res); + return [text]; + } catch (error) { + DEBUG_BUILD && utils.logger.warn('[Replay] Failed to get text body from response', error); + return [undefined, 'BODY_PARSE_ERROR']; + } } -/** 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}.`); +function _getFetchRequestArgBody(fetchArgs = []) { + // We only support getting the body from the fetch options + if (fetchArgs.length !== 2 || typeof fetchArgs[1] !== 'object') { + return undefined; } -} -/** - * 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. */ + return (fetchArgs[1] ).body; +} - /** @inheritdoc */ +function getAllHeaders(headers, allowedHeaders) { + const allHeaders = {}; - constructor() { - this.events = []; - this._totalSize = 0; - this.hasCheckout = false; - } + allowedHeaders.forEach(header => { + if (headers.get(header)) { + allHeaders[header] = headers.get(header) ; + } + }); - /** @inheritdoc */ - get hasEvents() { - return this.events.length > 0; - } + return allHeaders; +} - /** @inheritdoc */ - get type() { - return 'sync'; +function getRequestHeaders(fetchArgs, allowedHeaders) { + if (fetchArgs.length === 1 && typeof fetchArgs[0] !== 'string') { + return getHeadersFromOptions(fetchArgs[0] , allowedHeaders); } - /** @inheritdoc */ - destroy() { - this.events = []; + if (fetchArgs.length === 2) { + return getHeadersFromOptions(fetchArgs[1] , allowedHeaders); } - /** @inheritdoc */ - async addEvent(event) { - const eventSize = JSON.stringify(event).length; - this._totalSize += eventSize; - if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) { - throw new EventBufferSizeExceededError(); - } + return {}; +} - this.events.push(event); +function getHeadersFromOptions( + input, + allowedHeaders, +) { + if (!input) { + return {}; } - /** @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)); - }); + const headers = input.headers; + + if (!headers) { + return {}; } - /** @inheritdoc */ - clear() { - this.events = []; - this._totalSize = 0; - this.hasCheckout = false; + if (headers instanceof Headers) { + return getAllHeaders(headers, allowedHeaders); } - /** @inheritdoc */ - getEarliestTimestamp() { - const timestamp = this.events.map(event => event.timestamp).sort()[0]; + // We do not support this, as it is not really documented (anymore?) + if (Array.isArray(headers)) { + return {}; + } - if (!timestamp) { - return null; - } + return getAllowedHeaders(headers, allowedHeaders); +} - return timestampToMs(timestamp); +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 && utils.logger.warn('[Replay] Failed to clone response body', error); } } /** - * Event buffer that uses a web worker to compress events. - * Exported only for testing. + * 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. */ -class WorkerHandler { - - constructor(worker) { - this._worker = worker; - this._id = 0; - } +function _tryGetResponseText(response) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Timeout while trying to read response body')), 500); - /** - * 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; - } + _getResponseText(response) + .then( + txt => resolve(txt), + reason => reject(reason), + ) + .finally(() => clearTimeout(timeout)); + }); +} - this._ensureReadyPromise = new Promise((resolve, reject) => { - this._worker.addEventListener( - 'message', - ({ data }) => { - if ((data ).success) { - resolve(); - } else { - reject(); - } - }, - { once: true }, - ); +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(); +} - this._worker.addEventListener( - 'error', - error => { - reject(error); - }, - { once: true }, - ); - }); +/** + * 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); - return this._ensureReadyPromise; + // Create a replay performance entry from this breadcrumb + const result = makeNetworkReplayBreadcrumb('resource.xhr', data); + addNetworkBreadcrumb(options.replay, result); + } catch (error) { + DEBUG_BUILD && utils.logger.error('[Replay] Failed to capture xhr breadcrumb', error); } +} - /** - * Destroy the worker. - */ - destroy() { - logInfo('[Replay] Destroying compression worker'); - this._worker.terminate(); +/** + * 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; } - /** - * Post message to worker and wait for response before resolving promise. - */ - postMessage(method, arg) { - const id = this._getAndIncrementId(); + const reqSize = getBodySize(input, options.textEncoder); + const resSize = xhr.getResponseHeader('content-length') + ? parseContentLengthHeader(xhr.getResponseHeader('content-length')) + : _getBodySize(xhr.response, xhr.responseType, options.textEncoder); - return new Promise((resolve, reject) => { - const listener = ({ data }) => { - const response = data ; - if (response.method !== method) { - return; - } + if (reqSize !== undefined) { + breadcrumb.data.request_body_size = reqSize; + } + if (resSize !== undefined) { + breadcrumb.data.response_body_size = resSize; + } +} - // There can be multiple listeners for a single method, the id ensures - // that the response matches the caller. - if (response.id !== id) { - return; - } +function _prepareXhrData( + breadcrumb, + hint, + options, +) { + const now = Date.now(); + const { startTimestamp = now, endTimestamp = now, input, xhr } = hint; - // At this point, we'll always want to remove listener regardless of result status - this._worker.removeEventListener('message', listener); + const { + url, + method, + status_code: statusCode = 0, + request_body_size: requestBodySize, + response_body_size: responseBodySize, + } = breadcrumb.data; - if (!response.success) { - // TODO: Do some error handling, not sure what - DEBUG_BUILD && utils.logger.error('[Replay]', response.response); + if (!url) { + return null; + } - reject(new Error('Error in compression worker')); - return; - } + 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, + }; + } - resolve(response.response ); - }; + const xhrInfo = xhr[utils.SENTRY_XHR_DATA_KEY]; + const networkRequestHeaders = xhrInfo + ? getAllowedHeaders(xhrInfo.request_headers, options.networkRequestHeaders) + : {}; + const networkResponseHeaders = getAllowedHeaders(getResponseHeaders(xhr), options.networkResponseHeaders); - // 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 }); - }); - } + const [requestBody, requestWarning] = options.networkCaptureBodies ? getBodyString(input) : [undefined]; + const [responseBody, responseWarning] = options.networkCaptureBodies ? _getXhrResponseBody(xhr) : [undefined]; - /** Get the current ID and increment it for the next call. */ - _getAndIncrementId() { - return this._id++; - } + 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, + }; } -/** - * Event buffer that uses a web worker to compress events. - * Exported only for testing. - */ -class EventBufferCompressionWorker { - /** @inheritdoc */ +function getResponseHeaders(xhr) { + const headers = xhr.getAllResponseHeaders(); - constructor(worker) { - this._worker = new WorkerHandler(worker); - this._earliestTimestamp = null; - this._totalSize = 0; - this.hasCheckout = false; + if (!headers) { + return {}; } - /** @inheritdoc */ - get hasEvents() { - return !!this._earliestTimestamp; - } + return headers.split('\r\n').reduce((acc, line) => { + const [key, value] = line.split(': '); + acc[key.toLowerCase()] = value; + return acc; + }, {}); +} - /** @inheritdoc */ - get type() { - return 'worker'; - } +function _getXhrResponseBody(xhr) { + // We collect errors that happen, but only log them if we can't get any response body + const errors = []; - /** - * 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(); + try { + return [xhr.responseText]; + } catch (e) { + errors.push(e); } - /** - * Destroy the event buffer. - */ - destroy() { - this._worker.destroy(); + // Try to manually parse the response body, if responseText fails + try { + return _parseXhrResponse(xhr.response, xhr.responseType); + } catch (e) { + errors.push(e); } - /** - * 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; - } + DEBUG_BUILD && utils.logger.warn('[Replay] Failed to get xhr response body', ...errors); - const data = JSON.stringify(event); - this._totalSize += data.length; + return [undefined]; +} - if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) { - return Promise.reject(new EventBufferSizeExceededError()); +/** + * 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]; } - 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 && utils.logger.warn('[Replay] Sending "clear" message to worker failed', e); - }); - } + if (body instanceof Document) { + return [body.body.outerHTML]; + } - /** @inheritdoc */ - getEarliestTimestamp() { - return this._earliestTimestamp; - } + if (responseType === 'json' && body && typeof body === 'object') { + return [JSON.stringify(body)]; + } - /** - * Send the event to the worker. - */ - _sendEventToWorker(data) { - return this._worker.postMessage('addEvent', data); + if (!body) { + return [undefined]; + } + } catch (e2) { + DEBUG_BUILD && utils.logger.warn('[Replay] Failed to serialize body', body); + return [undefined, 'BODY_PARSE_ERROR']; } - /** - * Finish the request and return the compressed data from the worker. - */ - async _finishRequest() { - const response = await this._worker.postMessage('finish'); + DEBUG_BUILD && utils.logger.info('[Replay] Skipping network body because of body type', body); - this._earliestTimestamp = null; - this._totalSize = 0; + return [undefined, 'UNPARSEABLE_BODY_TYPE']; +} - return response; +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 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. + * 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) */ -class EventBufferProxy { +function handleNetworkBreadcrumbs(replay) { + const client = core.getClient(); - constructor(worker) { - this._fallback = new EventBufferArray(); - this._compression = new EventBufferCompressionWorker(worker); - this._used = this._fallback; + try { + const textEncoder = new TextEncoder(); - this._ensureWorkerIsLoadedPromise = this._ensureWorkerIsLoaded(); - } + const { + networkDetailAllowUrls, + networkDetailDenyUrls, + networkCaptureBodies, + networkRequestHeaders, + networkResponseHeaders, + } = replay.getOptions(); - /** @inheritdoc */ - get type() { - return this._used.type; - } + const options = { + replay, + textEncoder, + networkDetailAllowUrls, + networkDetailDenyUrls, + networkCaptureBodies, + networkRequestHeaders, + networkResponseHeaders, + }; - /** @inheritDoc */ - get hasEvents() { - return this._used.hasEvents; + if (client && client.on) { + client.on('beforeAddBreadcrumb', (breadcrumb, hint) => beforeAddNetworkBreadcrumb(options, breadcrumb, hint)); + } else { + // Fallback behavior + utils.addFetchInstrumentationHandler(handleFetchSpanListener(replay)); + utils.addXhrInstrumentationHandler(handleXhrSpanListener(replay)); + } + } catch (e2) { + // Do nothing } +} - /** @inheritdoc */ - get hasCheckout() { - return this._used.hasCheckout; - } - /** @inheritdoc */ - set hasCheckout(value) { - this._used.hasCheckout = value; +/** just exported for tests */ +function beforeAddNetworkBreadcrumb( + options, + breadcrumb, + hint, +) { + if (!breadcrumb.data) { + return; } - /** @inheritDoc */ - destroy() { - this._fallback.destroy(); - this._compression.destroy(); - } + 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); - /** @inheritdoc */ - clear() { - return this._used.clear(); - } + // This call should not reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + captureXhrBreadcrumbToReplay(breadcrumb, hint, options); + } - /** @inheritdoc */ - getEarliestTimestamp() { - return this._used.getEarliestTimestamp(); - } + 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); - /** - * Add an event to the event buffer. - * - * Returns true if event was successfully added. - */ - addEvent(event) { - return this._used.addEvent(event); + // This call should not reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + captureFetchBreadcrumbToReplay(breadcrumb, hint, options); + } + } catch (e) { + DEBUG_BUILD && utils.logger.warn('Error when enriching network breadcrumb'); } +} - /** @inheritDoc */ - async finish() { - // Ensure the worker is loaded, so the sent event is compressed - await this.ensureWorkerIsLoaded(); +function _isXhrBreadcrumb(breadcrumb) { + return breadcrumb.category === 'xhr'; +} - return this._used.finish(); - } +function _isFetchBreadcrumb(breadcrumb) { + return breadcrumb.category === 'fetch'; +} - /** Ensure the worker has loaded. */ - ensureWorkerIsLoaded() { - return this._ensureWorkerIsLoadedPromise; - } +function _isXhrHint(hint) { + return hint && hint.xhr; +} - /** 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'); +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; } - // 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 result = handleScope(scope); - const addEventPromises = []; - for (const event of events) { - addEventPromises.push(this._compression.addEvent(event)); + if (!result) { + return; } - this._compression.hasCheckout = hasCheckout; + addBreadcrumbEvent(replay, result); + }; - // We switch over to the new buffer immediately - any further events will be added - // after the previously buffered ones - this._used = this._compression; +/** + * 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(); - // Wait for original events to be re-added before resolving - try { - await Promise.all(addEventPromises); - } catch (error) { - DEBUG_BUILD && utils.logger.warn('[Replay] Failed to add events when switching buffers.', error); - } + // 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; } -} -/** - * Create an event buffer for replays. - */ -function createEventBuffer({ - useCompression, - workerUrl: customWorkerUrl, -}) { + _LAST_BREADCRUMB = newBreadcrumb; + if ( - useCompression && - // eslint-disable-next-line no-restricted-globals - window.Worker + !isBreadcrumbWithCategory(newBreadcrumb) || + ['fetch', 'xhr', 'sentry.event', 'sentry.transaction'].includes(newBreadcrumb.category) || + newBreadcrumb.category.startsWith('ui.') ) { - const worker = _loadWorker(customWorkerUrl); + return null; + } - if (worker) { - return worker; - } + if (newBreadcrumb.category === 'console') { + return normalizeConsoleBreadcrumb(newBreadcrumb); } - logInfo('[Replay] Using simple buffer'); - return new EventBufferArray(); + return createBreadcrumb(newBreadcrumb); } -function _loadWorker(customWorkerUrl) { - try { - const workerUrl = customWorkerUrl || _getWorkerUrl(); - - if (!workerUrl) { - return; - } +/** exported for tests only */ +function normalizeConsoleBreadcrumb( + breadcrumb, +) { + const args = breadcrumb.data && breadcrumb.data.arguments; - 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 + if (!Array.isArray(args) || args.length === 0) { + return createBreadcrumb(breadcrumb); } -} -function _getWorkerUrl() { - if (typeof __SENTRY_EXCLUDE_REPLAY_WORKER__ === 'undefined' || !__SENTRY_EXCLUDE_REPLAY_WORKER__) { - return e(); - } + let isTruncated = false; - return ''; -} + // 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)}…`; + } -/** 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; - } -} + return arg; + } + if (typeof arg === 'object') { + try { + const normalizedArg = utils.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 + } + } -/** - * Removes the session from Session Storage and unsets session in replay instance - */ -function clearSession(replay) { - deleteSession(); - replay.session = undefined; + return arg; + }); + + return createBreadcrumb({ + ...breadcrumb, + data: { + ...breadcrumb.data, + arguments: normalizedArgs, + ...(isTruncated ? { _meta: { warnings: ['CONSOLE_ARG_TRUNCATED'] } } : {}), + }, + }); } /** - * Deletes a session from storage + * Add global listeners that cannot be removed. */ -function deleteSession() { - if (!hasSessionStorage()) { - return; - } +function addGlobalListeners(replay) { + // Listeners from core SDK // + const scope = core.getCurrentScope(); + const client = core.getClient(); - try { - WINDOW.sessionStorage.removeItem(REPLAY_SESSION_KEY); - } catch (e) { - // Ignore potential SecurityError exceptions - } -} + scope.addScopeListener(handleScopeListener(replay)); + utils.addClickKeypressInstrumentationHandler(handleDomListener(replay)); + utils.addHistoryInstrumentationHandler(handleHistorySpanListener(replay)); + handleNetworkBreadcrumbs(replay); -/** - * 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; + // 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 { + core.addEventProcessor(eventProcessor); } - // Math.random() returns a number in range of 0 to 1 (inclusive of 0, but not 1) - return Math.random() < sampleRate; -} + // 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; + } + } + }); -/** - * Get a session with defaults & applied sampling. - */ -function makeSession(session) { - const now = Date.now(); - const id = session.id || utils.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; + client.on('startTransaction', transaction => { + replay.lastTransaction = transaction; + }); - return { - id, - started, - lastActivity, - segmentId, - sampled, - previousSessionId, - }; + // 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 + 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); } /** - * Save a session to session storage. + * Create a "span" for the total amount of memory being used by JS objects + * (including v8 internal objects). */ -function saveSession(session) { - if (!hasSessionStorage()) { - return; - } - +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 { - WINDOW.sessionStorage.setItem(REPLAY_SESSION_KEY, JSON.stringify(session)); - } catch (e) { - // Ignore potential SecurityError exceptions + 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 []; } } -/** - * 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; +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, + }, + }, + }; } /** - * 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". + * 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 createSession( - { sessionSampleRate, allowBuffering, stickySession = false }, - { previousSessionId } = {}, -) { - const sampled = getSessionSampleType(sessionSampleRate, allowBuffering); - const session = makeSession({ - sampled, - previousSessionId, - }); +function debounce(func, wait, options) { + let callbackReturnValue; - if (stickySession) { - saveSession(session); - } + let timerId; + let maxTimerId; - return session; -} + const maxWait = options && options.maxWait ? Math.max(options.maxWait, wait) : 0; -/** - * Fetches a session from storage - */ -function fetchSession(traceInternals) { - if (!hasSessionStorage()) { - return null; + function invokeFunc() { + cancelTimers(); + callbackReturnValue = func(); + return callbackReturnValue; } - try { - // This can throw if cookies are disabled - const sessionStringFromStorage = WINDOW.sessionStorage.getItem(REPLAY_SESSION_KEY); + function cancelTimers() { + timerId !== undefined && clearTimeout(timerId); + maxTimerId !== undefined && clearTimeout(maxTimerId); + timerId = maxTimerId = undefined; + } - if (!sessionStringFromStorage) { - return null; + function flush() { + if (timerId !== undefined || maxTimerId !== undefined) { + return invokeFunc(); } + return callbackReturnValue; + } - const sessionObj = JSON.parse(sessionStringFromStorage) ; + function debounced() { + if (timerId) { + clearTimeout(timerId); + } + timerId = setTimeout(invokeFunc, wait); - logInfoNextTick('[Replay] Loading existing session', traceInternals); + if (maxWait && maxTimerId === undefined) { + maxTimerId = setTimeout(invokeFunc, maxWait); + } - return makeSession(sessionObj); - } catch (e) { - return null; + return callbackReturnValue; } + + debounced.cancel = cancelTimers; + debounced.flush = flush; + return debounced; } /** - * Given an initial timestamp and an expiry duration, checks to see if current - * time should be considered as expired. + * Handler for recording events. + * + * Adds to event buffer, and has varying flushing behaviors if the event was a checkout. */ -function isExpired( - initialTime, - expiry, - targetTime = +new Date(), -) { - // Always expired if < 0 - if (initialTime === null || expiry === undefined || expiry < 0) { - return true; - } +function getHandleRecordingEmit(replay) { + let hadFirstEvent = false; - // Never expires if == 0 - if (expiry === 0) { - return 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 && utils.logger.warn('[Replay] Received replay event after session expired.'); - return initialTime + expiry <= targetTime; -} + return; + } -/** - * 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) - ); -} + // `_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 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 (replay.clickDetector) { + updateClickDetectorForRecordingEvent(replay.clickDetector, event); + } - // If we are buffering & haven't ever flushed yet, always continue - if (session.sampled === 'buffer' && session.segmentId === 0) { - return false; - } + // 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(); + } - return true; -} + // 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; + } -/** - * Get or create a session, when initializing the replay. - * Returns a session that may be unsampled. - */ -function loadOrCreateSession( - { - traceInternals, - sessionIdleExpire, - maxReplayDuration, - previousSessionId, - } + // 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; + } -, - sessionOptions, -) { - const existingSession = sessionOptions.stickySession && fetchSession(traceInternals); + // 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); + } + } + } - // No session exists yet, just create a new one - if (!existingSession) { - logInfoNextTick('[Replay] Creating new session', traceInternals); - return createSession(sessionOptions, { previousSessionId }); - } + 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. - if (!shouldRefreshSession(existingSession, { sessionIdleExpire, maxReplayDuration })) { - return existingSession; - } + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + void replay.flush(); + } - logInfoNextTick('[Replay] Session in sessionStorage is expired, creating new one...'); - return createSession(sessionOptions, { previousSessionId: existingSession.id }); + return true; + }); + }; } -function isCustomEvent(event) { - return event.type === EventType.Custom; +/** + * 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 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`. + * 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 addEventSync(replay, event, isCheckout) { - if (!shouldAddEvent(replay, event)) { - return false; +function addSettingsEvent(replay, isCheckout) { + // Only need to add this event when sending the first segment + if (!isCheckout || !replay.session || replay.session.segmentId !== 0) { + return; } - // This should never reject - // eslint-disable-next-line @typescript-eslint/no-floating-promises - _addEvent(replay, event, isCheckout); - - return true; + addEventSync(replay, createOptionsEvent(replay), false); } /** - * 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`. + * Create a replay envelope ready to be sent. + * This includes both the replay event, as well as the recording data. */ -function addEvent( - replay, - event, - isCheckout, +function createReplayEnvelope( + replayEvent, + recordingData, + dsn, + tunnel, ) { - if (!shouldAddEvent(replay, event)) { - return Promise.resolve(null); - } + return utils.createEnvelope( + utils.createEventEnvelopeHeaders(replayEvent, utils.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, + ], + ], + ); +} - return _addEvent(replay, event, isCheckout); +/** + * Prepare the recording data ready to be sent. + */ +function prepareRecordingData({ + recordingData, + headers, } -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 && utils.logger.error(error); - await replay.stop({ reason }); + let payloadWithSequence; - const client = core.getClient(); + // XXX: newline is needed to separate sequence id from events + const replayHeaders = `${JSON.stringify(headers)} +`; - if (client) { - client.recordDroppedEvent('internal_sdk_error', 'replay'); - } + 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; } -/** Exported only for tests. */ -function shouldAddEvent(replay, event) { - if (!replay.eventBuffer || replay.isPaused() || !replay.isEnabled()) { - return false; - } +/** + * Prepare a replay event & enrich it with the SDK metadata. + */ +async function prepareReplayEvent({ + client, + scope, + replayId: event_id, + event, +} - const timestampInMs = timestampToMs(event.timestamp); +) { + const integrations = + typeof client._integrations === 'object' && client._integrations !== null && !Array.isArray(client._integrations) + ? Object.keys(client._integrations) + : undefined; - // 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; - } + const eventHint = { event_id, integrations }; - // 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; + if (client.emit) { + client.emit('preprocessEvent', event, eventHint); } - return true; -} + const preparedEvent = (await core.prepareEvent( + client.getOptions(), + event, + eventHint, + scope, + client, + core.getIsolationScope(), + )) ; -function maybeApplyCallback( - event, - callback, -) { - try { - if (typeof callback === 'function' && isCustomEvent(event)) { - return callback(event); - } - } catch (error) { - DEBUG_BUILD && - utils.logger.error('[Replay] An error occured in the `beforeAddRecordingEvent` callback, skipping the event...', error); + // If e.g. a global event processor returned null + if (!preparedEvent) { return null; } - return event; -} - -/** If the event is an error event */ -function isErrorEvent(event) { - return !event.type; -} + // 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'; -/** If the event is a transaction event */ -function isTransactionEvent(event) { - return event.type === 'transaction'; -} + // 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) || {}; -/** If the event is an replay event */ -function isReplayEvent(event) { - return event.type === 'replay_event'; -} + preparedEvent.sdk = { + ...preparedEvent.sdk, + name: name || 'sentry.javascript.unknown', + version: version || '0.0.0', + }; -/** If the event is a feedback event */ -function isFeedbackEvent(event) { - return event.type === 'feedback'; + return preparedEvent; } /** - * Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`. + * Send replay attachment using `fetch()` */ -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; - } +async function sendReplayRequest({ + recordingData, + replayId, + segmentId: segment_id, + eventContext, + timestamp, + session, +}) { + const preparedRecordingData = prepareRecordingData({ + recordingData, + headers: { + segment_id, + }, + }); - handleErrorEvent(replay, event); - }; -} + const { urls, errorIds, traceIds, initialTimestamp } = eventContext; -function handleTransactionEvent(replay, event) { - const replayContext = replay.getContext(); + const client = core.getClient(); + const scope = core.getCurrentScope(); + const transport = client && client.getTransport(); + const dsn = client && client.getDsn(); - // 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 ); + if (!client || !transport || !dsn || !session.sampled) { + return; } -} -function handleErrorEvent(replay, event) { - const replayContext = replay.getContext(); + 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, + }; - // 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); - } + const replayEvent = await prepareReplayEvent({ scope, client, replayId, event: baseEvent }); - // 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) { + 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; } - const { beforeErrorSampling } = replay.getOptions(); - if (typeof beforeErrorSampling === 'function' && !beforeErrorSampling(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": { + }, } + */ - setTimeout(() => { - // Capture current event buffer as new replay - // This should never reject - // eslint-disable-next-line @typescript-eslint/no-floating-promises - replay.sendBufferedReplayOrFlush(); - }); -} + // 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; -function isBaseTransportSend() { - const client = core.getClient(); - if (!client) { - return false; - } + const envelope = createReplayEnvelope(replayEvent, preparedRecordingData, dsn, client.getOptions().tunnel); - const transport = client.getTransport(); - if (!transport) { - return false; - } + let response; - return ( - (transport.send ).__sentry__baseTransport__ || false - ); -} + try { + response = await transport.send(envelope); + } catch (err) { + const error = new Error(UNABLE_TO_SEND_REPLAY); -/** - * Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`. - */ -function handleBeforeSendEvent(replay) { - return (event) => { - if (!replay.isEnabled() || !isErrorEvent(event)) { - return; + 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 } - - handleHydrationError(replay, event); - }; -} - -function handleHydrationError(replay, event) { - const exceptionValue = event.exception && event.exception.values && event.exception.values[0].value; - if (typeof exceptionValue !== 'string') { - return; + throw error; } - 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); + // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore + if (!response) { + return response; } -} -/** - * 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; + // 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); } - // @ts-expect-error this may be set by rrweb when it finds errors - if (hint.originalException && hint.originalException.__rrweb__) { - return true; + const rateLimits = utils.updateRateLimits({}, response); + if (utils.isRateLimited(rateLimits, 'replay')) { + throw new RateLimitError(rateLimits); } - 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; - }); + return response; } /** - * 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. + * This error indicates that the transport returned an invalid status code. */ -function shouldSampleForBufferEvent(replay, event) { - if (replay.recordingMode !== 'buffer') { - return false; +class TransportStatusCodeError extends Error { + constructor(statusCode) { + super(`Transport returned status code ${statusCode}`); } +} - // 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; - } +/** + * This error indicates that we hit a rate limit API error. + */ +class RateLimitError extends Error { - // Require the event to be an error event & to have an exception - if (!event.exception || event.type) { - return false; + constructor(rateLimits) { + super('Rate limit hit'); + this.rateLimits = rateLimits; } - - return isSampled(replay.getOptions().errorSampleRate); } /** - * Returns a listener to be added to `addEventProcessor(listener)`. + * Finalize and send the current replay event to Sentry */ -function handleGlobalEventListener( - replay, - includeAfterSendEventHandling = false, +async function sendReplay( + replayData, + retryConfig = { + count: 0, + interval: RETRY_BASE_INTERVAL, + }, ) { - const afterSendHandler = includeAfterSendEventHandling ? handleAfterSendEvent(replay) : undefined; + const { recordingData, options } = replayData; - return Object.assign( - (event, hint) => { - // Do nothing if replay has been disabled - if (!replay.isEnabled()) { - return event; - } + // short circuit if there's no events to upload (this shouldn't happen as _runFlush makes this check) + if (!recordingData.length) { + return; + } - if (isReplayEvent(event)) { - // Replays have separate set of breadcrumbs, do not include breadcrumbs - // from core SDK - delete event.breadcrumbs; - return event; - } + try { + await sendReplayRequest(replayData); + return true; + } catch (err) { + if (err instanceof TransportStatusCodeError || err instanceof RateLimitError) { + throw err; + } - // We only want to handle errors, transactions, and feedbacks, nothing else - if (!isErrorEvent(event) && !isTransactionEvent(event) && !isFeedbackEvent(event)) { - return event; - } + // Capture error for every failed replay + core.setContext('Replays', { + _retryCount: retryConfig.count, + }); - // Ensure we do not add replay_id if the session is expired - const isSessionActive = replay.checkAndHandleExpiredSession(); - if (!isSessionActive) { - return event; - } + if (DEBUG_BUILD && options._experiments && options._experiments.captureExceptions) { + core.captureException(err); + } - 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; - } + // 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`); - // 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 && utils.logger.log('[Replay] Ignoring error from rrweb internals', event); - return null; + 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 } - // 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() }; - } + throw error; + } - // 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 }); - } + // will retry in intervals of 5, 10, 30 + retryConfig.interval *= ++retryConfig.count; - return event; - }, - { id: 'Replay' }, - ); + 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 "span" for each performance entry. + * 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. */ -function createPerformanceSpans( - replay, - entries, +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function throttle( + fn, + maxCount, + durationSeconds, ) { - 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, - }, - }, + const counter = new Map(); + + const _cleanup = (now) => { + const threshold = now - durationSeconds; + counter.forEach((_value, key) => { + if (key < threshold) { + counter.delete(key); + } }); + }; - // If response is a string, it means its either THROTTLED or SKIPPED - return typeof response === 'string' ? Promise.resolve(null) : response; - }); -} + const _getTotalCount = () => { + return [...counter.values()].reduce((a, b) => a + b, 0); + }; -function handleHistory(handlerData) { - const { from, to } = handlerData; + let isThrottled = false; - const now = Date.now() / 1000; + return (...rest) => { + // Date in second-precision, which we use as basis for the throttling + const now = Math.floor(Date.now() / 1000); - return { - type: 'navigation.push', - start: now, - end: now, - name: to, - data: { - previous: from, - }, + // 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 + /** - * Returns a listener to be added to `addHistoryInstrumentationHandler(listener)`. + * The main replay container class, which holds all the state and methods for recording and sending replays. */ -function handleHistorySpanListener(replay) { - return (handlerData) => { - if (!replay.isEnabled()) { - return; - } +class ReplayContainer { - const result = handleHistory(handlerData); + /** + * 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 + */ - if (result === null) { - return; - } + /** + * The current or last active transcation. + * This is only available when performance is enabled. + */ - // Need to collect visited URLs - replay.getContext().urls.push(result.name); - replay.triggerUserActivity(); + /** + * These are here so we can overwrite them in tests etc. + * @hidden + */ - replay.addUpdate(() => { - createPerformanceSpans(replay, [result]); - // Returning false to flush - return false; - }); - }; -} + /** + * Options to pass to `rrweb.record()` + */ -/** - * 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; - } + /** + * Timestamp of the last user activity. This lives across sessions. + */ - return core.isSentryRequestUrl(url, core.getClient()); -} + /** + * Is the integration currently active? + */ -/** Add a performance entry breadcrumb */ -function addNetworkBreadcrumb( - replay, - result, -) { - if (!replay.isEnabled()) { - return; - } + /** + * Paused is a state where: + * - DOM Recording is not listening at all + * - Nothing will be added to event buffer (e.g. core SDK events) + */ - if (result === null) { - return; - } + /** + * Have we attached listeners to the core SDK? + * Note we have to track this as there is no way to remove instrumentation handlers. + */ - if (shouldFilterRequest(replay, result.name)) { - return; + /** + * Function to stop recording + */ + + /** + * Internal use for canvas recording options + */ + + constructor({ + options, + recordingOptions, } - 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; - }); -} +) {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: '', + }; -/** only exported for tests */ -function handleFetch(handlerData) { - const { startTimestamp, endTimestamp, fetchData, response } = handlerData; + this._recordingOptions = recordingOptions; + this._options = options; - if (!endTimestamp) { - return null; - } + this._debouncedFlush = debounce(() => this._flush(), this._options.flushMinDelay, { + maxWait: this._options.flushMaxDelay, + }); - // This is only used as a fallback, so we know the body sizes are never set here - const { method, url } = fetchData; + this._throttledAddEvent = throttle( + (event, isCheckout) => addEvent(this, event, isCheckout), + // Max 300 events... + 300, + // ... per 5s + 5, + ); - return { - type: 'resource.fetch', - start: startTimestamp / 1000, - end: endTimestamp / 1000, - name: url, - data: { - method, - statusCode: response ? (response ).status : undefined, - }, - }; -} + const { slowClickTimeout, slowClickIgnoreSelectors } = this.getOptions(); -/** - * Returns a listener to be added to `addFetchInstrumentationHandler(listener)`. - */ -function handleFetchSpanListener(replay) { - return (handlerData) => { - if (!replay.isEnabled()) { - return; + 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); } + } - const result = handleFetch(handlerData); + /** Get the event context. */ + getContext() { + return this._context; + } - addNetworkBreadcrumb(replay, result); - }; -} + /** If recording is currently enabled. */ + isEnabled() { + return this._isEnabled; + } -/** only exported for tests */ -function handleXhr(handlerData) { - const { startTimestamp, endTimestamp, xhr } = handlerData; + /** If recording is currently paused. */ + isPaused() { + return this._isPaused; + } - const sentryXhrData = xhr[utils.SENTRY_XHR_DATA_KEY]; + /** + * Determine if canvas recording is enabled + */ + isRecordingCanvas() { + return Boolean(this._canvas); + } - if (!startTimestamp || !endTimestamp || !sentryXhrData) { - return null; + /** Get the replay integration options. */ + getOptions() { + return this._options; } - // This is only used as a fallback, so we know the body sizes are never set here - const { method, url, status_code: statusCode } = sentryXhrData; + /** + * Initializes the plugin based on sampling configuration. Should not be + * called outside of constructor. + */ + initializeSampling(previousSessionId) { + const { errorSampleRate, sessionSampleRate } = this._options; - if (url === undefined) { - return null; - } + // 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; + } - return { - type: 'resource.xhr', - name: url, - start: startTimestamp / 1000, - end: endTimestamp / 1000, - data: { - method, - statusCode, - }, - }; -} + // Otherwise if there is _any_ sample rate set, try to load an existing + // session, or create a new one. + this._initializeSessionForSampling(previousSessionId); -/** - * Returns a listener to be added to `addXhrInstrumentationHandler(listener)`. - */ -function handleXhrSpanListener(replay) { - return (handlerData) => { - if (!replay.isEnabled()) { + if (!this.session) { + // This should not happen, something wrong has occurred + this._handleException(new Error('Unable to initialize and create session')); return; } - const result = handleXhr(handlerData); + 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; + } - addNetworkBreadcrumb(replay, result); - }; -} + // 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'; -/** Get the size of a body. */ -function getBodySize( - body, - textEncoder, -) { - if (!body) { - return undefined; + logInfoNextTick( + `[Replay] Starting replay in ${this.recordingMode} mode`, + this._options._experiments.traceInternals, + ); + + this._initializeRecording(); } - try { - if (typeof body === 'string') { - return textEncoder.encode(body).length; + /** + * 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 (body instanceof URLSearchParams) { - return textEncoder.encode(body.toString()).length; + 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'); } - if (body instanceof FormData) { - const formDataStr = _serializeFormData(body); - return textEncoder.encode(formDataStr).length; - } + logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals); - if (body instanceof Blob) { - return body.size; - } + const session = loadOrCreateSession( + { + sessionIdleExpire: this.timeouts.sessionIdleExpire, + maxReplayDuration: this._options.maxReplayDuration, + traceInternals: this._options._experiments.traceInternals, + }, + { + stickySession: this._options.stickySession, + sessionSampleRate: 0, + allowBuffering: true, + }, + ); - if (body instanceof ArrayBuffer) { - return body.byteLength; - } + this.session = session; - // Currently unhandled types: ArrayBufferView, ReadableStream - } catch (e) { - // just return undefined + this.recordingMode = 'buffer'; + this._initializeRecording(); } - return undefined; -} + /** + * Start recording. + * + * Note that this will cause a new DOM checkout + */ + startRecording() { + try { + const canvasOptions = this._canvas; -/** Convert a Content-Length header to number/undefined. */ -function parseContentLengthHeader(header) { - if (!header) { - return undefined; + 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); + } } - const size = parseInt(header, 10); - return isNaN(size) ? undefined : size; -} + /** + * 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; + } -/** Get the string representation of a body. */ -function getBodyString(body) { - try { - if (typeof body === 'string') { - return [body]; + return true; + } catch (err) { + this._handleException(err); + return false; } + } - if (body instanceof URLSearchParams) { - return [body.toString()]; + /** + * 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; } - if (body instanceof FormData) { - return [_serializeFormData(body)]; - } + // 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; - if (!body) { - return [undefined]; + 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); } - } catch (e2) { - DEBUG_BUILD && utils.logger.warn('[Replay] Failed to serialize body', body); - return [undefined, 'BODY_PARSE_ERROR']; } - DEBUG_BUILD && utils.logger.info('[Replay] Skipping network body because of body type', body); + /** + * 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; + } - return [undefined, 'UNPARSEABLE_BODY_TYPE']; -} + this._isPaused = true; + this.stopRecording(); -/** Merge a warning into an existing network request/response. */ -function mergeWarning( - info, - warning, -) { - if (!info) { - return { - headers: {}, - size: undefined, - _meta: { - warnings: [warning], - }, - }; + logInfo('[Replay] Pausing replay', this._options._experiments.traceInternals); } - const newMeta = { ...info._meta }; - const existingWarnings = newMeta.warnings || []; - newMeta.warnings = [...existingWarnings, warning]; + /** + * Resumes recording, see notes for `pause(). + * + * Note that calling `startRecording()` here will cause a + * new DOM checkout.` + */ + resume() { + if (!this._isPaused || !this._checkSession()) { + return; + } - info._meta = newMeta; - return info; -} + this._isPaused = false; + this.startRecording(); -/** Convert ReplayNetworkRequestData to a PerformanceEntry. */ -function makeNetworkReplayBreadcrumb( - type, - data, -) { - if (!data) { - return null; + logInfo('[Replay] Resuming replay', this._options._experiments.traceInternals); } - const { startTimestamp, endTimestamp, url, method, statusCode, request, response } = data; + /** + * 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 result = { - type, - start: startTimestamp / 1000, - end: endTimestamp / 1000, - name: url, - data: utils.dropUndefinedKeys({ - method, - statusCode, - request, - response, - }), - }; + const activityTime = Date.now(); - return result; -} + logInfo('[Replay] Converting buffer to session', this._options._experiments.traceInternals); -/** 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'], - }, - }; -} + // 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(); -/** 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; - } + const hasStoppedRecording = this.stopRecording(); - if (!bodySize) { - return { - headers, - }; - } + if (!continueRecording || !hasStoppedRecording) { + return; + } - if (!body) { - return { - headers, - size: bodySize, - }; - } + // To avoid race conditions where this is called multiple times, we check here again that we are still buffering + if ((this.recordingMode ) === 'session') { + return; + } - const info = { - headers, - size: bodySize, - }; + // Re-start recording in session-mode + this.recordingMode = 'session'; - const { body: normalizedBody, warnings } = normalizeNetworkBody(body); - info.body = normalizedBody; - if (warnings && warnings.length > 0) { - info._meta = { - warnings, - }; + // Once this session ends, we do not want to refresh it + if (this.session) { + this._updateUserActivity(activityTime); + this._updateSessionActivity(activityTime); + this._maybeSaveSession(); + } + + this.startRecording(); } - return info; -} + /** + * 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(); -/** 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]; + // If this option is turned on then we will only want to call `flush` + // explicitly + if (this.recordingMode === 'buffer') { + return; } - 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 callback is true, we do not want to continue with flushing -- the + // caller will need to handle it. + if (cbResult === true) { + return; + } - { - if (!body || typeof body !== 'string') { - return { - body, - }; + // addUpdate is called quite frequently - use _debouncedFlush so that it + // respects the flush delays and does not flush immediately + this._debouncedFlush(); } - const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE; - const isProbablyJson = _strIsProbablyJson(body); + /** + * 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(); - if (exceedsSizeLimit) { - const truncatedBody = body.slice(0, NETWORK_BODY_MAX_SIZE); + // 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; + } - if (isProbablyJson) { - return { - body: truncatedBody, - warnings: ['MAYBE_JSON_TRUNCATED'], - }; + // Note: This will cause a new DOM checkout + this.resume(); + return; } - return { - body: `${truncatedBody}…`, - warnings: ['TEXT_TRUNCATED'], - }; - } + // Otherwise... recording was never suspended, continue as normalish + this.checkAndHandleExpiredSession(); - if (isProbablyJson) { - try { - const jsonBody = JSON.parse(body); - return { - body: jsonBody, - }; - } catch (e3) { - // fall back to just send the body as string - } + this._updateSessionActivity(); } - return { - body, - }; -} + /** + * 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(); + } -function _strIsProbablyJson(str) { - const first = str[0]; - const last = str[str.length - 1]; + /** + * Only flush if `this.recordingMode === 'session'` + */ + conditionalFlush() { + if (this.recordingMode === 'buffer') { + return Promise.resolve(); + } - // Simple check: If this does not start & end with {} or [], it's not JSON - return (first === '[' && last === ']') || (first === '{' && last === '}'); -} + return this.flushImmediate(); + } -/** Match an URL against a list of strings/Regex. */ -function urlMatches(url, urls) { - const fullUrl = getFullUrl(url); + /** + * Flush using debounce flush + */ + flush() { + return this._debouncedFlush() ; + } - return utils.stringMatchesSomePattern(fullUrl, urls); -} + /** + * 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() ; + } -/** 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; + /** + * Cancels queued up flushes. + */ + cancelFlush() { + this._debouncedFlush.cancel(); } - 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; + /** Get the current sesion (=replay) ID */ + getSessionId() { + return this.session && this.session.id; } - const fullUrl = fixedUrl.href; + /** + * 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; + } - // Remove trailing slashes, if they don't match the original URL - if (!url.endsWith('/') && fullUrl.endsWith('/')) { - return fullUrl.slice(0, -1); + // --- 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; } - return fullUrl; -} + /** + * 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}`; -/** - * Capture a fetch breadcrumb to a replay. - * This adds additional data (where approriate). - */ -async function captureFetchBreadcrumbToReplay( - breadcrumb, - hint, - options + this.performanceEntries = []; + this.replayPerformanceEntries = []; -, -) { - try { - const data = await _prepareFetchData(breadcrumb, hint, options); + // Reset _context as well + this._clearContext(); - // Create a replay performance entry from this breadcrumb - const result = makeNetworkReplayBreadcrumb('resource.fetch', data); - addNetworkBreadcrumb(options.replay, result); - } catch (error) { - DEBUG_BUILD && utils.logger.error('[Replay] Failed to capture fetch breadcrumb', error); + this._context.initialUrl = url; + this._context.initialTimestamp = Date.now(); + this._context.urls.push(url); } -} -/** - * 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; + /** + * 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); - const body = input ? _getFetchRequestArgBody(input) : undefined; - const reqSize = getBodySize(body, options.textEncoder); + // 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', + }); - const resSize = response ? parseContentLengthHeader(response.headers.get('content-length')) : undefined; + 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, + }, + }); + }); + } - if (reqSize !== undefined) { - breadcrumb.data.request_body_size = reqSize; - } - if (resSize !== undefined) { - breadcrumb.data.response_body_size = resSize; + return res; } -} - -async function _prepareFetchData( - breadcrumb, - hint, - options -, -) { - const now = Date.now(); - const { startTimestamp = now, endTimestamp = now } = hint; + /** + * 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 || core.getCurrentScope().getTransaction(); - const { - url, - method, - status_code: statusCode = 0, - request_body_size: requestBodySize, - response_body_size: responseBodySize, - } = breadcrumb.data; + const attributes = (lastTransaction && core.spanToJSON(lastTransaction).data) || {}; + const source = attributes[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + if (!lastTransaction || !source || !['route', 'custom'].includes(source)) { + return undefined; + } - const captureDetails = - urlMatches(url, options.networkDetailAllowUrls) && !urlMatches(url, options.networkDetailDenyUrls); + return core.spanToJSON(lastTransaction).description; + } - const request = captureDetails - ? _getRequestInfo(options, hint.input, requestBodySize) - : buildSkippedNetworkRequestOrResponse(requestBodySize); - const response = await _getResponseInfo(captureDetails, options, hint.response, responseBodySize); + /** + * Initialize and start all listeners to varying events (DOM, + * Performance Observer, Recording, Sentry SDK, etc) + */ + _initializeRecording() { + this.setInitialState(); - return { - startTimestamp, - endTimestamp, - url, - method, - statusCode, - request, - response, - }; -} + // this method is generally called on page load or manually - in both cases + // we should treat it as an activity + this._updateSessionActivity(); -function _getRequestInfo( - { networkCaptureBodies, networkRequestHeaders }, - input, - requestBodySize, -) { - const headers = input ? getRequestHeaders(input, networkRequestHeaders) : {}; + this.eventBuffer = createEventBuffer({ + useCompression: this._options.useCompression, + workerUrl: this._options.workerUrl, + }); - if (!networkCaptureBodies) { - return buildNetworkRequestOrResponse(headers, requestBodySize, undefined); - } + this._removeListeners(); + this._addListeners(); - // 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); + // 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; - if (warning) { - return mergeWarning(data, warning); + this.startRecording(); } - return data; -} + /** A wrapper to conditionally capture exceptions. */ + _handleException(error) { + DEBUG_BUILD && utils.logger.error('[Replay]', error); -/** Exported only for tests. */ -async function _getResponseInfo( - captureDetails, - { - networkCaptureBodies, - textEncoder, - networkResponseHeaders, + if (DEBUG_BUILD && this._options._experiments && this._options._experiments.captureExceptions) { + core.captureException(error); + } } -, - response, - responseBodySize, -) { - if (!captureDetails && responseBodySize !== undefined) { - return buildSkippedNetworkRequestOrResponse(responseBodySize); - } + /** + * 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 headers = response ? getAllHeaders(response.headers, networkResponseHeaders) : {}; + 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, + }, + ); - if (!response || (!networkCaptureBodies && responseBodySize !== undefined)) { - return buildNetworkRequestOrResponse(headers, responseBodySize, undefined); + this.session = session; } - const [bodyText, warning] = await _parseFetchResponseBody(response); - const result = getResponseData(bodyText, { - networkCaptureBodies, - textEncoder, - responseBodySize, - captureDetails, - headers, - }); + /** + * 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; + } - if (warning) { - return mergeWarning(result, warning); - } + const currentSession = this.session; - return result; -} + 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; + } -function getResponseData( - bodyText, - { - networkCaptureBodies, - textEncoder, - responseBodySize, - captureDetails, - headers, + return true; } -, -) { - try { - const size = - bodyText && bodyText.length && responseBodySize === undefined - ? getBodySize(bodyText, textEncoder) - : responseBodySize; - - if (!captureDetails) { - return buildSkippedNetworkRequestOrResponse(size); + /** + * 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); + } - if (networkCaptureBodies) { - return buildNetworkRequestOrResponse(headers, size, bodyText); - } + /** + * 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); - return buildNetworkRequestOrResponse(headers, size, undefined); - } catch (error) { - DEBUG_BUILD && utils.logger.warn('[Replay] Failed to serialize response body', error); - // fallback - return buildNetworkRequestOrResponse(headers, responseBodySize, undefined); - } -} + if (this.clickDetector) { + this.clickDetector.addListeners(); + } -async function _parseFetchResponseBody(response) { - const res = _tryCloneResponse(response); + // There is no way to remove these listeners, so ensure they are only added once + if (!this._hasInitializedCoreListeners) { + addGlobalListeners(this); - if (!res) { - return [undefined, 'BODY_PARSE_ERROR']; - } + this._hasInitializedCoreListeners = true; + } + } catch (err) { + this._handleException(err); + } - try { - const text = await _tryGetResponseText(res); - return [text]; - } catch (error) { - DEBUG_BUILD && utils.logger.warn('[Replay] Failed to get text body from response', error); - return [undefined, 'BODY_PARSE_ERROR']; + this._performanceCleanupCallback = setupPerformanceObserver(this); } -} -function _getFetchRequestArgBody(fetchArgs = []) { - // We only support getting the body from the fetch options - if (fetchArgs.length !== 2 || typeof fetchArgs[1] !== 'object') { - return undefined; - } + /** + * Cleans up listeners that were created in `_addListeners` + */ + _removeListeners() { + try { + WINDOW.document.removeEventListener('visibilitychange', this._handleVisibilityChange); - return (fetchArgs[1] ).body; -} + WINDOW.removeEventListener('blur', this._handleWindowBlur); + WINDOW.removeEventListener('focus', this._handleWindowFocus); + WINDOW.removeEventListener('keydown', this._handleKeyboardEvent); -function getAllHeaders(headers, allowedHeaders) { - const allHeaders = {}; + if (this.clickDetector) { + this.clickDetector.removeListeners(); + } - allowedHeaders.forEach(header => { - if (headers.get(header)) { - allHeaders[header] = headers.get(header) ; + if (this._performanceCleanupCallback) { + this._performanceCleanupCallback(); + } + } catch (err) { + this._handleException(err); } - }); + } - return allHeaders; -} + /** + * 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(); + } + };} -function getRequestHeaders(fetchArgs, allowedHeaders) { - if (fetchArgs.length === 1 && typeof fetchArgs[0] !== 'string') { - return getHeadersFromOptions(fetchArgs[0] , allowedHeaders); - } + /** + * Handle when page is blurred + */ + __init2() {this._handleWindowBlur = () => { + const breadcrumb = createBreadcrumb({ + category: 'ui.blur', + }); - if (fetchArgs.length === 2) { - return getHeadersFromOptions(fetchArgs[1] , allowedHeaders); - } + // Do not count blur as a user action -- it's part of the process of them + // leaving the page + this._doChangeToBackgroundTasks(breadcrumb); + };} - return {}; -} + /** + * Handle when page is focused + */ + __init3() {this._handleWindowFocus = () => { + const breadcrumb = createBreadcrumb({ + category: 'ui.focus', + }); -function getHeadersFromOptions( - input, - allowedHeaders, -) { - if (!input) { - return {}; - } + // Do not count focus as a user action -- instead wait until they focus and + // interactive with page + this._doChangeToForegroundTasks(breadcrumb); + };} - const headers = input.headers; + /** Ensure page remains active when a key is pressed. */ + __init4() {this._handleKeyboardEvent = (event) => { + handleKeyboardEvent(this, event); + };} - if (!headers) { - return {}; - } + /** + * Tasks to run when we consider a page to be hidden (via blurring and/or visibility) + */ + _doChangeToBackgroundTasks(breadcrumb) { + if (!this.session) { + return; + } - if (headers instanceof Headers) { - return getAllHeaders(headers, allowedHeaders); - } + const expired = isSessionExpired(this.session, { + maxReplayDuration: this._options.maxReplayDuration, + sessionIdleExpire: this.timeouts.sessionIdleExpire, + }); - // We do not support this, as it is not really documented (anymore?) - if (Array.isArray(headers)) { - return {}; - } + if (expired) { + return; + } - return getAllowedHeaders(headers, allowedHeaders); -} + if (breadcrumb) { + this._createCustomBreadcrumb(breadcrumb); + } -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 && utils.logger.warn('[Replay] Failed to clone response body', error); + // 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(); } -} -/** - * 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); + /** + * Tasks to run when we consider a page to be visible (via focus and/or visibility) + */ + _doChangeToForegroundTasks(breadcrumb) { + if (!this.session) { + return; + } - _getResponseText(response) - .then( - txt => resolve(txt), - reason => reject(reason), - ) - .finally(() => clearTimeout(timeout)); - }); -} + const isSessionActive = this.checkAndHandleExpiredSession(); -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(); -} + 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; + } -/** - * 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); + if (breadcrumb) { + this._createCustomBreadcrumb(breadcrumb); + } + } - // Create a replay performance entry from this breadcrumb - const result = makeNetworkReplayBreadcrumb('resource.xhr', data); - addNetworkBreadcrumb(options.replay, result); - } catch (error) { - DEBUG_BUILD && utils.logger.error('[Replay] Failed to capture xhr breadcrumb', error); + /** + * Update user activity (across session lifespans) + */ + _updateUserActivity(_lastActivity = Date.now()) { + this._lastActivity = _lastActivity; } -} -/** - * 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; + /** + * Updates the session's last activity timestamp + */ + _updateSessionActivity(_lastActivity = Date.now()) { + if (this.session) { + this.session.lastActivity = _lastActivity; + this._maybeSaveSession(); + } + } - if (!xhr) { - return; + /** + * 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, + }, + }); + }); } - const reqSize = getBodySize(input, options.textEncoder); - const resSize = xhr.getResponseHeader('content-length') - ? parseContentLengthHeader(xhr.getResponseHeader('content-length')) - : _getBodySize(xhr.response, xhr.responseType, options.textEncoder); + /** + * 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); - if (reqSize !== undefined) { - breadcrumb.data.request_body_size = reqSize; + this.performanceEntries = []; + this.replayPerformanceEntries = []; + + return Promise.all(createPerformanceSpans(this, performanceEntries)); } - if (resSize !== undefined) { - breadcrumb.data.response_body_size = resSize; + + /** + * Clear _context + */ + _clearContext() { + // XXX: `initialTimestamp` and `initialUrl` do not get cleared + this._context.errorIds.clear(); + this._context.traceIds.clear(); + this._context.urls = []; } -} -function _prepareXhrData( - breadcrumb, - hint, - options, -) { - const now = Date.now(); - const { startTimestamp = now, endTimestamp = now, input, xhr } = hint; + /** Update the initial timestamp based on the buffer content. */ + _updateInitialTimestampFromEventBuffer() { + const { session, eventBuffer } = this; + if (!session || !eventBuffer) { + return; + } - const { - url, - method, - status_code: statusCode = 0, - request_body_size: requestBodySize, - response_body_size: responseBodySize, - } = breadcrumb.data; + // we only ever update this on the initial segment + if (session.segmentId) { + return; + } - if (!url) { - return null; + const earliestEvent = eventBuffer.getEarliestTimestamp(); + if (earliestEvent && earliestEvent < this._context.initialTimestamp) { + this._context.initialTimestamp = earliestEvent; + } } - 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, + /** + * 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; } - const xhrInfo = xhr[utils.SENTRY_XHR_DATA_KEY]; - const networkRequestHeaders = xhrInfo - ? getAllowedHeaders(xhrInfo.request_headers, options.networkRequestHeaders) - : {}; - const networkResponseHeaders = getAllowedHeaders(getResponseHeaders(xhr), options.networkResponseHeaders); + /** + * 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(); - const [requestBody, requestWarning] = options.networkCaptureBodies ? getBodyString(input) : [undefined]; - const [responseBody, responseWarning] = options.networkCaptureBodies ? _getXhrResponseBody(xhr) : [undefined]; + if (!this.session || !this.eventBuffer || !replayId) { + DEBUG_BUILD && utils.logger.error('[Replay] No session or eventBuffer found to flush.'); + return; + } - const request = buildNetworkRequestOrResponse(networkRequestHeaders, requestBodySize, requestBody); - const response = buildNetworkRequestOrResponse(networkResponseHeaders, responseBodySize, responseBody); + await this._addPerformanceEntries(); - return { - startTimestamp, - endTimestamp, - url, - method, - statusCode, - request: requestWarning ? mergeWarning(request, requestWarning) : request, - response: responseWarning ? mergeWarning(response, responseWarning) : response, - }; -} + // Check eventBuffer again, as it could have been stopped in the meanwhile + if (!this.eventBuffer || !this.eventBuffer.hasEvents) { + return; + } -function getResponseHeaders(xhr) { - const headers = xhr.getAllResponseHeaders(); + // Only attach memory event if eventBuffer is not empty + await addMemoryEntry(this); - if (!headers) { - return {}; - } + // Check eventBuffer again, as it could have been stopped in the meanwhile + if (!this.eventBuffer) { + return; + } - return headers.split('\r\n').reduce((acc, line) => { - const [key, value] = line.split(': '); - acc[key.toLowerCase()] = value; - return acc; - }, {}); -} + // if this changed in the meanwhile, e.g. because the session was refreshed or similar, we abort here + if (replayId !== this.getSessionId()) { + return; + } -function _getXhrResponseBody(xhr) { - // We collect errors that happen, but only log them if we can't get any response body - const errors = []; + try { + // This uses the data from the eventBuffer, so we need to call this before `finish() + this._updateInitialTimestampFromEventBuffer(); - try { - return [xhr.responseText]; - } catch (e) { - errors.push(e); - } + const timestamp = Date.now(); - // Try to manually parse the response body, if responseText fails - try { - return _parseXhrResponse(xhr.response, xhr.responseType); - } catch (e) { - errors.push(e); - } + // 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'); + } - DEBUG_BUILD && utils.logger.warn('[Replay] Failed to get xhr response body', ...errors); + const eventContext = this._popEventContext(); + // Always increment segmentId regardless of outcome of sending replay + const segmentId = this.session.segmentId++; + this._maybeSaveSession(); - return [undefined]; -} + // Note this empties the event buffer regardless of outcome of sending replay + const recordingData = await this.eventBuffer.finish(); -/** - * 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]; + 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 = core.getClient(); + + if (client) { + client.recordDroppedEvent('send_error', 'replay'); + } } + } - if (body instanceof Document) { - return [body.body.outerHTML]; + /** + * 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 (responseType === 'json' && body && typeof body === 'object') { - return [JSON.stringify(body)]; + if (!this.checkAndHandleExpiredSession()) { + DEBUG_BUILD && utils.logger.error('[Replay] Attempting to finish replay event after session expired.'); + return; } - if (!body) { - return [undefined]; + if (!this.session) { + // should never happen, as we would have bailed out before + return; } - } catch (e2) { - DEBUG_BUILD && utils.logger.warn('[Replay] Failed to serialize body', body); - return [undefined, 'BODY_PARSE_ERROR']; - } - DEBUG_BUILD && utils.logger.info('[Replay] Skipping network body because of body type', body); + const start = this.session.started; + const now = Date.now(); + const duration = now - start; - return [undefined, 'UNPARSEABLE_BODY_TYPE']; -} + // A flush is about to happen, cancel any queued flushes + this._debouncedFlush.cancel(); -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; - } -} + // 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, + ); -/** - * 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 = core.getClient(); + if (tooShort) { + this._debouncedFlush(); + } + return; + } - try { - const textEncoder = new TextEncoder(); + 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? + } - const { - networkDetailAllowUrls, - networkDetailDenyUrls, - networkCaptureBodies, - networkRequestHeaders, - networkResponseHeaders, - } = replay.getOptions(); + // 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; + } - const options = { - replay, - textEncoder, - networkDetailAllowUrls, - networkDetailDenyUrls, - networkCaptureBodies, - networkRequestHeaders, - networkResponseHeaders, - }; + // 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. - if (client && client.on) { - client.on('beforeAddBreadcrumb', (breadcrumb, hint) => beforeAddNetworkBreadcrumb(options, breadcrumb, hint)); - } else { - // Fallback behavior - utils.addFetchInstrumentationHandler(handleFetchSpanListener(replay)); - utils.addXhrInstrumentationHandler(handleXhrSpanListener(replay)); + try { + await this._flushLock; + } catch (err) { + DEBUG_BUILD && utils.logger.error(err); + } finally { + this._debouncedFlush(); } - } catch (e2) { - // Do nothing - } -} + };} -/** just exported for tests */ -function beforeAddNetworkBreadcrumb( - options, - breadcrumb, - hint, -) { - if (!breadcrumb.data) { - return; + /** Save the session, if it is sticky */ + _maybeSaveSession() { + if (this.session && this._options.stickySession) { + saveSession(this.session); + } } - 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); + /** Handler for rrweb.record.onMutation */ + __init6() {this._onMutationHandler = (mutations) => { + const count = mutations.length; - // This call should not reject - // eslint-disable-next-line @typescript-eslint/no-floating-promises - captureXhrBreadcrumbToReplay(breadcrumb, hint, options); - } + const mutationLimit = this._options.mutationLimit; + const mutationBreadcrumbLimit = this._options.mutationBreadcrumbLimit; + const overMutationLimit = mutationLimit && count > mutationLimit; - 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); + // 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); + } - // This call should not reject + // Stop replay if over the mutation limit + if (overMutationLimit) { + // This should never reject // eslint-disable-next-line @typescript-eslint/no-floating-promises - captureFetchBreadcrumbToReplay(breadcrumb, hint, options); + this.stop({ reason: 'mutationLimit', forceFlush: this.recordingMode === 'session' }); + return false; } - } catch (e) { - DEBUG_BUILD && utils.logger.warn('Error when enriching network breadcrumb'); - } -} -function _isXhrBreadcrumb(breadcrumb) { - return breadcrumb.category === 'xhr'; + // `true` means we use the regular mutation handling by rrweb + return true; + };} } -function _isFetchBreadcrumb(breadcrumb) { - return breadcrumb.category === 'fetch'; -} +function getOption( + selectors, + defaultSelectors, + deprecatedClassOption, + deprecatedSelectorOption, +) { + const deprecatedSelectors = typeof deprecatedSelectorOption === 'string' ? deprecatedSelectorOption.split(',') : []; -function _isXhrHint(hint) { - return hint && hint.xhr; -} + const allSelectors = [ + ...selectors, + // @deprecated + ...deprecatedSelectors, -function _isFetchHint(hint) { - return hint && hint.response; -} + // sentry defaults + ...defaultSelectors, + ]; -let _LAST_BREADCRUMB = null; + // @deprecated + if (typeof deprecatedClassOption !== 'undefined') { + // NOTE: No support for RegExp + if (typeof deprecatedClassOption === 'string') { + allSelectors.push(`.${deprecatedClassOption}`); + } -function isBreadcrumbWithCategory(breadcrumb) { - return !!breadcrumb.category; + utils.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(','); } -const handleScopeListener = - (replay) => - (scope) => { - if (!replay.isEnabled()) { - return; - } +/** + * Returns privacy related configuration for use in rrweb + */ +function getPrivacyOptions({ + mask, + unmask, + block, + unblock, + ignore, - const result = handleScope(scope); + // 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="/"]']; - if (!result) { - return; - } + 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; + } - addBreadcrumbEvent(replay, result); - }; + return options; +} /** - * An event handler to handle scope changes. + * Masks an attribute if necessary, otherwise return attribute value as-is. */ -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; +function maskAttribute({ + el, + key, + maskAttributes, + maskAllText, + privacyOptions, + value, +}) { + // We only mask attributes if `maskAllText` is true + if (!maskAllText) { + return value; } - _LAST_BREADCRUMB = newBreadcrumb; + // unmaskTextSelector takes precendence + if (privacyOptions.unmaskTextSelector && el.matches(privacyOptions.unmaskTextSelector)) { + return value; + } if ( - !isBreadcrumbWithCategory(newBreadcrumb) || - ['fetch', 'xhr', 'sentry.event', 'sentry.transaction'].includes(newBreadcrumb.category) || - newBreadcrumb.category.startsWith('ui.') + 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 null; - } - - if (newBreadcrumb.category === 'console') { - return normalizeConsoleBreadcrumb(newBreadcrumb); + return value.replace(/[\S]/g, '*'); } - return createBreadcrumb(newBreadcrumb); + return value; } -/** exported for tests only */ -function normalizeConsoleBreadcrumb( - breadcrumb, -) { - const args = breadcrumb.data && breadcrumb.data.arguments; +const MEDIA_SELECTORS = + 'img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]'; - if (!Array.isArray(args) || args.length === 0) { - return createBreadcrumb(breadcrumb); - } +const DEFAULT_NETWORK_HEADERS = ['content-length', 'content-type', 'accept']; - let isTruncated = false; +let _initialized = 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)}…`; - } +const replayIntegration$1 = ((options) => { + // eslint-disable-next-line deprecation/deprecation + return new Replay$1(options); +}) ; - return arg; - } - if (typeof arg === 'object') { - try { - const normalizedArg = utils.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 - } - } +/** + * The main replay integration class, to be passed to `init({ integrations: [] })`. + * @deprecated Use `replayIntegration()` instead. + */ +class Replay$1 { + /** + * @inheritDoc + */ + static __initStatic() {this.id = 'Replay';} - return arg; - }); + /** + * @inheritDoc + */ - return createBreadcrumb({ - ...breadcrumb, - data: { - ...breadcrumb.data, - arguments: normalizedArgs, - ...(isTruncated ? { _meta: { warnings: ['CONSOLE_ARG_TRUNCATED'] } } : {}), - }, - }); -} + /** + * Options to pass to `rrweb.record()` + */ -/** - * Add global listeners that cannot be removed. - */ -function addGlobalListeners(replay) { - // Listeners from core SDK // - const scope = core.getCurrentScope(); - const client = core.getClient(); + /** + * 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 + */ - scope.addScopeListener(handleScopeListener(replay)); - utils.addClickKeypressInstrumentationHandler(handleDomListener(replay)); - utils.addHistoryInstrumentationHandler(handleHistorySpanListener(replay)); - handleNetworkBreadcrumbs(replay); + 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, - // 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 { - core.addEventProcessor(eventProcessor); - } + mutationBreadcrumbLimit = 750, + mutationLimit = 10000, - // 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; - } - } - }); + slowClickTimeout = 7000, + slowClickIgnoreSelectors = [], - client.on('startTransaction', transaction => { - replay.lastTransaction = transaction; - }); + networkDetailAllowUrls = [], + networkDetailDenyUrls = [], + networkCaptureBodies = true, + networkRequestHeaders = [], + networkResponseHeaders = [], - // 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; - }); + mask = [], + maskAttributes = ['title', 'placeholder'], + unmask = [], + block = [], + unblock = [], + ignore = [], + maskFn, - // 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 - if (feedbackEvent.contexts && feedbackEvent.contexts.feedback) { - feedbackEvent.contexts.feedback.replay_id = replayId; - } - } + 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$1.id; + + const privacyOptions = getPrivacyOptions({ + mask, + unmask, + block, + unblock, + ignore, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + ignoreClass, }); - } -} -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function hasHooks(client) { - return !!(client && client.on); -} + this._recordingOptions = { + maskAllInputs, + maskAllText, + maskInputOptions: { ...(maskInputOptions || {}), password: true }, + maskTextFn: maskFn, + maskInputFn: maskFn, + maskAttributeFn: (key, value, el) => + maskAttribute({ + maskAttributes, + maskAllText, + privacyOptions, + key, + value, + el, + }), -/** - * 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 []; - } -} + ...privacyOptions, -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, + // 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 + } }, - }, - }; -} + }; -/** - * 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; + 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, - let timerId; - let maxTimerId; + _experiments, + }; - const maxWait = options && options.maxWait ? Math.max(options.maxWait, wait) : 0; + 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} })`, + ); - function invokeFunc() { - cancelTimers(); - callbackReturnValue = func(); - return callbackReturnValue; - } + this._initialOptions.sessionSampleRate = sessionSampleRate; + } - function cancelTimers() { - timerId !== undefined && clearTimeout(timerId); - maxTimerId !== undefined && clearTimeout(maxTimerId); - timerId = maxTimerId = undefined; - } + 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} })`, + ); - function flush() { - if (timerId !== undefined || maxTimerId !== undefined) { - return invokeFunc(); + this._initialOptions.errorSampleRate = errorSampleRate; } - return callbackReturnValue; - } - function debounced() { - if (timerId) { - clearTimeout(timerId); + 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}`; } - timerId = setTimeout(invokeFunc, wait); - if (maxWait && maxTimerId === undefined) { - maxTimerId = setTimeout(invokeFunc, maxWait); + if (this._isInitialized && utils.isBrowser()) { + throw new Error('Multiple Sentry Session Replay instances are not supported'); } - return callbackReturnValue; + this._isInitialized = true; } - debounced.cancel = cancelTimers; - debounced.flush = flush; - return debounced; -} + /** If replay has already been initialized */ + get _isInitialized() { + return _initialized; + } -/** - * 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; + /** Update _isInitialized */ + set _isInitialized(value) { + _initialized = value; + } - 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 && utils.logger.warn('[Replay] Received replay event after session expired.'); + /** + * Setup and initialize replay container + */ + setupOnce() { + if (!utils.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; } - // `_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; + this._replay.start(); + } - if (replay.clickDetector) { - updateClickDetectorForRecordingEvent(replay.clickDetector, event); + /** + * Start replay buffering. Buffers until `flush()` is called or, if + * `replaysOnErrorSampleRate` > 0, until an error occurs. + */ + startBuffering() { + if (!this._replay) { + return; } - // 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(); - } + this._replay.startBuffering(); + } - // 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; - } + /** + * 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(); + } - // 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; - } + 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(); + } - // 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); + return this._replay.sendBufferedReplayOrFlush(options); + } - // 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; - } + /** + * Get the current session ID. + */ + getReplayId() { + if (!this._replay || !this._replay.isEnabled()) { + return; + } - // 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, - ); + return this._replay.getSessionId(); + } - replay.session.started = earliestEvent; + /** + * Initializes replay. + */ + _initialize() { + if (!this._replay) { + return; + } - if (replay.getOptions().stickySession) { - saveSession(replay.session); - } - } - } + // 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(); - 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._replay.initializeSampling(); + } - // This should never reject - // eslint-disable-next-line @typescript-eslint/no-floating-promises - void replay.flush(); - } + /** Setup the integration. */ + _setup() { + // Client is not available in constructor, so we need to wait until setupOnce + const finalOptions = loadReplayOptionsFromClient(this._initialOptions); - return true; + this._replay = new ReplayContainer({ + options: finalOptions, + recordingOptions: this._recordingOptions, }); - }; -} + } -/** - * 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, - }, - }, - }; -} + /** 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 = core.getClient(); + const canvasIntegration = client.getIntegrationByName('ReplayCanvas') -/** - * 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; +; + if (!canvasIntegration) { + return; + } + + this._replay['_canvas'] = canvasIntegration.getOptions(); + } catch (e) { + // ignore errors here + } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ } +}Replay$1.__initStatic(); - addEventSync(replay, createOptionsEvent(replay), false); -} +/** Parse Replay-related options from SDK options */ +function loadReplayOptionsFromClient(initialOptions) { + const client = core.getClient(); + const opt = client && (client.getOptions() ); -/** - * 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 utils.createEnvelope( - utils.createEventEnvelopeHeaders(replayEvent, utils.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, - ], - ], - ); -} + const finalOptions = { sessionSampleRate: 0, errorSampleRate: 0, ...utils.dropUndefinedKeys(initialOptions) }; -/** - * Prepare the recording data ready to be sent. - */ -function prepareRecordingData({ - recordingData, - headers, -} + if (!opt) { + utils.consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('SDK client is not available.'); + }); + return finalOptions; + } -) { - let payloadWithSequence; + 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 + ) { + utils.consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + 'Replay is disabled because neither `replaysSessionSampleRate` nor `replaysOnErrorSampleRate` are set.', + ); + }); + } - // XXX: newline is needed to separate sequence id from events - const replayHeaders = `${JSON.stringify(headers)} -`; + if (typeof opt.replaysSessionSampleRate === 'number') { + finalOptions.sessionSampleRate = opt.replaysSessionSampleRate; + } - 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); + if (typeof opt.replaysOnErrorSampleRate === 'number') { + finalOptions.errorSampleRate = opt.replaysOnErrorSampleRate; } - return payloadWithSequence; + return finalOptions; +} + +function _getMergedNetworkHeaders(headers) { + return [...DEFAULT_NETWORK_HEADERS, ...headers.map(header => header.toLowerCase())]; } /** - * Prepare a replay event & enrich it with the SDK metadata. + * This is a small utility to get a type-safe instance of the Replay integration. */ -async function prepareReplayEvent({ - client, - scope, - replayId: event_id, - event, +// eslint-disable-next-line deprecation/deprecation +function getReplay$1() { + const client = core.getClient(); + return ( + client && client.getIntegrationByName && client.getIntegrationByName('Replay') + ); } -) { - const integrations = - typeof client._integrations === 'object' && client._integrations !== null && !Array.isArray(client._integrations) - ? Object.keys(client._integrations) - : undefined; - - const eventHint = { event_id, integrations }; +// eslint-disable-next-line deprecation/deprecation - if (client.emit) { - client.emit('preprocessEvent', event, eventHint); - } +/** @deprecated Use the export from `@sentry/replay` or from framework-specific SDKs like `@sentry/react` or `@sentry/vue` */ +const getReplay = getReplay$1; - const preparedEvent = (await core.prepareEvent( - client.getOptions(), - event, - eventHint, - scope, - client, - core.getIsolationScope(), - )) ; +/** @deprecated Use the export from `@sentry/replay` or from framework-specific SDKs like `@sentry/react` or `@sentry/vue` */ +const replayIntegration = replayIntegration$1; - // If e.g. a global event processor returned null - if (!preparedEvent) { - return null; - } +/** @deprecated Use the export from `@sentry/replay` or from framework-specific SDKs like `@sentry/react` or `@sentry/vue` */ +// eslint-disable-next-line deprecation/deprecation +class Replay extends Replay$1 {} - // 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'; +exports.InternalReplay = Replay$1; +exports.Replay = Replay; +exports.getReplay = getReplay; +exports.internalGetReplay = getReplay$1; +exports.internalReplayIntegration = replayIntegration$1; +exports.replayIntegration = replayIntegration; - // 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', - }; +},{"@sentry-internal/tracing":29,"@sentry/core":70,"@sentry/utils":152}],133:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - return preparedEvent; -} +const is = require('./is.js'); +const string = require('./string.js'); /** - * Send replay attachment using `fetch()` + * Creates exceptions inside `event.exception.values` for errors that are nested on properties based on the `key` parameter. */ -async function sendReplayRequest({ - recordingData, - replayId, - segmentId: segment_id, - eventContext, - timestamp, - session, -}) { - const preparedRecordingData = prepareRecordingData({ - recordingData, - headers: { - segment_id, - }, - }); - - const { urls, errorIds, traceIds, initialTimestamp } = eventContext; +function applyAggregateErrorsToEvent( + exceptionFromErrorImplementation, + parser, + maxValueLimit = 250, + key, + limit, + event, + hint, +) { + if (!event.exception || !event.exception.values || !hint || !is.isInstanceOf(hint.originalException, Error)) { + return; + } - const client = core.getClient(); - const scope = core.getCurrentScope(); - const transport = client && client.getTransport(); - const dsn = client && client.getDsn(); + // Generally speaking the last item in `event.exception.values` is the exception originating from the original Error + const originalException = + event.exception.values.length > 0 ? event.exception.values[event.exception.values.length - 1] : undefined; - if (!client || !transport || !dsn || !session.sampled) { - return; + // We only create exception grouping if there is an exception in the event. + if (originalException) { + event.exception.values = truncateAggregateExceptions( + aggregateExceptionsFromError( + exceptionFromErrorImplementation, + parser, + limit, + hint.originalException , + key, + event.exception.values, + originalException, + 0, + ), + maxValueLimit, + ); } +} - 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, - }; +function aggregateExceptionsFromError( + exceptionFromErrorImplementation, + parser, + limit, + error, + key, + prevExceptions, + exception, + exceptionId, +) { + if (prevExceptions.length >= limit + 1) { + return prevExceptions; + } - const replayEvent = await prepareReplayEvent({ scope, client, replayId, event: baseEvent }); + let newExceptions = [...prevExceptions]; - 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; + // Recursively call this function in order to walk down a chain of errors + if (is.isInstanceOf(error[key], Error)) { + applyExceptionGroupFieldsForParentException(exception, exceptionId); + const newException = exceptionFromErrorImplementation(parser, error[key]); + const newExceptionId = newExceptions.length; + applyExceptionGroupFieldsForChildException(newException, key, newExceptionId, exceptionId); + newExceptions = aggregateExceptionsFromError( + exceptionFromErrorImplementation, + parser, + limit, + error[key], + key, + [newException, ...newExceptions], + newException, + newExceptionId, + ); } - /* - 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": { - }, + // This will create exception grouping for AggregateErrors + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError + if (Array.isArray(error.errors)) { + error.errors.forEach((childError, i) => { + if (is.isInstanceOf(childError, Error)) { + applyExceptionGroupFieldsForParentException(exception, exceptionId); + const newException = exceptionFromErrorImplementation(parser, childError); + const newExceptionId = newExceptions.length; + applyExceptionGroupFieldsForChildException(newException, `errors[${i}]`, newExceptionId, exceptionId); + newExceptions = aggregateExceptionsFromError( + exceptionFromErrorImplementation, + parser, + limit, + childError, + key, + [newException, ...newExceptions], + newException, + newExceptionId, + ); + } + }); } - */ - // 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; + return newExceptions; +} - const envelope = createReplayEnvelope(replayEvent, preparedRecordingData, dsn, client.getOptions().tunnel); +function applyExceptionGroupFieldsForParentException(exception, exceptionId) { + // Don't know if this default makes sense. The protocol requires us to set these values so we pick *some* default. + exception.mechanism = exception.mechanism || { type: 'generic', handled: true }; - let response; + exception.mechanism = { + ...exception.mechanism, + ...(exception.type === 'AggregateError' && { is_exception_group: true }), + exception_id: exceptionId, + }; +} - try { - response = await transport.send(envelope); - } catch (err) { - const error = new Error(UNABLE_TO_SEND_REPLAY); +function applyExceptionGroupFieldsForChildException( + exception, + source, + exceptionId, + parentId, +) { + // Don't know if this default makes sense. The protocol requires us to set these values so we pick *some* default. + exception.mechanism = exception.mechanism || { type: 'generic', handled: true }; - 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 + exception.mechanism = { + ...exception.mechanism, + type: 'chained', + source, + exception_id: exceptionId, + parent_id: parentId, + }; +} + +/** + * Truncate the message (exception.value) of all exceptions in the event. + * Because this event processor is ran after `applyClientOptions`, + * we need to truncate the message of the added exceptions here. + */ +function truncateAggregateExceptions(exceptions, maxValueLength) { + return exceptions.map(exception => { + if (exception.value) { + exception.value = string.truncate(exception.value, maxValueLength); } - throw error; - } + return exception; + }); +} - // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore - if (!response) { - return response; - } +exports.applyAggregateErrorsToEvent = applyAggregateErrorsToEvent; - // 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 = utils.updateRateLimits({}, response); - if (utils.isRateLimited(rateLimits, 'replay')) { - throw new RateLimitError(rateLimits); - } +},{"./is.js":162,"./string.js":178}],134:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - return response; -} +const object = require('./object.js'); +const nodeStackTrace = require('./node-stack-trace.js'); /** - * This error indicates that the transport returned an invalid status code. + * A node.js watchdog timer + * @param pollInterval The interval that we expect to get polled at + * @param anrThreshold The threshold for when we consider ANR + * @param callback The callback to call for ANR + * @returns An object with `poll` and `enabled` functions {@link WatchdogReturn} */ -class TransportStatusCodeError extends Error { - constructor(statusCode) { - super(`Transport returned status code ${statusCode}`); - } -} +function watchdogTimer( + createTimer, + pollInterval, + anrThreshold, + callback, +) { + const timer = createTimer(); + let triggered = false; + let enabled = true; -/** - * This error indicates that we hit a rate limit API error. - */ -class RateLimitError extends Error { + setInterval(() => { + const diffMs = timer.getTimeMs(); - constructor(rateLimits) { - super('Rate limit hit'); - this.rateLimits = rateLimits; - } + if (triggered === false && diffMs > pollInterval + anrThreshold) { + triggered = true; + if (enabled) { + callback(); + } + } + + if (diffMs < pollInterval + anrThreshold) { + triggered = false; + } + }, 20); + + return { + poll: () => { + timer.reset(); + }, + enabled: (state) => { + enabled = state; + }, + }; } +// types copied from inspector.d.ts + /** - * Finalize and send the current replay event to Sentry + * Converts Debugger.CallFrame to Sentry StackFrame */ -async function sendReplay( - replayData, - retryConfig = { - count: 0, - interval: RETRY_BASE_INTERVAL, - }, +function callFrameToStackFrame( + frame, + url, + getModuleFromFilename, ) { - const { recordingData, options } = replayData; + const filename = url ? url.replace(/^file:\/\//, '') : undefined; - // short circuit if there's no events to upload (this shouldn't happen as _runFlush makes this check) - if (!recordingData.length) { - return; - } + // CallFrame row/col are 0 based, whereas StackFrame are 1 based + const colno = frame.location.columnNumber ? frame.location.columnNumber + 1 : undefined; + const lineno = frame.location.lineNumber ? frame.location.lineNumber + 1 : undefined; - try { - await sendReplayRequest(replayData); - return true; - } catch (err) { - if (err instanceof TransportStatusCodeError || err instanceof RateLimitError) { - throw err; - } + return object.dropUndefinedKeys({ + filename, + module: getModuleFromFilename(filename), + function: frame.functionName || '?', + colno, + lineno, + in_app: filename ? nodeStackTrace.filenameIsInApp(filename) : undefined, + }); +} - // Capture error for every failed replay - core.setContext('Replays', { - _retryCount: retryConfig.count, - }); +exports.callFrameToStackFrame = callFrameToStackFrame; +exports.watchdogTimer = watchdogTimer; - if (DEBUG_BUILD && options._experiments && options._experiments.captureExceptions) { - core.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`); +},{"./node-stack-trace.js":168,"./object.js":171}],135:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - 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 - } +const debugBuild = require('./debug-build.js'); +const is = require('./is.js'); +const logger = require('./logger.js'); - throw error; - } +const BAGGAGE_HEADER_NAME = 'baggage'; - // will retry in intervals of 5, 10, 30 - retryConfig.interval *= ++retryConfig.count; +const SENTRY_BAGGAGE_KEY_PREFIX = 'sentry-'; - return new Promise((resolve, reject) => { - setTimeout(async () => { - try { - await sendReplay(replayData, retryConfig); - resolve(true); - } catch (err) { - reject(err); - } - }, retryConfig.interval); - }); - } -} +const SENTRY_BAGGAGE_KEY_PREFIX_REGEX = /^sentry-/; -const THROTTLED = '__THROTTLED'; -const SKIPPED = '__SKIPPED'; +/** + * Max length of a serialized baggage string + * + * https://www.w3.org/TR/baggage/#limits + */ +const MAX_BAGGAGE_STRING_LENGTH = 8192; /** - * 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`. + * Takes a baggage header and turns it into Dynamic Sampling Context, by extracting all the "sentry-" prefixed values + * from it. * - * Returns `THROTTLED` if throttled for the first time, after that `SKIPPED`, - * or else the return value of the original function. + * @param baggageHeader A very bread definition of a baggage header as it might appear in various frameworks. + * @returns The Dynamic Sampling Context that was found on `baggageHeader`, if there was any, `undefined` otherwise. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function throttle( - fn, - maxCount, - durationSeconds, +function baggageHeaderToDynamicSamplingContext( + // Very liberal definition of what any incoming header might look like + baggageHeader, ) { - const counter = new Map(); + if (!is.isString(baggageHeader) && !Array.isArray(baggageHeader)) { + return undefined; + } - const _cleanup = (now) => { - const threshold = now - durationSeconds; - counter.forEach((_value, key) => { - if (key < threshold) { - counter.delete(key); - } - }); - }; + // Intermediary object to store baggage key value pairs of incoming baggage headers on. + // It is later used to read Sentry-DSC-values from. + let baggageObject = {}; - const _getTotalCount = () => { - return [...counter.values()].reduce((a, b) => a + b, 0); - }; + if (Array.isArray(baggageHeader)) { + // Combine all baggage headers into one object containing the baggage values so we can later read the Sentry-DSC-values from it + baggageObject = baggageHeader.reduce((acc, curr) => { + const currBaggageObject = baggageHeaderToObject(curr); + for (const key of Object.keys(currBaggageObject)) { + acc[key] = currBaggageObject[key]; + } + return acc; + }, {}); + } else { + // Return undefined if baggage header is an empty string (technically an empty baggage header is not spec conform but + // this is how we choose to handle it) + if (!baggageHeader) { + return undefined; + } - let isThrottled = false; + baggageObject = baggageHeaderToObject(baggageHeader); + } - return (...rest) => { - // Date in second-precision, which we use as basis for the throttling - const now = Math.floor(Date.now() / 1000); + // Read all "sentry-" prefixed values out of the baggage object and put it onto a dynamic sampling context object. + const dynamicSamplingContext = Object.entries(baggageObject).reduce((acc, [key, value]) => { + if (key.match(SENTRY_BAGGAGE_KEY_PREFIX_REGEX)) { + const nonPrefixedKey = key.slice(SENTRY_BAGGAGE_KEY_PREFIX.length); + acc[nonPrefixedKey] = value; + } + return acc; + }, {}); - // First, make sure to delete any old entries - _cleanup(now); + // Only return a dynamic sampling context object if there are keys in it. + // A keyless object means there were no sentry values on the header, which means that there is no DSC. + if (Object.keys(dynamicSamplingContext).length > 0) { + return dynamicSamplingContext ; + } else { + return undefined; + } +} - // If already over limit, do nothing - if (_getTotalCount() >= maxCount) { - const wasThrottled = isThrottled; - isThrottled = true; - return wasThrottled ? SKIPPED : THROTTLED; - } +/** + * Turns a Dynamic Sampling Object into a baggage header by prefixing all the keys on the object with "sentry-". + * + * @param dynamicSamplingContext The Dynamic Sampling Context to turn into a header. For convenience and compatibility + * with the `getDynamicSamplingContext` method on the Transaction class ,this argument can also be `undefined`. If it is + * `undefined` the function will return `undefined`. + * @returns a baggage header, created from `dynamicSamplingContext`, or `undefined` either if `dynamicSamplingContext` + * was `undefined`, or if `dynamicSamplingContext` didn't contain any values. + */ +function dynamicSamplingContextToSentryBaggageHeader( + // this also takes undefined for convenience and bundle size in other places + dynamicSamplingContext, +) { + if (!dynamicSamplingContext) { + return undefined; + } - isThrottled = false; - const count = counter.get(now) || 0; - counter.set(now, count + 1); + // Prefix all DSC keys with "sentry-" and put them into a new object + const sentryPrefixedDSC = Object.entries(dynamicSamplingContext).reduce( + (acc, [dscKey, dscValue]) => { + if (dscValue) { + acc[`${SENTRY_BAGGAGE_KEY_PREFIX}${dscKey}`] = dscValue; + } + return acc; + }, + {}, + ); - return fn(...rest); - }; + return objectToBaggageHeader(sentryPrefixedDSC); } -/* eslint-disable max-lines */ // TODO: We might want to split this file up +/** + * Will parse a baggage header, which is a simple key-value map, into a flat object. + * + * @param baggageHeader The baggage header to parse. + * @returns a flat object containing all the key-value pairs from `baggageHeader`. + */ +function baggageHeaderToObject(baggageHeader) { + return baggageHeader + .split(',') + .map(baggageEntry => baggageEntry.split('=').map(keyOrValue => decodeURIComponent(keyOrValue.trim()))) + .reduce((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}); +} /** - * The main replay container class, which holds all the state and methods for recording and sending replays. + * Turns a flat object (key-value pairs) into a baggage header, which is also just key-value pairs. + * + * @param object The object to turn into a baggage header. + * @returns a baggage header string, or `undefined` if the object didn't have any values, since an empty baggage header + * is not spec compliant. */ -class ReplayContainer { +function objectToBaggageHeader(object) { + if (Object.keys(object).length === 0) { + // An empty baggage header is not spec compliant: We return undefined. + return undefined; + } - /** - * 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 - */ + return Object.entries(object).reduce((baggageHeader, [objectKey, objectValue], currentIndex) => { + const baggageEntry = `${encodeURIComponent(objectKey)}=${encodeURIComponent(objectValue)}`; + const newBaggageHeader = currentIndex === 0 ? baggageEntry : `${baggageHeader},${baggageEntry}`; + if (newBaggageHeader.length > MAX_BAGGAGE_STRING_LENGTH) { + debugBuild.DEBUG_BUILD && + logger.logger.warn( + `Not adding key: ${objectKey} with val: ${objectValue} to baggage header due to exceeding baggage size limits.`, + ); + return baggageHeader; + } else { + return newBaggageHeader; + } + }, ''); +} - /** - * The current or last active transcation. - * This is only available when performance is enabled. - */ +exports.BAGGAGE_HEADER_NAME = BAGGAGE_HEADER_NAME; +exports.MAX_BAGGAGE_STRING_LENGTH = MAX_BAGGAGE_STRING_LENGTH; +exports.SENTRY_BAGGAGE_KEY_PREFIX = SENTRY_BAGGAGE_KEY_PREFIX; +exports.SENTRY_BAGGAGE_KEY_PREFIX_REGEX = SENTRY_BAGGAGE_KEY_PREFIX_REGEX; +exports.baggageHeaderToDynamicSamplingContext = baggageHeaderToDynamicSamplingContext; +exports.dynamicSamplingContextToSentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader; - /** - * These are here so we can overwrite them in tests etc. - * @hidden - */ - /** - * Options to pass to `rrweb.record()` - */ +},{"./debug-build.js":146,"./is.js":162,"./logger.js":164}],136:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - /** - * Timestamp of the last user activity. This lives across sessions. - */ +const is = require('./is.js'); +const worldwide = require('./worldwide.js'); - /** - * Is the integration currently active? - */ +// eslint-disable-next-line deprecation/deprecation +const WINDOW = worldwide.getGlobalObject(); - /** - * Paused is a state where: - * - DOM Recording is not listening at all - * - Nothing will be added to event buffer (e.g. core SDK events) - */ +const DEFAULT_MAX_STRING_LENGTH = 80; - /** - * Have we attached listeners to the core SDK? - * Note we have to track this as there is no way to remove instrumentation handlers. - */ +/** + * Given a child DOM element, returns a query-selector statement describing that + * and its ancestors + * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz] + * @returns generated DOM path + */ +function htmlTreeAsString( + elem, + options = {}, +) { + if (!elem) { + return ''; + } - /** - * Function to stop recording - */ + // try/catch both: + // - accessing event.target (see getsentry/raven-js#838, #768) + // - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly + // - can throw an exception in some circumstances. + try { + let currentElem = elem ; + const MAX_TRAVERSE_HEIGHT = 5; + const out = []; + let height = 0; + let len = 0; + const separator = ' > '; + const sepLength = separator.length; + let nextStr; + const keyAttrs = Array.isArray(options) ? options : options.keyAttrs; + const maxStringLength = (!Array.isArray(options) && options.maxStringLength) || DEFAULT_MAX_STRING_LENGTH; - /** - * Internal use for canvas recording options - */ + while (currentElem && height++ < MAX_TRAVERSE_HEIGHT) { + nextStr = _htmlElementAsString(currentElem, keyAttrs); + // bail out if + // - nextStr is the 'html' element + // - the length of the string that would be created exceeds maxStringLength + // (ignore this limit if we are on the first iteration) + if (nextStr === 'html' || (height > 1 && len + out.length * sepLength + nextStr.length >= maxStringLength)) { + break; + } - constructor({ - options, - recordingOptions, - } + out.push(nextStr); -) {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: '', - }; + len += nextStr.length; + currentElem = currentElem.parentNode; + } - this._recordingOptions = recordingOptions; - this._options = options; + return out.reverse().join(separator); + } catch (_oO) { + return ''; + } +} - this._debouncedFlush = debounce(() => this._flush(), this._options.flushMinDelay, { - maxWait: this._options.flushMaxDelay, - }); +/** + * Returns a simple, query-selector representation of a DOM element + * e.g. [HTMLElement] => input#foo.btn[name=baz] + * @returns generated DOM path + */ +function _htmlElementAsString(el, keyAttrs) { + const elem = el - this._throttledAddEvent = throttle( - (event, isCheckout) => addEvent(this, event, isCheckout), - // Max 300 events... - 300, - // ... per 5s - 5, - ); +; - const { slowClickTimeout, slowClickIgnoreSelectors } = this.getOptions(); + const out = []; + let className; + let classes; + let key; + let attr; + let i; - const slowClickConfig = slowClickTimeout - ? { - threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout), - timeout: slowClickTimeout, - scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT, - ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '', - } - : undefined; + if (!elem || !elem.tagName) { + return ''; + } - if (slowClickConfig) { - this.clickDetector = new ClickDetector(this, slowClickConfig); + // @ts-expect-error WINDOW has HTMLElement + if (WINDOW.HTMLElement) { + // If using the component name annotation plugin, this value may be available on the DOM node + if (elem instanceof HTMLElement && elem.dataset && elem.dataset['sentryComponent']) { + return elem.dataset['sentryComponent']; } } - /** Get the event context. */ - getContext() { - return this._context; - } + out.push(elem.tagName.toLowerCase()); - /** If recording is currently enabled. */ - isEnabled() { - return this._isEnabled; + // Pairs of attribute keys defined in `serializeAttribute` and their values on element. + const keyAttrPairs = + keyAttrs && keyAttrs.length + ? keyAttrs.filter(keyAttr => elem.getAttribute(keyAttr)).map(keyAttr => [keyAttr, elem.getAttribute(keyAttr)]) + : null; + + if (keyAttrPairs && keyAttrPairs.length) { + keyAttrPairs.forEach(keyAttrPair => { + out.push(`[${keyAttrPair[0]}="${keyAttrPair[1]}"]`); + }); + } else { + if (elem.id) { + out.push(`#${elem.id}`); + } + + // eslint-disable-next-line prefer-const + className = elem.className; + if (className && is.isString(className)) { + classes = className.split(/\s+/); + for (i = 0; i < classes.length; i++) { + out.push(`.${classes[i]}`); + } + } + } + const allowedAttrs = ['aria-label', 'type', 'name', 'title', 'alt']; + for (i = 0; i < allowedAttrs.length; i++) { + key = allowedAttrs[i]; + attr = elem.getAttribute(key); + if (attr) { + out.push(`[${key}="${attr}"]`); + } } + return out.join(''); +} - /** If recording is currently paused. */ - isPaused() { - return this._isPaused; +/** + * A safe form of location.href + */ +function getLocationHref() { + try { + return WINDOW.document.location.href; + } catch (oO) { + return ''; } +} - /** - * Determine if canvas recording is enabled - */ - isRecordingCanvas() { - return Boolean(this._canvas); +/** + * Gets a DOM element by using document.querySelector. + * + * This wrapper will first check for the existance of the function before + * actually calling it so that we don't have to take care of this check, + * every time we want to access the DOM. + * + * Reason: DOM/querySelector is not available in all environments. + * + * We have to cast to any because utils can be consumed by a variety of environments, + * and we don't want to break TS users. If you know what element will be selected by + * `document.querySelector`, specify it as part of the generic call. For example, + * `const element = getDomElement('selector');` + * + * @param selector the selector string passed on to document.querySelector + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function getDomElement(selector) { + if (WINDOW.document && WINDOW.document.querySelector) { + return WINDOW.document.querySelector(selector) ; } + return null; +} - /** Get the replay integration options. */ - getOptions() { - return this._options; +/** + * Given a DOM element, traverses up the tree until it finds the first ancestor node + * that has the `data-sentry-component` attribute. This attribute is added at build-time + * by projects that have the component name annotation plugin installed. + * + * @returns a string representation of the component for the provided DOM element, or `null` if not found + */ +function getComponentName(elem) { + // @ts-expect-error WINDOW has HTMLElement + if (!WINDOW.HTMLElement) { + return null; } - /** - * Initializes the plugin based on sampling configuration. Should not be - * called outside of constructor. - */ - initializeSampling(previousSessionId) { - const { errorSampleRate, sessionSampleRate } = this._options; + let currentElem = elem ; + const MAX_TRAVERSE_HEIGHT = 5; + for (let i = 0; i < MAX_TRAVERSE_HEIGHT; i++) { + if (!currentElem) { + return null; + } - // 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; + if (currentElem instanceof HTMLElement && currentElem.dataset['sentryComponent']) { + return currentElem.dataset['sentryComponent']; } - // Otherwise if there is _any_ sample rate set, try to load an existing - // session, or create a new one. - this._initializeSessionForSampling(previousSessionId); + currentElem = currentElem.parentNode; + } - if (!this.session) { - // This should not happen, something wrong has occurred - this._handleException(new Error('Unable to initialize and create session')); - return; - } + return null; +} - 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; - } +exports.getComponentName = getComponentName; +exports.getDomElement = getDomElement; +exports.getLocationHref = getLocationHref; +exports.htmlTreeAsString = htmlTreeAsString; - // 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, - ); +},{"./is.js":162,"./worldwide.js":187}],137:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - this._initializeRecording(); - } +const _nullishCoalesce = require('./_nullishCoalesce.js'); - /** - * 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'); - } +// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f - if (this._isEnabled && this.recordingMode === 'buffer') { - throw new Error('Replay buffering is in progress, call `flush()` to save the replay'); - } +/** + * Polyfill for the nullish coalescing operator (`??`), when used in situations where at least one of the values is the + * result of an async operation. + * + * Note that the RHS is wrapped in a function so that if it's a computed value, that evaluation won't happen unless the + * LHS evaluates to a nullish value, to mimic the operator's short-circuiting behavior. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) + * + * @param lhs The value of the expression to the left of the `??` + * @param rhsFn A function returning the value of the expression to the right of the `??` + * @returns The LHS value, unless it's `null` or `undefined`, in which case, the RHS value + */ +async function _asyncNullishCoalesce(lhs, rhsFn) { + return _nullishCoalesce._nullishCoalesce(lhs, rhsFn); +} - logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals); +// Sucrase version: +// async function _asyncNullishCoalesce(lhs, rhsFn) { +// if (lhs != null) { +// return lhs; +// } else { +// return await rhsFn(); +// } +// } - 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, - }, - ); +exports._asyncNullishCoalesce = _asyncNullishCoalesce; - this.session = session; - this._initializeRecording(); - } +},{"./_nullishCoalesce.js":140}],138:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - /** - * 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'); +/** + * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, + * descriptors, and functions, for situations in which at least one part of the expression is async. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See + * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 + * + * @param ops Array result of expression conversion + * @returns The value of the expression + */ +async function _asyncOptionalChain(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; + // by checking for loose equality to `null`, we catch both `null` and `undefined` + if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { + // really we're meaning to return `undefined` as an actual value here, but it saves bytes not to write it + return; + } + if (op === 'access' || op === 'optionalAccess') { + lastAccessLHS = value; + value = await fn(value); + } else if (op === 'call' || op === 'optionalCall') { + value = await fn((...args) => (value ).call(lastAccessLHS, ...args)); + lastAccessLHS = undefined; } + } + return value; +} - logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals); +// Sucrase version: +// async function _asyncOptionalChain(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 = await fn(value); +// } else if (op === 'call' || op === 'optionalCall') { +// value = await fn((...args) => value.call(lastAccessLHS, ...args)); +// lastAccessLHS = undefined; +// } +// } +// return value; +// } - const session = loadOrCreateSession( - { - sessionIdleExpire: this.timeouts.sessionIdleExpire, - maxReplayDuration: this._options.maxReplayDuration, - traceInternals: this._options._experiments.traceInternals, - }, - { - stickySession: this._options.stickySession, - sessionSampleRate: 0, - allowBuffering: true, - }, - ); +exports._asyncOptionalChain = _asyncOptionalChain; - this.session = session; - this.recordingMode = 'buffer'; - this._initializeRecording(); - } +},{}],139:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - /** - * Start recording. - * - * Note that this will cause a new DOM checkout - */ - startRecording() { - try { - const canvasOptions = this._canvas; +const _asyncOptionalChain = require('./_asyncOptionalChain.js'); - 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); - } - } +// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f - /** - * 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; - } +/** + * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, + * descriptors, and functions, in cases where the value of the expression is to be deleted. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See + * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 + * + * @param ops Array result of expression conversion + * @returns The return value of the `delete` operator: `true`, unless the deletion target is an own, non-configurable + * property (one which can't be deleted or turned into an accessor, and whose enumerability can't be changed), in which + * case `false`. + */ +async function _asyncOptionalChainDelete(ops) { + const result = (await _asyncOptionalChain._asyncOptionalChain(ops)) ; + // If `result` is `null`, it means we didn't get to the end of the chain and so nothing was deleted (in which case, + // return `true` since that's what `delete` does when it no-ops). If it's non-null, we know the delete happened, in + // which case we return whatever the `delete` returned, which will be a boolean. + return result == null ? true : (result ); +} - return true; - } catch (err) { - this._handleException(err); - return false; - } - } +// Sucrase version: +// async function asyncOptionalChainDelete(ops) { +// const result = await ASYNC_OPTIONAL_CHAIN_NAME(ops); +// return result == null ? true : result; +// } - /** - * 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; - } +exports._asyncOptionalChainDelete = _asyncOptionalChainDelete; - // 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, - ); +},{"./_asyncOptionalChain.js":138}],140:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - this._removeListeners(); - this.stopRecording(); +// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f +// +// The MIT License (MIT) +// +// Copyright (c) 2012-2018 various contributors (see AUTHORS) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. - 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 }); - } +/** + * Polyfill for the nullish coalescing operator (`??`). + * + * Note that the RHS is wrapped in a function so that if it's a computed value, that evaluation won't happen unless the + * LHS evaluates to a nullish value, to mimic the operator's short-circuiting behavior. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) + * + * @param lhs The value of the expression to the left of the `??` + * @param rhsFn A function returning the value of the expression to the right of the `??` + * @returns The LHS value, unless it's `null` or `undefined`, in which case, the RHS value + */ +function _nullishCoalesce(lhs, rhsFn) { + // by checking for loose equality to `null`, we catch both `null` and `undefined` + return lhs != null ? lhs : rhsFn(); +} - // After flush, destroy event buffer - this.eventBuffer && this.eventBuffer.destroy(); - this.eventBuffer = null; +// Sucrase version: +// function _nullishCoalesce(lhs, rhsFn) { +// if (lhs != null) { +// return lhs; +// } else { +// return rhsFn(); +// } +// } - // 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); - } - } +exports._nullishCoalesce = _nullishCoalesce; - /** - * 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) { + +},{}],141:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +/** + * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, + * descriptors, and functions. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) + * See https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 + * + * @param ops Array result of expression conversion + * @returns The value of the expression + */ +function _optionalChain(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; + // by checking for loose equality to `null`, we catch both `null` and `undefined` + if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { + // really we're meaning to return `undefined` as an actual value here, but it saves bytes not to write it return; } + 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; +} - this._isPaused = true; - this.stopRecording(); +// Sucrase version +// function _optionalChain(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; +// } - logInfo('[Replay] Pausing replay', this._options._experiments.traceInternals); - } +exports._optionalChain = _optionalChain; - /** - * 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(); +},{}],142:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - logInfo('[Replay] Resuming replay', this._options._experiments.traceInternals); - } +const _optionalChain = require('./_optionalChain.js'); - /** - * 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(); - } +// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f - const activityTime = Date.now(); +/** + * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, + * descriptors, and functions, in cases where the value of the expression is to be deleted. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See + * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 + * + * @param ops Array result of expression conversion + * @returns The return value of the `delete` operator: `true`, unless the deletion target is an own, non-configurable + * property (one which can't be deleted or turned into an accessor, and whose enumerability can't be changed), in which + * case `false`. + */ +function _optionalChainDelete(ops) { + const result = _optionalChain._optionalChain(ops) ; + // If `result` is `null`, it means we didn't get to the end of the chain and so nothing was deleted (in which case, + // return `true` since that's what `delete` does when it no-ops). If it's non-null, we know the delete happened, in + // which case we return whatever the `delete` returned, which will be a boolean. + return result == null ? true : result; +} - logInfo('[Replay] Converting buffer to session', this._options._experiments.traceInternals); +// Sucrase version: +// function _optionalChainDelete(ops) { +// const result = _optionalChain(ops); +// // by checking for loose equality to `null`, we catch both `null` and `undefined` +// return result == null ? true : result; +// } - // 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(); +exports._optionalChainDelete = _optionalChainDelete; - const hasStoppedRecording = this.stopRecording(); - if (!continueRecording || !hasStoppedRecording) { - return; - } +},{"./_optionalChain.js":141}],143:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - // To avoid race conditions where this is called multiple times, we check here again that we are still buffering - if ((this.recordingMode ) === 'session') { - return; - } +/** + * Creates a cache that evicts keys in fifo order + * @param size {Number} + */ +function makeFifoCache( + size, +) - // Re-start recording in session-mode - this.recordingMode = 'session'; + { + // Maintain a fifo queue of keys, we cannot rely on Object.keys as the browser may not support it. + let evictionOrder = []; + let cache = {}; - // Once this session ends, we do not want to refresh it - if (this.session) { - this._updateUserActivity(activityTime); - this._updateSessionActivity(activityTime); - this._maybeSaveSession(); - } + return { + add(key, value) { + while (evictionOrder.length >= size) { + // shift is O(n) but this is small size and only happens if we are + // exceeding the cache size so it should be fine. + const evictCandidate = evictionOrder.shift(); - this.startRecording(); - } + if (evictCandidate !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete cache[evictCandidate]; + } + } - /** - * 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(); + // in case we have a collision, delete the old key. + if (cache[key]) { + this.delete(key); + } - // If this option is turned on then we will only want to call `flush` - // explicitly - if (this.recordingMode === 'buffer') { - return; - } + evictionOrder.push(key); + cache[key] = value; + }, + clear() { + cache = {}; + evictionOrder = []; + }, + get(key) { + return cache[key]; + }, + size() { + return evictionOrder.length; + }, + // Delete cache key and return true if it existed, false otherwise. + delete(key) { + if (!cache[key]) { + return false; + } - // If callback is true, we do not want to continue with flushing -- the - // caller will need to handle it. - if (cbResult === true) { - return; - } + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete cache[key]; - // addUpdate is called quite frequently - use _debouncedFlush so that it - // respects the flush delays and does not flush immediately - this._debouncedFlush(); - } + for (let i = 0; i < evictionOrder.length; i++) { + if (evictionOrder[i] === key) { + evictionOrder.splice(i, 1); + break; + } + } - /** - * 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(); + return true; + }, + }; +} - // 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; - } +exports.makeFifoCache = makeFifoCache; - // Note: This will cause a new DOM checkout - this.resume(); - return; - } - // Otherwise... recording was never suspended, continue as normalish - this.checkAndHandleExpiredSession(); +},{}],144:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - this._updateSessionActivity(); - } +const envelope = require('./envelope.js'); +const time = require('./time.js'); - /** - * 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(); - } +/** + * Creates client report envelope + * @param discarded_events An array of discard events + * @param dsn A DSN that can be set on the header. Optional. + */ +function createClientReportEnvelope( + discarded_events, + dsn, + timestamp, +) { + const clientReportItem = [ + { type: 'client_report' }, + { + timestamp: timestamp || time.dateTimestampInSeconds(), + discarded_events, + }, + ]; + return envelope.createEnvelope(dsn ? { dsn } : {}, [clientReportItem]); +} - /** - * Only flush if `this.recordingMode === 'session'` - */ - conditionalFlush() { - if (this.recordingMode === 'buffer') { - return Promise.resolve(); - } +exports.createClientReportEnvelope = createClientReportEnvelope; - return this.flushImmediate(); - } - /** - * Flush using debounce flush - */ - flush() { - return this._debouncedFlush() ; - } +},{"./envelope.js":149,"./time.js":181}],145:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - /** - * 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() ; - } +/** + * This code was originally copied from the 'cookie` module at v0.5.0 and was simplified for our use case. + * https://github.com/jshttp/cookie/blob/a0c84147aab6266bdb3996cf4062e93907c0b0fc/index.js + * It had the following license: + * + * (The MIT License) + * + * Copyright (c) 2012-2014 Roman Shtylman + * Copyright (c) 2015 Douglas Christopher Wilson + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * 'Software'), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ - /** - * Cancels queued up flushes. - */ - cancelFlush() { - this._debouncedFlush.cancel(); - } +/** + * Parses a cookie string + */ +function parseCookie(str) { + const obj = {}; + let index = 0; - /** Get the current sesion (=replay) ID */ - getSessionId() { - return this.session && this.session.id; - } + while (index < str.length) { + const eqIdx = str.indexOf('=', index); - /** - * 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; + // no more cookie pairs + if (eqIdx === -1) { + break; } - // --- 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; + let endIdx = str.indexOf(';', index); + + if (endIdx === -1) { + endIdx = str.length; + } else if (endIdx < eqIdx) { + // backtrack on prior semicolon + index = str.lastIndexOf(';', eqIdx - 1) + 1; + continue; } - return true; - } + const key = str.slice(index, eqIdx).trim(); - /** - * 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}`; + // only assign once + if (undefined === obj[key]) { + let val = str.slice(eqIdx + 1, endIdx).trim(); - this.performanceEntries = []; - this.replayPerformanceEntries = []; + // quoted values + if (val.charCodeAt(0) === 0x22) { + val = val.slice(1, -1); + } - // Reset _context as well - this._clearContext(); + try { + obj[key] = val.indexOf('%') !== -1 ? decodeURIComponent(val) : val; + } catch (e) { + obj[key] = val; + } + } - this._context.initialUrl = url; - this._context.initialTimestamp = Date.now(); - this._context.urls.push(url); + index = endIdx + 1; } - /** - * 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); + return obj; +} - // 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', - }); +exports.parseCookie = parseCookie; - 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; - } +},{}],146:[function(require,module,exports){ +arguments[4][26][0].apply(exports,arguments) +},{"dup":26}],147:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - /** - * 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 || core.getCurrentScope().getTransaction(); +const debugBuild = require('./debug-build.js'); +const logger = require('./logger.js'); - const attributes = (lastTransaction && core.spanToJSON(lastTransaction).data) || {}; - const source = attributes[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; - if (!lastTransaction || !source || !['route', 'custom'].includes(source)) { - return undefined; - } +/** Regular expression used to parse a Dsn. */ +const DSN_REGEX = /^(?:(\w+):)\/\/(?:(\w+)(?::(\w+)?)?@)([\w.-]+)(?::(\d+))?\/(.+)/; - return core.spanToJSON(lastTransaction).description; - } +function isValidProtocol(protocol) { + return protocol === 'http' || protocol === 'https'; +} - /** - * Initialize and start all listeners to varying events (DOM, - * Performance Observer, Recording, Sentry SDK, etc) - */ - _initializeRecording() { - this.setInitialState(); +/** + * Renders the string representation of this Dsn. + * + * By default, this will render the public representation without the password + * component. To get the deprecated private representation, set `withPassword` + * to true. + * + * @param withPassword When set to true, the password will be included. + */ +function dsnToString(dsn, withPassword = false) { + const { host, path, pass, port, projectId, protocol, publicKey } = dsn; + return ( + `${protocol}://${publicKey}${withPassword && pass ? `:${pass}` : ''}` + + `@${host}${port ? `:${port}` : ''}/${path ? `${path}/` : path}${projectId}` + ); +} - // this method is generally called on page load or manually - in both cases - // we should treat it as an activity - this._updateSessionActivity(); +/** + * Parses a Dsn from a given string. + * + * @param str A Dsn as string + * @returns Dsn as DsnComponents or undefined if @param str is not a valid DSN string + */ +function dsnFromString(str) { + const match = DSN_REGEX.exec(str); - this.eventBuffer = createEventBuffer({ - useCompression: this._options.useCompression, - workerUrl: this._options.workerUrl, + if (!match) { + // This should be logged to the console + logger.consoleSandbox(() => { + // eslint-disable-next-line no-console + console.error(`Invalid Sentry Dsn: ${str}`); }); + return undefined; + } - 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; + const [protocol, publicKey, pass = '', host, port = '', lastPath] = match.slice(1); + let path = ''; + let projectId = lastPath; - this.startRecording(); + const split = projectId.split('/'); + if (split.length > 1) { + path = split.slice(0, -1).join('/'); + projectId = split.pop() ; } - /** A wrapper to conditionally capture exceptions. */ - _handleException(error) { - DEBUG_BUILD && utils.logger.error('[Replay]', error); - - if (DEBUG_BUILD && this._options._experiments && this._options._experiments.captureExceptions) { - core.captureException(error); + if (projectId) { + const projectMatch = projectId.match(/^\d+/); + if (projectMatch) { + projectId = projectMatch[0]; } } - /** - * 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; + return dsnFromComponents({ host, pass, path, projectId, port, protocol: protocol , publicKey }); +} - 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, - }, - ); +function dsnFromComponents(components) { + return { + protocol: components.protocol, + publicKey: components.publicKey || '', + pass: components.pass || '', + host: components.host, + port: components.port || '', + path: components.path || '', + projectId: components.projectId, + }; +} - this.session = session; +function validateDsn(dsn) { + if (!debugBuild.DEBUG_BUILD) { + return true; } - /** - * 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; + const { port, projectId, protocol } = dsn; - 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; + const requiredComponents = ['protocol', 'publicKey', 'host', 'projectId']; + const hasMissingRequiredComponent = requiredComponents.find(component => { + if (!dsn[component]) { + logger.logger.error(`Invalid Sentry Dsn: ${component} missing`); + return true; } + return false; + }); - return true; + if (hasMissingRequiredComponent) { + return false; } - /** - * 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); + if (!projectId.match(/^\d+$/)) { + logger.logger.error(`Invalid Sentry Dsn: Invalid projectId ${projectId}`); + return false; } - /** - * 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(); - } + if (!isValidProtocol(protocol)) { + logger.logger.error(`Invalid Sentry Dsn: Invalid protocol ${protocol}`); + return false; + } - // There is no way to remove these listeners, so ensure they are only added once - if (!this._hasInitializedCoreListeners) { - addGlobalListeners(this); + if (port && isNaN(parseInt(port, 10))) { + logger.logger.error(`Invalid Sentry Dsn: Invalid port ${port}`); + return false; + } - this._hasInitializedCoreListeners = true; - } - } catch (err) { - this._handleException(err); - } + return true; +} - this._performanceCleanupCallback = setupPerformanceObserver(this); +/** + * Creates a valid Sentry Dsn object, identifying a Sentry instance and project. + * @returns a valid DsnComponents object or `undefined` if @param from is an invalid DSN source + */ +function makeDsn(from) { + const components = typeof from === 'string' ? dsnFromString(from) : dsnFromComponents(from); + if (!components || !validateDsn(components)) { + return undefined; } + return components; +} - /** - * Cleans up listeners that were created in `_addListeners` - */ - _removeListeners() { - try { - WINDOW.document.removeEventListener('visibilitychange', this._handleVisibilityChange); +exports.dsnFromString = dsnFromString; +exports.dsnToString = dsnToString; +exports.makeDsn = makeDsn; - WINDOW.removeEventListener('blur', this._handleWindowBlur); - WINDOW.removeEventListener('focus', this._handleWindowFocus); - WINDOW.removeEventListener('keydown', this._handleKeyboardEvent); - if (this.clickDetector) { - this.clickDetector.removeListeners(); - } +},{"./debug-build.js":146,"./logger.js":164}],148:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - if (this._performanceCleanupCallback) { - this._performanceCleanupCallback(); - } - } catch (err) { - this._handleException(err); - } - } +/* + * This module exists for optimizations in the build process through rollup and terser. We define some global + * constants, which can be overridden during build. By guarding certain pieces of code with functions that return these + * constants, we can control whether or not they appear in the final bundle. (Any code guarded by a false condition will + * never run, and will hence be dropped during treeshaking.) The two primary uses for this are stripping out calls to + * `logger` and preventing node-related code from appearing in browser bundles. + * + * Attention: + * This file should not be used to define constants/flags that are intended to be used for tree-shaking conducted by + * users. These flags should live in their respective packages, as we identified user tooling (specifically webpack) + * having issues tree-shaking these constants across package boundaries. + * An example for this is the __SENTRY_DEBUG__ constant. It is declared in each package individually because we want + * users to be able to shake away expressions that it guards. + */ - /** - * 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(); - } - };} +/** + * Figures out if we're building a browser bundle. + * + * @returns true if this is a browser bundle build. + */ +function isBrowserBundle() { + return typeof __SENTRY_BROWSER_BUNDLE__ !== 'undefined' && !!__SENTRY_BROWSER_BUNDLE__; +} - /** - * Handle when page is blurred - */ - __init2() {this._handleWindowBlur = () => { - const breadcrumb = createBreadcrumb({ - category: 'ui.blur', - }); +/** + * Get source of SDK. + */ +function getSDKSource() { + // @ts-expect-error "npm" is injected by rollup during build process + return "npm"; +} - // Do not count blur as a user action -- it's part of the process of them - // leaving the page - this._doChangeToBackgroundTasks(breadcrumb); - };} +exports.getSDKSource = getSDKSource; +exports.isBrowserBundle = isBrowserBundle; - /** - * 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); - };} +},{}],149:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - /** Ensure page remains active when a key is pressed. */ - __init4() {this._handleKeyboardEvent = (event) => { - handleKeyboardEvent(this, event); - };} +const dsn = require('./dsn.js'); +const normalize = require('./normalize.js'); +const object = require('./object.js'); - /** - * Tasks to run when we consider a page to be hidden (via blurring and/or visibility) - */ - _doChangeToBackgroundTasks(breadcrumb) { - if (!this.session) { - return; - } +/** + * Creates an envelope. + * Make sure to always explicitly provide the generic to this function + * so that the envelope types resolve correctly. + */ +function createEnvelope(headers, items = []) { + return [headers, items] ; +} - const expired = isSessionExpired(this.session, { - maxReplayDuration: this._options.maxReplayDuration, - sessionIdleExpire: this.timeouts.sessionIdleExpire, - }); +/** + * Add an item to an envelope. + * Make sure to always explicitly provide the generic to this function + * so that the envelope types resolve correctly. + */ +function addItemToEnvelope(envelope, newItem) { + const [headers, items] = envelope; + return [headers, [...items, newItem]] ; +} - if (expired) { - return; - } +/** + * Convenience function to loop through the items and item types of an envelope. + * (This function was mostly created because working with envelope types is painful at the moment) + * + * If the callback returns true, the rest of the items will be skipped. + */ +function forEachEnvelopeItem( + envelope, + callback, +) { + const envelopeItems = envelope[1]; - if (breadcrumb) { - this._createCustomBreadcrumb(breadcrumb); - } + for (const envelopeItem of envelopeItems) { + const envelopeItemType = envelopeItem[0].type; + const result = callback(envelopeItem, envelopeItemType); - // 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(); + if (result) { + return true; + } } - /** - * Tasks to run when we consider a page to be visible (via focus and/or visibility) - */ - _doChangeToForegroundTasks(breadcrumb) { - if (!this.session) { - return; - } + return false; +} - const isSessionActive = this.checkAndHandleExpiredSession(); +/** + * Returns true if the envelope contains any of the given envelope item types + */ +function envelopeContainsItemType(envelope, types) { + return forEachEnvelopeItem(envelope, (_, type) => types.includes(type)); +} - 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; - } +/** + * Encode a string to UTF8. + */ +function encodeUTF8(input, textEncoder) { + const utf8 = textEncoder || new TextEncoder(); + return utf8.encode(input); +} - if (breadcrumb) { - this._createCustomBreadcrumb(breadcrumb); - } - } +/** + * Serializes an envelope. + */ +function serializeEnvelope(envelope, textEncoder) { + const [envHeaders, items] = envelope; - /** - * Update user activity (across session lifespans) - */ - _updateUserActivity(_lastActivity = Date.now()) { - this._lastActivity = _lastActivity; - } + // Initially we construct our envelope as a string and only convert to binary chunks if we encounter binary data + let parts = JSON.stringify(envHeaders); - /** - * Updates the session's last activity timestamp - */ - _updateSessionActivity(_lastActivity = Date.now()) { - if (this.session) { - this.session.lastActivity = _lastActivity; - this._maybeSaveSession(); + function append(next) { + if (typeof parts === 'string') { + parts = typeof next === 'string' ? parts + next : [encodeUTF8(parts, textEncoder), next]; + } else { + parts.push(typeof next === 'string' ? encodeUTF8(next, textEncoder) : next); } } - /** - * 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, - }, - }); - }); + for (const item of items) { + const [itemHeaders, payload] = item; + + append(`\n${JSON.stringify(itemHeaders)}\n`); + + if (typeof payload === 'string' || payload instanceof Uint8Array) { + append(payload); + } else { + let stringifiedPayload; + try { + stringifiedPayload = JSON.stringify(payload); + } catch (e) { + // In case, despite all our efforts to keep `payload` circular-dependency-free, `JSON.strinify()` still + // fails, we try again after normalizing it again with infinite normalization depth. This of course has a + // performance impact but in this case a performance hit is better than throwing. + stringifiedPayload = JSON.stringify(normalize.normalize(payload)); + } + append(stringifiedPayload); + } } - /** - * 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); + return typeof parts === 'string' ? parts : concatBuffers(parts); +} - this.performanceEntries = []; - this.replayPerformanceEntries = []; +function concatBuffers(buffers) { + const totalLength = buffers.reduce((acc, buf) => acc + buf.length, 0); - return Promise.all(createPerformanceSpans(this, performanceEntries)); + const merged = new Uint8Array(totalLength); + let offset = 0; + for (const buffer of buffers) { + merged.set(buffer, offset); + offset += buffer.length; } - /** - * Clear _context - */ - _clearContext() { - // XXX: `initialTimestamp` and `initialUrl` do not get cleared - this._context.errorIds.clear(); - this._context.traceIds.clear(); - this._context.urls = []; - } + return merged; +} - /** Update the initial timestamp based on the buffer content. */ - _updateInitialTimestampFromEventBuffer() { - const { session, eventBuffer } = this; - if (!session || !eventBuffer) { - return; - } +/** + * Parses an envelope + */ +function parseEnvelope( + env, + textEncoder, + textDecoder, +) { + let buffer = typeof env === 'string' ? textEncoder.encode(env) : env; - // we only ever update this on the initial segment - if (session.segmentId) { - return; - } + function readBinary(length) { + const bin = buffer.subarray(0, length); + // Replace the buffer with the remaining data excluding trailing newline + buffer = buffer.subarray(length + 1); + return bin; + } - const earliestEvent = eventBuffer.getEarliestTimestamp(); - if (earliestEvent && earliestEvent < this._context.initialTimestamp) { - this._context.initialTimestamp = earliestEvent; + function readJson() { + let i = buffer.indexOf(0xa); + // If we couldn't find a newline, we must have found the end of the buffer + if (i < 0) { + i = buffer.length; } + + return JSON.parse(textDecoder.decode(readBinary(i))) ; } - /** - * 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, - }; + const envelopeHeader = readJson(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const items = []; - this._clearContext(); + while (buffer.length) { + const itemHeader = readJson(); + const binaryLength = typeof itemHeader.length === 'number' ? itemHeader.length : undefined; - return _context; + items.push([itemHeader, binaryLength ? readBinary(binaryLength) : readJson()]); } - /** - * 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 && utils.logger.error('[Replay] No session or eventBuffer found to flush.'); - return; - } - - await this._addPerformanceEntries(); + return [envelopeHeader, items]; +} - // Check eventBuffer again, as it could have been stopped in the meanwhile - if (!this.eventBuffer || !this.eventBuffer.hasEvents) { - return; - } +/** + * Creates attachment envelope items + */ +function createAttachmentEnvelopeItem( + attachment, + textEncoder, +) { + const buffer = typeof attachment.data === 'string' ? encodeUTF8(attachment.data, textEncoder) : attachment.data; - // Only attach memory event if eventBuffer is not empty - await addMemoryEntry(this); + return [ + object.dropUndefinedKeys({ + type: 'attachment', + length: buffer.length, + filename: attachment.filename, + content_type: attachment.contentType, + attachment_type: attachment.attachmentType, + }), + buffer, + ]; +} - // Check eventBuffer again, as it could have been stopped in the meanwhile - if (!this.eventBuffer) { - return; - } +const ITEM_TYPE_TO_DATA_CATEGORY_MAP = { + session: 'session', + sessions: 'session', + attachment: 'attachment', + transaction: 'transaction', + event: 'error', + client_report: 'internal', + user_report: 'default', + profile: 'profile', + replay_event: 'replay', + replay_recording: 'replay', + check_in: 'monitor', + feedback: 'feedback', + span: 'span', + statsd: 'metric_bucket', +}; - // if this changed in the meanwhile, e.g. because the session was refreshed or similar, we abort here - if (replayId !== this.getSessionId()) { - return; - } +/** + * Maps the type of an envelope item to a data category. + */ +function envelopeItemTypeToDataCategory(type) { + return ITEM_TYPE_TO_DATA_CATEGORY_MAP[type]; +} - try { - // This uses the data from the eventBuffer, so we need to call this before `finish() - this._updateInitialTimestampFromEventBuffer(); +/** Extracts the minimal SDK info from the metadata or an events */ +function getSdkMetadataForEnvelopeHeader(metadataOrEvent) { + if (!metadataOrEvent || !metadataOrEvent.sdk) { + return; + } + const { name, version } = metadataOrEvent.sdk; + return { name, version }; +} - const timestamp = Date.now(); +/** + * Creates event envelope headers, based on event, sdk info and tunnel + * Note: This function was extracted from the core package to make it available in Replay + */ +function createEventEnvelopeHeaders( + event, + sdkInfo, + tunnel, + dsn$1, +) { + const dynamicSamplingContext = event.sdkProcessingMetadata && event.sdkProcessingMetadata.dynamicSamplingContext; + return { + event_id: event.event_id , + sent_at: new Date().toISOString(), + ...(sdkInfo && { sdk: sdkInfo }), + ...(!!tunnel && dsn$1 && { dsn: dsn.dsnToString(dsn$1) }), + ...(dynamicSamplingContext && { + trace: object.dropUndefinedKeys({ ...dynamicSamplingContext }), + }), + }; +} - // 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'); - } +exports.addItemToEnvelope = addItemToEnvelope; +exports.createAttachmentEnvelopeItem = createAttachmentEnvelopeItem; +exports.createEnvelope = createEnvelope; +exports.createEventEnvelopeHeaders = createEventEnvelopeHeaders; +exports.envelopeContainsItemType = envelopeContainsItemType; +exports.envelopeItemTypeToDataCategory = envelopeItemTypeToDataCategory; +exports.forEachEnvelopeItem = forEachEnvelopeItem; +exports.getSdkMetadataForEnvelopeHeader = getSdkMetadataForEnvelopeHeader; +exports.parseEnvelope = parseEnvelope; +exports.serializeEnvelope = serializeEnvelope; - 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(); +},{"./dsn.js":147,"./normalize.js":170,"./object.js":171}],150:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - await sendReplay({ - replayId, - recordingData, - segmentId, - eventContext, - session: this.session, - options: this.getOptions(), - timestamp, - }); - } catch (err) { - this._handleException(err); +/** An error emitted by Sentry SDKs and related utilities. */ +class SentryError extends Error { + /** Display name of this error instance. */ - // 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' }); + constructor( message, logLevel = 'warn') { + super(message);this.message = message; + this.name = new.target.prototype.constructor.name; + // This sets the prototype to be `Error`, not `SentryError`. It's unclear why we do this, but commenting this line + // out causes various (seemingly totally unrelated) playwright tests consistently time out. FYI, this makes + // instances of `SentryError` fail `obj instanceof SentryError` checks. + Object.setPrototypeOf(this, new.target.prototype); + this.logLevel = logLevel; + } +} - const client = core.getClient(); +exports.SentryError = SentryError; - 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, - } +},{}],151:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - = {}) => { - if (!this._isEnabled && !force) { - // This can happen if e.g. the replay was stopped because of exceeding the retry limit - return; - } +const is = require('./is.js'); +const misc = require('./misc.js'); +const normalize = require('./normalize.js'); +const object = require('./object.js'); - if (!this.checkAndHandleExpiredSession()) { - DEBUG_BUILD && utils.logger.error('[Replay] Attempting to finish replay event after session expired.'); - return; - } +/** + * Extracts stack frames from the error.stack string + */ +function parseStackFrames(stackParser, error) { + return stackParser(error.stack || '', 1); +} - if (!this.session) { - // should never happen, as we would have bailed out before - return; - } +/** + * Extracts stack frames from the error and builds a Sentry Exception + */ +function exceptionFromError(stackParser, error) { + const exception = { + type: error.name || error.constructor.name, + value: error.message, + }; - const start = this.session.started; - const now = Date.now(); - const duration = now - start; + const frames = parseStackFrames(stackParser, error); + if (frames.length) { + exception.stacktrace = { frames }; + } - // A flush is about to happen, cancel any queued flushes - this._debouncedFlush.cancel(); + return exception; +} - // 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, - ); +function getMessageForObject(exception) { + if ('name' in exception && typeof exception.name === 'string') { + let message = `'${exception.name}' captured as exception`; - if (tooShort) { - this._debouncedFlush(); - } - return; + if ('message' in exception && typeof exception.message === 'string') { + message += ` with message '${exception.message}'`; } - 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? - } + return message; + } else if ('message' in exception && typeof exception.message === 'string') { + return exception.message; + } else { + // This will allow us to group events based on top-level keys + // which is much better than creating new group when any key/value change + return `Object captured as exception with keys: ${object.extractExceptionKeysForMessage( + exception , + )}`; + } +} - // 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; - } +/** + * Builds and Event from a Exception + * + * TODO(v8): Remove getHub fallback + * @hidden + */ +function eventFromUnknownInput( + // eslint-disable-next-line deprecation/deprecation + getHubOrClient, + stackParser, + exception, + hint, +) { + const client = + typeof getHubOrClient === 'function' + ? // eslint-disable-next-line deprecation/deprecation + getHubOrClient().getClient() + : getHubOrClient; - // 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. + let ex = exception; + const providedMechanism = + hint && hint.data && (hint.data ).mechanism; + const mechanism = providedMechanism || { + handled: true, + type: 'generic', + }; - try { - await this._flushLock; - } catch (err) { - DEBUG_BUILD && utils.logger.error(err); - } finally { - this._debouncedFlush(); - } - };} + let extras; - /** Save the session, if it is sticky */ - _maybeSaveSession() { - if (this.session && this._options.stickySession) { - saveSession(this.session); + if (!is.isError(exception)) { + if (is.isPlainObject(exception)) { + const normalizeDepth = client && client.getOptions().normalizeDepth; + extras = { ['__serialized__']: normalize.normalizeToSize(exception , normalizeDepth) }; + + const message = getMessageForObject(exception); + ex = (hint && hint.syntheticException) || new Error(message); + (ex ).message = message; + } else { + // This handles when someone does: `throw "something awesome";` + // We use synthesized Error here so we can extract a (rough) stack trace. + ex = (hint && hint.syntheticException) || new Error(exception ); + (ex ).message = exception ; } + mechanism.synthetic = true; } - /** 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; + const event = { + exception: { + values: [exceptionFromError(stackParser, ex )], + }, + }; - // 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); - } + if (extras) { + event.extra = extras; + } - // 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; - } + misc.addExceptionTypeValue(event, undefined, undefined); + misc.addExceptionMechanism(event, mechanism); - // `true` means we use the regular mutation handling by rrweb - return true; - };} + return { + ...event, + event_id: hint && hint.event_id, + }; } -function getOption( - selectors, - defaultSelectors, - deprecatedClassOption, - deprecatedSelectorOption, +/** + * Builds and Event from a Message + * @hidden + */ +function eventFromMessage( + stackParser, + message, + // eslint-disable-next-line deprecation/deprecation + level = 'info', + hint, + attachStacktrace, ) { - const deprecatedSelectors = typeof deprecatedSelectorOption === 'string' ? deprecatedSelectorOption.split(',') : []; - - const allSelectors = [ - ...selectors, - // @deprecated - ...deprecatedSelectors, - - // sentry defaults - ...defaultSelectors, - ]; + const event = { + event_id: hint && hint.event_id, + level, + }; - // @deprecated - if (typeof deprecatedClassOption !== 'undefined') { - // NOTE: No support for RegExp - if (typeof deprecatedClassOption === 'string') { - allSelectors.push(`.${deprecatedClassOption}`); + if (attachStacktrace && hint && hint.syntheticException) { + const frames = parseStackFrames(stackParser, hint.syntheticException); + if (frames.length) { + event.exception = { + values: [ + { + value: message, + stacktrace: { frames }, + }, + ], + }; } + } - utils.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.', - ); - }); + if (is.isParameterizedString(message)) { + const { __sentry_template_string__, __sentry_template_values__ } = message; + + event.logentry = { + message: __sentry_template_string__, + params: __sentry_template_values__, + }; + return event; } - return allSelectors.join(','); + event.message = message; + return event; } -/** - * Returns privacy related configuration for use in rrweb - */ -function getPrivacyOptions({ - mask, - unmask, - block, - unblock, - ignore, +exports.eventFromMessage = eventFromMessage; +exports.eventFromUnknownInput = eventFromUnknownInput; +exports.exceptionFromError = exceptionFromError; +exports.parseStackFrames = parseStackFrames; - // 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]']); +},{"./is.js":162,"./misc.js":167,"./normalize.js":170,"./object.js":171}],152:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const aggregateErrors = require('./aggregate-errors.js'); +const browser = require('./browser.js'); +const dsn = require('./dsn.js'); +const error = require('./error.js'); +const worldwide = require('./worldwide.js'); +const index = require('./instrument/index.js'); +const is = require('./is.js'); +const isBrowser = require('./isBrowser.js'); +const logger = require('./logger.js'); +const memo = require('./memo.js'); +const misc = require('./misc.js'); +const node = require('./node.js'); +const normalize = require('./normalize.js'); +const object = require('./object.js'); +const path = require('./path.js'); +const promisebuffer = require('./promisebuffer.js'); +const requestdata = require('./requestdata.js'); +const severity = require('./severity.js'); +const stacktrace = require('./stacktrace.js'); +const string = require('./string.js'); +const supports = require('./supports.js'); +const syncpromise = require('./syncpromise.js'); +const time = require('./time.js'); +const tracing = require('./tracing.js'); +const env = require('./env.js'); +const envelope = require('./envelope.js'); +const clientreport = require('./clientreport.js'); +const ratelimit = require('./ratelimit.js'); +const baggage = require('./baggage.js'); +const url = require('./url.js'); +const userIntegrations = require('./userIntegrations.js'); +const cache = require('./cache.js'); +const eventbuilder = require('./eventbuilder.js'); +const anr = require('./anr.js'); +const lru = require('./lru.js'); +const _asyncNullishCoalesce = require('./buildPolyfills/_asyncNullishCoalesce.js'); +const _asyncOptionalChain = require('./buildPolyfills/_asyncOptionalChain.js'); +const _asyncOptionalChainDelete = require('./buildPolyfills/_asyncOptionalChainDelete.js'); +const _nullishCoalesce = require('./buildPolyfills/_nullishCoalesce.js'); +const _optionalChain = require('./buildPolyfills/_optionalChain.js'); +const _optionalChainDelete = require('./buildPolyfills/_optionalChainDelete.js'); +const console = require('./instrument/console.js'); +const dom = require('./instrument/dom.js'); +const xhr = require('./instrument/xhr.js'); +const fetch = require('./instrument/fetch.js'); +const history = require('./instrument/history.js'); +const globalError = require('./instrument/globalError.js'); +const globalUnhandledRejection = require('./instrument/globalUnhandledRejection.js'); +const _handlers = require('./instrument/_handlers.js'); +const nodeStackTrace = require('./node-stack-trace.js'); +const escapeStringForRegex = require('./vendor/escapeStringForRegex.js'); +const supportsHistory = require('./vendor/supportsHistory.js'); + + + +exports.applyAggregateErrorsToEvent = aggregateErrors.applyAggregateErrorsToEvent; +exports.getComponentName = browser.getComponentName; +exports.getDomElement = browser.getDomElement; +exports.getLocationHref = browser.getLocationHref; +exports.htmlTreeAsString = browser.htmlTreeAsString; +exports.dsnFromString = dsn.dsnFromString; +exports.dsnToString = dsn.dsnToString; +exports.makeDsn = dsn.makeDsn; +exports.SentryError = error.SentryError; +exports.GLOBAL_OBJ = worldwide.GLOBAL_OBJ; +exports.getGlobalObject = worldwide.getGlobalObject; +exports.getGlobalSingleton = worldwide.getGlobalSingleton; +exports.addInstrumentationHandler = index.addInstrumentationHandler; +exports.isDOMError = is.isDOMError; +exports.isDOMException = is.isDOMException; +exports.isElement = is.isElement; +exports.isError = is.isError; +exports.isErrorEvent = is.isErrorEvent; +exports.isEvent = is.isEvent; +exports.isInstanceOf = is.isInstanceOf; +exports.isNaN = is.isNaN; +exports.isParameterizedString = is.isParameterizedString; +exports.isPlainObject = is.isPlainObject; +exports.isPrimitive = is.isPrimitive; +exports.isRegExp = is.isRegExp; +exports.isString = is.isString; +exports.isSyntheticEvent = is.isSyntheticEvent; +exports.isThenable = is.isThenable; +exports.isVueViewModel = is.isVueViewModel; +exports.isBrowser = isBrowser.isBrowser; +exports.CONSOLE_LEVELS = logger.CONSOLE_LEVELS; +exports.consoleSandbox = logger.consoleSandbox; +exports.logger = logger.logger; +exports.originalConsoleMethods = logger.originalConsoleMethods; +exports.memoBuilder = memo.memoBuilder; +exports.addContextToFrame = misc.addContextToFrame; +exports.addExceptionMechanism = misc.addExceptionMechanism; +exports.addExceptionTypeValue = misc.addExceptionTypeValue; +exports.arrayify = misc.arrayify; +exports.checkOrSetAlreadyCaught = misc.checkOrSetAlreadyCaught; +exports.getEventDescription = misc.getEventDescription; +exports.parseSemver = misc.parseSemver; +exports.uuid4 = misc.uuid4; +exports.dynamicRequire = node.dynamicRequire; +exports.isNodeEnv = node.isNodeEnv; +exports.loadModule = node.loadModule; +exports.normalize = normalize.normalize; +exports.normalizeToSize = normalize.normalizeToSize; +exports.normalizeUrlToBase = normalize.normalizeUrlToBase; +exports.walk = normalize.walk; +exports.addNonEnumerableProperty = object.addNonEnumerableProperty; +exports.convertToPlainObject = object.convertToPlainObject; +exports.dropUndefinedKeys = object.dropUndefinedKeys; +exports.extractExceptionKeysForMessage = object.extractExceptionKeysForMessage; +exports.fill = object.fill; +exports.getOriginalFunction = object.getOriginalFunction; +exports.markFunctionWrapped = object.markFunctionWrapped; +exports.objectify = object.objectify; +exports.urlEncode = object.urlEncode; +exports.basename = path.basename; +exports.dirname = path.dirname; +exports.isAbsolute = path.isAbsolute; +exports.join = path.join; +exports.normalizePath = path.normalizePath; +exports.relative = path.relative; +exports.resolve = path.resolve; +exports.makePromiseBuffer = promisebuffer.makePromiseBuffer; +exports.DEFAULT_USER_INCLUDES = requestdata.DEFAULT_USER_INCLUDES; +exports.addRequestDataToEvent = requestdata.addRequestDataToEvent; +exports.addRequestDataToTransaction = requestdata.addRequestDataToTransaction; +exports.extractPathForTransaction = requestdata.extractPathForTransaction; +exports.extractRequestData = requestdata.extractRequestData; +exports.winterCGHeadersToDict = requestdata.winterCGHeadersToDict; +exports.winterCGRequestToRequestData = requestdata.winterCGRequestToRequestData; +exports.severityFromString = severity.severityFromString; +exports.severityLevelFromString = severity.severityLevelFromString; +exports.validSeverityLevels = severity.validSeverityLevels; +exports.createStackParser = stacktrace.createStackParser; +exports.getFunctionName = stacktrace.getFunctionName; +exports.nodeStackLineParser = stacktrace.nodeStackLineParser; +exports.stackParserFromStackParserOptions = stacktrace.stackParserFromStackParserOptions; +exports.stripSentryFramesAndReverse = stacktrace.stripSentryFramesAndReverse; +exports.isMatchingPattern = string.isMatchingPattern; +exports.safeJoin = string.safeJoin; +exports.snipLine = string.snipLine; +exports.stringMatchesSomePattern = string.stringMatchesSomePattern; +exports.truncate = string.truncate; +exports.isNativeFetch = supports.isNativeFetch; +exports.supportsDOMError = supports.supportsDOMError; +exports.supportsDOMException = supports.supportsDOMException; +exports.supportsErrorEvent = supports.supportsErrorEvent; +exports.supportsFetch = supports.supportsFetch; +exports.supportsNativeFetch = supports.supportsNativeFetch; +exports.supportsReferrerPolicy = supports.supportsReferrerPolicy; +exports.supportsReportingObserver = supports.supportsReportingObserver; +exports.SyncPromise = syncpromise.SyncPromise; +exports.rejectedSyncPromise = syncpromise.rejectedSyncPromise; +exports.resolvedSyncPromise = syncpromise.resolvedSyncPromise; +Object.defineProperty(exports, '_browserPerformanceTimeOriginMode', { + enumerable: true, + get: () => time._browserPerformanceTimeOriginMode +}); +exports.browserPerformanceTimeOrigin = time.browserPerformanceTimeOrigin; +exports.dateTimestampInSeconds = time.dateTimestampInSeconds; +exports.timestampInSeconds = time.timestampInSeconds; +exports.timestampWithMs = time.timestampWithMs; +exports.TRACEPARENT_REGEXP = tracing.TRACEPARENT_REGEXP; +exports.extractTraceparentData = tracing.extractTraceparentData; +exports.generateSentryTraceHeader = tracing.generateSentryTraceHeader; +exports.propagationContextFromHeaders = tracing.propagationContextFromHeaders; +exports.tracingContextFromHeaders = tracing.tracingContextFromHeaders; +exports.getSDKSource = env.getSDKSource; +exports.isBrowserBundle = env.isBrowserBundle; +exports.addItemToEnvelope = envelope.addItemToEnvelope; +exports.createAttachmentEnvelopeItem = envelope.createAttachmentEnvelopeItem; +exports.createEnvelope = envelope.createEnvelope; +exports.createEventEnvelopeHeaders = envelope.createEventEnvelopeHeaders; +exports.envelopeContainsItemType = envelope.envelopeContainsItemType; +exports.envelopeItemTypeToDataCategory = envelope.envelopeItemTypeToDataCategory; +exports.forEachEnvelopeItem = envelope.forEachEnvelopeItem; +exports.getSdkMetadataForEnvelopeHeader = envelope.getSdkMetadataForEnvelopeHeader; +exports.parseEnvelope = envelope.parseEnvelope; +exports.serializeEnvelope = envelope.serializeEnvelope; +exports.createClientReportEnvelope = clientreport.createClientReportEnvelope; +exports.DEFAULT_RETRY_AFTER = ratelimit.DEFAULT_RETRY_AFTER; +exports.disabledUntil = ratelimit.disabledUntil; +exports.isRateLimited = ratelimit.isRateLimited; +exports.parseRetryAfterHeader = ratelimit.parseRetryAfterHeader; +exports.updateRateLimits = ratelimit.updateRateLimits; +exports.BAGGAGE_HEADER_NAME = baggage.BAGGAGE_HEADER_NAME; +exports.MAX_BAGGAGE_STRING_LENGTH = baggage.MAX_BAGGAGE_STRING_LENGTH; +exports.SENTRY_BAGGAGE_KEY_PREFIX = baggage.SENTRY_BAGGAGE_KEY_PREFIX; +exports.SENTRY_BAGGAGE_KEY_PREFIX_REGEX = baggage.SENTRY_BAGGAGE_KEY_PREFIX_REGEX; +exports.baggageHeaderToDynamicSamplingContext = baggage.baggageHeaderToDynamicSamplingContext; +exports.dynamicSamplingContextToSentryBaggageHeader = baggage.dynamicSamplingContextToSentryBaggageHeader; +exports.getNumberOfUrlSegments = url.getNumberOfUrlSegments; +exports.getSanitizedUrlString = url.getSanitizedUrlString; +exports.parseUrl = url.parseUrl; +exports.stripUrlQueryAndFragment = url.stripUrlQueryAndFragment; +exports.addOrUpdateIntegration = userIntegrations.addOrUpdateIntegration; +exports.makeFifoCache = cache.makeFifoCache; +exports.eventFromMessage = eventbuilder.eventFromMessage; +exports.eventFromUnknownInput = eventbuilder.eventFromUnknownInput; +exports.exceptionFromError = eventbuilder.exceptionFromError; +exports.parseStackFrames = eventbuilder.parseStackFrames; +exports.callFrameToStackFrame = anr.callFrameToStackFrame; +exports.watchdogTimer = anr.watchdogTimer; +exports.LRUMap = lru.LRUMap; +exports._asyncNullishCoalesce = _asyncNullishCoalesce._asyncNullishCoalesce; +exports._asyncOptionalChain = _asyncOptionalChain._asyncOptionalChain; +exports._asyncOptionalChainDelete = _asyncOptionalChainDelete._asyncOptionalChainDelete; +exports._nullishCoalesce = _nullishCoalesce._nullishCoalesce; +exports._optionalChain = _optionalChain._optionalChain; +exports._optionalChainDelete = _optionalChainDelete._optionalChainDelete; +exports.addConsoleInstrumentationHandler = console.addConsoleInstrumentationHandler; +exports.addClickKeypressInstrumentationHandler = dom.addClickKeypressInstrumentationHandler; +exports.SENTRY_XHR_DATA_KEY = xhr.SENTRY_XHR_DATA_KEY; +exports.addXhrInstrumentationHandler = xhr.addXhrInstrumentationHandler; +exports.addFetchInstrumentationHandler = fetch.addFetchInstrumentationHandler; +exports.addHistoryInstrumentationHandler = history.addHistoryInstrumentationHandler; +exports.addGlobalErrorInstrumentationHandler = globalError.addGlobalErrorInstrumentationHandler; +exports.addGlobalUnhandledRejectionInstrumentationHandler = globalUnhandledRejection.addGlobalUnhandledRejectionInstrumentationHandler; +exports.resetInstrumentationHandlers = _handlers.resetInstrumentationHandlers; +exports.filenameIsInApp = nodeStackTrace.filenameIsInApp; +exports.escapeStringForRegex = escapeStringForRegex.escapeStringForRegex; +exports.supportsHistory = supportsHistory.supportsHistory; - 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), - }; +},{"./aggregate-errors.js":133,"./anr.js":134,"./baggage.js":135,"./browser.js":136,"./buildPolyfills/_asyncNullishCoalesce.js":137,"./buildPolyfills/_asyncOptionalChain.js":138,"./buildPolyfills/_asyncOptionalChainDelete.js":139,"./buildPolyfills/_nullishCoalesce.js":140,"./buildPolyfills/_optionalChain.js":141,"./buildPolyfills/_optionalChainDelete.js":142,"./cache.js":143,"./clientreport.js":144,"./dsn.js":147,"./env.js":148,"./envelope.js":149,"./error.js":150,"./eventbuilder.js":151,"./instrument/_handlers.js":153,"./instrument/console.js":154,"./instrument/dom.js":155,"./instrument/fetch.js":156,"./instrument/globalError.js":157,"./instrument/globalUnhandledRejection.js":158,"./instrument/history.js":159,"./instrument/index.js":160,"./instrument/xhr.js":161,"./is.js":162,"./isBrowser.js":163,"./logger.js":164,"./lru.js":165,"./memo.js":166,"./misc.js":167,"./node-stack-trace.js":168,"./node.js":169,"./normalize.js":170,"./object.js":171,"./path.js":172,"./promisebuffer.js":173,"./ratelimit.js":174,"./requestdata.js":175,"./severity.js":176,"./stacktrace.js":177,"./string.js":178,"./supports.js":179,"./syncpromise.js":180,"./time.js":181,"./tracing.js":182,"./url.js":183,"./userIntegrations.js":184,"./vendor/escapeStringForRegex.js":185,"./vendor/supportsHistory.js":186,"./worldwide.js":187}],153:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - if (blockClass instanceof RegExp) { - options.blockClass = blockClass; - } +const debugBuild = require('../debug-build.js'); +const logger = require('../logger.js'); +const stacktrace = require('../stacktrace.js'); - if (maskTextClass instanceof RegExp) { - options.maskTextClass = maskTextClass; - } +// We keep the handlers globally +const handlers = {}; +const instrumented = {}; - return options; +/** Add a handler function. */ +function addHandler(type, handler) { + handlers[type] = handlers[type] || []; + (handlers[type] ).push(handler); } /** - * Masks an attribute if necessary, otherwise return attribute value as-is. + * Reset all instrumentation handlers. + * This can be used by tests to ensure we have a clean slate of instrumentation handlers. */ -function maskAttribute({ - el, - key, - maskAttributes, - maskAllText, - privacyOptions, - value, -}) { - // We only mask attributes if `maskAllText` is true - if (!maskAllText) { - return value; - } +function resetInstrumentationHandlers() { + Object.keys(handlers).forEach(key => { + handlers[key ] = undefined; + }); +} - // unmaskTextSelector takes precendence - if (privacyOptions.unmaskTextSelector && el.matches(privacyOptions.unmaskTextSelector)) { - return value; +/** Maybe run an instrumentation function, unless it was already called. */ +function maybeInstrument(type, instrumentFn) { + if (!instrumented[type]) { + instrumentFn(); + instrumented[type] = true; } +} - 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, '*'); +/** Trigger handlers for a given instrumentation type. */ +function triggerHandlers(type, data) { + const typeHandlers = type && handlers[type]; + if (!typeHandlers) { + return; } - return value; + for (const handler of typeHandlers) { + try { + handler(data); + } catch (e) { + debugBuild.DEBUG_BUILD && + logger.logger.error( + `Error while triggering instrumentation handler.\nType: ${type}\nName: ${stacktrace.getFunctionName(handler)}\nError:`, + e, + ); + } + } } -const MEDIA_SELECTORS = - 'img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]'; +exports.addHandler = addHandler; +exports.maybeInstrument = maybeInstrument; +exports.resetInstrumentationHandlers = resetInstrumentationHandlers; +exports.triggerHandlers = triggerHandlers; -const DEFAULT_NETWORK_HEADERS = ['content-length', 'content-type', 'accept']; -let _initialized = false; +},{"../debug-build.js":146,"../logger.js":164,"../stacktrace.js":177}],154:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); -const replayIntegration$1 = ((options) => { - // eslint-disable-next-line deprecation/deprecation - return new Replay$1(options); -}) ; +const logger = require('../logger.js'); +const object = require('../object.js'); +const worldwide = require('../worldwide.js'); +const _handlers = require('./_handlers.js'); /** - * The main replay integration class, to be passed to `init({ integrations: [] })`. - * @deprecated Use `replayIntegration()` instead. + * Add an instrumentation handler for when a console.xxx method is called. + * + * Use at your own risk, this might break without changelog notice, only used internally. + * @hidden */ -class Replay$1 { - /** - * @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 - */ +function addConsoleInstrumentationHandler(handler) { + const type = 'console'; + _handlers.addHandler(type, handler); + _handlers.maybeInstrument(type, instrumentConsole); +} - 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, +function instrumentConsole() { + if (!('console' in worldwide.GLOBAL_OBJ)) { + return; + } - mutationBreadcrumbLimit = 750, - mutationLimit = 10000, + logger.CONSOLE_LEVELS.forEach(function (level) { + if (!(level in worldwide.GLOBAL_OBJ.console)) { + return; + } - slowClickTimeout = 7000, - slowClickIgnoreSelectors = [], + object.fill(worldwide.GLOBAL_OBJ.console, level, function (originalConsoleMethod) { + logger.originalConsoleMethods[level] = originalConsoleMethod; - networkDetailAllowUrls = [], - networkDetailDenyUrls = [], - networkCaptureBodies = true, - networkRequestHeaders = [], - networkResponseHeaders = [], + return function (...args) { + const handlerData = { args, level }; + _handlers.triggerHandlers('console', handlerData); - mask = [], - maskAttributes = ['title', 'placeholder'], - unmask = [], - block = [], - unblock = [], - ignore = [], - maskFn, + const log = logger.originalConsoleMethods[level]; + log && log.apply(worldwide.GLOBAL_OBJ.console, args); + }; + }); + }); +} - beforeAddRecordingEvent, - beforeErrorSampling, +exports.addConsoleInstrumentationHandler = addConsoleInstrumentationHandler; - // 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$1.id; - const privacyOptions = getPrivacyOptions({ - mask, - unmask, - block, - unblock, - ignore, - blockClass, - blockSelector, - maskTextClass, - maskTextSelector, - ignoreClass, - }); +},{"../logger.js":164,"../object.js":171,"../worldwide.js":187,"./_handlers.js":153}],155:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - this._recordingOptions = { - maskAllInputs, - maskAllText, - maskInputOptions: { ...(maskInputOptions || {}), password: true }, - maskTextFn: maskFn, - maskInputFn: maskFn, - maskAttributeFn: (key, value, el) => - maskAttribute({ - maskAttributes, - maskAllText, - privacyOptions, - key, - value, - el, - }), +const misc = require('../misc.js'); +const object = require('../object.js'); +const worldwide = require('../worldwide.js'); +const _handlers = require('./_handlers.js'); - ...privacyOptions, +const WINDOW = worldwide.GLOBAL_OBJ ; +const DEBOUNCE_DURATION = 1000; - // 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 - } - }, - }; +let debounceTimerID; +let lastCapturedEventType; +let lastCapturedEventTargetId; - 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, +/** + * Add an instrumentation handler for when a click or a keypress happens. + * + * Use at your own risk, this might break without changelog notice, only used internally. + * @hidden + */ +function addClickKeypressInstrumentationHandler(handler) { + const type = 'dom'; + _handlers.addHandler(type, handler); + _handlers.maybeInstrument(type, instrumentDOM); +} - _experiments, - }; +/** Exported for tests only. */ +function instrumentDOM() { + if (!WINDOW.document) { + return; + } - 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} })`, - ); + // Make it so that any click or keypress that is unhandled / bubbled up all the way to the document triggers our dom + // handlers. (Normally we have only one, which captures a breadcrumb for each click or keypress.) Do this before + // we instrument `addEventListener` so that we don't end up attaching this handler twice. + const triggerDOMHandler = _handlers.triggerHandlers.bind(null, 'dom'); + const globalDOMEventHandler = makeDOMEventHandler(triggerDOMHandler, true); + WINDOW.document.addEventListener('click', globalDOMEventHandler, false); + WINDOW.document.addEventListener('keypress', globalDOMEventHandler, false); - this._initialOptions.sessionSampleRate = sessionSampleRate; + // After hooking into click and keypress events bubbled up to `document`, we also hook into user-handled + // clicks & keypresses, by adding an event listener of our own to any element to which they add a listener. That + // way, whenever one of their handlers is triggered, ours will be, too. (This is needed because their handler + // could potentially prevent the event from bubbling up to our global listeners. This way, our handler are still + // guaranteed to fire at least once.) + ['EventTarget', 'Node'].forEach((target) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const proto = (WINDOW )[target] && (WINDOW )[target].prototype; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-prototype-builtins + if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) { + return; } - 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; - } + object.fill(proto, 'addEventListener', function (originalAddEventListener) { + return function ( - 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}`; - } + type, + listener, + options, + ) { + if (type === 'click' || type == 'keypress') { + try { + const el = this ; + const handlers = (el.__sentry_instrumentation_handlers__ = el.__sentry_instrumentation_handlers__ || {}); + const handlerForType = (handlers[type] = handlers[type] || { refCount: 0 }); - if (this._isInitialized && utils.isBrowser()) { - throw new Error('Multiple Sentry Session Replay instances are not supported'); - } + if (!handlerForType.handler) { + const handler = makeDOMEventHandler(triggerDOMHandler); + handlerForType.handler = handler; + originalAddEventListener.call(this, type, handler, options); + } - this._isInitialized = true; - } + handlerForType.refCount++; + } catch (e) { + // Accessing dom properties is always fragile. + // Also allows us to skip `addEventListenrs` calls with no proper `this` context. + } + } - /** If replay has already been initialized */ - get _isInitialized() { - return _initialized; - } + return originalAddEventListener.call(this, type, listener, options); + }; + }); - /** Update _isInitialized */ - set _isInitialized(value) { - _initialized = value; - } + object.fill( + proto, + 'removeEventListener', + function (originalRemoveEventListener) { + return function ( - /** - * Setup and initialize replay container - */ - setupOnce() { - if (!utils.isBrowser()) { - return; - } + type, + listener, + options, + ) { + if (type === 'click' || type == 'keypress') { + try { + const el = this ; + const handlers = el.__sentry_instrumentation_handlers__ || {}; + const handlerForType = handlers[type]; - this._setup(); + if (handlerForType) { + handlerForType.refCount--; + // If there are no longer any custom handlers of the current type on this element, we can remove ours, too. + if (handlerForType.refCount <= 0) { + originalRemoveEventListener.call(this, type, handlerForType.handler, options); + handlerForType.handler = undefined; + delete handlers[type]; // eslint-disable-line @typescript-eslint/no-dynamic-delete + } - // 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()); - } + // If there are no longer any custom handlers of any type on this element, cleanup everything. + if (Object.keys(handlers).length === 0) { + delete el.__sentry_instrumentation_handlers__; + } + } + } catch (e) { + // Accessing dom properties is always fragile. + // Also allows us to skip `addEventListenrs` calls with no proper `this` context. + } + } - /** - * 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; - } + return originalRemoveEventListener.call(this, type, listener, options); + }; + }, + ); + }); +} - this._replay.start(); +/** + * Check whether the event is similar to the last captured one. For example, two click events on the same button. + */ +function isSimilarToLastCapturedEvent(event) { + // If both events have different type, then user definitely performed two separate actions. e.g. click + keypress. + if (event.type !== lastCapturedEventType) { + return false; } - /** - * Start replay buffering. Buffers until `flush()` is called or, if - * `replaysOnErrorSampleRate` > 0, until an error occurs. - */ - startBuffering() { - if (!this._replay) { - return; + try { + // If both events have the same type, it's still possible that actions were performed on different targets. + // e.g. 2 clicks on different buttons. + if (!event.target || (event.target )._sentryId !== lastCapturedEventTargetId) { + return false; } - - this._replay.startBuffering(); + } catch (e) { + // just accessing `target` property can throw an exception in some rare circumstances + // see: https://github.com/getsentry/sentry-javascript/issues/838 } - /** - * 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(); - } + // If both events have the same type _and_ same `target` (an element which triggered an event, _not necessarily_ + // to which an event listener was attached), we treat them as the same action, as we want to capture + // only one breadcrumb. e.g. multiple clicks on the same button, or typing inside a user input box. + return true; +} - return this._replay.stop({ forceFlush: this._replay.recordingMode === 'session' }); +/** + * Decide whether an event should be captured. + * @param event event to be captured + */ +function shouldSkipDOMEvent(eventType, target) { + // We are only interested in filtering `keypress` events for now. + if (eventType !== 'keypress') { + return false; } - /** - * 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(); - } + if (!target || !target.tagName) { + return true; + } - return this._replay.sendBufferedReplayOrFlush(options); + // Only consider keypress events on actual input elements. This will disregard keypresses targeting body + // e.g.tabbing through elements, hotkeys, etc. + if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { + return false; } - /** - * Get the current session ID. - */ - getReplayId() { - if (!this._replay || !this._replay.isEnabled()) { + return true; +} + +/** + * Wraps addEventListener to capture UI breadcrumbs + */ +function makeDOMEventHandler( + handler, + globalListener = false, +) { + return (event) => { + // It's possible this handler might trigger multiple times for the same + // event (e.g. event propagation through node ancestors). + // Ignore if we've already captured that event. + if (!event || event['_sentryCaptured']) { return; } - return this._replay.getSessionId(); - } + const target = getEventTarget(event); - /** - * Initializes replay. - */ - _initialize() { - if (!this._replay) { + // We always want to skip _some_ events. + if (shouldSkipDOMEvent(event.type, target)) { 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 = core.getClient(); - const canvasIntegration = client.getIntegrationByName('ReplayCanvas') - -; - if (!canvasIntegration) { - return; - } + // Mark event as "seen" + object.addNonEnumerableProperty(event, '_sentryCaptured', true); - this._replay['_canvas'] = canvasIntegration.getOptions(); - } catch (e) { - // ignore errors here + if (target && !target._sentryId) { + // Add UUID to event target so we can identify if + object.addNonEnumerableProperty(target, '_sentryId', misc.uuid4()); } - /* eslint-enable @typescript-eslint/no-non-null-assertion */ - } -}Replay$1.__initStatic(); -/** Parse Replay-related options from SDK options */ -function loadReplayOptionsFromClient(initialOptions) { - const client = core.getClient(); - const opt = client && (client.getOptions() ); + const name = event.type === 'keypress' ? 'input' : event.type; - const finalOptions = { sessionSampleRate: 0, errorSampleRate: 0, ...utils.dropUndefinedKeys(initialOptions) }; + // If there is no last captured event, it means that we can safely capture the new event and store it for future comparisons. + // If there is a last captured event, see if the new event is different enough to treat it as a unique one. + // If that's the case, emit the previous event and store locally the newly-captured DOM event. + if (!isSimilarToLastCapturedEvent(event)) { + const handlerData = { event, name, global: globalListener }; + handler(handlerData); + lastCapturedEventType = event.type; + lastCapturedEventTargetId = target ? target._sentryId : undefined; + } - if (!opt) { - utils.consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn('SDK client is not available.'); - }); - return finalOptions; - } + // Start a new debounce timer that will prevent us from capturing multiple events that should be grouped together. + clearTimeout(debounceTimerID); + debounceTimerID = WINDOW.setTimeout(() => { + lastCapturedEventTargetId = undefined; + lastCapturedEventType = undefined; + }, DEBOUNCE_DURATION); + }; +} - 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 - ) { - utils.consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - 'Replay is disabled because neither `replaysSessionSampleRate` nor `replaysOnErrorSampleRate` are set.', - ); - }); +function getEventTarget(event) { + try { + return event.target ; + } catch (e) { + // just accessing `target` property can throw an exception in some rare circumstances + // see: https://github.com/getsentry/sentry-javascript/issues/838 + return null; } +} - if (typeof opt.replaysSessionSampleRate === 'number') { - finalOptions.sessionSampleRate = opt.replaysSessionSampleRate; - } +exports.addClickKeypressInstrumentationHandler = addClickKeypressInstrumentationHandler; +exports.instrumentDOM = instrumentDOM; - if (typeof opt.replaysOnErrorSampleRate === 'number') { - finalOptions.errorSampleRate = opt.replaysOnErrorSampleRate; - } - return finalOptions; -} +},{"../misc.js":167,"../object.js":171,"../worldwide.js":187,"./_handlers.js":153}],156:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); -function _getMergedNetworkHeaders(headers) { - return [...DEFAULT_NETWORK_HEADERS, ...headers.map(header => header.toLowerCase())]; -} +const object = require('../object.js'); +const supports = require('../supports.js'); +const worldwide = require('../worldwide.js'); +const _handlers = require('./_handlers.js'); /** - * This is a small utility to get a type-safe instance of the Replay integration. + * Add an instrumentation handler for when a fetch request happens. + * The handler function is called once when the request starts and once when it ends, + * which can be identified by checking if it has an `endTimestamp`. + * + * Use at your own risk, this might break without changelog notice, only used internally. + * @hidden */ -// eslint-disable-next-line deprecation/deprecation -function getReplay$1() { - const client = core.getClient(); - return ( - client && client.getIntegrationByName && client.getIntegrationByName('Replay') - ); +function addFetchInstrumentationHandler(handler) { + const type = 'fetch'; + _handlers.addHandler(type, handler); + _handlers.maybeInstrument(type, instrumentFetch); } -// eslint-disable-next-line deprecation/deprecation +function instrumentFetch() { + if (!supports.supportsNativeFetch()) { + return; + } -/** @deprecated Use the export from `@sentry/replay` or from framework-specific SDKs like `@sentry/react` or `@sentry/vue` */ -const getReplay = getReplay$1; + object.fill(worldwide.GLOBAL_OBJ, 'fetch', function (originalFetch) { + return function (...args) { + const { method, url } = parseFetchArgs(args); -/** @deprecated Use the export from `@sentry/replay` or from framework-specific SDKs like `@sentry/react` or `@sentry/vue` */ -const replayIntegration = replayIntegration$1; + const handlerData = { + args, + fetchData: { + method, + url, + }, + startTimestamp: Date.now(), + }; -/** @deprecated Use the export from `@sentry/replay` or from framework-specific SDKs like `@sentry/react` or `@sentry/vue` */ -// eslint-disable-next-line deprecation/deprecation -class Replay extends Replay$1 {} + _handlers.triggerHandlers('fetch', { + ...handlerData, + }); -exports.InternalReplay = Replay$1; -exports.Replay = Replay; -exports.getReplay = getReplay; -exports.internalGetReplay = getReplay$1; -exports.internalReplayIntegration = replayIntegration$1; -exports.replayIntegration = replayIntegration; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return originalFetch.apply(worldwide.GLOBAL_OBJ, args).then( + (response) => { + const finishedHandlerData = { + ...handlerData, + endTimestamp: Date.now(), + response, + }; + _handlers.triggerHandlers('fetch', finishedHandlerData); + return response; + }, + (error) => { + const erroredHandlerData = { + ...handlerData, + endTimestamp: Date.now(), + error, + }; -},{"@sentry-internal/tracing":29,"@sentry/core":70,"@sentry/utils":139}],120:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + _handlers.triggerHandlers('fetch', erroredHandlerData); + // NOTE: If you are a Sentry user, and you are seeing this stack frame, + // it means the sentry.javascript SDK caught an error invoking your application code. + // This is expected behavior and NOT indicative of a bug with sentry.javascript. + throw error; + }, + ); + }; + }); +} -const is = require('./is.js'); -const string = require('./string.js'); +function hasProp(obj, prop) { + return !!obj && typeof obj === 'object' && !!(obj )[prop]; +} -/** - * Creates exceptions inside `event.exception.values` for errors that are nested on properties based on the `key` parameter. - */ -function applyAggregateErrorsToEvent( - exceptionFromErrorImplementation, - parser, - maxValueLimit = 250, - key, - limit, - event, - hint, -) { - if (!event.exception || !event.exception.values || !hint || !is.isInstanceOf(hint.originalException, Error)) { - return; +function getUrlFromResource(resource) { + if (typeof resource === 'string') { + return resource; } - // Generally speaking the last item in `event.exception.values` is the exception originating from the original Error - const originalException = - event.exception.values.length > 0 ? event.exception.values[event.exception.values.length - 1] : undefined; + if (!resource) { + return ''; + } - // We only create exception grouping if there is an exception in the event. - if (originalException) { - event.exception.values = truncateAggregateExceptions( - aggregateExceptionsFromError( - exceptionFromErrorImplementation, - parser, - limit, - hint.originalException , - key, - event.exception.values, - originalException, - 0, - ), - maxValueLimit, - ); + if (hasProp(resource, 'url')) { + return resource.url; } -} -function aggregateExceptionsFromError( - exceptionFromErrorImplementation, - parser, - limit, - error, - key, - prevExceptions, - exception, - exceptionId, -) { - if (prevExceptions.length >= limit + 1) { - return prevExceptions; + if (resource.toString) { + return resource.toString(); } - let newExceptions = [...prevExceptions]; + return ''; +} - // Recursively call this function in order to walk down a chain of errors - if (is.isInstanceOf(error[key], Error)) { - applyExceptionGroupFieldsForParentException(exception, exceptionId); - const newException = exceptionFromErrorImplementation(parser, error[key]); - const newExceptionId = newExceptions.length; - applyExceptionGroupFieldsForChildException(newException, key, newExceptionId, exceptionId); - newExceptions = aggregateExceptionsFromError( - exceptionFromErrorImplementation, - parser, - limit, - error[key], - key, - [newException, ...newExceptions], - newException, - newExceptionId, - ); +/** + * Parses the fetch arguments to find the used Http method and the url of the request. + * Exported for tests only. + */ +function parseFetchArgs(fetchArgs) { + if (fetchArgs.length === 0) { + return { method: 'GET', url: '' }; } - // This will create exception grouping for AggregateErrors - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError - if (Array.isArray(error.errors)) { - error.errors.forEach((childError, i) => { - if (is.isInstanceOf(childError, Error)) { - applyExceptionGroupFieldsForParentException(exception, exceptionId); - const newException = exceptionFromErrorImplementation(parser, childError); - const newExceptionId = newExceptions.length; - applyExceptionGroupFieldsForChildException(newException, `errors[${i}]`, newExceptionId, exceptionId); - newExceptions = aggregateExceptionsFromError( - exceptionFromErrorImplementation, - parser, - limit, - childError, - key, - [newException, ...newExceptions], - newException, - newExceptionId, - ); - } - }); + if (fetchArgs.length === 2) { + const [url, options] = fetchArgs ; + + return { + url: getUrlFromResource(url), + method: hasProp(options, 'method') ? String(options.method).toUpperCase() : 'GET', + }; } - return newExceptions; + const arg = fetchArgs[0]; + return { + url: getUrlFromResource(arg ), + method: hasProp(arg, 'method') ? String(arg.method).toUpperCase() : 'GET', + }; } -function applyExceptionGroupFieldsForParentException(exception, exceptionId) { - // Don't know if this default makes sense. The protocol requires us to set these values so we pick *some* default. - exception.mechanism = exception.mechanism || { type: 'generic', handled: true }; +exports.addFetchInstrumentationHandler = addFetchInstrumentationHandler; +exports.parseFetchArgs = parseFetchArgs; - exception.mechanism = { - ...exception.mechanism, - ...(exception.type === 'AggregateError' && { is_exception_group: true }), - exception_id: exceptionId, - }; -} -function applyExceptionGroupFieldsForChildException( - exception, - source, - exceptionId, - parentId, -) { - // Don't know if this default makes sense. The protocol requires us to set these values so we pick *some* default. - exception.mechanism = exception.mechanism || { type: 'generic', handled: true }; +},{"../object.js":171,"../supports.js":179,"../worldwide.js":187,"./_handlers.js":153}],157:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - exception.mechanism = { - ...exception.mechanism, - type: 'chained', - source, - exception_id: exceptionId, - parent_id: parentId, - }; -} +const worldwide = require('../worldwide.js'); +const _handlers = require('./_handlers.js'); + +let _oldOnErrorHandler = null; /** - * Truncate the message (exception.value) of all exceptions in the event. - * Because this event processor is ran after `applyClientOptions`, - * we need to truncate the message of the added exceptions here. + * Add an instrumentation handler for when an error is captured by the global error handler. + * + * Use at your own risk, this might break without changelog notice, only used internally. + * @hidden */ -function truncateAggregateExceptions(exceptions, maxValueLength) { - return exceptions.map(exception => { - if (exception.value) { - exception.value = string.truncate(exception.value, maxValueLength); +function addGlobalErrorInstrumentationHandler(handler) { + const type = 'error'; + _handlers.addHandler(type, handler); + _handlers.maybeInstrument(type, instrumentError); +} + +function instrumentError() { + _oldOnErrorHandler = worldwide.GLOBAL_OBJ.onerror; + + worldwide.GLOBAL_OBJ.onerror = function ( + msg, + url, + line, + column, + error, + ) { + const handlerData = { + column, + error, + line, + msg, + url, + }; + _handlers.triggerHandlers('error', handlerData); + + if (_oldOnErrorHandler && !_oldOnErrorHandler.__SENTRY_LOADER__) { + // eslint-disable-next-line prefer-rest-params + return _oldOnErrorHandler.apply(this, arguments); } - return exception; - }); + + return false; + }; + + worldwide.GLOBAL_OBJ.onerror.__SENTRY_INSTRUMENTED__ = true; } -exports.applyAggregateErrorsToEvent = applyAggregateErrorsToEvent; +exports.addGlobalErrorInstrumentationHandler = addGlobalErrorInstrumentationHandler; -},{"./is.js":149,"./string.js":165}],121:[function(require,module,exports){ +},{"../worldwide.js":187,"./_handlers.js":153}],158:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const object = require('./object.js'); -const nodeStackTrace = require('./node-stack-trace.js'); +const worldwide = require('../worldwide.js'); +const _handlers = require('./_handlers.js'); + +let _oldOnUnhandledRejectionHandler = null; /** - * A node.js watchdog timer - * @param pollInterval The interval that we expect to get polled at - * @param anrThreshold The threshold for when we consider ANR - * @param callback The callback to call for ANR - * @returns An object with `poll` and `enabled` functions {@link WatchdogReturn} + * Add an instrumentation handler for when an unhandled promise rejection is captured. + * + * Use at your own risk, this might break without changelog notice, only used internally. + * @hidden */ -function watchdogTimer( - createTimer, - pollInterval, - anrThreshold, - callback, +function addGlobalUnhandledRejectionInstrumentationHandler( + handler, ) { - const timer = createTimer(); - let triggered = false; - let enabled = true; - - setInterval(() => { - const diffMs = timer.getTimeMs(); - - if (triggered === false && diffMs > pollInterval + anrThreshold) { - triggered = true; - if (enabled) { - callback(); - } - } - - if (diffMs < pollInterval + anrThreshold) { - triggered = false; - } - }, 20); - - return { - poll: () => { - timer.reset(); - }, - enabled: (state) => { - enabled = state; - }, - }; + const type = 'unhandledrejection'; + _handlers.addHandler(type, handler); + _handlers.maybeInstrument(type, instrumentUnhandledRejection); } -// types copied from inspector.d.ts +function instrumentUnhandledRejection() { + _oldOnUnhandledRejectionHandler = worldwide.GLOBAL_OBJ.onunhandledrejection; -/** - * Converts Debugger.CallFrame to Sentry StackFrame - */ -function callFrameToStackFrame( - frame, - url, - getModuleFromFilename, -) { - const filename = url ? url.replace(/^file:\/\//, '') : undefined; + worldwide.GLOBAL_OBJ.onunhandledrejection = function (e) { + const handlerData = e; + _handlers.triggerHandlers('unhandledrejection', handlerData); - // CallFrame row/col are 0 based, whereas StackFrame are 1 based - const colno = frame.location.columnNumber ? frame.location.columnNumber + 1 : undefined; - const lineno = frame.location.lineNumber ? frame.location.lineNumber + 1 : undefined; + if (_oldOnUnhandledRejectionHandler && !_oldOnUnhandledRejectionHandler.__SENTRY_LOADER__) { + // eslint-disable-next-line prefer-rest-params + return _oldOnUnhandledRejectionHandler.apply(this, arguments); + } - return object.dropUndefinedKeys({ - filename, - module: getModuleFromFilename(filename), - function: frame.functionName || '?', - colno, - lineno, - in_app: filename ? nodeStackTrace.filenameIsInApp(filename) : undefined, - }); + return true; + }; + + worldwide.GLOBAL_OBJ.onunhandledrejection.__SENTRY_INSTRUMENTED__ = true; } -exports.callFrameToStackFrame = callFrameToStackFrame; -exports.watchdogTimer = watchdogTimer; +exports.addGlobalUnhandledRejectionInstrumentationHandler = addGlobalUnhandledRejectionInstrumentationHandler; -},{"./node-stack-trace.js":155,"./object.js":158}],122:[function(require,module,exports){ +},{"../worldwide.js":187,"./_handlers.js":153}],159:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const debugBuild = require('./debug-build.js'); -const is = require('./is.js'); -const logger = require('./logger.js'); - -const BAGGAGE_HEADER_NAME = 'baggage'; +const object = require('../object.js'); +require('../debug-build.js'); +require('../logger.js'); +const worldwide = require('../worldwide.js'); +const supportsHistory = require('../vendor/supportsHistory.js'); +const _handlers = require('./_handlers.js'); -const SENTRY_BAGGAGE_KEY_PREFIX = 'sentry-'; +const WINDOW = worldwide.GLOBAL_OBJ ; -const SENTRY_BAGGAGE_KEY_PREFIX_REGEX = /^sentry-/; +let lastHref; /** - * Max length of a serialized baggage string + * Add an instrumentation handler for when a fetch request happens. + * The handler function is called once when the request starts and once when it ends, + * which can be identified by checking if it has an `endTimestamp`. * - * https://www.w3.org/TR/baggage/#limits + * Use at your own risk, this might break without changelog notice, only used internally. + * @hidden */ -const MAX_BAGGAGE_STRING_LENGTH = 8192; +function addHistoryInstrumentationHandler(handler) { + const type = 'history'; + _handlers.addHandler(type, handler); + _handlers.maybeInstrument(type, instrumentHistory); +} -/** - * Takes a baggage header and turns it into Dynamic Sampling Context, by extracting all the "sentry-" prefixed values - * from it. - * - * @param baggageHeader A very bread definition of a baggage header as it might appear in various frameworks. - * @returns The Dynamic Sampling Context that was found on `baggageHeader`, if there was any, `undefined` otherwise. - */ -function baggageHeaderToDynamicSamplingContext( - // Very liberal definition of what any incoming header might look like - baggageHeader, -) { - if (!is.isString(baggageHeader) && !Array.isArray(baggageHeader)) { - return undefined; +function instrumentHistory() { + if (!supportsHistory.supportsHistory()) { + return; } - // Intermediary object to store baggage key value pairs of incoming baggage headers on. - // It is later used to read Sentry-DSC-values from. - let baggageObject = {}; - - if (Array.isArray(baggageHeader)) { - // Combine all baggage headers into one object containing the baggage values so we can later read the Sentry-DSC-values from it - baggageObject = baggageHeader.reduce((acc, curr) => { - const currBaggageObject = baggageHeaderToObject(curr); - for (const key of Object.keys(currBaggageObject)) { - acc[key] = currBaggageObject[key]; + const oldOnPopState = WINDOW.onpopstate; + WINDOW.onpopstate = function ( ...args) { + const to = WINDOW.location.href; + // keep track of the current URL state, as we always receive only the updated state + const from = lastHref; + lastHref = to; + const handlerData = { from, to }; + _handlers.triggerHandlers('history', handlerData); + if (oldOnPopState) { + // Apparently this can throw in Firefox when incorrectly implemented plugin is installed. + // https://github.com/getsentry/sentry-javascript/issues/3344 + // https://github.com/bugsnag/bugsnag-js/issues/469 + try { + return oldOnPopState.apply(this, args); + } catch (_oO) { + // no-empty } - return acc; - }, {}); - } else { - // Return undefined if baggage header is an empty string (technically an empty baggage header is not spec conform but - // this is how we choose to handle it) - if (!baggageHeader) { - return undefined; } + }; - baggageObject = baggageHeaderToObject(baggageHeader); + function historyReplacementFunction(originalHistoryFunction) { + return function ( ...args) { + const url = args.length > 2 ? args[2] : undefined; + if (url) { + // coerce to string (this is what pushState does) + const from = lastHref; + const to = String(url); + // keep track of the current URL state, as we always receive only the updated state + lastHref = to; + const handlerData = { from, to }; + _handlers.triggerHandlers('history', handlerData); + } + return originalHistoryFunction.apply(this, args); + }; } - // Read all "sentry-" prefixed values out of the baggage object and put it onto a dynamic sampling context object. - const dynamicSamplingContext = Object.entries(baggageObject).reduce((acc, [key, value]) => { - if (key.match(SENTRY_BAGGAGE_KEY_PREFIX_REGEX)) { - const nonPrefixedKey = key.slice(SENTRY_BAGGAGE_KEY_PREFIX.length); - acc[nonPrefixedKey] = value; - } - return acc; - }, {}); - - // Only return a dynamic sampling context object if there are keys in it. - // A keyless object means there were no sentry values on the header, which means that there is no DSC. - if (Object.keys(dynamicSamplingContext).length > 0) { - return dynamicSamplingContext ; - } else { - return undefined; - } + object.fill(WINDOW.history, 'pushState', historyReplacementFunction); + object.fill(WINDOW.history, 'replaceState', historyReplacementFunction); } -/** - * Turns a Dynamic Sampling Object into a baggage header by prefixing all the keys on the object with "sentry-". - * - * @param dynamicSamplingContext The Dynamic Sampling Context to turn into a header. For convenience and compatibility - * with the `getDynamicSamplingContext` method on the Transaction class ,this argument can also be `undefined`. If it is - * `undefined` the function will return `undefined`. - * @returns a baggage header, created from `dynamicSamplingContext`, or `undefined` either if `dynamicSamplingContext` - * was `undefined`, or if `dynamicSamplingContext` didn't contain any values. - */ -function dynamicSamplingContextToSentryBaggageHeader( - // this also takes undefined for convenience and bundle size in other places - dynamicSamplingContext, -) { - if (!dynamicSamplingContext) { - return undefined; - } +exports.addHistoryInstrumentationHandler = addHistoryInstrumentationHandler; - // Prefix all DSC keys with "sentry-" and put them into a new object - const sentryPrefixedDSC = Object.entries(dynamicSamplingContext).reduce( - (acc, [dscKey, dscValue]) => { - if (dscValue) { - acc[`${SENTRY_BAGGAGE_KEY_PREFIX}${dscKey}`] = dscValue; - } - return acc; - }, - {}, - ); - return objectToBaggageHeader(sentryPrefixedDSC); -} +},{"../debug-build.js":146,"../logger.js":164,"../object.js":171,"../vendor/supportsHistory.js":186,"../worldwide.js":187,"./_handlers.js":153}],160:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); -/** - * Will parse a baggage header, which is a simple key-value map, into a flat object. - * - * @param baggageHeader The baggage header to parse. - * @returns a flat object containing all the key-value pairs from `baggageHeader`. - */ -function baggageHeaderToObject(baggageHeader) { - return baggageHeader - .split(',') - .map(baggageEntry => baggageEntry.split('=').map(keyOrValue => decodeURIComponent(keyOrValue.trim()))) - .reduce((acc, [key, value]) => { - acc[key] = value; - return acc; - }, {}); -} +const debugBuild = require('../debug-build.js'); +const logger = require('../logger.js'); +const console = require('./console.js'); +const dom = require('./dom.js'); +const fetch = require('./fetch.js'); +const globalError = require('./globalError.js'); +const globalUnhandledRejection = require('./globalUnhandledRejection.js'); +const history = require('./history.js'); +const xhr = require('./xhr.js'); + +// TODO(v8): Consider moving this file (or at least parts of it) into the browser package. The registered handlers are mostly non-generic and we risk leaking runtime specific code into generic packages. /** - * Turns a flat object (key-value pairs) into a baggage header, which is also just key-value pairs. - * - * @param object The object to turn into a baggage header. - * @returns a baggage header string, or `undefined` if the object didn't have any values, since an empty baggage header - * is not spec compliant. + * Add handler that will be called when given type of instrumentation triggers. + * Use at your own risk, this might break without changelog notice, only used internally. + * @hidden + * @deprecated Use the proper function per instrumentation type instead! */ -function objectToBaggageHeader(object) { - if (Object.keys(object).length === 0) { - // An empty baggage header is not spec compliant: We return undefined. - return undefined; +function addInstrumentationHandler(type, callback) { + switch (type) { + case 'console': + return console.addConsoleInstrumentationHandler(callback); + case 'dom': + return dom.addClickKeypressInstrumentationHandler(callback); + case 'xhr': + return xhr.addXhrInstrumentationHandler(callback); + case 'fetch': + return fetch.addFetchInstrumentationHandler(callback); + case 'history': + return history.addHistoryInstrumentationHandler(callback); + case 'error': + return globalError.addGlobalErrorInstrumentationHandler(callback); + case 'unhandledrejection': + return globalUnhandledRejection.addGlobalUnhandledRejectionInstrumentationHandler(callback); + default: + debugBuild.DEBUG_BUILD && logger.logger.warn('unknown instrumentation type:', type); } - - return Object.entries(object).reduce((baggageHeader, [objectKey, objectValue], currentIndex) => { - const baggageEntry = `${encodeURIComponent(objectKey)}=${encodeURIComponent(objectValue)}`; - const newBaggageHeader = currentIndex === 0 ? baggageEntry : `${baggageHeader},${baggageEntry}`; - if (newBaggageHeader.length > MAX_BAGGAGE_STRING_LENGTH) { - debugBuild.DEBUG_BUILD && - logger.logger.warn( - `Not adding key: ${objectKey} with val: ${objectValue} to baggage header due to exceeding baggage size limits.`, - ); - return baggageHeader; - } else { - return newBaggageHeader; - } - }, ''); } -exports.BAGGAGE_HEADER_NAME = BAGGAGE_HEADER_NAME; -exports.MAX_BAGGAGE_STRING_LENGTH = MAX_BAGGAGE_STRING_LENGTH; -exports.SENTRY_BAGGAGE_KEY_PREFIX = SENTRY_BAGGAGE_KEY_PREFIX; -exports.SENTRY_BAGGAGE_KEY_PREFIX_REGEX = SENTRY_BAGGAGE_KEY_PREFIX_REGEX; -exports.baggageHeaderToDynamicSamplingContext = baggageHeaderToDynamicSamplingContext; -exports.dynamicSamplingContextToSentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader; +exports.addConsoleInstrumentationHandler = console.addConsoleInstrumentationHandler; +exports.addClickKeypressInstrumentationHandler = dom.addClickKeypressInstrumentationHandler; +exports.addFetchInstrumentationHandler = fetch.addFetchInstrumentationHandler; +exports.addGlobalErrorInstrumentationHandler = globalError.addGlobalErrorInstrumentationHandler; +exports.addGlobalUnhandledRejectionInstrumentationHandler = globalUnhandledRejection.addGlobalUnhandledRejectionInstrumentationHandler; +exports.addHistoryInstrumentationHandler = history.addHistoryInstrumentationHandler; +exports.SENTRY_XHR_DATA_KEY = xhr.SENTRY_XHR_DATA_KEY; +exports.addXhrInstrumentationHandler = xhr.addXhrInstrumentationHandler; +exports.addInstrumentationHandler = addInstrumentationHandler; -},{"./debug-build.js":133,"./is.js":149,"./logger.js":151}],123:[function(require,module,exports){ +},{"../debug-build.js":146,"../logger.js":164,"./console.js":154,"./dom.js":155,"./fetch.js":156,"./globalError.js":157,"./globalUnhandledRejection.js":158,"./history.js":159,"./xhr.js":161}],161:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const is = require('./is.js'); -const worldwide = require('./worldwide.js'); +const is = require('../is.js'); +const object = require('../object.js'); +const worldwide = require('../worldwide.js'); +const _handlers = require('./_handlers.js'); -// eslint-disable-next-line deprecation/deprecation -const WINDOW = worldwide.getGlobalObject(); +const WINDOW = worldwide.GLOBAL_OBJ ; -const DEFAULT_MAX_STRING_LENGTH = 80; +const SENTRY_XHR_DATA_KEY = '__sentry_xhr_v3__'; /** - * Given a child DOM element, returns a query-selector statement describing that - * and its ancestors - * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz] - * @returns generated DOM path + * Add an instrumentation handler for when an XHR request happens. + * The handler function is called once when the request starts and once when it ends, + * which can be identified by checking if it has an `endTimestamp`. + * + * Use at your own risk, this might break without changelog notice, only used internally. + * @hidden */ -function htmlTreeAsString( - elem, - options = {}, -) { - if (!elem) { - return ''; +function addXhrInstrumentationHandler(handler) { + const type = 'xhr'; + _handlers.addHandler(type, handler); + _handlers.maybeInstrument(type, instrumentXHR); +} + +/** Exported only for tests. */ +function instrumentXHR() { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!(WINDOW ).XMLHttpRequest) { + return; } - // try/catch both: - // - accessing event.target (see getsentry/raven-js#838, #768) - // - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly - // - can throw an exception in some circumstances. - try { - let currentElem = elem ; - const MAX_TRAVERSE_HEIGHT = 5; - const out = []; - let height = 0; - let len = 0; - const separator = ' > '; - const sepLength = separator.length; - let nextStr; - const keyAttrs = Array.isArray(options) ? options : options.keyAttrs; - const maxStringLength = (!Array.isArray(options) && options.maxStringLength) || DEFAULT_MAX_STRING_LENGTH; + const xhrproto = XMLHttpRequest.prototype; - while (currentElem && height++ < MAX_TRAVERSE_HEIGHT) { - nextStr = _htmlElementAsString(currentElem, keyAttrs); - // bail out if - // - nextStr is the 'html' element - // - the length of the string that would be created exceeds maxStringLength - // (ignore this limit if we are on the first iteration) - if (nextStr === 'html' || (height > 1 && len + out.length * sepLength + nextStr.length >= maxStringLength)) { - break; + object.fill(xhrproto, 'open', function (originalOpen) { + return function ( ...args) { + const startTimestamp = Date.now(); + + // open() should always be called with two or more arguments + // But to be on the safe side, we actually validate this and bail out if we don't have a method & url + const method = is.isString(args[0]) ? args[0].toUpperCase() : undefined; + const url = parseUrl(args[1]); + + if (!method || !url) { + return originalOpen.apply(this, args); } - out.push(nextStr); + this[SENTRY_XHR_DATA_KEY] = { + method, + url, + request_headers: {}, + }; - len += nextStr.length; - currentElem = currentElem.parentNode; - } + // if Sentry key appears in URL, don't capture it as a request + if (method === 'POST' && url.match(/sentry_key/)) { + this.__sentry_own_request__ = true; + } - return out.reverse().join(separator); - } catch (_oO) { - return ''; - } -} + const onreadystatechangeHandler = () => { + // For whatever reason, this is not the same instance here as from the outer method + const xhrInfo = this[SENTRY_XHR_DATA_KEY]; -/** - * Returns a simple, query-selector representation of a DOM element - * e.g. [HTMLElement] => input#foo.btn[name=baz] - * @returns generated DOM path - */ -function _htmlElementAsString(el, keyAttrs) { - const elem = el + if (!xhrInfo) { + return; + } -; + if (this.readyState === 4) { + try { + // touching statusCode in some platforms throws + // an exception + xhrInfo.status_code = this.status; + } catch (e) { + /* do nothing */ + } - const out = []; - let className; - let classes; - let key; - let attr; - let i; + const handlerData = { + args: [method, url], + endTimestamp: Date.now(), + startTimestamp, + xhr: this, + }; + _handlers.triggerHandlers('xhr', handlerData); + } + }; - if (!elem || !elem.tagName) { - return ''; - } + if ('onreadystatechange' in this && typeof this.onreadystatechange === 'function') { + object.fill(this, 'onreadystatechange', function (original) { + return function ( ...readyStateArgs) { + onreadystatechangeHandler(); + return original.apply(this, readyStateArgs); + }; + }); + } else { + this.addEventListener('readystatechange', onreadystatechangeHandler); + } - // @ts-expect-error WINDOW has HTMLElement - if (WINDOW.HTMLElement) { - // If using the component name annotation plugin, this value may be available on the DOM node - if (elem instanceof HTMLElement && elem.dataset && elem.dataset['sentryComponent']) { - return elem.dataset['sentryComponent']; - } - } + // Intercepting `setRequestHeader` to access the request headers of XHR instance. + // This will only work for user/library defined headers, not for the default/browser-assigned headers. + // Request cookies are also unavailable for XHR, as `Cookie` header can't be defined by `setRequestHeader`. + object.fill(this, 'setRequestHeader', function (original) { + return function ( ...setRequestHeaderArgs) { + const [header, value] = setRequestHeaderArgs; - out.push(elem.tagName.toLowerCase()); + const xhrInfo = this[SENTRY_XHR_DATA_KEY]; - // Pairs of attribute keys defined in `serializeAttribute` and their values on element. - const keyAttrPairs = - keyAttrs && keyAttrs.length - ? keyAttrs.filter(keyAttr => elem.getAttribute(keyAttr)).map(keyAttr => [keyAttr, elem.getAttribute(keyAttr)]) - : null; + if (xhrInfo && is.isString(header) && is.isString(value)) { + xhrInfo.request_headers[header.toLowerCase()] = value; + } - if (keyAttrPairs && keyAttrPairs.length) { - keyAttrPairs.forEach(keyAttrPair => { - out.push(`[${keyAttrPair[0]}="${keyAttrPair[1]}"]`); - }); - } else { - if (elem.id) { - out.push(`#${elem.id}`); - } + return original.apply(this, setRequestHeaderArgs); + }; + }); - // eslint-disable-next-line prefer-const - className = elem.className; - if (className && is.isString(className)) { - classes = className.split(/\s+/); - for (i = 0; i < classes.length; i++) { - out.push(`.${classes[i]}`); + return originalOpen.apply(this, args); + }; + }); + + object.fill(xhrproto, 'send', function (originalSend) { + return function ( ...args) { + const sentryXhrData = this[SENTRY_XHR_DATA_KEY]; + + if (!sentryXhrData) { + return originalSend.apply(this, args); } - } + + if (args[0] !== undefined) { + sentryXhrData.body = args[0]; + } + + const handlerData = { + args: [sentryXhrData.method, sentryXhrData.url], + startTimestamp: Date.now(), + xhr: this, + }; + _handlers.triggerHandlers('xhr', handlerData); + + return originalSend.apply(this, args); + }; + }); +} + +function parseUrl(url) { + if (is.isString(url)) { + return url; } - const allowedAttrs = ['aria-label', 'type', 'name', 'title', 'alt']; - for (i = 0; i < allowedAttrs.length; i++) { - key = allowedAttrs[i]; - attr = elem.getAttribute(key); - if (attr) { - out.push(`[${key}="${attr}"]`); - } + + try { + // url can be a string or URL + // but since URL is not available in IE11, we do not check for it, + // but simply assume it is an URL and return `toString()` from it (which returns the full URL) + // If that fails, we just return undefined + return (url ).toString(); + } catch (e2) {} // eslint-disable-line no-empty + + return undefined; +} + +exports.SENTRY_XHR_DATA_KEY = SENTRY_XHR_DATA_KEY; +exports.addXhrInstrumentationHandler = addXhrInstrumentationHandler; +exports.instrumentXHR = instrumentXHR; + + +},{"../is.js":162,"../object.js":171,"../worldwide.js":187,"./_handlers.js":153}],162:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +// eslint-disable-next-line @typescript-eslint/unbound-method +const objectToString = Object.prototype.toString; + +/** + * Checks whether given value's type is one of a few Error or Error-like + * {@link isError}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isError(wat) { + switch (objectToString.call(wat)) { + case '[object Error]': + case '[object Exception]': + case '[object DOMException]': + return true; + default: + return isInstanceOf(wat, Error); } - return out.join(''); } - /** - * A safe form of location.href + * Checks whether given value is an instance of the given built-in class. + * + * @param wat The value to be checked + * @param className + * @returns A boolean representing the result. */ -function getLocationHref() { - try { - return WINDOW.document.location.href; - } catch (oO) { - return ''; - } +function isBuiltin(wat, className) { + return objectToString.call(wat) === `[object ${className}]`; } /** - * Gets a DOM element by using document.querySelector. - * - * This wrapper will first check for the existance of the function before - * actually calling it so that we don't have to take care of this check, - * every time we want to access the DOM. - * - * Reason: DOM/querySelector is not available in all environments. - * - * We have to cast to any because utils can be consumed by a variety of environments, - * and we don't want to break TS users. If you know what element will be selected by - * `document.querySelector`, specify it as part of the generic call. For example, - * `const element = getDomElement('selector');` + * Checks whether given value's type is ErrorEvent + * {@link isErrorEvent}. * - * @param selector the selector string passed on to document.querySelector + * @param wat A value to be checked. + * @returns A boolean representing the result. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function getDomElement(selector) { - if (WINDOW.document && WINDOW.document.querySelector) { - return WINDOW.document.querySelector(selector) ; - } - return null; +function isErrorEvent(wat) { + return isBuiltin(wat, 'ErrorEvent'); } /** - * Given a DOM element, traverses up the tree until it finds the first ancestor node - * that has the `data-sentry-component` attribute. This attribute is added at build-time - * by projects that have the component name annotation plugin installed. + * Checks whether given value's type is DOMError + * {@link isDOMError}. * - * @returns a string representation of the component for the provided DOM element, or `null` if not found + * @param wat A value to be checked. + * @returns A boolean representing the result. */ -function getComponentName(elem) { - // @ts-expect-error WINDOW has HTMLElement - if (!WINDOW.HTMLElement) { - return null; - } - - let currentElem = elem ; - const MAX_TRAVERSE_HEIGHT = 5; - for (let i = 0; i < MAX_TRAVERSE_HEIGHT; i++) { - if (!currentElem) { - return null; - } - - if (currentElem instanceof HTMLElement && currentElem.dataset['sentryComponent']) { - return currentElem.dataset['sentryComponent']; - } - - currentElem = currentElem.parentNode; - } - - return null; +function isDOMError(wat) { + return isBuiltin(wat, 'DOMError'); } -exports.getComponentName = getComponentName; -exports.getDomElement = getDomElement; -exports.getLocationHref = getLocationHref; -exports.htmlTreeAsString = htmlTreeAsString; - - -},{"./is.js":149,"./worldwide.js":174}],124:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); +/** + * Checks whether given value's type is DOMException + * {@link isDOMException}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isDOMException(wat) { + return isBuiltin(wat, 'DOMException'); +} -const _nullishCoalesce = require('./_nullishCoalesce.js'); +/** + * Checks whether given value's type is a string + * {@link isString}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isString(wat) { + return isBuiltin(wat, 'String'); +} -// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f +/** + * Checks whether given string is parameterized + * {@link isParameterizedString}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isParameterizedString(wat) { + return ( + typeof wat === 'object' && + wat !== null && + '__sentry_template_string__' in wat && + '__sentry_template_values__' in wat + ); +} /** - * Polyfill for the nullish coalescing operator (`??`), when used in situations where at least one of the values is the - * result of an async operation. + * Checks whether given value is a primitive (undefined, null, number, boolean, string, bigint, symbol) + * {@link isPrimitive}. * - * Note that the RHS is wrapped in a function so that if it's a computed value, that evaluation won't happen unless the - * LHS evaluates to a nullish value, to mimic the operator's short-circuiting behavior. + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isPrimitive(wat) { + return wat === null || isParameterizedString(wat) || (typeof wat !== 'object' && typeof wat !== 'function'); +} + +/** + * Checks whether given value's type is an object literal, or a class instance. + * {@link isPlainObject}. * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isPlainObject(wat) { + return isBuiltin(wat, 'Object'); +} + +/** + * Checks whether given value's type is an Event instance + * {@link isEvent}. * - * @param lhs The value of the expression to the left of the `??` - * @param rhsFn A function returning the value of the expression to the right of the `??` - * @returns The LHS value, unless it's `null` or `undefined`, in which case, the RHS value + * @param wat A value to be checked. + * @returns A boolean representing the result. */ -async function _asyncNullishCoalesce(lhs, rhsFn) { - return _nullishCoalesce._nullishCoalesce(lhs, rhsFn); +function isEvent(wat) { + return typeof Event !== 'undefined' && isInstanceOf(wat, Event); } -// Sucrase version: -// async function _asyncNullishCoalesce(lhs, rhsFn) { -// if (lhs != null) { -// return lhs; -// } else { -// return await rhsFn(); -// } -// } +/** + * Checks whether given value's type is an Element instance + * {@link isElement}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isElement(wat) { + return typeof Element !== 'undefined' && isInstanceOf(wat, Element); +} -exports._asyncNullishCoalesce = _asyncNullishCoalesce; +/** + * Checks whether given value's type is an regexp + * {@link isRegExp}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isRegExp(wat) { + return isBuiltin(wat, 'RegExp'); +} +/** + * Checks whether given value has a then function. + * @param wat A value to be checked. + */ +function isThenable(wat) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return Boolean(wat && wat.then && typeof wat.then === 'function'); +} -},{"./_nullishCoalesce.js":127}],125:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); +/** + * Checks whether given value's type is a SyntheticEvent + * {@link isSyntheticEvent}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isSyntheticEvent(wat) { + return isPlainObject(wat) && 'nativeEvent' in wat && 'preventDefault' in wat && 'stopPropagation' in wat; +} /** - * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, - * descriptors, and functions, for situations in which at least one part of the expression is async. + * Checks whether given value is NaN + * {@link isNaN}. * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See - * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isNaN(wat) { + return typeof wat === 'number' && wat !== wat; +} + +/** + * Checks whether given value's type is an instance of provided constructor. + * {@link isInstanceOf}. * - * @param ops Array result of expression conversion - * @returns The value of the expression + * @param wat A value to be checked. + * @param base A constructor to be used in a check. + * @returns A boolean representing the result. */ -async function _asyncOptionalChain(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; - // by checking for loose equality to `null`, we catch both `null` and `undefined` - if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { - // really we're meaning to return `undefined` as an actual value here, but it saves bytes not to write it - return; - } - if (op === 'access' || op === 'optionalAccess') { - lastAccessLHS = value; - value = await fn(value); - } else if (op === 'call' || op === 'optionalCall') { - value = await fn((...args) => (value ).call(lastAccessLHS, ...args)); - lastAccessLHS = undefined; - } +function isInstanceOf(wat, base) { + try { + return wat instanceof base; + } catch (_e) { + return false; } - return value; } -// Sucrase version: -// async function _asyncOptionalChain(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 = await fn(value); -// } else if (op === 'call' || op === 'optionalCall') { -// value = await fn((...args) => value.call(lastAccessLHS, ...args)); -// lastAccessLHS = undefined; -// } -// } -// return value; -// } +/** + * Checks whether given value's type is a Vue ViewModel. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isVueViewModel(wat) { + // Not using Object.prototype.toString because in Vue 3 it would read the instance's Symbol(Symbol.toStringTag) property. + return !!(typeof wat === 'object' && wat !== null && ((wat ).__isVue || (wat )._isVue)); +} -exports._asyncOptionalChain = _asyncOptionalChain; +exports.isDOMError = isDOMError; +exports.isDOMException = isDOMException; +exports.isElement = isElement; +exports.isError = isError; +exports.isErrorEvent = isErrorEvent; +exports.isEvent = isEvent; +exports.isInstanceOf = isInstanceOf; +exports.isNaN = isNaN; +exports.isParameterizedString = isParameterizedString; +exports.isPlainObject = isPlainObject; +exports.isPrimitive = isPrimitive; +exports.isRegExp = isRegExp; +exports.isString = isString; +exports.isSyntheticEvent = isSyntheticEvent; +exports.isThenable = isThenable; +exports.isVueViewModel = isVueViewModel; -},{}],126:[function(require,module,exports){ +},{}],163:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const _asyncOptionalChain = require('./_asyncOptionalChain.js'); - -// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f +const node = require('./node.js'); +const worldwide = require('./worldwide.js'); /** - * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, - * descriptors, and functions, in cases where the value of the expression is to be deleted. - * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See - * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 - * - * @param ops Array result of expression conversion - * @returns The return value of the `delete` operator: `true`, unless the deletion target is an own, non-configurable - * property (one which can't be deleted or turned into an accessor, and whose enumerability can't be changed), in which - * case `false`. + * Returns true if we are in the browser. */ -async function _asyncOptionalChainDelete(ops) { - const result = (await _asyncOptionalChain._asyncOptionalChain(ops)) ; - // If `result` is `null`, it means we didn't get to the end of the chain and so nothing was deleted (in which case, - // return `true` since that's what `delete` does when it no-ops). If it's non-null, we know the delete happened, in - // which case we return whatever the `delete` returned, which will be a boolean. - return result == null ? true : (result ); +function isBrowser() { + // eslint-disable-next-line no-restricted-globals + return typeof window !== 'undefined' && (!node.isNodeEnv() || isElectronNodeRenderer()); } -// Sucrase version: -// async function asyncOptionalChainDelete(ops) { -// const result = await ASYNC_OPTIONAL_CHAIN_NAME(ops); -// return result == null ? true : result; -// } +// Electron renderers with nodeIntegration enabled are detected as Node.js so we specifically test for them +function isElectronNodeRenderer() { + return ( + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + (worldwide.GLOBAL_OBJ ).process !== undefined && ((worldwide.GLOBAL_OBJ ).process ).type === 'renderer' + ); +} -exports._asyncOptionalChainDelete = _asyncOptionalChainDelete; +exports.isBrowser = isBrowser; -},{"./_asyncOptionalChain.js":125}],127:[function(require,module,exports){ +},{"./node.js":169,"./worldwide.js":187}],164:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f -// -// The MIT License (MIT) -// -// Copyright (c) 2012-2018 various contributors (see AUTHORS) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +const debugBuild = require('./debug-build.js'); +const worldwide = require('./worldwide.js'); -/** - * Polyfill for the nullish coalescing operator (`??`). - * - * Note that the RHS is wrapped in a function so that if it's a computed value, that evaluation won't happen unless the - * LHS evaluates to a nullish value, to mimic the operator's short-circuiting behavior. - * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) - * - * @param lhs The value of the expression to the left of the `??` - * @param rhsFn A function returning the value of the expression to the right of the `??` - * @returns The LHS value, unless it's `null` or `undefined`, in which case, the RHS value - */ -function _nullishCoalesce(lhs, rhsFn) { - // by checking for loose equality to `null`, we catch both `null` and `undefined` - return lhs != null ? lhs : rhsFn(); -} +/** Prefix for logging strings */ +const PREFIX = 'Sentry Logger '; -// Sucrase version: -// function _nullishCoalesce(lhs, rhsFn) { -// if (lhs != null) { -// return lhs; -// } else { -// return rhsFn(); -// } -// } +const CONSOLE_LEVELS = [ + 'debug', + 'info', + 'warn', + 'error', + 'log', + 'assert', + 'trace', +] ; -exports._nullishCoalesce = _nullishCoalesce; +/** This may be mutated by the console instrumentation. */ +const originalConsoleMethods + = {}; -},{}],128:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); +/** JSDoc */ /** - * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, - * descriptors, and functions. - * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) - * See https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 + * Temporarily disable sentry console instrumentations. * - * @param ops Array result of expression conversion - * @returns The value of the expression + * @param callback The function to run against the original `console` messages + * @returns The results of the callback */ -function _optionalChain(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; - // by checking for loose equality to `null`, we catch both `null` and `undefined` - if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { - // really we're meaning to return `undefined` as an actual value here, but it saves bytes not to write it - return; - } - 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; - } +function consoleSandbox(callback) { + if (!('console' in worldwide.GLOBAL_OBJ)) { + return callback(); } - return value; -} -// Sucrase version -// function _optionalChain(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 console = worldwide.GLOBAL_OBJ.console ; + const wrappedFuncs = {}; -exports._optionalChain = _optionalChain; + const wrappedLevels = Object.keys(originalConsoleMethods) ; + // Restore all wrapped console methods + wrappedLevels.forEach(level => { + const originalConsoleMethod = originalConsoleMethods[level] ; + wrappedFuncs[level] = console[level] ; + console[level] = originalConsoleMethod; + }); -},{}],129:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + try { + return callback(); + } finally { + // Revert restoration to wrapped state + wrappedLevels.forEach(level => { + console[level] = wrappedFuncs[level] ; + }); + } +} -const _optionalChain = require('./_optionalChain.js'); +function makeLogger() { + let enabled = false; + const logger = { + enable: () => { + enabled = true; + }, + disable: () => { + enabled = false; + }, + isEnabled: () => enabled, + }; -// https://github.com/alangpierce/sucrase/tree/265887868966917f3b924ce38dfad01fbab1329f + if (debugBuild.DEBUG_BUILD) { + CONSOLE_LEVELS.forEach(name => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logger[name] = (...args) => { + if (enabled) { + consoleSandbox(() => { + worldwide.GLOBAL_OBJ.console[name](`${PREFIX}[${name}]:`, ...args); + }); + } + }; + }); + } else { + CONSOLE_LEVELS.forEach(name => { + logger[name] = () => undefined; + }); + } -/** - * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, - * descriptors, and functions, in cases where the value of the expression is to be deleted. - * - * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See - * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 - * - * @param ops Array result of expression conversion - * @returns The return value of the `delete` operator: `true`, unless the deletion target is an own, non-configurable - * property (one which can't be deleted or turned into an accessor, and whose enumerability can't be changed), in which - * case `false`. - */ -function _optionalChainDelete(ops) { - const result = _optionalChain._optionalChain(ops) ; - // If `result` is `null`, it means we didn't get to the end of the chain and so nothing was deleted (in which case, - // return `true` since that's what `delete` does when it no-ops). If it's non-null, we know the delete happened, in - // which case we return whatever the `delete` returned, which will be a boolean. - return result == null ? true : result; + return logger ; } -// Sucrase version: -// function _optionalChainDelete(ops) { -// const result = _optionalChain(ops); -// // by checking for loose equality to `null`, we catch both `null` and `undefined` -// return result == null ? true : result; -// } +const logger = makeLogger(); -exports._optionalChainDelete = _optionalChainDelete; +exports.CONSOLE_LEVELS = CONSOLE_LEVELS; +exports.consoleSandbox = consoleSandbox; +exports.logger = logger; +exports.originalConsoleMethods = originalConsoleMethods; -},{"./_optionalChain.js":128}],130:[function(require,module,exports){ +},{"./debug-build.js":146,"./worldwide.js":187}],165:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -/** - * Creates a cache that evicts keys in fifo order - * @param size {Number} - */ -function makeFifoCache( - size, -) +/** A simple Least Recently Used map */ +class LRUMap { - { - // Maintain a fifo queue of keys, we cannot rely on Object.keys as the browser may not support it. - let evictionOrder = []; - let cache = {}; + constructor( _maxSize) {this._maxSize = _maxSize; + this._cache = new Map(); + } - return { - add(key, value) { - while (evictionOrder.length >= size) { - // shift is O(n) but this is small size and only happens if we are - // exceeding the cache size so it should be fine. - const evictCandidate = evictionOrder.shift(); + /** Get the current size of the cache */ + get size() { + return this._cache.size; + } - if (evictCandidate !== undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete cache[evictCandidate]; - } - } + /** Get an entry or undefined if it was not in the cache. Re-inserts to update the recently used order */ + get(key) { + const value = this._cache.get(key); + if (value === undefined) { + return undefined; + } + // Remove and re-insert to update the order + this._cache.delete(key); + this._cache.set(key, value); + return value; + } - // in case we have a collision, delete the old key. - if (cache[key]) { - this.delete(key); - } + /** Insert an entry and evict an older entry if we've reached maxSize */ + set(key, value) { + if (this._cache.size >= this._maxSize) { + // keys() returns an iterator in insertion order so keys().next() gives us the oldest key + this._cache.delete(this._cache.keys().next().value); + } + this._cache.set(key, value); + } - evictionOrder.push(key); - cache[key] = value; - }, - clear() { - cache = {}; - evictionOrder = []; - }, - get(key) { - return cache[key]; - }, - size() { - return evictionOrder.length; - }, - // Delete cache key and return true if it existed, false otherwise. - delete(key) { - if (!cache[key]) { - return false; - } + /** Remove an entry and return the entry if it was in the cache */ + remove(key) { + const value = this._cache.get(key); + if (value) { + this._cache.delete(key); + } + return value; + } - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete cache[key]; + /** Clear all entries */ + clear() { + this._cache.clear(); + } - for (let i = 0; i < evictionOrder.length; i++) { - if (evictionOrder[i] === key) { - evictionOrder.splice(i, 1); - break; - } - } + /** Get all the keys */ + keys() { + return Array.from(this._cache.keys()); + } - return true; - }, - }; + /** Get all the values */ + values() { + const values = []; + this._cache.forEach(value => values.push(value)); + return values; + } } -exports.makeFifoCache = makeFifoCache; +exports.LRUMap = LRUMap; -},{}],131:[function(require,module,exports){ +},{}],166:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const envelope = require('./envelope.js'); -const time = require('./time.js'); +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-explicit-any */ /** - * Creates client report envelope - * @param discarded_events An array of discard events - * @param dsn A DSN that can be set on the header. Optional. + * Helper to decycle json objects */ -function createClientReportEnvelope( - discarded_events, - dsn, - timestamp, -) { - const clientReportItem = [ - { type: 'client_report' }, - { - timestamp: timestamp || time.dateTimestampInSeconds(), - discarded_events, - }, - ]; - return envelope.createEnvelope(dsn ? { dsn } : {}, [clientReportItem]); +function memoBuilder() { + const hasWeakSet = typeof WeakSet === 'function'; + const inner = hasWeakSet ? new WeakSet() : []; + function memoize(obj) { + if (hasWeakSet) { + if (inner.has(obj)) { + return true; + } + inner.add(obj); + return false; + } + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < inner.length; i++) { + const value = inner[i]; + if (value === obj) { + return true; + } + } + inner.push(obj); + return false; + } + + function unmemoize(obj) { + if (hasWeakSet) { + inner.delete(obj); + } else { + for (let i = 0; i < inner.length; i++) { + if (inner[i] === obj) { + inner.splice(i, 1); + break; + } + } + } + } + return [memoize, unmemoize]; } -exports.createClientReportEnvelope = createClientReportEnvelope; +exports.memoBuilder = memoBuilder; -},{"./envelope.js":136,"./time.js":168}],132:[function(require,module,exports){ +},{}],167:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -/** - * This code was originally copied from the 'cookie` module at v0.5.0 and was simplified for our use case. - * https://github.com/jshttp/cookie/blob/a0c84147aab6266bdb3996cf4062e93907c0b0fc/index.js - * It had the following license: - * - * (The MIT License) - * - * Copyright (c) 2012-2014 Roman Shtylman - * Copyright (c) 2015 Douglas Christopher Wilson - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * 'Software'), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ +const object = require('./object.js'); +const string = require('./string.js'); +const worldwide = require('./worldwide.js'); /** - * Parses a cookie string + * UUID4 generator + * + * @returns string Generated UUID4. */ -function parseCookie(str) { - const obj = {}; - let index = 0; - - while (index < str.length) { - const eqIdx = str.indexOf('=', index); - - // no more cookie pairs - if (eqIdx === -1) { - break; - } - - let endIdx = str.indexOf(';', index); +function uuid4() { + const gbl = worldwide.GLOBAL_OBJ ; + const crypto = gbl.crypto || gbl.msCrypto; - if (endIdx === -1) { - endIdx = str.length; - } else if (endIdx < eqIdx) { - // backtrack on prior semicolon - index = str.lastIndexOf(';', eqIdx - 1) + 1; - continue; + let getRandomByte = () => Math.random() * 16; + try { + if (crypto && crypto.randomUUID) { + return crypto.randomUUID().replace(/-/g, ''); } - - const key = str.slice(index, eqIdx).trim(); - - // only assign once - if (undefined === obj[key]) { - let val = str.slice(eqIdx + 1, endIdx).trim(); - - // quoted values - if (val.charCodeAt(0) === 0x22) { - val = val.slice(1, -1); - } - - try { - obj[key] = val.indexOf('%') !== -1 ? decodeURIComponent(val) : val; - } catch (e) { - obj[key] = val; - } + if (crypto && crypto.getRandomValues) { + getRandomByte = () => { + // crypto.getRandomValues might return undefined instead of the typed array + // in old Chromium versions (e.g. 23.0.1235.0 (151422)) + // However, `typedArray` is still filled in-place. + // @see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#typedarray + const typedArray = new Uint8Array(1); + crypto.getRandomValues(typedArray); + return typedArray[0]; + }; } - - index = endIdx + 1; + } catch (_) { + // some runtimes can crash invoking crypto + // https://github.com/getsentry/sentry-javascript/issues/8935 } - return obj; + // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 + // Concatenating the following numbers as strings results in '10000000100040008000100000000000' + return (([1e7] ) + 1e3 + 4e3 + 8e3 + 1e11).replace(/[018]/g, c => + // eslint-disable-next-line no-bitwise + ((c ) ^ ((getRandomByte() & 15) >> ((c ) / 4))).toString(16), + ); } -exports.parseCookie = parseCookie; - - -},{}],133:[function(require,module,exports){ -arguments[4][26][0].apply(exports,arguments) -},{"dup":26}],134:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); - -const debugBuild = require('./debug-build.js'); -const logger = require('./logger.js'); +function getFirstException(event) { + return event.exception && event.exception.values ? event.exception.values[0] : undefined; +} -/** Regular expression used to parse a Dsn. */ -const DSN_REGEX = /^(?:(\w+):)\/\/(?:(\w+)(?::(\w+)?)?@)([\w.-]+)(?::(\d+))?\/(.+)/; +/** + * Extracts either message or type+value from an event that can be used for user-facing logs + * @returns event's description + */ +function getEventDescription(event) { + const { message, event_id: eventId } = event; + if (message) { + return message; + } -function isValidProtocol(protocol) { - return protocol === 'http' || protocol === 'https'; + const firstException = getFirstException(event); + if (firstException) { + if (firstException.type && firstException.value) { + return `${firstException.type}: ${firstException.value}`; + } + return firstException.type || firstException.value || eventId || ''; + } + return eventId || ''; } /** - * Renders the string representation of this Dsn. - * - * By default, this will render the public representation without the password - * component. To get the deprecated private representation, set `withPassword` - * to true. - * - * @param withPassword When set to true, the password will be included. + * Adds exception values, type and value to an synthetic Exception. + * @param event The event to modify. + * @param value Value of the exception. + * @param type Type of the exception. + * @hidden */ -function dsnToString(dsn, withPassword = false) { - const { host, path, pass, port, projectId, protocol, publicKey } = dsn; - return ( - `${protocol}://${publicKey}${withPassword && pass ? `:${pass}` : ''}` + - `@${host}${port ? `:${port}` : ''}/${path ? `${path}/` : path}${projectId}` - ); +function addExceptionTypeValue(event, value, type) { + const exception = (event.exception = event.exception || {}); + const values = (exception.values = exception.values || []); + const firstException = (values[0] = values[0] || {}); + if (!firstException.value) { + firstException.value = value || ''; + } + if (!firstException.type) { + firstException.type = type || 'Error'; + } } /** - * Parses a Dsn from a given string. + * Adds exception mechanism data to a given event. Uses defaults if the second parameter is not passed. * - * @param str A Dsn as string - * @returns Dsn as DsnComponents or undefined if @param str is not a valid DSN string + * @param event The event to modify. + * @param newMechanism Mechanism data to add to the event. + * @hidden */ -function dsnFromString(str) { - const match = DSN_REGEX.exec(str); - - if (!match) { - // This should be logged to the console - logger.consoleSandbox(() => { - // eslint-disable-next-line no-console - console.error(`Invalid Sentry Dsn: ${str}`); - }); - return undefined; +function addExceptionMechanism(event, newMechanism) { + const firstException = getFirstException(event); + if (!firstException) { + return; } - const [protocol, publicKey, pass = '', host, port = '', lastPath] = match.slice(1); - let path = ''; - let projectId = lastPath; + const defaultMechanism = { type: 'generic', handled: true }; + const currentMechanism = firstException.mechanism; + firstException.mechanism = { ...defaultMechanism, ...currentMechanism, ...newMechanism }; - const split = projectId.split('/'); - if (split.length > 1) { - path = split.slice(0, -1).join('/'); - projectId = split.pop() ; + if (newMechanism && 'data' in newMechanism) { + const mergedData = { ...(currentMechanism && currentMechanism.data), ...newMechanism.data }; + firstException.mechanism.data = mergedData; } +} - if (projectId) { - const projectMatch = projectId.match(/^\d+/); - if (projectMatch) { - projectId = projectMatch[0]; - } - } +// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +const SEMVER_REGEXP = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; - return dsnFromComponents({ host, pass, path, projectId, port, protocol: protocol , publicKey }); -} +/** + * Represents Semantic Versioning object + */ -function dsnFromComponents(components) { +/** + * Parses input into a SemVer interface + * @param input string representation of a semver version + */ +function parseSemver(input) { + const match = input.match(SEMVER_REGEXP) || []; + const major = parseInt(match[1], 10); + const minor = parseInt(match[2], 10); + const patch = parseInt(match[3], 10); return { - protocol: components.protocol, - publicKey: components.publicKey || '', - pass: components.pass || '', - host: components.host, - port: components.port || '', - path: components.path || '', - projectId: components.projectId, + buildmetadata: match[5], + major: isNaN(major) ? undefined : major, + minor: isNaN(minor) ? undefined : minor, + patch: isNaN(patch) ? undefined : patch, + prerelease: match[4], }; } -function validateDsn(dsn) { - if (!debugBuild.DEBUG_BUILD) { - return true; +/** + * This function adds context (pre/post/line) lines to the provided frame + * + * @param lines string[] containing all lines + * @param frame StackFrame that will be mutated + * @param linesOfContext number of context lines we want to add pre/post + */ +function addContextToFrame(lines, frame, linesOfContext = 5) { + // When there is no line number in the frame, attaching context is nonsensical and will even break grouping + if (frame.lineno === undefined) { + return; } - const { port, projectId, protocol } = dsn; + const maxLines = lines.length; + const sourceLine = Math.max(Math.min(maxLines - 1, frame.lineno - 1), 0); - const requiredComponents = ['protocol', 'publicKey', 'host', 'projectId']; - const hasMissingRequiredComponent = requiredComponents.find(component => { - if (!dsn[component]) { - logger.logger.error(`Invalid Sentry Dsn: ${component} missing`); - return true; - } - return false; - }); + frame.pre_context = lines + .slice(Math.max(0, sourceLine - linesOfContext), sourceLine) + .map((line) => string.snipLine(line, 0)); - if (hasMissingRequiredComponent) { - return false; - } + frame.context_line = string.snipLine(lines[Math.min(maxLines - 1, sourceLine)], frame.colno || 0); - if (!projectId.match(/^\d+$/)) { - logger.logger.error(`Invalid Sentry Dsn: Invalid projectId ${projectId}`); - return false; - } + frame.post_context = lines + .slice(Math.min(sourceLine + 1, maxLines), sourceLine + 1 + linesOfContext) + .map((line) => string.snipLine(line, 0)); +} - if (!isValidProtocol(protocol)) { - logger.logger.error(`Invalid Sentry Dsn: Invalid protocol ${protocol}`); - return false; +/** + * Checks whether or not we've already captured the given exception (note: not an identical exception - the very object + * in question), and marks it captured if not. + * + * This is useful because it's possible for an error to get captured by more than one mechanism. After we intercept and + * record an error, we rethrow it (assuming we've intercepted it before it's reached the top-level global handlers), so + * that we don't interfere with whatever effects the error might have had were the SDK not there. At that point, because + * the error has been rethrown, it's possible for it to bubble up to some other code we've instrumented. If it's not + * caught after that, it will bubble all the way up to the global handlers (which of course we also instrument). This + * function helps us ensure that even if we encounter the same error more than once, we only record it the first time we + * see it. + * + * Note: It will ignore primitives (always return `false` and not mark them as seen), as properties can't be set on + * them. {@link: Object.objectify} can be used on exceptions to convert any that are primitives into their equivalent + * object wrapper forms so that this check will always work. However, because we need to flag the exact object which + * will get rethrown, and because that rethrowing happens outside of the event processing pipeline, the objectification + * must be done before the exception captured. + * + * @param A thrown exception to check or flag as having been seen + * @returns `true` if the exception has already been captured, `false` if not (with the side effect of marking it seen) + */ +function checkOrSetAlreadyCaught(exception) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (exception && (exception ).__sentry_captured__) { + return true; } - if (port && isNaN(parseInt(port, 10))) { - logger.logger.error(`Invalid Sentry Dsn: Invalid port ${port}`); - return false; + try { + // set it this way rather than by assignment so that it's not ennumerable and therefore isn't recorded by the + // `ExtraErrorData` integration + object.addNonEnumerableProperty(exception , '__sentry_captured__', true); + } catch (err) { + // `exception` is a primitive, so we can't mark it seen } - return true; + return false; } /** - * Creates a valid Sentry Dsn object, identifying a Sentry instance and project. - * @returns a valid DsnComponents object or `undefined` if @param from is an invalid DSN source + * Checks whether the given input is already an array, and if it isn't, wraps it in one. + * + * @param maybeArray Input to turn into an array, if necessary + * @returns The input, if already an array, or an array with the input as the only element, if not */ -function makeDsn(from) { - const components = typeof from === 'string' ? dsnFromString(from) : dsnFromComponents(from); - if (!components || !validateDsn(components)) { - return undefined; - } - return components; +function arrayify(maybeArray) { + return Array.isArray(maybeArray) ? maybeArray : [maybeArray]; } -exports.dsnFromString = dsnFromString; -exports.dsnToString = dsnToString; -exports.makeDsn = makeDsn; +exports.addContextToFrame = addContextToFrame; +exports.addExceptionMechanism = addExceptionMechanism; +exports.addExceptionTypeValue = addExceptionTypeValue; +exports.arrayify = arrayify; +exports.checkOrSetAlreadyCaught = checkOrSetAlreadyCaught; +exports.getEventDescription = getEventDescription; +exports.parseSemver = parseSemver; +exports.uuid4 = uuid4; -},{"./debug-build.js":133,"./logger.js":151}],135:[function(require,module,exports){ +},{"./object.js":171,"./string.js":178,"./worldwide.js":187}],168:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -/* - * This module exists for optimizations in the build process through rollup and terser. We define some global - * constants, which can be overridden during build. By guarding certain pieces of code with functions that return these - * constants, we can control whether or not they appear in the final bundle. (Any code guarded by a false condition will - * never run, and will hence be dropped during treeshaking.) The two primary uses for this are stripping out calls to - * `logger` and preventing node-related code from appearing in browser bundles. - * - * Attention: - * This file should not be used to define constants/flags that are intended to be used for tree-shaking conducted by - * users. These flags should live in their respective packages, as we identified user tooling (specifically webpack) - * having issues tree-shaking these constants across package boundaries. - * An example for this is the __SENTRY_DEBUG__ constant. It is declared in each package individually because we want - * users to be able to shake away expressions that it guards. - */ - /** - * Figures out if we're building a browser bundle. - * - * @returns true if this is a browser bundle build. + * Does this filename look like it's part of the app code? */ -function isBrowserBundle() { - return typeof __SENTRY_BROWSER_BUNDLE__ !== 'undefined' && !!__SENTRY_BROWSER_BUNDLE__; -} +function filenameIsInApp(filename, isNative = false) { + const isInternal = + isNative || + (filename && + // It's not internal if it's an absolute linux path + !filename.startsWith('/') && + // It's not internal if it's an absolute windows path + !filename.match(/^[A-Z]:/) && + // It's not internal if the path is starting with a dot + !filename.startsWith('.') && + // It's not internal if the frame has a protocol. In node, this is usually the case if the file got pre-processed with a bundler like webpack + !filename.match(/^[a-zA-Z]([a-zA-Z0-9.\-+])*:\/\//)); // Schema from: https://stackoverflow.com/a/3641782 -/** - * Get source of SDK. - */ -function getSDKSource() { - // @ts-expect-error "npm" is injected by rollup during build process - return "npm"; + // in_app is all that's not an internal Node function or a module within node_modules + // note that isNative appears to return true even for node core libraries + // see https://github.com/getsentry/raven-node/issues/176 + + return !isInternal && filename !== undefined && !filename.includes('node_modules/'); } -exports.getSDKSource = getSDKSource; -exports.isBrowserBundle = isBrowserBundle; +/** Node Stack line parser */ +// eslint-disable-next-line complexity +function node(getModule) { + const FILENAME_MATCH = /^\s*[-]{4,}$/; + const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/; + // eslint-disable-next-line complexity + return (line) => { + const lineMatch = line.match(FULL_MATCH); -},{}],136:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + if (lineMatch) { + let object; + let method; + let functionName; + let typeName; + let methodName; -const dsn = require('./dsn.js'); -const normalize = require('./normalize.js'); -const object = require('./object.js'); + if (lineMatch[1]) { + functionName = lineMatch[1]; -/** - * Creates an envelope. - * Make sure to always explicitly provide the generic to this function - * so that the envelope types resolve correctly. - */ -function createEnvelope(headers, items = []) { - return [headers, items] ; -} + let methodStart = functionName.lastIndexOf('.'); + if (functionName[methodStart - 1] === '.') { + methodStart--; + } -/** - * Add an item to an envelope. - * Make sure to always explicitly provide the generic to this function - * so that the envelope types resolve correctly. - */ -function addItemToEnvelope(envelope, newItem) { - const [headers, items] = envelope; - return [headers, [...items, newItem]] ; -} + if (methodStart > 0) { + object = functionName.slice(0, methodStart); + method = functionName.slice(methodStart + 1); + const objectEnd = object.indexOf('.Module'); + if (objectEnd > 0) { + functionName = functionName.slice(objectEnd + 1); + object = object.slice(0, objectEnd); + } + } + typeName = undefined; + } -/** - * Convenience function to loop through the items and item types of an envelope. - * (This function was mostly created because working with envelope types is painful at the moment) - * - * If the callback returns true, the rest of the items will be skipped. - */ -function forEachEnvelopeItem( - envelope, - callback, -) { - const envelopeItems = envelope[1]; + if (method) { + typeName = object; + methodName = method; + } - for (const envelopeItem of envelopeItems) { - const envelopeItemType = envelopeItem[0].type; - const result = callback(envelopeItem, envelopeItemType); + if (method === '') { + methodName = undefined; + functionName = undefined; + } - if (result) { - return true; + if (functionName === undefined) { + methodName = methodName || ''; + functionName = typeName ? `${typeName}.${methodName}` : methodName; + } + + let filename = lineMatch[2] && lineMatch[2].startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2]; + const isNative = lineMatch[5] === 'native'; + + // If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo` + if (filename && filename.match(/\/[A-Z]:/)) { + filename = filename.slice(1); + } + + if (!filename && lineMatch[5] && !isNative) { + filename = lineMatch[5]; + } + + return { + filename, + module: getModule ? getModule(filename) : undefined, + function: functionName, + lineno: parseInt(lineMatch[3], 10) || undefined, + colno: parseInt(lineMatch[4], 10) || undefined, + in_app: filenameIsInApp(filename, isNative), + }; } - } - return false; + if (line.match(FILENAME_MATCH)) { + return { + filename: line, + }; + } + + return undefined; + }; } +exports.filenameIsInApp = filenameIsInApp; +exports.node = node; + + +},{}],169:[function(require,module,exports){ +(function (process){(function (){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const env = require('./env.js'); + /** - * Returns true if the envelope contains any of the given envelope item types + * NOTE: In order to avoid circular dependencies, if you add a function to this module and it needs to print something, + * you must either a) use `console.log` rather than the logger, or b) put your function elsewhere. */ -function envelopeContainsItemType(envelope, types) { - return forEachEnvelopeItem(envelope, (_, type) => types.includes(type)); -} /** - * Encode a string to UTF8. + * Checks whether we're in the Node.js or Browser environment + * + * @returns Answer to given question */ -function encodeUTF8(input, textEncoder) { - const utf8 = textEncoder || new TextEncoder(); - return utf8.encode(input); +function isNodeEnv() { + // explicitly check for browser bundles as those can be optimized statically + // by terser/rollup. + return ( + !env.isBrowserBundle() && + Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]' + ); } /** - * Serializes an envelope. + * Requires a module which is protected against bundler minification. + * + * @param request The module path to resolve */ -function serializeEnvelope(envelope, textEncoder) { - const [envHeaders, items] = envelope; +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any +function dynamicRequire(mod, request) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return mod.require(request); +} - // Initially we construct our envelope as a string and only convert to binary chunks if we encounter binary data - let parts = JSON.stringify(envHeaders); +/** + * Helper for dynamically loading module that should work with linked dependencies. + * The problem is that we _should_ be using `require(require.resolve(moduleName, { paths: [cwd()] }))` + * However it's _not possible_ to do that with Webpack, as it has to know all the dependencies during + * build time. `require.resolve` is also not available in any other way, so we cannot create, + * a fake helper like we do with `dynamicRequire`. + * + * We always prefer to use local package, thus the value is not returned early from each `try/catch` block. + * That is to mimic the behavior of `require.resolve` exactly. + * + * @param moduleName module name to require + * @returns possibly required module + */ +function loadModule(moduleName) { + let mod; - function append(next) { - if (typeof parts === 'string') { - parts = typeof next === 'string' ? parts + next : [encodeUTF8(parts, textEncoder), next]; - } else { - parts.push(typeof next === 'string' ? encodeUTF8(next, textEncoder) : next); - } + try { + mod = dynamicRequire(module, moduleName); + } catch (e) { + // no-empty } - for (const item of items) { - const [itemHeaders, payload] = item; - - append(`\n${JSON.stringify(itemHeaders)}\n`); - - if (typeof payload === 'string' || payload instanceof Uint8Array) { - append(payload); - } else { - let stringifiedPayload; - try { - stringifiedPayload = JSON.stringify(payload); - } catch (e) { - // In case, despite all our efforts to keep `payload` circular-dependency-free, `JSON.strinify()` still - // fails, we try again after normalizing it again with infinite normalization depth. This of course has a - // performance impact but in this case a performance hit is better than throwing. - stringifiedPayload = JSON.stringify(normalize.normalize(payload)); - } - append(stringifiedPayload); - } + try { + const { cwd } = dynamicRequire(module, 'process'); + mod = dynamicRequire(module, `${cwd()}/node_modules/${moduleName}`) ; + } catch (e) { + // no-empty } - return typeof parts === 'string' ? parts : concatBuffers(parts); + return mod; } -function concatBuffers(buffers) { - const totalLength = buffers.reduce((acc, buf) => acc + buf.length, 0); +exports.dynamicRequire = dynamicRequire; +exports.isNodeEnv = isNodeEnv; +exports.loadModule = loadModule; - const merged = new Uint8Array(totalLength); - let offset = 0; - for (const buffer of buffers) { - merged.set(buffer, offset); - offset += buffer.length; - } - return merged; -} +}).call(this)}).call(this,require('_process')) +},{"./env.js":148,"_process":189}],170:[function(require,module,exports){ +(function (global){(function (){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const is = require('./is.js'); +const memo = require('./memo.js'); +const object = require('./object.js'); +const stacktrace = require('./stacktrace.js'); /** - * Parses an envelope + * Recursively normalizes the given object. + * + * - Creates a copy to prevent original input mutation + * - Skips non-enumerable properties + * - When stringifying, calls `toJSON` if implemented + * - Removes circular references + * - Translates non-serializable values (`undefined`/`NaN`/functions) to serializable format + * - Translates known global objects/classes to a string representations + * - Takes care of `Error` object serialization + * - Optionally limits depth of final output + * - Optionally limits number of properties/elements included in any single object/array + * + * @param input The object to be normalized. + * @param depth The max depth to which to normalize the object. (Anything deeper stringified whole.) + * @param maxProperties The max number of elements or properties to be included in any single array or + * object in the normallized output. + * @returns A normalized version of the object, or `"**non-serializable**"` if any errors are thrown during normalization. */ -function parseEnvelope( - env, - textEncoder, - textDecoder, -) { - let buffer = typeof env === 'string' ? textEncoder.encode(env) : env; - - function readBinary(length) { - const bin = buffer.subarray(0, length); - // Replace the buffer with the remaining data excluding trailing newline - buffer = buffer.subarray(length + 1); - return bin; - } - - function readJson() { - let i = buffer.indexOf(0xa); - // If we couldn't find a newline, we must have found the end of the buffer - if (i < 0) { - i = buffer.length; - } - - return JSON.parse(textDecoder.decode(readBinary(i))) ; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function normalize(input, depth = 100, maxProperties = +Infinity) { + try { + // since we're at the outermost level, we don't provide a key + return visit('', input, depth, maxProperties); + } catch (err) { + return { ERROR: `**non-serializable** (${err})` }; } +} - const envelopeHeader = readJson(); +/** JSDoc */ +function normalizeToSize( // eslint-disable-next-line @typescript-eslint/no-explicit-any - const items = []; - - while (buffer.length) { - const itemHeader = readJson(); - const binaryLength = typeof itemHeader.length === 'number' ? itemHeader.length : undefined; + object, + // Default Node.js REPL depth + depth = 3, + // 100kB, as 200kB is max payload size, so half sounds reasonable + maxSize = 100 * 1024, +) { + const normalized = normalize(object, depth); - items.push([itemHeader, binaryLength ? readBinary(binaryLength) : readJson()]); + if (jsonSize(normalized) > maxSize) { + return normalizeToSize(object, depth - 1, maxSize); } - return [envelopeHeader, items]; + return normalized ; } /** - * Creates attachment envelope items + * Visits a node to perform normalization on it + * + * @param key The key corresponding to the given node + * @param value The node to be visited + * @param depth Optional number indicating the maximum recursion depth + * @param maxProperties Optional maximum number of properties/elements included in any single object/array + * @param memo Optional Memo class handling decycling */ -function createAttachmentEnvelopeItem( - attachment, - textEncoder, +function visit( + key, + value, + depth = +Infinity, + maxProperties = +Infinity, + memo$1 = memo.memoBuilder(), ) { - const buffer = typeof attachment.data === 'string' ? encodeUTF8(attachment.data, textEncoder) : attachment.data; - - return [ - object.dropUndefinedKeys({ - type: 'attachment', - length: buffer.length, - filename: attachment.filename, - content_type: attachment.contentType, - attachment_type: attachment.attachmentType, - }), - buffer, - ]; -} + const [memoize, unmemoize] = memo$1; -const ITEM_TYPE_TO_DATA_CATEGORY_MAP = { - session: 'session', - sessions: 'session', - attachment: 'attachment', - transaction: 'transaction', - event: 'error', - client_report: 'internal', - user_report: 'default', - profile: 'profile', - replay_event: 'replay', - replay_recording: 'replay', - check_in: 'monitor', - feedback: 'feedback', - span: 'span', - statsd: 'metric_bucket', -}; + // Get the simple cases out of the way first + if ( + value == null || // this matches null and undefined -> eqeq not eqeqeq + (['number', 'boolean', 'string'].includes(typeof value) && !is.isNaN(value)) + ) { + return value ; + } -/** - * Maps the type of an envelope item to a data category. - */ -function envelopeItemTypeToDataCategory(type) { - return ITEM_TYPE_TO_DATA_CATEGORY_MAP[type]; -} + const stringified = stringifyValue(key, value); -/** Extracts the minimal SDK info from the metadata or an events */ -function getSdkMetadataForEnvelopeHeader(metadataOrEvent) { - if (!metadataOrEvent || !metadataOrEvent.sdk) { - return; + // Anything we could potentially dig into more (objects or arrays) will have come back as `"[object XXXX]"`. + // Everything else will have already been serialized, so if we don't see that pattern, we're done. + if (!stringified.startsWith('[object ')) { + return stringified; } - const { name, version } = metadataOrEvent.sdk; - return { name, version }; -} -/** - * Creates event envelope headers, based on event, sdk info and tunnel - * Note: This function was extracted from the core package to make it available in Replay - */ -function createEventEnvelopeHeaders( - event, - sdkInfo, - tunnel, - dsn$1, -) { - const dynamicSamplingContext = event.sdkProcessingMetadata && event.sdkProcessingMetadata.dynamicSamplingContext; - return { - event_id: event.event_id , - sent_at: new Date().toISOString(), - ...(sdkInfo && { sdk: sdkInfo }), - ...(!!tunnel && dsn$1 && { dsn: dsn.dsnToString(dsn$1) }), - ...(dynamicSamplingContext && { - trace: object.dropUndefinedKeys({ ...dynamicSamplingContext }), - }), - }; -} + // From here on, we can assert that `value` is either an object or an array. -exports.addItemToEnvelope = addItemToEnvelope; -exports.createAttachmentEnvelopeItem = createAttachmentEnvelopeItem; -exports.createEnvelope = createEnvelope; -exports.createEventEnvelopeHeaders = createEventEnvelopeHeaders; -exports.envelopeContainsItemType = envelopeContainsItemType; -exports.envelopeItemTypeToDataCategory = envelopeItemTypeToDataCategory; -exports.forEachEnvelopeItem = forEachEnvelopeItem; -exports.getSdkMetadataForEnvelopeHeader = getSdkMetadataForEnvelopeHeader; -exports.parseEnvelope = parseEnvelope; -exports.serializeEnvelope = serializeEnvelope; + // Do not normalize objects that we know have already been normalized. As a general rule, the + // "__sentry_skip_normalization__" property should only be used sparingly and only should only be set on objects that + // have already been normalized. + if ((value )['__sentry_skip_normalization__']) { + return value ; + } + // We can set `__sentry_override_normalization_depth__` on an object to ensure that from there + // We keep a certain amount of depth. + // This should be used sparingly, e.g. we use it for the redux integration to ensure we get a certain amount of state. + const remainingDepth = + typeof (value )['__sentry_override_normalization_depth__'] === 'number' + ? ((value )['__sentry_override_normalization_depth__'] ) + : depth; -},{"./dsn.js":134,"./normalize.js":157,"./object.js":158}],137:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + // We're also done if we've reached the max depth + if (remainingDepth === 0) { + // At this point we know `serialized` is a string of the form `"[object XXXX]"`. Clean it up so it's just `"[XXXX]"`. + return stringified.replace('object ', ''); + } -/** An error emitted by Sentry SDKs and related utilities. */ -class SentryError extends Error { - /** Display name of this error instance. */ + // If we've already visited this branch, bail out, as it's circular reference. If not, note that we're seeing it now. + if (memoize(value)) { + return '[Circular ~]'; + } - constructor( message, logLevel = 'warn') { - super(message);this.message = message; - this.name = new.target.prototype.constructor.name; - // This sets the prototype to be `Error`, not `SentryError`. It's unclear why we do this, but commenting this line - // out causes various (seemingly totally unrelated) playwright tests consistently time out. FYI, this makes - // instances of `SentryError` fail `obj instanceof SentryError` checks. - Object.setPrototypeOf(this, new.target.prototype); - this.logLevel = logLevel; + // If the value has a `toJSON` method, we call it to extract more information + const valueWithToJSON = value ; + if (valueWithToJSON && typeof valueWithToJSON.toJSON === 'function') { + try { + const jsonValue = valueWithToJSON.toJSON(); + // We need to normalize the return value of `.toJSON()` in case it has circular references + return visit('', jsonValue, remainingDepth - 1, maxProperties, memo$1); + } catch (err) { + // pass (The built-in `toJSON` failed, but we can still try to do it ourselves) + } } -} -exports.SentryError = SentryError; + // At this point we know we either have an object or an array, we haven't seen it before, and we're going to recurse + // because we haven't yet reached the max depth. Create an accumulator to hold the results of visiting each + // property/entry, and keep track of the number of items we add to it. + const normalized = (Array.isArray(value) ? [] : {}) ; + let numAdded = 0; + // Before we begin, convert`Error` and`Event` instances into plain objects, since some of each of their relevant + // properties are non-enumerable and otherwise would get missed. + const visitable = object.convertToPlainObject(value ); -},{}],138:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + for (const visitKey in visitable) { + // Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration. + if (!Object.prototype.hasOwnProperty.call(visitable, visitKey)) { + continue; + } -const is = require('./is.js'); -const misc = require('./misc.js'); -const normalize = require('./normalize.js'); -const object = require('./object.js'); + if (numAdded >= maxProperties) { + normalized[visitKey] = '[MaxProperties ~]'; + break; + } -/** - * Extracts stack frames from the error.stack string - */ -function parseStackFrames(stackParser, error) { - return stackParser(error.stack || '', 1); + // Recursively visit all the child nodes + const visitValue = visitable[visitKey]; + normalized[visitKey] = visit(visitKey, visitValue, remainingDepth - 1, maxProperties, memo$1); + + numAdded++; + } + + // Once we've visited all the branches, remove the parent from memo storage + unmemoize(value); + + // Return accumulated values + return normalized; } +/* eslint-disable complexity */ /** - * Extracts stack frames from the error and builds a Sentry Exception + * Stringify the given value. Handles various known special values and types. + * + * Not meant to be used on simple primitives which already have a string representation, as it will, for example, turn + * the number 1231 into "[Object Number]", nor on `null`, as it will throw. + * + * @param value The value to stringify + * @returns A stringified representation of the given value */ -function exceptionFromError(stackParser, error) { - const exception = { - type: error.name || error.constructor.name, - value: error.message, - }; +function stringifyValue( + key, + // this type is a tiny bit of a cheat, since this function does handle NaN (which is technically a number), but for + // our internal use, it'll do + value, +) { + try { + if (key === 'domain' && value && typeof value === 'object' && (value )._events) { + return '[Domain]'; + } - const frames = parseStackFrames(stackParser, error); - if (frames.length) { - exception.stacktrace = { frames }; - } + if (key === 'domainEmitter') { + return '[DomainEmitter]'; + } - return exception; -} + // It's safe to use `global`, `window`, and `document` here in this manner, as we are asserting using `typeof` first + // which won't throw if they are not present. -function getMessageForObject(exception) { - if ('name' in exception && typeof exception.name === 'string') { - let message = `'${exception.name}' captured as exception`; + if (typeof global !== 'undefined' && value === global) { + return '[Global]'; + } - if ('message' in exception && typeof exception.message === 'string') { - message += ` with message '${exception.message}'`; + // eslint-disable-next-line no-restricted-globals + if (typeof window !== 'undefined' && value === window) { + return '[Window]'; } - return message; - } else if ('message' in exception && typeof exception.message === 'string') { - return exception.message; - } else { - // This will allow us to group events based on top-level keys - // which is much better than creating new group when any key/value change - return `Object captured as exception with keys: ${object.extractExceptionKeysForMessage( - exception , - )}`; - } -} + // eslint-disable-next-line no-restricted-globals + if (typeof document !== 'undefined' && value === document) { + return '[Document]'; + } -/** - * Builds and Event from a Exception - * - * TODO(v8): Remove getHub fallback - * @hidden - */ -function eventFromUnknownInput( - // eslint-disable-next-line deprecation/deprecation - getHubOrClient, - stackParser, - exception, - hint, -) { - const client = - typeof getHubOrClient === 'function' - ? // eslint-disable-next-line deprecation/deprecation - getHubOrClient().getClient() - : getHubOrClient; + if (is.isVueViewModel(value)) { + return '[VueViewModel]'; + } - let ex = exception; - const providedMechanism = - hint && hint.data && (hint.data ).mechanism; - const mechanism = providedMechanism || { - handled: true, - type: 'generic', - }; + // React's SyntheticEvent thingy + if (is.isSyntheticEvent(value)) { + return '[SyntheticEvent]'; + } - let extras; + if (typeof value === 'number' && value !== value) { + return '[NaN]'; + } - if (!is.isError(exception)) { - if (is.isPlainObject(exception)) { - const normalizeDepth = client && client.getOptions().normalizeDepth; - extras = { ['__serialized__']: normalize.normalizeToSize(exception , normalizeDepth) }; + if (typeof value === 'function') { + return `[Function: ${stacktrace.getFunctionName(value)}]`; + } - const message = getMessageForObject(exception); - ex = (hint && hint.syntheticException) || new Error(message); - (ex ).message = message; - } else { - // This handles when someone does: `throw "something awesome";` - // We use synthesized Error here so we can extract a (rough) stack trace. - ex = (hint && hint.syntheticException) || new Error(exception ); - (ex ).message = exception ; + if (typeof value === 'symbol') { + return `[${String(value)}]`; } - mechanism.synthetic = true; - } - const event = { - exception: { - values: [exceptionFromError(stackParser, ex )], - }, - }; + // stringified BigInts are indistinguishable from regular numbers, so we need to label them to avoid confusion + if (typeof value === 'bigint') { + return `[BigInt: ${String(value)}]`; + } - if (extras) { - event.extra = extras; + // Now that we've knocked out all the special cases and the primitives, all we have left are objects. Simply casting + // them to strings means that instances of classes which haven't defined their `toStringTag` will just come out as + // `"[object Object]"`. If we instead look at the constructor's name (which is the same as the name of the class), + // we can make sure that only plain objects come out that way. + const objName = getConstructorName(value); + + // Handle HTML Elements + if (/^HTML(\w*)Element$/.test(objName)) { + return `[HTMLElement: ${objName}]`; + } + + return `[object ${objName}]`; + } catch (err) { + return `**non-serializable** (${err})`; } +} +/* eslint-enable complexity */ - misc.addExceptionTypeValue(event, undefined, undefined); - misc.addExceptionMechanism(event, mechanism); +function getConstructorName(value) { + const prototype = Object.getPrototypeOf(value); - return { - ...event, - event_id: hint && hint.event_id, - }; + return prototype ? prototype.constructor.name : 'null prototype'; } -/** - * Builds and Event from a Message - * @hidden - */ -function eventFromMessage( - stackParser, - message, - // eslint-disable-next-line deprecation/deprecation - level = 'info', - hint, - attachStacktrace, -) { - const event = { - event_id: hint && hint.event_id, - level, - }; +/** Calculates bytes size of input string */ +function utf8Length(value) { + // eslint-disable-next-line no-bitwise + return ~-encodeURI(value).split(/%..|./).length; +} - if (attachStacktrace && hint && hint.syntheticException) { - const frames = parseStackFrames(stackParser, hint.syntheticException); - if (frames.length) { - event.exception = { - values: [ - { - value: message, - stacktrace: { frames }, - }, - ], - }; - } - } +/** Calculates bytes size of input object */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function jsonSize(value) { + return utf8Length(JSON.stringify(value)); +} - if (is.isParameterizedString(message)) { - const { __sentry_template_string__, __sentry_template_values__ } = message; +/** + * Normalizes URLs in exceptions and stacktraces to a base path so Sentry can fingerprint + * across platforms and working directory. + * + * @param url The URL to be normalized. + * @param basePath The application base path. + * @returns The normalized URL. + */ +function normalizeUrlToBase(url, basePath) { + const escapedBase = basePath + // Backslash to forward + .replace(/\\/g, '/') + // Escape RegExp special characters + .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); - event.logentry = { - message: __sentry_template_string__, - params: __sentry_template_values__, - }; - return event; + let newUrl = url; + try { + newUrl = decodeURI(url); + } catch (_Oo) { + // Sometime this breaks } - - event.message = message; - return event; + return ( + newUrl + .replace(/\\/g, '/') + .replace(/webpack:\/?/g, '') // Remove intermediate base path + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + .replace(new RegExp(`(file://)?/*${escapedBase}/*`, 'ig'), 'app:///') + ); } -exports.eventFromMessage = eventFromMessage; -exports.eventFromUnknownInput = eventFromUnknownInput; -exports.exceptionFromError = exceptionFromError; -exports.parseStackFrames = parseStackFrames; +exports.normalize = normalize; +exports.normalizeToSize = normalizeToSize; +exports.normalizeUrlToBase = normalizeUrlToBase; +exports.walk = visit; -},{"./is.js":149,"./misc.js":154,"./normalize.js":157,"./object.js":158}],139:[function(require,module,exports){ +}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./is.js":162,"./memo.js":166,"./object.js":171,"./stacktrace.js":177}],171:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const aggregateErrors = require('./aggregate-errors.js'); const browser = require('./browser.js'); -const dsn = require('./dsn.js'); -const error = require('./error.js'); -const worldwide = require('./worldwide.js'); -const index = require('./instrument/index.js'); +const debugBuild = require('./debug-build.js'); const is = require('./is.js'); -const isBrowser = require('./isBrowser.js'); const logger = require('./logger.js'); -const memo = require('./memo.js'); -const misc = require('./misc.js'); -const node = require('./node.js'); -const normalize = require('./normalize.js'); -const object = require('./object.js'); -const path = require('./path.js'); -const promisebuffer = require('./promisebuffer.js'); -const requestdata = require('./requestdata.js'); -const severity = require('./severity.js'); -const stacktrace = require('./stacktrace.js'); const string = require('./string.js'); -const supports = require('./supports.js'); -const syncpromise = require('./syncpromise.js'); -const time = require('./time.js'); -const tracing = require('./tracing.js'); -const env = require('./env.js'); -const envelope = require('./envelope.js'); -const clientreport = require('./clientreport.js'); -const ratelimit = require('./ratelimit.js'); -const baggage = require('./baggage.js'); -const url = require('./url.js'); -const userIntegrations = require('./userIntegrations.js'); -const cache = require('./cache.js'); -const eventbuilder = require('./eventbuilder.js'); -const anr = require('./anr.js'); -const lru = require('./lru.js'); -const _asyncNullishCoalesce = require('./buildPolyfills/_asyncNullishCoalesce.js'); -const _asyncOptionalChain = require('./buildPolyfills/_asyncOptionalChain.js'); -const _asyncOptionalChainDelete = require('./buildPolyfills/_asyncOptionalChainDelete.js'); -const _nullishCoalesce = require('./buildPolyfills/_nullishCoalesce.js'); -const _optionalChain = require('./buildPolyfills/_optionalChain.js'); -const _optionalChainDelete = require('./buildPolyfills/_optionalChainDelete.js'); -const console = require('./instrument/console.js'); -const dom = require('./instrument/dom.js'); -const xhr = require('./instrument/xhr.js'); -const fetch = require('./instrument/fetch.js'); -const history = require('./instrument/history.js'); -const globalError = require('./instrument/globalError.js'); -const globalUnhandledRejection = require('./instrument/globalUnhandledRejection.js'); -const _handlers = require('./instrument/_handlers.js'); -const nodeStackTrace = require('./node-stack-trace.js'); -const escapeStringForRegex = require('./vendor/escapeStringForRegex.js'); -const supportsHistory = require('./vendor/supportsHistory.js'); - +/** + * Replace a method in an object with a wrapped version of itself. + * + * @param source An object that contains a method to be wrapped. + * @param name The name of the method to be wrapped. + * @param replacementFactory A higher-order function that takes the original version of the given method and returns a + * wrapped version. Note: The function returned by `replacementFactory` needs to be a non-arrow function, in order to + * preserve the correct value of `this`, and the original method must be called using `origMethod.call(this, )` or `origMethod.apply(this, [])` (rather than being called directly), again to preserve `this`. + * @returns void + */ +function fill(source, name, replacementFactory) { + if (!(name in source)) { + return; + } -exports.applyAggregateErrorsToEvent = aggregateErrors.applyAggregateErrorsToEvent; -exports.getComponentName = browser.getComponentName; -exports.getDomElement = browser.getDomElement; -exports.getLocationHref = browser.getLocationHref; -exports.htmlTreeAsString = browser.htmlTreeAsString; -exports.dsnFromString = dsn.dsnFromString; -exports.dsnToString = dsn.dsnToString; -exports.makeDsn = dsn.makeDsn; -exports.SentryError = error.SentryError; -exports.GLOBAL_OBJ = worldwide.GLOBAL_OBJ; -exports.getGlobalObject = worldwide.getGlobalObject; -exports.getGlobalSingleton = worldwide.getGlobalSingleton; -exports.addInstrumentationHandler = index.addInstrumentationHandler; -exports.isDOMError = is.isDOMError; -exports.isDOMException = is.isDOMException; -exports.isElement = is.isElement; -exports.isError = is.isError; -exports.isErrorEvent = is.isErrorEvent; -exports.isEvent = is.isEvent; -exports.isInstanceOf = is.isInstanceOf; -exports.isNaN = is.isNaN; -exports.isParameterizedString = is.isParameterizedString; -exports.isPlainObject = is.isPlainObject; -exports.isPrimitive = is.isPrimitive; -exports.isRegExp = is.isRegExp; -exports.isString = is.isString; -exports.isSyntheticEvent = is.isSyntheticEvent; -exports.isThenable = is.isThenable; -exports.isVueViewModel = is.isVueViewModel; -exports.isBrowser = isBrowser.isBrowser; -exports.CONSOLE_LEVELS = logger.CONSOLE_LEVELS; -exports.consoleSandbox = logger.consoleSandbox; -exports.logger = logger.logger; -exports.originalConsoleMethods = logger.originalConsoleMethods; -exports.memoBuilder = memo.memoBuilder; -exports.addContextToFrame = misc.addContextToFrame; -exports.addExceptionMechanism = misc.addExceptionMechanism; -exports.addExceptionTypeValue = misc.addExceptionTypeValue; -exports.arrayify = misc.arrayify; -exports.checkOrSetAlreadyCaught = misc.checkOrSetAlreadyCaught; -exports.getEventDescription = misc.getEventDescription; -exports.parseSemver = misc.parseSemver; -exports.uuid4 = misc.uuid4; -exports.dynamicRequire = node.dynamicRequire; -exports.isNodeEnv = node.isNodeEnv; -exports.loadModule = node.loadModule; -exports.normalize = normalize.normalize; -exports.normalizeToSize = normalize.normalizeToSize; -exports.normalizeUrlToBase = normalize.normalizeUrlToBase; -exports.walk = normalize.walk; -exports.addNonEnumerableProperty = object.addNonEnumerableProperty; -exports.convertToPlainObject = object.convertToPlainObject; -exports.dropUndefinedKeys = object.dropUndefinedKeys; -exports.extractExceptionKeysForMessage = object.extractExceptionKeysForMessage; -exports.fill = object.fill; -exports.getOriginalFunction = object.getOriginalFunction; -exports.markFunctionWrapped = object.markFunctionWrapped; -exports.objectify = object.objectify; -exports.urlEncode = object.urlEncode; -exports.basename = path.basename; -exports.dirname = path.dirname; -exports.isAbsolute = path.isAbsolute; -exports.join = path.join; -exports.normalizePath = path.normalizePath; -exports.relative = path.relative; -exports.resolve = path.resolve; -exports.makePromiseBuffer = promisebuffer.makePromiseBuffer; -exports.DEFAULT_USER_INCLUDES = requestdata.DEFAULT_USER_INCLUDES; -exports.addRequestDataToEvent = requestdata.addRequestDataToEvent; -exports.addRequestDataToTransaction = requestdata.addRequestDataToTransaction; -exports.extractPathForTransaction = requestdata.extractPathForTransaction; -exports.extractRequestData = requestdata.extractRequestData; -exports.winterCGHeadersToDict = requestdata.winterCGHeadersToDict; -exports.winterCGRequestToRequestData = requestdata.winterCGRequestToRequestData; -exports.severityFromString = severity.severityFromString; -exports.severityLevelFromString = severity.severityLevelFromString; -exports.validSeverityLevels = severity.validSeverityLevels; -exports.createStackParser = stacktrace.createStackParser; -exports.getFunctionName = stacktrace.getFunctionName; -exports.nodeStackLineParser = stacktrace.nodeStackLineParser; -exports.stackParserFromStackParserOptions = stacktrace.stackParserFromStackParserOptions; -exports.stripSentryFramesAndReverse = stacktrace.stripSentryFramesAndReverse; -exports.isMatchingPattern = string.isMatchingPattern; -exports.safeJoin = string.safeJoin; -exports.snipLine = string.snipLine; -exports.stringMatchesSomePattern = string.stringMatchesSomePattern; -exports.truncate = string.truncate; -exports.isNativeFetch = supports.isNativeFetch; -exports.supportsDOMError = supports.supportsDOMError; -exports.supportsDOMException = supports.supportsDOMException; -exports.supportsErrorEvent = supports.supportsErrorEvent; -exports.supportsFetch = supports.supportsFetch; -exports.supportsNativeFetch = supports.supportsNativeFetch; -exports.supportsReferrerPolicy = supports.supportsReferrerPolicy; -exports.supportsReportingObserver = supports.supportsReportingObserver; -exports.SyncPromise = syncpromise.SyncPromise; -exports.rejectedSyncPromise = syncpromise.rejectedSyncPromise; -exports.resolvedSyncPromise = syncpromise.resolvedSyncPromise; -Object.defineProperty(exports, '_browserPerformanceTimeOriginMode', { - enumerable: true, - get: () => time._browserPerformanceTimeOriginMode -}); -exports.browserPerformanceTimeOrigin = time.browserPerformanceTimeOrigin; -exports.dateTimestampInSeconds = time.dateTimestampInSeconds; -exports.timestampInSeconds = time.timestampInSeconds; -exports.timestampWithMs = time.timestampWithMs; -exports.TRACEPARENT_REGEXP = tracing.TRACEPARENT_REGEXP; -exports.extractTraceparentData = tracing.extractTraceparentData; -exports.generateSentryTraceHeader = tracing.generateSentryTraceHeader; -exports.propagationContextFromHeaders = tracing.propagationContextFromHeaders; -exports.tracingContextFromHeaders = tracing.tracingContextFromHeaders; -exports.getSDKSource = env.getSDKSource; -exports.isBrowserBundle = env.isBrowserBundle; -exports.addItemToEnvelope = envelope.addItemToEnvelope; -exports.createAttachmentEnvelopeItem = envelope.createAttachmentEnvelopeItem; -exports.createEnvelope = envelope.createEnvelope; -exports.createEventEnvelopeHeaders = envelope.createEventEnvelopeHeaders; -exports.envelopeContainsItemType = envelope.envelopeContainsItemType; -exports.envelopeItemTypeToDataCategory = envelope.envelopeItemTypeToDataCategory; -exports.forEachEnvelopeItem = envelope.forEachEnvelopeItem; -exports.getSdkMetadataForEnvelopeHeader = envelope.getSdkMetadataForEnvelopeHeader; -exports.parseEnvelope = envelope.parseEnvelope; -exports.serializeEnvelope = envelope.serializeEnvelope; -exports.createClientReportEnvelope = clientreport.createClientReportEnvelope; -exports.DEFAULT_RETRY_AFTER = ratelimit.DEFAULT_RETRY_AFTER; -exports.disabledUntil = ratelimit.disabledUntil; -exports.isRateLimited = ratelimit.isRateLimited; -exports.parseRetryAfterHeader = ratelimit.parseRetryAfterHeader; -exports.updateRateLimits = ratelimit.updateRateLimits; -exports.BAGGAGE_HEADER_NAME = baggage.BAGGAGE_HEADER_NAME; -exports.MAX_BAGGAGE_STRING_LENGTH = baggage.MAX_BAGGAGE_STRING_LENGTH; -exports.SENTRY_BAGGAGE_KEY_PREFIX = baggage.SENTRY_BAGGAGE_KEY_PREFIX; -exports.SENTRY_BAGGAGE_KEY_PREFIX_REGEX = baggage.SENTRY_BAGGAGE_KEY_PREFIX_REGEX; -exports.baggageHeaderToDynamicSamplingContext = baggage.baggageHeaderToDynamicSamplingContext; -exports.dynamicSamplingContextToSentryBaggageHeader = baggage.dynamicSamplingContextToSentryBaggageHeader; -exports.getNumberOfUrlSegments = url.getNumberOfUrlSegments; -exports.getSanitizedUrlString = url.getSanitizedUrlString; -exports.parseUrl = url.parseUrl; -exports.stripUrlQueryAndFragment = url.stripUrlQueryAndFragment; -exports.addOrUpdateIntegration = userIntegrations.addOrUpdateIntegration; -exports.makeFifoCache = cache.makeFifoCache; -exports.eventFromMessage = eventbuilder.eventFromMessage; -exports.eventFromUnknownInput = eventbuilder.eventFromUnknownInput; -exports.exceptionFromError = eventbuilder.exceptionFromError; -exports.parseStackFrames = eventbuilder.parseStackFrames; -exports.callFrameToStackFrame = anr.callFrameToStackFrame; -exports.watchdogTimer = anr.watchdogTimer; -exports.LRUMap = lru.LRUMap; -exports._asyncNullishCoalesce = _asyncNullishCoalesce._asyncNullishCoalesce; -exports._asyncOptionalChain = _asyncOptionalChain._asyncOptionalChain; -exports._asyncOptionalChainDelete = _asyncOptionalChainDelete._asyncOptionalChainDelete; -exports._nullishCoalesce = _nullishCoalesce._nullishCoalesce; -exports._optionalChain = _optionalChain._optionalChain; -exports._optionalChainDelete = _optionalChainDelete._optionalChainDelete; -exports.addConsoleInstrumentationHandler = console.addConsoleInstrumentationHandler; -exports.addClickKeypressInstrumentationHandler = dom.addClickKeypressInstrumentationHandler; -exports.SENTRY_XHR_DATA_KEY = xhr.SENTRY_XHR_DATA_KEY; -exports.addXhrInstrumentationHandler = xhr.addXhrInstrumentationHandler; -exports.addFetchInstrumentationHandler = fetch.addFetchInstrumentationHandler; -exports.addHistoryInstrumentationHandler = history.addHistoryInstrumentationHandler; -exports.addGlobalErrorInstrumentationHandler = globalError.addGlobalErrorInstrumentationHandler; -exports.addGlobalUnhandledRejectionInstrumentationHandler = globalUnhandledRejection.addGlobalUnhandledRejectionInstrumentationHandler; -exports.resetInstrumentationHandlers = _handlers.resetInstrumentationHandlers; -exports.filenameIsInApp = nodeStackTrace.filenameIsInApp; -exports.escapeStringForRegex = escapeStringForRegex.escapeStringForRegex; -exports.supportsHistory = supportsHistory.supportsHistory; + const original = source[name] ; + const wrapped = replacementFactory(original) ; + // Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work + // otherwise it'll throw "TypeError: Object.defineProperties called on non-object" + if (typeof wrapped === 'function') { + markFunctionWrapped(wrapped, original); + } -},{"./aggregate-errors.js":120,"./anr.js":121,"./baggage.js":122,"./browser.js":123,"./buildPolyfills/_asyncNullishCoalesce.js":124,"./buildPolyfills/_asyncOptionalChain.js":125,"./buildPolyfills/_asyncOptionalChainDelete.js":126,"./buildPolyfills/_nullishCoalesce.js":127,"./buildPolyfills/_optionalChain.js":128,"./buildPolyfills/_optionalChainDelete.js":129,"./cache.js":130,"./clientreport.js":131,"./dsn.js":134,"./env.js":135,"./envelope.js":136,"./error.js":137,"./eventbuilder.js":138,"./instrument/_handlers.js":140,"./instrument/console.js":141,"./instrument/dom.js":142,"./instrument/fetch.js":143,"./instrument/globalError.js":144,"./instrument/globalUnhandledRejection.js":145,"./instrument/history.js":146,"./instrument/index.js":147,"./instrument/xhr.js":148,"./is.js":149,"./isBrowser.js":150,"./logger.js":151,"./lru.js":152,"./memo.js":153,"./misc.js":154,"./node-stack-trace.js":155,"./node.js":156,"./normalize.js":157,"./object.js":158,"./path.js":159,"./promisebuffer.js":160,"./ratelimit.js":161,"./requestdata.js":162,"./severity.js":163,"./stacktrace.js":164,"./string.js":165,"./supports.js":166,"./syncpromise.js":167,"./time.js":168,"./tracing.js":169,"./url.js":170,"./userIntegrations.js":171,"./vendor/escapeStringForRegex.js":172,"./vendor/supportsHistory.js":173,"./worldwide.js":174}],140:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + source[name] = wrapped; +} -const debugBuild = require('../debug-build.js'); -const logger = require('../logger.js'); -const stacktrace = require('../stacktrace.js'); +/** + * Defines a non-enumerable property on the given object. + * + * @param obj The object on which to set the property + * @param name The name of the property to be set + * @param value The value to which to set the property + */ +function addNonEnumerableProperty(obj, name, value) { + try { + Object.defineProperty(obj, name, { + // enumerable: false, // the default, so we can save on bundle size by not explicitly setting it + value: value, + writable: true, + configurable: true, + }); + } catch (o_O) { + debugBuild.DEBUG_BUILD && logger.logger.log(`Failed to add non-enumerable property "${name}" to object`, obj); + } +} -// We keep the handlers globally -const handlers = {}; -const instrumented = {}; +/** + * Remembers the original function on the wrapped function and + * patches up the prototype. + * + * @param wrapped the wrapper function + * @param original the original function that gets wrapped + */ +function markFunctionWrapped(wrapped, original) { + try { + const proto = original.prototype || {}; + wrapped.prototype = original.prototype = proto; + addNonEnumerableProperty(wrapped, '__sentry_original__', original); + } catch (o_O) {} // eslint-disable-line no-empty +} -/** Add a handler function. */ -function addHandler(type, handler) { - handlers[type] = handlers[type] || []; - (handlers[type] ).push(handler); +/** + * This extracts the original function if available. See + * `markFunctionWrapped` for more information. + * + * @param func the function to unwrap + * @returns the unwrapped version of the function if available. + */ +function getOriginalFunction(func) { + return func.__sentry_original__; } /** - * Reset all instrumentation handlers. - * This can be used by tests to ensure we have a clean slate of instrumentation handlers. + * Encodes given object into url-friendly format + * + * @param object An object that contains serializable values + * @returns string Encoded */ -function resetInstrumentationHandlers() { - Object.keys(handlers).forEach(key => { - handlers[key ] = undefined; - }); +function urlEncode(object) { + return Object.keys(object) + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(object[key])}`) + .join('&'); } -/** Maybe run an instrumentation function, unless it was already called. */ -function maybeInstrument(type, instrumentFn) { - if (!instrumented[type]) { - instrumentFn(); - instrumented[type] = true; +/** + * Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their + * non-enumerable properties attached. + * + * @param value Initial source that we have to transform in order for it to be usable by the serializer + * @returns An Event or Error turned into an object - or the value argurment itself, when value is neither an Event nor + * an Error. + */ +function convertToPlainObject( + value, +) + + { + if (is.isError(value)) { + return { + message: value.message, + name: value.name, + stack: value.stack, + ...getOwnProperties(value), + }; + } else if (is.isEvent(value)) { + const newObj + + = { + type: value.type, + target: serializeEventTarget(value.target), + currentTarget: serializeEventTarget(value.currentTarget), + ...getOwnProperties(value), + }; + + if (typeof CustomEvent !== 'undefined' && is.isInstanceOf(value, CustomEvent)) { + newObj.detail = value.detail; + } + + return newObj; + } else { + return value; } } -/** Trigger handlers for a given instrumentation type. */ -function triggerHandlers(type, data) { - const typeHandlers = type && handlers[type]; - if (!typeHandlers) { - return; +/** Creates a string representation of the target of an `Event` object */ +function serializeEventTarget(target) { + try { + return is.isElement(target) ? browser.htmlTreeAsString(target) : Object.prototype.toString.call(target); + } catch (_oO) { + return ''; } +} - for (const handler of typeHandlers) { - try { - handler(data); - } catch (e) { - debugBuild.DEBUG_BUILD && - logger.logger.error( - `Error while triggering instrumentation handler.\nType: ${type}\nName: ${stacktrace.getFunctionName(handler)}\nError:`, - e, - ); +/** Filters out all but an object's own properties */ +function getOwnProperties(obj) { + if (typeof obj === 'object' && obj !== null) { + const extractedProps = {}; + for (const property in obj) { + if (Object.prototype.hasOwnProperty.call(obj, property)) { + extractedProps[property] = (obj )[property]; + } } + return extractedProps; + } else { + return {}; } } -exports.addHandler = addHandler; -exports.maybeInstrument = maybeInstrument; -exports.resetInstrumentationHandlers = resetInstrumentationHandlers; -exports.triggerHandlers = triggerHandlers; +/** + * Given any captured exception, extract its keys and create a sorted + * and truncated list that will be used inside the event message. + * eg. `Non-error exception captured with keys: foo, bar, baz` + */ +function extractExceptionKeysForMessage(exception, maxLength = 40) { + const keys = Object.keys(convertToPlainObject(exception)); + keys.sort(); + + if (!keys.length) { + return '[object has no keys]'; + } + if (keys[0].length >= maxLength) { + return string.truncate(keys[0], maxLength); + } -},{"../debug-build.js":133,"../logger.js":151,"../stacktrace.js":164}],141:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + for (let includedKeys = keys.length; includedKeys > 0; includedKeys--) { + const serialized = keys.slice(0, includedKeys).join(', '); + if (serialized.length > maxLength) { + continue; + } + if (includedKeys === keys.length) { + return serialized; + } + return string.truncate(serialized, maxLength); + } -const logger = require('../logger.js'); -const object = require('../object.js'); -const worldwide = require('../worldwide.js'); -const _handlers = require('./_handlers.js'); + return ''; +} /** - * Add an instrumentation handler for when a console.xxx method is called. + * Given any object, return a new object having removed all fields whose value was `undefined`. + * Works recursively on objects and arrays. * - * Use at your own risk, this might break without changelog notice, only used internally. - * @hidden + * Attention: This function keeps circular references in the returned object. */ -function addConsoleInstrumentationHandler(handler) { - const type = 'console'; - _handlers.addHandler(type, handler); - _handlers.maybeInstrument(type, instrumentConsole); +function dropUndefinedKeys(inputValue) { + // This map keeps track of what already visited nodes map to. + // Our Set - based memoBuilder doesn't work here because we want to the output object to have the same circular + // references as the input object. + const memoizationMap = new Map(); + + // This function just proxies `_dropUndefinedKeys` to keep the `memoBuilder` out of this function's API + return _dropUndefinedKeys(inputValue, memoizationMap); } -function instrumentConsole() { - if (!('console' in worldwide.GLOBAL_OBJ)) { - return; - } +function _dropUndefinedKeys(inputValue, memoizationMap) { + if (isPojo(inputValue)) { + // If this node has already been visited due to a circular reference, return the object it was mapped to in the new object + const memoVal = memoizationMap.get(inputValue); + if (memoVal !== undefined) { + return memoVal ; + } - logger.CONSOLE_LEVELS.forEach(function (level) { - if (!(level in worldwide.GLOBAL_OBJ.console)) { - return; + const returnValue = {}; + // Store the mapping of this value in case we visit it again, in case of circular data + memoizationMap.set(inputValue, returnValue); + + for (const key of Object.keys(inputValue)) { + if (typeof inputValue[key] !== 'undefined') { + returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap); + } } - object.fill(worldwide.GLOBAL_OBJ.console, level, function (originalConsoleMethod) { - logger.originalConsoleMethods[level] = originalConsoleMethod; + return returnValue ; + } - return function (...args) { - const handlerData = { args, level }; - _handlers.triggerHandlers('console', handlerData); + if (Array.isArray(inputValue)) { + // If this node has already been visited due to a circular reference, return the array it was mapped to in the new object + const memoVal = memoizationMap.get(inputValue); + if (memoVal !== undefined) { + return memoVal ; + } - const log = logger.originalConsoleMethods[level]; - log && log.apply(worldwide.GLOBAL_OBJ.console, args); - }; + const returnValue = []; + // Store the mapping of this value in case we visit it again, in case of circular data + memoizationMap.set(inputValue, returnValue); + + inputValue.forEach((item) => { + returnValue.push(_dropUndefinedKeys(item, memoizationMap)); }); - }); + + return returnValue ; + } + + return inputValue; } -exports.addConsoleInstrumentationHandler = addConsoleInstrumentationHandler; +function isPojo(input) { + if (!is.isPlainObject(input)) { + return false; + } + try { + const name = (Object.getPrototypeOf(input) ).constructor.name; + return !name || name === 'Object'; + } catch (e) { + return true; + } +} + +/** + * Ensure that something is an object. + * + * Turns `undefined` and `null` into `String`s and all other primitives into instances of their respective wrapper + * classes (String, Boolean, Number, etc.). Acts as the identity function on non-primitives. + * + * @param wat The subject of the objectification + * @returns A version of `wat` which can safely be used with `Object` class methods + */ +function objectify(wat) { + let objectified; + switch (true) { + case wat === undefined || wat === null: + objectified = new String(wat); + break; + + // Though symbols and bigints do have wrapper classes (`Symbol` and `BigInt`, respectively), for whatever reason + // those classes don't have constructors which can be used with the `new` keyword. We therefore need to cast each as + // an object in order to wrap it. + case typeof wat === 'symbol' || typeof wat === 'bigint': + objectified = Object(wat); + break; + + // this will catch the remaining primitives: `String`, `Number`, and `Boolean` + case is.isPrimitive(wat): + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + objectified = new (wat ).constructor(wat); + break; + + // by process of elimination, at this point we know that `wat` must already be an object + default: + objectified = wat; + break; + } + return objectified; +} + +exports.addNonEnumerableProperty = addNonEnumerableProperty; +exports.convertToPlainObject = convertToPlainObject; +exports.dropUndefinedKeys = dropUndefinedKeys; +exports.extractExceptionKeysForMessage = extractExceptionKeysForMessage; +exports.fill = fill; +exports.getOriginalFunction = getOriginalFunction; +exports.markFunctionWrapped = markFunctionWrapped; +exports.objectify = objectify; +exports.urlEncode = urlEncode; -},{"../logger.js":151,"../object.js":158,"../worldwide.js":174,"./_handlers.js":140}],142:[function(require,module,exports){ + +},{"./browser.js":136,"./debug-build.js":146,"./is.js":162,"./logger.js":164,"./string.js":178}],172:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const misc = require('../misc.js'); -const object = require('../object.js'); -const worldwide = require('../worldwide.js'); -const _handlers = require('./_handlers.js'); +// Slightly modified (no IE8 support, ES6) and transcribed to TypeScript +// https://github.com/calvinmetcalf/rollup-plugin-node-builtins/blob/63ab8aacd013767445ca299e468d9a60a95328d7/src/es6/path.js +// +// Copyright Joyent, Inc.and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. -const WINDOW = worldwide.GLOBAL_OBJ ; -const DEBOUNCE_DURATION = 1000; +/** JSDoc */ +function normalizeArray(parts, allowAboveRoot) { + // if the path tries to go above the root, `up` ends up > 0 + let up = 0; + for (let i = parts.length - 1; i >= 0; i--) { + const last = parts[i]; + if (last === '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } -let debounceTimerID; -let lastCapturedEventType; -let lastCapturedEventTargetId; + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up--; up) { + parts.unshift('..'); + } + } -/** - * Add an instrumentation handler for when a click or a keypress happens. - * - * Use at your own risk, this might break without changelog notice, only used internally. - * @hidden - */ -function addClickKeypressInstrumentationHandler(handler) { - const type = 'dom'; - _handlers.addHandler(type, handler); - _handlers.maybeInstrument(type, instrumentDOM); + return parts; } -/** Exported for tests only. */ -function instrumentDOM() { - if (!WINDOW.document) { - return; +// Split a filename into [root, dir, basename, ext], unix version +// 'root' is just a slash, or nothing. +const splitPathRe = /^(\S+:\\|\/?)([\s\S]*?)((?:\.{1,2}|[^/\\]+?|)(\.[^./\\]*|))(?:[/\\]*)$/; +/** JSDoc */ +function splitPath(filename) { + // Truncate files names greater than 1024 characters to avoid regex dos + // https://github.com/getsentry/sentry-javascript/pull/8737#discussion_r1285719172 + const truncated = filename.length > 1024 ? `${filename.slice(-1024)}` : filename; + const parts = splitPathRe.exec(truncated); + return parts ? parts.slice(1) : []; +} + +// path.resolve([from ...], to) +// posix version +/** JSDoc */ +function resolve(...args) { + let resolvedPath = ''; + let resolvedAbsolute = false; + + for (let i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { + const path = i >= 0 ? args[i] : '/'; + + // Skip empty entries + if (!path) { + continue; + } + + resolvedPath = `${path}/${resolvedPath}`; + resolvedAbsolute = path.charAt(0) === '/'; } - // Make it so that any click or keypress that is unhandled / bubbled up all the way to the document triggers our dom - // handlers. (Normally we have only one, which captures a breadcrumb for each click or keypress.) Do this before - // we instrument `addEventListener` so that we don't end up attaching this handler twice. - const triggerDOMHandler = _handlers.triggerHandlers.bind(null, 'dom'); - const globalDOMEventHandler = makeDOMEventHandler(triggerDOMHandler, true); - WINDOW.document.addEventListener('click', globalDOMEventHandler, false); - WINDOW.document.addEventListener('keypress', globalDOMEventHandler, false); + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) - // After hooking into click and keypress events bubbled up to `document`, we also hook into user-handled - // clicks & keypresses, by adding an event listener of our own to any element to which they add a listener. That - // way, whenever one of their handlers is triggered, ours will be, too. (This is needed because their handler - // could potentially prevent the event from bubbling up to our global listeners. This way, our handler are still - // guaranteed to fire at least once.) - ['EventTarget', 'Node'].forEach((target) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const proto = (WINDOW )[target] && (WINDOW )[target].prototype; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-prototype-builtins - if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) { - return; + // Normalize the path + resolvedPath = normalizeArray( + resolvedPath.split('/').filter(p => !!p), + !resolvedAbsolute, + ).join('/'); + + return (resolvedAbsolute ? '/' : '') + resolvedPath || '.'; +} + +/** JSDoc */ +function trim(arr) { + let start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') { + break; } + } - object.fill(proto, 'addEventListener', function (originalAddEventListener) { - return function ( + let end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') { + break; + } + } - type, - listener, - options, - ) { - if (type === 'click' || type == 'keypress') { - try { - const el = this ; - const handlers = (el.__sentry_instrumentation_handlers__ = el.__sentry_instrumentation_handlers__ || {}); - const handlerForType = (handlers[type] = handlers[type] || { refCount: 0 }); + if (start > end) { + return []; + } + return arr.slice(start, end - start + 1); +} - if (!handlerForType.handler) { - const handler = makeDOMEventHandler(triggerDOMHandler); - handlerForType.handler = handler; - originalAddEventListener.call(this, type, handler, options); - } +// path.relative(from, to) +// posix version +/** JSDoc */ +function relative(from, to) { + /* eslint-disable no-param-reassign */ + from = resolve(from).slice(1); + to = resolve(to).slice(1); + /* eslint-enable no-param-reassign */ - handlerForType.refCount++; - } catch (e) { - // Accessing dom properties is always fragile. - // Also allows us to skip `addEventListenrs` calls with no proper `this` context. - } - } + const fromParts = trim(from.split('/')); + const toParts = trim(to.split('/')); - return originalAddEventListener.call(this, type, listener, options); - }; - }); + const length = Math.min(fromParts.length, toParts.length); + let samePartsLength = length; + for (let i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + + let outputParts = []; + for (let i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('/'); +} + +// path.normalize(path) +// posix version +/** JSDoc */ +function normalizePath(path) { + const isPathAbsolute = isAbsolute(path); + const trailingSlash = path.slice(-1) === '/'; - object.fill( - proto, - 'removeEventListener', - function (originalRemoveEventListener) { - return function ( + // Normalize the path + let normalizedPath = normalizeArray( + path.split('/').filter(p => !!p), + !isPathAbsolute, + ).join('/'); - type, - listener, - options, - ) { - if (type === 'click' || type == 'keypress') { - try { - const el = this ; - const handlers = el.__sentry_instrumentation_handlers__ || {}; - const handlerForType = handlers[type]; + if (!normalizedPath && !isPathAbsolute) { + normalizedPath = '.'; + } + if (normalizedPath && trailingSlash) { + normalizedPath += '/'; + } - if (handlerForType) { - handlerForType.refCount--; - // If there are no longer any custom handlers of the current type on this element, we can remove ours, too. - if (handlerForType.refCount <= 0) { - originalRemoveEventListener.call(this, type, handlerForType.handler, options); - handlerForType.handler = undefined; - delete handlers[type]; // eslint-disable-line @typescript-eslint/no-dynamic-delete - } + return (isPathAbsolute ? '/' : '') + normalizedPath; +} - // If there are no longer any custom handlers of any type on this element, cleanup everything. - if (Object.keys(handlers).length === 0) { - delete el.__sentry_instrumentation_handlers__; - } - } - } catch (e) { - // Accessing dom properties is always fragile. - // Also allows us to skip `addEventListenrs` calls with no proper `this` context. - } - } +// posix version +/** JSDoc */ +function isAbsolute(path) { + return path.charAt(0) === '/'; +} - return originalRemoveEventListener.call(this, type, listener, options); - }; - }, - ); - }); +// posix version +/** JSDoc */ +function join(...args) { + return normalizePath(args.join('/')); } -/** - * Check whether the event is similar to the last captured one. For example, two click events on the same button. - */ -function isSimilarToLastCapturedEvent(event) { - // If both events have different type, then user definitely performed two separate actions. e.g. click + keypress. - if (event.type !== lastCapturedEventType) { - return false; +/** JSDoc */ +function dirname(path) { + const result = splitPath(path); + const root = result[0]; + let dir = result[1]; + + if (!root && !dir) { + // No dirname whatsoever + return '.'; } - try { - // If both events have the same type, it's still possible that actions were performed on different targets. - // e.g. 2 clicks on different buttons. - if (!event.target || (event.target )._sentryId !== lastCapturedEventTargetId) { - return false; - } - } catch (e) { - // just accessing `target` property can throw an exception in some rare circumstances - // see: https://github.com/getsentry/sentry-javascript/issues/838 + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.slice(0, dir.length - 1); } - // If both events have the same type _and_ same `target` (an element which triggered an event, _not necessarily_ - // to which an event listener was attached), we treat them as the same action, as we want to capture - // only one breadcrumb. e.g. multiple clicks on the same button, or typing inside a user input box. - return true; + return root + dir; } -/** - * Decide whether an event should be captured. - * @param event event to be captured - */ -function shouldSkipDOMEvent(eventType, target) { - // We are only interested in filtering `keypress` events for now. - if (eventType !== 'keypress') { - return false; +/** JSDoc */ +function basename(path, ext) { + let f = splitPath(path)[2]; + if (ext && f.slice(ext.length * -1) === ext) { + f = f.slice(0, f.length - ext.length); } + return f; +} - if (!target || !target.tagName) { - return true; - } +exports.basename = basename; +exports.dirname = dirname; +exports.isAbsolute = isAbsolute; +exports.join = join; +exports.normalizePath = normalizePath; +exports.relative = relative; +exports.resolve = resolve; - // Only consider keypress events on actual input elements. This will disregard keypresses targeting body - // e.g.tabbing through elements, hotkeys, etc. - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { - return false; - } - return true; -} +},{}],173:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const error = require('./error.js'); +const syncpromise = require('./syncpromise.js'); /** - * Wraps addEventListener to capture UI breadcrumbs + * Creates an new PromiseBuffer object with the specified limit + * @param limit max number of promises that can be stored in the buffer */ -function makeDOMEventHandler( - handler, - globalListener = false, -) { - return (event) => { - // It's possible this handler might trigger multiple times for the same - // event (e.g. event propagation through node ancestors). - // Ignore if we've already captured that event. - if (!event || event['_sentryCaptured']) { - return; - } +function makePromiseBuffer(limit) { + const buffer = []; - const target = getEventTarget(event); + function isReady() { + return limit === undefined || buffer.length < limit; + } - // We always want to skip _some_ events. - if (shouldSkipDOMEvent(event.type, target)) { - return; - } + /** + * Remove a promise from the queue. + * + * @param task Can be any PromiseLike + * @returns Removed promise. + */ + function remove(task) { + return buffer.splice(buffer.indexOf(task), 1)[0]; + } - // Mark event as "seen" - object.addNonEnumerableProperty(event, '_sentryCaptured', true); + /** + * Add a promise (representing an in-flight action) to the queue, and set it to remove itself on fulfillment. + * + * @param taskProducer A function producing any PromiseLike; In previous versions this used to be `task: + * PromiseLike`, but under that model, Promises were instantly created on the call-site and their executor + * functions therefore ran immediately. Thus, even if the buffer was full, the action still happened. By + * requiring the promise to be wrapped in a function, we can defer promise creation until after the buffer + * limit check. + * @returns The original promise. + */ + function add(taskProducer) { + if (!isReady()) { + return syncpromise.rejectedSyncPromise(new error.SentryError('Not adding Promise because buffer limit was reached.')); + } - if (target && !target._sentryId) { - // Add UUID to event target so we can identify if - object.addNonEnumerableProperty(target, '_sentryId', misc.uuid4()); + // start the task and add its promise to the queue + const task = taskProducer(); + if (buffer.indexOf(task) === -1) { + buffer.push(task); } + void task + .then(() => remove(task)) + // Use `then(null, rejectionHandler)` rather than `catch(rejectionHandler)` so that we can use `PromiseLike` + // rather than `Promise`. `PromiseLike` doesn't have a `.catch` method, making its polyfill smaller. (ES5 didn't + // have promises, so TS has to polyfill when down-compiling.) + .then(null, () => + remove(task).then(null, () => { + // We have to add another catch here because `remove()` starts a new promise chain. + }), + ); + return task; + } - const name = event.type === 'keypress' ? 'input' : event.type; + /** + * Wait for all promises in the queue to resolve or for timeout to expire, whichever comes first. + * + * @param timeout The time, in ms, after which to resolve to `false` if the queue is still non-empty. Passing `0` (or + * not passing anything) will make the promise wait as long as it takes for the queue to drain before resolving to + * `true`. + * @returns A promise which will resolve to `true` if the queue is already empty or drains before the timeout, and + * `false` otherwise + */ + function drain(timeout) { + return new syncpromise.SyncPromise((resolve, reject) => { + let counter = buffer.length; - // If there is no last captured event, it means that we can safely capture the new event and store it for future comparisons. - // If there is a last captured event, see if the new event is different enough to treat it as a unique one. - // If that's the case, emit the previous event and store locally the newly-captured DOM event. - if (!isSimilarToLastCapturedEvent(event)) { - const handlerData = { event, name, global: globalListener }; - handler(handlerData); - lastCapturedEventType = event.type; - lastCapturedEventTargetId = target ? target._sentryId : undefined; - } + if (!counter) { + return resolve(true); + } - // Start a new debounce timer that will prevent us from capturing multiple events that should be grouped together. - clearTimeout(debounceTimerID); - debounceTimerID = WINDOW.setTimeout(() => { - lastCapturedEventTargetId = undefined; - lastCapturedEventType = undefined; - }, DEBOUNCE_DURATION); - }; -} + // wait for `timeout` ms and then resolve to `false` (if not cancelled first) + const capturedSetTimeout = setTimeout(() => { + if (timeout && timeout > 0) { + resolve(false); + } + }, timeout); -function getEventTarget(event) { - try { - return event.target ; - } catch (e) { - // just accessing `target` property can throw an exception in some rare circumstances - // see: https://github.com/getsentry/sentry-javascript/issues/838 - return null; + // if all promises resolve in time, cancel the timer and resolve to `true` + buffer.forEach(item => { + void syncpromise.resolvedSyncPromise(item).then(() => { + if (!--counter) { + clearTimeout(capturedSetTimeout); + resolve(true); + } + }, reject); + }); + }); } + + return { + $: buffer, + add, + drain, + }; } -exports.addClickKeypressInstrumentationHandler = addClickKeypressInstrumentationHandler; -exports.instrumentDOM = instrumentDOM; +exports.makePromiseBuffer = makePromiseBuffer; -},{"../misc.js":154,"../object.js":158,"../worldwide.js":174,"./_handlers.js":140}],143:[function(require,module,exports){ +},{"./error.js":150,"./syncpromise.js":180}],174:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const object = require('../object.js'); -const supports = require('../supports.js'); -const worldwide = require('../worldwide.js'); -const _handlers = require('./_handlers.js'); +// Intentionally keeping the key broad, as we don't know for sure what rate limit headers get returned from backend + +const DEFAULT_RETRY_AFTER = 60 * 1000; // 60 seconds /** - * Add an instrumentation handler for when a fetch request happens. - * The handler function is called once when the request starts and once when it ends, - * which can be identified by checking if it has an `endTimestamp`. + * Extracts Retry-After value from the request header or returns default value + * @param header string representation of 'Retry-After' header + * @param now current unix timestamp * - * Use at your own risk, this might break without changelog notice, only used internally. - * @hidden */ -function addFetchInstrumentationHandler(handler) { - const type = 'fetch'; - _handlers.addHandler(type, handler); - _handlers.maybeInstrument(type, instrumentFetch); -} +function parseRetryAfterHeader(header, now = Date.now()) { + const headerDelay = parseInt(`${header}`, 10); + if (!isNaN(headerDelay)) { + return headerDelay * 1000; + } -function instrumentFetch() { - if (!supports.supportsNativeFetch()) { - return; + const headerDate = Date.parse(`${header}`); + if (!isNaN(headerDate)) { + return headerDate - now; } - object.fill(worldwide.GLOBAL_OBJ, 'fetch', function (originalFetch) { - return function (...args) { - const { method, url } = parseFetchArgs(args); + return DEFAULT_RETRY_AFTER; +} - const handlerData = { - args, - fetchData: { - method, - url, - }, - startTimestamp: Date.now(), - }; +/** + * Gets the time that the given category is disabled until for rate limiting. + * In case no category-specific limit is set but a general rate limit across all categories is active, + * that time is returned. + * + * @return the time in ms that the category is disabled until or 0 if there's no active rate limit. + */ +function disabledUntil(limits, dataCategory) { + return limits[dataCategory] || limits.all || 0; +} - _handlers.triggerHandlers('fetch', { - ...handlerData, - }); +/** + * Checks if a category is rate limited + */ +function isRateLimited(limits, dataCategory, now = Date.now()) { + return disabledUntil(limits, dataCategory) > now; +} - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return originalFetch.apply(worldwide.GLOBAL_OBJ, args).then( - (response) => { - const finishedHandlerData = { - ...handlerData, - endTimestamp: Date.now(), - response, - }; +/** + * Update ratelimits from incoming headers. + * + * @return the updated RateLimits object. + */ +function updateRateLimits( + limits, + { statusCode, headers }, + now = Date.now(), +) { + const updatedRateLimits = { + ...limits, + }; - _handlers.triggerHandlers('fetch', finishedHandlerData); - return response; - }, - (error) => { - const erroredHandlerData = { - ...handlerData, - endTimestamp: Date.now(), - error, - }; + // "The name is case-insensitive." + // https://developer.mozilla.org/en-US/docs/Web/API/Headers/get + const rateLimitHeader = headers && headers['x-sentry-rate-limits']; + const retryAfterHeader = headers && headers['retry-after']; - _handlers.triggerHandlers('fetch', erroredHandlerData); - // NOTE: If you are a Sentry user, and you are seeing this stack frame, - // it means the sentry.javascript SDK caught an error invoking your application code. - // This is expected behavior and NOT indicative of a bug with sentry.javascript. - throw error; - }, - ); - }; - }); -} + if (rateLimitHeader) { + /** + * rate limit headers are of the form + *
,
,.. + * where each
is of the form + * : : : : + * where + * is a delay in seconds + * is the event type(s) (error, transaction, etc) being rate limited and is of the form + * ;;... + * is what's being limited (org, project, or key) - ignored by SDK + * is an arbitrary string like "org_quota" - ignored by SDK + * Semicolon-separated list of metric namespace identifiers. Defines which namespace(s) will be affected. + * Only present if rate limit applies to the metric_bucket data category. + */ + for (const limit of rateLimitHeader.trim().split(',')) { + const [retryAfter, categories, , , namespaces] = limit.split(':', 5); + const headerDelay = parseInt(retryAfter, 10); + const delay = (!isNaN(headerDelay) ? headerDelay : 60) * 1000; // 60sec default + if (!categories) { + updatedRateLimits.all = now + delay; + } else { + for (const category of categories.split(';')) { + if (category === 'metric_bucket') { + // namespaces will be present when category === 'metric_bucket' + if (!namespaces || namespaces.split(';').includes('custom')) { + updatedRateLimits[category] = now + delay; + } + } else { + updatedRateLimits[category] = now + delay; + } + } + } + } + } else if (retryAfterHeader) { + updatedRateLimits.all = now + parseRetryAfterHeader(retryAfterHeader, now); + } else if (statusCode === 429) { + updatedRateLimits.all = now + 60 * 1000; + } -function hasProp(obj, prop) { - return !!obj && typeof obj === 'object' && !!(obj )[prop]; + return updatedRateLimits; } -function getUrlFromResource(resource) { - if (typeof resource === 'string') { - return resource; - } +exports.DEFAULT_RETRY_AFTER = DEFAULT_RETRY_AFTER; +exports.disabledUntil = disabledUntil; +exports.isRateLimited = isRateLimited; +exports.parseRetryAfterHeader = parseRetryAfterHeader; +exports.updateRateLimits = updateRateLimits; - if (!resource) { - return ''; - } - if (hasProp(resource, 'url')) { - return resource.url; - } +},{}],175:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - if (resource.toString) { - return resource.toString(); - } +const cookie = require('./cookie.js'); +const debugBuild = require('./debug-build.js'); +const is = require('./is.js'); +const logger = require('./logger.js'); +const normalize = require('./normalize.js'); +const url = require('./url.js'); - return ''; +const DEFAULT_INCLUDES = { + ip: false, + request: true, + transaction: true, + user: true, +}; +const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url']; +const DEFAULT_USER_INCLUDES = ['id', 'username', 'email']; + +/** + * Sets parameterized route as transaction name e.g.: `GET /users/:id` + * Also adds more context data on the transaction from the request. + * + * @deprecated This utility will be removed in v8. + */ +function addRequestDataToTransaction( + transaction, + req, + deps, +) { + if (!transaction) return; + // eslint-disable-next-line deprecation/deprecation + if (!transaction.metadata.source || transaction.metadata.source === 'url') { + // Attempt to grab a parameterized route off of the request + const [name, source] = extractPathForTransaction(req, { path: true, method: true }); + transaction.updateName(name); + // TODO: SEMANTIC_ATTRIBUTE_SENTRY_SOURCE is in core, align this once we merge utils & core + // eslint-disable-next-line deprecation/deprecation + transaction.setMetadata({ source }); + } + transaction.setAttribute('url', req.originalUrl || req.url); + if (req.baseUrl) { + transaction.setAttribute('baseUrl', req.baseUrl); + } + // TODO: We need to rewrite this to a flat format? + // eslint-disable-next-line deprecation/deprecation + transaction.setData('query', extractQueryParams(req, deps)); } /** - * Parses the fetch arguments to find the used Http method and the url of the request. - * Exported for tests only. + * Extracts a complete and parameterized path from the request object and uses it to construct transaction name. + * If the parameterized transaction name cannot be extracted, we fall back to the raw URL. + * + * Additionally, this function determines and returns the transaction name source + * + * eg. GET /mountpoint/user/:id + * + * @param req A request object + * @param options What to include in the transaction name (method, path, or a custom route name to be + * used instead of the request's route) + * + * @returns A tuple of the fully constructed transaction name [0] and its source [1] (can be either 'route' or 'url') */ -function parseFetchArgs(fetchArgs) { - if (fetchArgs.length === 0) { - return { method: 'GET', url: '' }; +function extractPathForTransaction( + req, + options = {}, +) { + const method = req.method && req.method.toUpperCase(); + + let path = ''; + let source = 'url'; + + // Check to see if there's a parameterized route we can use (as there is in Express) + if (options.customRoute || req.route) { + path = options.customRoute || `${req.baseUrl || ''}${req.route && req.route.path}`; + source = 'route'; + } + + // Otherwise, just take the original URL + else if (req.originalUrl || req.url) { + path = url.stripUrlQueryAndFragment(req.originalUrl || req.url || ''); } - if (fetchArgs.length === 2) { - const [url, options] = fetchArgs ; - - return { - url: getUrlFromResource(url), - method: hasProp(options, 'method') ? String(options.method).toUpperCase() : 'GET', - }; + let name = ''; + if (options.method && method) { + name += method; + } + if (options.method && options.path) { + name += ' '; + } + if (options.path && path) { + name += path; } - const arg = fetchArgs[0]; - return { - url: getUrlFromResource(arg ), - method: hasProp(arg, 'method') ? String(arg.method).toUpperCase() : 'GET', - }; + return [name, source]; } -exports.addFetchInstrumentationHandler = addFetchInstrumentationHandler; -exports.parseFetchArgs = parseFetchArgs; +/** JSDoc */ +function extractTransaction(req, type) { + switch (type) { + case 'path': { + return extractPathForTransaction(req, { path: true })[0]; + } + case 'handler': { + return (req.route && req.route.stack && req.route.stack[0] && req.route.stack[0].name) || ''; + } + case 'methodPath': + default: { + // if exist _reconstructedRoute return that path instead of route.path + const customRoute = req._reconstructedRoute ? req._reconstructedRoute : undefined; + return extractPathForTransaction(req, { path: true, method: true, customRoute })[0]; + } + } +} +/** JSDoc */ +function extractUserData( + user -},{"../object.js":158,"../supports.js":166,"../worldwide.js":174,"./_handlers.js":140}],144:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); +, + keys, +) { + const extractedUser = {}; + const attributes = Array.isArray(keys) ? keys : DEFAULT_USER_INCLUDES; -const worldwide = require('../worldwide.js'); -const _handlers = require('./_handlers.js'); + attributes.forEach(key => { + if (user && key in user) { + extractedUser[key] = user[key]; + } + }); -let _oldOnErrorHandler = null; + return extractedUser; +} /** - * Add an instrumentation handler for when an error is captured by the global error handler. + * Normalize data from the request object, accounting for framework differences. * - * Use at your own risk, this might break without changelog notice, only used internally. - * @hidden + * @param req The request object from which to extract data + * @param options.include An optional array of keys to include in the normalized data. Defaults to + * DEFAULT_REQUEST_INCLUDES if not provided. + * @param options.deps Injected, platform-specific dependencies + * @returns An object containing normalized request data */ -function addGlobalErrorInstrumentationHandler(handler) { - const type = 'error'; - _handlers.addHandler(type, handler); - _handlers.maybeInstrument(type, instrumentError); -} - -function instrumentError() { - _oldOnErrorHandler = worldwide.GLOBAL_OBJ.onerror; - - worldwide.GLOBAL_OBJ.onerror = function ( - msg, - url, - line, - column, - error, - ) { - const handlerData = { - column, - error, - line, - msg, - url, - }; - _handlers.triggerHandlers('error', handlerData); - - if (_oldOnErrorHandler && !_oldOnErrorHandler.__SENTRY_LOADER__) { - // eslint-disable-next-line prefer-rest-params - return _oldOnErrorHandler.apply(this, arguments); - } - - return false; - }; +function extractRequestData( + req, + options - worldwide.GLOBAL_OBJ.onerror.__SENTRY_INSTRUMENTED__ = true; -} +, +) { + const { include = DEFAULT_REQUEST_INCLUDES, deps } = options || {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const requestData = {}; -exports.addGlobalErrorInstrumentationHandler = addGlobalErrorInstrumentationHandler; + // headers: + // node, express, koa, nextjs: req.headers + const headers = (req.headers || {}) +; + // method: + // node, express, koa, nextjs: req.method + const method = req.method; + // host: + // express: req.hostname in > 4 and req.host in < 4 + // koa: req.host + // node, nextjs: req.headers.host + // Express 4 mistakenly strips off port number from req.host / req.hostname so we can't rely on them + // See: https://github.com/expressjs/express/issues/3047#issuecomment-236653223 + // Also: https://github.com/getsentry/sentry-javascript/issues/1917 + const host = headers.host || req.hostname || req.host || ''; + // protocol: + // node, nextjs: + // express, koa: req.protocol + const protocol = req.protocol === 'https' || (req.socket && req.socket.encrypted) ? 'https' : 'http'; + // url (including path and query string): + // node, express: req.originalUrl + // koa, nextjs: req.url + const originalUrl = req.originalUrl || req.url || ''; + // absolute url + const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`; + include.forEach(key => { + switch (key) { + case 'headers': { + requestData.headers = headers; -},{"../worldwide.js":174,"./_handlers.js":140}],145:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + // Remove the Cookie header in case cookie data should not be included in the event + if (!include.includes('cookies')) { + delete (requestData.headers ).cookie; + } -const worldwide = require('../worldwide.js'); -const _handlers = require('./_handlers.js'); + break; + } + case 'method': { + requestData.method = method; + break; + } + case 'url': { + requestData.url = absoluteUrl; + break; + } + case 'cookies': { + // cookies: + // node, express, koa: req.headers.cookie + // vercel, sails.js, express (w/ cookie middleware), nextjs: req.cookies + requestData.cookies = + // TODO (v8 / #5257): We're only sending the empty object for backwards compatibility, so the last bit can + // come off in v8 + req.cookies || (headers.cookie && cookie.parseCookie(headers.cookie)) || {}; + break; + } + case 'query_string': { + // query string: + // node: req.url (raw) + // express, koa, nextjs: req.query + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + requestData.query_string = extractQueryParams(req, deps); + break; + } + case 'data': { + if (method === 'GET' || method === 'HEAD') { + break; + } + // body data: + // express, koa, nextjs: req.body + // + // when using node by itself, you have to read the incoming stream(see + // https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know + // where they're going to store the final result, so they'll have to capture this data themselves + if (req.body !== undefined) { + requestData.data = is.isString(req.body) ? req.body : JSON.stringify(normalize.normalize(req.body)); + } + break; + } + default: { + if ({}.hasOwnProperty.call(req, key)) { + requestData[key] = (req )[key]; + } + } + } + }); -let _oldOnUnhandledRejectionHandler = null; + return requestData; +} /** - * Add an instrumentation handler for when an unhandled promise rejection is captured. + * Add data from the given request to the given event * - * Use at your own risk, this might break without changelog notice, only used internally. - * @hidden + * @param event The event to which the request data will be added + * @param req Request object + * @param options.include Flags to control what data is included + * @param options.deps Injected platform-specific dependencies + * @returns The mutated `Event` object */ -function addGlobalUnhandledRejectionInstrumentationHandler( - handler, +function addRequestDataToEvent( + event, + req, + options, ) { - const type = 'unhandledrejection'; - _handlers.addHandler(type, handler); - _handlers.maybeInstrument(type, instrumentUnhandledRejection); -} - -function instrumentUnhandledRejection() { - _oldOnUnhandledRejectionHandler = worldwide.GLOBAL_OBJ.onunhandledrejection; - - worldwide.GLOBAL_OBJ.onunhandledrejection = function (e) { - const handlerData = e; - _handlers.triggerHandlers('unhandledrejection', handlerData); - - if (_oldOnUnhandledRejectionHandler && !_oldOnUnhandledRejectionHandler.__SENTRY_LOADER__) { - // eslint-disable-next-line prefer-rest-params - return _oldOnUnhandledRejectionHandler.apply(this, arguments); - } - - return true; + const include = { + ...DEFAULT_INCLUDES, + ...(options && options.include), }; - worldwide.GLOBAL_OBJ.onunhandledrejection.__SENTRY_INSTRUMENTED__ = true; -} - -exports.addGlobalUnhandledRejectionInstrumentationHandler = addGlobalUnhandledRejectionInstrumentationHandler; + if (include.request) { + const extractedRequestData = Array.isArray(include.request) + ? extractRequestData(req, { include: include.request, deps: options && options.deps }) + : extractRequestData(req, { deps: options && options.deps }); + event.request = { + ...event.request, + ...extractedRequestData, + }; + } -},{"../worldwide.js":174,"./_handlers.js":140}],146:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + if (include.user) { + const extractedUser = req.user && is.isPlainObject(req.user) ? extractUserData(req.user, include.user) : {}; -const object = require('../object.js'); -require('../debug-build.js'); -require('../logger.js'); -const worldwide = require('../worldwide.js'); -const supportsHistory = require('../vendor/supportsHistory.js'); -const _handlers = require('./_handlers.js'); + if (Object.keys(extractedUser).length) { + event.user = { + ...event.user, + ...extractedUser, + }; + } + } -const WINDOW = worldwide.GLOBAL_OBJ ; + // client ip: + // node, nextjs: req.socket.remoteAddress + // express, koa: req.ip + if (include.ip) { + const ip = req.ip || (req.socket && req.socket.remoteAddress); + if (ip) { + event.user = { + ...event.user, + ip_address: ip, + }; + } + } -let lastHref; + if (include.transaction && !event.transaction) { + // TODO do we even need this anymore? + // TODO make this work for nextjs + event.transaction = extractTransaction(req, include.transaction); + } -/** - * Add an instrumentation handler for when a fetch request happens. - * The handler function is called once when the request starts and once when it ends, - * which can be identified by checking if it has an `endTimestamp`. - * - * Use at your own risk, this might break without changelog notice, only used internally. - * @hidden - */ -function addHistoryInstrumentationHandler(handler) { - const type = 'history'; - _handlers.addHandler(type, handler); - _handlers.maybeInstrument(type, instrumentHistory); + return event; } -function instrumentHistory() { - if (!supportsHistory.supportsHistory()) { +function extractQueryParams( + req, + deps, +) { + // url (including path and query string): + // node, express: req.originalUrl + // koa, nextjs: req.url + let originalUrl = req.originalUrl || req.url || ''; + + if (!originalUrl) { return; } - const oldOnPopState = WINDOW.onpopstate; - WINDOW.onpopstate = function ( ...args) { - const to = WINDOW.location.href; - // keep track of the current URL state, as we always receive only the updated state - const from = lastHref; - lastHref = to; - const handlerData = { from, to }; - _handlers.triggerHandlers('history', handlerData); - if (oldOnPopState) { - // Apparently this can throw in Firefox when incorrectly implemented plugin is installed. - // https://github.com/getsentry/sentry-javascript/issues/3344 - // https://github.com/bugsnag/bugsnag-js/issues/469 - try { - return oldOnPopState.apply(this, args); - } catch (_oO) { - // no-empty - } - } - }; - - function historyReplacementFunction(originalHistoryFunction) { - return function ( ...args) { - const url = args.length > 2 ? args[2] : undefined; - if (url) { - // coerce to string (this is what pushState does) - const from = lastHref; - const to = String(url); - // keep track of the current URL state, as we always receive only the updated state - lastHref = to; - const handlerData = { from, to }; - _handlers.triggerHandlers('history', handlerData); - } - return originalHistoryFunction.apply(this, args); - }; + // The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and + // hostname on the beginning. Since the point here is just to grab the query string, it doesn't matter what we use. + if (originalUrl.startsWith('/')) { + originalUrl = `http://dogs.are.great${originalUrl}`; } - object.fill(WINDOW.history, 'pushState', historyReplacementFunction); - object.fill(WINDOW.history, 'replaceState', historyReplacementFunction); + try { + return ( + req.query || + (typeof URL !== 'undefined' && new URL(originalUrl).search.slice(1)) || + // In Node 8, `URL` isn't in the global scope, so we have to use the built-in module from Node + (deps && deps.url && deps.url.parse(originalUrl).query) || + undefined + ); + } catch (e2) { + return undefined; + } } -exports.addHistoryInstrumentationHandler = addHistoryInstrumentationHandler; - - -},{"../debug-build.js":133,"../logger.js":151,"../object.js":158,"../vendor/supportsHistory.js":173,"../worldwide.js":174,"./_handlers.js":140}],147:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); - -const debugBuild = require('../debug-build.js'); -const logger = require('../logger.js'); -const console = require('./console.js'); -const dom = require('./dom.js'); -const fetch = require('./fetch.js'); -const globalError = require('./globalError.js'); -const globalUnhandledRejection = require('./globalUnhandledRejection.js'); -const history = require('./history.js'); -const xhr = require('./xhr.js'); +/** + * Transforms a `Headers` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into a simple key-value dict. + * The header keys will be lower case: e.g. A "Content-Type" header will be stored as "content-type". + */ +// TODO(v8): Make this function return undefined when the extraction fails. +function winterCGHeadersToDict(winterCGHeaders) { + const headers = {}; + try { + winterCGHeaders.forEach((value, key) => { + if (typeof value === 'string') { + // We check that value is a string even though it might be redundant to make sure prototype pollution is not possible. + headers[key] = value; + } + }); + } catch (e) { + debugBuild.DEBUG_BUILD && + logger.logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.'); + } -// TODO(v8): Consider moving this file (or at least parts of it) into the browser package. The registered handlers are mostly non-generic and we risk leaking runtime specific code into generic packages. + return headers; +} /** - * Add handler that will be called when given type of instrumentation triggers. - * Use at your own risk, this might break without changelog notice, only used internally. - * @hidden - * @deprecated Use the proper function per instrumentation type instead! + * Converts a `Request` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into the format that the `RequestData` integration understands. */ -function addInstrumentationHandler(type, callback) { - switch (type) { - case 'console': - return console.addConsoleInstrumentationHandler(callback); - case 'dom': - return dom.addClickKeypressInstrumentationHandler(callback); - case 'xhr': - return xhr.addXhrInstrumentationHandler(callback); - case 'fetch': - return fetch.addFetchInstrumentationHandler(callback); - case 'history': - return history.addHistoryInstrumentationHandler(callback); - case 'error': - return globalError.addGlobalErrorInstrumentationHandler(callback); - case 'unhandledrejection': - return globalUnhandledRejection.addGlobalUnhandledRejectionInstrumentationHandler(callback); - default: - debugBuild.DEBUG_BUILD && logger.logger.warn('unknown instrumentation type:', type); - } +function winterCGRequestToRequestData(req) { + const headers = winterCGHeadersToDict(req.headers); + return { + method: req.method, + url: req.url, + headers, + }; } -exports.addConsoleInstrumentationHandler = console.addConsoleInstrumentationHandler; -exports.addClickKeypressInstrumentationHandler = dom.addClickKeypressInstrumentationHandler; -exports.addFetchInstrumentationHandler = fetch.addFetchInstrumentationHandler; -exports.addGlobalErrorInstrumentationHandler = globalError.addGlobalErrorInstrumentationHandler; -exports.addGlobalUnhandledRejectionInstrumentationHandler = globalUnhandledRejection.addGlobalUnhandledRejectionInstrumentationHandler; -exports.addHistoryInstrumentationHandler = history.addHistoryInstrumentationHandler; -exports.SENTRY_XHR_DATA_KEY = xhr.SENTRY_XHR_DATA_KEY; -exports.addXhrInstrumentationHandler = xhr.addXhrInstrumentationHandler; -exports.addInstrumentationHandler = addInstrumentationHandler; +exports.DEFAULT_USER_INCLUDES = DEFAULT_USER_INCLUDES; +exports.addRequestDataToEvent = addRequestDataToEvent; +exports.addRequestDataToTransaction = addRequestDataToTransaction; +exports.extractPathForTransaction = extractPathForTransaction; +exports.extractRequestData = extractRequestData; +exports.winterCGHeadersToDict = winterCGHeadersToDict; +exports.winterCGRequestToRequestData = winterCGRequestToRequestData; -},{"../debug-build.js":133,"../logger.js":151,"./console.js":141,"./dom.js":142,"./fetch.js":143,"./globalError.js":144,"./globalUnhandledRejection.js":145,"./history.js":146,"./xhr.js":148}],148:[function(require,module,exports){ +},{"./cookie.js":145,"./debug-build.js":146,"./is.js":162,"./logger.js":164,"./normalize.js":170,"./url.js":183}],176:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const is = require('../is.js'); -const object = require('../object.js'); -const worldwide = require('../worldwide.js'); -const _handlers = require('./_handlers.js'); - -const WINDOW = worldwide.GLOBAL_OBJ ; +// Note: Ideally the `SeverityLevel` type would be derived from `validSeverityLevels`, but that would mean either +// +// a) moving `validSeverityLevels` to `@sentry/types`, +// b) moving the`SeverityLevel` type here, or +// c) importing `validSeverityLevels` from here into `@sentry/types`. +// +// Option A would make `@sentry/types` a runtime dependency of `@sentry/utils` (not good), and options B and C would +// create a circular dependency between `@sentry/types` and `@sentry/utils` (also not good). So a TODO accompanying the +// type, reminding anyone who changes it to change this list also, will have to do. -const SENTRY_XHR_DATA_KEY = '__sentry_xhr_v3__'; +const validSeverityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug']; /** - * Add an instrumentation handler for when an XHR request happens. - * The handler function is called once when the request starts and once when it ends, - * which can be identified by checking if it has an `endTimestamp`. + * Converts a string-based level into a member of the deprecated {@link Severity} enum. * - * Use at your own risk, this might break without changelog notice, only used internally. - * @hidden + * @deprecated `severityFromString` is deprecated. Please use `severityLevelFromString` instead. + * + * @param level String representation of Severity + * @returns Severity */ -function addXhrInstrumentationHandler(handler) { - const type = 'xhr'; - _handlers.addHandler(type, handler); - _handlers.maybeInstrument(type, instrumentXHR); +function severityFromString(level) { + return severityLevelFromString(level) ; } -/** Exported only for tests. */ -function instrumentXHR() { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!(WINDOW ).XMLHttpRequest) { - return; - } - - const xhrproto = XMLHttpRequest.prototype; - - object.fill(xhrproto, 'open', function (originalOpen) { - return function ( ...args) { - const startTimestamp = Date.now(); - - // open() should always be called with two or more arguments - // But to be on the safe side, we actually validate this and bail out if we don't have a method & url - const method = is.isString(args[0]) ? args[0].toUpperCase() : undefined; - const url = parseUrl(args[1]); - - if (!method || !url) { - return originalOpen.apply(this, args); - } - - this[SENTRY_XHR_DATA_KEY] = { - method, - url, - request_headers: {}, - }; - - // if Sentry key appears in URL, don't capture it as a request - if (method === 'POST' && url.match(/sentry_key/)) { - this.__sentry_own_request__ = true; - } +/** + * Converts a string-based level into a `SeverityLevel`, normalizing it along the way. + * + * @param level String representation of desired `SeverityLevel`. + * @returns The `SeverityLevel` corresponding to the given string, or 'log' if the string isn't a valid level. + */ +function severityLevelFromString(level) { + return (level === 'warn' ? 'warning' : validSeverityLevels.includes(level) ? level : 'log') ; +} - const onreadystatechangeHandler = () => { - // For whatever reason, this is not the same instance here as from the outer method - const xhrInfo = this[SENTRY_XHR_DATA_KEY]; +exports.severityFromString = severityFromString; +exports.severityLevelFromString = severityLevelFromString; +exports.validSeverityLevels = validSeverityLevels; - if (!xhrInfo) { - return; - } - if (this.readyState === 4) { - try { - // touching statusCode in some platforms throws - // an exception - xhrInfo.status_code = this.status; - } catch (e) { - /* do nothing */ - } +},{}],177:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - const handlerData = { - args: [method, url], - endTimestamp: Date.now(), - startTimestamp, - xhr: this, - }; - _handlers.triggerHandlers('xhr', handlerData); - } - }; +const nodeStackTrace = require('./node-stack-trace.js'); - if ('onreadystatechange' in this && typeof this.onreadystatechange === 'function') { - object.fill(this, 'onreadystatechange', function (original) { - return function ( ...readyStateArgs) { - onreadystatechangeHandler(); - return original.apply(this, readyStateArgs); - }; - }); - } else { - this.addEventListener('readystatechange', onreadystatechangeHandler); - } +const STACKTRACE_FRAME_LIMIT = 50; +// Used to sanitize webpack (error: *) wrapped stack errors +const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/; +const STRIP_FRAME_REGEXP = /captureMessage|captureException/; - // Intercepting `setRequestHeader` to access the request headers of XHR instance. - // This will only work for user/library defined headers, not for the default/browser-assigned headers. - // Request cookies are also unavailable for XHR, as `Cookie` header can't be defined by `setRequestHeader`. - object.fill(this, 'setRequestHeader', function (original) { - return function ( ...setRequestHeaderArgs) { - const [header, value] = setRequestHeaderArgs; +/** + * Creates a stack parser with the supplied line parsers + * + * StackFrames are returned in the correct order for Sentry Exception + * frames and with Sentry SDK internal frames removed from the top and bottom + * + */ +function createStackParser(...parsers) { + const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map(p => p[1]); - const xhrInfo = this[SENTRY_XHR_DATA_KEY]; + return (stack, skipFirst = 0) => { + const frames = []; + const lines = stack.split('\n'); - if (xhrInfo && is.isString(header) && is.isString(value)) { - xhrInfo.request_headers[header.toLowerCase()] = value; - } + for (let i = skipFirst; i < lines.length; i++) { + const line = lines[i]; + // Ignore lines over 1kb as they are unlikely to be stack frames. + // Many of the regular expressions use backtracking which results in run time that increases exponentially with + // input size. Huge strings can result in hangs/Denial of Service: + // https://github.com/getsentry/sentry-javascript/issues/2286 + if (line.length > 1024) { + continue; + } - return original.apply(this, setRequestHeaderArgs); - }; - }); + // https://github.com/getsentry/sentry-javascript/issues/5459 + // Remove webpack (error: *) wrappers + const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, '$1') : line; - return originalOpen.apply(this, args); - }; - }); + // https://github.com/getsentry/sentry-javascript/issues/7813 + // Skip Error: lines + if (cleanedLine.match(/\S*Error: /)) { + continue; + } - object.fill(xhrproto, 'send', function (originalSend) { - return function ( ...args) { - const sentryXhrData = this[SENTRY_XHR_DATA_KEY]; + for (const parser of sortedParsers) { + const frame = parser(cleanedLine); - if (!sentryXhrData) { - return originalSend.apply(this, args); + if (frame) { + frames.push(frame); + break; + } } - if (args[0] !== undefined) { - sentryXhrData.body = args[0]; + if (frames.length >= STACKTRACE_FRAME_LIMIT) { + break; } + } - const handlerData = { - args: [sentryXhrData.method, sentryXhrData.url], - startTimestamp: Date.now(), - xhr: this, - }; - _handlers.triggerHandlers('xhr', handlerData); + return stripSentryFramesAndReverse(frames); + }; +} - return originalSend.apply(this, args); - }; - }); +/** + * Gets a stack parser implementation from Options.stackParser + * @see Options + * + * If options contains an array of line parsers, it is converted into a parser + */ +function stackParserFromStackParserOptions(stackParser) { + if (Array.isArray(stackParser)) { + return createStackParser(...stackParser); + } + return stackParser; } -function parseUrl(url) { - if (is.isString(url)) { - return url; +/** + * Removes Sentry frames from the top and bottom of the stack if present and enforces a limit of max number of frames. + * Assumes stack input is ordered from top to bottom and returns the reverse representation so call site of the + * function that caused the crash is the last frame in the array. + * @hidden + */ +function stripSentryFramesAndReverse(stack) { + if (!stack.length) { + return []; } - try { - // url can be a string or URL - // but since URL is not available in IE11, we do not check for it, - // but simply assume it is an URL and return `toString()` from it (which returns the full URL) - // If that fails, we just return undefined - return (url ).toString(); - } catch (e2) {} // eslint-disable-line no-empty + const localStack = Array.from(stack); - return undefined; -} + // If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call) + if (/sentryWrapped/.test(localStack[localStack.length - 1].function || '')) { + localStack.pop(); + } -exports.SENTRY_XHR_DATA_KEY = SENTRY_XHR_DATA_KEY; -exports.addXhrInstrumentationHandler = addXhrInstrumentationHandler; -exports.instrumentXHR = instrumentXHR; + // Reversing in the middle of the procedure allows us to just pop the values off the stack + localStack.reverse(); + // If stack ends with one of our internal API calls, remove it (ends, meaning it's the bottom of the stack - aka top-most call) + if (STRIP_FRAME_REGEXP.test(localStack[localStack.length - 1].function || '')) { + localStack.pop(); -},{"../is.js":149,"../object.js":158,"../worldwide.js":174,"./_handlers.js":140}],149:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + // When using synthetic events, we will have a 2 levels deep stack, as `new Error('Sentry syntheticException')` + // is produced within the hub itself, making it: + // + // Sentry.captureException() + // getCurrentHub().captureException() + // + // instead of just the top `Sentry` call itself. + // This forces us to possibly strip an additional frame in the exact same was as above. + if (STRIP_FRAME_REGEXP.test(localStack[localStack.length - 1].function || '')) { + localStack.pop(); + } + } -// eslint-disable-next-line @typescript-eslint/unbound-method -const objectToString = Object.prototype.toString; + return localStack.slice(0, STACKTRACE_FRAME_LIMIT).map(frame => ({ + ...frame, + filename: frame.filename || localStack[localStack.length - 1].filename, + function: frame.function || '?', + })); +} + +const defaultFunctionName = ''; /** - * Checks whether given value's type is one of a few Error or Error-like - * {@link isError}. - * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * Safely extract function name from itself */ -function isError(wat) { - switch (objectToString.call(wat)) { - case '[object Error]': - case '[object Exception]': - case '[object DOMException]': - return true; - default: - return isInstanceOf(wat, Error); +function getFunctionName(fn) { + try { + if (!fn || typeof fn !== 'function') { + return defaultFunctionName; + } + return fn.name || defaultFunctionName; + } catch (e) { + // Just accessing custom props in some Selenium environments + // can cause a "Permission denied" exception (see raven-js#495). + return defaultFunctionName; } } -/** - * Checks whether given value is an instance of the given built-in class. - * - * @param wat The value to be checked - * @param className - * @returns A boolean representing the result. - */ -function isBuiltin(wat, className) { - return objectToString.call(wat) === `[object ${className}]`; -} /** - * Checks whether given value's type is ErrorEvent - * {@link isErrorEvent}. + * Node.js stack line parser * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * This is in @sentry/utils so it can be used from the Electron SDK in the browser for when `nodeIntegration == true`. + * This allows it to be used without referencing or importing any node specific code which causes bundlers to complain */ -function isErrorEvent(wat) { - return isBuiltin(wat, 'ErrorEvent'); +function nodeStackLineParser(getModule) { + return [90, nodeStackTrace.node(getModule)]; } -/** - * Checks whether given value's type is DOMError - * {@link isDOMError}. - * - * @param wat A value to be checked. - * @returns A boolean representing the result. - */ -function isDOMError(wat) { - return isBuiltin(wat, 'DOMError'); -} +exports.filenameIsInApp = nodeStackTrace.filenameIsInApp; +exports.createStackParser = createStackParser; +exports.getFunctionName = getFunctionName; +exports.nodeStackLineParser = nodeStackLineParser; +exports.stackParserFromStackParserOptions = stackParserFromStackParserOptions; +exports.stripSentryFramesAndReverse = stripSentryFramesAndReverse; -/** - * Checks whether given value's type is DOMException - * {@link isDOMException}. - * - * @param wat A value to be checked. - * @returns A boolean representing the result. - */ -function isDOMException(wat) { - return isBuiltin(wat, 'DOMException'); -} + +},{"./node-stack-trace.js":168}],178:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const is = require('./is.js'); /** - * Checks whether given value's type is a string - * {@link isString}. + * Truncates given string to the maximum characters count * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * @param str An object that contains serializable values + * @param max Maximum number of characters in truncated string (0 = unlimited) + * @returns string Encoded */ -function isString(wat) { - return isBuiltin(wat, 'String'); +function truncate(str, max = 0) { + if (typeof str !== 'string' || max === 0) { + return str; + } + return str.length <= max ? str : `${str.slice(0, max)}...`; } /** - * Checks whether given string is parameterized - * {@link isParameterizedString}. + * This is basically just `trim_line` from + * https://github.com/getsentry/sentry/blob/master/src/sentry/lang/javascript/processor.py#L67 * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * @param str An object that contains serializable values + * @param max Maximum number of characters in truncated string + * @returns string Encoded */ -function isParameterizedString(wat) { - return ( - typeof wat === 'object' && - wat !== null && - '__sentry_template_string__' in wat && - '__sentry_template_values__' in wat - ); +function snipLine(line, colno) { + let newLine = line; + const lineLength = newLine.length; + if (lineLength <= 150) { + return newLine; + } + if (colno > lineLength) { + // eslint-disable-next-line no-param-reassign + colno = lineLength; + } + + let start = Math.max(colno - 60, 0); + if (start < 5) { + start = 0; + } + + let end = Math.min(start + 140, lineLength); + if (end > lineLength - 5) { + end = lineLength; + } + if (end === lineLength) { + start = Math.max(end - 140, 0); + } + + newLine = newLine.slice(start, end); + if (start > 0) { + newLine = `'{snip} ${newLine}`; + } + if (end < lineLength) { + newLine += ' {snip}'; + } + + return newLine; } /** - * Checks whether given value is a primitive (undefined, null, number, boolean, string, bigint, symbol) - * {@link isPrimitive}. - * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * Join values in array + * @param input array of values to be joined together + * @param delimiter string to be placed in-between values + * @returns Joined values */ -function isPrimitive(wat) { - return wat === null || isParameterizedString(wat) || (typeof wat !== 'object' && typeof wat !== 'function'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function safeJoin(input, delimiter) { + if (!Array.isArray(input)) { + return ''; + } + + const output = []; + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < input.length; i++) { + const value = input[i]; + try { + // This is a hack to fix a Vue3-specific bug that causes an infinite loop of + // console warnings. This happens when a Vue template is rendered with + // an undeclared variable, which we try to stringify, ultimately causing + // Vue to issue another warning which repeats indefinitely. + // see: https://github.com/getsentry/sentry-javascript/pull/8981 + if (is.isVueViewModel(value)) { + output.push('[VueViewModel]'); + } else { + output.push(String(value)); + } + } catch (e) { + output.push('[value cannot be serialized]'); + } + } + + return output.join(delimiter); } /** - * Checks whether given value's type is an object literal, or a class instance. - * {@link isPlainObject}. + * Checks if the given value matches a regex or string * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * @param value The string to test + * @param pattern Either a regex or a string against which `value` will be matched + * @param requireExactStringMatch If true, `value` must match `pattern` exactly. If false, `value` will match + * `pattern` if it contains `pattern`. Only applies to string-type patterns. */ -function isPlainObject(wat) { - return isBuiltin(wat, 'Object'); +function isMatchingPattern( + value, + pattern, + requireExactStringMatch = false, +) { + if (!is.isString(value)) { + return false; + } + + if (is.isRegExp(pattern)) { + return pattern.test(value); + } + if (is.isString(pattern)) { + return requireExactStringMatch ? value === pattern : value.includes(pattern); + } + + return false; } /** - * Checks whether given value's type is an Event instance - * {@link isEvent}. + * Test the given string against an array of strings and regexes. By default, string matching is done on a + * substring-inclusion basis rather than a strict equality basis * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * @param testString The string to test + * @param patterns The patterns against which to test the string + * @param requireExactStringMatch If true, `testString` must match one of the given string patterns exactly in order to + * count. If false, `testString` will match a string pattern if it contains that pattern. + * @returns */ -function isEvent(wat) { - return typeof Event !== 'undefined' && isInstanceOf(wat, Event); +function stringMatchesSomePattern( + testString, + patterns = [], + requireExactStringMatch = false, +) { + return patterns.some(pattern => isMatchingPattern(testString, pattern, requireExactStringMatch)); } +exports.isMatchingPattern = isMatchingPattern; +exports.safeJoin = safeJoin; +exports.snipLine = snipLine; +exports.stringMatchesSomePattern = stringMatchesSomePattern; +exports.truncate = truncate; + + +},{"./is.js":162}],179:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); + +const debugBuild = require('./debug-build.js'); +const logger = require('./logger.js'); +const worldwide = require('./worldwide.js'); + +// eslint-disable-next-line deprecation/deprecation +const WINDOW = worldwide.getGlobalObject(); + /** - * Checks whether given value's type is an Element instance - * {@link isElement}. + * Tells whether current environment supports ErrorEvent objects + * {@link supportsErrorEvent}. * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * @returns Answer to the given question. */ -function isElement(wat) { - return typeof Element !== 'undefined' && isInstanceOf(wat, Element); +function supportsErrorEvent() { + try { + new ErrorEvent(''); + return true; + } catch (e) { + return false; + } } /** - * Checks whether given value's type is an regexp - * {@link isRegExp}. + * Tells whether current environment supports DOMError objects + * {@link supportsDOMError}. * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * @returns Answer to the given question. */ -function isRegExp(wat) { - return isBuiltin(wat, 'RegExp'); +function supportsDOMError() { + try { + // Chrome: VM89:1 Uncaught TypeError: Failed to construct 'DOMError': + // 1 argument required, but only 0 present. + // @ts-expect-error It really needs 1 argument, not 0. + new DOMError(''); + return true; + } catch (e) { + return false; + } } /** - * Checks whether given value has a then function. - * @param wat A value to be checked. + * Tells whether current environment supports DOMException objects + * {@link supportsDOMException}. + * + * @returns Answer to the given question. */ -function isThenable(wat) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return Boolean(wat && wat.then && typeof wat.then === 'function'); +function supportsDOMException() { + try { + new DOMException(''); + return true; + } catch (e) { + return false; + } } /** - * Checks whether given value's type is a SyntheticEvent - * {@link isSyntheticEvent}. + * Tells whether current environment supports Fetch API + * {@link supportsFetch}. * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * @returns Answer to the given question. */ -function isSyntheticEvent(wat) { - return isPlainObject(wat) && 'nativeEvent' in wat && 'preventDefault' in wat && 'stopPropagation' in wat; -} +function supportsFetch() { + if (!('fetch' in WINDOW)) { + return false; + } + try { + new Headers(); + new Request('http://www.example.com'); + new Response(); + return true; + } catch (e) { + return false; + } +} /** - * Checks whether given value is NaN - * {@link isNaN}. - * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * isNativeFetch checks if the given function is a native implementation of fetch() */ -function isNaN(wat) { - return typeof wat === 'number' && wat !== wat; +// eslint-disable-next-line @typescript-eslint/ban-types +function isNativeFetch(func) { + return func && /^function fetch\(\)\s+\{\s+\[native code\]\s+\}$/.test(func.toString()); } /** - * Checks whether given value's type is an instance of provided constructor. - * {@link isInstanceOf}. + * Tells whether current environment supports Fetch API natively + * {@link supportsNativeFetch}. * - * @param wat A value to be checked. - * @param base A constructor to be used in a check. - * @returns A boolean representing the result. + * @returns true if `window.fetch` is natively implemented, false otherwise */ -function isInstanceOf(wat, base) { - try { - return wat instanceof base; - } catch (_e) { +function supportsNativeFetch() { + if (typeof EdgeRuntime === 'string') { + return true; + } + + if (!supportsFetch()) { return false; } + + // Fast path to avoid DOM I/O + // eslint-disable-next-line @typescript-eslint/unbound-method + if (isNativeFetch(WINDOW.fetch)) { + return true; + } + + // window.fetch is implemented, but is polyfilled or already wrapped (e.g: by a chrome extension) + // so create a "pure" iframe to see if that has native fetch + let result = false; + const doc = WINDOW.document; + // eslint-disable-next-line deprecation/deprecation + if (doc && typeof (doc.createElement ) === 'function') { + try { + const sandbox = doc.createElement('iframe'); + sandbox.hidden = true; + doc.head.appendChild(sandbox); + if (sandbox.contentWindow && sandbox.contentWindow.fetch) { + // eslint-disable-next-line @typescript-eslint/unbound-method + result = isNativeFetch(sandbox.contentWindow.fetch); + } + doc.head.removeChild(sandbox); + } catch (err) { + debugBuild.DEBUG_BUILD && + logger.logger.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', err); + } + } + + return result; } /** - * Checks whether given value's type is a Vue ViewModel. + * Tells whether current environment supports ReportingObserver API + * {@link supportsReportingObserver}. * - * @param wat A value to be checked. - * @returns A boolean representing the result. + * @returns Answer to the given question. */ -function isVueViewModel(wat) { - // Not using Object.prototype.toString because in Vue 3 it would read the instance's Symbol(Symbol.toStringTag) property. - return !!(typeof wat === 'object' && wat !== null && ((wat ).__isVue || (wat )._isVue)); +function supportsReportingObserver() { + return 'ReportingObserver' in WINDOW; } -exports.isDOMError = isDOMError; -exports.isDOMException = isDOMException; -exports.isElement = isElement; -exports.isError = isError; -exports.isErrorEvent = isErrorEvent; -exports.isEvent = isEvent; -exports.isInstanceOf = isInstanceOf; -exports.isNaN = isNaN; -exports.isParameterizedString = isParameterizedString; -exports.isPlainObject = isPlainObject; -exports.isPrimitive = isPrimitive; -exports.isRegExp = isRegExp; -exports.isString = isString; -exports.isSyntheticEvent = isSyntheticEvent; -exports.isThenable = isThenable; -exports.isVueViewModel = isVueViewModel; - - -},{}],150:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); - -const node = require('./node.js'); -const worldwide = require('./worldwide.js'); - /** - * Returns true if we are in the browser. + * Tells whether current environment supports Referrer Policy API + * {@link supportsReferrerPolicy}. + * + * @returns Answer to the given question. */ -function isBrowser() { - // eslint-disable-next-line no-restricted-globals - return typeof window !== 'undefined' && (!node.isNodeEnv() || isElectronNodeRenderer()); -} +function supportsReferrerPolicy() { + // Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default' + // (see https://caniuse.com/#feat=referrer-policy), + // it doesn't. And it throws an exception instead of ignoring this parameter... + // REF: https://github.com/getsentry/raven-js/issues/1233 -// Electron renderers with nodeIntegration enabled are detected as Node.js so we specifically test for them -function isElectronNodeRenderer() { - return ( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - (worldwide.GLOBAL_OBJ ).process !== undefined && ((worldwide.GLOBAL_OBJ ).process ).type === 'renderer' - ); + if (!supportsFetch()) { + return false; + } + + try { + new Request('_', { + referrerPolicy: 'origin' , + }); + return true; + } catch (e) { + return false; + } } -exports.isBrowser = isBrowser; +exports.isNativeFetch = isNativeFetch; +exports.supportsDOMError = supportsDOMError; +exports.supportsDOMException = supportsDOMException; +exports.supportsErrorEvent = supportsErrorEvent; +exports.supportsFetch = supportsFetch; +exports.supportsNativeFetch = supportsNativeFetch; +exports.supportsReferrerPolicy = supportsReferrerPolicy; +exports.supportsReportingObserver = supportsReportingObserver; -},{"./node.js":156,"./worldwide.js":174}],151:[function(require,module,exports){ +},{"./debug-build.js":146,"./logger.js":164,"./worldwide.js":187}],180:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const debugBuild = require('./debug-build.js'); -const worldwide = require('./worldwide.js'); - -/** Prefix for logging strings */ -const PREFIX = 'Sentry Logger '; - -const CONSOLE_LEVELS = [ - 'debug', - 'info', - 'warn', - 'error', - 'log', - 'assert', - 'trace', -] ; +const is = require('./is.js'); -/** This may be mutated by the console instrumentation. */ -const originalConsoleMethods +/* eslint-disable @typescript-eslint/explicit-function-return-type */ - = {}; +/** SyncPromise internal states */ +var States; (function (States) { + /** Pending */ + const PENDING = 0; States[States["PENDING"] = PENDING] = "PENDING"; + /** Resolved / OK */ + const RESOLVED = 1; States[States["RESOLVED"] = RESOLVED] = "RESOLVED"; + /** Rejected / Error */ + const REJECTED = 2; States[States["REJECTED"] = REJECTED] = "REJECTED"; +})(States || (States = {})); -/** JSDoc */ +// Overloads so we can call resolvedSyncPromise without arguments and generic argument /** - * Temporarily disable sentry console instrumentations. + * Creates a resolved sync promise. * - * @param callback The function to run against the original `console` messages - * @returns The results of the callback + * @param value the value to resolve the promise with + * @returns the resolved sync promise */ -function consoleSandbox(callback) { - if (!('console' in worldwide.GLOBAL_OBJ)) { - return callback(); - } - - const console = worldwide.GLOBAL_OBJ.console ; - const wrappedFuncs = {}; - - const wrappedLevels = Object.keys(originalConsoleMethods) ; - - // Restore all wrapped console methods - wrappedLevels.forEach(level => { - const originalConsoleMethod = originalConsoleMethods[level] ; - wrappedFuncs[level] = console[level] ; - console[level] = originalConsoleMethod; +function resolvedSyncPromise(value) { + return new SyncPromise(resolve => { + resolve(value); }); - - try { - return callback(); - } finally { - // Revert restoration to wrapped state - wrappedLevels.forEach(level => { - console[level] = wrappedFuncs[level] ; - }); - } } -function makeLogger() { - let enabled = false; - const logger = { - enable: () => { - enabled = true; - }, - disable: () => { - enabled = false; - }, - isEnabled: () => enabled, - }; - - if (debugBuild.DEBUG_BUILD) { - CONSOLE_LEVELS.forEach(name => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - logger[name] = (...args) => { - if (enabled) { - consoleSandbox(() => { - worldwide.GLOBAL_OBJ.console[name](`${PREFIX}[${name}]:`, ...args); - }); - } - }; - }); - } else { - CONSOLE_LEVELS.forEach(name => { - logger[name] = () => undefined; - }); - } - - return logger ; +/** + * Creates a rejected sync promise. + * + * @param value the value to reject the promise with + * @returns the rejected sync promise + */ +function rejectedSyncPromise(reason) { + return new SyncPromise((_, reject) => { + reject(reason); + }); } -const logger = makeLogger(); - -exports.CONSOLE_LEVELS = CONSOLE_LEVELS; -exports.consoleSandbox = consoleSandbox; -exports.logger = logger; -exports.originalConsoleMethods = originalConsoleMethods; - - -},{"./debug-build.js":133,"./worldwide.js":174}],152:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); - -/** A simple Least Recently Used map */ -class LRUMap { - - constructor( _maxSize) {this._maxSize = _maxSize; - this._cache = new Map(); - } +/** + * Thenable class that behaves like a Promise and follows it's interface + * but is not async internally + */ +class SyncPromise { - /** Get the current size of the cache */ - get size() { - return this._cache.size; - } + constructor( + executor, + ) {SyncPromise.prototype.__init.call(this);SyncPromise.prototype.__init2.call(this);SyncPromise.prototype.__init3.call(this);SyncPromise.prototype.__init4.call(this); + this._state = States.PENDING; + this._handlers = []; - /** Get an entry or undefined if it was not in the cache. Re-inserts to update the recently used order */ - get(key) { - const value = this._cache.get(key); - if (value === undefined) { - return undefined; + try { + executor(this._resolve, this._reject); + } catch (e) { + this._reject(e); } - // Remove and re-insert to update the order - this._cache.delete(key); - this._cache.set(key, value); - return value; } - /** Insert an entry and evict an older entry if we've reached maxSize */ - set(key, value) { - if (this._cache.size >= this._maxSize) { - // keys() returns an iterator in insertion order so keys().next() gives us the oldest key - this._cache.delete(this._cache.keys().next().value); - } - this._cache.set(key, value); + /** JSDoc */ + then( + onfulfilled, + onrejected, + ) { + return new SyncPromise((resolve, reject) => { + this._handlers.push([ + false, + result => { + if (!onfulfilled) { + // TODO: ¯\_(ツ)_/¯ + // TODO: FIXME + resolve(result ); + } else { + try { + resolve(onfulfilled(result)); + } catch (e) { + reject(e); + } + } + }, + reason => { + if (!onrejected) { + reject(reason); + } else { + try { + resolve(onrejected(reason)); + } catch (e) { + reject(e); + } + } + }, + ]); + this._executeHandlers(); + }); } - /** Remove an entry and return the entry if it was in the cache */ - remove(key) { - const value = this._cache.get(key); - if (value) { - this._cache.delete(key); - } - return value; + /** JSDoc */ + catch( + onrejected, + ) { + return this.then(val => val, onrejected); } - /** Clear all entries */ - clear() { - this._cache.clear(); - } + /** JSDoc */ + finally(onfinally) { + return new SyncPromise((resolve, reject) => { + let val; + let isRejected; - /** Get all the keys */ - keys() { - return Array.from(this._cache.keys()); - } + return this.then( + value => { + isRejected = false; + val = value; + if (onfinally) { + onfinally(); + } + }, + reason => { + isRejected = true; + val = reason; + if (onfinally) { + onfinally(); + } + }, + ).then(() => { + if (isRejected) { + reject(val); + return; + } - /** Get all the values */ - values() { - const values = []; - this._cache.forEach(value => values.push(value)); - return values; + resolve(val ); + }); + }); } -} -exports.LRUMap = LRUMap; - - -},{}],153:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + /** JSDoc */ + __init() {this._resolve = (value) => { + this._setResult(States.RESOLVED, value); + };} -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-explicit-any */ + /** JSDoc */ + __init2() {this._reject = (reason) => { + this._setResult(States.REJECTED, reason); + };} -/** - * Helper to decycle json objects - */ -function memoBuilder() { - const hasWeakSet = typeof WeakSet === 'function'; - const inner = hasWeakSet ? new WeakSet() : []; - function memoize(obj) { - if (hasWeakSet) { - if (inner.has(obj)) { - return true; - } - inner.add(obj); - return false; - } - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < inner.length; i++) { - const value = inner[i]; - if (value === obj) { - return true; - } + /** JSDoc */ + __init3() {this._setResult = (state, value) => { + if (this._state !== States.PENDING) { + return; } - inner.push(obj); - return false; - } - function unmemoize(obj) { - if (hasWeakSet) { - inner.delete(obj); - } else { - for (let i = 0; i < inner.length; i++) { - if (inner[i] === obj) { - inner.splice(i, 1); - break; - } - } + if (is.isThenable(value)) { + void (value ).then(this._resolve, this._reject); + return; } - } - return [memoize, unmemoize]; -} - -exports.memoBuilder = memoBuilder; - -},{}],154:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); - -const object = require('./object.js'); -const string = require('./string.js'); -const worldwide = require('./worldwide.js'); + this._state = state; + this._value = value; -/** - * UUID4 generator - * - * @returns string Generated UUID4. - */ -function uuid4() { - const gbl = worldwide.GLOBAL_OBJ ; - const crypto = gbl.crypto || gbl.msCrypto; + this._executeHandlers(); + };} - let getRandomByte = () => Math.random() * 16; - try { - if (crypto && crypto.randomUUID) { - return crypto.randomUUID().replace(/-/g, ''); - } - if (crypto && crypto.getRandomValues) { - getRandomByte = () => { - // crypto.getRandomValues might return undefined instead of the typed array - // in old Chromium versions (e.g. 23.0.1235.0 (151422)) - // However, `typedArray` is still filled in-place. - // @see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#typedarray - const typedArray = new Uint8Array(1); - crypto.getRandomValues(typedArray); - return typedArray[0]; - }; + /** JSDoc */ + __init4() {this._executeHandlers = () => { + if (this._state === States.PENDING) { + return; } - } catch (_) { - // some runtimes can crash invoking crypto - // https://github.com/getsentry/sentry-javascript/issues/8935 - } - // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 - // Concatenating the following numbers as strings results in '10000000100040008000100000000000' - return (([1e7] ) + 1e3 + 4e3 + 8e3 + 1e11).replace(/[018]/g, c => - // eslint-disable-next-line no-bitwise - ((c ) ^ ((getRandomByte() & 15) >> ((c ) / 4))).toString(16), - ); -} + const cachedHandlers = this._handlers.slice(); + this._handlers = []; -function getFirstException(event) { - return event.exception && event.exception.values ? event.exception.values[0] : undefined; -} + cachedHandlers.forEach(handler => { + if (handler[0]) { + return; + } -/** - * Extracts either message or type+value from an event that can be used for user-facing logs - * @returns event's description - */ -function getEventDescription(event) { - const { message, event_id: eventId } = event; - if (message) { - return message; - } + if (this._state === States.RESOLVED) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handler[1](this._value ); + } - const firstException = getFirstException(event); - if (firstException) { - if (firstException.type && firstException.value) { - return `${firstException.type}: ${firstException.value}`; - } - return firstException.type || firstException.value || eventId || ''; - } - return eventId || ''; + if (this._state === States.REJECTED) { + handler[2](this._value); + } + + handler[0] = true; + }); + };} } -/** - * Adds exception values, type and value to an synthetic Exception. - * @param event The event to modify. - * @param value Value of the exception. - * @param type Type of the exception. - * @hidden - */ -function addExceptionTypeValue(event, value, type) { - const exception = (event.exception = event.exception || {}); - const values = (exception.values = exception.values || []); - const firstException = (values[0] = values[0] || {}); - if (!firstException.value) { - firstException.value = value || ''; - } - if (!firstException.type) { - firstException.type = type || 'Error'; - } -} +exports.SyncPromise = SyncPromise; +exports.rejectedSyncPromise = rejectedSyncPromise; +exports.resolvedSyncPromise = resolvedSyncPromise; -/** - * Adds exception mechanism data to a given event. Uses defaults if the second parameter is not passed. - * - * @param event The event to modify. - * @param newMechanism Mechanism data to add to the event. - * @hidden - */ -function addExceptionMechanism(event, newMechanism) { - const firstException = getFirstException(event); - if (!firstException) { - return; - } - const defaultMechanism = { type: 'generic', handled: true }; - const currentMechanism = firstException.mechanism; - firstException.mechanism = { ...defaultMechanism, ...currentMechanism, ...newMechanism }; +},{"./is.js":162}],181:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - if (newMechanism && 'data' in newMechanism) { - const mergedData = { ...(currentMechanism && currentMechanism.data), ...newMechanism.data }; - firstException.mechanism.data = mergedData; - } -} +const worldwide = require('./worldwide.js'); -// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string -const SEMVER_REGEXP = - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; +const ONE_SECOND_IN_MS = 1000; /** - * Represents Semantic Versioning object + * A partial definition of the [Performance Web API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Performance} + * for accessing a high-resolution monotonic clock. */ /** - * Parses input into a SemVer interface - * @param input string representation of a semver version + * Returns a timestamp in seconds since the UNIX epoch using the Date API. + * + * TODO(v8): Return type should be rounded. */ -function parseSemver(input) { - const match = input.match(SEMVER_REGEXP) || []; - const major = parseInt(match[1], 10); - const minor = parseInt(match[2], 10); - const patch = parseInt(match[3], 10); - return { - buildmetadata: match[5], - major: isNaN(major) ? undefined : major, - minor: isNaN(minor) ? undefined : minor, - patch: isNaN(patch) ? undefined : patch, - prerelease: match[4], - }; +function dateTimestampInSeconds() { + return Date.now() / ONE_SECOND_IN_MS; } /** - * This function adds context (pre/post/line) lines to the provided frame + * Returns a wrapper around the native Performance API browser implementation, or undefined for browsers that do not + * support the API. * - * @param lines string[] containing all lines - * @param frame StackFrame that will be mutated - * @param linesOfContext number of context lines we want to add pre/post + * Wrapping the native API works around differences in behavior from different browsers. */ -function addContextToFrame(lines, frame, linesOfContext = 5) { - // When there is no line number in the frame, attaching context is nonsensical and will even break grouping - if (frame.lineno === undefined) { - return; +function createUnixTimestampInSecondsFunc() { + const { performance } = worldwide.GLOBAL_OBJ ; + if (!performance || !performance.now) { + return dateTimestampInSeconds; } - const maxLines = lines.length; - const sourceLine = Math.max(Math.min(maxLines - 1, frame.lineno - 1), 0); - - frame.pre_context = lines - .slice(Math.max(0, sourceLine - linesOfContext), sourceLine) - .map((line) => string.snipLine(line, 0)); - - frame.context_line = string.snipLine(lines[Math.min(maxLines - 1, sourceLine)], frame.colno || 0); + // Some browser and environments don't have a timeOrigin, so we fallback to + // using Date.now() to compute the starting time. + const approxStartingTimeOrigin = Date.now() - performance.now(); + const timeOrigin = performance.timeOrigin == undefined ? approxStartingTimeOrigin : performance.timeOrigin; - frame.post_context = lines - .slice(Math.min(sourceLine + 1, maxLines), sourceLine + 1 + linesOfContext) - .map((line) => string.snipLine(line, 0)); + // performance.now() is a monotonic clock, which means it starts at 0 when the process begins. To get the current + // wall clock time (actual UNIX timestamp), we need to add the starting time origin and the current time elapsed. + // + // TODO: This does not account for the case where the monotonic clock that powers performance.now() drifts from the + // wall clock time, which causes the returned timestamp to be inaccurate. We should investigate how to detect and + // correct for this. + // See: https://github.com/getsentry/sentry-javascript/issues/2590 + // See: https://github.com/mdn/content/issues/4713 + // See: https://dev.to/noamr/when-a-millisecond-is-not-a-millisecond-3h6 + return () => { + return (timeOrigin + performance.now()) / ONE_SECOND_IN_MS; + }; } /** - * Checks whether or not we've already captured the given exception (note: not an identical exception - the very object - * in question), and marks it captured if not. - * - * This is useful because it's possible for an error to get captured by more than one mechanism. After we intercept and - * record an error, we rethrow it (assuming we've intercepted it before it's reached the top-level global handlers), so - * that we don't interfere with whatever effects the error might have had were the SDK not there. At that point, because - * the error has been rethrown, it's possible for it to bubble up to some other code we've instrumented. If it's not - * caught after that, it will bubble all the way up to the global handlers (which of course we also instrument). This - * function helps us ensure that even if we encounter the same error more than once, we only record it the first time we - * see it. - * - * Note: It will ignore primitives (always return `false` and not mark them as seen), as properties can't be set on - * them. {@link: Object.objectify} can be used on exceptions to convert any that are primitives into their equivalent - * object wrapper forms so that this check will always work. However, because we need to flag the exact object which - * will get rethrown, and because that rethrowing happens outside of the event processing pipeline, the objectification - * must be done before the exception captured. + * Returns a timestamp in seconds since the UNIX epoch using either the Performance or Date APIs, depending on the + * availability of the Performance API. * - * @param A thrown exception to check or flag as having been seen - * @returns `true` if the exception has already been captured, `false` if not (with the side effect of marking it seen) + * BUG: Note that because of how browsers implement the Performance API, the clock might stop when the computer is + * asleep. This creates a skew between `dateTimestampInSeconds` and `timestampInSeconds`. The + * skew can grow to arbitrary amounts like days, weeks or months. + * See https://github.com/getsentry/sentry-javascript/issues/2590. */ -function checkOrSetAlreadyCaught(exception) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (exception && (exception ).__sentry_captured__) { - return true; - } - - try { - // set it this way rather than by assignment so that it's not ennumerable and therefore isn't recorded by the - // `ExtraErrorData` integration - object.addNonEnumerableProperty(exception , '__sentry_captured__', true); - } catch (err) { - // `exception` is a primitive, so we can't mark it seen - } - - return false; -} +const timestampInSeconds = createUnixTimestampInSecondsFunc(); /** - * Checks whether the given input is already an array, and if it isn't, wraps it in one. + * Re-exported with an old name for backwards-compatibility. + * TODO (v8): Remove this * - * @param maybeArray Input to turn into an array, if necessary - * @returns The input, if already an array, or an array with the input as the only element, if not + * @deprecated Use `timestampInSeconds` instead. */ -function arrayify(maybeArray) { - return Array.isArray(maybeArray) ? maybeArray : [maybeArray]; -} - -exports.addContextToFrame = addContextToFrame; -exports.addExceptionMechanism = addExceptionMechanism; -exports.addExceptionTypeValue = addExceptionTypeValue; -exports.arrayify = arrayify; -exports.checkOrSetAlreadyCaught = checkOrSetAlreadyCaught; -exports.getEventDescription = getEventDescription; -exports.parseSemver = parseSemver; -exports.uuid4 = uuid4; - - -},{"./object.js":158,"./string.js":165,"./worldwide.js":174}],155:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); +const timestampWithMs = timestampInSeconds; /** - * Does this filename look like it's part of the app code? + * Internal helper to store what is the source of browserPerformanceTimeOrigin below. For debugging only. */ -function filenameIsInApp(filename, isNative = false) { - const isInternal = - isNative || - (filename && - // It's not internal if it's an absolute linux path - !filename.startsWith('/') && - // It's not internal if it's an absolute windows path - !filename.match(/^[A-Z]:/) && - // It's not internal if the path is starting with a dot - !filename.startsWith('.') && - // It's not internal if the frame has a protocol. In node, this is usually the case if the file got pre-processed with a bundler like webpack - !filename.match(/^[a-zA-Z]([a-zA-Z0-9.\-+])*:\/\//)); // Schema from: https://stackoverflow.com/a/3641782 - - // in_app is all that's not an internal Node function or a module within node_modules - // note that isNative appears to return true even for node core libraries - // see https://github.com/getsentry/raven-node/issues/176 - - return !isInternal && filename !== undefined && !filename.includes('node_modules/'); -} - -/** Node Stack line parser */ -// eslint-disable-next-line complexity -function node(getModule) { - const FILENAME_MATCH = /^\s*[-]{4,}$/; - const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/; - - // eslint-disable-next-line complexity - return (line) => { - const lineMatch = line.match(FULL_MATCH); - - if (lineMatch) { - let object; - let method; - let functionName; - let typeName; - let methodName; - - if (lineMatch[1]) { - functionName = lineMatch[1]; - - let methodStart = functionName.lastIndexOf('.'); - if (functionName[methodStart - 1] === '.') { - methodStart--; - } - - if (methodStart > 0) { - object = functionName.slice(0, methodStart); - method = functionName.slice(methodStart + 1); - const objectEnd = object.indexOf('.Module'); - if (objectEnd > 0) { - functionName = functionName.slice(objectEnd + 1); - object = object.slice(0, objectEnd); - } - } - typeName = undefined; - } - - if (method) { - typeName = object; - methodName = method; - } - - if (method === '') { - methodName = undefined; - functionName = undefined; - } +exports._browserPerformanceTimeOriginMode = void 0; - if (functionName === undefined) { - methodName = methodName || ''; - functionName = typeName ? `${typeName}.${methodName}` : methodName; - } +/** + * The number of milliseconds since the UNIX epoch. This value is only usable in a browser, and only when the + * performance API is available. + */ +const browserPerformanceTimeOrigin = (() => { + // Unfortunately browsers may report an inaccurate time origin data, through either performance.timeOrigin or + // performance.timing.navigationStart, which results in poor results in performance data. We only treat time origin + // data as reliable if they are within a reasonable threshold of the current time. - let filename = lineMatch[2] && lineMatch[2].startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2]; - const isNative = lineMatch[5] === 'native'; + const { performance } = worldwide.GLOBAL_OBJ ; + if (!performance || !performance.now) { + exports._browserPerformanceTimeOriginMode = 'none'; + return undefined; + } - // If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo` - if (filename && filename.match(/\/[A-Z]:/)) { - filename = filename.slice(1); - } + const threshold = 3600 * 1000; + const performanceNow = performance.now(); + const dateNow = Date.now(); - if (!filename && lineMatch[5] && !isNative) { - filename = lineMatch[5]; - } + // if timeOrigin isn't available set delta to threshold so it isn't used + const timeOriginDelta = performance.timeOrigin + ? Math.abs(performance.timeOrigin + performanceNow - dateNow) + : threshold; + const timeOriginIsReliable = timeOriginDelta < threshold; - return { - filename, - module: getModule ? getModule(filename) : undefined, - function: functionName, - lineno: parseInt(lineMatch[3], 10) || undefined, - colno: parseInt(lineMatch[4], 10) || undefined, - in_app: filenameIsInApp(filename, isNative), - }; - } + // While performance.timing.navigationStart is deprecated in favor of performance.timeOrigin, performance.timeOrigin + // is not as widely supported. Namely, performance.timeOrigin is undefined in Safari as of writing. + // Also as of writing, performance.timing is not available in Web Workers in mainstream browsers, so it is not always + // a valid fallback. In the absence of an initial time provided by the browser, fallback to the current time from the + // Date API. + // eslint-disable-next-line deprecation/deprecation + const navigationStart = performance.timing && performance.timing.navigationStart; + const hasNavigationStart = typeof navigationStart === 'number'; + // if navigationStart isn't available set delta to threshold so it isn't used + const navigationStartDelta = hasNavigationStart ? Math.abs(navigationStart + performanceNow - dateNow) : threshold; + const navigationStartIsReliable = navigationStartDelta < threshold; - if (line.match(FILENAME_MATCH)) { - return { - filename: line, - }; + if (timeOriginIsReliable || navigationStartIsReliable) { + // Use the more reliable time origin + if (timeOriginDelta <= navigationStartDelta) { + exports._browserPerformanceTimeOriginMode = 'timeOrigin'; + return performance.timeOrigin; + } else { + exports._browserPerformanceTimeOriginMode = 'navigationStart'; + return navigationStart; } + } - return undefined; - }; -} + // Either both timeOrigin and navigationStart are skewed or neither is available, fallback to Date. + exports._browserPerformanceTimeOriginMode = 'dateNow'; + return dateNow; +})(); -exports.filenameIsInApp = filenameIsInApp; -exports.node = node; +exports.browserPerformanceTimeOrigin = browserPerformanceTimeOrigin; +exports.dateTimestampInSeconds = dateTimestampInSeconds; +exports.timestampInSeconds = timestampInSeconds; +exports.timestampWithMs = timestampWithMs; -},{}],156:[function(require,module,exports){ -(function (process){(function (){ +},{"./worldwide.js":187}],182:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const env = require('./env.js'); - -/** - * NOTE: In order to avoid circular dependencies, if you add a function to this module and it needs to print something, - * you must either a) use `console.log` rather than the logger, or b) put your function elsewhere. - */ - -/** - * Checks whether we're in the Node.js or Browser environment - * - * @returns Answer to given question - */ -function isNodeEnv() { - // explicitly check for browser bundles as those can be optimized statically - // by terser/rollup. - return ( - !env.isBrowserBundle() && - Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]' - ); -} +const baggage = require('./baggage.js'); +const misc = require('./misc.js'); -/** - * Requires a module which is protected against bundler minification. - * - * @param request The module path to resolve - */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -function dynamicRequire(mod, request) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return mod.require(request); -} +// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp is used for readability here +const TRACEPARENT_REGEXP = new RegExp( + '^[ \\t]*' + // whitespace + '([0-9a-f]{32})?' + // trace_id + '-?([0-9a-f]{16})?' + // span_id + '-?([01])?' + // sampled + '[ \\t]*$', // whitespace +); /** - * Helper for dynamically loading module that should work with linked dependencies. - * The problem is that we _should_ be using `require(require.resolve(moduleName, { paths: [cwd()] }))` - * However it's _not possible_ to do that with Webpack, as it has to know all the dependencies during - * build time. `require.resolve` is also not available in any other way, so we cannot create, - * a fake helper like we do with `dynamicRequire`. + * Extract transaction context data from a `sentry-trace` header. * - * We always prefer to use local package, thus the value is not returned early from each `try/catch` block. - * That is to mimic the behavior of `require.resolve` exactly. + * @param traceparent Traceparent string * - * @param moduleName module name to require - * @returns possibly required module + * @returns Object containing data from the header, or undefined if traceparent string is malformed */ -function loadModule(moduleName) { - let mod; +function extractTraceparentData(traceparent) { + if (!traceparent) { + return undefined; + } - try { - mod = dynamicRequire(module, moduleName); - } catch (e) { - // no-empty + const matches = traceparent.match(TRACEPARENT_REGEXP); + if (!matches) { + return undefined; } - try { - const { cwd } = dynamicRequire(module, 'process'); - mod = dynamicRequire(module, `${cwd()}/node_modules/${moduleName}`) ; - } catch (e) { - // no-empty + let parentSampled; + if (matches[3] === '1') { + parentSampled = true; + } else if (matches[3] === '0') { + parentSampled = false; } - return mod; + return { + traceId: matches[1], + parentSampled, + parentSpanId: matches[2], + }; } -exports.dynamicRequire = dynamicRequire; -exports.isNodeEnv = isNodeEnv; -exports.loadModule = loadModule; - - -}).call(this)}).call(this,require('_process')) -},{"./env.js":135,"_process":175}],157:[function(require,module,exports){ -(function (global){(function (){ -Object.defineProperty(exports, '__esModule', { value: true }); - -const is = require('./is.js'); -const memo = require('./memo.js'); -const object = require('./object.js'); -const stacktrace = require('./stacktrace.js'); - /** - * Recursively normalizes the given object. - * - * - Creates a copy to prevent original input mutation - * - Skips non-enumerable properties - * - When stringifying, calls `toJSON` if implemented - * - Removes circular references - * - Translates non-serializable values (`undefined`/`NaN`/functions) to serializable format - * - Translates known global objects/classes to a string representations - * - Takes care of `Error` object serialization - * - Optionally limits depth of final output - * - Optionally limits number of properties/elements included in any single object/array + * Create tracing context from incoming headers. * - * @param input The object to be normalized. - * @param depth The max depth to which to normalize the object. (Anything deeper stringified whole.) - * @param maxProperties The max number of elements or properties to be included in any single array or - * object in the normallized output. - * @returns A normalized version of the object, or `"**non-serializable**"` if any errors are thrown during normalization. + * @deprecated Use `propagationContextFromHeaders` instead. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function normalize(input, depth = 100, maxProperties = +Infinity) { - try { - // since we're at the outermost level, we don't provide a key - return visit('', input, depth, maxProperties); - } catch (err) { - return { ERROR: `**non-serializable** (${err})` }; +// TODO(v8): Remove this function +function tracingContextFromHeaders( + sentryTrace, + baggage$1, +) + + { + const traceparentData = extractTraceparentData(sentryTrace); + const dynamicSamplingContext = baggage.baggageHeaderToDynamicSamplingContext(baggage$1); + + const { traceId, parentSpanId, parentSampled } = traceparentData || {}; + + if (!traceparentData) { + return { + traceparentData, + dynamicSamplingContext: undefined, + propagationContext: { + traceId: traceId || misc.uuid4(), + spanId: misc.uuid4().substring(16), + }, + }; + } else { + return { + traceparentData, + dynamicSamplingContext: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it + propagationContext: { + traceId: traceId || misc.uuid4(), + parentSpanId: parentSpanId || misc.uuid4().substring(16), + spanId: misc.uuid4().substring(16), + sampled: parentSampled, + dsc: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it + }, + }; } } -/** JSDoc */ -function normalizeToSize( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - object, - // Default Node.js REPL depth - depth = 3, - // 100kB, as 200kB is max payload size, so half sounds reasonable - maxSize = 100 * 1024, +/** + * Create a propagation context from incoming headers. + */ +function propagationContextFromHeaders( + sentryTrace, + baggage$1, ) { - const normalized = normalize(object, depth); + const traceparentData = extractTraceparentData(sentryTrace); + const dynamicSamplingContext = baggage.baggageHeaderToDynamicSamplingContext(baggage$1); - if (jsonSize(normalized) > maxSize) { - return normalizeToSize(object, depth - 1, maxSize); - } + const { traceId, parentSpanId, parentSampled } = traceparentData || {}; - return normalized ; + if (!traceparentData) { + return { + traceId: traceId || misc.uuid4(), + spanId: misc.uuid4().substring(16), + }; + } else { + return { + traceId: traceId || misc.uuid4(), + parentSpanId: parentSpanId || misc.uuid4().substring(16), + spanId: misc.uuid4().substring(16), + sampled: parentSampled, + dsc: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it + }; + } } /** - * Visits a node to perform normalization on it - * - * @param key The key corresponding to the given node - * @param value The node to be visited - * @param depth Optional number indicating the maximum recursion depth - * @param maxProperties Optional maximum number of properties/elements included in any single object/array - * @param memo Optional Memo class handling decycling + * Create sentry-trace header from span context values. */ -function visit( - key, - value, - depth = +Infinity, - maxProperties = +Infinity, - memo$1 = memo.memoBuilder(), +function generateSentryTraceHeader( + traceId = misc.uuid4(), + spanId = misc.uuid4().substring(16), + sampled, ) { - const [memoize, unmemoize] = memo$1; - - // Get the simple cases out of the way first - if ( - value == null || // this matches null and undefined -> eqeq not eqeqeq - (['number', 'boolean', 'string'].includes(typeof value) && !is.isNaN(value)) - ) { - return value ; + let sampledString = ''; + if (sampled !== undefined) { + sampledString = sampled ? '-1' : '-0'; } + return `${traceId}-${spanId}${sampledString}`; +} - const stringified = stringifyValue(key, value); +exports.TRACEPARENT_REGEXP = TRACEPARENT_REGEXP; +exports.extractTraceparentData = extractTraceparentData; +exports.generateSentryTraceHeader = generateSentryTraceHeader; +exports.propagationContextFromHeaders = propagationContextFromHeaders; +exports.tracingContextFromHeaders = tracingContextFromHeaders; - // Anything we could potentially dig into more (objects or arrays) will have come back as `"[object XXXX]"`. - // Everything else will have already been serialized, so if we don't see that pattern, we're done. - if (!stringified.startsWith('[object ')) { - return stringified; - } - // From here on, we can assert that `value` is either an object or an array. +},{"./baggage.js":135,"./misc.js":167}],183:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - // Do not normalize objects that we know have already been normalized. As a general rule, the - // "__sentry_skip_normalization__" property should only be used sparingly and only should only be set on objects that - // have already been normalized. - if ((value )['__sentry_skip_normalization__']) { - return value ; +/** + * Parses string form of URL into an object + * // borrowed from https://tools.ietf.org/html/rfc3986#appendix-B + * // intentionally using regex and not href parsing trick because React Native and other + * // environments where DOM might not be available + * @returns parsed URL object + */ +function parseUrl(url) { + if (!url) { + return {}; } - // We can set `__sentry_override_normalization_depth__` on an object to ensure that from there - // We keep a certain amount of depth. - // This should be used sparingly, e.g. we use it for the redux integration to ensure we get a certain amount of state. - const remainingDepth = - typeof (value )['__sentry_override_normalization_depth__'] === 'number' - ? ((value )['__sentry_override_normalization_depth__'] ) - : depth; + const match = url.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/); - // We're also done if we've reached the max depth - if (remainingDepth === 0) { - // At this point we know `serialized` is a string of the form `"[object XXXX]"`. Clean it up so it's just `"[XXXX]"`. - return stringified.replace('object ', ''); + if (!match) { + return {}; } - // If we've already visited this branch, bail out, as it's circular reference. If not, note that we're seeing it now. - if (memoize(value)) { - return '[Circular ~]'; - } + // coerce to undefined values to empty string so we don't get 'undefined' + const query = match[6] || ''; + const fragment = match[8] || ''; + return { + host: match[4], + path: match[5], + protocol: match[2], + search: query, + hash: fragment, + relative: match[5] + query + fragment, // everything minus origin + }; +} - // If the value has a `toJSON` method, we call it to extract more information - const valueWithToJSON = value ; - if (valueWithToJSON && typeof valueWithToJSON.toJSON === 'function') { - try { - const jsonValue = valueWithToJSON.toJSON(); - // We need to normalize the return value of `.toJSON()` in case it has circular references - return visit('', jsonValue, remainingDepth - 1, maxProperties, memo$1); - } catch (err) { - // pass (The built-in `toJSON` failed, but we can still try to do it ourselves) - } - } +/** + * Strip the query string and fragment off of a given URL or path (if present) + * + * @param urlPath Full URL or path, including possible query string and/or fragment + * @returns URL or path without query string or fragment + */ +function stripUrlQueryAndFragment(urlPath) { + // eslint-disable-next-line no-useless-escape + return urlPath.split(/[\?#]/, 1)[0]; +} - // At this point we know we either have an object or an array, we haven't seen it before, and we're going to recurse - // because we haven't yet reached the max depth. Create an accumulator to hold the results of visiting each - // property/entry, and keep track of the number of items we add to it. - const normalized = (Array.isArray(value) ? [] : {}) ; - let numAdded = 0; +/** + * Returns number of URL segments of a passed string URL. + */ +function getNumberOfUrlSegments(url) { + // split at '/' or at '\/' to split regex urls correctly + return url.split(/\\?\//).filter(s => s.length > 0 && s !== ',').length; +} - // Before we begin, convert`Error` and`Event` instances into plain objects, since some of each of their relevant - // properties are non-enumerable and otherwise would get missed. - const visitable = object.convertToPlainObject(value ); +/** + * Takes a URL object and returns a sanitized string which is safe to use as span description + * see: https://develop.sentry.dev/sdk/data-handling/#structuring-data + */ +function getSanitizedUrlString(url) { + const { protocol, host, path } = url; - for (const visitKey in visitable) { - // Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration. - if (!Object.prototype.hasOwnProperty.call(visitable, visitKey)) { - continue; - } + const filteredHost = + (host && + host + // Always filter out authority + .replace(/^.*@/, '[filtered]:[filtered]@') + // Don't show standard :80 (http) and :443 (https) ports to reduce the noise + // TODO: Use new URL global if it exists + .replace(/(:80)$/, '') + .replace(/(:443)$/, '')) || + ''; - if (numAdded >= maxProperties) { - normalized[visitKey] = '[MaxProperties ~]'; - break; - } + return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`; +} - // Recursively visit all the child nodes - const visitValue = visitable[visitKey]; - normalized[visitKey] = visit(visitKey, visitValue, remainingDepth - 1, maxProperties, memo$1); +exports.getNumberOfUrlSegments = getNumberOfUrlSegments; +exports.getSanitizedUrlString = getSanitizedUrlString; +exports.parseUrl = parseUrl; +exports.stripUrlQueryAndFragment = stripUrlQueryAndFragment; - numAdded++; - } - // Once we've visited all the branches, remove the parent from memo storage - unmemoize(value); +},{}],184:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); - // Return accumulated values - return normalized; +/** + * Recursively traverses an object to update an existing nested key. + * Note: The provided key path must include existing properties, + * the function will not create objects while traversing. + * + * @param obj An object to update + * @param value The value to update the nested key with + * @param keyPath The path to the key to update ex. fizz.buzz.foo + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setNestedKey(obj, keyPath, value) { + // Ex. foo.bar.zoop will extract foo and bar.zoop + const match = keyPath.match(/([a-z_]+)\.(.*)/i); + // The match will be null when there's no more recursing to do, i.e., when we've reached the right level of the object + if (match === null) { + obj[keyPath] = value; + } else { + // `match[1]` is the initial segment of the path, and `match[2]` is the remainder of the path + const innerObj = obj[match[1]]; + setNestedKey(innerObj, match[2], value); + } } -/* eslint-disable complexity */ /** - * Stringify the given value. Handles various known special values and types. + * Enforces inclusion of a given integration with specified options in an integration array originally determined by the + * user, by either including the given default instance or by patching an existing user instance with the given options. * - * Not meant to be used on simple primitives which already have a string representation, as it will, for example, turn - * the number 1231 into "[Object Number]", nor on `null`, as it will throw. + * Ideally this would happen when integrations are set up, but there isn't currently a mechanism there for merging + * options from a default integration instance with those from a user-provided instance of the same integration, only + * for allowing the user to override a default instance entirely. (TODO: Fix that.) * - * @param value The value to stringify - * @returns A stringified representation of the given value + * @param defaultIntegrationInstance An instance of the integration with the correct options already set + * @param userIntegrations Integrations defined by the user. + * @param forcedOptions Options with which to patch an existing user-derived instance on the integration. + * @returns A final integrations array. + * + * @deprecated This will be removed in v8. */ -function stringifyValue( - key, - // this type is a tiny bit of a cheat, since this function does handle NaN (which is technically a number), but for - // our internal use, it'll do - value, +function addOrUpdateIntegration( + defaultIntegrationInstance, + userIntegrations, + forcedOptions = {}, ) { - try { - if (key === 'domain' && value && typeof value === 'object' && (value )._events) { - return '[Domain]'; - } - - if (key === 'domainEmitter') { - return '[DomainEmitter]'; - } - - // It's safe to use `global`, `window`, and `document` here in this manner, as we are asserting using `typeof` first - // which won't throw if they are not present. - - if (typeof global !== 'undefined' && value === global) { - return '[Global]'; - } - - // eslint-disable-next-line no-restricted-globals - if (typeof window !== 'undefined' && value === window) { - return '[Window]'; - } - - // eslint-disable-next-line no-restricted-globals - if (typeof document !== 'undefined' && value === document) { - return '[Document]'; - } + return ( + Array.isArray(userIntegrations) + ? addOrUpdateIntegrationInArray(defaultIntegrationInstance, userIntegrations, forcedOptions) + : addOrUpdateIntegrationInFunction( + defaultIntegrationInstance, + // Somehow TS can't figure out that not being an array makes this necessarily a function + userIntegrations , + forcedOptions, + ) + ) ; +} - if (is.isVueViewModel(value)) { - return '[VueViewModel]'; - } +function addOrUpdateIntegrationInArray( + defaultIntegrationInstance, + userIntegrations, + forcedOptions, +) { + const userInstance = userIntegrations.find(integration => integration.name === defaultIntegrationInstance.name); - // React's SyntheticEvent thingy - if (is.isSyntheticEvent(value)) { - return '[SyntheticEvent]'; + if (userInstance) { + for (const [keyPath, value] of Object.entries(forcedOptions)) { + setNestedKey(userInstance, keyPath, value); } - if (typeof value === 'number' && value !== value) { - return '[NaN]'; - } + return userIntegrations; + } - if (typeof value === 'function') { - return `[Function: ${stacktrace.getFunctionName(value)}]`; - } + return [...userIntegrations, defaultIntegrationInstance]; +} - if (typeof value === 'symbol') { - return `[${String(value)}]`; - } +function addOrUpdateIntegrationInFunction( + defaultIntegrationInstance, + userIntegrationsFunc, + forcedOptions, +) { + const wrapper = defaultIntegrations => { + const userFinalIntegrations = userIntegrationsFunc(defaultIntegrations); - // stringified BigInts are indistinguishable from regular numbers, so we need to label them to avoid confusion - if (typeof value === 'bigint') { - return `[BigInt: ${String(value)}]`; + // There are instances where we want the user to be able to prevent an integration from appearing at all, which they + // would do by providing a function which filters out the integration in question. If that's happened in one of + // those cases, don't add our default back in. + if (defaultIntegrationInstance.allowExclusionByUser) { + const userFinalInstance = userFinalIntegrations.find( + integration => integration.name === defaultIntegrationInstance.name, + ); + if (!userFinalInstance) { + return userFinalIntegrations; + } } - // Now that we've knocked out all the special cases and the primitives, all we have left are objects. Simply casting - // them to strings means that instances of classes which haven't defined their `toStringTag` will just come out as - // `"[object Object]"`. If we instead look at the constructor's name (which is the same as the name of the class), - // we can make sure that only plain objects come out that way. - const objName = getConstructorName(value); - - // Handle HTML Elements - if (/^HTML(\w*)Element$/.test(objName)) { - return `[HTMLElement: ${objName}]`; - } + return addOrUpdateIntegrationInArray(defaultIntegrationInstance, userFinalIntegrations, forcedOptions); + }; - return `[object ${objName}]`; - } catch (err) { - return `**non-serializable** (${err})`; - } + return wrapper; } -/* eslint-enable complexity */ -function getConstructorName(value) { - const prototype = Object.getPrototypeOf(value); +exports.addOrUpdateIntegration = addOrUpdateIntegration; - return prototype ? prototype.constructor.name : 'null prototype'; -} -/** Calculates bytes size of input string */ -function utf8Length(value) { - // eslint-disable-next-line no-bitwise - return ~-encodeURI(value).split(/%..|./).length; -} +},{}],185:[function(require,module,exports){ +Object.defineProperty(exports, '__esModule', { value: true }); -/** Calculates bytes size of input object */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function jsonSize(value) { - return utf8Length(JSON.stringify(value)); -} +// Based on https://github.com/sindresorhus/escape-string-regexp but with modifications to: +// a) reduce the size by skipping the runtime type - checking +// b) ensure it gets down - compiled for old versions of Node(the published package only supports Node 12+). +// +// MIT License +// +// Copyright (c) Sindre Sorhus (https://sindresorhus.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files(the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of +// the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. /** - * Normalizes URLs in exceptions and stacktraces to a base path so Sentry can fingerprint - * across platforms and working directory. + * Given a string, escape characters which have meaning in the regex grammar, such that the result is safe to feed to + * `new RegExp()`. * - * @param url The URL to be normalized. - * @param basePath The application base path. - * @returns The normalized URL. + * @param regexString The string to escape + * @returns An version of the string with all special regex characters escaped */ -function normalizeUrlToBase(url, basePath) { - const escapedBase = basePath - // Backslash to forward - .replace(/\\/g, '/') - // Escape RegExp special characters - .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); - - let newUrl = url; - try { - newUrl = decodeURI(url); - } catch (_Oo) { - // Sometime this breaks - } - return ( - newUrl - .replace(/\\/g, '/') - .replace(/webpack:\/?/g, '') // Remove intermediate base path - // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor - .replace(new RegExp(`(file://)?/*${escapedBase}/*`, 'ig'), 'app:///') - ); +function escapeStringForRegex(regexString) { + // escape the hyphen separately so we can also replace it with a unicode literal hyphen, to avoid the problems + // discussed in https://github.com/sindresorhus/escape-string-regexp/issues/20. + return regexString.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d'); } -exports.normalize = normalize; -exports.normalizeToSize = normalizeToSize; -exports.normalizeUrlToBase = normalizeUrlToBase; -exports.walk = visit; +exports.escapeStringForRegex = escapeStringForRegex; -}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./is.js":149,"./memo.js":153,"./object.js":158,"./stacktrace.js":164}],158:[function(require,module,exports){ +},{}],186:[function(require,module,exports){ Object.defineProperty(exports, '__esModule', { value: true }); -const browser = require('./browser.js'); -const debugBuild = require('./debug-build.js'); -const is = require('./is.js'); -const logger = require('./logger.js'); -const string = require('./string.js'); +const worldwide = require('../worldwide.js'); + +// Based on https://github.com/angular/angular.js/pull/13945/files + +// eslint-disable-next-line deprecation/deprecation +const WINDOW = worldwide.getGlobalObject(); /** - * Replace a method in an object with a wrapped version of itself. + * Tells whether current environment supports History API + * {@link supportsHistory}. * - * @param source An object that contains a method to be wrapped. - * @param name The name of the method to be wrapped. - * @param replacementFactory A higher-order function that takes the original version of the given method and returns a - * wrapped version. Note: The function returned by `replacementFactory` needs to be a non-arrow function, in order to - * preserve the correct value of `this`, and the original method must be called using `origMethod.call(this, )` or `origMethod.apply(this, [])` (rather than being called directly), again to preserve `this`. - * @returns void + * @returns Answer to the given question. */ -function fill(source, name, replacementFactory) { - if (!(name in source)) { - return; - } +function supportsHistory() { + // NOTE: in Chrome App environment, touching history.pushState, *even inside + // a try/catch block*, will cause Chrome to output an error to console.error + // borrowed from: https://github.com/angular/angular.js/pull/13945/files + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const chromeVar = (WINDOW ).chrome; + const isChromePackagedApp = chromeVar && chromeVar.app && chromeVar.app.runtime; + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ + const hasHistoryApi = 'history' in WINDOW && !!WINDOW.history.pushState && !!WINDOW.history.replaceState; - const original = source[name] ; - const wrapped = replacementFactory(original) ; + return !isChromePackagedApp && hasHistoryApi; +} - // Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work - // otherwise it'll throw "TypeError: Object.defineProperties called on non-object" - if (typeof wrapped === 'function') { - markFunctionWrapped(wrapped, original); - } +exports.supportsHistory = supportsHistory; - source[name] = wrapped; -} -/** - * Defines a non-enumerable property on the given object. - * - * @param obj The object on which to set the property - * @param name The name of the property to be set - * @param value The value to which to set the property - */ -function addNonEnumerableProperty(obj, name, value) { - try { - Object.defineProperty(obj, name, { - // enumerable: false, // the default, so we can save on bundle size by not explicitly setting it - value: value, - writable: true, - configurable: true, - }); - } catch (o_O) { - debugBuild.DEBUG_BUILD && logger.logger.log(`Failed to add non-enumerable property "${name}" to object`, obj); - } -} +},{"../worldwide.js":187}],187:[function(require,module,exports){ +(function (global){(function (){ +Object.defineProperty(exports, '__esModule', { value: true }); -/** - * Remembers the original function on the wrapped function and - * patches up the prototype. - * - * @param wrapped the wrapper function - * @param original the original function that gets wrapped - */ -function markFunctionWrapped(wrapped, original) { - try { - const proto = original.prototype || {}; - wrapped.prototype = original.prototype = proto; - addNonEnumerableProperty(wrapped, '__sentry_original__', original); - } catch (o_O) {} // eslint-disable-line no-empty +/** Internal global with common properties and Sentry extensions */ + +// The code below for 'isGlobalObj' and 'GLOBAL_OBJ' was copied from core-js before modification +// https://github.com/zloirock/core-js/blob/1b944df55282cdc99c90db5f49eb0b6eda2cc0a3/packages/core-js/internals/global.js +// core-js has the following licence: +// +// Copyright (c) 2014-2022 Denis Pushkarev +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/** Returns 'obj' if it's the global object, otherwise returns undefined */ +function isGlobalObj(obj) { + return obj && obj.Math == Math ? obj : undefined; } +/** Get's the global object for the current JavaScript runtime */ +const GLOBAL_OBJ = + (typeof globalThis == 'object' && isGlobalObj(globalThis)) || + // eslint-disable-next-line no-restricted-globals + (typeof window == 'object' && isGlobalObj(window)) || + (typeof self == 'object' && isGlobalObj(self)) || + (typeof global == 'object' && isGlobalObj(global)) || + (function () { + return this; + })() || + {}; + /** - * This extracts the original function if available. See - * `markFunctionWrapped` for more information. - * - * @param func the function to unwrap - * @returns the unwrapped version of the function if available. + * @deprecated Use GLOBAL_OBJ instead or WINDOW from @sentry/browser. This will be removed in v8 */ -function getOriginalFunction(func) { - return func.__sentry_original__; +function getGlobalObject() { + return GLOBAL_OBJ ; } /** - * Encodes given object into url-friendly format + * Returns a global singleton contained in the global `__SENTRY__` object. * - * @param object An object that contains serializable values - * @returns string Encoded + * If the singleton doesn't already exist in `__SENTRY__`, it will be created using the given factory + * function and added to the `__SENTRY__` object. + * + * @param name name of the global singleton on __SENTRY__ + * @param creator creator Factory function to create the singleton if it doesn't already exist on `__SENTRY__` + * @param obj (Optional) The global object on which to look for `__SENTRY__`, if not `GLOBAL_OBJ`'s return value + * @returns the singleton */ -function urlEncode(object) { - return Object.keys(object) - .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(object[key])}`) - .join('&'); +function getGlobalSingleton(name, creator, obj) { + const gbl = (obj || GLOBAL_OBJ) ; + const __SENTRY__ = (gbl.__SENTRY__ = gbl.__SENTRY__ || {}); + const singleton = __SENTRY__[name] || (__SENTRY__[name] = creator()); + return singleton; } -/** - * Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their - * non-enumerable properties attached. - * - * @param value Initial source that we have to transform in order for it to be usable by the serializer - * @returns An Event or Error turned into an object - or the value argurment itself, when value is neither an Event nor - * an Error. - */ -function convertToPlainObject( - value, -) +exports.GLOBAL_OBJ = GLOBAL_OBJ; +exports.getGlobalObject = getGlobalObject; +exports.getGlobalSingleton = getGlobalSingleton; - { - if (is.isError(value)) { - return { - message: value.message, - name: value.name, - stack: value.stack, - ...getOwnProperties(value), - }; - } else if (is.isEvent(value)) { - const newObj - = { - type: value.type, - target: serializeEventTarget(value.target), - currentTarget: serializeEventTarget(value.currentTarget), - ...getOwnProperties(value), +}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],188:[function(require,module,exports){ +(function (global){(function (){ +/*! + localForage -- Offline Storage, Improved + Version 1.10.0 + https://localforage.github.io/localForage + (c) 2013-2017 Mozilla, Apache License 2.0 +*/ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.localforage = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw (f.code="MODULE_NOT_FOUND", f)}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o element; its readystatechange event will be fired asynchronously once it is inserted + // into the document. Do so, thus queuing up the task. Remember to clean up once it's been called. + var scriptEl = global.document.createElement('script'); + scriptEl.onreadystatechange = function () { + nextTick(); + + scriptEl.onreadystatechange = null; + scriptEl.parentNode.removeChild(scriptEl); + scriptEl = null; + }; + global.document.documentElement.appendChild(scriptEl); + }; + } else { + scheduleDrain = function () { + setTimeout(nextTick, 0); }; + } +} - if (typeof CustomEvent !== 'undefined' && is.isInstanceOf(value, CustomEvent)) { - newObj.detail = value.detail; +var draining; +var queue = []; +//named nextTick for less confusing stack traces +function nextTick() { + draining = true; + var i, oldQueue; + var len = queue.length; + while (len) { + oldQueue = queue; + queue = []; + i = -1; + while (++i < len) { + oldQueue[i](); } + len = queue.length; + } + draining = false; +} - return newObj; - } else { - return value; +module.exports = immediate; +function immediate(task) { + if (queue.push(task) === 1 && !draining) { + scheduleDrain(); } } -/** Creates a string representation of the target of an `Event` object */ -function serializeEventTarget(target) { - try { - return is.isElement(target) ? browser.htmlTreeAsString(target) : Object.prototype.toString.call(target); - } catch (_oO) { - return ''; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],2:[function(_dereq_,module,exports){ +'use strict'; +var immediate = _dereq_(1); + +/* istanbul ignore next */ +function INTERNAL() {} + +var handlers = {}; + +var REJECTED = ['REJECTED']; +var FULFILLED = ['FULFILLED']; +var PENDING = ['PENDING']; + +module.exports = Promise; + +function Promise(resolver) { + if (typeof resolver !== 'function') { + throw new TypeError('resolver must be a function'); + } + this.state = PENDING; + this.queue = []; + this.outcome = void 0; + if (resolver !== INTERNAL) { + safelyResolveThenable(this, resolver); } } -/** Filters out all but an object's own properties */ -function getOwnProperties(obj) { - if (typeof obj === 'object' && obj !== null) { - const extractedProps = {}; - for (const property in obj) { - if (Object.prototype.hasOwnProperty.call(obj, property)) { - extractedProps[property] = (obj )[property]; - } - } - return extractedProps; +Promise.prototype["catch"] = function (onRejected) { + return this.then(null, onRejected); +}; +Promise.prototype.then = function (onFulfilled, onRejected) { + if (typeof onFulfilled !== 'function' && this.state === FULFILLED || + typeof onRejected !== 'function' && this.state === REJECTED) { + return this; + } + var promise = new this.constructor(INTERNAL); + if (this.state !== PENDING) { + var resolver = this.state === FULFILLED ? onFulfilled : onRejected; + unwrap(promise, resolver, this.outcome); } else { - return {}; + this.queue.push(new QueueItem(promise, onFulfilled, onRejected)); + } + + return promise; +}; +function QueueItem(promise, onFulfilled, onRejected) { + this.promise = promise; + if (typeof onFulfilled === 'function') { + this.onFulfilled = onFulfilled; + this.callFulfilled = this.otherCallFulfilled; + } + if (typeof onRejected === 'function') { + this.onRejected = onRejected; + this.callRejected = this.otherCallRejected; } } +QueueItem.prototype.callFulfilled = function (value) { + handlers.resolve(this.promise, value); +}; +QueueItem.prototype.otherCallFulfilled = function (value) { + unwrap(this.promise, this.onFulfilled, value); +}; +QueueItem.prototype.callRejected = function (value) { + handlers.reject(this.promise, value); +}; +QueueItem.prototype.otherCallRejected = function (value) { + unwrap(this.promise, this.onRejected, value); +}; -/** - * Given any captured exception, extract its keys and create a sorted - * and truncated list that will be used inside the event message. - * eg. `Non-error exception captured with keys: foo, bar, baz` - */ -function extractExceptionKeysForMessage(exception, maxLength = 40) { - const keys = Object.keys(convertToPlainObject(exception)); - keys.sort(); +function unwrap(promise, func, value) { + immediate(function () { + var returnValue; + try { + returnValue = func(value); + } catch (e) { + return handlers.reject(promise, e); + } + if (returnValue === promise) { + handlers.reject(promise, new TypeError('Cannot resolve promise with itself')); + } else { + handlers.resolve(promise, returnValue); + } + }); +} - if (!keys.length) { - return '[object has no keys]'; +handlers.resolve = function (self, value) { + var result = tryCatch(getThen, value); + if (result.status === 'error') { + return handlers.reject(self, result.value); } + var thenable = result.value; - if (keys[0].length >= maxLength) { - return string.truncate(keys[0], maxLength); + if (thenable) { + safelyResolveThenable(self, thenable); + } else { + self.state = FULFILLED; + self.outcome = value; + var i = -1; + var len = self.queue.length; + while (++i < len) { + self.queue[i].callFulfilled(value); + } } + return self; +}; +handlers.reject = function (self, error) { + self.state = REJECTED; + self.outcome = error; + var i = -1; + var len = self.queue.length; + while (++i < len) { + self.queue[i].callRejected(error); + } + return self; +}; - for (let includedKeys = keys.length; includedKeys > 0; includedKeys--) { - const serialized = keys.slice(0, includedKeys).join(', '); - if (serialized.length > maxLength) { - continue; +function getThen(obj) { + // Make sure we only access the accessor once as required by the spec + var then = obj && obj.then; + if (obj && (typeof obj === 'object' || typeof obj === 'function') && typeof then === 'function') { + return function appyThen() { + then.apply(obj, arguments); + }; + } +} + +function safelyResolveThenable(self, thenable) { + // Either fulfill, reject or reject with error + var called = false; + function onError(value) { + if (called) { + return; } - if (includedKeys === keys.length) { - return serialized; + called = true; + handlers.reject(self, value); + } + + function onSuccess(value) { + if (called) { + return; } - return string.truncate(serialized, maxLength); + called = true; + handlers.resolve(self, value); } - return ''; + function tryToUnwrap() { + thenable(onSuccess, onError); + } + + var result = tryCatch(tryToUnwrap); + if (result.status === 'error') { + onError(result.value); + } } -/** - * Given any object, return a new object having removed all fields whose value was `undefined`. - * Works recursively on objects and arrays. - * - * Attention: This function keeps circular references in the returned object. - */ -function dropUndefinedKeys(inputValue) { - // This map keeps track of what already visited nodes map to. - // Our Set - based memoBuilder doesn't work here because we want to the output object to have the same circular - // references as the input object. - const memoizationMap = new Map(); +function tryCatch(func, value) { + var out = {}; + try { + out.value = func(value); + out.status = 'success'; + } catch (e) { + out.status = 'error'; + out.value = e; + } + return out; +} - // This function just proxies `_dropUndefinedKeys` to keep the `memoBuilder` out of this function's API - return _dropUndefinedKeys(inputValue, memoizationMap); +Promise.resolve = resolve; +function resolve(value) { + if (value instanceof this) { + return value; + } + return handlers.resolve(new this(INTERNAL), value); } -function _dropUndefinedKeys(inputValue, memoizationMap) { - if (isPojo(inputValue)) { - // If this node has already been visited due to a circular reference, return the object it was mapped to in the new object - const memoVal = memoizationMap.get(inputValue); - if (memoVal !== undefined) { - return memoVal ; - } +Promise.reject = reject; +function reject(reason) { + var promise = new this(INTERNAL); + return handlers.reject(promise, reason); +} - const returnValue = {}; - // Store the mapping of this value in case we visit it again, in case of circular data - memoizationMap.set(inputValue, returnValue); +Promise.all = all; +function all(iterable) { + var self = this; + if (Object.prototype.toString.call(iterable) !== '[object Array]') { + return this.reject(new TypeError('must be an array')); + } - for (const key of Object.keys(inputValue)) { - if (typeof inputValue[key] !== 'undefined') { - returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap); + var len = iterable.length; + var called = false; + if (!len) { + return this.resolve([]); + } + + var values = new Array(len); + var resolved = 0; + var i = -1; + var promise = new this(INTERNAL); + + while (++i < len) { + allResolver(iterable[i], i); + } + return promise; + function allResolver(value, i) { + self.resolve(value).then(resolveFromAll, function (error) { + if (!called) { + called = true; + handlers.reject(promise, error); + } + }); + function resolveFromAll(outValue) { + values[i] = outValue; + if (++resolved === len && !called) { + called = true; + handlers.resolve(promise, values); } } + } +} - return returnValue ; +Promise.race = race; +function race(iterable) { + var self = this; + if (Object.prototype.toString.call(iterable) !== '[object Array]') { + return this.reject(new TypeError('must be an array')); } - if (Array.isArray(inputValue)) { - // If this node has already been visited due to a circular reference, return the array it was mapped to in the new object - const memoVal = memoizationMap.get(inputValue); - if (memoVal !== undefined) { - return memoVal ; - } + var len = iterable.length; + var called = false; + if (!len) { + return this.resolve([]); + } - const returnValue = []; - // Store the mapping of this value in case we visit it again, in case of circular data - memoizationMap.set(inputValue, returnValue); + var i = -1; + var promise = new this(INTERNAL); - inputValue.forEach((item) => { - returnValue.push(_dropUndefinedKeys(item, memoizationMap)); + while (++i < len) { + resolver(iterable[i]); + } + return promise; + function resolver(value) { + self.resolve(value).then(function (response) { + if (!called) { + called = true; + handlers.resolve(promise, response); + } + }, function (error) { + if (!called) { + called = true; + handlers.reject(promise, error); + } }); - - return returnValue ; } - - return inputValue; } -function isPojo(input) { - if (!is.isPlainObject(input)) { - return false; - } - - try { - const name = (Object.getPrototypeOf(input) ).constructor.name; - return !name || name === 'Object'; - } catch (e) { - return true; - } +},{"1":1}],3:[function(_dereq_,module,exports){ +(function (global){ +'use strict'; +if (typeof global.Promise !== 'function') { + global.Promise = _dereq_(2); } -/** - * Ensure that something is an object. - * - * Turns `undefined` and `null` into `String`s and all other primitives into instances of their respective wrapper - * classes (String, Boolean, Number, etc.). Acts as the identity function on non-primitives. - * - * @param wat The subject of the objectification - * @returns A version of `wat` which can safely be used with `Object` class methods - */ -function objectify(wat) { - let objectified; - switch (true) { - case wat === undefined || wat === null: - objectified = new String(wat); - break; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"2":2}],4:[function(_dereq_,module,exports){ +'use strict'; - // Though symbols and bigints do have wrapper classes (`Symbol` and `BigInt`, respectively), for whatever reason - // those classes don't have constructors which can be used with the `new` keyword. We therefore need to cast each as - // an object in order to wrap it. - case typeof wat === 'symbol' || typeof wat === 'bigint': - objectified = Object(wat); - break; +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - // this will catch the remaining primitives: `String`, `Number`, and `Boolean` - case is.isPrimitive(wat): - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - objectified = new (wat ).constructor(wat); - break; +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - // by process of elimination, at this point we know that `wat` must already be an object - default: - objectified = wat; - break; - } - return objectified; +function getIDB() { + /* global indexedDB,webkitIndexedDB,mozIndexedDB,OIndexedDB,msIndexedDB */ + try { + if (typeof indexedDB !== 'undefined') { + return indexedDB; + } + if (typeof webkitIndexedDB !== 'undefined') { + return webkitIndexedDB; + } + if (typeof mozIndexedDB !== 'undefined') { + return mozIndexedDB; + } + if (typeof OIndexedDB !== 'undefined') { + return OIndexedDB; + } + if (typeof msIndexedDB !== 'undefined') { + return msIndexedDB; + } + } catch (e) { + return; + } } -exports.addNonEnumerableProperty = addNonEnumerableProperty; -exports.convertToPlainObject = convertToPlainObject; -exports.dropUndefinedKeys = dropUndefinedKeys; -exports.extractExceptionKeysForMessage = extractExceptionKeysForMessage; -exports.fill = fill; -exports.getOriginalFunction = getOriginalFunction; -exports.markFunctionWrapped = markFunctionWrapped; -exports.objectify = objectify; -exports.urlEncode = urlEncode; +var idb = getIDB(); +function isIndexedDBValid() { + try { + // Initialize IndexedDB; fall back to vendor-prefixed versions + // if needed. + if (!idb || !idb.open) { + return false; + } + // We mimic PouchDB here; + // + // We test for openDatabase because IE Mobile identifies itself + // as Safari. Oh the lulz... + var isSafari = typeof openDatabase !== 'undefined' && /(Safari|iPhone|iPad|iPod)/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent) && !/BlackBerry/.test(navigator.platform); + + var hasFetch = typeof fetch === 'function' && fetch.toString().indexOf('[native code') !== -1; + + // Safari <10.1 does not meet our requirements for IDB support + // (see: https://github.com/pouchdb/pouchdb/issues/5572). + // Safari 10.1 shipped with fetch, we can use that to detect it. + // Note: this creates issues with `window.fetch` polyfills and + // overrides; see: + // https://github.com/localForage/localForage/issues/856 + return (!isSafari || hasFetch) && typeof indexedDB !== 'undefined' && + // some outdated implementations of IDB that appear on Samsung + // and HTC Android devices <4.4 are missing IDBKeyRange + // See: https://github.com/mozilla/localForage/issues/128 + // See: https://github.com/mozilla/localForage/issues/272 + typeof IDBKeyRange !== 'undefined'; + } catch (e) { + return false; + } +} -},{"./browser.js":123,"./debug-build.js":133,"./is.js":149,"./logger.js":151,"./string.js":165}],159:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); +// Abstracts constructing a Blob object, so it also works in older +// browsers that don't support the native Blob constructor. (i.e. +// old QtWebKit versions, at least). +// Abstracts constructing a Blob object, so it also works in older +// browsers that don't support the native Blob constructor. (i.e. +// old QtWebKit versions, at least). +function createBlob(parts, properties) { + /* global BlobBuilder,MSBlobBuilder,MozBlobBuilder,WebKitBlobBuilder */ + parts = parts || []; + properties = properties || {}; + try { + return new Blob(parts, properties); + } catch (e) { + if (e.name !== 'TypeError') { + throw e; + } + var Builder = typeof BlobBuilder !== 'undefined' ? BlobBuilder : typeof MSBlobBuilder !== 'undefined' ? MSBlobBuilder : typeof MozBlobBuilder !== 'undefined' ? MozBlobBuilder : WebKitBlobBuilder; + var builder = new Builder(); + for (var i = 0; i < parts.length; i += 1) { + builder.append(parts[i]); + } + return builder.getBlob(properties.type); + } +} -// Slightly modified (no IE8 support, ES6) and transcribed to TypeScript -// https://github.com/calvinmetcalf/rollup-plugin-node-builtins/blob/63ab8aacd013767445ca299e468d9a60a95328d7/src/es6/path.js -// -// Copyright Joyent, Inc.and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. +// This is CommonJS because lie is an external dependency, so Rollup +// can just ignore it. +if (typeof Promise === 'undefined') { + // In the "nopromises" build this will just throw if you don't have + // a global promise object, but it would throw anyway later. + _dereq_(3); +} +var Promise$1 = Promise; -/** JSDoc */ -function normalizeArray(parts, allowAboveRoot) { - // if the path tries to go above the root, `up` ends up > 0 - let up = 0; - for (let i = parts.length - 1; i >= 0; i--) { - const last = parts[i]; - if (last === '.') { - parts.splice(i, 1); - } else if (last === '..') { - parts.splice(i, 1); - up++; - } else if (up) { - parts.splice(i, 1); - up--; +function executeCallback(promise, callback) { + if (callback) { + promise.then(function (result) { + callback(null, result); + }, function (error) { + callback(error); + }); } - } +} - // if the path is allowed to go above the root, restore leading ..s - if (allowAboveRoot) { - for (; up--; up) { - parts.unshift('..'); +function executeTwoCallbacks(promise, callback, errorCallback) { + if (typeof callback === 'function') { + promise.then(callback); } - } - return parts; + if (typeof errorCallback === 'function') { + promise["catch"](errorCallback); + } } -// Split a filename into [root, dir, basename, ext], unix version -// 'root' is just a slash, or nothing. -const splitPathRe = /^(\S+:\\|\/?)([\s\S]*?)((?:\.{1,2}|[^/\\]+?|)(\.[^./\\]*|))(?:[/\\]*)$/; -/** JSDoc */ -function splitPath(filename) { - // Truncate files names greater than 1024 characters to avoid regex dos - // https://github.com/getsentry/sentry-javascript/pull/8737#discussion_r1285719172 - const truncated = filename.length > 1024 ? `${filename.slice(-1024)}` : filename; - const parts = splitPathRe.exec(truncated); - return parts ? parts.slice(1) : []; +function normalizeKey(key) { + // Cast the key to a string, as that's all we can set as a key. + if (typeof key !== 'string') { + console.warn(key + ' used as a key, but it is not a string.'); + key = String(key); + } + + return key; } -// path.resolve([from ...], to) -// posix version -/** JSDoc */ -function resolve(...args) { - let resolvedPath = ''; - let resolvedAbsolute = false; +function getCallback() { + if (arguments.length && typeof arguments[arguments.length - 1] === 'function') { + return arguments[arguments.length - 1]; + } +} - for (let i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { - const path = i >= 0 ? args[i] : '/'; +// Some code originally from async_storage.js in +// [Gaia](https://github.com/mozilla-b2g/gaia). - // Skip empty entries - if (!path) { - continue; - } +var DETECT_BLOB_SUPPORT_STORE = 'local-forage-detect-blob-support'; +var supportsBlobs = void 0; +var dbContexts = {}; +var toString = Object.prototype.toString; - resolvedPath = `${path}/${resolvedPath}`; - resolvedAbsolute = path.charAt(0) === '/'; - } +// Transaction Modes +var READ_ONLY = 'readonly'; +var READ_WRITE = 'readwrite'; - // At this point the path should be resolved to a full absolute path, but - // handle relative paths to be safe (might happen when process.cwd() fails) +// Transform a binary string to an array buffer, because otherwise +// weird stuff happens when you try to work with the binary string directly. +// It is known. +// From http://stackoverflow.com/questions/14967647/ (continues on next line) +// encode-decode-image-with-base64-breaks-image (2013-04-21) +function _binStringToArrayBuffer(bin) { + var length = bin.length; + var buf = new ArrayBuffer(length); + var arr = new Uint8Array(buf); + for (var i = 0; i < length; i++) { + arr[i] = bin.charCodeAt(i); + } + return buf; +} - // Normalize the path - resolvedPath = normalizeArray( - resolvedPath.split('/').filter(p => !!p), - !resolvedAbsolute, - ).join('/'); +// +// Blobs are not supported in all versions of IndexedDB, notably +// Chrome <37 and Android <5. In those versions, storing a blob will throw. +// +// Various other blob bugs exist in Chrome v37-42 (inclusive). +// Detecting them is expensive and confusing to users, and Chrome 37-42 +// is at very low usage worldwide, so we do a hacky userAgent check instead. +// +// content-type bug: https://code.google.com/p/chromium/issues/detail?id=408120 +// 404 bug: https://code.google.com/p/chromium/issues/detail?id=447916 +// FileReader bug: https://code.google.com/p/chromium/issues/detail?id=447836 +// +// Code borrowed from PouchDB. See: +// https://github.com/pouchdb/pouchdb/blob/master/packages/node_modules/pouchdb-adapter-idb/src/blobSupport.js +// +function _checkBlobSupportWithoutCaching(idb) { + return new Promise$1(function (resolve) { + var txn = idb.transaction(DETECT_BLOB_SUPPORT_STORE, READ_WRITE); + var blob = createBlob(['']); + txn.objectStore(DETECT_BLOB_SUPPORT_STORE).put(blob, 'key'); + + txn.onabort = function (e) { + // If the transaction aborts now its due to not being able to + // write to the database, likely due to the disk being full + e.preventDefault(); + e.stopPropagation(); + resolve(false); + }; - return (resolvedAbsolute ? '/' : '') + resolvedPath || '.'; + txn.oncomplete = function () { + var matchedChrome = navigator.userAgent.match(/Chrome\/(\d+)/); + var matchedEdge = navigator.userAgent.match(/Edge\//); + // MS Edge pretends to be Chrome 42: + // https://msdn.microsoft.com/en-us/library/hh869301%28v=vs.85%29.aspx + resolve(matchedEdge || !matchedChrome || parseInt(matchedChrome[1], 10) >= 43); + }; + })["catch"](function () { + return false; // error, so assume unsupported + }); } -/** JSDoc */ -function trim(arr) { - let start = 0; - for (; start < arr.length; start++) { - if (arr[start] !== '') { - break; +function _checkBlobSupport(idb) { + if (typeof supportsBlobs === 'boolean') { + return Promise$1.resolve(supportsBlobs); } - } + return _checkBlobSupportWithoutCaching(idb).then(function (value) { + supportsBlobs = value; + return supportsBlobs; + }); +} - let end = arr.length - 1; - for (; end >= 0; end--) { - if (arr[end] !== '') { - break; +function _deferReadiness(dbInfo) { + var dbContext = dbContexts[dbInfo.name]; + + // Create a deferred object representing the current database operation. + var deferredOperation = {}; + + deferredOperation.promise = new Promise$1(function (resolve, reject) { + deferredOperation.resolve = resolve; + deferredOperation.reject = reject; + }); + + // Enqueue the deferred operation. + dbContext.deferredOperations.push(deferredOperation); + + // Chain its promise to the database readiness. + if (!dbContext.dbReady) { + dbContext.dbReady = deferredOperation.promise; + } else { + dbContext.dbReady = dbContext.dbReady.then(function () { + return deferredOperation.promise; + }); } - } +} - if (start > end) { - return []; - } - return arr.slice(start, end - start + 1); +function _advanceReadiness(dbInfo) { + var dbContext = dbContexts[dbInfo.name]; + + // Dequeue a deferred operation. + var deferredOperation = dbContext.deferredOperations.pop(); + + // Resolve its promise (which is part of the database readiness + // chain of promises). + if (deferredOperation) { + deferredOperation.resolve(); + return deferredOperation.promise; + } } -// path.relative(from, to) -// posix version -/** JSDoc */ -function relative(from, to) { - /* eslint-disable no-param-reassign */ - from = resolve(from).slice(1); - to = resolve(to).slice(1); - /* eslint-enable no-param-reassign */ +function _rejectReadiness(dbInfo, err) { + var dbContext = dbContexts[dbInfo.name]; - const fromParts = trim(from.split('/')); - const toParts = trim(to.split('/')); + // Dequeue a deferred operation. + var deferredOperation = dbContext.deferredOperations.pop(); - const length = Math.min(fromParts.length, toParts.length); - let samePartsLength = length; - for (let i = 0; i < length; i++) { - if (fromParts[i] !== toParts[i]) { - samePartsLength = i; - break; + // Reject its promise (which is part of the database readiness + // chain of promises). + if (deferredOperation) { + deferredOperation.reject(err); + return deferredOperation.promise; } - } +} - let outputParts = []; - for (let i = samePartsLength; i < fromParts.length; i++) { - outputParts.push('..'); - } +function _getConnection(dbInfo, upgradeNeeded) { + return new Promise$1(function (resolve, reject) { + dbContexts[dbInfo.name] = dbContexts[dbInfo.name] || createDbContext(); - outputParts = outputParts.concat(toParts.slice(samePartsLength)); + if (dbInfo.db) { + if (upgradeNeeded) { + _deferReadiness(dbInfo); + dbInfo.db.close(); + } else { + return resolve(dbInfo.db); + } + } - return outputParts.join('/'); + var dbArgs = [dbInfo.name]; + + if (upgradeNeeded) { + dbArgs.push(dbInfo.version); + } + + var openreq = idb.open.apply(idb, dbArgs); + + if (upgradeNeeded) { + openreq.onupgradeneeded = function (e) { + var db = openreq.result; + try { + db.createObjectStore(dbInfo.storeName); + if (e.oldVersion <= 1) { + // Added when support for blob shims was added + db.createObjectStore(DETECT_BLOB_SUPPORT_STORE); + } + } catch (ex) { + if (ex.name === 'ConstraintError') { + console.warn('The database "' + dbInfo.name + '"' + ' has been upgraded from version ' + e.oldVersion + ' to version ' + e.newVersion + ', but the storage "' + dbInfo.storeName + '" already exists.'); + } else { + throw ex; + } + } + }; + } + + openreq.onerror = function (e) { + e.preventDefault(); + reject(openreq.error); + }; + + openreq.onsuccess = function () { + var db = openreq.result; + db.onversionchange = function (e) { + // Triggered when the database is modified (e.g. adding an objectStore) or + // deleted (even when initiated by other sessions in different tabs). + // Closing the connection here prevents those operations from being blocked. + // If the database is accessed again later by this instance, the connection + // will be reopened or the database recreated as needed. + e.target.close(); + }; + resolve(db); + _advanceReadiness(dbInfo); + }; + }); } -// path.normalize(path) -// posix version -/** JSDoc */ -function normalizePath(path) { - const isPathAbsolute = isAbsolute(path); - const trailingSlash = path.slice(-1) === '/'; +function _getOriginalConnection(dbInfo) { + return _getConnection(dbInfo, false); +} - // Normalize the path - let normalizedPath = normalizeArray( - path.split('/').filter(p => !!p), - !isPathAbsolute, - ).join('/'); +function _getUpgradedConnection(dbInfo) { + return _getConnection(dbInfo, true); +} - if (!normalizedPath && !isPathAbsolute) { - normalizedPath = '.'; - } - if (normalizedPath && trailingSlash) { - normalizedPath += '/'; - } +function _isUpgradeNeeded(dbInfo, defaultVersion) { + if (!dbInfo.db) { + return true; + } - return (isPathAbsolute ? '/' : '') + normalizedPath; + var isNewStore = !dbInfo.db.objectStoreNames.contains(dbInfo.storeName); + var isDowngrade = dbInfo.version < dbInfo.db.version; + var isUpgrade = dbInfo.version > dbInfo.db.version; + + if (isDowngrade) { + // If the version is not the default one + // then warn for impossible downgrade. + if (dbInfo.version !== defaultVersion) { + console.warn('The database "' + dbInfo.name + '"' + " can't be downgraded from version " + dbInfo.db.version + ' to version ' + dbInfo.version + '.'); + } + // Align the versions to prevent errors. + dbInfo.version = dbInfo.db.version; + } + + if (isUpgrade || isNewStore) { + // If the store is new then increment the version (if needed). + // This will trigger an "upgradeneeded" event which is required + // for creating a store. + if (isNewStore) { + var incVersion = dbInfo.db.version + 1; + if (incVersion > dbInfo.version) { + dbInfo.version = incVersion; + } + } + + return true; + } + + return false; } -// posix version -/** JSDoc */ -function isAbsolute(path) { - return path.charAt(0) === '/'; +// encode a blob for indexeddb engines that don't support blobs +function _encodeBlob(blob) { + return new Promise$1(function (resolve, reject) { + var reader = new FileReader(); + reader.onerror = reject; + reader.onloadend = function (e) { + var base64 = btoa(e.target.result || ''); + resolve({ + __local_forage_encoded_blob: true, + data: base64, + type: blob.type + }); + }; + reader.readAsBinaryString(blob); + }); } -// posix version -/** JSDoc */ -function join(...args) { - return normalizePath(args.join('/')); +// decode an encoded blob +function _decodeBlob(encodedBlob) { + var arrayBuff = _binStringToArrayBuffer(atob(encodedBlob.data)); + return createBlob([arrayBuff], { type: encodedBlob.type }); } -/** JSDoc */ -function dirname(path) { - const result = splitPath(path); - const root = result[0]; - let dir = result[1]; +// is this one of our fancy encoded blobs? +function _isEncodedBlob(value) { + return value && value.__local_forage_encoded_blob; +} + +// Specialize the default `ready()` function by making it dependent +// on the current database operations. Thus, the driver will be actually +// ready when it's been initialized (default) *and* there are no pending +// operations on the database (initiated by some other instances). +function _fullyReady(callback) { + var self = this; - if (!root && !dir) { - // No dirname whatsoever - return '.'; - } + var promise = self._initReady().then(function () { + var dbContext = dbContexts[self._dbInfo.name]; - if (dir) { - // It has a dirname, strip trailing slash - dir = dir.slice(0, dir.length - 1); - } + if (dbContext && dbContext.dbReady) { + return dbContext.dbReady; + } + }); - return root + dir; + executeTwoCallbacks(promise, callback, callback); + return promise; } -/** JSDoc */ -function basename(path, ext) { - let f = splitPath(path)[2]; - if (ext && f.slice(ext.length * -1) === ext) { - f = f.slice(0, f.length - ext.length); - } - return f; +// Try to establish a new db connection to replace the +// current one which is broken (i.e. experiencing +// InvalidStateError while creating a transaction). +function _tryReconnect(dbInfo) { + _deferReadiness(dbInfo); + + var dbContext = dbContexts[dbInfo.name]; + var forages = dbContext.forages; + + for (var i = 0; i < forages.length; i++) { + var forage = forages[i]; + if (forage._dbInfo.db) { + forage._dbInfo.db.close(); + forage._dbInfo.db = null; + } + } + dbInfo.db = null; + + return _getOriginalConnection(dbInfo).then(function (db) { + dbInfo.db = db; + if (_isUpgradeNeeded(dbInfo)) { + // Reopen the database for upgrading. + return _getUpgradedConnection(dbInfo); + } + return db; + }).then(function (db) { + // store the latest db reference + // in case the db was upgraded + dbInfo.db = dbContext.db = db; + for (var i = 0; i < forages.length; i++) { + forages[i]._dbInfo.db = db; + } + })["catch"](function (err) { + _rejectReadiness(dbInfo, err); + throw err; + }); } -exports.basename = basename; -exports.dirname = dirname; -exports.isAbsolute = isAbsolute; -exports.join = join; -exports.normalizePath = normalizePath; -exports.relative = relative; -exports.resolve = resolve; +// FF doesn't like Promises (micro-tasks) and IDDB store operations, +// so we have to do it with callbacks +function createTransaction(dbInfo, mode, callback, retries) { + if (retries === undefined) { + retries = 1; + } + try { + var tx = dbInfo.db.transaction(dbInfo.storeName, mode); + callback(null, tx); + } catch (err) { + if (retries > 0 && (!dbInfo.db || err.name === 'InvalidStateError' || err.name === 'NotFoundError')) { + return Promise$1.resolve().then(function () { + if (!dbInfo.db || err.name === 'NotFoundError' && !dbInfo.db.objectStoreNames.contains(dbInfo.storeName) && dbInfo.version <= dbInfo.db.version) { + // increase the db version, to create the new ObjectStore + if (dbInfo.db) { + dbInfo.version = dbInfo.db.version + 1; + } + // Reopen the database for upgrading. + return _getUpgradedConnection(dbInfo); + } + }).then(function () { + return _tryReconnect(dbInfo).then(function () { + createTransaction(dbInfo, mode, callback, retries - 1); + }); + })["catch"](callback); + } -},{}],160:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + callback(err); + } +} -const error = require('./error.js'); -const syncpromise = require('./syncpromise.js'); +function createDbContext() { + return { + // Running localForages sharing a database. + forages: [], + // Shared database. + db: null, + // Database readiness (promise). + dbReady: null, + // Deferred operations on the database. + deferredOperations: [] + }; +} -/** - * Creates an new PromiseBuffer object with the specified limit - * @param limit max number of promises that can be stored in the buffer - */ -function makePromiseBuffer(limit) { - const buffer = []; +// Open the IndexedDB database (automatically creates one if one didn't +// previously exist), using any options set in the config. +function _initStorage(options) { + var self = this; + var dbInfo = { + db: null + }; - function isReady() { - return limit === undefined || buffer.length < limit; - } + if (options) { + for (var i in options) { + dbInfo[i] = options[i]; + } + } - /** - * Remove a promise from the queue. - * - * @param task Can be any PromiseLike - * @returns Removed promise. - */ - function remove(task) { - return buffer.splice(buffer.indexOf(task), 1)[0]; - } + // Get the current context of the database; + var dbContext = dbContexts[dbInfo.name]; - /** - * Add a promise (representing an in-flight action) to the queue, and set it to remove itself on fulfillment. - * - * @param taskProducer A function producing any PromiseLike; In previous versions this used to be `task: - * PromiseLike`, but under that model, Promises were instantly created on the call-site and their executor - * functions therefore ran immediately. Thus, even if the buffer was full, the action still happened. By - * requiring the promise to be wrapped in a function, we can defer promise creation until after the buffer - * limit check. - * @returns The original promise. - */ - function add(taskProducer) { - if (!isReady()) { - return syncpromise.rejectedSyncPromise(new error.SentryError('Not adding Promise because buffer limit was reached.')); + // ...or create a new context. + if (!dbContext) { + dbContext = createDbContext(); + // Register the new context in the global container. + dbContexts[dbInfo.name] = dbContext; } - // start the task and add its promise to the queue - const task = taskProducer(); - if (buffer.indexOf(task) === -1) { - buffer.push(task); + // Register itself as a running localForage in the current context. + dbContext.forages.push(self); + + // Replace the default `ready()` function with the specialized one. + if (!self._initReady) { + self._initReady = self.ready; + self.ready = _fullyReady; } - void task - .then(() => remove(task)) - // Use `then(null, rejectionHandler)` rather than `catch(rejectionHandler)` so that we can use `PromiseLike` - // rather than `Promise`. `PromiseLike` doesn't have a `.catch` method, making its polyfill smaller. (ES5 didn't - // have promises, so TS has to polyfill when down-compiling.) - .then(null, () => - remove(task).then(null, () => { - // We have to add another catch here because `remove()` starts a new promise chain. - }), - ); - return task; - } - /** - * Wait for all promises in the queue to resolve or for timeout to expire, whichever comes first. - * - * @param timeout The time, in ms, after which to resolve to `false` if the queue is still non-empty. Passing `0` (or - * not passing anything) will make the promise wait as long as it takes for the queue to drain before resolving to - * `true`. - * @returns A promise which will resolve to `true` if the queue is already empty or drains before the timeout, and - * `false` otherwise - */ - function drain(timeout) { - return new syncpromise.SyncPromise((resolve, reject) => { - let counter = buffer.length; + // Create an array of initialization states of the related localForages. + var initPromises = []; - if (!counter) { - return resolve(true); - } + function ignoreErrors() { + // Don't handle errors here, + // just makes sure related localForages aren't pending. + return Promise$1.resolve(); + } - // wait for `timeout` ms and then resolve to `false` (if not cancelled first) - const capturedSetTimeout = setTimeout(() => { - if (timeout && timeout > 0) { - resolve(false); + for (var j = 0; j < dbContext.forages.length; j++) { + var forage = dbContext.forages[j]; + if (forage !== self) { + // Don't wait for itself... + initPromises.push(forage._initReady()["catch"](ignoreErrors)); } - }, timeout); + } - // if all promises resolve in time, cancel the timer and resolve to `true` - buffer.forEach(item => { - void syncpromise.resolvedSyncPromise(item).then(() => { - if (!--counter) { - clearTimeout(capturedSetTimeout); - resolve(true); - } - }, reject); - }); + // Take a snapshot of the related localForages. + var forages = dbContext.forages.slice(0); + + // Initialize the connection process only when + // all the related localForages aren't pending. + return Promise$1.all(initPromises).then(function () { + dbInfo.db = dbContext.db; + // Get the connection or open a new one without upgrade. + return _getOriginalConnection(dbInfo); + }).then(function (db) { + dbInfo.db = db; + if (_isUpgradeNeeded(dbInfo, self._defaultConfig.version)) { + // Reopen the database for upgrading. + return _getUpgradedConnection(dbInfo); + } + return db; + }).then(function (db) { + dbInfo.db = dbContext.db = db; + self._dbInfo = dbInfo; + // Share the final connection amongst related localForages. + for (var k = 0; k < forages.length; k++) { + var forage = forages[k]; + if (forage !== self) { + // Self is already up-to-date. + forage._dbInfo.db = dbInfo.db; + forage._dbInfo.version = dbInfo.version; + } + } }); - } - - return { - $: buffer, - add, - drain, - }; } -exports.makePromiseBuffer = makePromiseBuffer; - +function getItem(key, callback) { + var self = this; -},{"./error.js":137,"./syncpromise.js":167}],161:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + key = normalizeKey(key); -// Intentionally keeping the key broad, as we don't know for sure what rate limit headers get returned from backend + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) { + if (err) { + return reject(err); + } -const DEFAULT_RETRY_AFTER = 60 * 1000; // 60 seconds + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.get(key); -/** - * Extracts Retry-After value from the request header or returns default value - * @param header string representation of 'Retry-After' header - * @param now current unix timestamp - * - */ -function parseRetryAfterHeader(header, now = Date.now()) { - const headerDelay = parseInt(`${header}`, 10); - if (!isNaN(headerDelay)) { - return headerDelay * 1000; - } + req.onsuccess = function () { + var value = req.result; + if (value === undefined) { + value = null; + } + if (_isEncodedBlob(value)) { + value = _decodeBlob(value); + } + resolve(value); + }; - const headerDate = Date.parse(`${header}`); - if (!isNaN(headerDate)) { - return headerDate - now; - } + req.onerror = function () { + reject(req.error); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); - return DEFAULT_RETRY_AFTER; + executeCallback(promise, callback); + return promise; } -/** - * Gets the time that the given category is disabled until for rate limiting. - * In case no category-specific limit is set but a general rate limit across all categories is active, - * that time is returned. - * - * @return the time in ms that the category is disabled until or 0 if there's no active rate limit. - */ -function disabledUntil(limits, dataCategory) { - return limits[dataCategory] || limits.all || 0; -} +// Iterate over all items stored in database. +function iterate(iterator, callback) { + var self = this; -/** - * Checks if a category is rate limited - */ -function isRateLimited(limits, dataCategory, now = Date.now()) { - return disabledUntil(limits, dataCategory) > now; -} + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) { + if (err) { + return reject(err); + } -/** - * Update ratelimits from incoming headers. - * - * @return the updated RateLimits object. - */ -function updateRateLimits( - limits, - { statusCode, headers }, - now = Date.now(), -) { - const updatedRateLimits = { - ...limits, - }; + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.openCursor(); + var iterationNumber = 1; - // "The name is case-insensitive." - // https://developer.mozilla.org/en-US/docs/Web/API/Headers/get - const rateLimitHeader = headers && headers['x-sentry-rate-limits']; - const retryAfterHeader = headers && headers['retry-after']; + req.onsuccess = function () { + var cursor = req.result; - if (rateLimitHeader) { - /** - * rate limit headers are of the form - *
,
,.. - * where each
is of the form - * : : : : - * where - * is a delay in seconds - * is the event type(s) (error, transaction, etc) being rate limited and is of the form - * ;;... - * is what's being limited (org, project, or key) - ignored by SDK - * is an arbitrary string like "org_quota" - ignored by SDK - * Semicolon-separated list of metric namespace identifiers. Defines which namespace(s) will be affected. - * Only present if rate limit applies to the metric_bucket data category. - */ - for (const limit of rateLimitHeader.trim().split(',')) { - const [retryAfter, categories, , , namespaces] = limit.split(':', 5); - const headerDelay = parseInt(retryAfter, 10); - const delay = (!isNaN(headerDelay) ? headerDelay : 60) * 1000; // 60sec default - if (!categories) { - updatedRateLimits.all = now + delay; - } else { - for (const category of categories.split(';')) { - if (category === 'metric_bucket') { - // namespaces will be present when category === 'metric_bucket' - if (!namespaces || namespaces.split(';').includes('custom')) { - updatedRateLimits[category] = now + delay; - } - } else { - updatedRateLimits[category] = now + delay; - } - } - } - } - } else if (retryAfterHeader) { - updatedRateLimits.all = now + parseRetryAfterHeader(retryAfterHeader, now); - } else if (statusCode === 429) { - updatedRateLimits.all = now + 60 * 1000; - } + if (cursor) { + var value = cursor.value; + if (_isEncodedBlob(value)) { + value = _decodeBlob(value); + } + var result = iterator(value, cursor.key, iterationNumber++); + + // when the iterator callback returns any + // (non-`undefined`) value, then we stop + // the iteration immediately + if (result !== void 0) { + resolve(result); + } else { + cursor["continue"](); + } + } else { + resolve(); + } + }; - return updatedRateLimits; + req.onerror = function () { + reject(req.error); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + + return promise; } -exports.DEFAULT_RETRY_AFTER = DEFAULT_RETRY_AFTER; -exports.disabledUntil = disabledUntil; -exports.isRateLimited = isRateLimited; -exports.parseRetryAfterHeader = parseRetryAfterHeader; -exports.updateRateLimits = updateRateLimits; +function setItem(key, value, callback) { + var self = this; + key = normalizeKey(key); -},{}],162:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + var promise = new Promise$1(function (resolve, reject) { + var dbInfo; + self.ready().then(function () { + dbInfo = self._dbInfo; + if (toString.call(value) === '[object Blob]') { + return _checkBlobSupport(dbInfo.db).then(function (blobSupport) { + if (blobSupport) { + return value; + } + return _encodeBlob(value); + }); + } + return value; + }).then(function (value) { + createTransaction(self._dbInfo, READ_WRITE, function (err, transaction) { + if (err) { + return reject(err); + } -const cookie = require('./cookie.js'); -const debugBuild = require('./debug-build.js'); -const is = require('./is.js'); -const logger = require('./logger.js'); -const normalize = require('./normalize.js'); -const url = require('./url.js'); + try { + var store = transaction.objectStore(self._dbInfo.storeName); + + // The reason we don't _save_ null is because IE 10 does + // not support saving the `null` type in IndexedDB. How + // ironic, given the bug below! + // See: https://github.com/mozilla/localForage/issues/161 + if (value === null) { + value = undefined; + } -const DEFAULT_INCLUDES = { - ip: false, - request: true, - transaction: true, - user: true, -}; -const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url']; -const DEFAULT_USER_INCLUDES = ['id', 'username', 'email']; + var req = store.put(value, key); + + transaction.oncomplete = function () { + // Cast to undefined so the value passed to + // callback/promise is the same as what one would get out + // of `getItem()` later. This leads to some weirdness + // (setItem('foo', undefined) will return `null`), but + // it's not my fault localStorage is our baseline and that + // it's weird. + if (value === undefined) { + value = null; + } -/** - * Sets parameterized route as transaction name e.g.: `GET /users/:id` - * Also adds more context data on the transaction from the request. - * - * @deprecated This utility will be removed in v8. - */ -function addRequestDataToTransaction( - transaction, - req, - deps, -) { - if (!transaction) return; - // eslint-disable-next-line deprecation/deprecation - if (!transaction.metadata.source || transaction.metadata.source === 'url') { - // Attempt to grab a parameterized route off of the request - const [name, source] = extractPathForTransaction(req, { path: true, method: true }); - transaction.updateName(name); - // TODO: SEMANTIC_ATTRIBUTE_SENTRY_SOURCE is in core, align this once we merge utils & core - // eslint-disable-next-line deprecation/deprecation - transaction.setMetadata({ source }); - } - transaction.setAttribute('url', req.originalUrl || req.url); - if (req.baseUrl) { - transaction.setAttribute('baseUrl', req.baseUrl); - } - // TODO: We need to rewrite this to a flat format? - // eslint-disable-next-line deprecation/deprecation - transaction.setData('query', extractQueryParams(req, deps)); + resolve(value); + }; + transaction.onabort = transaction.onerror = function () { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; } -/** - * Extracts a complete and parameterized path from the request object and uses it to construct transaction name. - * If the parameterized transaction name cannot be extracted, we fall back to the raw URL. - * - * Additionally, this function determines and returns the transaction name source - * - * eg. GET /mountpoint/user/:id - * - * @param req A request object - * @param options What to include in the transaction name (method, path, or a custom route name to be - * used instead of the request's route) - * - * @returns A tuple of the fully constructed transaction name [0] and its source [1] (can be either 'route' or 'url') - */ -function extractPathForTransaction( - req, - options = {}, -) { - const method = req.method && req.method.toUpperCase(); +function removeItem(key, callback) { + var self = this; - let path = ''; - let source = 'url'; + key = normalizeKey(key); - // Check to see if there's a parameterized route we can use (as there is in Express) - if (options.customRoute || req.route) { - path = options.customRoute || `${req.baseUrl || ''}${req.route && req.route.path}`; - source = 'route'; - } + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_WRITE, function (err, transaction) { + if (err) { + return reject(err); + } - // Otherwise, just take the original URL - else if (req.originalUrl || req.url) { - path = url.stripUrlQueryAndFragment(req.originalUrl || req.url || ''); - } + try { + var store = transaction.objectStore(self._dbInfo.storeName); + // We use a Grunt task to make this safe for IE and some + // versions of Android (including those used by Cordova). + // Normally IE won't like `.delete()` and will insist on + // using `['delete']()`, but we have a build step that + // fixes this for us now. + var req = store["delete"](key); + transaction.oncomplete = function () { + resolve(); + }; - let name = ''; - if (options.method && method) { - name += method; - } - if (options.method && options.path) { - name += ' '; - } - if (options.path && path) { - name += path; - } + transaction.onerror = function () { + reject(req.error); + }; - return [name, source]; -} + // The request will be also be aborted if we've exceeded our storage + // space. + transaction.onabort = function () { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); -/** JSDoc */ -function extractTransaction(req, type) { - switch (type) { - case 'path': { - return extractPathForTransaction(req, { path: true })[0]; - } - case 'handler': { - return (req.route && req.route.stack && req.route.stack[0] && req.route.stack[0].name) || ''; - } - case 'methodPath': - default: { - // if exist _reconstructedRoute return that path instead of route.path - const customRoute = req._reconstructedRoute ? req._reconstructedRoute : undefined; - return extractPathForTransaction(req, { path: true, method: true, customRoute })[0]; - } - } + executeCallback(promise, callback); + return promise; } -/** JSDoc */ -function extractUserData( - user +function clear(callback) { + var self = this; -, - keys, -) { - const extractedUser = {}; - const attributes = Array.isArray(keys) ? keys : DEFAULT_USER_INCLUDES; + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_WRITE, function (err, transaction) { + if (err) { + return reject(err); + } - attributes.forEach(key => { - if (user && key in user) { - extractedUser[key] = user[key]; - } - }); + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.clear(); - return extractedUser; -} + transaction.oncomplete = function () { + resolve(); + }; -/** - * Normalize data from the request object, accounting for framework differences. - * - * @param req The request object from which to extract data - * @param options.include An optional array of keys to include in the normalized data. Defaults to - * DEFAULT_REQUEST_INCLUDES if not provided. - * @param options.deps Injected, platform-specific dependencies - * @returns An object containing normalized request data - */ -function extractRequestData( - req, - options + transaction.onabort = transaction.onerror = function () { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); -, -) { - const { include = DEFAULT_REQUEST_INCLUDES, deps } = options || {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const requestData = {}; + executeCallback(promise, callback); + return promise; +} - // headers: - // node, express, koa, nextjs: req.headers - const headers = (req.headers || {}) +function length(callback) { + var self = this; -; - // method: - // node, express, koa, nextjs: req.method - const method = req.method; - // host: - // express: req.hostname in > 4 and req.host in < 4 - // koa: req.host - // node, nextjs: req.headers.host - // Express 4 mistakenly strips off port number from req.host / req.hostname so we can't rely on them - // See: https://github.com/expressjs/express/issues/3047#issuecomment-236653223 - // Also: https://github.com/getsentry/sentry-javascript/issues/1917 - const host = headers.host || req.hostname || req.host || ''; - // protocol: - // node, nextjs: - // express, koa: req.protocol - const protocol = req.protocol === 'https' || (req.socket && req.socket.encrypted) ? 'https' : 'http'; - // url (including path and query string): - // node, express: req.originalUrl - // koa, nextjs: req.url - const originalUrl = req.originalUrl || req.url || ''; - // absolute url - const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`; - include.forEach(key => { - switch (key) { - case 'headers': { - requestData.headers = headers; + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) { + if (err) { + return reject(err); + } - // Remove the Cookie header in case cookie data should not be included in the event - if (!include.includes('cookies')) { - delete (requestData.headers ).cookie; - } + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.count(); - break; - } - case 'method': { - requestData.method = method; - break; - } - case 'url': { - requestData.url = absoluteUrl; - break; - } - case 'cookies': { - // cookies: - // node, express, koa: req.headers.cookie - // vercel, sails.js, express (w/ cookie middleware), nextjs: req.cookies - requestData.cookies = - // TODO (v8 / #5257): We're only sending the empty object for backwards compatibility, so the last bit can - // come off in v8 - req.cookies || (headers.cookie && cookie.parseCookie(headers.cookie)) || {}; - break; - } - case 'query_string': { - // query string: - // node: req.url (raw) - // express, koa, nextjs: req.query - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - requestData.query_string = extractQueryParams(req, deps); - break; - } - case 'data': { - if (method === 'GET' || method === 'HEAD') { - break; - } - // body data: - // express, koa, nextjs: req.body - // - // when using node by itself, you have to read the incoming stream(see - // https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know - // where they're going to store the final result, so they'll have to capture this data themselves - if (req.body !== undefined) { - requestData.data = is.isString(req.body) ? req.body : JSON.stringify(normalize.normalize(req.body)); - } - break; - } - default: { - if ({}.hasOwnProperty.call(req, key)) { - requestData[key] = (req )[key]; - } - } - } - }); + req.onsuccess = function () { + resolve(req.result); + }; + + req.onerror = function () { + reject(req.error); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); - return requestData; + executeCallback(promise, callback); + return promise; } -/** - * Add data from the given request to the given event - * - * @param event The event to which the request data will be added - * @param req Request object - * @param options.include Flags to control what data is included - * @param options.deps Injected platform-specific dependencies - * @returns The mutated `Event` object - */ -function addRequestDataToEvent( - event, - req, - options, -) { - const include = { - ...DEFAULT_INCLUDES, - ...(options && options.include), - }; +function key(n, callback) { + var self = this; - if (include.request) { - const extractedRequestData = Array.isArray(include.request) - ? extractRequestData(req, { include: include.request, deps: options && options.deps }) - : extractRequestData(req, { deps: options && options.deps }); + var promise = new Promise$1(function (resolve, reject) { + if (n < 0) { + resolve(null); - event.request = { - ...event.request, - ...extractedRequestData, - }; - } + return; + } - if (include.user) { - const extractedUser = req.user && is.isPlainObject(req.user) ? extractUserData(req.user, include.user) : {}; + self.ready().then(function () { + createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) { + if (err) { + return reject(err); + } - if (Object.keys(extractedUser).length) { - event.user = { - ...event.user, - ...extractedUser, - }; - } - } + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var advanced = false; + var req = store.openKeyCursor(); - // client ip: - // node, nextjs: req.socket.remoteAddress - // express, koa: req.ip - if (include.ip) { - const ip = req.ip || (req.socket && req.socket.remoteAddress); - if (ip) { - event.user = { - ...event.user, - ip_address: ip, - }; - } - } + req.onsuccess = function () { + var cursor = req.result; + if (!cursor) { + // this means there weren't enough keys + resolve(null); - if (include.transaction && !event.transaction) { - // TODO do we even need this anymore? - // TODO make this work for nextjs - event.transaction = extractTransaction(req, include.transaction); - } + return; + } - return event; + if (n === 0) { + // We have the first key, return it if that's what they + // wanted. + resolve(cursor.key); + } else { + if (!advanced) { + // Otherwise, ask the cursor to skip ahead n + // records. + advanced = true; + cursor.advance(n); + } else { + // When we get here, we've got the nth key. + resolve(cursor.key); + } + } + }; + + req.onerror = function () { + reject(req.error); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; } -function extractQueryParams( - req, - deps, -) { - // url (including path and query string): - // node, express: req.originalUrl - // koa, nextjs: req.url - let originalUrl = req.originalUrl || req.url || ''; +function keys(callback) { + var self = this; - if (!originalUrl) { - return; - } + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) { + if (err) { + return reject(err); + } - // The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and - // hostname on the beginning. Since the point here is just to grab the query string, it doesn't matter what we use. - if (originalUrl.startsWith('/')) { - originalUrl = `http://dogs.are.great${originalUrl}`; - } + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.openKeyCursor(); + var keys = []; - try { - return ( - req.query || - (typeof URL !== 'undefined' && new URL(originalUrl).search.slice(1)) || - // In Node 8, `URL` isn't in the global scope, so we have to use the built-in module from Node - (deps && deps.url && deps.url.parse(originalUrl).query) || - undefined - ); - } catch (e2) { - return undefined; - } -} + req.onsuccess = function () { + var cursor = req.result; -/** - * Transforms a `Headers` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into a simple key-value dict. - * The header keys will be lower case: e.g. A "Content-Type" header will be stored as "content-type". - */ -// TODO(v8): Make this function return undefined when the extraction fails. -function winterCGHeadersToDict(winterCGHeaders) { - const headers = {}; - try { - winterCGHeaders.forEach((value, key) => { - if (typeof value === 'string') { - // We check that value is a string even though it might be redundant to make sure prototype pollution is not possible. - headers[key] = value; - } + if (!cursor) { + resolve(keys); + return; + } + + keys.push(cursor.key); + cursor["continue"](); + }; + + req.onerror = function () { + reject(req.error); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); }); - } catch (e) { - debugBuild.DEBUG_BUILD && - logger.logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.'); - } - return headers; + executeCallback(promise, callback); + return promise; } -/** - * Converts a `Request` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into the format that the `RequestData` integration understands. - */ -function winterCGRequestToRequestData(req) { - const headers = winterCGHeadersToDict(req.headers); - return { - method: req.method, - url: req.url, - headers, - }; -} +function dropInstance(options, callback) { + callback = getCallback.apply(this, arguments); -exports.DEFAULT_USER_INCLUDES = DEFAULT_USER_INCLUDES; -exports.addRequestDataToEvent = addRequestDataToEvent; -exports.addRequestDataToTransaction = addRequestDataToTransaction; -exports.extractPathForTransaction = extractPathForTransaction; -exports.extractRequestData = extractRequestData; -exports.winterCGHeadersToDict = winterCGHeadersToDict; -exports.winterCGRequestToRequestData = winterCGRequestToRequestData; + var currentConfig = this.config(); + options = typeof options !== 'function' && options || {}; + if (!options.name) { + options.name = options.name || currentConfig.name; + options.storeName = options.storeName || currentConfig.storeName; + } + var self = this; + var promise; + if (!options.name) { + promise = Promise$1.reject('Invalid arguments'); + } else { + var isCurrentDb = options.name === currentConfig.name && self._dbInfo.db; + + var dbPromise = isCurrentDb ? Promise$1.resolve(self._dbInfo.db) : _getOriginalConnection(options).then(function (db) { + var dbContext = dbContexts[options.name]; + var forages = dbContext.forages; + dbContext.db = db; + for (var i = 0; i < forages.length; i++) { + forages[i]._dbInfo.db = db; + } + return db; + }); -},{"./cookie.js":132,"./debug-build.js":133,"./is.js":149,"./logger.js":151,"./normalize.js":157,"./url.js":170}],163:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + if (!options.storeName) { + promise = dbPromise.then(function (db) { + _deferReadiness(options); -// Note: Ideally the `SeverityLevel` type would be derived from `validSeverityLevels`, but that would mean either -// -// a) moving `validSeverityLevels` to `@sentry/types`, -// b) moving the`SeverityLevel` type here, or -// c) importing `validSeverityLevels` from here into `@sentry/types`. -// -// Option A would make `@sentry/types` a runtime dependency of `@sentry/utils` (not good), and options B and C would -// create a circular dependency between `@sentry/types` and `@sentry/utils` (also not good). So a TODO accompanying the -// type, reminding anyone who changes it to change this list also, will have to do. + var dbContext = dbContexts[options.name]; + var forages = dbContext.forages; -const validSeverityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug']; + db.close(); + for (var i = 0; i < forages.length; i++) { + var forage = forages[i]; + forage._dbInfo.db = null; + } -/** - * Converts a string-based level into a member of the deprecated {@link Severity} enum. - * - * @deprecated `severityFromString` is deprecated. Please use `severityLevelFromString` instead. - * - * @param level String representation of Severity - * @returns Severity - */ -function severityFromString(level) { - return severityLevelFromString(level) ; -} + var dropDBPromise = new Promise$1(function (resolve, reject) { + var req = idb.deleteDatabase(options.name); -/** - * Converts a string-based level into a `SeverityLevel`, normalizing it along the way. - * - * @param level String representation of desired `SeverityLevel`. - * @returns The `SeverityLevel` corresponding to the given string, or 'log' if the string isn't a valid level. - */ -function severityLevelFromString(level) { - return (level === 'warn' ? 'warning' : validSeverityLevels.includes(level) ? level : 'log') ; -} + req.onerror = function () { + var db = req.result; + if (db) { + db.close(); + } + reject(req.error); + }; -exports.severityFromString = severityFromString; -exports.severityLevelFromString = severityLevelFromString; -exports.validSeverityLevels = validSeverityLevels; + req.onblocked = function () { + // Closing all open connections in onversionchange handler should prevent this situation, but if + // we do get here, it just means the request remains pending - eventually it will succeed or error + console.warn('dropInstance blocked for database "' + options.name + '" until all open connections are closed'); + }; + req.onsuccess = function () { + var db = req.result; + if (db) { + db.close(); + } + resolve(db); + }; + }); -},{}],164:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + return dropDBPromise.then(function (db) { + dbContext.db = db; + for (var i = 0; i < forages.length; i++) { + var _forage = forages[i]; + _advanceReadiness(_forage._dbInfo); + } + })["catch"](function (err) { + (_rejectReadiness(options, err) || Promise$1.resolve())["catch"](function () {}); + throw err; + }); + }); + } else { + promise = dbPromise.then(function (db) { + if (!db.objectStoreNames.contains(options.storeName)) { + return; + } -const nodeStackTrace = require('./node-stack-trace.js'); + var newVersion = db.version + 1; -const STACKTRACE_FRAME_LIMIT = 50; -// Used to sanitize webpack (error: *) wrapped stack errors -const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/; -const STRIP_FRAME_REGEXP = /captureMessage|captureException/; + _deferReadiness(options); -/** - * Creates a stack parser with the supplied line parsers - * - * StackFrames are returned in the correct order for Sentry Exception - * frames and with Sentry SDK internal frames removed from the top and bottom - * - */ -function createStackParser(...parsers) { - const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map(p => p[1]); + var dbContext = dbContexts[options.name]; + var forages = dbContext.forages; - return (stack, skipFirst = 0) => { - const frames = []; - const lines = stack.split('\n'); + db.close(); + for (var i = 0; i < forages.length; i++) { + var forage = forages[i]; + forage._dbInfo.db = null; + forage._dbInfo.version = newVersion; + } - for (let i = skipFirst; i < lines.length; i++) { - const line = lines[i]; - // Ignore lines over 1kb as they are unlikely to be stack frames. - // Many of the regular expressions use backtracking which results in run time that increases exponentially with - // input size. Huge strings can result in hangs/Denial of Service: - // https://github.com/getsentry/sentry-javascript/issues/2286 - if (line.length > 1024) { - continue; - } + var dropObjectPromise = new Promise$1(function (resolve, reject) { + var req = idb.open(options.name, newVersion); - // https://github.com/getsentry/sentry-javascript/issues/5459 - // Remove webpack (error: *) wrappers - const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, '$1') : line; + req.onerror = function (err) { + var db = req.result; + db.close(); + reject(err); + }; - // https://github.com/getsentry/sentry-javascript/issues/7813 - // Skip Error: lines - if (cleanedLine.match(/\S*Error: /)) { - continue; - } + req.onupgradeneeded = function () { + var db = req.result; + db.deleteObjectStore(options.storeName); + }; - for (const parser of sortedParsers) { - const frame = parser(cleanedLine); + req.onsuccess = function () { + var db = req.result; + db.close(); + resolve(db); + }; + }); - if (frame) { - frames.push(frame); - break; + return dropObjectPromise.then(function (db) { + dbContext.db = db; + for (var j = 0; j < forages.length; j++) { + var _forage2 = forages[j]; + _forage2._dbInfo.db = db; + _advanceReadiness(_forage2._dbInfo); + } + })["catch"](function (err) { + (_rejectReadiness(options, err) || Promise$1.resolve())["catch"](function () {}); + throw err; + }); + }); } - } - - if (frames.length >= STACKTRACE_FRAME_LIMIT) { - break; - } } - return stripSentryFramesAndReverse(frames); - }; + executeCallback(promise, callback); + return promise; } -/** - * Gets a stack parser implementation from Options.stackParser - * @see Options - * - * If options contains an array of line parsers, it is converted into a parser - */ -function stackParserFromStackParserOptions(stackParser) { - if (Array.isArray(stackParser)) { - return createStackParser(...stackParser); - } - return stackParser; -} +var asyncStorage = { + _driver: 'asyncStorage', + _initStorage: _initStorage, + _support: isIndexedDBValid(), + iterate: iterate, + getItem: getItem, + setItem: setItem, + removeItem: removeItem, + clear: clear, + length: length, + key: key, + keys: keys, + dropInstance: dropInstance +}; -/** - * Removes Sentry frames from the top and bottom of the stack if present and enforces a limit of max number of frames. - * Assumes stack input is ordered from top to bottom and returns the reverse representation so call site of the - * function that caused the crash is the last frame in the array. - * @hidden - */ -function stripSentryFramesAndReverse(stack) { - if (!stack.length) { - return []; - } +function isWebSQLValid() { + return typeof openDatabase === 'function'; +} + +// Sadly, the best way to save binary data in WebSQL/localStorage is serializing +// it to Base64, so this is how we store it to prevent very strange errors with less +// verbose ways of binary <-> string data storage. +var BASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + +var BLOB_TYPE_PREFIX = '~~local_forage_type~'; +var BLOB_TYPE_PREFIX_REGEX = /^~~local_forage_type~([^~]+)~/; + +var SERIALIZED_MARKER = '__lfsc__:'; +var SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER.length; + +// OMG the serializations! +var TYPE_ARRAYBUFFER = 'arbf'; +var TYPE_BLOB = 'blob'; +var TYPE_INT8ARRAY = 'si08'; +var TYPE_UINT8ARRAY = 'ui08'; +var TYPE_UINT8CLAMPEDARRAY = 'uic8'; +var TYPE_INT16ARRAY = 'si16'; +var TYPE_INT32ARRAY = 'si32'; +var TYPE_UINT16ARRAY = 'ur16'; +var TYPE_UINT32ARRAY = 'ui32'; +var TYPE_FLOAT32ARRAY = 'fl32'; +var TYPE_FLOAT64ARRAY = 'fl64'; +var TYPE_SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER_LENGTH + TYPE_ARRAYBUFFER.length; + +var toString$1 = Object.prototype.toString; + +function stringToBuffer(serializedString) { + // Fill the string into a ArrayBuffer. + var bufferLength = serializedString.length * 0.75; + var len = serializedString.length; + var i; + var p = 0; + var encoded1, encoded2, encoded3, encoded4; + + if (serializedString[serializedString.length - 1] === '=') { + bufferLength--; + if (serializedString[serializedString.length - 2] === '=') { + bufferLength--; + } + } - const localStack = Array.from(stack); + var buffer = new ArrayBuffer(bufferLength); + var bytes = new Uint8Array(buffer); - // If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call) - if (/sentryWrapped/.test(localStack[localStack.length - 1].function || '')) { - localStack.pop(); - } + for (i = 0; i < len; i += 4) { + encoded1 = BASE_CHARS.indexOf(serializedString[i]); + encoded2 = BASE_CHARS.indexOf(serializedString[i + 1]); + encoded3 = BASE_CHARS.indexOf(serializedString[i + 2]); + encoded4 = BASE_CHARS.indexOf(serializedString[i + 3]); - // Reversing in the middle of the procedure allows us to just pop the values off the stack - localStack.reverse(); + /*jslint bitwise: true */ + bytes[p++] = encoded1 << 2 | encoded2 >> 4; + bytes[p++] = (encoded2 & 15) << 4 | encoded3 >> 2; + bytes[p++] = (encoded3 & 3) << 6 | encoded4 & 63; + } + return buffer; +} - // If stack ends with one of our internal API calls, remove it (ends, meaning it's the bottom of the stack - aka top-most call) - if (STRIP_FRAME_REGEXP.test(localStack[localStack.length - 1].function || '')) { - localStack.pop(); +// Converts a buffer to a string to store, serialized, in the backend +// storage library. +function bufferToString(buffer) { + // base64-arraybuffer + var bytes = new Uint8Array(buffer); + var base64String = ''; + var i; - // When using synthetic events, we will have a 2 levels deep stack, as `new Error('Sentry syntheticException')` - // is produced within the hub itself, making it: - // - // Sentry.captureException() - // getCurrentHub().captureException() - // - // instead of just the top `Sentry` call itself. - // This forces us to possibly strip an additional frame in the exact same was as above. - if (STRIP_FRAME_REGEXP.test(localStack[localStack.length - 1].function || '')) { - localStack.pop(); + for (i = 0; i < bytes.length; i += 3) { + /*jslint bitwise: true */ + base64String += BASE_CHARS[bytes[i] >> 2]; + base64String += BASE_CHARS[(bytes[i] & 3) << 4 | bytes[i + 1] >> 4]; + base64String += BASE_CHARS[(bytes[i + 1] & 15) << 2 | bytes[i + 2] >> 6]; + base64String += BASE_CHARS[bytes[i + 2] & 63]; } - } - return localStack.slice(0, STACKTRACE_FRAME_LIMIT).map(frame => ({ - ...frame, - filename: frame.filename || localStack[localStack.length - 1].filename, - function: frame.function || '?', - })); + if (bytes.length % 3 === 2) { + base64String = base64String.substring(0, base64String.length - 1) + '='; + } else if (bytes.length % 3 === 1) { + base64String = base64String.substring(0, base64String.length - 2) + '=='; + } + + return base64String; } -const defaultFunctionName = ''; +// Serialize a value, afterwards executing a callback (which usually +// instructs the `setItem()` callback/promise to be executed). This is how +// we store binary data with localStorage. +function serialize(value, callback) { + var valueType = ''; + if (value) { + valueType = toString$1.call(value); + } -/** - * Safely extract function name from itself - */ -function getFunctionName(fn) { - try { - if (!fn || typeof fn !== 'function') { - return defaultFunctionName; + // Cannot use `value instanceof ArrayBuffer` or such here, as these + // checks fail when running the tests using casper.js... + // + // TODO: See why those tests fail and use a better solution. + if (value && (valueType === '[object ArrayBuffer]' || value.buffer && toString$1.call(value.buffer) === '[object ArrayBuffer]')) { + // Convert binary arrays to a string and prefix the string with + // a special marker. + var buffer; + var marker = SERIALIZED_MARKER; + + if (value instanceof ArrayBuffer) { + buffer = value; + marker += TYPE_ARRAYBUFFER; + } else { + buffer = value.buffer; + + if (valueType === '[object Int8Array]') { + marker += TYPE_INT8ARRAY; + } else if (valueType === '[object Uint8Array]') { + marker += TYPE_UINT8ARRAY; + } else if (valueType === '[object Uint8ClampedArray]') { + marker += TYPE_UINT8CLAMPEDARRAY; + } else if (valueType === '[object Int16Array]') { + marker += TYPE_INT16ARRAY; + } else if (valueType === '[object Uint16Array]') { + marker += TYPE_UINT16ARRAY; + } else if (valueType === '[object Int32Array]') { + marker += TYPE_INT32ARRAY; + } else if (valueType === '[object Uint32Array]') { + marker += TYPE_UINT32ARRAY; + } else if (valueType === '[object Float32Array]') { + marker += TYPE_FLOAT32ARRAY; + } else if (valueType === '[object Float64Array]') { + marker += TYPE_FLOAT64ARRAY; + } else { + callback(new Error('Failed to get type for BinaryArray')); + } + } + + callback(marker + bufferToString(buffer)); + } else if (valueType === '[object Blob]') { + // Conver the blob to a binaryArray and then to a string. + var fileReader = new FileReader(); + + fileReader.onload = function () { + // Backwards-compatible prefix for the blob type. + var str = BLOB_TYPE_PREFIX + value.type + '~' + bufferToString(this.result); + + callback(SERIALIZED_MARKER + TYPE_BLOB + str); + }; + + fileReader.readAsArrayBuffer(value); + } else { + try { + callback(JSON.stringify(value)); + } catch (e) { + console.error("Couldn't convert value into a JSON string: ", value); + + callback(null, e); + } } - return fn.name || defaultFunctionName; - } catch (e) { - // Just accessing custom props in some Selenium environments - // can cause a "Permission denied" exception (see raven-js#495). - return defaultFunctionName; - } } -/** - * Node.js stack line parser +// Deserialize data we've inserted into a value column/field. We place +// special markers into our strings to mark them as encoded; this isn't +// as nice as a meta field, but it's the only sane thing we can do whilst +// keeping localStorage support intact. +// +// Oftentimes this will just deserialize JSON content, but if we have a +// special marker (SERIALIZED_MARKER, defined above), we will extract +// some kind of arraybuffer/binary data/typed array out of the string. +function deserialize(value) { + // If we haven't marked this string as being specially serialized (i.e. + // something other than serialized JSON), we can just return it and be + // done with it. + if (value.substring(0, SERIALIZED_MARKER_LENGTH) !== SERIALIZED_MARKER) { + return JSON.parse(value); + } + + // The following code deals with deserializing some kind of Blob or + // TypedArray. First we separate out the type of data we're dealing + // with from the data itself. + var serializedString = value.substring(TYPE_SERIALIZED_MARKER_LENGTH); + var type = value.substring(SERIALIZED_MARKER_LENGTH, TYPE_SERIALIZED_MARKER_LENGTH); + + var blobType; + // Backwards-compatible blob type serialization strategy. + // DBs created with older versions of localForage will simply not have the blob type. + if (type === TYPE_BLOB && BLOB_TYPE_PREFIX_REGEX.test(serializedString)) { + var matcher = serializedString.match(BLOB_TYPE_PREFIX_REGEX); + blobType = matcher[1]; + serializedString = serializedString.substring(matcher[0].length); + } + var buffer = stringToBuffer(serializedString); + + // Return the right type based on the code/type set during + // serialization. + switch (type) { + case TYPE_ARRAYBUFFER: + return buffer; + case TYPE_BLOB: + return createBlob([buffer], { type: blobType }); + case TYPE_INT8ARRAY: + return new Int8Array(buffer); + case TYPE_UINT8ARRAY: + return new Uint8Array(buffer); + case TYPE_UINT8CLAMPEDARRAY: + return new Uint8ClampedArray(buffer); + case TYPE_INT16ARRAY: + return new Int16Array(buffer); + case TYPE_UINT16ARRAY: + return new Uint16Array(buffer); + case TYPE_INT32ARRAY: + return new Int32Array(buffer); + case TYPE_UINT32ARRAY: + return new Uint32Array(buffer); + case TYPE_FLOAT32ARRAY: + return new Float32Array(buffer); + case TYPE_FLOAT64ARRAY: + return new Float64Array(buffer); + default: + throw new Error('Unkown type: ' + type); + } +} + +var localforageSerializer = { + serialize: serialize, + deserialize: deserialize, + stringToBuffer: stringToBuffer, + bufferToString: bufferToString +}; + +/* + * Includes code from: * - * This is in @sentry/utils so it can be used from the Electron SDK in the browser for when `nodeIntegration == true`. - * This allows it to be used without referencing or importing any node specific code which causes bundlers to complain + * base64-arraybuffer + * https://github.com/niklasvh/base64-arraybuffer + * + * Copyright (c) 2012 Niklas von Hertzen + * Licensed under the MIT license. */ -function nodeStackLineParser(getModule) { - return [90, nodeStackTrace.node(getModule)]; + +function createDbTable(t, dbInfo, callback, errorCallback) { + t.executeSql('CREATE TABLE IF NOT EXISTS ' + dbInfo.storeName + ' ' + '(id INTEGER PRIMARY KEY, key unique, value)', [], callback, errorCallback); } -exports.filenameIsInApp = nodeStackTrace.filenameIsInApp; -exports.createStackParser = createStackParser; -exports.getFunctionName = getFunctionName; -exports.nodeStackLineParser = nodeStackLineParser; -exports.stackParserFromStackParserOptions = stackParserFromStackParserOptions; -exports.stripSentryFramesAndReverse = stripSentryFramesAndReverse; +// Open the WebSQL database (automatically creates one if one didn't +// previously exist), using any options set in the config. +function _initStorage$1(options) { + var self = this; + var dbInfo = { + db: null + }; + if (options) { + for (var i in options) { + dbInfo[i] = typeof options[i] !== 'string' ? options[i].toString() : options[i]; + } + } -},{"./node-stack-trace.js":155}],165:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + var dbInfoPromise = new Promise$1(function (resolve, reject) { + // Open the database; the openDatabase API will automatically + // create it for us if it doesn't exist. + try { + dbInfo.db = openDatabase(dbInfo.name, String(dbInfo.version), dbInfo.description, dbInfo.size); + } catch (e) { + return reject(e); + } -const is = require('./is.js'); + // Create our key/value table if it doesn't exist. + dbInfo.db.transaction(function (t) { + createDbTable(t, dbInfo, function () { + self._dbInfo = dbInfo; + resolve(); + }, function (t, error) { + reject(error); + }); + }, reject); + }); -/** - * Truncates given string to the maximum characters count - * - * @param str An object that contains serializable values - * @param max Maximum number of characters in truncated string (0 = unlimited) - * @returns string Encoded - */ -function truncate(str, max = 0) { - if (typeof str !== 'string' || max === 0) { - return str; - } - return str.length <= max ? str : `${str.slice(0, max)}...`; + dbInfo.serializer = localforageSerializer; + return dbInfoPromise; +} + +function tryExecuteSql(t, dbInfo, sqlStatement, args, callback, errorCallback) { + t.executeSql(sqlStatement, args, callback, function (t, error) { + if (error.code === error.SYNTAX_ERR) { + t.executeSql('SELECT name FROM sqlite_master ' + "WHERE type='table' AND name = ?", [dbInfo.storeName], function (t, results) { + if (!results.rows.length) { + // if the table is missing (was deleted) + // re-create it table and retry + createDbTable(t, dbInfo, function () { + t.executeSql(sqlStatement, args, callback, errorCallback); + }, errorCallback); + } else { + errorCallback(t, error); + } + }, errorCallback); + } else { + errorCallback(t, error); + } + }, errorCallback); } -/** - * This is basically just `trim_line` from - * https://github.com/getsentry/sentry/blob/master/src/sentry/lang/javascript/processor.py#L67 - * - * @param str An object that contains serializable values - * @param max Maximum number of characters in truncated string - * @returns string Encoded - */ -function snipLine(line, colno) { - let newLine = line; - const lineLength = newLine.length; - if (lineLength <= 150) { - return newLine; - } - if (colno > lineLength) { - // eslint-disable-next-line no-param-reassign - colno = lineLength; - } +function getItem$1(key, callback) { + var self = this; - let start = Math.max(colno - 60, 0); - if (start < 5) { - start = 0; - } + key = normalizeKey(key); - let end = Math.min(start + 140, lineLength); - if (end > lineLength - 5) { - end = lineLength; - } - if (end === lineLength) { - start = Math.max(end - 140, 0); - } + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'SELECT * FROM ' + dbInfo.storeName + ' WHERE key = ? LIMIT 1', [key], function (t, results) { + var result = results.rows.length ? results.rows.item(0).value : null; - newLine = newLine.slice(start, end); - if (start > 0) { - newLine = `'{snip} ${newLine}`; - } - if (end < lineLength) { - newLine += ' {snip}'; - } + // Check to see if this is serialized content we need to + // unpack. + if (result) { + result = dbInfo.serializer.deserialize(result); + } - return newLine; + resolve(result); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; } -/** - * Join values in array - * @param input array of values to be joined together - * @param delimiter string to be placed in-between values - * @returns Joined values - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function safeJoin(input, delimiter) { - if (!Array.isArray(input)) { - return ''; - } +function iterate$1(iterator, callback) { + var self = this; - const output = []; - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < input.length; i++) { - const value = input[i]; - try { - // This is a hack to fix a Vue3-specific bug that causes an infinite loop of - // console warnings. This happens when a Vue template is rendered with - // an undeclared variable, which we try to stringify, ultimately causing - // Vue to issue another warning which repeats indefinitely. - // see: https://github.com/getsentry/sentry-javascript/pull/8981 - if (is.isVueViewModel(value)) { - output.push('[VueViewModel]'); - } else { - output.push(String(value)); - } - } catch (e) { - output.push('[value cannot be serialized]'); - } - } + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; - return output.join(delimiter); -} + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'SELECT * FROM ' + dbInfo.storeName, [], function (t, results) { + var rows = results.rows; + var length = rows.length; -/** - * Checks if the given value matches a regex or string - * - * @param value The string to test - * @param pattern Either a regex or a string against which `value` will be matched - * @param requireExactStringMatch If true, `value` must match `pattern` exactly. If false, `value` will match - * `pattern` if it contains `pattern`. Only applies to string-type patterns. - */ -function isMatchingPattern( - value, - pattern, - requireExactStringMatch = false, -) { - if (!is.isString(value)) { - return false; - } + for (var i = 0; i < length; i++) { + var item = rows.item(i); + var result = item.value; - if (is.isRegExp(pattern)) { - return pattern.test(value); - } - if (is.isString(pattern)) { - return requireExactStringMatch ? value === pattern : value.includes(pattern); - } + // Check to see if this is serialized content + // we need to unpack. + if (result) { + result = dbInfo.serializer.deserialize(result); + } - return false; -} + result = iterator(result, item.key, i + 1); -/** - * Test the given string against an array of strings and regexes. By default, string matching is done on a - * substring-inclusion basis rather than a strict equality basis - * - * @param testString The string to test - * @param patterns The patterns against which to test the string - * @param requireExactStringMatch If true, `testString` must match one of the given string patterns exactly in order to - * count. If false, `testString` will match a string pattern if it contains that pattern. - * @returns - */ -function stringMatchesSomePattern( - testString, - patterns = [], - requireExactStringMatch = false, -) { - return patterns.some(pattern => isMatchingPattern(testString, pattern, requireExactStringMatch)); -} + // void(0) prevents problems with redefinition + // of `undefined`. + if (result !== void 0) { + resolve(result); + return; + } + } -exports.isMatchingPattern = isMatchingPattern; -exports.safeJoin = safeJoin; -exports.snipLine = snipLine; -exports.stringMatchesSomePattern = stringMatchesSomePattern; -exports.truncate = truncate; + resolve(); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} +function _setItem(key, value, callback, retriesLeft) { + var self = this; -},{"./is.js":149}],166:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + key = normalizeKey(key); -const debugBuild = require('./debug-build.js'); -const logger = require('./logger.js'); -const worldwide = require('./worldwide.js'); + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + // The localStorage API doesn't return undefined values in an + // "expected" way, so undefined is always cast to null in all + // drivers. See: https://github.com/mozilla/localForage/pull/42 + if (value === undefined) { + value = null; + } -// eslint-disable-next-line deprecation/deprecation -const WINDOW = worldwide.getGlobalObject(); + // Save the original value to pass to the callback. + var originalValue = value; + + var dbInfo = self._dbInfo; + dbInfo.serializer.serialize(value, function (value, error) { + if (error) { + reject(error); + } else { + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'INSERT OR REPLACE INTO ' + dbInfo.storeName + ' ' + '(key, value) VALUES (?, ?)', [key, value], function () { + resolve(originalValue); + }, function (t, error) { + reject(error); + }); + }, function (sqlError) { + // The transaction failed; check + // to see if it's a quota error. + if (sqlError.code === sqlError.QUOTA_ERR) { + // We reject the callback outright for now, but + // it's worth trying to re-run the transaction. + // Even if the user accepts the prompt to use + // more storage on Safari, this error will + // be called. + // + // Try to re-run the transaction. + if (retriesLeft > 0) { + resolve(_setItem.apply(self, [key, originalValue, callback, retriesLeft - 1])); + return; + } + reject(sqlError); + } + }); + } + }); + })["catch"](reject); + }); -/** - * Tells whether current environment supports ErrorEvent objects - * {@link supportsErrorEvent}. - * - * @returns Answer to the given question. - */ -function supportsErrorEvent() { - try { - new ErrorEvent(''); - return true; - } catch (e) { - return false; - } + executeCallback(promise, callback); + return promise; } -/** - * Tells whether current environment supports DOMError objects - * {@link supportsDOMError}. - * - * @returns Answer to the given question. - */ -function supportsDOMError() { - try { - // Chrome: VM89:1 Uncaught TypeError: Failed to construct 'DOMError': - // 1 argument required, but only 0 present. - // @ts-expect-error It really needs 1 argument, not 0. - new DOMError(''); - return true; - } catch (e) { - return false; - } +function setItem$1(key, value, callback) { + return _setItem.apply(this, [key, value, callback, 1]); } -/** - * Tells whether current environment supports DOMException objects - * {@link supportsDOMException}. - * - * @returns Answer to the given question. - */ -function supportsDOMException() { - try { - new DOMException(''); - return true; - } catch (e) { - return false; - } -} +function removeItem$1(key, callback) { + var self = this; -/** - * Tells whether current environment supports Fetch API - * {@link supportsFetch}. - * - * @returns Answer to the given question. - */ -function supportsFetch() { - if (!('fetch' in WINDOW)) { - return false; - } + key = normalizeKey(key); - try { - new Headers(); - new Request('http://www.example.com'); - new Response(); - return true; - } catch (e) { - return false; - } -} -/** - * isNativeFetch checks if the given function is a native implementation of fetch() - */ -// eslint-disable-next-line @typescript-eslint/ban-types -function isNativeFetch(func) { - return func && /^function fetch\(\)\s+\{\s+\[native code\]\s+\}$/.test(func.toString()); + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'DELETE FROM ' + dbInfo.storeName + ' WHERE key = ?', [key], function () { + resolve(); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; } -/** - * Tells whether current environment supports Fetch API natively - * {@link supportsNativeFetch}. - * - * @returns true if `window.fetch` is natively implemented, false otherwise - */ -function supportsNativeFetch() { - if (typeof EdgeRuntime === 'string') { - return true; - } +// Deletes every item in the table. +// TODO: Find out if this resets the AUTO_INCREMENT number. +function clear$1(callback) { + var self = this; - if (!supportsFetch()) { - return false; - } + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'DELETE FROM ' + dbInfo.storeName, [], function () { + resolve(); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); - // Fast path to avoid DOM I/O - // eslint-disable-next-line @typescript-eslint/unbound-method - if (isNativeFetch(WINDOW.fetch)) { - return true; - } + executeCallback(promise, callback); + return promise; +} + +// Does a simple `COUNT(key)` to get the number of items stored in +// localForage. +function length$1(callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + // Ahhh, SQL makes this one soooooo easy. + tryExecuteSql(t, dbInfo, 'SELECT COUNT(key) as c FROM ' + dbInfo.storeName, [], function (t, results) { + var result = results.rows.item(0).c; + resolve(result); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); - // window.fetch is implemented, but is polyfilled or already wrapped (e.g: by a chrome extension) - // so create a "pure" iframe to see if that has native fetch - let result = false; - const doc = WINDOW.document; - // eslint-disable-next-line deprecation/deprecation - if (doc && typeof (doc.createElement ) === 'function') { - try { - const sandbox = doc.createElement('iframe'); - sandbox.hidden = true; - doc.head.appendChild(sandbox); - if (sandbox.contentWindow && sandbox.contentWindow.fetch) { - // eslint-disable-next-line @typescript-eslint/unbound-method - result = isNativeFetch(sandbox.contentWindow.fetch); - } - doc.head.removeChild(sandbox); - } catch (err) { - debugBuild.DEBUG_BUILD && - logger.logger.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', err); - } - } + executeCallback(promise, callback); + return promise; +} + +// Return the key located at key index X; essentially gets the key from a +// `WHERE id = ?`. This is the most efficient way I can think to implement +// this rarely-used (in my experience) part of the API, but it can seem +// inconsistent, because we do `INSERT OR REPLACE INTO` on `setItem()`, so +// the ID of each key will change every time it's updated. Perhaps a stored +// procedure for the `setItem()` SQL would solve this problem? +// TODO: Don't change ID on `setItem()`. +function key$1(n, callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'SELECT key FROM ' + dbInfo.storeName + ' WHERE id = ? LIMIT 1', [n + 1], function (t, results) { + var result = results.rows.length ? results.rows.item(0).key : null; + resolve(result); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); - return result; + executeCallback(promise, callback); + return promise; } -/** - * Tells whether current environment supports ReportingObserver API - * {@link supportsReportingObserver}. - * - * @returns Answer to the given question. - */ -function supportsReportingObserver() { - return 'ReportingObserver' in WINDOW; -} +function keys$1(callback) { + var self = this; -/** - * Tells whether current environment supports Referrer Policy API - * {@link supportsReferrerPolicy}. - * - * @returns Answer to the given question. - */ -function supportsReferrerPolicy() { - // Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default' - // (see https://caniuse.com/#feat=referrer-policy), - // it doesn't. And it throws an exception instead of ignoring this parameter... - // REF: https://github.com/getsentry/raven-js/issues/1233 + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'SELECT key FROM ' + dbInfo.storeName, [], function (t, results) { + var keys = []; - if (!supportsFetch()) { - return false; - } + for (var i = 0; i < results.rows.length; i++) { + keys.push(results.rows.item(i).key); + } - try { - new Request('_', { - referrerPolicy: 'origin' , + resolve(keys); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); }); - return true; - } catch (e) { - return false; - } + + executeCallback(promise, callback); + return promise; } -exports.isNativeFetch = isNativeFetch; -exports.supportsDOMError = supportsDOMError; -exports.supportsDOMException = supportsDOMException; -exports.supportsErrorEvent = supportsErrorEvent; -exports.supportsFetch = supportsFetch; -exports.supportsNativeFetch = supportsNativeFetch; -exports.supportsReferrerPolicy = supportsReferrerPolicy; -exports.supportsReportingObserver = supportsReportingObserver; +// https://www.w3.org/TR/webdatabase/#databases +// > There is no way to enumerate or delete the databases available for an origin from this API. +function getAllStoreNames(db) { + return new Promise$1(function (resolve, reject) { + db.transaction(function (t) { + t.executeSql('SELECT name FROM sqlite_master ' + "WHERE type='table' AND name <> '__WebKitDatabaseInfoTable__'", [], function (t, results) { + var storeNames = []; + for (var i = 0; i < results.rows.length; i++) { + storeNames.push(results.rows.item(i).name); + } -},{"./debug-build.js":133,"./logger.js":151,"./worldwide.js":174}],167:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + resolve({ + db: db, + storeNames: storeNames + }); + }, function (t, error) { + reject(error); + }); + }, function (sqlError) { + reject(sqlError); + }); + }); +} -const is = require('./is.js'); +function dropInstance$1(options, callback) { + callback = getCallback.apply(this, arguments); -/* eslint-disable @typescript-eslint/explicit-function-return-type */ + var currentConfig = this.config(); + options = typeof options !== 'function' && options || {}; + if (!options.name) { + options.name = options.name || currentConfig.name; + options.storeName = options.storeName || currentConfig.storeName; + } -/** SyncPromise internal states */ -var States; (function (States) { - /** Pending */ - const PENDING = 0; States[States["PENDING"] = PENDING] = "PENDING"; - /** Resolved / OK */ - const RESOLVED = 1; States[States["RESOLVED"] = RESOLVED] = "RESOLVED"; - /** Rejected / Error */ - const REJECTED = 2; States[States["REJECTED"] = REJECTED] = "REJECTED"; -})(States || (States = {})); + var self = this; + var promise; + if (!options.name) { + promise = Promise$1.reject('Invalid arguments'); + } else { + promise = new Promise$1(function (resolve) { + var db; + if (options.name === currentConfig.name) { + // use the db reference of the current instance + db = self._dbInfo.db; + } else { + db = openDatabase(options.name, '', '', 0); + } -// Overloads so we can call resolvedSyncPromise without arguments and generic argument + if (!options.storeName) { + // drop all database tables + resolve(getAllStoreNames(db)); + } else { + resolve({ + db: db, + storeNames: [options.storeName] + }); + } + }).then(function (operationInfo) { + return new Promise$1(function (resolve, reject) { + operationInfo.db.transaction(function (t) { + function dropTable(storeName) { + return new Promise$1(function (resolve, reject) { + t.executeSql('DROP TABLE IF EXISTS ' + storeName, [], function () { + resolve(); + }, function (t, error) { + reject(error); + }); + }); + } -/** - * Creates a resolved sync promise. - * - * @param value the value to resolve the promise with - * @returns the resolved sync promise - */ -function resolvedSyncPromise(value) { - return new SyncPromise(resolve => { - resolve(value); - }); + var operations = []; + for (var i = 0, len = operationInfo.storeNames.length; i < len; i++) { + operations.push(dropTable(operationInfo.storeNames[i])); + } + + Promise$1.all(operations).then(function () { + resolve(); + })["catch"](function (e) { + reject(e); + }); + }, function (sqlError) { + reject(sqlError); + }); + }); + }); + } + + executeCallback(promise, callback); + return promise; } -/** - * Creates a rejected sync promise. - * - * @param value the value to reject the promise with - * @returns the rejected sync promise - */ -function rejectedSyncPromise(reason) { - return new SyncPromise((_, reject) => { - reject(reason); - }); +var webSQLStorage = { + _driver: 'webSQLStorage', + _initStorage: _initStorage$1, + _support: isWebSQLValid(), + iterate: iterate$1, + getItem: getItem$1, + setItem: setItem$1, + removeItem: removeItem$1, + clear: clear$1, + length: length$1, + key: key$1, + keys: keys$1, + dropInstance: dropInstance$1 +}; + +function isLocalStorageValid() { + try { + return typeof localStorage !== 'undefined' && 'setItem' in localStorage && + // in IE8 typeof localStorage.setItem === 'object' + !!localStorage.setItem; + } catch (e) { + return false; + } } -/** - * Thenable class that behaves like a Promise and follows it's interface - * but is not async internally - */ -class SyncPromise { +function _getKeyPrefix(options, defaultConfig) { + var keyPrefix = options.name + '/'; - constructor( - executor, - ) {SyncPromise.prototype.__init.call(this);SyncPromise.prototype.__init2.call(this);SyncPromise.prototype.__init3.call(this);SyncPromise.prototype.__init4.call(this); - this._state = States.PENDING; - this._handlers = []; + if (options.storeName !== defaultConfig.storeName) { + keyPrefix += options.storeName + '/'; + } + return keyPrefix; +} + +// Check if localStorage throws when saving an item +function checkIfLocalStorageThrows() { + var localStorageTestKey = '_localforage_support_test'; try { - executor(this._resolve, this._reject); + localStorage.setItem(localStorageTestKey, true); + localStorage.removeItem(localStorageTestKey); + + return false; } catch (e) { - this._reject(e); + return true; } - } +} - /** JSDoc */ - then( - onfulfilled, - onrejected, - ) { - return new SyncPromise((resolve, reject) => { - this._handlers.push([ - false, - result => { - if (!onfulfilled) { - // TODO: ¯\_(ツ)_/¯ - // TODO: FIXME - resolve(result ); - } else { - try { - resolve(onfulfilled(result)); - } catch (e) { - reject(e); - } - } - }, - reason => { - if (!onrejected) { - reject(reason); - } else { - try { - resolve(onrejected(reason)); - } catch (e) { - reject(e); - } - } - }, - ]); - this._executeHandlers(); - }); - } +// Check if localStorage is usable and allows to save an item +// This method checks if localStorage is usable in Safari Private Browsing +// mode, or in any other case where the available quota for localStorage +// is 0 and there wasn't any saved items yet. +function _isLocalStorageUsable() { + return !checkIfLocalStorageThrows() || localStorage.length > 0; +} - /** JSDoc */ - catch( - onrejected, - ) { - return this.then(val => val, onrejected); - } +// Config the localStorage backend, using options set in the config. +function _initStorage$2(options) { + var self = this; + var dbInfo = {}; + if (options) { + for (var i in options) { + dbInfo[i] = options[i]; + } + } - /** JSDoc */ - finally(onfinally) { - return new SyncPromise((resolve, reject) => { - let val; - let isRejected; + dbInfo.keyPrefix = _getKeyPrefix(options, self._defaultConfig); - return this.then( - value => { - isRejected = false; - val = value; - if (onfinally) { - onfinally(); - } - }, - reason => { - isRejected = true; - val = reason; - if (onfinally) { - onfinally(); - } - }, - ).then(() => { - if (isRejected) { - reject(val); - return; - } + if (!_isLocalStorageUsable()) { + return Promise$1.reject(); + } - resolve(val ); - }); + self._dbInfo = dbInfo; + dbInfo.serializer = localforageSerializer; + + return Promise$1.resolve(); +} + +// Remove all keys from the datastore, effectively destroying all data in +// the app's key/value store! +function clear$2(callback) { + var self = this; + var promise = self.ready().then(function () { + var keyPrefix = self._dbInfo.keyPrefix; + + for (var i = localStorage.length - 1; i >= 0; i--) { + var key = localStorage.key(i); + + if (key.indexOf(keyPrefix) === 0) { + localStorage.removeItem(key); + } + } }); - } - /** JSDoc */ - __init() {this._resolve = (value) => { - this._setResult(States.RESOLVED, value); - };} + executeCallback(promise, callback); + return promise; +} - /** JSDoc */ - __init2() {this._reject = (reason) => { - this._setResult(States.REJECTED, reason); - };} +// Retrieve an item from the store. Unlike the original async_storage +// library in Gaia, we don't modify return values at all. If a key's value +// is `undefined`, we pass that value to the callback function. +function getItem$2(key, callback) { + var self = this; - /** JSDoc */ - __init3() {this._setResult = (state, value) => { - if (this._state !== States.PENDING) { - return; - } + key = normalizeKey(key); - if (is.isThenable(value)) { - void (value ).then(this._resolve, this._reject); - return; - } + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var result = localStorage.getItem(dbInfo.keyPrefix + key); - this._state = state; - this._value = value; + // If a result was found, parse it from the serialized + // string into a JS object. If result isn't truthy, the key + // is likely undefined and we'll pass it straight to the + // callback. + if (result) { + result = dbInfo.serializer.deserialize(result); + } - this._executeHandlers(); - };} + return result; + }); - /** JSDoc */ - __init4() {this._executeHandlers = () => { - if (this._state === States.PENDING) { - return; - } + executeCallback(promise, callback); + return promise; +} - const cachedHandlers = this._handlers.slice(); - this._handlers = []; +// Iterate over all items in the store. +function iterate$2(iterator, callback) { + var self = this; - cachedHandlers.forEach(handler => { - if (handler[0]) { - return; - } + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var keyPrefix = dbInfo.keyPrefix; + var keyPrefixLength = keyPrefix.length; + var length = localStorage.length; - if (this._state === States.RESOLVED) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - handler[1](this._value ); - } + // We use a dedicated iterator instead of the `i` variable below + // so other keys we fetch in localStorage aren't counted in + // the `iterationNumber` argument passed to the `iterate()` + // callback. + // + // See: github.com/mozilla/localForage/pull/435#discussion_r38061530 + var iterationNumber = 1; - if (this._state === States.REJECTED) { - handler[2](this._value); - } + for (var i = 0; i < length; i++) { + var key = localStorage.key(i); + if (key.indexOf(keyPrefix) !== 0) { + continue; + } + var value = localStorage.getItem(key); + + // If a result was found, parse it from the serialized + // string into a JS object. If result isn't truthy, the + // key is likely undefined and we'll pass it straight + // to the iterator. + if (value) { + value = dbInfo.serializer.deserialize(value); + } - handler[0] = true; + value = iterator(value, key.substring(keyPrefixLength), iterationNumber++); + + if (value !== void 0) { + return value; + } + } }); - };} + + executeCallback(promise, callback); + return promise; } -exports.SyncPromise = SyncPromise; -exports.rejectedSyncPromise = rejectedSyncPromise; -exports.resolvedSyncPromise = resolvedSyncPromise; +// Same as localStorage's key() method, except takes a callback. +function key$2(n, callback) { + var self = this; + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var result; + try { + result = localStorage.key(n); + } catch (error) { + result = null; + } + // Remove the prefix from the key, if a key is found. + if (result) { + result = result.substring(dbInfo.keyPrefix.length); + } -},{"./is.js":149}],168:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + return result; + }); -const worldwide = require('./worldwide.js'); + executeCallback(promise, callback); + return promise; +} -const ONE_SECOND_IN_MS = 1000; +function keys$2(callback) { + var self = this; + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var length = localStorage.length; + var keys = []; -/** - * A partial definition of the [Performance Web API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Performance} - * for accessing a high-resolution monotonic clock. - */ + for (var i = 0; i < length; i++) { + var itemKey = localStorage.key(i); + if (itemKey.indexOf(dbInfo.keyPrefix) === 0) { + keys.push(itemKey.substring(dbInfo.keyPrefix.length)); + } + } -/** - * Returns a timestamp in seconds since the UNIX epoch using the Date API. - * - * TODO(v8): Return type should be rounded. - */ -function dateTimestampInSeconds() { - return Date.now() / ONE_SECOND_IN_MS; -} + return keys; + }); -/** - * Returns a wrapper around the native Performance API browser implementation, or undefined for browsers that do not - * support the API. - * - * Wrapping the native API works around differences in behavior from different browsers. - */ -function createUnixTimestampInSecondsFunc() { - const { performance } = worldwide.GLOBAL_OBJ ; - if (!performance || !performance.now) { - return dateTimestampInSeconds; - } + executeCallback(promise, callback); + return promise; +} - // Some browser and environments don't have a timeOrigin, so we fallback to - // using Date.now() to compute the starting time. - const approxStartingTimeOrigin = Date.now() - performance.now(); - const timeOrigin = performance.timeOrigin == undefined ? approxStartingTimeOrigin : performance.timeOrigin; +// Supply the number of keys in the datastore to the callback function. +function length$2(callback) { + var self = this; + var promise = self.keys().then(function (keys) { + return keys.length; + }); - // performance.now() is a monotonic clock, which means it starts at 0 when the process begins. To get the current - // wall clock time (actual UNIX timestamp), we need to add the starting time origin and the current time elapsed. - // - // TODO: This does not account for the case where the monotonic clock that powers performance.now() drifts from the - // wall clock time, which causes the returned timestamp to be inaccurate. We should investigate how to detect and - // correct for this. - // See: https://github.com/getsentry/sentry-javascript/issues/2590 - // See: https://github.com/mdn/content/issues/4713 - // See: https://dev.to/noamr/when-a-millisecond-is-not-a-millisecond-3h6 - return () => { - return (timeOrigin + performance.now()) / ONE_SECOND_IN_MS; - }; + executeCallback(promise, callback); + return promise; } -/** - * Returns a timestamp in seconds since the UNIX epoch using either the Performance or Date APIs, depending on the - * availability of the Performance API. - * - * BUG: Note that because of how browsers implement the Performance API, the clock might stop when the computer is - * asleep. This creates a skew between `dateTimestampInSeconds` and `timestampInSeconds`. The - * skew can grow to arbitrary amounts like days, weeks or months. - * See https://github.com/getsentry/sentry-javascript/issues/2590. - */ -const timestampInSeconds = createUnixTimestampInSecondsFunc(); +// Remove an item from the store, nice and simple. +function removeItem$2(key, callback) { + var self = this; -/** - * Re-exported with an old name for backwards-compatibility. - * TODO (v8): Remove this - * - * @deprecated Use `timestampInSeconds` instead. - */ -const timestampWithMs = timestampInSeconds; + key = normalizeKey(key); -/** - * Internal helper to store what is the source of browserPerformanceTimeOrigin below. For debugging only. - */ -exports._browserPerformanceTimeOriginMode = void 0; + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + localStorage.removeItem(dbInfo.keyPrefix + key); + }); -/** - * The number of milliseconds since the UNIX epoch. This value is only usable in a browser, and only when the - * performance API is available. - */ -const browserPerformanceTimeOrigin = (() => { - // Unfortunately browsers may report an inaccurate time origin data, through either performance.timeOrigin or - // performance.timing.navigationStart, which results in poor results in performance data. We only treat time origin - // data as reliable if they are within a reasonable threshold of the current time. + executeCallback(promise, callback); + return promise; +} - const { performance } = worldwide.GLOBAL_OBJ ; - if (!performance || !performance.now) { - exports._browserPerformanceTimeOriginMode = 'none'; - return undefined; - } +// Set a key's value and run an optional callback once the value is set. +// Unlike Gaia's implementation, the callback function is passed the value, +// in case you want to operate on that value only after you're sure it +// saved, or something like that. +function setItem$2(key, value, callback) { + var self = this; - const threshold = 3600 * 1000; - const performanceNow = performance.now(); - const dateNow = Date.now(); + key = normalizeKey(key); - // if timeOrigin isn't available set delta to threshold so it isn't used - const timeOriginDelta = performance.timeOrigin - ? Math.abs(performance.timeOrigin + performanceNow - dateNow) - : threshold; - const timeOriginIsReliable = timeOriginDelta < threshold; + var promise = self.ready().then(function () { + // Convert undefined values to null. + // https://github.com/mozilla/localForage/pull/42 + if (value === undefined) { + value = null; + } - // While performance.timing.navigationStart is deprecated in favor of performance.timeOrigin, performance.timeOrigin - // is not as widely supported. Namely, performance.timeOrigin is undefined in Safari as of writing. - // Also as of writing, performance.timing is not available in Web Workers in mainstream browsers, so it is not always - // a valid fallback. In the absence of an initial time provided by the browser, fallback to the current time from the - // Date API. - // eslint-disable-next-line deprecation/deprecation - const navigationStart = performance.timing && performance.timing.navigationStart; - const hasNavigationStart = typeof navigationStart === 'number'; - // if navigationStart isn't available set delta to threshold so it isn't used - const navigationStartDelta = hasNavigationStart ? Math.abs(navigationStart + performanceNow - dateNow) : threshold; - const navigationStartIsReliable = navigationStartDelta < threshold; + // Save the original value to pass to the callback. + var originalValue = value; + + return new Promise$1(function (resolve, reject) { + var dbInfo = self._dbInfo; + dbInfo.serializer.serialize(value, function (value, error) { + if (error) { + reject(error); + } else { + try { + localStorage.setItem(dbInfo.keyPrefix + key, value); + resolve(originalValue); + } catch (e) { + // localStorage capacity exceeded. + // TODO: Make this a specific error/event. + if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') { + reject(e); + } + reject(e); + } + } + }); + }); + }); - if (timeOriginIsReliable || navigationStartIsReliable) { - // Use the more reliable time origin - if (timeOriginDelta <= navigationStartDelta) { - exports._browserPerformanceTimeOriginMode = 'timeOrigin'; - return performance.timeOrigin; + executeCallback(promise, callback); + return promise; +} + +function dropInstance$2(options, callback) { + callback = getCallback.apply(this, arguments); + + options = typeof options !== 'function' && options || {}; + if (!options.name) { + var currentConfig = this.config(); + options.name = options.name || currentConfig.name; + options.storeName = options.storeName || currentConfig.storeName; + } + + var self = this; + var promise; + if (!options.name) { + promise = Promise$1.reject('Invalid arguments'); } else { - exports._browserPerformanceTimeOriginMode = 'navigationStart'; - return navigationStart; + promise = new Promise$1(function (resolve) { + if (!options.storeName) { + resolve(options.name + '/'); + } else { + resolve(_getKeyPrefix(options, self._defaultConfig)); + } + }).then(function (keyPrefix) { + for (var i = localStorage.length - 1; i >= 0; i--) { + var key = localStorage.key(i); + + if (key.indexOf(keyPrefix) === 0) { + localStorage.removeItem(key); + } + } + }); } - } - // Either both timeOrigin and navigationStart are skewed or neither is available, fallback to Date. - exports._browserPerformanceTimeOriginMode = 'dateNow'; - return dateNow; -})(); + executeCallback(promise, callback); + return promise; +} -exports.browserPerformanceTimeOrigin = browserPerformanceTimeOrigin; -exports.dateTimestampInSeconds = dateTimestampInSeconds; -exports.timestampInSeconds = timestampInSeconds; -exports.timestampWithMs = timestampWithMs; +var localStorageWrapper = { + _driver: 'localStorageWrapper', + _initStorage: _initStorage$2, + _support: isLocalStorageValid(), + iterate: iterate$2, + getItem: getItem$2, + setItem: setItem$2, + removeItem: removeItem$2, + clear: clear$2, + length: length$2, + key: key$2, + keys: keys$2, + dropInstance: dropInstance$2 +}; +var sameValue = function sameValue(x, y) { + return x === y || typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y); +}; -},{"./worldwide.js":174}],169:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); +var includes = function includes(array, searchElement) { + var len = array.length; + var i = 0; + while (i < len) { + if (sameValue(array[i], searchElement)) { + return true; + } + i++; + } -const baggage = require('./baggage.js'); -const misc = require('./misc.js'); + return false; +}; -// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp is used for readability here -const TRACEPARENT_REGEXP = new RegExp( - '^[ \\t]*' + // whitespace - '([0-9a-f]{32})?' + // trace_id - '-?([0-9a-f]{16})?' + // span_id - '-?([01])?' + // sampled - '[ \\t]*$', // whitespace -); +var isArray = Array.isArray || function (arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; +}; -/** - * Extract transaction context data from a `sentry-trace` header. - * - * @param traceparent Traceparent string - * - * @returns Object containing data from the header, or undefined if traceparent string is malformed - */ -function extractTraceparentData(traceparent) { - if (!traceparent) { - return undefined; - } +// Drivers are stored here when `defineDriver()` is called. +// They are shared across all instances of localForage. +var DefinedDrivers = {}; - const matches = traceparent.match(TRACEPARENT_REGEXP); - if (!matches) { - return undefined; - } +var DriverSupport = {}; - let parentSampled; - if (matches[3] === '1') { - parentSampled = true; - } else if (matches[3] === '0') { - parentSampled = false; - } +var DefaultDrivers = { + INDEXEDDB: asyncStorage, + WEBSQL: webSQLStorage, + LOCALSTORAGE: localStorageWrapper +}; - return { - traceId: matches[1], - parentSampled, - parentSpanId: matches[2], - }; -} +var DefaultDriverOrder = [DefaultDrivers.INDEXEDDB._driver, DefaultDrivers.WEBSQL._driver, DefaultDrivers.LOCALSTORAGE._driver]; -/** - * Create tracing context from incoming headers. - * - * @deprecated Use `propagationContextFromHeaders` instead. - */ -// TODO(v8): Remove this function -function tracingContextFromHeaders( - sentryTrace, - baggage$1, -) +var OptionalDriverMethods = ['dropInstance']; - { - const traceparentData = extractTraceparentData(sentryTrace); - const dynamicSamplingContext = baggage.baggageHeaderToDynamicSamplingContext(baggage$1); +var LibraryMethods = ['clear', 'getItem', 'iterate', 'key', 'keys', 'length', 'removeItem', 'setItem'].concat(OptionalDriverMethods); - const { traceId, parentSpanId, parentSampled } = traceparentData || {}; +var DefaultConfig = { + description: '', + driver: DefaultDriverOrder.slice(), + name: 'localforage', + // Default DB size is _JUST UNDER_ 5MB, as it's the highest size + // we can use without a prompt. + size: 4980736, + storeName: 'keyvaluepairs', + version: 1.0 +}; - if (!traceparentData) { - return { - traceparentData, - dynamicSamplingContext: undefined, - propagationContext: { - traceId: traceId || misc.uuid4(), - spanId: misc.uuid4().substring(16), - }, - }; - } else { - return { - traceparentData, - dynamicSamplingContext: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it - propagationContext: { - traceId: traceId || misc.uuid4(), - parentSpanId: parentSpanId || misc.uuid4().substring(16), - spanId: misc.uuid4().substring(16), - sampled: parentSampled, - dsc: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it - }, +function callWhenReady(localForageInstance, libraryMethod) { + localForageInstance[libraryMethod] = function () { + var _args = arguments; + return localForageInstance.ready().then(function () { + return localForageInstance[libraryMethod].apply(localForageInstance, _args); + }); }; - } } -/** - * Create a propagation context from incoming headers. - */ -function propagationContextFromHeaders( - sentryTrace, - baggage$1, -) { - const traceparentData = extractTraceparentData(sentryTrace); - const dynamicSamplingContext = baggage.baggageHeaderToDynamicSamplingContext(baggage$1); +function extend() { + for (var i = 1; i < arguments.length; i++) { + var arg = arguments[i]; - const { traceId, parentSpanId, parentSampled } = traceparentData || {}; + if (arg) { + for (var _key in arg) { + if (arg.hasOwnProperty(_key)) { + if (isArray(arg[_key])) { + arguments[0][_key] = arg[_key].slice(); + } else { + arguments[0][_key] = arg[_key]; + } + } + } + } + } - if (!traceparentData) { - return { - traceId: traceId || misc.uuid4(), - spanId: misc.uuid4().substring(16), - }; - } else { - return { - traceId: traceId || misc.uuid4(), - parentSpanId: parentSpanId || misc.uuid4().substring(16), - spanId: misc.uuid4().substring(16), - sampled: parentSampled, - dsc: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it - }; - } + return arguments[0]; } -/** - * Create sentry-trace header from span context values. - */ -function generateSentryTraceHeader( - traceId = misc.uuid4(), - spanId = misc.uuid4().substring(16), - sampled, -) { - let sampledString = ''; - if (sampled !== undefined) { - sampledString = sampled ? '-1' : '-0'; - } - return `${traceId}-${spanId}${sampledString}`; -} +var LocalForage = function () { + function LocalForage(options) { + _classCallCheck(this, LocalForage); -exports.TRACEPARENT_REGEXP = TRACEPARENT_REGEXP; -exports.extractTraceparentData = extractTraceparentData; -exports.generateSentryTraceHeader = generateSentryTraceHeader; -exports.propagationContextFromHeaders = propagationContextFromHeaders; -exports.tracingContextFromHeaders = tracingContextFromHeaders; + for (var driverTypeKey in DefaultDrivers) { + if (DefaultDrivers.hasOwnProperty(driverTypeKey)) { + var driver = DefaultDrivers[driverTypeKey]; + var driverName = driver._driver; + this[driverTypeKey] = driverName; + if (!DefinedDrivers[driverName]) { + // we don't need to wait for the promise, + // since the default drivers can be defined + // in a blocking manner + this.defineDriver(driver); + } + } + } -},{"./baggage.js":122,"./misc.js":154}],170:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + this._defaultConfig = extend({}, DefaultConfig); + this._config = extend({}, this._defaultConfig, options); + this._driverSet = null; + this._initDriver = null; + this._ready = false; + this._dbInfo = null; -/** - * Parses string form of URL into an object - * // borrowed from https://tools.ietf.org/html/rfc3986#appendix-B - * // intentionally using regex and not href parsing trick because React Native and other - * // environments where DOM might not be available - * @returns parsed URL object - */ -function parseUrl(url) { - if (!url) { - return {}; - } + this._wrapLibraryMethodsWithReady(); + this.setDriver(this._config.driver)["catch"](function () {}); + } - const match = url.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/); + // Set any config values for localForage; can be called anytime before + // the first API call (e.g. `getItem`, `setItem`). + // We loop through options so we don't overwrite existing config + // values. - if (!match) { - return {}; - } - // coerce to undefined values to empty string so we don't get 'undefined' - const query = match[6] || ''; - const fragment = match[8] || ''; - return { - host: match[4], - path: match[5], - protocol: match[2], - search: query, - hash: fragment, - relative: match[5] + query + fragment, // everything minus origin - }; -} + LocalForage.prototype.config = function config(options) { + // If the options argument is an object, we use it to set values. + // Otherwise, we return either a specified config value or all + // config values. + if ((typeof options === 'undefined' ? 'undefined' : _typeof(options)) === 'object') { + // If localforage is ready and fully initialized, we can't set + // any new configuration values. Instead, we return an error. + if (this._ready) { + return new Error("Can't call config() after localforage " + 'has been used.'); + } -/** - * Strip the query string and fragment off of a given URL or path (if present) - * - * @param urlPath Full URL or path, including possible query string and/or fragment - * @returns URL or path without query string or fragment - */ -function stripUrlQueryAndFragment(urlPath) { - // eslint-disable-next-line no-useless-escape - return urlPath.split(/[\?#]/, 1)[0]; -} + for (var i in options) { + if (i === 'storeName') { + options[i] = options[i].replace(/\W/g, '_'); + } -/** - * Returns number of URL segments of a passed string URL. - */ -function getNumberOfUrlSegments(url) { - // split at '/' or at '\/' to split regex urls correctly - return url.split(/\\?\//).filter(s => s.length > 0 && s !== ',').length; -} + if (i === 'version' && typeof options[i] !== 'number') { + return new Error('Database version must be a number.'); + } -/** - * Takes a URL object and returns a sanitized string which is safe to use as span description - * see: https://develop.sentry.dev/sdk/data-handling/#structuring-data - */ -function getSanitizedUrlString(url) { - const { protocol, host, path } = url; + this._config[i] = options[i]; + } - const filteredHost = - (host && - host - // Always filter out authority - .replace(/^.*@/, '[filtered]:[filtered]@') - // Don't show standard :80 (http) and :443 (https) ports to reduce the noise - // TODO: Use new URL global if it exists - .replace(/(:80)$/, '') - .replace(/(:443)$/, '')) || - ''; + // after all config options are set and + // the driver option is used, try setting it + if ('driver' in options && options.driver) { + return this.setDriver(this._config.driver); + } - return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`; -} + return true; + } else if (typeof options === 'string') { + return this._config[options]; + } else { + return this._config; + } + }; -exports.getNumberOfUrlSegments = getNumberOfUrlSegments; -exports.getSanitizedUrlString = getSanitizedUrlString; -exports.parseUrl = parseUrl; -exports.stripUrlQueryAndFragment = stripUrlQueryAndFragment; + // Used to define a custom driver, shared across all instances of + // localForage. -},{}],171:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + LocalForage.prototype.defineDriver = function defineDriver(driverObject, callback, errorCallback) { + var promise = new Promise$1(function (resolve, reject) { + try { + var driverName = driverObject._driver; + var complianceError = new Error('Custom driver not compliant; see ' + 'https://mozilla.github.io/localForage/#definedriver'); -/** - * Recursively traverses an object to update an existing nested key. - * Note: The provided key path must include existing properties, - * the function will not create objects while traversing. - * - * @param obj An object to update - * @param value The value to update the nested key with - * @param keyPath The path to the key to update ex. fizz.buzz.foo - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function setNestedKey(obj, keyPath, value) { - // Ex. foo.bar.zoop will extract foo and bar.zoop - const match = keyPath.match(/([a-z_]+)\.(.*)/i); - // The match will be null when there's no more recursing to do, i.e., when we've reached the right level of the object - if (match === null) { - obj[keyPath] = value; - } else { - // `match[1]` is the initial segment of the path, and `match[2]` is the remainder of the path - const innerObj = obj[match[1]]; - setNestedKey(innerObj, match[2], value); - } -} + // A driver name should be defined and not overlap with the + // library-defined, default drivers. + if (!driverObject._driver) { + reject(complianceError); + return; + } -/** - * Enforces inclusion of a given integration with specified options in an integration array originally determined by the - * user, by either including the given default instance or by patching an existing user instance with the given options. - * - * Ideally this would happen when integrations are set up, but there isn't currently a mechanism there for merging - * options from a default integration instance with those from a user-provided instance of the same integration, only - * for allowing the user to override a default instance entirely. (TODO: Fix that.) - * - * @param defaultIntegrationInstance An instance of the integration with the correct options already set - * @param userIntegrations Integrations defined by the user. - * @param forcedOptions Options with which to patch an existing user-derived instance on the integration. - * @returns A final integrations array. - * - * @deprecated This will be removed in v8. - */ -function addOrUpdateIntegration( - defaultIntegrationInstance, - userIntegrations, - forcedOptions = {}, -) { - return ( - Array.isArray(userIntegrations) - ? addOrUpdateIntegrationInArray(defaultIntegrationInstance, userIntegrations, forcedOptions) - : addOrUpdateIntegrationInFunction( - defaultIntegrationInstance, - // Somehow TS can't figure out that not being an array makes this necessarily a function - userIntegrations , - forcedOptions, - ) - ) ; -} + var driverMethods = LibraryMethods.concat('_initStorage'); + for (var i = 0, len = driverMethods.length; i < len; i++) { + var driverMethodName = driverMethods[i]; -function addOrUpdateIntegrationInArray( - defaultIntegrationInstance, - userIntegrations, - forcedOptions, -) { - const userInstance = userIntegrations.find(integration => integration.name === defaultIntegrationInstance.name); + // when the property is there, + // it should be a method even when optional + var isRequired = !includes(OptionalDriverMethods, driverMethodName); + if ((isRequired || driverObject[driverMethodName]) && typeof driverObject[driverMethodName] !== 'function') { + reject(complianceError); + return; + } + } - if (userInstance) { - for (const [keyPath, value] of Object.entries(forcedOptions)) { - setNestedKey(userInstance, keyPath, value); - } + var configureMissingMethods = function configureMissingMethods() { + var methodNotImplementedFactory = function methodNotImplementedFactory(methodName) { + return function () { + var error = new Error('Method ' + methodName + ' is not implemented by the current driver'); + var promise = Promise$1.reject(error); + executeCallback(promise, arguments[arguments.length - 1]); + return promise; + }; + }; - return userIntegrations; - } + for (var _i = 0, _len = OptionalDriverMethods.length; _i < _len; _i++) { + var optionalDriverMethod = OptionalDriverMethods[_i]; + if (!driverObject[optionalDriverMethod]) { + driverObject[optionalDriverMethod] = methodNotImplementedFactory(optionalDriverMethod); + } + } + }; - return [...userIntegrations, defaultIntegrationInstance]; -} + configureMissingMethods(); -function addOrUpdateIntegrationInFunction( - defaultIntegrationInstance, - userIntegrationsFunc, - forcedOptions, -) { - const wrapper = defaultIntegrations => { - const userFinalIntegrations = userIntegrationsFunc(defaultIntegrations); + var setDriverSupport = function setDriverSupport(support) { + if (DefinedDrivers[driverName]) { + console.info('Redefining LocalForage driver: ' + driverName); + } + DefinedDrivers[driverName] = driverObject; + DriverSupport[driverName] = support; + // don't use a then, so that we can define + // drivers that have simple _support methods + // in a blocking manner + resolve(); + }; - // There are instances where we want the user to be able to prevent an integration from appearing at all, which they - // would do by providing a function which filters out the integration in question. If that's happened in one of - // those cases, don't add our default back in. - if (defaultIntegrationInstance.allowExclusionByUser) { - const userFinalInstance = userFinalIntegrations.find( - integration => integration.name === defaultIntegrationInstance.name, - ); - if (!userFinalInstance) { - return userFinalIntegrations; - } - } + if ('_support' in driverObject) { + if (driverObject._support && typeof driverObject._support === 'function') { + driverObject._support().then(setDriverSupport, reject); + } else { + setDriverSupport(!!driverObject._support); + } + } else { + setDriverSupport(true); + } + } catch (e) { + reject(e); + } + }); - return addOrUpdateIntegrationInArray(defaultIntegrationInstance, userFinalIntegrations, forcedOptions); - }; + executeTwoCallbacks(promise, callback, errorCallback); + return promise; + }; - return wrapper; -} + LocalForage.prototype.driver = function driver() { + return this._driver || null; + }; -exports.addOrUpdateIntegration = addOrUpdateIntegration; + LocalForage.prototype.getDriver = function getDriver(driverName, callback, errorCallback) { + var getDriverPromise = DefinedDrivers[driverName] ? Promise$1.resolve(DefinedDrivers[driverName]) : Promise$1.reject(new Error('Driver not found.')); + executeTwoCallbacks(getDriverPromise, callback, errorCallback); + return getDriverPromise; + }; -},{}],172:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + LocalForage.prototype.getSerializer = function getSerializer(callback) { + var serializerPromise = Promise$1.resolve(localforageSerializer); + executeTwoCallbacks(serializerPromise, callback); + return serializerPromise; + }; -// Based on https://github.com/sindresorhus/escape-string-regexp but with modifications to: -// a) reduce the size by skipping the runtime type - checking -// b) ensure it gets down - compiled for old versions of Node(the published package only supports Node 12+). -// -// MIT License -// -// Copyright (c) Sindre Sorhus (https://sindresorhus.com) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files(the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of -// the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -// IN THE SOFTWARE. + LocalForage.prototype.ready = function ready(callback) { + var self = this; -/** - * Given a string, escape characters which have meaning in the regex grammar, such that the result is safe to feed to - * `new RegExp()`. - * - * @param regexString The string to escape - * @returns An version of the string with all special regex characters escaped - */ -function escapeStringForRegex(regexString) { - // escape the hyphen separately so we can also replace it with a unicode literal hyphen, to avoid the problems - // discussed in https://github.com/sindresorhus/escape-string-regexp/issues/20. - return regexString.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d'); -} + var promise = self._driverSet.then(function () { + if (self._ready === null) { + self._ready = self._initDriver(); + } -exports.escapeStringForRegex = escapeStringForRegex; + return self._ready; + }); + executeTwoCallbacks(promise, callback, callback); + return promise; + }; -},{}],173:[function(require,module,exports){ -Object.defineProperty(exports, '__esModule', { value: true }); + LocalForage.prototype.setDriver = function setDriver(drivers, callback, errorCallback) { + var self = this; -const worldwide = require('../worldwide.js'); + if (!isArray(drivers)) { + drivers = [drivers]; + } -// Based on https://github.com/angular/angular.js/pull/13945/files + var supportedDrivers = this._getSupportedDrivers(drivers); -// eslint-disable-next-line deprecation/deprecation -const WINDOW = worldwide.getGlobalObject(); + function setDriverToConfig() { + self._config.driver = self.driver(); + } -/** - * Tells whether current environment supports History API - * {@link supportsHistory}. - * - * @returns Answer to the given question. - */ -function supportsHistory() { - // NOTE: in Chrome App environment, touching history.pushState, *even inside - // a try/catch block*, will cause Chrome to output an error to console.error - // borrowed from: https://github.com/angular/angular.js/pull/13945/files - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const chromeVar = (WINDOW ).chrome; - const isChromePackagedApp = chromeVar && chromeVar.app && chromeVar.app.runtime; - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ - const hasHistoryApi = 'history' in WINDOW && !!WINDOW.history.pushState && !!WINDOW.history.replaceState; + function extendSelfWithDriver(driver) { + self._extend(driver); + setDriverToConfig(); - return !isChromePackagedApp && hasHistoryApi; -} + self._ready = self._initStorage(self._config); + return self._ready; + } -exports.supportsHistory = supportsHistory; + function initDriver(supportedDrivers) { + return function () { + var currentDriverIndex = 0; + function driverPromiseLoop() { + while (currentDriverIndex < supportedDrivers.length) { + var driverName = supportedDrivers[currentDriverIndex]; + currentDriverIndex++; -},{"../worldwide.js":174}],174:[function(require,module,exports){ -(function (global){(function (){ -Object.defineProperty(exports, '__esModule', { value: true }); + self._dbInfo = null; + self._ready = null; -/** Internal global with common properties and Sentry extensions */ + return self.getDriver(driverName).then(extendSelfWithDriver)["catch"](driverPromiseLoop); + } -// The code below for 'isGlobalObj' and 'GLOBAL_OBJ' was copied from core-js before modification -// https://github.com/zloirock/core-js/blob/1b944df55282cdc99c90db5f49eb0b6eda2cc0a3/packages/core-js/internals/global.js -// core-js has the following licence: -// -// Copyright (c) 2014-2022 Denis Pushkarev -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. + setDriverToConfig(); + var error = new Error('No available storage method found.'); + self._driverSet = Promise$1.reject(error); + return self._driverSet; + } -/** Returns 'obj' if it's the global object, otherwise returns undefined */ -function isGlobalObj(obj) { - return obj && obj.Math == Math ? obj : undefined; -} + return driverPromiseLoop(); + }; + } -/** Get's the global object for the current JavaScript runtime */ -const GLOBAL_OBJ = - (typeof globalThis == 'object' && isGlobalObj(globalThis)) || - // eslint-disable-next-line no-restricted-globals - (typeof window == 'object' && isGlobalObj(window)) || - (typeof self == 'object' && isGlobalObj(self)) || - (typeof global == 'object' && isGlobalObj(global)) || - (function () { - return this; - })() || - {}; + // There might be a driver initialization in progress + // so wait for it to finish in order to avoid a possible + // race condition to set _dbInfo + var oldDriverSetDone = this._driverSet !== null ? this._driverSet["catch"](function () { + return Promise$1.resolve(); + }) : Promise$1.resolve(); + + this._driverSet = oldDriverSetDone.then(function () { + var driverName = supportedDrivers[0]; + self._dbInfo = null; + self._ready = null; + + return self.getDriver(driverName).then(function (driver) { + self._driver = driver._driver; + setDriverToConfig(); + self._wrapLibraryMethodsWithReady(); + self._initDriver = initDriver(supportedDrivers); + }); + })["catch"](function () { + setDriverToConfig(); + var error = new Error('No available storage method found.'); + self._driverSet = Promise$1.reject(error); + return self._driverSet; + }); -/** - * @deprecated Use GLOBAL_OBJ instead or WINDOW from @sentry/browser. This will be removed in v8 - */ -function getGlobalObject() { - return GLOBAL_OBJ ; -} + executeTwoCallbacks(this._driverSet, callback, errorCallback); + return this._driverSet; + }; -/** - * Returns a global singleton contained in the global `__SENTRY__` object. - * - * If the singleton doesn't already exist in `__SENTRY__`, it will be created using the given factory - * function and added to the `__SENTRY__` object. - * - * @param name name of the global singleton on __SENTRY__ - * @param creator creator Factory function to create the singleton if it doesn't already exist on `__SENTRY__` - * @param obj (Optional) The global object on which to look for `__SENTRY__`, if not `GLOBAL_OBJ`'s return value - * @returns the singleton - */ -function getGlobalSingleton(name, creator, obj) { - const gbl = (obj || GLOBAL_OBJ) ; - const __SENTRY__ = (gbl.__SENTRY__ = gbl.__SENTRY__ || {}); - const singleton = __SENTRY__[name] || (__SENTRY__[name] = creator()); - return singleton; -} + LocalForage.prototype.supports = function supports(driverName) { + return !!DriverSupport[driverName]; + }; -exports.GLOBAL_OBJ = GLOBAL_OBJ; -exports.getGlobalObject = getGlobalObject; -exports.getGlobalSingleton = getGlobalSingleton; + LocalForage.prototype._extend = function _extend(libraryMethodsAndProperties) { + extend(this, libraryMethodsAndProperties); + }; + LocalForage.prototype._getSupportedDrivers = function _getSupportedDrivers(drivers) { + var supportedDrivers = []; + for (var i = 0, len = drivers.length; i < len; i++) { + var driverName = drivers[i]; + if (this.supports(driverName)) { + supportedDrivers.push(driverName); + } + } + return supportedDrivers; + }; + + LocalForage.prototype._wrapLibraryMethodsWithReady = function _wrapLibraryMethodsWithReady() { + // Add a stub for each driver API method that delays the call to the + // corresponding driver method until localForage is ready. These stubs + // will be replaced by the driver methods as soon as the driver is + // loaded, so there is no performance impact. + for (var i = 0, len = LibraryMethods.length; i < len; i++) { + callWhenReady(this, LibraryMethods[i]); + } + }; + + LocalForage.prototype.createInstance = function createInstance(options) { + return new LocalForage(options); + }; + + return LocalForage; +}(); + +// The actual localForage object that we expose as a module or via a +// global. It's extended by pulling in one of our other libraries. + + +var localforage_js = new LocalForage(); + +module.exports = localforage_js; + +},{"3":3}]},{},[4])(4) +}); }).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{}],175:[function(require,module,exports){ +},{}],189:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {};