From f1e6a51dc6bbc2837b9c315e74278fd9f220961d Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 1 May 2024 16:14:05 +0100 Subject: [PATCH 01/12] Ensure there is separation of timestamps (#1455) * Ensure there is separation of timestamps so mutations can be evaluated separately - was failing in the github build process probably due to higher perf * Remove space from test file * Create curvy-balloons-brake.md --------- Co-authored-by: Justin Halsall --- .changeset/curvy-balloons-brake.md | 2 ++ packages/rrweb/test/integration.test.ts | 8 +++++--- .../rrweb/test/replay/{ video.test.ts => video.test.ts} | 0 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 .changeset/curvy-balloons-brake.md rename packages/rrweb/test/replay/{ video.test.ts => video.test.ts} (100%) diff --git a/.changeset/curvy-balloons-brake.md b/.changeset/curvy-balloons-brake.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/curvy-balloons-brake.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 616b6e91cb..5ce9b76469 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -117,17 +117,19 @@ describe('record integration tests', function (this: ISuite) { ta.innerText = 'pre value'; document.body.append(ta); }); + await page.waitForTimeout(5); await page.evaluate(() => { const t = document.querySelector('textarea') as HTMLTextAreaElement; t.innerText = 'ok'; // this mutation should be recorded }); + await page.waitForTimeout(5); await page.evaluate(() => { const t = document.querySelector('textarea') as HTMLTextAreaElement; (t.childNodes[0] as Text).appendData('3'); // this mutation is also valid }); - + await page.waitForTimeout(5); await page.type('textarea', '1'); // types (inserts) at index 0, in front of existing text - + await page.waitForTimeout(5); await page.evaluate(() => { const t = document.querySelector('textarea') as HTMLTextAreaElement; // user has typed so childNode content should now be ignored @@ -138,7 +140,7 @@ describe('record integration tests', function (this: ISuite) { // there is nothing explicit in rrweb which enforces this, but this test may protect against // a future change where a mutation on a textarea incorrectly updates the .value }); - + await page.waitForTimeout(5); await page.type('textarea', '2'); // cursor is at index 1 const snapshots = (await page.evaluate( diff --git a/packages/rrweb/test/replay/ video.test.ts b/packages/rrweb/test/replay/video.test.ts similarity index 100% rename from packages/rrweb/test/replay/ video.test.ts rename to packages/rrweb/test/replay/video.test.ts From 5e7943dbae6e2cde76c484bdd26bc0b96f1b6dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vinicius=20Louren=C3=A7o?= <12551007+H4ad@users.noreply.github.com> Date: Thu, 2 May 2024 09:44:58 -0300 Subject: [PATCH 02/12] perf(snapshot): avoid recreate element `a` every time (#1387) perf(snapshot): avoid costly generation of element on each call to `getHref`, instead cache an anchor element and reuse it's href attributed --------- Co-authored-by: Eoghan Murray --- .changeset/hungry-dodos-taste.md | 10 +++++++++ packages/rrweb-snapshot/src/snapshot.ts | 30 ++++++++++++++++--------- 2 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 .changeset/hungry-dodos-taste.md diff --git a/.changeset/hungry-dodos-taste.md b/.changeset/hungry-dodos-taste.md new file mode 100644 index 0000000000..76217d072f --- /dev/null +++ b/.changeset/hungry-dodos-taste.md @@ -0,0 +1,10 @@ +--- +'rrweb-snapshot': patch +--- + +Avoid recreating the same element every time, instead, we cache and we just update the element. + +Before: 779k ops/s +After: 860k ops/s + +Benchmark: https://jsbench.me/ktlqztuf95/1 diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index d323e1af8c..822fa04967 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -196,23 +196,31 @@ function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { return output.join(', '); } +const cachedDocument = new WeakMap(); + export function absoluteToDoc(doc: Document, attributeValue: string): string { if (!attributeValue || attributeValue.trim() === '') { return attributeValue; } - const a: HTMLAnchorElement = doc.createElement('a'); - a.href = attributeValue; - return a.href; + + return getHref(doc, attributeValue); } function isSVGElement(el: Element): boolean { return Boolean(el.tagName === 'svg' || (el as SVGElement).ownerSVGElement); } -function getHref() { - // return a href without hash - const a = document.createElement('a'); - a.href = ''; +function getHref(doc: Document, customHref?: string) { + let a = cachedDocument.get(doc); + if (!a) { + a = doc.createElement('a'); + cachedDocument.set(doc, a); + } + if (!customHref) { + customHref = ''; + } + // note: using `new URL` is slower. See #1434 or https://jsbench.me/uqlud17rxo/1 + a.setAttribute('href', customHref); return a.href; } @@ -244,7 +252,7 @@ export function transformAttribute( } else if (name === 'srcset') { return getAbsoluteSrcsetString(doc, value); } else if (name === 'style') { - return absoluteToStylesheet(value, getHref()); + return absoluteToStylesheet(value, getHref(doc)); } else if (tagName === 'object' && name === 'data') { return absoluteToDoc(doc, value); } @@ -506,6 +514,7 @@ function serializeNode( }); case n.TEXT_NODE: return serializeTextNode(n as Text, { + doc, needsMask, maskTextFn, rootId, @@ -536,6 +545,7 @@ function getRootId(doc: Document, mirror: Mirror): number | undefined { function serializeTextNode( n: Text, options: { + doc: Document; needsMask: boolean | undefined; maskTextFn: MaskTextFn | undefined; rootId: number | undefined; @@ -567,7 +577,7 @@ function serializeTextNode( n, ); } - textContent = absoluteToStylesheet(textContent, getHref()); + textContent = absoluteToStylesheet(textContent, getHref(options.doc)); } if (isScript) { textContent = 'SCRIPT_PLACEHOLDER'; @@ -661,7 +671,7 @@ function serializeElementNode( (n as HTMLStyleElement).sheet as CSSStyleSheet, ); if (cssText) { - attributes._cssText = absoluteToStylesheet(cssText, getHref()); + attributes._cssText = absoluteToStylesheet(cssText, getHref(doc)); } } // form fields From e96f668c86bd0ab5dc190bb2957a170271bb2ebc Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 3 May 2024 12:20:20 +0100 Subject: [PATCH 03/12] Fix that blob urls persist on the shared anchor element and can't be later modified (#1467) * Fix that blob urls persist on the shared anchor element and can't be later modified * Create nervous-kiwis-nail.md --- .changeset/nervous-kiwis-nail.md | 6 ++++++ packages/rrweb-snapshot/src/snapshot.ts | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 .changeset/nervous-kiwis-nail.md diff --git a/.changeset/nervous-kiwis-nail.md b/.changeset/nervous-kiwis-nail.md new file mode 100644 index 0000000000..897df7ed5f --- /dev/null +++ b/.changeset/nervous-kiwis-nail.md @@ -0,0 +1,6 @@ +--- +'rrweb-snapshot': patch +'rrweb': patch +--- + +Bugfix after #1434 perf improvements: fix that blob urls persist on the shared anchor element and can't be later modified diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 822fa04967..81dc2133a0 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -218,6 +218,8 @@ function getHref(doc: Document, customHref?: string) { } if (!customHref) { customHref = ''; + } else if (customHref.startsWith('blob:') || customHref.startsWith('data:')) { + return customHref; } // note: using `new URL` is slower. See #1434 or https://jsbench.me/uqlud17rxo/1 a.setAttribute('href', customHref); From 26c331b3b7e1a45091904fd1a6a875e16c9b777f Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Mon, 13 May 2024 11:53:08 +0100 Subject: [PATCH 04/12] yarn format - prettier improvements & add .editorconfig (#1471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some dev improvements: * Add .editorconfig config file https://editorconfig.org/ * move the singleQuote spec into .editorconfig and add old .changesets/*.md to .prettierignore so that we don't incorrectly reformat new changeset files to single quote from the double quote which they can be autogenerated with in github * .gitignore Ignore emacs chaff files * Add `yarn format:head` a convenience command to run prettier against just those files in the head commit * Some mention of `yarn format` in the docs * Fix some test html closing tags; authoring mistakes, rather than deliberately malformed html — picked up by an explicit `yarn prettier --write '**/*.html'` --- .changeset/format-head-prettier.md | 2 + .editorconfig | 14 +++ .gitignore | 3 + .prettierignore | 107 ++++++++++++++++++ .prettierrc | 1 - CONTRIBUTING.md | 1 + docs/development/coding-style.md | 4 +- package.json | 1 + .../__snapshots__/integration.test.ts.snap | 41 ++++--- packages/rrweb/test/html/link.html | 2 +- packages/rrweb/test/html/shadow-dom.html | 1 - 11 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 .changeset/format-head-prettier.md create mode 100644 .editorconfig create mode 100644 .prettierignore diff --git a/.changeset/format-head-prettier.md b/.changeset/format-head-prettier.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/format-head-prettier.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..098f1c28c7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +# initialized from https://prettier.io/docs/en/configuration.html#editorconfig +[*] +charset = utf-8 +insert_final_newline = true +end_of_line = lf +indent_style = space +indent_size = 2 +max_line_length = 80 +quote_type = single + +[.changeset/*.md] +quote_type = double diff --git a/.gitignore b/.gitignore index 02a1c32fa7..97ae63a201 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ dist .turbo +# emacs working files end in a tilde +*~ + # `.yarn/install-state.gz` is an optimization file that you shouldn't ever have to commit. # It simply stores the exact state of your project so that the next commands can boot without having to resolve your workspaces all over again. .yarn/install-state.gz \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..d380c1457f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,107 @@ +# list of old changeset files that were mutated to use single quotes by a previous version of prettier: +.changeset/attribute-text-reductions.md +.changeset/avoid-costly-createlement.md +.changeset/brave-numbers-joke.md +.changeset/breezy-cats-heal.md +.changeset/breezy-mice-breathe.md +.changeset/calm-bulldogs-speak.md +.changeset/calm-oranges-sin.md +.changeset/chatty-cherries-train.md +.changeset/clean-plants-play.md +.changeset/clean-shrimps-lay.md +.changeset/cold-eyes-hunt.md +.changeset/cold-hounds-teach.md +.changeset/controller-finish-flag.md +.changeset/cool-grapes-hug.md +.changeset/cuddly-readers-warn.md +.changeset/curvy-apples-lay.md +.changeset/curvy-balloons-brake.md +.changeset/date-now-guard.md +.changeset/dirty-rules-dress.md +.changeset/eight-terms-hunt.md +.changeset/empty-bikes-cheer.md +.changeset/event-single-wrap.md +.changeset/fair-dragons-greet.md +.changeset/fast-chefs-smell.md +.changeset/few-rockets-travel.md +.changeset/few-turkeys-reflect.md +.changeset/five-peas-lay.md +.changeset/fluffy-planes-retire.md +.changeset/forty-elephants-attack.md +.changeset/fresh-cars-impress.md +.changeset/fresh-spoons-drive.md +.changeset/friendly-numbers-leave.md +.changeset/gold-apples-joke.md +.changeset/gold-terms-look.md +.changeset/grumpy-ways-own.md +.changeset/hip-worms-relax.md +.changeset/hungry-dodos-taste.md +.changeset/itchy-dryers-double.md +.changeset/khaki-dots-bathe.md +.changeset/large-ants-prove.md +.changeset/lazy-squids-draw.md +.changeset/lazy-toes-confess.md +.changeset/lemon-lamps-switch.md +.changeset/light-fireants-exercise.md +.changeset/little-radios-thank.md +.changeset/little-suits-leave.md +.changeset/loud-seals-raise.md +.changeset/lovely-pears-cross.md +.changeset/lovely-students-boil.md +.changeset/mean-tips-impress.md +.changeset/mighty-ads-worry.md +.changeset/mighty-bulldogs-begin.md +.changeset/mighty-frogs-sparkle.md +.changeset/modern-doors-watch.md +.changeset/moody-dots-refuse.md +.changeset/nervous-buses-pump.md +.changeset/nervous-kiwis-nail.md +.changeset/nervous-mirrors-perform.md +.changeset/nervous-poets-grin.md +.changeset/nervous-tables-travel.md +.changeset/new-snakes-call.md +.changeset/nice-pugs-reply.md +.changeset/old-dryers-hide.md +.changeset/polite-olives-wave.md +.changeset/pretty-plums-rescue.md +.changeset/pretty-schools-remember.md +.changeset/proud-experts-jam.md +.changeset/rare-adults-sneeze.md +.changeset/README.md +.changeset/real-masks-explode.md +.changeset/real-trains-switch.md +.changeset/rich-crews-protect.md +.changeset/rich-dots-lay.md +.changeset/rich-jars-remember.md +.changeset/rotten-spies-enjoy.md +.changeset/serious-ants-juggle.md +.changeset/silver-pots-sit.md +.changeset/silver-windows-float.md +.changeset/sixty-impalas-laugh.md +.changeset/small-olives-arrive.md +.changeset/smart-ears-refuse.md +.changeset/smart-geckos-cover.md +.changeset/smooth-papayas-boil.md +.changeset/smooth-poems-bake.md +.changeset/spotty-bees-destroy.md +.changeset/stupid-ghosts-help.md +.changeset/swift-dancers-rest.md +.changeset/swift-peas-film.md +.changeset/thin-vans-applaud.md +.changeset/thirty-baboons-punch.md +.changeset/three-baboons-bow.md +.changeset/tidy-swans-repair.md +.changeset/tidy-yaks-joke.md +.changeset/tiny-buckets-love.md +.changeset/tiny-candles-whisper.md +.changeset/tiny-chairs-build.md +.changeset/tricky-panthers-guess.md +.changeset/twenty-goats-kneel.md +.changeset/twenty-lies-switch.md +.changeset/twenty-planets-repeat.md +.changeset/violet-melons-itch.md +.changeset/violet-zebras-cry.md +.changeset/wise-spiders-jog.md +.changeset/witty-kids-talk.md +.changeset/yellow-mails-cheat.md +.changeset/young-timers-grow.md diff --git a/.prettierrc b/.prettierrc index a20502b7f0..bf357fbbc0 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,3 @@ { - "singleQuote": true, "trailingComma": "all" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 991d5b80ee..e6c87ea457 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,7 @@ clear and has sufficient instructions to be able to reproduce the issue. - Run a cobrowsing/mirroring session locally: `yarn live-stream` - Test: `yarn test` or `yarn test:watch` - Lint: `yarn lint` +- Rewrite files with prettier: `yarn format` or `yarn format:head` ## Coding style diff --git a/docs/development/coding-style.md b/docs/development/coding-style.md index d2f7598833..26227a87ea 100644 --- a/docs/development/coding-style.md +++ b/docs/development/coding-style.md @@ -1,6 +1,6 @@ # Coding Style -These are the style guidelines for coding in Electron. +These have been adapted from the style guidelines for coding in Electron. You can run `yarn lint` to show any style issues detected by `eslint`. @@ -9,6 +9,8 @@ You can run `yarn lint` to show any style issues detected by `eslint`. - End files with a newline. - Using a plain `return` when returning explicitly at the end of a function. - Not `return null`, `return undefined`, `null` or `undefined` +- run `yarn format` to rewrite all files in the standard format +- run `yarn format:head` to rewrite files from your last commit ## Documentation diff --git a/package.json b/package.json index e210dda8ce..702137ea58 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "test:watch": "yarn turbo run test:watch", "test:update": "yarn turbo run test:update", "format": "yarn prettier --write '**/*.{ts,md}'", + "format:head": "git diff --name-only HEAD^ |grep '\\.ts$\\|\\.md$' |xargs yarn prettier --write", "dev": "yarn turbo run dev", "repl": "cd packages/rrweb && npm run repl", "live-stream": "cd packages/rrweb && yarn live-stream", diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index ae1aa7bf54..f349bd2669 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -1716,29 +1716,34 @@ exports[`record integration tests can record clicks 1`] = ` \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"link\\\\n \\\\n \\", + \\"textContent\\": \\"link\\", \\"id\\": 22 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"script\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 24 - } - ], - \\"id\\": 23 - }, + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", \\"id\\": 25 } ], - \\"id\\": 21 + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 26 } ], \\"id\\": 16 @@ -15688,7 +15693,7 @@ exports[`record integration tests should record shadow DOM 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"textContent\\": \\"\\\\n \\\\n \\", \\"id\\": 39 }, { diff --git a/packages/rrweb/test/html/link.html b/packages/rrweb/test/html/link.html index 0d7b13739d..3db0817ede 100644 --- a/packages/rrweb/test/html/link.html +++ b/packages/rrweb/test/html/link.html @@ -9,6 +9,6 @@ not link - link + link diff --git a/packages/rrweb/test/html/shadow-dom.html b/packages/rrweb/test/html/shadow-dom.html index bf4c683798..fb04aea243 100644 --- a/packages/rrweb/test/html/shadow-dom.html +++ b/packages/rrweb/test/html/shadow-dom.html @@ -78,6 +78,5 @@ }); } - From cbbd1e55f1f7fa2eed9fa11e4152b509bdfd88f7 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 17 May 2024 16:28:37 +0100 Subject: [PATCH 05/12] Fixup for background-clip replacement (#1476) * Fix that the `.replace()` function only replaces the first occurrence * This should have been included in #1174 * Create fast-pets-exist.md --- .changeset/fast-pets-exist.md | 6 ++++++ packages/rrweb-snapshot/src/utils.ts | 2 +- .../test/__snapshots__/integration.test.ts.snap | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .changeset/fast-pets-exist.md diff --git a/.changeset/fast-pets-exist.md b/.changeset/fast-pets-exist.md new file mode 100644 index 0000000000..82d5216379 --- /dev/null +++ b/.changeset/fast-pets-exist.md @@ -0,0 +1,6 @@ +--- +"rrweb-snapshot": patch +"rrweb": patch +--- + +Fixup for multiple background-clip replacement diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 5ccc9082ed..97094b40e8 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -47,7 +47,7 @@ function fixBrowserCompatibilityIssuesInCSS(cssText: string): string { !cssText.includes(' -webkit-background-clip: text;') ) { cssText = cssText.replace( - ' background-clip: text;', + /\sbackground-clip:\s*text;/g, ' -webkit-background-clip: text; background-clip: text;', ); } diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index f537fc7b29..39c8c49ee1 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -364,6 +364,12 @@ exports[`integration tests [html file]: picture-in-frame.html 1`] = ` " `; +exports[`integration tests [html file]: picture-with-inline-onload.html 1`] = ` +" + \\"This + " +`; + exports[`integration tests [html file]: preload.html 1`] = ` " From 03b5216a9403f1509b4f69d1d71ef9874277fe91 Mon Sep 17 00:00:00 2001 From: Colin Maxfield Date: Fri, 17 May 2024 11:32:02 -0400 Subject: [PATCH 06/12] Replace Array.from with clean implementation (#1464) This work is to try to provide support where rrweb might be included in applications with various tools that might override Array.from so that the 2nd parameter (the map function) will always work for rrweb. Co-authored-by: Michael Dellanoce --- .changeset/eleven-bobcats-peel.md | 6 ++++++ packages/rrweb/src/record/index.ts | 14 ++++++++++++++ packages/rrweb/src/types.ts | 1 + 3 files changed, 21 insertions(+) create mode 100644 .changeset/eleven-bobcats-peel.md diff --git a/.changeset/eleven-bobcats-peel.md b/.changeset/eleven-bobcats-peel.md new file mode 100644 index 0000000000..75f7263e1f --- /dev/null +++ b/.changeset/eleven-bobcats-peel.md @@ -0,0 +1,6 @@ +--- +'rrweb-snapshot': patch +'rrweb': patch +--- + +better support for coexistence with older libraries (e.g. MooTools & Prototype.js) which modify the in-built `Array.from` function diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 7be978199d..484a8a7ecb 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -46,6 +46,20 @@ let takeFullSnapshot!: (isCheckout?: boolean) => void; let canvasManager!: CanvasManager; let recording = false; +// Multiple tools (i.e. MooTools, Prototype.js) override Array.from and drop support for the 2nd parameter +// Try to pull a clean implementation from a newly created iframe +try { + if (Array.from([1], (x) => x * 2)[0] !== 2) { + const cleanFrame = document.createElement('iframe'); + document.body.appendChild(cleanFrame); + // eslint-disable-next-line @typescript-eslint/unbound-method -- Array.from is static and doesn't rely on binding + Array.from = cleanFrame.contentWindow?.Array.from || Array.from; + document.body.removeChild(cleanFrame); + } +} catch (err) { + console.debug('Unable to override Array.from', err); +} + const mirror = createMirror(); function record( options: recordOptions = {}, diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 10ffb62b43..03c9a59fdc 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -207,6 +207,7 @@ export type missingNodeMap = { declare global { interface Window { FontFace: typeof FontFace; + Array: typeof Array; } } From 46f1b252a5919c68c68e825bd6089cc2e7d34e7c Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 22 May 2024 13:07:55 +0100 Subject: [PATCH 07/12] Fix and test for bug #1457 (#1481) * Fix and test for bug #1457 (Uncaught SyntaxError: Regular expression too large) - see test case which is extracted from a real world css file; the selector regex was able to traverse the curly brace as when looking for quotes, it wasn't taking into account that the start quote could be escaped * Apply formatting changes * Create fair-ducks-clean.md * Fix @import regex bit which was stopping consumption in the middle of a url - need to consume quotes. Thanks dave.kindel@pendo.io for reporting and isolating this case --------- Co-authored-by: eoghanmurray --- .changeset/eleven-bobcats-peel.md | 4 ++-- .changeset/fair-ducks-clean.md | 6 ++++++ packages/rrweb-snapshot/src/css.ts | 14 ++++++++++++-- packages/rrweb-snapshot/test/rebuild.test.ts | 19 +++++++++++++++++++ 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 .changeset/fair-ducks-clean.md diff --git a/.changeset/eleven-bobcats-peel.md b/.changeset/eleven-bobcats-peel.md index 75f7263e1f..e3dbcf911e 100644 --- a/.changeset/eleven-bobcats-peel.md +++ b/.changeset/eleven-bobcats-peel.md @@ -1,6 +1,6 @@ --- -'rrweb-snapshot': patch -'rrweb': patch +"rrweb-snapshot": patch +"rrweb": patch --- better support for coexistence with older libraries (e.g. MooTools & Prototype.js) which modify the in-built `Array.from` function diff --git a/.changeset/fair-ducks-clean.md b/.changeset/fair-ducks-clean.md new file mode 100644 index 0000000000..19269db760 --- /dev/null +++ b/.changeset/fair-ducks-clean.md @@ -0,0 +1,6 @@ +--- +"rrweb-snapshot": patch +"rrweb": patch +--- + +Fix and test for bug #1457 which was affecting replay of complex tailwind css diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index 1a3157d40f..220e1a1fce 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -433,7 +433,7 @@ export function parse(css: string, options: ParserOptions = {}): Stylesheet { } // Use match logic from https://github.com/NxtChg/pieces/blob/3eb39c8287a97632e9347a24f333d52d916bc816/js/css_parser/css_parse.js#L46C1-L47C1 - const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/); + const m = match(/^(((? { const pos = position(); const m = match(re); diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 097ff0989a..5d544ccfa7 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -203,4 +203,23 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img { expect(getDuration(cachedEnd) * factor).toBeLessThan(getDuration(end)); }); }); + + it('should not incorrectly interpret escaped quotes', () => { + // the ':hover' in the below is a decoy which is not part of the selector, + // previously that part was being incorrectly consumed by the selector regex + const should_not_modify = + ".tailwind :is(.before\\:content-\\[\\'\\'\\])::before { --tw-content: \":hover\"; content: var(--tw-content); }.tailwind :is(.\\[\\&\\>li\\]\\:before\\:content-\\[\\'-\\'\\] > li)::before { color: pink; }"; + expect(adaptCssForReplay(should_not_modify, cache)).toEqual( + should_not_modify, + ); + }); + + it('should not incorrectly interpret at rules', () => { + // the ':hover' in the below is a decoy which is not part of the selector, + const should_not_modify = + '@import url("https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,700;1,400&display=:hover");'; + expect(adaptCssForReplay(should_not_modify, cache)).toEqual( + should_not_modify, + ); + }); }); From 7bfebf68aad9b4fca33a348160dea3150ea07730 Mon Sep 17 00:00:00 2001 From: Colin Maxfield Date: Fri, 17 May 2024 11:32:02 -0400 Subject: [PATCH 08/12] Replace Array.from with clean implementation (#1464) This work is to try to provide support where rrweb might be included in applications with various tools that might override Array.from so that the 2nd parameter (the map function) will always work for rrweb. Co-authored-by: Michael Dellanoce --- .changeset/eleven-bobcats-peel.md | 6 ++++++ packages/rrweb/src/record/index.ts | 14 ++++++++++++++ packages/rrweb/src/types.ts | 1 + 3 files changed, 21 insertions(+) create mode 100644 .changeset/eleven-bobcats-peel.md diff --git a/.changeset/eleven-bobcats-peel.md b/.changeset/eleven-bobcats-peel.md new file mode 100644 index 0000000000..75f7263e1f --- /dev/null +++ b/.changeset/eleven-bobcats-peel.md @@ -0,0 +1,6 @@ +--- +'rrweb-snapshot': patch +'rrweb': patch +--- + +better support for coexistence with older libraries (e.g. MooTools & Prototype.js) which modify the in-built `Array.from` function diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 7be978199d..484a8a7ecb 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -46,6 +46,20 @@ let takeFullSnapshot!: (isCheckout?: boolean) => void; let canvasManager!: CanvasManager; let recording = false; +// Multiple tools (i.e. MooTools, Prototype.js) override Array.from and drop support for the 2nd parameter +// Try to pull a clean implementation from a newly created iframe +try { + if (Array.from([1], (x) => x * 2)[0] !== 2) { + const cleanFrame = document.createElement('iframe'); + document.body.appendChild(cleanFrame); + // eslint-disable-next-line @typescript-eslint/unbound-method -- Array.from is static and doesn't rely on binding + Array.from = cleanFrame.contentWindow?.Array.from || Array.from; + document.body.removeChild(cleanFrame); + } +} catch (err) { + console.debug('Unable to override Array.from', err); +} + const mirror = createMirror(); function record( options: recordOptions = {}, diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 10ffb62b43..03c9a59fdc 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -207,6 +207,7 @@ export type missingNodeMap = { declare global { interface Window { FontFace: typeof FontFace; + Array: typeof Array; } } From b2ce12aea28dddd45a4be280a588ce80d6126951 Mon Sep 17 00:00:00 2001 From: billyvg Date: Tue, 27 Aug 2024 21:26:31 +0000 Subject: [PATCH 09/12] Apply formatting changes --- .changeset/little-moons-camp.md | 4 ++-- .changeset/twenty-tables-call.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/little-moons-camp.md b/.changeset/little-moons-camp.md index 776f214f40..1904b81687 100644 --- a/.changeset/little-moons-camp.md +++ b/.changeset/little-moons-camp.md @@ -1,6 +1,6 @@ --- -'rrweb-snapshot': minor -'rrweb': minor +"rrweb-snapshot": minor +"rrweb": minor --- feat: Better masking of option/radio/checkbox values diff --git a/.changeset/twenty-tables-call.md b/.changeset/twenty-tables-call.md index add796cc59..55387fa5b5 100644 --- a/.changeset/twenty-tables-call.md +++ b/.changeset/twenty-tables-call.md @@ -1,6 +1,6 @@ --- -'rrweb-snapshot': patch -'rrweb': patch +"rrweb-snapshot": patch +"rrweb": patch --- Add `maskAttributesFn` to be called when transforming an attribute. This is typically used to determine if an attribute should be masked or not. From 8c331dd0c7d0e20fe0acef2fd18bb19762cc1cb7 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 28 Aug 2024 11:39:16 -0400 Subject: [PATCH 10/12] ci From 5b202fc8e9d79fbc6300741b4be8cdcb61c9b3f7 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 28 Aug 2024 12:03:57 -0400 Subject: [PATCH 11/12] remove mirror export --- packages/rrweb/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/rrweb/src/index.ts b/packages/rrweb/src/index.ts index 9c65c6cdd9..a096d47a07 100644 --- a/packages/rrweb/src/index.ts +++ b/packages/rrweb/src/index.ts @@ -29,7 +29,6 @@ export { deserializeArg } from './replay/canvas/deserialize-args'; export { CanvasManager, takeFullSnapshot, - mirror, freezePage, addCustomEvent, } from './record'; From cbc4e924c65c83cc2366d2784027e420bddb5e68 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 28 Aug 2024 16:46:42 -0400 Subject: [PATCH 12/12] skipping tests --- packages/rrweb-snapshot/test/rebuild.test.ts | 38 ++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 58ffba3e59..08aff402c0 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -183,22 +183,24 @@ describe('rebuild', function () { }); }); - it('should not incorrectly interpret escaped quotes', () => { - // the ':hover' in the below is a decoy which is not part of the selector, - // previously that part was being incorrectly consumed by the selector regex - const should_not_modify = - ".tailwind :is(.before\\:content-\\[\\'\\'\\])::before { --tw-content: \":hover\"; content: var(--tw-content); }.tailwind :is(.\\[\\&\\>li\\]\\:before\\:content-\\[\\'-\\'\\] > li)::before { color: pink; }"; - expect(adaptCssForReplay(should_not_modify, cache)).toEqual( - should_not_modify, - ); - }); - - it('should not incorrectly interpret at rules', () => { - // the ':hover' in the below is a decoy which is not part of the selector, - const should_not_modify = - '@import url("https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,700;1,400&display=:hover");'; - expect(adaptCssForReplay(should_not_modify, cache)).toEqual( - should_not_modify, - ); - }); + // sentry: skipped because we've removed `adaptCssForReplay` for now + // it('should not incorrectly interpret escaped quotes', () => { + // // the ':hover' in the below is a decoy which is not part of the selector, + // // previously that part was being incorrectly consumed by the selector regex + // const should_not_modify = + // ".tailwind :is(.before\\:content-\\[\\'\\'\\])::before { --tw-content: \":hover\"; content: var(--tw-content); }.tailwind :is(.\\[\\&\\>li\\]\\:before\\:content-\\[\\'-\\'\\] > li)::before { color: pink; }"; + // expect(adaptCssForReplay(should_not_modify, cache)).toEqual( + // should_not_modify, + // ); + // }); + + // sentry: skipped because we've removed `adaptCssForReplay` for now + // it('should not incorrectly interpret at rules', () => { + // // the ':hover' in the below is a decoy which is not part of the selector, + // const should_not_modify = + // '@import url("https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,700;1,400&display=:hover");'; + // expect(adaptCssForReplay(should_not_modify, cache)).toEqual( + // should_not_modify, + // ); + // }); });