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/.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/.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/.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/.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/.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/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/.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. 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 7f52132e74..2977c94d61 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 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 036f83d131..2a6e4c8a10 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,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", "typings": "yarn turbo run typings", diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index 90d413133b..09d56a7001 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -893,7 +893,17 @@ export function parse(css: string, options: ParserOptions = {}) { */ function _compileAtrule(name: string) { - const re = new RegExp('^@' + name + '\\s*([^;]+);'); + const re = new RegExp( + '^@' + + name + + '\\s*((?:' + + [ + '(? { const pos = position(); const m = match(re); diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 502e222e8e..8d7f891470 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -200,23 +200,33 @@ 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 = ''; + } 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); return a.href; } @@ -250,7 +260,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); } @@ -635,6 +645,7 @@ function serializeNode( }); case n.TEXT_NODE: return serializeTextNode(n as Text, { + doc, maskAllText, maskTextClass, unmaskTextClass, @@ -671,6 +682,7 @@ function getRootId(doc: Document, mirror: Mirror): number | undefined { function serializeTextNode( n: Text, options: { + doc: Document; maskAllText: boolean; maskTextClass: string | RegExp; unmaskTextClass: string | RegExp | null; @@ -719,7 +731,7 @@ function serializeTextNode( n, ); } - textContent = absoluteToStylesheet(textContent, getHref()); + textContent = absoluteToStylesheet(textContent, getHref(options.doc)); } if (isScript) { textContent = 'SCRIPT_PLACEHOLDER'; @@ -873,7 +885,7 @@ function serializeElementNode( (n as HTMLStyleElement).sheet as CSSStyleSheet, ); if (cssText) { - attributes._cssText = absoluteToStylesheet(cssText, getHref()); + attributes._cssText = absoluteToStylesheet(cssText, getHref(doc)); } } // form fields diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index a059610a98..d7d824e4c2 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/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 50faad38e8..08aff402c0 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -182,4 +182,25 @@ describe('rebuild', function () { expect(getDuration(cachedEnd) * factor).toBeLessThan(getDuration(end)); }); }); + + // 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, + // ); + // }); }); 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'; diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index f433e53095..c9e26e714b 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -82,8 +82,7 @@ try { console.debug('Unable to override Array.from', err); } -export const mirror = createMirror(); - +const mirror = createMirror(); function record( options: recordOptions = {}, ): listenerHandler | undefined { diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 992b6446b3..bd6c008c44 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -1792,29 +1792,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 @@ -21637,7 +21642,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 @@ }); } -