From 71f6fb42f4aa7862768eaf46f3f9de68b1f360f9 Mon Sep 17 00:00:00 2001 From: Jay Khatri Date: Mon, 14 Dec 2020 21:46:53 -0500 Subject: [PATCH] [HIG] add Highlight features for rrweb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parent 5992f03c844fcb00268fe1b6c49648f701116e59 author Vadim Korolik 1608000413 -0700 committer Vadim Korolik 1666033419 -0700 gpgsig -----BEGIN PGP SIGNATURE----- iQIyBAABCgAdFiEE5qJt+0KcVgDpzp9NmG2rb3pHHDQFAmNNpwsACgkQmG2rb3pH HDT+Qg/2Lwz671fehqmv/yC4LksqoSVdFH1JG0e61qKEaUqaCGST/c0GqUpHiI7D Y3yFTlhatbWqaDV+bpJ/9KzB3cRtOhV6URbw5xLrhsR3pGEzNqv4c+ZkaWR2U32k pSFidY7MZTJURbEosOR0cXc+WhHfiVEgzJ6/ny6e4r7zY6gBbE66DLVRNelhMwW7 wdpwzh4KcfLroOecA49skgvTj7xO3Bv3B5X2Ok2Rch18WzWiFDDVYFdIiArf7Kua /gIn1AybT6B6nb4kl99H763zATHLXKdWeLS9nFySGG73ILVpaefMS5cHZnmvmw10 6hltrcYpcZTbPJCV9npFUS+1ViSTK8FpegBVtlUZj5+ZhL3I+McBYyzjgrv/67vc fJKkVCgtgkEgbHHwouw4KnsyRZLcb4/0krOhyqdEKHxmEy+nNvIQ6OVyw4+vYW5B DN84bQ9wWC/p5wEdhJoEz7o42o2UYboGW9Gmzpsef7NaK0MUZr/XHMonOvwL9Yd+ STe9WjkVgWs3Ah6WQrXyYHDsxyn/PXCquSQK1qfnj5qSa3Aajrn1MHVYwNN1IdmL 4+Yn+JQe43Ahb3BEUVuD1w0V3cZs15UoHfKbNuc1VGKfmbvztu/48gwvUCE/BgkG E33iWf+6LcSjQJvEM8HHsBmMq64QOP2Tnsklfn/WJMZF41Pvbg== =P0Tg -----END PGP SIGNATURE----- add typings change change more changes to release change change try again try again try again try again change change change v0.9.12 change change added npm ignore change change chagne change change change change change Preprocess inactive segments Cleaned up code Renamed, and removed generated file changes change change change bump version delete old files change add typings change try again change another change change change change change Undef handling for intervals Another check Add custom build command (#21) Switch to version `1.0.4` of `rrweb-snapshot` (#23) Added support for inactive threshold (#20) * Added support for inactive threshold * Updated version num * Prettified * Fixed names * Fixed name * Renamed Missed an edge case where the last part of a session is inactive, and counted that as active. (#24) * Added support for inactive threshold * Updated version num * Prettified * Fixed names * Fixed name * Renamed * Cleaned code and added handling for edge cases * Updated package num Anthony/hig 156 change the color style of the cursor to (#25) * Added support for inactive threshold * Updated version num * Prettified * Fixed names * Fixed name * Renamed * Cleaned code and added handling for edge cases * Updated package num * More skip customize, changed cursor * Update package version * Changed to use webpack instead of rollup Co-authored-by: Jay Khatri * Fixed files * Added missing styles from CDN Co-authored-by: Jay Khatri Anthony/hig 190 move rrweb getactivityinterval logic (#27) * Added support for inactive threshold * Updated version num * Prettified * Fixed names * Fixed name * Renamed * Cleaned code and added handling for edge cases * Updated package num * More skip customize, changed cursor * Update package version * Changed to use webpack instead of rollup Co-authored-by: Jay Khatri * Fixed files * Added missing styles from CDN * Moved activity intervals calculation * Updated version Co-authored-by: Jay Khatri Anthony/hig 190 move rrweb getactivityinterval logic (#28) * Added support for inactive threshold * Updated version num * Prettified * Fixed names * Fixed name * Renamed * Cleaned code and added handling for edge cases * Updated package num * More skip customize, changed cursor * Update package version * Changed to use webpack instead of rollup Co-authored-by: Jay Khatri * Fixed files * Added missing styles from CDN * Moved activity intervals calculation * Updated version * Only calculate intervals once * Updated version Co-authored-by: Jay Khatri john/hig-231-block-recording-on-all-text-nodes (#30) * Adds text randomization * Update blocked styles * Add privacy mode * Don't record images * v0.10.0 * Rename flag to enableStrictPrivacy * Set redacted style on replayer side Consider allevents for inactivity (#31) * Consider allevents for inactivity * Remove log * skip inactive Don't record value attribute on password inputs (#32) * Don't record password * v0.10.2 Redact the value attribute on mutations (#33) * Redact the value attribute on mutations * v0.10.3 Added 2s delay (#34) * Added 2s delay * updated version * Changed back to fast_forwarding Update rrweb (#35) * fix: sometimes currentTime is smaller than the totalTime when player is finished (#445) plus: fix the problem that sometimes return value of getCurrentTime() is negative * Fix broken link to design docs * Update to fflate (#448) * Update to fflate * Update docs, bundler config * Scroll replayer iframe on firstFullsnapshot (#451) * upgrade snapshot * Release 0.9.12 * Protect against generation of no-change viewport resize events. (#454) I noticed 8 or 10 of these events being generated in a multi-tab browsing session on Chrome 87.0 on Win10. I'm speculating they were generated as a side effect of changing tabs but I can't recreate * fix #452 check isBlocked on add mutation's target * Release 0.9.13 * let mouse tail duration respect timer speed * clean addList when meet a corner case * fix #460 ignore added node that are not in document anymore * upgrade 0.9.14 * Release 0.9.14 * Tweaks to timings to get tests passing on my dev laptop (#466) * Tweaks to timings to get tests passing on my dev laptop - hopefully this makes tests more deterministic * Okay understand what's going on now that the test has run in the travis environment * fix #469 try to get original MutationObserver We found Angular's zone module will patch MutationObserver which make the browser hang in some scenarios. Reference: angular/angular#26948 * Discovered that the common case of mouse movement or scrolling happening during `takeFullSnapshot` was causing mutations to be immediately emitted, contrary to the goal of https://github.com/rrweb-io/rrweb/pull/385 (#470) * Don't remove the style attributes altogether from tests; they are an important part of the mutations (#468) These were removed in https://github.com/rrweb-io/rrweb/commit/8ed1c999cff657c84cdcb88a14ff977c573485a6 in order to smooth over differences in test environments so have maintained that by converting pixel values to 'Npx' (could also try rounding, but didn't attempt that) * read __rrMutationObserver from window * update guide (#483) * Fix RangeError: Maximum call stack size exceeded (#479) Saw this line cause issues in production, causing the following error: ``` RangeError Maximum call stack size exceeded ``` I believe this is caused by javascript engine max argument length - see note from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply#using_apply_and_built-in_functions > The consequences of applying a function with too many arguments (that is, more than tens of thousands of arguments) varies across engines. (The JavaScriptCore engine has hard-coded argument limit of 65536. * Impl record iframe (#481) * Impl record iframe * iframe observe * temp: add bundle file to git * update bundle * update with pick * update bundle * fix fragment map remove * feat: add an option to determine whether to pause CSS animation when playback is paused (#428) set pauseAnimation to true by default * fix: elements would lose some states like scroll position because of "virtual parent" optimization (#427) * fix: elements would lose some state like scroll position because of "virtual parent" optimization * refactor: the bugfix code bug: elements would lose some state like scroll position because of "virtual parent" optimization * fix: an error occured at applyMutation(remove nodes part) error message: Uncaught (in promise) DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node * pick fixes * revert ignore file * re-impl iframe record * re-impl iframe replay * code housekeeping * move multi layer dimension calculation to replay side * update test cases * teardown test server * upgrade rrweb-snapshot with iframe load timeout Co-authored-by: Lucky Feng * remove debugging warning (#486) I can't see a reason for the warning here so believe it's a debugging statement that crept in? * Add prettier as a dependency (#487) * start impl rrdom * upgrade rrweb-snapshot to 1.0.7 * Adding prepare npm statement (#490) * added prepare statement * using master rrweb snapshot Co-authored-by: filip slatinac * Added mousemoveCallback threshold option to sampling config. (#492) * Added mousemoveCallback threshold option to sampling config. * Added mousemoveCallback to definitions file. * Add yarn support for installing unreleased rrweb as a dependency (#497) * Use prepack instead of prepare for yarn support * add prepare and prepack for yarn v1 & v2 compatibility * Create .npmignore * update guide * close #501 do not count attach iframe event in checkout * close #491 check whether link node is head * update test snapshot * fix lint errors * add hiring link * impl #507 export takeFullSnapshot as a public API * Update observer.md (#504) Fixed some grammatical errors * add an experiment config to set max speed in fast forward * Handle event undefined in initMoveObserver (#515) * fix: errors of replaying iframe records (#520) * fix: errors of replaying iframe records error1: HierarchyRequestError: Failed to execute 'appendChild' on 'Node': Nodes of type '#document' may not be inserted inside nodes of type '#document-fragment'. code: parent.appendChild(target) error2: Uncaught DOMException: Failed to execute 'appendChild' on 'Node': Only one element on document allowed. code: parent.appendChild(target); * improve the comment for bugfix * rename node_modules in es bundle to ext * fix: inaccurate mouse position (#522) 1. Position of mouse was inaccurate when replaying and this PR will fix it. 2. Fix the bug that if one nested iframe has a scale transform and the position of mouse was inaccurate as well. * impl shadow DOM manager part of #38 1. observe DOM mutations in shadow DOM 2. rebuild DOM mutations in shadow DOM * Fix docs to point to correct event format (#523) * Fix docs to point to correct event attribute * Update customize-replayer.zh_CN.md * correct event object in guide * Update guide.zh_CN.md * Update snapshot to Release 1.1.1 Co-authored-by: Lucky Feng Co-authored-by: Fanis Katsimpas Co-authored-by: Lucky Feng Co-authored-by: 101arrowz Co-authored-by: Jarosław Salwa Co-authored-by: Yanzhen Yu Co-authored-by: Eoghan Murray Co-authored-by: zzq0826 <770166635@qq.com> Co-authored-by: Karl-Aksel Puulmann Co-authored-by: Moji Izadmehr Co-authored-by: Filip Slatinac Co-authored-by: filip slatinac Co-authored-by: Province Innovation <69924001+provinceinnovation@users.noreply.github.com> Co-authored-by: Justin Halsall Co-authored-by: Season Co-authored-by: arshabh-copods <77658085+arshabh-copods@users.noreply.github.com> Co-authored-by: Yakko Majuri <38760734+yakkomajuri@users.noreply.github.com> Disable onAddHoverClass and duplicate full snapshot (#36) * Don't rebuild full snapshot * Add config for hover class * Revert "Update rrweb (#35)" This reverts commit 5dc4ca2fe74c5c781707ed6e6d7416d1835214cd. * Bump version fix-rrweb-patch (#37) * Make check conditional * Update to 0.11.0 Bump (#38) Skip the first fullSnapshotBuild (#39) User events for inactivity calculation (#40) Make sure we publish a production build (#41) Pull in fix for #528 from rrweb (#42) Buffer modifications to virtual stylesheets (#43) Test rrweb 1.0.1 Co-authored-by: Lucky Feng Co-authored-by: filip slatinac Co-authored-by: zhaoziqiu Co-authored-by: yz-yu Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yash Kumar Co-authored-by: Lucky Feng <294889365@qq.com> Co-authored-by: Vladimir Milenko Co-authored-by: Eoghan Murray Co-authored-by: Fanis Katsimpas Co-authored-by: Lucky Feng Co-authored-by: 101arrowz Co-authored-by: Jarosław Salwa Co-authored-by: Yanzhen Yu Co-authored-by: zzq0826 <770166635@qq.com> Co-authored-by: Karl-Aksel Puulmann Co-authored-by: Moji Izadmehr Co-authored-by: Filip Slatinac Co-authored-by: Province Innovation <69924001+provinceinnovation@users.noreply.github.com> Co-authored-by: Justin Halsall Co-authored-by: Season Co-authored-by: arshabh-copods <77658085+arshabh-copods@users.noreply.github.com> Co-authored-by: Yakko Majuri <38760734+yakkomajuri@users.noreply.github.com> Co-authored-by: re-fort Co-authored-by: Ziqiu Zhao <39512431+ZzqiZQute@users.noreply.github.com> Co-authored-by: yashkumar18 Co-authored-by: bachmanity1 <81428651+bachmanity1@users.noreply.github.com> Co-authored-by: Omair Nabiel Copy https://github.com/rrweb-io/rrweb/pull/630 for DragEvent errors (#45) Record and replay nested stylesheet rules (#46) john/hig-1014-2nd-attempt-at-stitches-fix-in-rrweb (#47) Apply nested styles refactor https://github.com/rrweb-io/rrweb/pull/667/files john/hig-1046-apply-rrweb-css-change (#48) Update Mirror to support null IDs John/hig-1121-add-rrweb-patch-for-element-blocking (#49) Set the blockClass background color to the same as the color (#51) John/hig-1053-strictprivacy-mode-is-injecting-random (#52) gc virtual style map when DOM has been removed (#53) Monkeypatch each iframe (#54) Update rrweb to 1.0.7 (#55) Pause animations on pseudo elements (#56) Add webgl recording and playback (#57) Webgl-patches (#58) * Add diffs * Add missing changes * Bump version Patch webgl support for older Safari browsers Make sure WebGL instance exists before saving More patches for webgl recording Update mouse cursor to make it visible on dark and light backgrounds Bump version Support loading CORS fonts through our proxy (#59) John/hig-1919-update-rrweb (#60) * Port over remaining webgl changes * More updates * https://github.com/rrweb-io/rrweb/pull/810/files * https://github.com/rrweb-io/rrweb/pull/720/files * Update package version Make sure text mutations respect strict privacy mode (#61) John/hig-1930-sync-rrweb-changes (#62) * Sync change * Port fix: an error when I stop the recording process Change click behavior/visual (#63) Silence the "please add custom even after start recording" (#64) John/hig-1984-pull-in-rrweb-sequential-id-pr (#65) * Add sequential IDs * https://github.com/rrweb-io/rrweb/pull/840//files * bump version Export sequential ID plugin Ignore recording rrweb internal click events for canvas elements John/hig-1998-apply-rrweb-commits (#66) [HIG-2114] add replace events function for use by chunking (#67) bump version (#68) https://github.com/rrweb-io/rrweb/pull/866/files (#69) remove promise to resolve immediately (#70) baseline time check should include offset to avoid replaying past events (#71) v1.1.20 fix rollup config (#72) fix the bundle script so that we can use rrweb directly from a -
+
record 2
-
-
+
+
" `; @@ -453,11 +453,11 @@ exports[`integration tests > [html file]: mask-text.html 1`] = ` Document -

**** *

-
+

**** *

+
**** *
-
**** *
+
**** *
" `; diff --git a/packages/rrweb-snapshot/test/html/block-element.html b/packages/rrweb-snapshot/test/html/block-element.html index f9671d2a..573c4fa2 100644 --- a/packages/rrweb-snapshot/test/html/block-element.html +++ b/packages/rrweb-snapshot/test/html/block-element.html @@ -19,9 +19,9 @@ -
block 1
+
block 1
record 2
-
block 3
-
block 3
+
block 3
+
block 3
diff --git a/packages/rrweb-snapshot/test/html/mask-text.html b/packages/rrweb-snapshot/test/html/mask-text.html index fe177a61..e31eab8f 100644 --- a/packages/rrweb-snapshot/test/html/mask-text.html +++ b/packages/rrweb-snapshot/test/html/mask-text.html @@ -8,10 +8,10 @@ -

mask 1

-
+

mask 1

+
mask 2
-
mask 3
+
mask 3
diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index c3ff607f..a8f75c76 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -129,7 +129,7 @@ describe('absolute url to stylesheet', () => { describe('isBlockedElement()', () => { const subject = (html: string, opt: any = {}) => - _isBlockedElement(render(html), 'rr-block', opt.blockSelector); + _isBlockedElement(render(html), 'highlight-block', opt.blockSelector); const render = (html: string): HTMLElement => JSDOM.fragment(html).querySelector('div')!; @@ -139,16 +139,18 @@ describe('isBlockedElement()', () => { }); it('blocks prohibited className', () => { - expect(subject('
')).toEqual(true); + expect(subject('
')).toEqual(true); }); it('does not block random data selector', () => { - expect(subject('
')).toEqual(false); + expect(subject('
')).toEqual(false); }); it('blocks blocked selector', () => { expect( - subject('
', { blockSelector: '[data-rr-block]' }), + subject('
', { + blockSelector: '[data-highlight-block]', + }), ).toEqual(true); }); }); diff --git a/packages/rrweb/LICENSE b/packages/rrweb/LICENSE new file mode 100644 index 00000000..fce28eb8 --- /dev/null +++ b/packages/rrweb/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Contributors (https://github.com/rrweb-io/rrweb/graphs/contributors) and SmartX Inc. + +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. diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index d8ff22b7..24e0258e 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -74,18 +74,18 @@ "simple-peer-light": "^9.10.0", "ts-node": "^10.9.1", "tslib": "^2.3.1", + "turbo": "^2.0.5", "typescript": "^5.4.5", "vite": "^5.3.1", "vite-plugin-dts": "^3.9.1" }, "dependencies": { - "@rrweb/types": "^2.0.0-alpha.17", - "@rrweb/utils": "^2.0.0-alpha.17", + "@rrweb/types": "workspace:*", "@types/css-font-loading-module": "0.0.7", "@xstate/fsm": "^1.4.0", "base64-arraybuffer": "^1.0.1", "mitt": "^3.0.0", - "rrdom": "^2.0.0-alpha.17", - "rrweb-snapshot": "^2.0.0-alpha.17" + "rrdom": "workspace:*", + "rrweb-snapshot": "workspace:*" } } diff --git a/packages/rrweb/rollup.config.js b/packages/rrweb/rollup.config.js index 998433bd..fd95a808 100644 --- a/packages/rrweb/rollup.config.js +++ b/packages/rrweb/rollup.config.js @@ -119,7 +119,7 @@ const baseConfigs = [ let configs = []; function getPlugins(options = {}) { - const { minify = false, sourceMap = false } = options; + const { minify = true, sourceMap = false } = options; return [ resolve({ browser: true }), webWorkerLoader({ @@ -135,7 +135,7 @@ function getPlugins(options = {}) { extract: true, inject: false, minimize: minify, - sourceMap, + sourceMap: true, }), ]; } @@ -157,6 +157,8 @@ for (const c of baseConfigs) { postcss({ extract: false, inject: false, + minimize: true, + sourceMap: true, }), ); // browser @@ -239,19 +241,38 @@ if (process.env.BROWSER_ONLY) { name: 'rrwebCanvasWebRTCReplay', pathFn: toPluginPath('canvas-webrtc', 'replay'), }, + { + input: './src/plugins/sequential-id/record/index.ts', + name: 'rrwebSequentialIdRecord', + pathFn: toPluginPath('sequential-id', 'record'), + }, ]; configs = []; + // browser record + replay, unminified (for profiling and performance testing) + configs.push({ + input: './src/index.ts', + plugins: getPlugins(), + output: [ + { + name: 'rrweb', + format: 'iife', + file: pkg.unpkg, + }, + ], + }); + for (const c of browserOnlyBaseConfigs) { configs.push({ input: c.input, - plugins: getPlugins(), + plugins: getPlugins({ sourceMap: true, minify: true }), output: [ { name: c.name, format: 'iife', file: c.pathFn(pkg.unpkg), + sourcemap: true, }, ], }); diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 20553fea..b7b2cf0c 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -1,4 +1,7 @@ -import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot'; +import type { + Mirror, + serializedNodeWithId, +} from 'rrweb-snapshot'; import { genId, NodeType } from 'rrweb-snapshot'; import type { CrossOriginIframeMessageEvent } from '../types'; import CrossOriginIframeMirror from './cross-origin-iframe-mirror'; @@ -106,7 +109,7 @@ export class IframeManager { const iframeSourceWindow = message.source; if (!iframeSourceWindow) return; - const iframeEl = this.crossOriginIframeMap.get(message.source); + const iframeEl = this.crossOriginIframeMap.get(iframeSourceWindow); if (!iframeEl) return; const transformedEvent = this.transformCrossOriginEvent( diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 1308c378..03546314 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -33,6 +33,7 @@ import { IframeManager } from './iframe-manager'; import { ShadowDomManager } from './shadow-dom-manager'; import { CanvasManager } from './observers/canvas/canvas-manager'; import { StylesheetManager } from './stylesheet-manager'; +import { obfuscateText } from 'rrweb-snapshot'; import ProcessedNodeManager from './processed-node-manager'; import { callbackWrapper, @@ -69,18 +70,18 @@ function record( emit, checkoutEveryNms, checkoutEveryNth, - blockClass = 'rr-block', + blockClass = 'highlight-block', blockSelector = null, - ignoreClass = 'rr-ignore', + ignoreClass = 'highlight-ignore', ignoreSelector = null, - maskTextClass = 'rr-mask', + maskTextClass = 'highlight-mask', maskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, slimDOMOptions: _slimDOMOptions, maskInputFn, - maskTextFn, + maskTextFn = obfuscateText, hooks, packFn, sampling = {}, @@ -97,6 +98,7 @@ function record( inlineImages = false, plugins, keepIframeSrcFn = () => false, + enableStrictPrivacy = false, ignoreCSSAttributes = new Set([]), errorHandler, } = options; @@ -323,8 +325,11 @@ function record( blockClass, blockSelector, mirror, - sampling: sampling.canvas, + sampling: sampling?.canvas?.fps, dataURLOptions, + resizeQuality: sampling?.canvas?.resizeQuality, + resizeFactor: sampling?.canvas?.resizeFactor, + maxSnapshotDimension: sampling?.canvas?.maxSnapshotDimension, }); const shadowDomManager = new ShadowDomManager({ @@ -342,6 +347,7 @@ function record( maskInputFn, recordCanvas, inlineImages, + enableStrictPrivacy, sampling, slimDOMOptions, iframeManager, @@ -389,6 +395,7 @@ function record( dataURLOptions, recordCanvas, inlineImages, + enableStrictPrivacy, onSerialize: (n) => { if (isSerializedIframe(n, mirror)) { iframeManager.addIframe(n as HTMLIFrameElement); @@ -553,6 +560,7 @@ function record( processedNodeManager, canvasManager, ignoreCSSAttributes, + enableStrictPrivacy, plugins: plugins ?.filter((p) => p.observer) @@ -630,7 +638,9 @@ function record( record.addCustomEvent = (tag: string, payload: T) => { if (!recording) { - throw new Error('please add custom event after start recording'); + /* Highlight Code - disable this warning */ + // throw new Error('please add custom event after start recording'); + return; } wrappedEmit({ type: EventType.Custom, diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 3ab7f191..7709dc8b 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -6,6 +6,7 @@ import { isShadowRoot, needMaskingText, maskInputValue, + obfuscateText, Mirror, isNativeShadowDom, getInputType, @@ -182,6 +183,7 @@ export default class MutationBuffer { private keepIframeSrcFn: observerParam['keepIframeSrcFn']; private recordCanvas: observerParam['recordCanvas']; private inlineImages: observerParam['inlineImages']; + private enableStrictPrivacy: observerParam['enableStrictPrivacy']; private slimDOMOptions: observerParam['slimDOMOptions']; private dataURLOptions: observerParam['dataURLOptions']; private doc: observerParam['doc']; @@ -208,6 +210,7 @@ export default class MutationBuffer { 'keepIframeSrcFn', 'recordCanvas', 'inlineImages', + 'enableStrictPrivacy', 'slimDOMOptions', 'dataURLOptions', 'doc', @@ -314,6 +317,7 @@ export default class MutationBuffer { dataURLOptions: this.dataURLOptions, recordCanvas: this.recordCanvas, inlineImages: this.inlineImages, + enableStrictPrivacy: this.enableStrictPrivacy, onSerialize: (currentN) => { if (isSerializedIframe(currentN, this.mirror)) { this.iframeManager.addIframe(currentN as HTMLIFrameElement); @@ -441,10 +445,16 @@ export default class MutationBuffer { // the node is being ignored as it isn't in the mirror, so shift mutation to attributes on parent textarea this.genTextAreaValueMutation(parent as HTMLTextAreaElement); } + /* Begin Highlight Code */ + let value = text.value; + if (this.enableStrictPrivacy && value) { + value = obfuscateText(value); + } return { id: this.mirror.getId(n), - value: text.value, + value, }; + /* End Highlight Code */ }) // no need to include them on added elements, as they have just been serialized with up to date attribubtes .filter((text) => !addedIds.has(text.id)) @@ -611,6 +621,16 @@ export default class MutationBuffer { } if (!ignoreAttribute(target.tagName, attributeName, value)) { + /* Begin Highlight Code */ + const tagName = (m.target as HTMLElement).tagName; + if (tagName === 'INPUT') { + const node = m.target as HTMLInputElement; + if (node.type === 'password') { + item.attributes['value'] = '*'.repeat(node.value.length); + break; + } + } + /* End Highlight Code */ // overwrite attribute if the mutations was triggered in same time item.attributes[attributeName] = transformAttribute( this.doc, diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 7e3aab3f..48c403f0 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -16,6 +16,7 @@ import { isBlocked, legacy_isTouchEvent, patch, + isCanvasNode, StyleSheetMirror, nowTimestamp, } from '../utils'; @@ -212,9 +213,15 @@ function initMouseInteractionObserver({ const getHandler = (eventKey: keyof typeof MouseInteractions) => { return (event: MouseEvent | TouchEvent | PointerEvent) => { const target = getEventTarget(event) as Node; - if (isBlocked(target, blockClass, blockSelector, true)) { + /* Start of Highlight Code */ + if ( + isBlocked(target, blockClass, blockSelector, true) || + // We ignore canvas elements for rage click detection because we cannot infer what inside the canvas is getting interacted with. + isCanvasNode(target) + ) { return; } + /* End of Highlight Code */ let pointerType: PointerTypes | null = null; let thisEventKey = eventKey; if ('pointerType' in event) { diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index 6e6bfdf1..ae54671d 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -1,4 +1,8 @@ -import type { ICanvas, Mirror, DataURLOptions } from 'rrweb-snapshot'; +import type { + ICanvas, + Mirror, + DataURLOptions, +} from 'rrweb-snapshot'; import type { blockClass, canvasManagerMutationCallback, @@ -64,6 +68,9 @@ export class CanvasManager { mirror: Mirror; sampling?: 'all' | number; dataURLOptions: DataURLOptions; + resizeQuality?: 'pixelated' | 'low' | 'medium' | 'high'; + resizeFactor?: number; + maxSnapshotDimension?: number; }) { const { sampling = 'all', @@ -79,9 +86,18 @@ export class CanvasManager { if (recordCanvas && sampling === 'all') this.initCanvasMutationObserver(win, blockClass, blockSelector); if (recordCanvas && typeof sampling === 'number') - this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector, { - dataURLOptions, - }); + this.initCanvasFPSObserver( + sampling, + win, + blockClass, + blockSelector, + { + dataURLOptions, + }, + options.resizeQuality, + options.resizeFactor, + options.maxSnapshotDimension, + ); } private processMutation: canvasManagerMutationCallback = ( @@ -109,6 +125,9 @@ export class CanvasManager { options: { dataURLOptions: DataURLOptions; }, + resizeQuality?: 'pixelated' | 'low' | 'medium' | 'high', + resizeFactor?: number, + maxSnapshotDimension?: number, ) { const canvasContextReset = initCanvasContextObserver( win, @@ -125,14 +144,14 @@ export class CanvasManager { if (!('base64' in e.data)) return; - const { base64, type, width, height } = e.data; + const { base64, type, canvasWidth, canvasHeight } = e.data; this.mutationCb({ id, type: CanvasContext['2D'], commands: [ { property: 'clearRect', // wipe canvas - args: [0, 0, width, height], + args: [0, 0, canvasWidth, canvasHeight], }, { property: 'drawImage', // draws (semi-transparent) image @@ -149,6 +168,8 @@ export class CanvasManager { } as CanvasArg, 0, 0, + canvasWidth, + canvasHeight, ], }, ], @@ -213,13 +234,32 @@ export class CanvasManager { context.clear(context.COLOR_BUFFER_BIT); } } - const bitmap = await createImageBitmap(canvas); + // canvas is not yet ready... this retry on the next sampling iteration. + // we don't want to crash the worker if the canvas is not yet rendered. + if (canvas.width === 0 || canvas.height === 0) { + return; + } + let scale = resizeFactor || 1; + if (maxSnapshotDimension) { + const maxDim = Math.max(canvas.width, canvas.height); + scale = Math.min(scale, maxSnapshotDimension / maxDim); + } + const width = canvas.width * scale; + const height = canvas.height * scale; + + const bitmap = await createImageBitmap(canvas, { + resizeQuality: resizeQuality || 'low', + resizeWidth: width, + resizeHeight: height, + }); worker.postMessage( { id, bitmap, - width: canvas.width, - height: canvas.height, + width, + height, + canvasWidth: canvas.width, + canvasHeight: canvas.height, dataURLOptions: options.dataURLOptions, }, [bitmap], diff --git a/packages/rrweb/src/record/observers/canvas/canvas.ts b/packages/rrweb/src/record/observers/canvas/canvas.ts index 4f6b30fc..77386eab 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas.ts @@ -1,5 +1,9 @@ import type { ICanvas } from 'rrweb-snapshot'; -import type { blockClass, IWindow, listenerHandler } from '@rrweb/types'; +import type { + blockClass, + IWindow, + listenerHandler, +} from '@rrweb/types'; import { isBlocked, patch } from '../../../utils'; function getNormalizedContextName(contextType: string) { diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts index c2bbacc6..20e40b83 100644 --- a/packages/rrweb/src/record/stylesheet-manager.ts +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -1,4 +1,7 @@ -import type { elementNode, serializedNodeWithId } from 'rrweb-snapshot'; +import type { + elementNode, + serializedNodeWithId, +} from 'rrweb-snapshot'; import { stringifyRule } from 'rrweb-snapshot'; import type { adoptedStyleSheetCallback, diff --git a/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts index 374edfe1..a03a9a22 100644 --- a/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts +++ b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts @@ -49,7 +49,15 @@ const worker: ImageBitmapDataURLResponseWorker = self; // eslint-disable-next-line @typescript-eslint/no-misused-promises worker.onmessage = async function (e) { if ('OffscreenCanvas' in globalThis) { - const { id, bitmap, width, height, dataURLOptions } = e.data; + const { + id, + bitmap, + width, + height, + canvasWidth, + canvasHeight, + dataURLOptions, + } = e.data; const transparentBase64 = getTransparentBlobFor( width, @@ -60,7 +68,7 @@ worker.onmessage = async function (e) { const offscreen = new OffscreenCanvas(width, height); const ctx = offscreen.getContext('2d')!; - ctx.drawImage(bitmap, 0, 0); + ctx.drawImage(bitmap, 0, 0, width, height); bitmap.close(); const blob = await offscreen.convertToBlob(dataURLOptions); // takes a while const type = blob.type; @@ -81,6 +89,8 @@ worker.onmessage = async function (e) { base64, width, height, + canvasWidth, + canvasHeight, }); lastBlobMap.set(id, base64); } else { diff --git a/packages/rrweb/src/record/workers/tsconfig.json b/packages/rrweb/src/record/workers/tsconfig.json index eadbc9ce..95e17d34 100644 --- a/packages/rrweb/src/record/workers/tsconfig.json +++ b/packages/rrweb/src/record/workers/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../../tsconfig.json", "compilerOptions": { + "types": ["@types/offscreencanvas", "@types/node"], "lib": ["webworker"] } } diff --git a/packages/rrweb/src/replay/canvas/deserialize-args.ts b/packages/rrweb/src/replay/canvas/deserialize-args.ts index a690d798..ad2077f8 100644 --- a/packages/rrweb/src/replay/canvas/deserialize-args.ts +++ b/packages/rrweb/src/replay/canvas/deserialize-args.ts @@ -1,6 +1,9 @@ import { decode } from 'base64-arraybuffer'; import type { Replayer } from '../'; -import type { CanvasArg, SerializedCanvasArg } from '@rrweb/types'; +import type { + CanvasArg, + SerializedCanvasArg, +} from '@rrweb/types'; // TODO: add ability to wipe this list type GLVarMap = Map; diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts index b4faf4c8..35bb44e3 100644 --- a/packages/rrweb/src/replay/canvas/webgl.ts +++ b/packages/rrweb/src/replay/canvas/webgl.ts @@ -1,5 +1,8 @@ import type { Replayer } from '../'; -import { CanvasContext, type canvasMutationCommand } from '@rrweb/types'; +import { + CanvasContext, + type canvasMutationCommand, +} from '@rrweb/types'; import { deserializeArg, variableListFor } from './deserialize-args'; function getContext( diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 4ac17df0..eace2776 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -65,6 +65,7 @@ import type { canvasMutationCommand, canvasMutationParam, canvasEventWithTime, + SessionInterval, selectionData, styleSheetRuleData, styleDeclarationData, @@ -91,6 +92,8 @@ import { MediaManager } from './media'; import { applyDialogToTopLevel, removeDialogFromTopLevel } from './dialog'; const SKIP_TIME_INTERVAL = 5 * 1000; +const SKIP_TIME_MIN = 1 * 1000; +const SKIP_DURATION_LIMIT = 60 * 60 * 1000; // https://github.com/rollup/rollup/issues/1267#issuecomment-296395734 const mitt = mittProxy.default || mittProxy; @@ -136,6 +139,8 @@ export class Replayer { private emitter: Emitter = mitt(); private nextUserInteractionEvent: eventWithTime | null; + private activityIntervals: Array = []; + private inactiveEndTimestamp: number | null; private legacy_missingNodeRetryMap: missingNodeMap = {}; @@ -192,7 +197,7 @@ export class Replayer { inactivePeriodThreshold: 10 * 1000, showWarning: true, showDebug: false, - blockClass: 'rr-block', + blockClass: 'highlight-block', liveMode: false, insertStyleRules: [], triggerFocus: true, @@ -201,6 +206,8 @@ export class Replayer { mouseTail: defaultMouseTailConfig, useVirtualDom: true, // Virtual-dom optimization is enabled by default. logger: console, + inactiveThreshold: 0.02, + inactiveSkipTime: SKIP_TIME_INTERVAL, }; this.config = Object.assign({}, defaultConfig, config); @@ -470,6 +477,102 @@ export class Replayer { } } + /* Start Highlight Code */ + public getActivityIntervals(): Array { + if (this.activityIntervals.length == 0) { + // Preprocessing to get all active/inactive segments in a session + const allIntervals: Array = []; + const metadata = this.getMetaData(); + const userInteractionEvents = [ + { timestamp: metadata.startTime }, + ...this.service.state.context.events.filter((ev) => + this.isUserInteraction(ev), + ), + { timestamp: metadata.endTime }, + ]; + for (let i = 1; i < userInteractionEvents.length; i++) { + const currentInterval = userInteractionEvents[i - 1]; + const _event = userInteractionEvents[i]; + if ( + _event.timestamp - currentInterval.timestamp > + this.config.inactivePeriodThreshold + ) { + allIntervals.push({ + startTime: currentInterval.timestamp, + endTime: _event.timestamp, + duration: _event.timestamp - currentInterval.timestamp, + active: false, + }); + } else { + allIntervals.push({ + startTime: currentInterval.timestamp, + endTime: _event.timestamp, + duration: _event.timestamp - currentInterval.timestamp, + active: true, + }); + } + } + // Merges continuous active/inactive ranges + const mergedIntervals: Array = []; + let currentInterval = allIntervals[0]; + for (let i = 1; i < allIntervals.length; i++) { + if (allIntervals[i].active != allIntervals[i - 1].active) { + mergedIntervals.push({ + startTime: currentInterval.startTime, + endTime: allIntervals[i - 1].endTime, + duration: allIntervals[i - 1].endTime - currentInterval.startTime, + active: allIntervals[i - 1].active, + }); + currentInterval = allIntervals[i]; + } + } + if (currentInterval && allIntervals.length > 0) { + mergedIntervals.push({ + startTime: currentInterval.startTime, + endTime: allIntervals[allIntervals.length - 1].endTime, + duration: + allIntervals[allIntervals.length - 1].endTime - + currentInterval.startTime, + active: allIntervals[allIntervals.length - 1].active, + }); + } + // Merges inactive segments that are less than a threshold into surrounding active sessions + // TODO: Change this from a 3n pass to n + currentInterval = mergedIntervals[0]; + for (let i = 1; i < mergedIntervals.length; i++) { + if ( + (!mergedIntervals[i].active && + mergedIntervals[i].duration > + this.config.inactiveThreshold * metadata.totalTime) || + (!mergedIntervals[i - 1].active && + mergedIntervals[i - 1].duration > + this.config.inactiveThreshold * metadata.totalTime) + ) { + this.activityIntervals.push({ + startTime: currentInterval.startTime, + endTime: mergedIntervals[i - 1].endTime, + duration: + mergedIntervals[i - 1].endTime - currentInterval.startTime, + active: mergedIntervals[i - 1].active, + }); + currentInterval = mergedIntervals[i]; + } + } + if (currentInterval && mergedIntervals.length > 0) { + this.activityIntervals.push({ + startTime: currentInterval.startTime, + endTime: mergedIntervals[mergedIntervals.length - 1].endTime, + duration: + mergedIntervals[mergedIntervals.length - 1].endTime - + currentInterval.startTime, + active: mergedIntervals[mergedIntervals.length - 1].active, + }); + } + } + return this.activityIntervals; + } + /* End Highlight Code */ + public getMetaData(): playerMetaData { const firstEvent = this.service.state.context.events[0]; const lastEvent = @@ -575,6 +678,16 @@ export class Replayer { ); } + public replaceEvents(events: eventWithTime[]) { + for (const event of events) { + if (indicatesTouchDevice(event)) { + this.mouse.classList.add('touch-device'); + break; + } + } + this.service.send({ type: 'REPLACE_EVENTS', payload: { events } }); + } + public enableInteract() { this.iframe.setAttribute('scrolling', 'auto'); this.iframe.style.pointerEvents = 'auto'; @@ -708,6 +821,7 @@ export class Replayer { // do not check skip in sync return; } + this.handleInactivity(event.timestamp); if (event === this.nextUserInteractionEvent) { this.nextUserInteractionEvent = null; this.backToNormal(); @@ -790,6 +904,42 @@ export class Replayer { return wrappedCastFn; }; + /* Start of Highlight Code */ + private handleInactivity(timestamp: number, resetNext?: boolean) { + if (timestamp === this.inactiveEndTimestamp || resetNext) { + this.inactiveEndTimestamp = null; + this.backToNormal(); + } + if (this.config.skipInactive && !this.inactiveEndTimestamp) { + for (const interval of this.getActivityIntervals()) { + if ( + timestamp >= interval.startTime && + timestamp < interval.endTime && + !interval.active + ) { + this.inactiveEndTimestamp = interval.endTime; + break; + } + } + if (this.inactiveEndTimestamp) { + const skipTime = this.inactiveEndTimestamp - timestamp; + const payload = { + speed: + (skipTime / SKIP_DURATION_LIMIT) * this.config.inactiveSkipTime < + SKIP_TIME_MIN + ? skipTime / SKIP_TIME_MIN + : Math.round( + Math.max(skipTime, SKIP_DURATION_LIMIT) / + this.config.inactiveSkipTime, + ), + }; + this.speedService.send({ type: 'FAST_FORWARD', payload }); + this.emitter.emit(ReplayerEvents.SkipStart, payload); + } + } + } + /* End of Highlight Code */ + private rebuildFullSnapshot( event: fullSnapshotEvent & { timestamp: number }, isSync = false, diff --git a/packages/rrweb/src/replay/machine.ts b/packages/rrweb/src/replay/machine.ts index 08b72c95..6f994c52 100644 --- a/packages/rrweb/src/replay/machine.ts +++ b/packages/rrweb/src/replay/machine.ts @@ -11,6 +11,7 @@ import { EventType, type Emitter, IncrementalSource, + type actionWithDelay, } from '@rrweb/types'; import { Timer, addDelay } from './timer'; @@ -42,6 +43,10 @@ export type PlayerEvent = event: eventWithTime; }; } + | { + type: 'REPLACE_EVENTS'; + payload: { events: eventWithTime[] }; + } | { type: 'END'; }; @@ -111,6 +116,10 @@ export function createPlayerService( target: 'playing', actions: ['addEvent'], }, + REPLACE_EVENTS: { + target: 'playing', + actions: ['replaceEvents'], + }, }, }, paused: { @@ -131,6 +140,10 @@ export function createPlayerService( target: 'paused', actions: ['addEvent'], }, + REPLACE_EVENTS: { + target: 'paused', + actions: ['replaceEvents'], + }, }, }, live: { @@ -235,6 +248,34 @@ export function createPlayerService( return Date.now(); }, }), + /* Highlight Code Start */ + replaceEvents: assign((ctx, machineEvent) => { + const { events: curEvents, timer, baselineTime } = ctx; + if (machineEvent.type === 'REPLACE_EVENTS') { + const { events: newEvents } = machineEvent.payload; + curEvents.length = 0; + const actions: actionWithDelay[] = []; + for (const event of newEvents) { + addDelay(event, baselineTime); + curEvents.push(event); + if (event.timestamp >= timer.timeOffset + baselineTime) { + const castFn = getCastFn(event, false); + actions.push({ + doAction: () => { + castFn(); + }, + delay: event.delay!, + }); + } + } + + if (timer.isActive()) { + timer.replaceActions(actions); + } + } + return { ...ctx, events: curEvents }; + }), + /* Highlight Code End */ addEvent: assign((ctx, machineEvent) => { const { baselineTime, timer, events } = ctx; if (machineEvent.type === 'ADD_EVENT') { diff --git a/packages/rrweb/src/replay/styles/inject-style.ts b/packages/rrweb/src/replay/styles/inject-style.ts index f5018561..2ba28dfb 100644 --- a/packages/rrweb/src/replay/styles/inject-style.ts +++ b/packages/rrweb/src/replay/styles/inject-style.ts @@ -1,6 +1,7 @@ const rules: (blockClass: string) => string[] = (blockClass: string) => [ - `.${blockClass} { background: currentColor }`, 'noscript { display: none !important; }', + `.${blockClass} { background: currentColor; border-radius: 5px; }`, + `.${blockClass}:hover::after {content: 'Redacted'; color: white; background: black; text-align: center; width: 100%; display: block;}`, ]; export default rules; diff --git a/packages/rrweb/src/replay/styles/style.css b/packages/rrweb/src/replay/styles/style.css index b459e515..60578d0b 100644 --- a/packages/rrweb/src/replay/styles/style.css +++ b/packages/rrweb/src/replay/styles/style.css @@ -10,7 +10,7 @@ background-position: center center; background-repeat: no-repeat; background-image: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDUwIDUwIiB4PSIwcHgiIHk9IjBweCI+PHRpdGxlPkRlc2lnbl90bnA8L3RpdGxlPjxwYXRoIGQ9Ik00OC43MSw0Mi45MUwzNC4wOCwyOC4yOSw0NC4zMywxOEExLDEsMCwwLDAsNDQsMTYuMzlMMi4zNSwxLjA2QTEsMSwwLDAsMCwxLjA2LDIuMzVMMTYuMzksNDRhMSwxLDAsMCwwLDEuNjUuMzZMMjguMjksMzQuMDgsNDIuOTEsNDguNzFhMSwxLDAsMCwwLDEuNDEsMGw0LjM4LTQuMzhBMSwxLDAsMCwwLDQ4LjcxLDQyLjkxWm0tNS4wOSwzLjY3TDI5LDMyYTEsMSwwLDAsMC0xLjQxLDBsLTkuODUsOS44NUwzLjY5LDMuNjlsMzguMTIsMTRMMzIsMjcuNThBMSwxLDAsMCwwLDMyLDI5TDQ2LjU5LDQzLjYyWiI+PC9wYXRoPjwvc3ZnPg=='); - border-color: transparent; /* otherwise we transition from black when .touch-device class is added */ + border-color: transparent; /* otherwise we transition from black when .touch-device class is added */ } .replayer-mouse::after { content: ''; @@ -26,7 +26,7 @@ animation: click 0.2s ease-in-out 1; } .replayer-mouse.touch-device { - background-image: none; /* there's no passive cursor on touch-only screens */ + background-image: none; /* there's no passive cursor on touch-only screens */ width: 70px; height: 70px; border-width: 4px; @@ -42,7 +42,7 @@ transition: left 0.25s linear, top 0.25s linear, border-color 0.2s ease-in-out; } .replayer-mouse.touch-device::after { - opacity: 0; /* there's no passive cursor on touch-only screens */ + opacity: 0; /* there's no passive cursor on touch-only screens */ } .replayer-mouse.touch-device.active::after { animation: touch-click 0.2s ease-in-out 1; @@ -77,3 +77,15 @@ height: 10px; } } + +.rr-player { + position: relative; + background: white; + float: left; + border-radius: 5px; + box-shadow: 0 24px 48px rgba(17, 16, 62, 0.12); +} + +.rr-player__frame { + overflow: hidden; +} diff --git a/packages/rrweb/src/replay/timer.ts b/packages/rrweb/src/replay/timer.ts index 9fc3932b..2233e355 100644 --- a/packages/rrweb/src/replay/timer.ts +++ b/packages/rrweb/src/replay/timer.ts @@ -43,6 +43,20 @@ export class Timer { } } + /* Begin Highlight Code */ + /** + * Add all actions before the timer starts + */ + public addActions(actions: actionWithDelay[]) { + this.actions = this.actions.concat(actions); + } + + public replaceActions(actions: actionWithDelay[]) { + this.actions.length = 0; + this.actions.splice(0, 0, ...actions); + } + /* End Highlight Code */ + public start() { this.timeOffset = 0; this.lastTimestamp = performance.now(); diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 75be0fc6..43b3a0b9 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -74,6 +74,11 @@ export type recordOptions = { mousemoveWait?: number; keepIframeSrcFn?: KeepIframeSrcFn; errorHandler?: ErrorHandler; + /** + * Enabling this will disable recording of text data on the page. This is useful if you do not want to record personally identifiable information. + * Text will be randomized. Instead of seeing "Hello World" in a recording, you will see "1fds1 j59a0". + */ + enableStrictPrivacy?: boolean; }; export type observerParam = { @@ -105,6 +110,7 @@ export type observerParam = { recordDOM: boolean; recordCanvas: boolean; inlineImages: boolean; + enableStrictPrivacy: boolean; userTriggeredOnInput: boolean; collectFonts: boolean; slimDOMOptions: SlimDOMOptions; @@ -142,6 +148,7 @@ export type MutationBufferParam = Pick< | 'keepIframeSrcFn' | 'recordCanvas' | 'inlineImages' + | 'enableStrictPrivacy' | 'slimDOMOptions' | 'dataURLOptions' | 'doc' @@ -196,6 +203,8 @@ export type playerConfig = { warn: (...args: Parameters) => void; }; plugins?: ReplayPlugin[]; + inactiveThreshold: number; + inactiveSkipTime: number; }; export type missingNode = { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index f216819f..5df3e1b3 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -10,7 +10,11 @@ import type { textMutation, } from '@rrweb/types'; import type { IMirror, Mirror, SlimDOMOptions } from 'rrweb-snapshot'; -import { isShadowRoot, IGNORED_NODE, classMatchesRegex } from 'rrweb-snapshot'; +import { + isShadowRoot, + IGNORED_NODE, + classMatchesRegex, +} from 'rrweb-snapshot'; import { RRNode, RRIFrameElement, BaseRRNode } from 'rrdom'; import dom from '@rrweb/utils'; @@ -233,6 +237,23 @@ export function closestElementOfNode(node: Node | null): HTMLElement | null { return el; } +/** + * Start of Highlight Code + */ +export const isCanvasNode = (node: Node | null): boolean => { + try { + if (node instanceof HTMLElement) { + return node.tagName === 'CANVAS'; + } + } catch { + return false; + } + return false; +}; +/** + * End of Highlight Code + */ + /** * Checks if the given element set to be blocked by rrweb * @param node - node to check diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 032bdbec..56cdd495 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -749,7 +749,7 @@ exports[`record integration tests > can mask character data mutations 1`] = ` { \\"id\\": 7, \\"attributes\\": { - \\"class\\": \\"rr-mask\\" + \\"class\\": \\"highlight-mask\\" } } ], @@ -767,7 +767,7 @@ exports[`record integration tests > can mask character data mutations 1`] = ` \\"type\\": 2, \\"tagName\\": \\"li\\", \\"attributes\\": { - \\"class\\": \\"rr-mask\\" + \\"class\\": \\"highlight-mask\\" }, \\"childNodes\\": [], \\"id\\": 20 @@ -5390,7 +5390,7 @@ exports[`record integration tests > mutations should work when blocked class is \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": { - \\"class\\": \\"rr-block\\", + \\"class\\": \\"highlight-block\\", \\"rr_width\\": \\"200px\\", \\"rr_height\\": \\"33px\\" }, @@ -5562,7 +5562,7 @@ exports[`record integration tests > mutations should work when blocked class is \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": { - \\"class\\": \\"rr-block\\", + \\"class\\": \\"highlight-block\\", \\"rr_width\\": \\"200px\\", \\"rr_height\\": \\"33px\\" }, @@ -7370,7 +7370,7 @@ exports[`record integration tests > should mask texts 1`] = ` \\"type\\": 2, \\"tagName\\": \\"p\\", \\"attributes\\": { - \\"class\\": \\"rr-mask\\" + \\"class\\": \\"highlight-mask\\" }, \\"childNodes\\": [ { @@ -7390,7 +7390,7 @@ exports[`record integration tests > should mask texts 1`] = ` \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": { - \\"class\\": \\"rr-mask\\" + \\"class\\": \\"highlight-mask\\" }, \\"childNodes\\": [ { @@ -7668,7 +7668,7 @@ exports[`record integration tests > should mask texts using maskTextFn 1`] = ` \\"type\\": 2, \\"tagName\\": \\"p\\", \\"attributes\\": { - \\"class\\": \\"rr-mask\\" + \\"class\\": \\"highlight-mask\\" }, \\"childNodes\\": [ { @@ -7688,7 +7688,7 @@ exports[`record integration tests > should mask texts using maskTextFn 1`] = ` \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": { - \\"class\\": \\"rr-mask\\" + \\"class\\": \\"highlight-mask\\" }, \\"childNodes\\": [ { @@ -8725,7 +8725,7 @@ exports[`record integration tests > should not record blocked elements and its c \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": { - \\"class\\": \\"rr-block\\", + \\"class\\": \\"highlight-block\\", \\"rr_width\\": \\"50px\\", \\"rr_height\\": \\"50px\\" }, @@ -8905,7 +8905,7 @@ exports[`record integration tests > should not record blocked elements dynamical \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": { - \\"class\\": \\"rr-block\\", + \\"class\\": \\"highlight-block\\", \\"rr_width\\": \\"50px\\", \\"rr_height\\": \\"50px\\" }, @@ -8965,7 +8965,7 @@ exports[`record integration tests > should not record blocked elements dynamical \\"type\\": 2, \\"tagName\\": \\"button\\", \\"attributes\\": { - \\"class\\": \\"rr-block\\", + \\"class\\": \\"highlight-block\\", \\"rr_width\\": \\"100px\\", \\"rr_height\\": \\"100px\\" }, @@ -9132,7 +9132,7 @@ exports[`record integration tests > should not record input events on ignored el \\"tagName\\": \\"input\\", \\"attributes\\": { \\"type\\": \\"text\\", - \\"class\\": \\"rr-ignore\\" + \\"class\\": \\"highlight-ignore\\" }, \\"childNodes\\": [], \\"id\\": 22 diff --git a/packages/rrweb/test/__snapshots__/replayer.test.ts.snap b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap index c4edd6d5..58455629 100644 --- a/packages/rrweb/test/__snapshots__/replayer.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap @@ -46,7 +46,7 @@ file-frame-5 file-cid-0 @charset \\"utf-8\\"; -.rr-block { background: currentcolor; } +.highlight-block { background: currentcolor; } noscript { display: none !important; } @@ -126,7 +126,7 @@ file-frame-5 file-cid-0 @charset \\"utf-8\\"; -.rr-block { background: currentcolor; } +.highlight-block { background: currentcolor; } noscript { display: none !important; } @@ -198,7 +198,7 @@ file-frame-2 file-cid-0 @charset \\"utf-8\\"; -.rr-block { background: currentcolor; } +.highlight-block { background: currentcolor; } noscript { display: none !important; } @@ -249,7 +249,7 @@ file-frame-2 file-cid-0 @charset \\"utf-8\\"; -.rr-block { background: currentcolor; } +.highlight-block { background: currentcolor; } noscript { display: none !important; } @@ -300,7 +300,7 @@ file-frame-2 file-cid-0 @charset \\"utf-8\\"; -.rr-block { background: currentcolor; } +.highlight-block { background: currentcolor; } noscript { display: none !important; } diff --git a/packages/rrweb/test/html/block.html b/packages/rrweb/test/html/block.html index 6fee77f7..4cef3cb6 100644 --- a/packages/rrweb/test/html/block.html +++ b/packages/rrweb/test/html/block.html @@ -7,7 +7,7 @@ Block record -
+
diff --git a/packages/rrweb/test/html/blocked-unblocked.html b/packages/rrweb/test/html/blocked-unblocked.html index 5d82c6cd..38d26696 100644 --- a/packages/rrweb/test/html/blocked-unblocked.html +++ b/packages/rrweb/test/html/blocked-unblocked.html @@ -71,7 +71,7 @@

Verify that block class bugs are fixed




-
+



@@ -83,7 +83,7 @@

Verify that block class bugs are fixed




-
+



diff --git a/packages/rrweb/test/html/ignore.html b/packages/rrweb/test/html/ignore.html index bb057c68..da7b73b8 100644 --- a/packages/rrweb/test/html/ignore.html +++ b/packages/rrweb/test/html/ignore.html @@ -10,10 +10,10 @@