From eb1fb6d60f4381ee87272bdceccdf6ad5297284f Mon Sep 17 00:00:00 2001
From: Shay Malchi <shaymalchi@gmail.com>
Date: Wed, 6 Nov 2024 10:12:29 +0200
Subject: [PATCH] Merge upstream from remote rrweb (#26)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Skip mask check on leaf elements (#1512)

* Minor fixup for #1349; the 'we can avoid the check on leaf elements' optimisation wasn't being applied as `n.childNodes` was always truthy even when there were no childNodes.

Changing it to `n.childNodes.length` directly there (see #1402) actually caused a bug as during a mutation, we serialize the text node directly, and need to jump to the parentElement to do the check.
This is why I've reimplemented this optimisation inside `needMaskingText` where we are already had an `isElement` test

Thanks to @Paulhejia (https://github.com/Paulhejia/rrweb/) for spotting that `Boolean(n.childNodes)` is aways true.

* Assuming all jest should have been removed in #1033 (#1511)

* all references to jest should have been removed in #1033
* clarify that `cross-env` is used to ensure that environmental variables get applied on Windows (previous usage of cross-env was removed in #1033)

* Fix async assertions in test files (#1510)

* fix: await assertSnapshot in test files for async assertions

* Fix maskInputFn is ignored during the creation of the full snapshot (#1386)

Fix that the optional `maskInputFn` was being accidentally ignored during the creation of the full snapshot

* Improve development tooling (#1516)

- Running `yarn build` in a `packages/*/` directory will trigger build of all dependencies too, and cache them if possible.
- Fix for `yarn dev` breaking for `rrweb` package whenever changing files in `rrweb` package
- Update typescript, turbo, vite and vite-plugin-dts
- Require `workspaces-to-typescript-project-references` from `prepublish`

* Version Packages (alpha) (#1513)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Keep all packages in sync

* feat: add new css parser - postcss (#1458)

* feat: add new css parser

* make selectors change

* selectors and tests

* media changes

* remove old css references

* better variable name

* use postcss and port tests

* fix media test

* inline plugins

* fix failing multiline selector

* correct test result

* move tests to correct file

* cleanup all tests

* remove unused css-tree

* update bundle

* cleanup dependencies

* revert config files to master

* remove d.ts files

* update snapshot

* reset rebuilt test

* apply fuzzy css matching

* remove extra test

* Fix imports

* Newer versions of nswapi break rrdom-nodejs tests.
Example:
 FAIL  test/document-nodejs.test.ts > RRDocument for nodejs environment > RRDocument API > querySelectorAll
TypeError: e[api] is not a function
 ❯ byTag ../../node_modules/nwsapi/src/nwsapi.js:390:37
 ❯ Array.<anonymous> ../../node_modules/nwsapi/src/nwsapi.js:327:113
 ❯ collect ../../node_modules/nwsapi/src/nwsapi.js:1578:32
 ❯ Object._querySelectorAll [as select] ../../node_modules/nwsapi/src/nwsapi.js:1533:36
 ❯ RRDocument.querySelectorAll src/document-nodejs.ts:96:24

* Migrate from jest to vitest

* Order of selectors has changed with postcss

* Remove unused eslint

---------

Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>

* fix: console assert only logs when arg 0 is falsy (#1530)

* fix: console assert only logs when arg 0 is falsy

* [Feature] Include takeFullSnapshot function in rrweb (#1527)

* export takeFullSnapshot function in rrweb

* chore: reduce flakey test due to '[vite] connected' message (#1525)

* fix: duplicate textContent for style element cause incremental style mutation invalid (#1417)

fix style element corner case
 - historically we have recorded duplicated css content in certain cases (demonstrated by the attached replayer test). This fix ensures that the replayer doesn't doubly add the content, which can cause problems when further mutations occur
---------
Review and further tests contributed by: Eoghan Murray <eoghan@getthere.ie>

* Added support for deprecated addRule & removeRule methods (#1515)

* Added support for deprecated addRule & removeRule methods

* Respect addRule default value

* fix: nested stylesheets should have absolute URLs (#1533)

* Replace relative URLs with absolute URLs when stringifying stylesheets

* Add test to show desired behavior for imported stylesheets from seperate directory

* Rename `absoluteToStylesheet` to `absolutifyURLs` and call it once after stringifying imported stylesheet

* Don't create the intermediary array of the spread operator

* Formalize that `stringifyRule` should expect a sheet href

* Ensure a <style> element can also import and gets it's url absolutized

* Handle case where non imported stylesheet has relative urls that need to be absolutified

* Clarify in test files where jpegs are expected to appear in absolutified urls

* Move absolutifyURLs call for import rules out of trycatch

* Add a benchmarking test for stringifyStylesheet

* Avoid the duplication on how to fall back

---------

Co-authored-by: Eoghan Murray <eoghan@getthere.ie>
Co-authored-by: eoghanmurray <eoghanmurray@users.noreply.github.com>

* Support top-layer <dialog> recording & replay (#1503)

* chore: its important to run `yarn build:all` before running `yarn dev`

* feat: trigger showModal from rrdom and rrweb

* feat: Add support for replaying modal and non modal dialog elements

* chore: Update dev script to remove CLEAR_DIST_DIR flag

* Get modal recording and replay working

* DRY up dialog test and dedupe snapshot images

* feat: Refactor dialog test to use updated attribute name

* feat: Update dialog test to include rr_open attribute

* chore: Add npm dependency happy-dom@14.12.0

* Add more test cases for dialog

* Clean up naming

* Refactor dialog open code

* Revert changed code that doesn't do anything

* Add documentation for unimplemented type

* chore: Remove unnecessary comments in dialog.test.ts

* rename rr_open to rr_openMode

* Replace todo with a skipped test

* Add better logging for CI

* Rename rr_openMode to rr_open_mode

rrdom downcases all attribute names which made `rr_openMode` tricky to deal with

* Remove unused images

* Move after iframe append based on @YunFeng0817's comment
https://github.com/rrweb-io/rrweb/pull/1503#discussion_r1666363931

* Remove redundant dialog handling from rrdom.

rrdom already handles dialog element creation it's self

* Rename variables for dialog handling in rrweb replay module

* Update packages/rrdom/src/document.ts

---------

Co-authored-by: Eoghan Murray <eoghan@getthere.ie>

* Added session downloader for chrome extension (#1522)

* Added session downloader for chrome extension

- The session list now has a button to download sessions as .json files for use with rrweb-player
- Improved styling for the delete and download buttons

* Reverse monkey patch built in methods to support LWC (#1509)

* Get around monkey patched Nodes

* inlineImages: Setting of `image.crossOrigin` is not always necessary (#1468)

Setting of the `crossorigin` attribute is not necessary for same-origin images, and causes an immediate image reload (albeit from cache) necessitating the use of a load event listener which subsequently mutates the snapshot.  This change allows us to  avoid the mutation of the snapshot for the same-origin case.

* Modify inlineImages test to remove delay and show that we can inline images without mutation

* Add an explicit test for when the `image.crossOrigin = 'anonymous';` method is necessary.  Uses a combination of about:blank and our test server to simulate a cross-origin context

* Other test changes: there were some spurious rrweb mutations being generated by the addition of the crossorigin attribute that are now elimnated from the rrweb/__snapshots__/integration.test.ts.snap after this PR - this is good

* Move `childNodes` to @rrweb/utils

* Use non-monkey patched versions of the `childNodes`, `parentNode` `parentElement` `textContent` accessors

* Add getRootNode and contains, and add comprehensive todo list

* chore: Update turbo.json tasks for better build process

* Update caniuse-lite

* chore: Update eslint-plugin-compat to version 5.0.0

* chore: Bump @rrweb/utils version to 2.0.0-alpha.15

* delete unused yarn.lock files

* Set correct @rrweb/utils version in package.json

* Migrate over some accessors to reverse-monkey-patched version

* Add missing functions

* Fix illegal invocation error

* Revert closer to what it was.

This feels incorrect to me (Justin Halsall), but some of the tests break without it so I'm restoring this to be closer to its original here:
https://github.com/rrweb-io/rrweb/blame/cfd686d488a9b88dba6b6f8880b5e4375dd8062c/packages/rrweb-snapshot/src/snapshot.ts#L1011

* Reverse monkey patch all methods LWC hijacks

* Make tests more stable

* Safely handle rrdom nodes in hasShadowRoot

* Remove duplicated test

* Use variable `serverURL` in test

* Use monorepo default browserlist

* Fix typing issue for new typescript

* Remove unused package

* Remove unused code

* Add prefix to reverse-monkey-patched methods to make them more explicit

* Add default exports to @rrweb/utils

---------

Co-authored-by: Eoghan Murray <eoghan@getthere.ie>

* Version Packages (alpha) (#1526)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Single style capture (#1437)

Support a contrived/rare case where a <style> element has multiple text node children (this is usually only possible to recreate via javascript append) ... this PR fixes cases where there are subsequent text mutations to these nodes; previously these would have been lost

* In this scenario, a new CSS comment may now be inserted into the captured `_cssText` for a <style> element to show where it should be broken up into text elements upon replay: `/* rr_split */`
* The new 'can record and replay style mutations' test is the principal way to the problematic scenarios, and is a detailed 'catch-all' test with many checks to cover most of the ways things can fail
* There are new tests for splitting/rebuilding the css using the rr_split marker
* The prior 'dynamic stylesheet' route is now the main route for serializing a stylesheet; dynamic stylesheet were missed out in #1533 but that case is now covered with this PR

This PR was originally extracted from #1475 so the  initial motivation was to change the approach on stringifying <style> elements to do so in a single place.  This is also the motivating factor for always serializing <style> elements via the `_cssText` attribute rather than in it's childNodes; in #1475 we will be delaying populating `_cssText` for performance and instead recorrding them as assets.

Thanks for the detailed review to  Justin Halsall <Juice10@users.noreply.github.com> & Yun Feng <https://github.com/YunFeng0817>

* Simplify the hover replacement function (#1535)

Simplify the hover replacement function, which has been borrowed from postcss-pseudo-classes

Note: 'parses nested commas in selectors correctly' was failing after this PR, however I don't think that the previous behaviour was desirable, so have added a new test to formalize this expectation

* fix some typos in optimize-storage.md (#1565)

* fix some typos in optimize-storage.md

* Update docs/recipes/optimize-storage.md

* Create metal-mugs-mate.md

---------

Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>

* fix(rrdom): Ignore invalid DOM attributes when diffing (#1561)

* fix(rrdom): Ignore invalid DOM attributes when diffing (#213)

We encountered an issue where replays with invalid attributes (e.g.
`@click`) would break rendering the replay after seeking. The exception
bubbles up to
[here](https://github.com/rrweb-io/rrweb/blob/62093d4385a09eb0980c2ac02d97eea5ce2882be/packages/rrweb/src/replay/index.ts#L270-L279),
which means the replay will continue to play, but the replay mirror will
be incomplete.

Closes https://github.com/getsentry/team-replay/issues/458

* add changeset

* fix(snapshot): dimensions for blocked element not being applied (#1331)

fix for replay of a blocked element when using 'fast forward' (rrdom)

 - Dimensions were not being properly applied when you seek to a position in the replay. Need to use `setProperty` rather than trying to set the width/height directly

* ref: isParentRemoved to cache subtree (#1543)

* ref: isParentRemoved to cache subtree
* ref: cache at insertion too
* ref: remove wrapper function

---------

Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>

* changeset to 2.0.13

* fix snapshot build

---------

Co-authored-by: Eoghan Murray <eoghan@getthere.ie>
Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>
Co-authored-by: Alexey Babik <alexey.babik@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: David Newell <david@posthog.com>
Co-authored-by: Paul D'Ambra <paul@posthog.com>
Co-authored-by: Christopher Arredondo <arredgroup@gmail.com>
Co-authored-by: Yun Feng <yun.feng0817@gmail.com>
Co-authored-by: minja malešević <33005827+okejminja@users.noreply.github.com>
Co-authored-by: Jeff Nguyen <jeffduy@gmail.com>
Co-authored-by: eoghanmurray <eoghanmurray@users.noreply.github.com>
Co-authored-by: Arun Kunigiri <kirankunigiri@gmail.com>
Co-authored-by: Riadh Mouamnia <85134557+riadhmouamnia@users.noreply.github.com>
Co-authored-by: Billy Vong <billyvg@users.noreply.github.com>
Co-authored-by: Jonas <jonas@badalic.com>
Co-authored-by: Shay Malchi <shay@saola.ai>
---
 .changeset/config.json                        |   11 +
 .github/workflows/ci-cd.yml.disabled          |    2 +-
 .vscode/rrweb-monorepo.code-workspace         |    8 +-
 README.md                                     |    2 +-
 docs/recipes/optimize-storage.md              |    6 +-
 guide.md                                      |    1 +
 guide.zh_CN.md                                |    3 +-
 package.json                                  |   18 +-
 packages/all/CHANGELOG.md                     |   22 +
 packages/all/package.json                     |   18 +-
 .../test/cross-origin-iframe-packer.test.ts   |    2 +-
 packages/packer/CHANGELOG.md                  |   18 +
 packages/packer/package.json                  |   14 +-
 packages/packer/src/pack.ts                   |    2 +-
 packages/packer/src/unpack.ts                 |    2 +-
 .../CHANGELOG.md                              |   18 +
 .../package.json                              |   16 +-
 .../CHANGELOG.md                              |   18 +
 .../package.json                              |   16 +-
 .../rrweb-plugin-console-record/CHANGELOG.md  |   20 +
 .../rrweb-plugin-console-record/package.json  |   16 +-
 .../rrweb-plugin-console-record/src/index.ts  |   12 +-
 .../test/__snapshots__/index.test.ts.snap     |   41 +-
 .../test/index.test.ts                        |    9 +-
 .../rrweb-plugin-console-replay/CHANGELOG.md  |   18 +
 .../rrweb-plugin-console-replay/package.json  |   18 +-
 .../rrweb-plugin-console-replay/src/index.ts  |    4 +-
 .../CHANGELOG.md                              |   18 +
 .../package.json                              |   16 +-
 .../CHANGELOG.md                              |   18 +
 .../package.json                              |   18 +-
 packages/record/CHANGELOG.md                  |   22 +
 packages/record/package.json                  |   16 +-
 packages/replay/CHANGELOG.md                  |   20 +
 packages/replay/package.json                  |   16 +-
 packages/replay/src/index.ts                  |    7 +-
 packages/rrdom-nodejs/CHANGELOG.md            |   26 +
 packages/rrdom-nodejs/package.json            |   21 +-
 packages/rrdom-nodejs/src/document-nodejs.ts  |    4 +-
 packages/rrdom/CHANGELOG.md                   |   27 +
 packages/rrdom/package.json                   |   16 +-
 packages/rrdom/src/diff.ts                    |   38 +-
 packages/rrdom/src/document.ts                |   27 +-
 packages/rrdom/src/index.ts                   |    6 +
 packages/rrdom/test/diff.test.ts              |   26 +
 packages/rrdom/test/diff/dialog.test.ts       |  112 +
 packages/rrdom/test/virtual-dom.test.ts       |   28 +
 packages/rrvideo/CHANGELOG.md                 |   18 +-
 packages/rrvideo/package.json                 |    6 +-
 .../rrweb-player/.svelte-kit/ambient.d.ts     |   64 +-
 packages/rrweb-player/CHANGELOG.md            |   20 +
 packages/rrweb-player/package.json            |   10 +-
 packages/rrweb-snapshot/CHANGELOG.md          |   28 +
 packages/rrweb-snapshot/package.json          |   25 +-
 packages/rrweb-snapshot/src/css.ts            | 1012 +-----
 packages/rrweb-snapshot/src/rebuild.ts        |  201 +-
 packages/rrweb-snapshot/src/snapshot.ts       |  232 +-
 packages/rrweb-snapshot/src/types.ts          |   39 +-
 packages/rrweb-snapshot/src/utils.ts          |  186 +-
 .../__snapshots__/integration.test.ts.snap    |  352 +-
 .../rrweb-snapshot/test/alt-css/alt-style.css |   12 +
 packages/rrweb-snapshot/test/css.test.ts      |  445 ++-
 .../test/css/style-with-import.css            |    1 +
 packages/rrweb-snapshot/test/css/style.css    |    4 +-
 packages/rrweb-snapshot/test/html/dialog.html |    5 +
 .../test/html/monkey-patched-elements.html    |   45 +
 .../html/with-style-sheet-with-import.html    |    4 +
 .../rrweb-snapshot/test/integration.test.ts   |  112 +-
 packages/rrweb-snapshot/test/rebuild.test.ts  |   89 +-
 packages/rrweb-snapshot/test/snapshot.test.ts |  134 +-
 .../test/stringify-stylesheet.bench.ts        |   37 +
 packages/rrweb-snapshot/test/utils.test.ts    |   83 +-
 packages/rrweb-snapshot/tsconfig.json         |   10 +-
 packages/rrweb-snapshot/vitest.config.ts      |    2 +-
 packages/rrweb/CHANGELOG.md                   |   47 +
 packages/rrweb/package.json                   |   31 +-
 packages/rrweb/src/index.ts                   |   11 +-
 packages/rrweb/src/record/index.ts            |   23 +-
 packages/rrweb/src/record/mutation.ts         |  128 +-
 packages/rrweb/src/record/observer.ts         |   71 +-
 .../rrweb/src/record/observers/canvas/2d.ts   |    8 +-
 .../record/observers/canvas/canvas-manager.ts |    1 -
 .../src/record/observers/canvas/webgl.ts      |   15 +-
 .../rrweb/src/record/shadow-dom-manager.ts    |   12 +-
 .../rrweb/src/record/stylesheet-manager.ts    |    2 +-
 packages/rrweb/src/replay/canvas/index.ts     |    6 +-
 packages/rrweb/src/replay/canvas/webgl.ts     |    2 +-
 packages/rrweb/src/replay/dialog/index.ts     |   67 +
 packages/rrweb/src/replay/index.ts            |  107 +-
 packages/rrweb/src/replay/machine.ts          |   11 +-
 packages/rrweb/src/replay/media/index.ts      |    2 +-
 packages/rrweb/src/replay/timer.ts            |    4 +-
 packages/rrweb/src/utils.ts                   |   56 +-
 .../__snapshots__/integration.test.ts.snap    | 3126 +++++++++++------
 .../test/__snapshots__/record.test.ts.snap    |  152 +-
 packages/rrweb/test/events/bad-style.ts       |  231 ++
 packages/rrweb/test/events/dialog-playback.ts |  458 +++
 packages/rrweb/test/html/dialog.html          |    5 +
 packages/rrweb/test/html/style.html           |   31 +
 packages/rrweb/test/integration.test.ts       |  409 ++-
 packages/rrweb/test/record.test.ts            |  102 +-
 .../record/__snapshots__/dialog.test.ts.snap  |  487 +++
 .../test/record/cross-origin-iframes.test.ts  |   36 +-
 packages/rrweb/test/record/dialog.test.ts     |  229 ++
 packages/rrweb/test/record/webgl.test.ts      |   16 +-
 ...log-closed-dialogs-show-nothing-1-snap.png |  Bin 0 -> 10592 bytes
 ...al-in-incremental-snapshot-alternative.png |  Bin 0 -> 12721 bytes
 ...ith-show-modal-in-incremental-snapshot.png |  Bin 0 -> 12721 bytes
 ...again-when-open-attribute-gets-removed.png |  Bin 0 -> 10592 bytes
 ...open-dialog-with-show-in-full-snapshot.png |  Bin 0 -> 12261 bytes
 ...ialog-with-show-modal-in-full-snapshot.png |  Bin 0 -> 12505 bytes
 ...log-should-open-dialog-with-show-modal.png |  Bin 0 -> 12690 bytes
 ...uld-switch-between-show-and-show-modal.png |  Bin 0 -> 12690 bytes
 ...uld-switch-between-show-modal-and-show.png |  Bin 0 -> 12445 bytes
 ...-dialog-when-open-attribute-gets-added.png |  Bin 0 -> 12445 bytes
 packages/rrweb/test/replay/dialog.test.ts     |  159 +
 packages/rrweb/test/replayer.test.ts          |   26 +-
 packages/rrweb/test/utils.ts                  |   34 +-
 packages/rrweb/tsconfig.json                  |    5 +-
 packages/types/CHANGELOG.md                   |   27 +
 packages/types/package.json                   |   20 +-
 packages/utils/CHANGELOG.md                   |   13 +
 packages/utils/Readme.md                      |  178 +
 packages/utils/package.json                   |   53 +
 packages/utils/src/index.ts                   |  221 ++
 packages/utils/tsconfig.json                  |   10 +
 packages/utils/vite.config.js                 |    4 +
 packages/web-extension/CHANGELOG.md           |   30 +
 packages/web-extension/package.json           |   11 +-
 .../web-extension/src/background/index.ts     |    6 +-
 .../src/components/CircleButton.tsx           |    2 +-
 .../src/components/SidebarWithHeader.tsx      |    4 +-
 packages/web-extension/src/content/index.ts   |   12 +-
 packages/web-extension/src/content/inject.ts  |    2 +-
 .../web-extension/src/pages/SessionList.tsx   |   58 +-
 packages/web-extension/src/popup/App.tsx      |   12 +-
 packages/web-extension/src/utils/channel.ts   |    2 +-
 packages/web-extension/src/utils/recording.ts |    6 +-
 packages/web-extension/src/utils/storage.ts   |   19 +
 packages/web-extension/vite.config.ts         |    4 +-
 tsconfig.base.json                            |    2 +-
 turbo.json                                    |   27 +-
 vite.config.default.ts                        |    3 +-
 yarn.lock                                     |  217 +-
 144 files changed, 7624 insertions(+), 3422 deletions(-)
 create mode 100644 packages/rrdom/test/diff/dialog.test.ts
 create mode 100644 packages/rrweb-snapshot/test/alt-css/alt-style.css
 create mode 100644 packages/rrweb-snapshot/test/html/dialog.html
 create mode 100644 packages/rrweb-snapshot/test/html/monkey-patched-elements.html
 create mode 100644 packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts
 create mode 100644 packages/rrweb/src/replay/dialog/index.ts
 create mode 100644 packages/rrweb/test/events/bad-style.ts
 create mode 100644 packages/rrweb/test/events/dialog-playback.ts
 create mode 100644 packages/rrweb/test/html/dialog.html
 create mode 100644 packages/rrweb/test/html/style.html
 create mode 100644 packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap
 create mode 100644 packages/rrweb/test/record/dialog.test.ts
 create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-closed-dialogs-show-nothing-1-snap.png
 create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-alternative.png
 create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot.png
 create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed.png
 create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot.png
 create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot.png
 create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal.png
 create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-and-show-modal.png
 create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show.png
 create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-the-dialog-when-open-attribute-gets-added.png
 create mode 100644 packages/rrweb/test/replay/dialog.test.ts
 create mode 100644 packages/utils/CHANGELOG.md
 create mode 100644 packages/utils/Readme.md
 create mode 100644 packages/utils/package.json
 create mode 100644 packages/utils/src/index.ts
 create mode 100644 packages/utils/tsconfig.json
 create mode 100644 packages/utils/vite.config.js

diff --git a/.changeset/config.json b/.changeset/config.json
index 9dea67bc61..e51f8aa68d 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -9,6 +9,17 @@
   "commit": false,
   "fixed": [
     [
+      "@saola.ai/rrweb-all",
+      "@saola.ai/replay",
+      "@saola.ai/record",
+      "@saola.ai/rrweb-packer",
+      "@saola.ai/rrweb-utils",
+      "@saola.ai/rrweb-plugin-console-record",
+      "@saola.ai/rrweb-plugin-console-replay",
+      "@saola.ai/rrweb-plugin-sequential-id-record",
+      "@saola.ai/rrweb-plugin-sequential-id-replay",
+      "@saola.ai/rrweb-plugin-canvas-webrtc-record",
+      "@saola.ai/rrweb-plugin-canvas-webrtc-replay",
       "@saola.ai/rrweb",
       "@saola.ai/rrweb-snapshot",
       "@saola.ai/rrdom",
diff --git a/.github/workflows/ci-cd.yml.disabled b/.github/workflows/ci-cd.yml.disabled
index 6351b826bd..c0c7b306a3 100644
--- a/.github/workflows/ci-cd.yml.disabled
+++ b/.github/workflows/ci-cd.yml.disabled
@@ -41,5 +41,5 @@ jobs:
         if: failure()
         with:
           name: image-diff
-          path: packages/rrweb/test/*/__image_snapshots__/__diff_output__/*.png
+          path: packages/**/__image_snapshots__/__diff_output__/*.png
           if-no-files-found: ignore
diff --git a/.vscode/rrweb-monorepo.code-workspace b/.vscode/rrweb-monorepo.code-workspace
index 98338cf192..ecb1672def 100644
--- a/.vscode/rrweb-monorepo.code-workspace
+++ b/.vscode/rrweb-monorepo.code-workspace
@@ -40,6 +40,10 @@
       "name": "@rrweb/types",
       "path": "../packages/types"
     },
+    {
+      "name": "@rrweb/utils",
+      "path": "../packages/utils"
+    },
     {
       "name": "@rrweb/packer",
       "path": "../packages/packer"
@@ -88,12 +92,14 @@
       "@rrweb/record",
       "@rrweb/replay",
       "@rrweb/types",
+      "@rrweb/utils",
       "@rrweb/packer",
       "@rrweb/rrweb-plugin-console-record",
       "@rrweb/rrweb-plugin-console-replay",
       "@rrweb/rrweb-plugin-sequential-id",
       "@rrweb/rrweb-plugin-canvas-webrtc-record",
       "@rrweb/rrweb-plugin-canvas-webrtc-replay"
-    ]
+    ],
+    "typescript.tsdk": " rrweb monorepo/node_modules/typescript/lib"
   }
 }
diff --git a/README.md b/README.md
index 17e6b5591a..e73c2a8768 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,7 @@ Since we want the record and replay sides to share a strongly typed data structu
 
 1. Fork this repository.
 2. Run `yarn install` in the root to install required dependencies for all sub-packages (note: `npm install` is _not_ recommended).
-3. Run `yarn dev` in the root to get auto-building for all the sub-packages whenever you modify anything.
+3. Run `yarn build:all` to build all packages and get a stable base, then `yarn dev` in the root to get auto-building for all the sub-packages whenever you modify anything.
 4. Navigate to one of the sub-packages (in the `packages` folder) where you'd like to make a change.
 5. Patch the code and run `yarn test` to run the tests, make sure they pass before you commit anything. Add test cases in order to avoid future regression.
 6. If tests are failing, but the change in output is desirable, run `yarn test:update` and carefully commit the changes in test output.
diff --git a/docs/recipes/optimize-storage.md b/docs/recipes/optimize-storage.md
index a50118bf89..9a67d10331 100644
--- a/docs/recipes/optimize-storage.md
+++ b/docs/recipes/optimize-storage.md
@@ -49,7 +49,7 @@ rrweb.record({
 rrweb.record({
   emit(event) {},
   sampling: {
-    // Configure which kins of mouse interaction should be recorded
+    // Configure which kinds of mouse interaction should be recorded
     mouseInteraction: {
       MouseUp: false,
       MouseDown: false,
@@ -78,7 +78,7 @@ import { pack } from '@rrweb/packer';
 
 rrweb.record({
   emit(event) {},
-  packFn: rrweb.pack,
+  packFn: pack,
 });
 ```
 
@@ -88,7 +88,7 @@ And you need to pass packer.unpack as the `unpackFn` in replaying.
 import { unpack } from '@rrweb/packer';
 
 const replayer = new rrweb.Replayer(events, {
-  unpackFn: rrweb.unpack,
+  unpackFn: unpack,
 });
 ```
 
diff --git a/guide.md b/guide.md
index bfdb2d14fd..764e359fb4 100644
--- a/guide.md
+++ b/guide.md
@@ -47,6 +47,7 @@ Besides the `rrweb` and `@rrweb/record` packages, rrweb also provides other pack
 - [@rrweb/replay](packages/replay): A package for replaying rrweb sessions.
 - [@rrweb/packer](packages/packer): A package for packing and unpacking rrweb data.
 - [@rrweb/types](packages/types): Contains types shared across rrweb packages.
+- [@rrweb/utils](packages/utils): Contains utility functions shared across rrweb packages.
 - [web-extension](packages/web-extension): A web extension for rrweb.
 - [rrvideo](packages/rrvideo): A package for handling video operations in rrweb.
 - [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record): A plugin for recording console logs.
diff --git a/guide.zh_CN.md b/guide.zh_CN.md
index 1d56998d74..4078cb2b6a 100644
--- a/guide.zh_CN.md
+++ b/guide.zh_CN.md
@@ -43,7 +43,8 @@ rrweb 代码分为录制和回放两部分,大多数时候用户在被录制
 - [@rrweb/record](packages/record):一个用于录制 rrweb 会话的包。
 - [@rrweb/replay](packages/replay):一个用于回放 rrweb 会话的包。
 - [@rrweb/packer](packages/packer):一个用于打包和解包 rrweb 数据的包。
-- [@rrweb/types](packages/types):包含 rrweb 中使用的类型定义。
+- [@rrweb/types](packages/types):包含 rrweb 包中共享的类型定义。
+- [@rrweb/utils](packages/utils):包含 rrweb 包中共享的工具函数。
 - [web-extension](packages/web-extension):rrweb 的网页扩展。
 - [rrvideo](packages/rrvideo):一个用于处理 rrweb 中视频操作的包。
 - [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record):一个用于记录控制台日志的插件。
diff --git a/package.json b/package.json
index 1fdbace462..e674515625 100644
--- a/package.json
+++ b/package.json
@@ -30,26 +30,29 @@
     "@typescript-eslint/parser": "^5.62.0",
     "browserslist": "^4.22.1",
     "concurrently": "^7.1.0",
+    "cross-env": "^7.0.3",
     "esbuild-plugin-umd-wrapper": "^2.0.0",
     "eslint": "^8.53.0",
-    "eslint-plugin-compat": "^4.2.0",
+    "eslint-plugin-compat": "^5.0.0",
     "eslint-plugin-jest": "^27.6.0",
     "eslint-plugin-tsdoc": "^0.2.17",
+    "happy-dom": "^14.12.0",
     "markdownlint": "^0.25.1",
     "markdownlint-cli": "^0.31.1",
     "prettier": "2.8.4",
-    "turbo": "^2.0.3",
-    "typescript": "^4.9.5"
+    "turbo": "^2.0.4",
+    "typescript": "^5.4.5"
   },
   "scripts": {
-    "build:all": "NODE_OPTIONS='--max-old-space-size=4096' yarn run concurrently --success=all -r -m=1 'yarn workspaces-to-typescript-project-references' 'yarn turbo run prepublish'",
-    "test": "yarn run concurrently --success=all -r -m=1 'yarn workspaces-to-typescript-project-references --check' 'yarn turbo run test --concurrency=1 --continue'",
+    "build:all": "NODE_OPTIONS='--max-old-space-size=4096' yarn turbo run prepublish",
+    "references:update": "yarn workspaces-to-typescript-project-references",
+    "test": "yarn turbo run test --concurrency=1 --continue",
     "test:watch": "yarn turbo run test:watch",
     "test:update": "yarn turbo run test:update",
     "check-types": "yarn turbo run check-types --continue",
     "format": "yarn prettier --write '**/*.{ts,md}'",
     "format:head": "git diff --name-only HEAD^ |grep '\\.ts$\\|\\.md$' |xargs yarn prettier --write",
-    "dev": "CLEAR_DIST_DIR=false yarn turbo run dev --concurrency=17",
+    "dev": "yarn turbo run dev --concurrency=18",
     "repl": "cd packages/rrweb && npm run repl",
     "live-stream": "cd packages/rrweb && yarn live-stream",
     "lint": "yarn run concurrently --success=all -r -m=1 'yarn run markdownlint docs' 'yarn eslint packages/*/src --ext .ts,.tsx,.js,.jsx,.svelte'",
@@ -58,7 +61,8 @@
   },
   "resolutions": {
     "**/cssom": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
-    "**/@types/dom-webcodecs": "0.1.5"
+    "**/@types/dom-webcodecs": "0.1.5",
+    "typescript": "^5.4.5"
   },
   "browserslist": [
     "defaults",
diff --git a/packages/all/CHANGELOG.md b/packages/all/CHANGELOG.md
index 44b1bfd6b3..c7b8963501 100644
--- a/packages/all/CHANGELOG.md
+++ b/packages/all/CHANGELOG.md
@@ -1,5 +1,27 @@
 # @rrweb/all
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb-packer@2.0.13
+  - @saola.ai/rrweb@2.0.13
+  - @saola.ai/rrweb-types@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages
+
+- Updated dependencies [[`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c), [`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - @rrweb/packer@2.0.0-alpha.17
+  - rrweb@2.0.0-alpha.17
+  - @rrweb/types@2.0.0-alpha.17
+
 ## 2.0.0
 
 ### Major Changes
diff --git a/packages/all/package.json b/packages/all/package.json
index e2ec6cc061..db0833a818 100644
--- a/packages/all/package.json
+++ b/packages/all/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/rrweb-all",
-  "version": "2.0.0",
+  "version": "2.0.13",
   "publishConfig": {
     "access": "public"
   },
@@ -10,11 +10,11 @@
   ],
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "test": "vitest run",
     "test:watch": "vitest watch",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build",
+    "prepublish": "tsc -noEmit && vite build",
     "lint": "yarn eslint src/**/*.ts"
   },
   "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/all#readme",
@@ -50,15 +50,15 @@
   ],
   "devDependencies": {
     "puppeteer": "^20.9.0",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1",
     "vitest": "^1.4.0",
-    "typescript": "^4.7.3"
+    "typescript": "^5.4.5"
   },
   "dependencies": {
-    "@saola.ai/rrweb-types": "^2.0.0",
-    "@saola.ai/rrweb-packer": "^2.0.0",
-    "@saola.ai/rrweb": "^2.0.0"
+    "@saola.ai/rrweb-types": "^2.0.13",
+    "@saola.ai/rrweb-packer": "^2.0.13",
+    "@saola.ai/rrweb": "^2.0.13"
   },
   "browserslist": [
     "supports es6-class"
diff --git a/packages/all/test/cross-origin-iframe-packer.test.ts b/packages/all/test/cross-origin-iframe-packer.test.ts
index f5df997977..ed8039d35c 100644
--- a/packages/all/test/cross-origin-iframe-packer.test.ts
+++ b/packages/all/test/cross-origin-iframe-packer.test.ts
@@ -149,7 +149,7 @@ describe('cross origin iframes & packer', function (this: ISuite) {
         const unpackedSnapshots = packedSnapshots.map((packed) =>
           unpack(packed),
         ) as eventWithTime[];
-        assertSnapshot(unpackedSnapshots);
+        await assertSnapshot(unpackedSnapshots);
       });
     });
   });
diff --git a/packages/packer/CHANGELOG.md b/packages/packer/CHANGELOG.md
index 1f2f6138ea..9e037c4883 100644
--- a/packages/packer/CHANGELOG.md
+++ b/packages/packer/CHANGELOG.md
@@ -1,5 +1,23 @@
 # @rrweb/packer
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb-types@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages
+
+- Updated dependencies [[`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158)]:
+  - @rrweb/types@2.0.0-alpha.17
+
 ## 2.0.0
 
 ### Major Changes
diff --git a/packages/packer/package.json b/packages/packer/package.json
index 177d046af8..43d545c6e2 100644
--- a/packages/packer/package.json
+++ b/packages/packer/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/rrweb-packer",
-  "version": "2.0.0",
+  "version": "2.0.13",
   "publishConfig": {
     "access": "public"
   },
@@ -10,11 +10,11 @@
   ],
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "test": "vitest run",
     "test:watch": "vitest watch",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build",
+    "prepublish": "tsc -noEmit && vite build",
     "lint": "yarn eslint src/**/*.ts"
   },
   "bugs": {
@@ -70,14 +70,14 @@
     "package.json"
   ],
   "devDependencies": {
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1",
     "vitest": "^1.4.0",
-    "typescript": "^4.7.3"
+    "typescript": "^5.4.5"
   },
   "dependencies": {
     "fflate": "^0.4.4",
-    "@saola.ai/rrweb-types": "^2.0.0"
+    "@saola.ai/rrweb-types": "^2.0.13"
   },
   "browserslist": [
     "supports es6-class"
diff --git a/packages/packer/src/pack.ts b/packages/packer/src/pack.ts
index 71bec26f53..218b2424b4 100644
--- a/packages/packer/src/pack.ts
+++ b/packages/packer/src/pack.ts
@@ -1,6 +1,6 @@
 import { strFromU8, strToU8, zlibSync } from 'fflate';
 import type { PackFn } from '@saola.ai/rrweb-types';
-import { eventWithTimeAndPacker, MARK } from './base';
+import { type eventWithTimeAndPacker, MARK } from './base';
 
 export const pack: PackFn = (event) => {
   const _e: eventWithTimeAndPacker = {
diff --git a/packages/packer/src/unpack.ts b/packages/packer/src/unpack.ts
index e352b6f303..78123bf9ed 100644
--- a/packages/packer/src/unpack.ts
+++ b/packages/packer/src/unpack.ts
@@ -1,5 +1,5 @@
 import { strFromU8, strToU8, unzlibSync } from 'fflate';
-import { eventWithTimeAndPacker, MARK } from './base';
+import { type eventWithTimeAndPacker, MARK } from './base';
 import type { UnpackFn, eventWithTime } from '@saola.ai/rrweb-types';
 
 export const unpack: UnpackFn = (raw: string) => {
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-record/CHANGELOG.md b/packages/plugins/rrweb-plugin-canvas-webrtc-record/CHANGELOG.md
index 354b9cadfc..2d6e25305a 100644
--- a/packages/plugins/rrweb-plugin-canvas-webrtc-record/CHANGELOG.md
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/CHANGELOG.md
@@ -1,5 +1,23 @@
 # @rrweb/rrweb-plugin-canvas-webrtc-record
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb@2.0.0-alpha.17
+
 ## 2.0.0
 
 ### Major Changes
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json b/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json
index c3e8710659..0e173ad2b0 100644
--- a/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/rrweb-plugin-canvas-webrtc-record",
-  "version": "2.0.0",
+  "version": "2.0.13",
   "description": "",
   "type": "module",
   "main": "./dist/rrweb-plugin-canvas-webrtc-record.umd.cjs",
@@ -25,9 +25,9 @@
   ],
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build"
+    "prepublish": "tsc -noEmit && vite build"
   },
   "repository": {
     "type": "git",
@@ -43,12 +43,12 @@
   },
   "homepage": "https://github.com/rrweb-io/rrweb#readme",
   "devDependencies": {
-    "@saola.ai/rrweb": "^2.0.0",
-    "typescript": "^4.7.3",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1"
+    "@saola.ai/rrweb": "^2.0.13",
+    "typescript": "^5.4.5",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1"
   },
   "peerDependencies": {
-    "@saola.ai/rrweb": "^2.0.0"
+    "@saola.ai/rrweb": "^2.0.13"
   }
 }
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/CHANGELOG.md b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/CHANGELOG.md
index 793c832bea..e741fc5f13 100644
--- a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/CHANGELOG.md
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/CHANGELOG.md
@@ -1,5 +1,23 @@
 # @rrweb/rrweb-plugin-canvas-webrtc-replay
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb@2.0.0-alpha.17
+
 ## 2.0.0
 
 ### Major Changes
diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json
index e20516ecb5..ad134fb853 100644
--- a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json
+++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/rrweb-plugin-canvas-webrtc-replay",
-  "version": "2.0.0",
+  "version": "2.0.13",
   "description": "",
   "type": "module",
   "main": "./dist/rrweb-plugin-canvas-webrtc-replay.umd.cjs",
@@ -25,9 +25,9 @@
   ],
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build"
+    "prepublish": "tsc -noEmit && vite build"
   },
   "repository": {
     "type": "git",
@@ -43,12 +43,12 @@
   },
   "homepage": "https://github.com/rrweb-io/rrweb#readme",
   "devDependencies": {
-    "@saola.ai/rrweb": "^2.0.0",
-    "typescript": "^4.7.3",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1"
+    "@saola.ai/rrweb": "^2.0.13",
+    "typescript": "^5.4.5",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1"
   },
   "peerDependencies": {
-    "@saola.ai/rrweb": "^2.0.0"
+    "@saola.ai/rrweb": "^2.0.13"
   }
 }
diff --git a/packages/plugins/rrweb-plugin-console-record/CHANGELOG.md b/packages/plugins/rrweb-plugin-console-record/CHANGELOG.md
index b39dbbf24c..892749f7fc 100644
--- a/packages/plugins/rrweb-plugin-console-record/CHANGELOG.md
+++ b/packages/plugins/rrweb-plugin-console-record/CHANGELOG.md
@@ -1,5 +1,25 @@
 # @rrweb/rrweb-plugin-console-record
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages
+
+- [#1530](https://github.com/rrweb-io/rrweb/pull/1530) [`874933b`](https://github.com/rrweb-io/rrweb/commit/874933b55069759b932b3365025449afc9b2f5c7) Thanks [@pauldambra](https://github.com/pauldambra)! - corrects behaviour of console.assert logging to only capture logs when the provided assertion is falsey
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb@2.0.0-alpha.17
+
 ## 2.0.0
 
 ### Major Changes
diff --git a/packages/plugins/rrweb-plugin-console-record/package.json b/packages/plugins/rrweb-plugin-console-record/package.json
index 5196f8119c..430dd6002f 100644
--- a/packages/plugins/rrweb-plugin-console-record/package.json
+++ b/packages/plugins/rrweb-plugin-console-record/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/rrweb-plugin-console-record",
-  "version": "2.0.0",
+  "version": "2.0.13",
   "description": "",
   "type": "module",
   "main": "./dist/rrweb-plugin-console-record.umd.cjs",
@@ -27,9 +27,9 @@
     "dev": "vite build --watch",
     "test": "vitest run",
     "test:watch": "vitest watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build"
+    "prepublish": "tsc -noEmit && vite build"
   },
   "repository": {
     "type": "git",
@@ -45,14 +45,14 @@
   },
   "homepage": "https://github.com/rrweb-io/rrweb#readme",
   "devDependencies": {
-    "@saola.ai/rrweb": "^2.0.0",
-    "typescript": "^4.7.3",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1",
+    "@saola.ai/rrweb": "^2.0.13",
+    "typescript": "^5.4.5",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1",
     "vitest": "^1.4.0",
     "puppeteer": "^20.9.0"
   },
   "peerDependencies": {
-    "@saola.ai/rrweb": "^2.0.0"
+    "@saola.ai/rrweb": "^2.0.13"
   }
 }
diff --git a/packages/plugins/rrweb-plugin-console-record/src/index.ts b/packages/plugins/rrweb-plugin-console-record/src/index.ts
index 9516816b37..cb38d2ab80 100644
--- a/packages/plugins/rrweb-plugin-console-record/src/index.ts
+++ b/packages/plugins/rrweb-plugin-console-record/src/index.ts
@@ -193,6 +193,12 @@ function initLogObserver(
       (original: (...args: Array<unknown>) => void) => {
         return (...args: Array<unknown>) => {
           original.apply(this, args);
+
+          if (level === 'assert' && !!args[0]) {
+            // assert does not log if the first argument evaluates to true
+            return;
+          }
+
           if (inStack) {
             // If we are already in a stack this means something from the following code is calling a console method
             // likely a proxy method called from stringify. We don't want to log this as it will cause an infinite loop
@@ -203,7 +209,11 @@ function initLogObserver(
             const trace = ErrorStackParser.parse(new Error())
               .map((stackFrame: StackFrame) => stackFrame.toString())
               .splice(1); // splice(1) to omit the hijacked log function
-            const payload = args.map((s) =>
+
+            // assert does not log its first arg, that's only used for deciding whether to log
+            const argsForPayload = level === 'assert' ? args.slice(1) : args;
+
+            const payload = argsForPayload.map((s) =>
               stringify(s, logOptions.stringifyOptions),
             );
             logCount++;
diff --git a/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap b/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap
index ad900ed916..cd0df64e96 100644
--- a/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap
+++ b/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap
@@ -356,11 +356,10 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"assert\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:2:15\\"
+          \\"__puppeteer_evaluation_script__:3:15\\"
         ],
         \\"payload\\": [
-          \\"true\\",
-          \\"\\\\\\"assert\\\\\\"\\"
+          \\"\\\\\\"should log assert\\\\\\"\\"
         ]
       }
     }
@@ -372,7 +371,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"count\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:3:15\\"
+          \\"__puppeteer_evaluation_script__:4:15\\"
         ],
         \\"payload\\": [
           \\"\\\\\\"count\\\\\\"\\"
@@ -387,7 +386,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"countReset\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:4:15\\"
+          \\"__puppeteer_evaluation_script__:5:15\\"
         ],
         \\"payload\\": [
           \\"\\\\\\"count\\\\\\"\\"
@@ -402,7 +401,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"debug\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:5:15\\"
+          \\"__puppeteer_evaluation_script__:6:15\\"
         ],
         \\"payload\\": [
           \\"\\\\\\"debug\\\\\\"\\"
@@ -417,7 +416,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"dir\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:6:15\\"
+          \\"__puppeteer_evaluation_script__:7:15\\"
         ],
         \\"payload\\": [
           \\"\\\\\\"dir\\\\\\"\\"
@@ -432,7 +431,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"dirxml\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:7:15\\"
+          \\"__puppeteer_evaluation_script__:8:15\\"
         ],
         \\"payload\\": [
           \\"\\\\\\"dirxml\\\\\\"\\"
@@ -447,7 +446,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"group\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:8:15\\"
+          \\"__puppeteer_evaluation_script__:9:15\\"
         ],
         \\"payload\\": []
       }
@@ -460,7 +459,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"groupCollapsed\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:9:15\\"
+          \\"__puppeteer_evaluation_script__:10:15\\"
         ],
         \\"payload\\": []
       }
@@ -473,7 +472,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"info\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:10:15\\"
+          \\"__puppeteer_evaluation_script__:11:15\\"
         ],
         \\"payload\\": [
           \\"\\\\\\"info\\\\\\"\\"
@@ -488,7 +487,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"log\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:11:15\\"
+          \\"__puppeteer_evaluation_script__:12:15\\"
         ],
         \\"payload\\": [
           \\"\\\\\\"log\\\\\\"\\"
@@ -503,7 +502,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"table\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:12:15\\"
+          \\"__puppeteer_evaluation_script__:13:15\\"
         ],
         \\"payload\\": [
           \\"\\\\\\"table\\\\\\"\\"
@@ -518,7 +517,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"time\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:13:15\\"
+          \\"__puppeteer_evaluation_script__:14:15\\"
         ],
         \\"payload\\": []
       }
@@ -531,7 +530,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"timeEnd\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:14:15\\"
+          \\"__puppeteer_evaluation_script__:15:15\\"
         ],
         \\"payload\\": []
       }
@@ -544,7 +543,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"timeLog\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:15:15\\"
+          \\"__puppeteer_evaluation_script__:16:15\\"
         ],
         \\"payload\\": []
       }
@@ -557,7 +556,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"trace\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:16:15\\"
+          \\"__puppeteer_evaluation_script__:17:15\\"
         ],
         \\"payload\\": [
           \\"\\\\\\"trace\\\\\\"\\"
@@ -572,7 +571,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"warn\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:17:15\\"
+          \\"__puppeteer_evaluation_script__:18:15\\"
         ],
         \\"payload\\": [
           \\"\\\\\\"warn\\\\\\"\\"
@@ -587,7 +586,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"clear\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:18:15\\"
+          \\"__puppeteer_evaluation_script__:19:15\\"
         ],
         \\"payload\\": []
       }
@@ -600,10 +599,10 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = `
       \\"payload\\": {
         \\"level\\": \\"log\\",
         \\"trace\\": [
-          \\"__puppeteer_evaluation_script__:19:15\\"
+          \\"__puppeteer_evaluation_script__:20:15\\"
         ],
         \\"payload\\": [
-          \\"\\\\\\"TypeError: a message\\\\\\\\n    at __puppeteer_evaluation_script__:19:19\\\\\\\\nEnd of stack for Error object\\\\\\"\\"
+          \\"\\\\\\"TypeError: a message\\\\\\\\n    at __puppeteer_evaluation_script__:20:19\\\\\\\\nEnd of stack for Error object\\\\\\"\\"
         ]
       }
     }
diff --git a/packages/plugins/rrweb-plugin-console-record/test/index.test.ts b/packages/plugins/rrweb-plugin-console-record/test/index.test.ts
index 6630a436c7..00f0d244b1 100644
--- a/packages/plugins/rrweb-plugin-console-record/test/index.test.ts
+++ b/packages/plugins/rrweb-plugin-console-record/test/index.test.ts
@@ -87,14 +87,17 @@ describe('rrweb-plugin-console-record', () => {
       'window.snapshots',
     )) as eventWithTime[];
     // The snapshots should containe 1 console log, not multiple.
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record console messages', async () => {
     await page.goto(`${serverUrl}test/html/log.html`);
 
     await page.evaluate(() => {
-      console.assert(0 === 0, 'assert');
+      // truthy assert does not log
+      console.assert(0 === 0, 'should not log assert');
+      // falsy assert does log
+      console.assert(false, 'should log assert');
       console.count('count');
       console.countReset('count');
       console.debug('debug');
@@ -123,6 +126,6 @@ describe('rrweb-plugin-console-record', () => {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 });
diff --git a/packages/plugins/rrweb-plugin-console-replay/CHANGELOG.md b/packages/plugins/rrweb-plugin-console-replay/CHANGELOG.md
index 324e492972..d264d26a87 100644
--- a/packages/plugins/rrweb-plugin-console-replay/CHANGELOG.md
+++ b/packages/plugins/rrweb-plugin-console-replay/CHANGELOG.md
@@ -1,5 +1,23 @@
 # @rrweb/rrweb-plugin-console-replay
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb@2.0.0-alpha.17
+
 ## 2.0.0
 
 ### Major Changes
diff --git a/packages/plugins/rrweb-plugin-console-replay/package.json b/packages/plugins/rrweb-plugin-console-replay/package.json
index 744be0d35f..be65602323 100644
--- a/packages/plugins/rrweb-plugin-console-replay/package.json
+++ b/packages/plugins/rrweb-plugin-console-replay/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/rrweb-plugin-console-replay",
-  "version": "2.0.0",
+  "version": "2.0.13",
   "description": "",
   "type": "module",
   "main": "./dist/rrweb-plugin-console-replay.umd.cjs",
@@ -25,9 +25,9 @@
   ],
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build"
+    "prepublish": "tsc -noEmit && vite build"
   },
   "repository": {
     "type": "git",
@@ -43,13 +43,13 @@
   },
   "homepage": "https://github.com/rrweb-io/rrweb#readme",
   "devDependencies": {
-    "@saola.ai/rrweb-plugin-console-record": "^2.0.0",
-    "@saola.ai/rrweb": "^2.0.0",
-    "typescript": "^4.7.3",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1"
+    "@saola.ai/rrweb-plugin-console-record": "^2.0.13",
+    "@saola.ai/rrweb": "^2.0.13",
+    "typescript": "^5.4.5",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1"
   },
   "peerDependencies": {
-    "@saola.ai/rrweb": "^2.0.0"
+    "@saola.ai/rrweb": "^2.0.13"
   }
 }
diff --git a/packages/plugins/rrweb-plugin-console-replay/src/index.ts b/packages/plugins/rrweb-plugin-console-replay/src/index.ts
index 065320c551..152713829e 100644
--- a/packages/plugins/rrweb-plugin-console-replay/src/index.ts
+++ b/packages/plugins/rrweb-plugin-console-replay/src/index.ts
@@ -1,6 +1,6 @@
 import {
-  LogLevel,
-  LogData,
+  type LogLevel,
+  type LogData,
   PLUGIN_NAME,
 } from '@saola.ai/rrweb-plugin-console-record';
 import type { eventWithTime } from '@saola.ai/rrweb-types';
diff --git a/packages/plugins/rrweb-plugin-sequential-id-record/CHANGELOG.md b/packages/plugins/rrweb-plugin-sequential-id-record/CHANGELOG.md
index d780eb5ae2..b3c0eb59bc 100644
--- a/packages/plugins/rrweb-plugin-sequential-id-record/CHANGELOG.md
+++ b/packages/plugins/rrweb-plugin-sequential-id-record/CHANGELOG.md
@@ -1,5 +1,23 @@
 # @rrweb/rrweb-plugin-sequential-id-record
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb@2.0.0-alpha.17
+
 ## 2.0.0
 
 ### Major Changes
diff --git a/packages/plugins/rrweb-plugin-sequential-id-record/package.json b/packages/plugins/rrweb-plugin-sequential-id-record/package.json
index 368489973c..65c4df5ca4 100644
--- a/packages/plugins/rrweb-plugin-sequential-id-record/package.json
+++ b/packages/plugins/rrweb-plugin-sequential-id-record/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/rrweb-plugin-sequential-id-record",
-  "version": "2.0.0",
+  "version": "2.0.13",
   "description": "",
   "type": "module",
   "main": "./dist/rrweb-plugin-sequential-id-record.umd.cjs",
@@ -25,9 +25,9 @@
   ],
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build"
+    "prepublish": "tsc -noEmit && vite build"
   },
   "repository": {
     "type": "git",
@@ -43,12 +43,12 @@
   },
   "homepage": "https://github.com/rrweb-io/rrweb#readme",
   "devDependencies": {
-    "@saola.ai/rrweb": "^2.0.0",
-    "typescript": "^4.7.3",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1"
+    "@saola.ai/rrweb": "^2.0.13",
+    "typescript": "^5.4.5",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1"
   },
   "peerDependencies": {
-    "@saola.ai/rrweb": "^2.0.0"
+    "@saola.ai/rrweb": "^2.0.13"
   }
 }
diff --git a/packages/plugins/rrweb-plugin-sequential-id-replay/CHANGELOG.md b/packages/plugins/rrweb-plugin-sequential-id-replay/CHANGELOG.md
index eeeb0eefd5..c1bb348b94 100644
--- a/packages/plugins/rrweb-plugin-sequential-id-replay/CHANGELOG.md
+++ b/packages/plugins/rrweb-plugin-sequential-id-replay/CHANGELOG.md
@@ -1,5 +1,23 @@
 # @rrweb/rrweb-plugin-sequential-id-replay
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb@2.0.0-alpha.17
+
 ## 2.0.0
 
 ### Major Changes
diff --git a/packages/plugins/rrweb-plugin-sequential-id-replay/package.json b/packages/plugins/rrweb-plugin-sequential-id-replay/package.json
index 70bc3ec9c5..89b120dff9 100644
--- a/packages/plugins/rrweb-plugin-sequential-id-replay/package.json
+++ b/packages/plugins/rrweb-plugin-sequential-id-replay/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/rrweb-plugin-sequential-id-replay",
-  "version": "2.0.0",
+  "version": "2.0.13",
   "description": "",
   "type": "module",
   "main": "./dist/rrweb-plugin-sequential-id-replay.umd.cjs",
@@ -25,9 +25,9 @@
   ],
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build"
+    "prepublish": "tsc -noEmit && vite build"
   },
   "repository": {
     "type": "git",
@@ -43,13 +43,13 @@
   },
   "homepage": "https://github.com/rrweb-io/rrweb#readme",
   "devDependencies": {
-    "@saola.ai/rrweb-plugin-sequential-id-record": "^2.0.0",
-    "@saola.ai/rrweb": "^2.0.0",
-    "typescript": "^4.7.3",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1"
+    "@saola.ai/rrweb-plugin-sequential-id-record": "^2.0.13",
+    "@saola.ai/rrweb": "^2.0.13",
+    "typescript": "^5.4.5",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1"
   },
   "peerDependencies": {
-    "@saola.ai/rrweb": "^2.0.0"
+    "@saola.ai/rrweb": "^2.0.13"
   }
 }
diff --git a/packages/record/CHANGELOG.md b/packages/record/CHANGELOG.md
index 1c24c04e4f..cb387a6097 100644
--- a/packages/record/CHANGELOG.md
+++ b/packages/record/CHANGELOG.md
@@ -1,5 +1,27 @@
 # @rrweb/record
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb@2.0.13
+  - @saola.ai/rrweb-types@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages
+
+- [#1515](https://github.com/rrweb-io/rrweb/pull/1515) [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6) Thanks [@okejminja](https://github.com/okejminja)! - Added support for deprecated addRule & removeRule methods
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb@2.0.0-alpha.17
+  - @rrweb/types@2.0.0-alpha.17
+
 ## 2.0.0
 
 ### Major Changes
diff --git a/packages/record/package.json b/packages/record/package.json
index f3db181c7a..58849dba2f 100644
--- a/packages/record/package.json
+++ b/packages/record/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/record",
-  "version": "2.0.0",
+  "version": "2.0.13",
   "publishConfig": {
     "access": "public"
   },
@@ -10,11 +10,11 @@
   ],
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "test": "vitest run",
     "test:watch": "vitest watch",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build",
+    "prepublish": "tsc -noEmit && vite build",
     "lint": "yarn eslint src/**/*.ts"
   },
   "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/record#readme",
@@ -49,14 +49,14 @@
   ],
   "devDependencies": {
     "puppeteer": "^20.9.0",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1",
     "vitest": "^1.4.0",
-    "typescript": "^4.7.3"
+    "typescript": "^5.4.5"
   },
   "dependencies": {
-    "@saola.ai/rrweb-types": "^2.0.0",
-    "@saola.ai/rrweb": "^2.0.0"
+    "@saola.ai/rrweb-types": "^2.0.13",
+    "@saola.ai/rrweb": "^2.0.13"
   },
   "browserslist": [
     "supports es6-class"
diff --git a/packages/replay/CHANGELOG.md b/packages/replay/CHANGELOG.md
index 4833c3147c..16374c7fca 100644
--- a/packages/replay/CHANGELOG.md
+++ b/packages/replay/CHANGELOG.md
@@ -1,5 +1,25 @@
 # @rrweb/replay
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb@2.0.13
+  - @saola.ai/rrweb-types@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb@2.0.0-alpha.17
+  - @rrweb/types@2.0.0-alpha.17
+
 ## 2.0.0
 
 ### Major Changes
diff --git a/packages/replay/package.json b/packages/replay/package.json
index dd9a770727..9c9cedf021 100644
--- a/packages/replay/package.json
+++ b/packages/replay/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/replay",
-  "version": "2.0.0",
+  "version": "2.0.13",
   "publishConfig": {
     "access": "public"
   },
@@ -10,11 +10,11 @@
   ],
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "test": "vitest run",
     "test:watch": "vitest watch",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build",
+    "prepublish": "tsc -noEmit && vite build",
     "lint": "yarn eslint src/**/*.ts"
   },
   "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/replay#readme",
@@ -50,14 +50,14 @@
   ],
   "devDependencies": {
     "puppeteer": "^20.9.0",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1",
     "vitest": "^1.4.0",
-    "typescript": "^4.7.3"
+    "typescript": "^5.4.5"
   },
   "dependencies": {
-    "@saola.ai/rrweb-types": "^2.0.0",
-    "@saola.ai/rrweb": "^2.0.0"
+    "@saola.ai/rrweb-types": "^2.0.13",
+    "@saola.ai/rrweb": "^2.0.13"
   },
   "browserslist": [
     "supports es6-class"
diff --git a/packages/replay/src/index.ts b/packages/replay/src/index.ts
index 51cbb4b7db..75ccf41858 100644
--- a/packages/replay/src/index.ts
+++ b/packages/replay/src/index.ts
@@ -6,4 +6,9 @@ import {
 } from '@saola.ai/rrweb';
 import '@saola.ai/rrweb/dist/style.css';
 
-export { Replayer, playerConfig, PlayerMachineState, SpeedMachineState };
+export {
+  Replayer,
+  type playerConfig,
+  type PlayerMachineState,
+  type SpeedMachineState,
+};
diff --git a/packages/rrdom-nodejs/CHANGELOG.md b/packages/rrdom-nodejs/CHANGELOG.md
index b198d1467e..0383ed3015 100644
--- a/packages/rrdom-nodejs/CHANGELOG.md
+++ b/packages/rrdom-nodejs/CHANGELOG.md
@@ -1,5 +1,31 @@
 # rrdom-nodejs
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb-snapshot@2.0.13
+  - @saola.ai/rrdom@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`d350da8`](https://github.com/rrweb-io/rrweb/commit/d350da8552d8616dd118ee550bdfbce082986562), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb-snapshot@2.0.0-alpha.17
+  - rrdom@2.0.0-alpha.17
+
+## 2.0.0-alpha.16
+
+### Patch Changes
+
+- Updated dependencies [[`a2c8a1a`](https://github.com/rrweb-io/rrweb/commit/a2c8a1a37bfcf8389b280af792262c8263a979a3), [`d08624c`](https://github.com/rrweb-io/rrweb/commit/d08624cb28add386c3618a0e6607424c3f1884d8)]:
+  - rrweb-snapshot@2.0.0-alpha.16
+  - rrdom@2.0.0-alpha.16
+
 ## 2.0.12
 
 ### Patch Changes
diff --git a/packages/rrdom-nodejs/package.json b/packages/rrdom-nodejs/package.json
index a320ae33d5..15f10e1552 100644
--- a/packages/rrdom-nodejs/package.json
+++ b/packages/rrdom-nodejs/package.json
@@ -1,13 +1,13 @@
 {
   "name": "@saola.ai/rrdom-nodejs",
-  "version": "2.0.12",
+  "version": "2.0.13",
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "check-types": "tsc -noEmit",
     "test": "vitest run",
     "test:watch": "vitest watch",
-    "prepublish": "npm run build",
+    "prepublish": "tsc -noEmit && vite build",
     "lint": "yarn eslint src/**/*.ts"
   },
   "keywords": [
@@ -46,19 +46,16 @@
     "compare-versions": "^4.1.3",
     "eslint": "^8.15.0",
     "puppeteer": "^9.1.1",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1",
     "vitest": "^1.4.0",
-    "typescript": "^4.7.3"
+    "typescript": "^5.4.5"
   },
   "dependencies": {
     "cssom": "^0.5.0",
     "cssstyle": "^2.3.0",
     "nwsapi": "^2.2.0",
-    "@saola.ai/rrdom": "^2.0.12",
-    "@saola.ai/rrweb-snapshot": "^2.0.12"
-  },
-  "browserslist": [
-    "supports es6-class"
-  ]
+    "@saola.ai/rrdom": "^2.0.13",
+    "@saola.ai/rrweb-snapshot": "^2.0.13"
+  }
 }
diff --git a/packages/rrdom-nodejs/src/document-nodejs.ts b/packages/rrdom-nodejs/src/document-nodejs.ts
index 7821cd4188..35564bc39d 100644
--- a/packages/rrdom-nodejs/src/document-nodejs.ts
+++ b/packages/rrdom-nodejs/src/document-nodejs.ts
@@ -12,8 +12,8 @@ import {
   BaseRRNode,
   BaseRRText,
   ClassList,
-  IRRDocument,
-  CSSStyleDeclaration,
+  type IRRDocument,
+  type CSSStyleDeclaration,
 } from '@saola.ai/rrdom';
 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
 const nwsapi = require('nwsapi');
diff --git a/packages/rrdom/CHANGELOG.md b/packages/rrdom/CHANGELOG.md
index 03521ab112..d61490fbf0 100644
--- a/packages/rrdom/CHANGELOG.md
+++ b/packages/rrdom/CHANGELOG.md
@@ -1,5 +1,32 @@
 # rrdom
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb-snapshot@2.0.13
+
+## 2.0.0-alpha.17
+
+### Minor Changes
+
+- [#1503](https://github.com/rrweb-io/rrweb/pull/1503) [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158) Thanks [@Juice10](https://github.com/Juice10)! - Support top-layer <dialog> components. Fixes #1381.
+
+### Patch Changes
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`d350da8`](https://github.com/rrweb-io/rrweb/commit/d350da8552d8616dd118ee550bdfbce082986562), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb-snapshot@2.0.0-alpha.17
+
+## 2.0.0-alpha.16
+
+### Patch Changes
+
+- Updated dependencies [[`a2c8a1a`](https://github.com/rrweb-io/rrweb/commit/a2c8a1a37bfcf8389b280af792262c8263a979a3), [`d08624c`](https://github.com/rrweb-io/rrweb/commit/d08624cb28add386c3618a0e6607424c3f1884d8)]:
+  - rrweb-snapshot@2.0.0-alpha.16
+
 ## 2.0.12
 
 ### Patch Changes
diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json
index 23a4e8e32c..fcaf5ecfc7 100644
--- a/packages/rrdom/package.json
+++ b/packages/rrdom/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/rrdom",
-  "version": "2.0.12",
+  "version": "2.0.13",
   "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/rrdom#readme",
   "license": "MIT",
   "type": "module",
@@ -30,28 +30,28 @@
   },
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "check-types": "tsc -noEmit",
     "test": "vitest run",
     "test:watch": "vitest",
-    "prepublish": "npm run build",
+    "prepublish": "tsc -noEmit && vite build",
     "lint": "yarn eslint src/**/*.ts"
   },
   "bugs": {
     "url": "https://github.com/rrweb-io/rrweb/issues"
   },
   "devDependencies": {
-    "@saola.ai/rrweb-types": "^2.0.12",
+    "@saola.ai/rrweb-types": "^2.0.13",
     "@types/puppeteer": "^5.4.4",
     "@typescript-eslint/eslint-plugin": "^5.23.0",
     "@typescript-eslint/parser": "^5.23.0",
     "eslint": "^8.15.0",
     "puppeteer": "^17.1.3",
-    "typescript": "^4.9.0",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1"
+    "typescript": "^5.4.5",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1"
   },
   "dependencies": {
-    "@saola.ai/rrweb-snapshot": "^2.0.12"
+    "@saola.ai/rrweb-snapshot": "^2.0.13"
   }
 }
diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts
index 52c4561f1e..3994b8ec61 100644
--- a/packages/rrdom/src/diff.ts
+++ b/packages/rrdom/src/diff.ts
@@ -1,7 +1,7 @@
 import {
   NodeType as RRNodeType,
   Mirror as NodeMirror,
-  elementNode,
+  type elementNode,
 } from '@saola.ai/rrweb-snapshot';
 import type {
   canvasMutationData,
@@ -21,6 +21,7 @@ import type {
 } from './document';
 import type {
   RRCanvasElement,
+  RRDialogElement,
   RRElement,
   RRIFrameElement,
   RRMediaElement,
@@ -285,6 +286,29 @@ function diffAfterUpdatingChildren(
             );
           break;
         }
+        case 'DIALOG': {
+          const dialog = oldElement as HTMLDialogElement;
+          const rrDialog = newRRElement as unknown as RRDialogElement;
+          const wasOpen = dialog.open;
+          const wasModal = dialog.matches('dialog:modal');
+          const shouldBeOpen = rrDialog.open;
+          const shouldBeModal = rrDialog.isModal;
+
+          const modalChanged = wasModal !== shouldBeModal;
+          const openChanged = wasOpen !== shouldBeOpen;
+
+          if (modalChanged || (wasOpen && openChanged)) dialog.close();
+          if (shouldBeOpen && (openChanged || modalChanged)) {
+            try {
+              if (shouldBeModal) dialog.showModal();
+              else dialog.show();
+            } catch (e) {
+              console.warn(e);
+            }
+          }
+
+          break;
+        }
       }
       break;
     }
@@ -330,12 +354,20 @@ function diffProps(
         }
       };
     } else if (newTree.tagName === 'IFRAME' && name === 'srcdoc') continue;
-    else oldTree.setAttribute(name, newValue);
+    else {
+      try {
+        oldTree.setAttribute(name, newValue);
+      } catch (err) {
+        // We want to continue diffing so we quietly catch
+        // this exception. Otherwise, this can throw and bubble up to
+        // the `ReplayerEvents.Flush` listener and break rendering
+        console.warn(err);
+      }
+    }
   }
 
   for (const { name } of Array.from(oldAttributes))
     if (!(name in newAttributes)) oldTree.removeAttribute(name);
-
   newTree.scrollLeft && (oldTree.scrollLeft = newTree.scrollLeft);
   newTree.scrollTop && (oldTree.scrollTop = newTree.scrollTop);
 }
diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts
index c82c56cea0..45ab2fede2 100644
--- a/packages/rrdom/src/document.ts
+++ b/packages/rrdom/src/document.ts
@@ -474,7 +474,8 @@ export class BaseRRElement extends BaseRRNode implements IRRElement {
   }
 
   public getAttribute(name: string): string | null {
-    return this.attributes[name] || null;
+    if (this.attributes[name] === undefined) return null;
+    return this.attributes[name];
   }
 
   public setAttribute(name: string, attribute: string) {
@@ -547,6 +548,30 @@ export class BaseRRMediaElement extends BaseRRElement {
   }
 }
 
+export class BaseRRDialogElement extends BaseRRElement {
+  public readonly tagName = 'DIALOG' as const;
+  public readonly nodeName = 'DIALOG' as const;
+
+  get isModal() {
+    return this.getAttribute('rr_open_mode') === 'modal';
+  }
+  get open() {
+    return this.getAttribute('open') !== null;
+  }
+  public close() {
+    this.removeAttribute('open');
+    this.removeAttribute('rr_open_mode');
+  }
+  public show() {
+    this.setAttribute('open', '');
+    this.setAttribute('rr_open_mode', 'non-modal');
+  }
+  public showModal() {
+    this.setAttribute('open', '');
+    this.setAttribute('rr_open_mode', 'modal');
+  }
+}
+
 export class BaseRRText extends BaseRRNode implements IRRText {
   public readonly nodeType: number = NodeType.TEXT_NODE;
   public readonly nodeName = '#text' as const;
diff --git a/packages/rrdom/src/index.ts b/packages/rrdom/src/index.ts
index c1c1d0ff83..a2d6e9e73b 100644
--- a/packages/rrdom/src/index.ts
+++ b/packages/rrdom/src/index.ts
@@ -31,6 +31,7 @@ import {
   type IRRDocumentType,
   type IRRText,
   type IRRComment,
+  BaseRRDialogElement,
 } from './document';
 
 export class RRDocument extends BaseRRDocument {
@@ -104,6 +105,9 @@ export class RRDocument extends BaseRRDocument {
       case 'STYLE':
         element = new RRStyleElement(upperTagName);
         break;
+      case 'DIALOG':
+        element = new RRDialogElement(upperTagName);
+        break;
       default:
         element = new RRElement(upperTagName);
         break;
@@ -151,6 +155,8 @@ export class RRElement extends BaseRRElement {
 
 export class RRMediaElement extends BaseRRMediaElement {}
 
+export class RRDialogElement extends BaseRRDialogElement {}
+
 export class RRCanvasElement extends RRElement implements IRRElement {
   public rr_dataURL: string | null = null;
   public canvasMutations: {
diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts
index ce38bbdd2f..b00b66a4be 100644
--- a/packages/rrdom/test/diff.test.ts
+++ b/packages/rrdom/test/diff.test.ts
@@ -338,6 +338,32 @@ describe('diff algorithm for rrdom', () => {
       expect((node as Node as HTMLElement).className).toBe('node');
     });
 
+    it('ignores invalid attributes', () => {
+      const tagName = 'DIV';
+      const node = document.createElement(tagName);
+      const sn = Object.assign({}, elementSn, {
+        attributes: { '@click': 'foo' },
+        tagName,
+      });
+      mirror.add(node, sn);
+
+      const rrDocument = new RRDocument();
+      const rrNode = rrDocument.createElement(tagName);
+      const sn2 = Object.assign({}, elementSn, {
+        attributes: { '@click': 'foo' },
+        tagName,
+      });
+      rrDocument.mirror.add(rrNode, sn2);
+
+      rrNode.attributes = { id: 'node1', class: 'node', '@click': 'foo' };
+      diff(node, rrNode, replayer);
+      expect((node as Node as HTMLElement).id).toBe('node1');
+      expect((node as Node as HTMLElement).className).toBe('node');
+      expect('@click' in (node as Node as HTMLElement)).toBe(false);
+      expect(warn).toHaveBeenCalledTimes(1);
+      warn.mockClear();
+    });
+
     it('can update exist properties', () => {
       const tagName = 'DIV';
       const node = document.createElement(tagName);
diff --git a/packages/rrdom/test/diff/dialog.test.ts b/packages/rrdom/test/diff/dialog.test.ts
new file mode 100644
index 0000000000..11a80e6ec5
--- /dev/null
+++ b/packages/rrdom/test/diff/dialog.test.ts
@@ -0,0 +1,112 @@
+/**
+ * @vitest-environment happy-dom
+ */
+import { vi, MockInstance } from 'vitest';
+import {
+  NodeType as RRNodeType,
+  createMirror,
+  Mirror as NodeMirror,
+  serializedNodeWithId,
+} from 'rrweb-snapshot';
+import { RRDocument } from '../../src';
+import { diff, ReplayerHandler } from '../../src/diff';
+
+describe('diff algorithm for rrdom', () => {
+  let mirror: NodeMirror;
+  let replayer: ReplayerHandler;
+  let warn: MockInstance;
+  let elementSn: serializedNodeWithId;
+  let elementSn2: serializedNodeWithId;
+
+  beforeEach(() => {
+    mirror = createMirror();
+    replayer = {
+      mirror,
+      applyCanvas: () => {},
+      applyInput: () => {},
+      applyScroll: () => {},
+      applyStyleSheetMutation: () => {},
+      afterAppend: () => {},
+    };
+    document.write('<!DOCTYPE html><html><head></head><body></body></html>');
+    // Mock the original console.warn function to make the test fail once console.warn is called.
+    warn = vi.spyOn(console, 'warn');
+
+    elementSn = {
+      type: RRNodeType.Element,
+      tagName: 'DIALOG',
+      attributes: {},
+      childNodes: [],
+      id: 1,
+    };
+
+    elementSn2 = {
+      ...elementSn,
+      attributes: {},
+    };
+  });
+
+  afterEach(() => {
+    // Check that warn was not called (fail on warning)
+    expect(warn).not.toBeCalled();
+    vi.resetAllMocks();
+  });
+  describe('diff dialog elements', () => {
+    vi.setConfig({ testTimeout: 60_000 });
+
+    it('should trigger `showModal` on rr_open_mode:modal attributes', () => {
+      const tagName = 'DIALOG';
+      const node = document.createElement(tagName) as HTMLDialogElement;
+      vi.spyOn(node, 'matches').mockReturnValue(false); // matches is used to check if the dialog was opened with showModal
+      const showModalFn = vi.spyOn(node, 'showModal');
+
+      const rrDocument = new RRDocument();
+      const rrNode = rrDocument.createElement(tagName);
+      rrNode.attributes = { rr_open_mode: 'modal', open: '' };
+
+      mirror.add(node, elementSn);
+      rrDocument.mirror.add(rrNode, elementSn);
+      diff(node, rrNode, replayer);
+
+      expect(showModalFn).toBeCalled();
+    });
+
+    it('should trigger `close` on rr_open_mode removed', () => {
+      const tagName = 'DIALOG';
+      const node = document.createElement(tagName) as HTMLDialogElement;
+      node.showModal();
+      vi.spyOn(node, 'matches').mockReturnValue(true); // matches is used to check if the dialog was opened with showModal
+      const closeFn = vi.spyOn(node, 'close');
+
+      const rrDocument = new RRDocument();
+      const rrNode = rrDocument.createElement(tagName);
+      rrNode.attributes = {};
+
+      mirror.add(node, elementSn);
+      rrDocument.mirror.add(rrNode, elementSn);
+      diff(node, rrNode, replayer);
+
+      expect(closeFn).toBeCalled();
+    });
+
+    it('should not trigger `close` on rr_open_mode is kept', () => {
+      const tagName = 'DIALOG';
+      const node = document.createElement(tagName) as HTMLDialogElement;
+      vi.spyOn(node, 'matches').mockReturnValue(true); // matches is used to check if the dialog was opened with showModal
+      node.setAttribute('rr_open_mode', 'modal');
+      node.setAttribute('open', '');
+      const closeFn = vi.spyOn(node, 'close');
+
+      const rrDocument = new RRDocument();
+      const rrNode = rrDocument.createElement(tagName);
+      rrNode.attributes = { rr_open_mode: 'modal', open: '' };
+
+      mirror.add(node, elementSn);
+      rrDocument.mirror.add(rrNode, elementSn);
+      diff(node, rrNode, replayer);
+
+      expect(closeFn).not.toBeCalled();
+      expect(node.open).toBe(true);
+    });
+  });
+});
diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts
index d191fdda3f..3c622e81e3 100644
--- a/packages/rrdom/test/virtual-dom.test.ts
+++ b/packages/rrdom/test/virtual-dom.test.ts
@@ -7,6 +7,7 @@ import * as puppeteer from 'puppeteer';
 import { vi } from 'vitest';
 import { JSDOM } from 'jsdom';
 import {
+  buildNodeWithSN,
   cdataNode,
   commentNode,
   documentNode,
@@ -207,6 +208,33 @@ describe('RRDocument for browser environment', () => {
       expect((rrNode as RRElement).tagName).toEqual('SHADOWROOT');
       expect(rrNode).toBe(parentRRNode.shadowRoot);
     });
+
+    it('can rebuild blocked element with correct dimensions', () => {
+      // @ts-expect-error Testing buildNodeWithSN with rr elements
+      const node = buildNodeWithSN(
+        {
+          id: 1,
+          tagName: 'svg',
+          type: NodeType.Element,
+          isSVG: true,
+          attributes: {
+            rr_width: '50px',
+            rr_height: '50px',
+          },
+          childNodes: [],
+        },
+        {
+          // @ts-expect-error
+          doc: new RRDocument(),
+          mirror,
+          blockSelector: '*',
+          slimDOMOptions: {},
+        },
+      ) as RRElement;
+
+      expect(node.style.width).toBe('50px');
+      expect(node.style.height).toBe('50px');
+    });
   });
 
   describe('create a RRDocument from a html document', () => {
diff --git a/packages/rrvideo/CHANGELOG.md b/packages/rrvideo/CHANGELOG.md
index 88bbeb1527..a6de24633f 100644
--- a/packages/rrvideo/CHANGELOG.md
+++ b/packages/rrvideo/CHANGELOG.md
@@ -1,17 +1,33 @@
 # rrvideo
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb-player@2.0.13
+
+## 2.0.0-alpha.17
+
 ## 2.0.12
 
 ### Patch Changes
 
 - Updated dependencies []:
-  - @saola.ai/rrweb-player@2.0.12
+  - rrweb-player@2.0.0-alpha.17
+
+## 2.0.0-alpha.16
+
+- @saola.ai/rrweb-player@2.0.12
 
 ## 2.0.11
 
 ### Patch Changes
 
 - Updated dependencies []:
+  - rrweb-player@2.0.0-alpha.16
   - @saola.ai/rrweb-player@2.0.11
 
 ## 2.0.10
diff --git a/packages/rrvideo/package.json b/packages/rrvideo/package.json
index 804b66ba3a..f4a92e729e 100644
--- a/packages/rrvideo/package.json
+++ b/packages/rrvideo/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/rrvideo",
-  "version": "2.0.12",
+  "version": "2.0.13",
   "description": "transform rrweb session into video",
   "main": "build/index.js",
   "bin": {
@@ -27,13 +27,13 @@
     "@types/node": "^18.15.11",
     "jest": "^27.5.1",
     "ts-jest": "^27.1.3",
-    "@saola.ai/rrweb-types": "^2.0.12"
+    "@saola.ai/rrweb-types": "^2.0.13"
   },
   "dependencies": {
     "@open-tech-world/cli-progress-bar": "^2.0.2",
     "fs-extra": "^11.1.1",
     "minimist": "^1.2.5",
     "playwright": "^1.32.1",
-    "@saola.ai/rrweb-player": "^2.0.12"
+    "@saola.ai/rrweb-player": "^2.0.13"
   }
 }
diff --git a/packages/rrweb-player/.svelte-kit/ambient.d.ts b/packages/rrweb-player/.svelte-kit/ambient.d.ts
index da2bb5235a..1d9b52fd46 100644
--- a/packages/rrweb-player/.svelte-kit/ambient.d.ts
+++ b/packages/rrweb-player/.svelte-kit/ambient.d.ts
@@ -30,50 +30,63 @@ declare module '$env/static/private' {
 	export const npm_package_exports___node_polyfills_types: string;
 	export const MANPATH: string;
 	export const npm_package_scripts_test_cross_platform_build: string;
+	export const rvm_bin_path: string;
 	export const TERM_PROGRAM: string;
 	export const npm_package_exports___vite_import: string;
 	export const npm_package_exports___hooks_import: string;
 	export const NODE: string;
 	export const npm_package_dependencies_sade: string;
 	export const INIT_CWD: string;
+	export const GEM_HOME: string;
 	export const NVM_CD_FLAGS: string;
+	export const PYENV_ROOT: string;
 	export const npm_package_devDependencies_typescript: string;
 	export const npm_package_homepage: string;
 	export const npm_config_version_git_tag: string;
-	export const SHELL: string;
 	export const TERM: string;
+	export const SHELL: string;
 	export const npm_package_devDependencies_vite: string;
+	export const CLICOLOR: string;
 	export const npm_package_dependencies_devalue: string;
-	export const TMPDIR: string;
+	export const IRBRC: string;
 	export const HOMEBREW_REPOSITORY: string;
+	export const TMPDIR: string;
 	export const npm_package_scripts_lint: string;
 	export const npm_config_init_license: string;
 	export const TERM_PROGRAM_VERSION: string;
 	export const npm_package_dependencies_set_cookie_parser: string;
 	export const npm_package_dependencies_cookie: string;
+	export const MY_RUBY_HOME: string;
+	export const red: string;
 	export const TERM_SESSION_ID: string;
 	export const npm_package_devDependencies_svelte_preprocess: string;
 	export const npm_config_registry: string;
+	export const LC_ALL: string;
 	export const npm_package_dependencies_import_meta_resolve: string;
 	export const npm_package_repository_url: string;
 	export const npm_package_readmeFilename: string;
-	export const USER: string;
 	export const NVM_DIR: string;
+	export const USER: string;
 	export const npm_package_exports___node_import: string;
 	export const npm_package_description: string;
 	export const npm_package_exports___package_json: string;
 	export const npm_package_dependencies_esm_env: string;
 	export const npm_package_license: string;
+	export const COMMAND_MODE: string;
 	export const npm_package_exports___import: string;
+	export const rvm_path: string;
 	export const npm_package_repository_directory: string;
 	export const SSH_AUTH_SOCK: string;
 	export const __CF_USER_TEXT_ENCODING: string;
 	export const npm_package_bin_svelte_kit: string;
+	export const BASH_SILENCE_DEPRECATION_WARNING: string;
 	export const npm_execpath: string;
 	export const npm_package_devDependencies__types_sade: string;
 	export const npm_package_peerDependencies__sveltejs_vite_plugin_svelte: string;
 	export const npm_package_devDependencies_svelte: string;
+	export const LSCOLORS: string;
 	export const YARN_IGNORE_PATH: string;
+	export const rvm_prefix: string;
 	export const PATH: string;
 	export const npm_config_argv: string;
 	export const npm_package_scripts_postinstall: string;
@@ -84,12 +97,14 @@ declare module '$env/static/private' {
 	export const __CFBundleIdentifier: string;
 	export const npm_package_keywords_4: string;
 	export const PWD: string;
+	export const JAVA_HOME: string;
 	export const npm_lifecycle_event: string;
 	export const npm_package_types: string;
 	export const npm_package_devDependencies__sveltejs_vite_plugin_svelte: string;
 	export const npm_package_repository_type: string;
 	export const npm_package_keywords_0: string;
 	export const npm_package_name: string;
+	export const ITERM_PROFILE: string;
 	export const npm_package_scripts_generate_types: string;
 	export const npm_package_scripts_test_integration: string;
 	export const npm_package_devDependencies__types_connect: string;
@@ -108,23 +123,28 @@ declare module '$env/static/private' {
 	export const npm_package_dependencies_sirv: string;
 	export const XPC_SERVICE_NAME: string;
 	export const npm_package_version: string;
+	export const rvm_version: string;
 	export const SHLVL: string;
 	export const HOME: string;
+	export const COLORFGBG: string;
 	export const npm_package_type: string;
 	export const npm_package_scripts_generate_version: string;
 	export const npm_package_scripts_test: string;
 	export const npm_package_scripts_check_all: string;
+	export const LC_TERMINAL_VERSION: string;
 	export const npm_package_exports___vite_types: string;
 	export const npm_package_exports___hooks_types: string;
 	export const npm_config_save_prefix: string;
 	export const npm_config_strict_ssl: string;
 	export const HOMEBREW_PREFIX: string;
 	export const npm_config_version_git_message: string;
+	export const ITERM_SESSION_ID: string;
 	export const LOGNAME: string;
 	export const npm_package_scripts_format: string;
 	export const npm_package_peerDependencies_vite: string;
 	export const npm_lifecycle_script: string;
 	export const npm_package_peerDependencies_svelte: string;
+	export const GEM_PATH: string;
 	export const LC_CTYPE: string;
 	export const npm_config_ignore_path: string;
 	export const npm_package_devDependencies__types_set_cookie_parser: string;
@@ -134,11 +154,13 @@ declare module '$env/static/private' {
 	export const npm_config_version_git_sign: string;
 	export const npm_config_ignore_scripts: string;
 	export const npm_config_user_agent: string;
-	export const HOMEBREW_CELLAR: string;
 	export const INFOPATH: string;
+	export const HOMEBREW_CELLAR: string;
 	export const npm_package_files_2: string;
 	export const npm_package_devDependencies__types_node: string;
 	export const npm_package_devDependencies__playwright_test: string;
+	export const DISPLAY: string;
+	export const LC_TERMINAL: string;
 	export const npm_package_files_1: string;
 	export const npm_package_devDependencies_dts_buddy: string;
 	export const npm_package_files_0: string;
@@ -146,11 +168,13 @@ declare module '$env/static/private' {
 	export const npm_package_dependencies_kleur: string;
 	export const npm_config_init_version: string;
 	export const npm_config_ignore_optional: string;
+	export const RUBY_VERSION: string;
 	export const SECURITYSESSIONID: string;
 	export const npm_package_exports___node_types: string;
 	export const npm_package_files_6: string;
 	export const npm_package_scripts_check: string;
 	export const npm_package_files_5: string;
+	export const COLORTERM: string;
 	export const npm_node_execpath: string;
 	export const npm_package_scripts_test_unit: string;
 	export const npm_package_files_4: string;
@@ -190,50 +214,63 @@ declare module '$env/dynamic/private' {
 		npm_package_exports___node_polyfills_types: string;
 		MANPATH: string;
 		npm_package_scripts_test_cross_platform_build: string;
+		rvm_bin_path: string;
 		TERM_PROGRAM: string;
 		npm_package_exports___vite_import: string;
 		npm_package_exports___hooks_import: string;
 		NODE: string;
 		npm_package_dependencies_sade: string;
 		INIT_CWD: string;
+		GEM_HOME: string;
 		NVM_CD_FLAGS: string;
+		PYENV_ROOT: string;
 		npm_package_devDependencies_typescript: string;
 		npm_package_homepage: string;
 		npm_config_version_git_tag: string;
-		SHELL: string;
 		TERM: string;
+		SHELL: string;
 		npm_package_devDependencies_vite: string;
+		CLICOLOR: string;
 		npm_package_dependencies_devalue: string;
-		TMPDIR: string;
+		IRBRC: string;
 		HOMEBREW_REPOSITORY: string;
+		TMPDIR: string;
 		npm_package_scripts_lint: string;
 		npm_config_init_license: string;
 		TERM_PROGRAM_VERSION: string;
 		npm_package_dependencies_set_cookie_parser: string;
 		npm_package_dependencies_cookie: string;
+		MY_RUBY_HOME: string;
+		red: string;
 		TERM_SESSION_ID: string;
 		npm_package_devDependencies_svelte_preprocess: string;
 		npm_config_registry: string;
+		LC_ALL: string;
 		npm_package_dependencies_import_meta_resolve: string;
 		npm_package_repository_url: string;
 		npm_package_readmeFilename: string;
-		USER: string;
 		NVM_DIR: string;
+		USER: string;
 		npm_package_exports___node_import: string;
 		npm_package_description: string;
 		npm_package_exports___package_json: string;
 		npm_package_dependencies_esm_env: string;
 		npm_package_license: string;
+		COMMAND_MODE: string;
 		npm_package_exports___import: string;
+		rvm_path: string;
 		npm_package_repository_directory: string;
 		SSH_AUTH_SOCK: string;
 		__CF_USER_TEXT_ENCODING: string;
 		npm_package_bin_svelte_kit: string;
+		BASH_SILENCE_DEPRECATION_WARNING: string;
 		npm_execpath: string;
 		npm_package_devDependencies__types_sade: string;
 		npm_package_peerDependencies__sveltejs_vite_plugin_svelte: string;
 		npm_package_devDependencies_svelte: string;
+		LSCOLORS: string;
 		YARN_IGNORE_PATH: string;
+		rvm_prefix: string;
 		PATH: string;
 		npm_config_argv: string;
 		npm_package_scripts_postinstall: string;
@@ -244,12 +281,14 @@ declare module '$env/dynamic/private' {
 		__CFBundleIdentifier: string;
 		npm_package_keywords_4: string;
 		PWD: string;
+		JAVA_HOME: string;
 		npm_lifecycle_event: string;
 		npm_package_types: string;
 		npm_package_devDependencies__sveltejs_vite_plugin_svelte: string;
 		npm_package_repository_type: string;
 		npm_package_keywords_0: string;
 		npm_package_name: string;
+		ITERM_PROFILE: string;
 		npm_package_scripts_generate_types: string;
 		npm_package_scripts_test_integration: string;
 		npm_package_devDependencies__types_connect: string;
@@ -268,23 +307,28 @@ declare module '$env/dynamic/private' {
 		npm_package_dependencies_sirv: string;
 		XPC_SERVICE_NAME: string;
 		npm_package_version: string;
+		rvm_version: string;
 		SHLVL: string;
 		HOME: string;
+		COLORFGBG: string;
 		npm_package_type: string;
 		npm_package_scripts_generate_version: string;
 		npm_package_scripts_test: string;
 		npm_package_scripts_check_all: string;
+		LC_TERMINAL_VERSION: string;
 		npm_package_exports___vite_types: string;
 		npm_package_exports___hooks_types: string;
 		npm_config_save_prefix: string;
 		npm_config_strict_ssl: string;
 		HOMEBREW_PREFIX: string;
 		npm_config_version_git_message: string;
+		ITERM_SESSION_ID: string;
 		LOGNAME: string;
 		npm_package_scripts_format: string;
 		npm_package_peerDependencies_vite: string;
 		npm_lifecycle_script: string;
 		npm_package_peerDependencies_svelte: string;
+		GEM_PATH: string;
 		LC_CTYPE: string;
 		npm_config_ignore_path: string;
 		npm_package_devDependencies__types_set_cookie_parser: string;
@@ -294,11 +338,13 @@ declare module '$env/dynamic/private' {
 		npm_config_version_git_sign: string;
 		npm_config_ignore_scripts: string;
 		npm_config_user_agent: string;
-		HOMEBREW_CELLAR: string;
 		INFOPATH: string;
+		HOMEBREW_CELLAR: string;
 		npm_package_files_2: string;
 		npm_package_devDependencies__types_node: string;
 		npm_package_devDependencies__playwright_test: string;
+		DISPLAY: string;
+		LC_TERMINAL: string;
 		npm_package_files_1: string;
 		npm_package_devDependencies_dts_buddy: string;
 		npm_package_files_0: string;
@@ -306,11 +352,13 @@ declare module '$env/dynamic/private' {
 		npm_package_dependencies_kleur: string;
 		npm_config_init_version: string;
 		npm_config_ignore_optional: string;
+		RUBY_VERSION: string;
 		SECURITYSESSIONID: string;
 		npm_package_exports___node_types: string;
 		npm_package_files_6: string;
 		npm_package_scripts_check: string;
 		npm_package_files_5: string;
+		COLORTERM: string;
 		npm_node_execpath: string;
 		npm_package_scripts_test_unit: string;
 		npm_package_files_4: string;
diff --git a/packages/rrweb-player/CHANGELOG.md b/packages/rrweb-player/CHANGELOG.md
index 92aa1f29ed..03fd79ace2 100644
--- a/packages/rrweb-player/CHANGELOG.md
+++ b/packages/rrweb-player/CHANGELOG.md
@@ -1,5 +1,25 @@
 # rrweb-player
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb-packer@2.0.13
+  - @saola.ai/replay@2.0.13
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- Updated dependencies [[`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c)]:
+  - @rrweb/packer@2.0.0-alpha.17
+  - @rrweb/replay@2.0.0-alpha.17
+
+## 2.0.0-alpha.16
+
 ## 2.0.12
 
 ### Patch Changes
diff --git a/packages/rrweb-player/package.json b/packages/rrweb-player/package.json
index 8d6ea74f16..8737cc5d64 100644
--- a/packages/rrweb-player/package.json
+++ b/packages/rrweb-player/package.json
@@ -1,8 +1,8 @@
 {
   "name": "@saola.ai/rrweb-player",
-  "version": "2.0.12",
+  "version": "2.0.13",
   "devDependencies": {
-    "@saola.ai/rrweb-types": "^2.0.12",
+    "@saola.ai/rrweb-types": "^2.0.13",
     "@sveltejs/adapter-auto": "^3.0.0",
     "@sveltejs/kit": "^2.0.0",
     "@sveltejs/package": "^2.0.0",
@@ -16,12 +16,12 @@
     "svelte-preprocess": "^5.0.3",
     "svelte2tsx": "^0.7.6",
     "tslib": "^2.0.0",
-    "vite": "^5.2.8"
+    "vite": "^5.3.1"
   },
   "dependencies": {
     "@tsconfig/svelte": "^1.0.0",
-    "@saola.ai/replay": "^2.0.0",
-    "@saola.ai/rrweb-packer": "^2.0.0"
+    "@saola.ai/replay": "^2.0.13",
+    "@saola.ai/rrweb-packer": "^2.0.13"
   },
   "scripts": {
     "dev": "vite build --watch",
diff --git a/packages/rrweb-snapshot/CHANGELOG.md b/packages/rrweb-snapshot/CHANGELOG.md
index a0752b4f0a..079cfe8f4f 100644
--- a/packages/rrweb-snapshot/CHANGELOG.md
+++ b/packages/rrweb-snapshot/CHANGELOG.md
@@ -1,5 +1,33 @@
 # rrweb-snapshot
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+## 2.0.0-alpha.17
+
+### Minor Changes
+
+- [#1503](https://github.com/rrweb-io/rrweb/pull/1503) [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158) Thanks [@Juice10](https://github.com/Juice10)! - Record dialog's modal status for replay in rrweb. (Currently triggering `dialog.showModal()` is not supported in rrweb-snapshot's rebuild)
+
+### Patch Changes
+
+- [#1417](https://github.com/rrweb-io/rrweb/pull/1417) [`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - fix: duplicate textContent for style elements cause incremental style mutations to be invalid
+
+- [#1533](https://github.com/rrweb-io/rrweb/pull/1533) [`d350da8`](https://github.com/rrweb-io/rrweb/commit/d350da8552d8616dd118ee550bdfbce082986562) Thanks [@jeffdnguyen](https://github.com/jeffdnguyen)! - Fix `url()` rewrite for nested stylesheets by rewriting during stringification instead of after
+
+- [#1509](https://github.com/rrweb-io/rrweb/pull/1509) [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414) Thanks [@Juice10](https://github.com/Juice10)! - Reverse monkey patch built in methods to support LWC (and other frameworks like angular which monkey patch built in methods).
+
+## 2.0.0-alpha.16
+
+### Patch Changes
+
+- [#1386](https://github.com/rrweb-io/rrweb/pull/1386) [`a2c8a1a`](https://github.com/rrweb-io/rrweb/commit/a2c8a1a37bfcf8389b280af792262c8263a979a3) Thanks [@ababik](https://github.com/ababik)! - Fix that the optional `maskInputFn` was being accidentally ignored during the creation of the full snapshot
+
+- [#1512](https://github.com/rrweb-io/rrweb/pull/1512) [`d08624c`](https://github.com/rrweb-io/rrweb/commit/d08624cb28add386c3618a0e6607424c3f1884d8) Thanks [@eoghanmurray](https://github.com/eoghanmurray)! - optimisation: skip mask check on leaf elements
+
 ## 2.0.12
 
 ## 2.0.11
diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json
index 0f8d24b45e..f45c60ec66 100644
--- a/packages/rrweb-snapshot/package.json
+++ b/packages/rrweb-snapshot/package.json
@@ -1,18 +1,20 @@
 {
   "name": "@saola.ai/rrweb-snapshot",
-  "version": "2.0.12",
+  "version": "2.0.13",
   "description": "rrweb's component to take a snapshot of DOM, aka DOM serializer",
   "scripts": {
     "prepare": "npm run prepack",
     "prepack": "npm run build",
-    "retest": "jest",
-    "test": "vitest run",
+    "retest": "vitest run",
+    "test": "yarn build && vitest run",
     "test:watch": "vitest watch",
-    "test:update": "vitest run --update",
+    "retest:update": "vitest run --update",
+    "test:update": "yarn build && vitest run --update",
+    "bench": "vite build && vitest bench",
     "dev": "vite build --watch",
-    "build": "yarn check-types && vite build",
+    "build": "yarn turbo prepublish -F @saola.ai/rrweb-snapshot",
     "check-types": "tsc --noEmit",
-    "prepublish": "npm run build",
+    "prepublish": "yarn check-types && vite build",
     "lint": "yarn eslint src"
   },
   "type": "module",
@@ -52,16 +54,19 @@
   },
   "homepage": "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-snapshot#readme",
   "devDependencies": {
+    "@rrweb/utils": "^2.0.0-alpha.17",
     "@types/jsdom": "^20.0.0",
     "@types/node": "^18.15.11",
     "@types/puppeteer": "^5.4.4",
-    "cross-env": "^5.2.0",
     "puppeteer": "^17.1.3",
     "ts-node": "^7.0.1",
     "tslib": "^1.9.3",
-    "typescript": "^4.7.3",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1",
+    "typescript": "^5.4.5",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1",
     "vitest": "^1.4.0"
+  },
+  "dependencies": {
+    "postcss": "^8.4.38"
   }
 }
diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts
index d2755a03e5..ec85468af6 100644
--- a/packages/rrweb-snapshot/src/css.ts
+++ b/packages/rrweb-snapshot/src/css.ts
@@ -1,987 +1,41 @@
-/**
- * This file is a fork of https://github.com/reworkcss/css/blob/master/lib/parse/index.js
- * I fork it because:
- * 1. The css library was built for node.js which does not have tree-shaking supports.
- * 2. Rewrites into typescript give us a better type interface.
- */
-/* eslint-disable tsdoc/syntax */
+import type { AcceptedPlugin, Rule } from 'postcss';
 
-export interface ParserOptions {
-  /** Silently fail on parse errors */
-  silent?: boolean;
-  /**
-   * The path to the file containing css.
-   * Makes errors and source maps more helpful, by letting them know where code comes from.
-   */
-  source?: string;
-}
-
-/**
- * Error thrown during parsing.
- */
-export interface ParserError {
-  /** The full error message with the source position. */
-  message?: string;
-  /** The error message without position. */
-  reason?: string;
-  /** The value of options.source if passed to css.parse. Otherwise undefined. */
-  filename?: string;
-  line?: number;
-  column?: number;
-  /** The portion of code that couldn't be parsed. */
-  source?: string;
-}
-
-export interface Loc {
-  line?: number;
-  column?: number;
-}
-
-/**
- * Base AST Tree Node.
- */
-export interface Node {
-  /** The possible values are the ones listed in the Types section on https://github.com/reworkcss/css page. */
-  type?: string;
-  /** A reference to the parent node, or null if the node has no parent. */
-  parent?: Node;
-  /** Information about the position in the source string that corresponds to the node. */
-  position?: {
-    start?: Loc;
-    end?: Loc;
-    /** The value of options.source if passed to css.parse. Otherwise undefined. */
-    source?: string;
-    /** The full source string passed to css.parse. */
-    content?: string;
-  };
-}
-
-export interface NodeWithRules extends Node {
-  /** Array of nodes with the types rule, comment and any of the at-rule types. */
-  rules: Array<Rule | Comment | AtRule>;
-}
-
-export interface Rule extends Node {
-  /** The list of selectors of the rule, split on commas. Each selector is trimmed from whitespace and comments. */
-  selectors?: string[];
-  /** Array of nodes with the types declaration and comment. */
-  declarations?: Array<Declaration | Comment>;
-}
-
-export interface Declaration extends Node {
-  /** The property name, trimmed from whitespace and comments. May not be empty. */
-  property?: string;
-  /** The value of the property, trimmed from whitespace and comments. Empty values are allowed. */
-  value?: string;
-}
-
-/**
- * A rule-level or declaration-level comment. Comments inside selectors, properties and values etc. are lost.
- */
-export interface Comment extends Node {
-  comment?: string;
-}
-
-/**
- * The @charset at-rule.
- */
-export interface Charset extends Node {
-  /** The part following @charset. */
-  charset?: string;
-}
-
-/**
- * The @custom-media at-rule
- */
-export interface CustomMedia extends Node {
-  /** The ---prefixed name. */
-  name?: string;
-  /** The part following the name. */
-  media?: string;
-}
-
-/**
- * The @document at-rule.
- */
-export interface Document extends NodeWithRules {
-  /** The part following @document. */
-  document?: string;
-  /** The vendor prefix in @document, or undefined if there is none. */
-  vendor?: string;
-}
-
-/**
- * The @font-face at-rule.
- */
-export interface FontFace extends Node {
-  /** Array of nodes with the types declaration and comment. */
-  declarations?: Array<Declaration | Comment>;
-}
-
-/**
- * The @host at-rule.
- */
-export type Host = NodeWithRules;
-
-/**
- * The @import at-rule.
- */
-export interface Import extends Node {
-  /** The part following @import. */
-  import?: string;
-}
-
-/**
- * The @keyframes at-rule.
- */
-export interface KeyFrames extends Node {
-  /** The name of the keyframes rule. */
-  name?: string;
-  /** The vendor prefix in @keyframes, or undefined if there is none. */
-  vendor?: string;
-  /** Array of nodes with the types keyframe and comment. */
-  keyframes?: Array<KeyFrame | Comment>;
-}
-
-export interface KeyFrame extends Node {
-  /** The list of "selectors" of the keyframe rule, split on commas. Each “selector” is trimmed from whitespace. */
-  values?: string[];
-  /** Array of nodes with the types declaration and comment. */
-  declarations?: Array<Declaration | Comment>;
-}
-
-/**
- * The @media at-rule.
- */
-export interface Media extends NodeWithRules {
-  /** The part following @media. */
-  media?: string;
-}
-
-/**
- * The @namespace at-rule.
- */
-export interface Namespace extends Node {
-  /** The part following @namespace. */
-  namespace?: string;
-}
-
-/**
- * The @page at-rule.
- */
-export interface Page extends Node {
-  /** The list of selectors of the rule, split on commas. Each selector is trimmed from whitespace and comments. */
-  selectors?: string[];
-  /** Array of nodes with the types declaration and comment. */
-  declarations?: Array<Declaration | Comment>;
-}
-
-/**
- * The @supports at-rule.
- */
-export interface Supports extends NodeWithRules {
-  /** The part following @supports. */
-  supports?: string;
-}
-
-/** All at-rules. */
-export type AtRule =
-  | Charset
-  | CustomMedia
-  | Document
-  | FontFace
-  | Host
-  | Import
-  | KeyFrames
-  | Media
-  | Namespace
-  | Page
-  | Supports;
-
-/**
- * A collection of rules
- */
-export interface StyleRules extends NodeWithRules {
-  source?: string;
-  /** Array of Errors. Errors collected during parsing when option silent is true. */
-  parsingErrors?: ParserError[];
-}
-
-/**
- * The root node returned by css.parse.
- */
-export interface Stylesheet extends Node {
-  stylesheet?: StyleRules;
-}
-
-// http://www.w3.org/TR/CSS21/grammar.html
-// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
-const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g;
-
-export function parse(css: string, options: ParserOptions = {}): Stylesheet {
-  /**
-   * Positional.
-   */
-
-  let lineno = 1;
-  let column = 1;
-
-  /**
-   * Update lineno and column based on `str`.
-   */
-
-  function updatePosition(str: string) {
-    const lines = str.match(/\n/g);
-    if (lines) {
-      lineno += lines.length;
-    }
-    const i = str.lastIndexOf('\n');
-    column = i === -1 ? column + str.length : str.length - i;
-  }
-
-  /**
-   * Mark position and patch `node.position`.
-   */
-
-  function position() {
-    const start = { line: lineno, column };
-    return (
-      node: Rule | Declaration | Comment | AtRule | Stylesheet | KeyFrame,
-    ) => {
-      node.position = new Position(start);
-      whitespace();
-      return node;
-    };
-  }
-
-  /**
-   * Store position information for a node
-   */
-
-  class Position {
-    public static content: string;
-    public content!: string;
-    public start!: Loc;
-    public end!: Loc;
-    public source?: string;
-
-    constructor(start: Loc) {
-      this.start = start;
-      this.end = { line: lineno, column };
-      this.source = options.source;
-      this.content = Position.content;
-    }
-  }
-
-  /**
-   * Non-enumerable source string
-   */
-
-  Position.content = css;
-
-  const errorsList: ParserError[] = [];
-
-  function error(msg: string) {
-    const err = new Error(
-      `${options.source || ''}:${lineno}:${column}: ${msg}`,
-    ) as ParserError;
-    err.reason = msg;
-    err.filename = options.source;
-    err.line = lineno;
-    err.column = column;
-    err.source = css;
-
-    if (options.silent) {
-      errorsList.push(err);
-    } else {
-      throw err;
-    }
-  }
-
-  /**
-   * Parse stylesheet.
-   */
-
-  function stylesheet(): Stylesheet {
-    const rulesList = rules();
+const MEDIA_SELECTOR = /(max|min)-device-(width|height)/;
+const MEDIA_SELECTOR_GLOBAL = new RegExp(MEDIA_SELECTOR.source, 'g');
 
+const mediaSelectorPlugin: AcceptedPlugin = {
+  postcssPlugin: 'postcss-custom-selectors',
+  prepare() {
     return {
-      type: 'stylesheet',
-      stylesheet: {
-        source: options.source,
-        rules: rulesList,
-        parsingErrors: errorsList,
+      postcssPlugin: 'postcss-custom-selectors',
+      AtRule: function (atrule) {
+        if (atrule.params.match(MEDIA_SELECTOR_GLOBAL)) {
+          atrule.params = atrule.params.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2');
+        }
       },
     };
-  }
-
-  /**
-   * Opening brace.
-   */
-
-  function open() {
-    return match(/^{\s*/);
-  }
-
-  /**
-   * Closing brace.
-   */
-
-  function close() {
-    return match(/^}/);
-  }
-
-  /**
-   * Parse ruleset.
-   */
-
-  function rules() {
-    let node: Rule | void;
-    const rules: Rule[] = [];
-    whitespace();
-    comments(rules);
-    while (css.length && css.charAt(0) !== '}' && (node = atrule() || rule())) {
-      if (node) {
-        rules.push(node);
-        comments(rules);
-      }
-    }
-    return rules;
-  }
-
-  /**
-   * Match `re` and return captures.
-   */
-
-  function match(re: RegExp) {
-    const m = re.exec(css);
-    if (!m) {
-      return;
-    }
-    const str = m[0];
-    updatePosition(str);
-    css = css.slice(str.length);
-    return m;
-  }
-
-  /**
-   * Parse whitespace.
-   */
-
-  function whitespace() {
-    match(/^\s*/);
-  }
-
-  /**
-   * Parse comments;
-   */
-
-  function comments(rules: Rule[] = []) {
-    let c: Comment | void;
-    while ((c = comment())) {
-      if (c) {
-        rules.push(c);
-      }
-      c = comment();
-    }
-    return rules;
-  }
-
-  /**
-   * Parse comment.
-   */
-
-  function comment() {
-    const pos = position();
-    if ('/' !== css.charAt(0) || '*' !== css.charAt(1)) {
-      return;
-    }
-
-    let i = 2;
-    while (
-      '' !== css.charAt(i) &&
-      ('*' !== css.charAt(i) || '/' !== css.charAt(i + 1))
-    ) {
-      ++i;
-    }
-    i += 2;
-
-    if ('' === css.charAt(i - 1)) {
-      return error('End of comment missing');
-    }
-
-    const str = css.slice(2, i - 2);
-    column += 2;
-    updatePosition(str);
-    css = css.slice(i);
-    column += 2;
-
-    return pos({
-      type: 'comment',
-      comment: str,
-    });
-  }
-
-  /**
-   * Parse selector.
-   */
-
-  // originally from https://github.com/NxtChg/pieces/blob/3eb39c8287a97632e9347a24f333d52d916bc816/js/css_parser/css_parse.js#L46C1-L47C1
-  const selectorMatcher = new RegExp(
-    '^((' +
-      [
-        /[^\\]"(?:\\"|[^"])*"/.source, // consume any quoted parts (checking that the double quote isn't itself escaped)
-        /[^\\]'(?:\\'|[^'])*'/.source, // same but for single quotes
-        '[^{]',
-      ].join('|') +
-      ')+)',
-  );
-
-  function selector() {
-    whitespace();
-    while (css[0] == '}') {
-      error('extra closing bracket');
-      css = css.slice(1);
-      whitespace();
-    }
-
-    const m = match(selectorMatcher);
-    if (!m) {
-      return;
-    }
-
-    /* @fix Remove all comments from selectors
-     * http://ostermiller.org/findcomment.html */
-    const cleanedInput = m[0]
-      .trim()
-      .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')
-
-      // Handle strings by replacing commas inside them
-      .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, (m) => {
-        return m.replace(/,/g, '\u200C');
-      });
-
-    // Split using a custom function and restore commas in strings
-    return customSplit(cleanedInput).map((s) =>
-      s.replace(/\u200C/g, ',').trim(),
-    );
-  }
-
-  /**
-   * Split selector correctly, ensuring not to split on comma if inside ().
-   */
-
-  function customSplit(input: string) {
-    const result = [];
-    let currentSegment = '';
-    let depthParentheses = 0; // Track depth of parentheses
-    let depthBrackets = 0; // Track depth of square brackets
-    let currentStringChar = null;
-
-    for (const char of input) {
-      const hasStringEscape = currentSegment.endsWith('\\');
-
-      if (currentStringChar) {
-        if (currentStringChar === char && !hasStringEscape) {
-          currentStringChar = null;
+  },
+};
+
+// Simplified from https://github.com/giuseppeg/postcss-pseudo-classes/blob/master/index.js
+const pseudoClassPlugin: AcceptedPlugin = {
+  postcssPlugin: 'postcss-hover-classes',
+  prepare: function () {
+    const fixed: Rule[] = [];
+    return {
+      Rule: function (rule) {
+        if (fixed.indexOf(rule) !== -1) {
+          return;
         }
-      } else if (char === '(') {
-        depthParentheses++;
-      } else if (char === ')') {
-        depthParentheses--;
-      } else if (char === '[') {
-        depthBrackets++;
-      } else if (char === ']') {
-        depthBrackets--;
-      } else if ('\'"'.includes(char)) {
-        currentStringChar = char;
-      }
-
-      // Split point is a comma that is not inside parentheses or square brackets
-      if (char === ',' && depthParentheses === 0 && depthBrackets === 0) {
-        result.push(currentSegment);
-        currentSegment = '';
-      } else {
-        currentSegment += char;
-      }
-    }
-
-    // Add the last segment
-    if (currentSegment) {
-      result.push(currentSegment);
-    }
-
-    return result;
-  }
-
-  /**
-   * Parse declaration.
-   */
-
-  function declaration(): Declaration | void | never {
-    const pos = position();
-
-    // prop
-    // eslint-disable-next-line no-useless-escape
-    const propMatch = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
-    if (!propMatch) {
-      return;
-    }
-    const prop = trim(propMatch[0]);
-
-    // :
-    if (!match(/^:\s*/)) {
-      return error(`property missing ':'`);
-    }
-
-    // val
-    // eslint-disable-next-line no-useless-escape
-    const val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
-
-    const ret = pos({
-      type: 'declaration',
-      property: prop.replace(commentre, ''),
-      value: val ? trim(val[0]).replace(commentre, '') : '',
-    });
-
-    // ;
-    match(/^[;\s]*/);
-
-    return ret;
-  }
-
-  /**
-   * Parse declarations.
-   */
-
-  function declarations() {
-    const decls: Array<object> = [];
-
-    if (!open()) {
-      return error(`missing '{'`);
-    }
-    comments(decls);
-
-    // declarations
-    let decl;
-    while ((decl = declaration())) {
-      if ((decl as unknown) !== false) {
-        decls.push(decl);
-        comments(decls);
-      }
-      decl = declaration();
-    }
-
-    if (!close()) {
-      return error(`missing '}'`);
-    }
-    return decls;
-  }
-
-  /**
-   * Parse keyframe.
-   */
-
-  function keyframe() {
-    let m;
-    const vals = [];
-    const pos = position();
-
-    while ((m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/))) {
-      vals.push(m[1]);
-      match(/^,\s*/);
-    }
-
-    if (!vals.length) {
-      return;
-    }
-
-    return pos({
-      type: 'keyframe',
-      values: vals,
-      declarations: declarations() as Declaration[],
-    });
-  }
-
-  /**
-   * Parse keyframes.
-   */
-
-  function atkeyframes() {
-    const pos = position();
-    let m = match(/^@([-\w]+)?keyframes\s*/);
-
-    if (!m) {
-      return;
-    }
-    const vendor = m[1];
-
-    // identifier
-    m = match(/^([-\w]+)\s*/);
-    if (!m) {
-      return error('@keyframes missing name');
-    }
-    const name = m[1];
-
-    if (!open()) {
-      return error(`@keyframes missing '{'`);
-    }
-
-    let frame;
-    let frames = comments();
-    while ((frame = keyframe())) {
-      frames.push(frame);
-      frames = frames.concat(comments());
-    }
-
-    if (!close()) {
-      return error(`@keyframes missing '}'`);
-    }
-
-    return pos({
-      type: 'keyframes',
-      name,
-      vendor,
-      keyframes: frames,
-    });
-  }
-
-  /**
-   * Parse supports.
-   */
-
-  function atsupports() {
-    const pos = position();
-    const m = match(/^@supports *([^{]+)/);
-
-    if (!m) {
-      return;
-    }
-    const supports = trim(m[1]);
-
-    if (!open()) {
-      return error(`@supports missing '{'`);
-    }
-
-    const style = comments().concat(rules());
-
-    if (!close()) {
-      return error(`@supports missing '}'`);
-    }
-
-    return pos({
-      type: 'supports',
-      supports,
-      rules: style,
-    });
-  }
-
-  /**
-   * Parse host.
-   */
-
-  function athost() {
-    const pos = position();
-    const m = match(/^@host\s*/);
-
-    if (!m) {
-      return;
-    }
-
-    if (!open()) {
-      return error(`@host missing '{'`);
-    }
-
-    const style = comments().concat(rules());
-
-    if (!close()) {
-      return error(`@host missing '}'`);
-    }
-
-    return pos({
-      type: 'host',
-      rules: style,
-    });
-  }
-
-  /**
-   * Parse media.
-   */
-
-  function atmedia() {
-    const pos = position();
-    const m = match(/^@media *([^{]+)/);
-
-    if (!m) {
-      return;
-    }
-    const media = trim(m[1]);
-
-    if (!open()) {
-      return error(`@media missing '{'`);
-    }
-
-    const style = comments().concat(rules());
-
-    if (!close()) {
-      return error(`@media missing '}'`);
-    }
-
-    return pos({
-      type: 'media',
-      media,
-      rules: style,
-    });
-  }
-
-  /**
-   * Parse custom-media.
-   */
-
-  function atcustommedia() {
-    const pos = position();
-    const m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/);
-    if (!m) {
-      return;
-    }
-
-    return pos({
-      type: 'custom-media',
-      name: trim(m[1]),
-      media: trim(m[2]),
-    });
-  }
-
-  /**
-   * Parse paged media.
-   */
-
-  function atpage() {
-    const pos = position();
-    const m = match(/^@page */);
-    if (!m) {
-      return;
-    }
-
-    const sel = selector() || [];
-
-    if (!open()) {
-      return error(`@page missing '{'`);
-    }
-    let decls = comments();
-
-    // declarations
-    let decl;
-    while ((decl = declaration())) {
-      decls.push(decl);
-      decls = decls.concat(comments());
-    }
-
-    if (!close()) {
-      return error(`@page missing '}'`);
-    }
-
-    return pos({
-      type: 'page',
-      selectors: sel,
-      declarations: decls,
-    });
-  }
-
-  /**
-   * Parse document.
-   */
-
-  function atdocument() {
-    const pos = position();
-    const m = match(/^@([-\w]+)?document *([^{]+)/);
-    if (!m) {
-      return;
-    }
-
-    const vendor = trim(m[1]);
-    const doc = trim(m[2]);
-
-    if (!open()) {
-      return error(`@document missing '{'`);
-    }
-
-    const style = comments().concat(rules());
-
-    if (!close()) {
-      return error(`@document missing '}'`);
-    }
-
-    return pos({
-      type: 'document',
-      document: doc,
-      vendor,
-      rules: style,
-    });
-  }
-
-  /**
-   * Parse font-face.
-   */
-
-  function atfontface() {
-    const pos = position();
-    const m = match(/^@font-face\s*/);
-    if (!m) {
-      return;
-    }
-
-    if (!open()) {
-      return error(`@font-face missing '{'`);
-    }
-    let decls = comments();
-
-    // declarations
-    let decl;
-    while ((decl = declaration())) {
-      decls.push(decl);
-      decls = decls.concat(comments());
-    }
-
-    if (!close()) {
-      return error(`@font-face missing '}'`);
-    }
-
-    return pos({
-      type: 'font-face',
-      declarations: decls,
-    });
-  }
-
-  /**
-   * Parse import
-   */
-
-  const atimport = _compileAtrule('import');
-
-  /**
-   * Parse charset
-   */
-
-  const atcharset = _compileAtrule('charset');
-
-  /**
-   * Parse namespace
-   */
-
-  const atnamespace = _compileAtrule('namespace');
-
-  /**
-   * Parse non-block at-rules
-   */
-
-  function _compileAtrule(name: string) {
-    const re = new RegExp(
-      '^@' +
-        name +
-        '\\s*((?:' +
-        [
-          /[^\\]"(?:\\"|[^"])*"/.source, // consume any quoted parts (checking that the double quote isn't itself escaped)
-          /[^\\]'(?:\\'|[^'])*'/.source, // same but for single quotes
-          '[^;]',
-        ].join('|') +
-        ')+);',
-    );
-    return () => {
-      const pos = position();
-      const m = match(re);
-      if (!m) {
-        return;
-      }
-      const ret: Record<string, string> = { type: name };
-      ret[name] = m[1].trim();
-      return pos(ret);
+        fixed.push(rule);
+        rule.selectors.forEach(function (selector) {
+          if (selector.includes(':hover')) {
+            rule.selector += ',\n' + selector.replace(/:hover/g, '.\\:hover');
+          }
+        });
+      },
     };
-  }
-
-  /**
-   * Parse at rule.
-   */
-
-  function atrule() {
-    if (css[0] !== '@') {
-      return;
-    }
-
-    return (
-      atkeyframes() ||
-      atmedia() ||
-      atcustommedia() ||
-      atsupports() ||
-      atimport() ||
-      atcharset() ||
-      atnamespace() ||
-      atdocument() ||
-      atpage() ||
-      athost() ||
-      atfontface()
-    );
-  }
-
-  /**
-   * Parse rule.
-   */
-
-  function rule() {
-    const pos = position();
-    const sel = selector();
-
-    if (!sel) {
-      return error('selector missing');
-    }
-    comments();
-
-    return pos({
-      type: 'rule',
-      selectors: sel,
-      declarations: declarations() as Declaration[],
-    });
-  }
-
-  return addParent(stylesheet());
-}
-
-/**
- * Trim `str`.
- */
-
-function trim(str: string) {
-  return str ? str.replace(/^\s+|\s+$/g, '') : '';
-}
-
-/**
- * Adds non-enumerable parent node reference to each node.
- */
-
-function addParent(obj: Stylesheet, parent?: Stylesheet): Stylesheet {
-  const isNode = obj && typeof obj.type === 'string';
-  const childParent = isNode ? obj : parent;
-
-  for (const k of Object.keys(obj)) {
-    const value = obj[k as keyof Stylesheet];
-    if (Array.isArray(value)) {
-      value.forEach((v) => {
-        // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
-        addParent(v, childParent);
-      });
-    } else if (value && typeof value === 'object') {
-      addParent(value as Stylesheet, childParent);
-    }
-  }
-
-  if (isNode) {
-    Object.defineProperty(obj, 'parent', {
-      configurable: true,
-      writable: true,
-      enumerable: false,
-      value: parent || null,
-    });
-  }
+  },
+};
 
-  return obj;
-}
+export { mediaSelectorPlugin, pseudoClassPlugin };
diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts
index ccb6624c66..6641c2aa41 100644
--- a/packages/rrweb-snapshot/src/rebuild.ts
+++ b/packages/rrweb-snapshot/src/rebuild.ts
@@ -1,13 +1,16 @@
-import { Rule, Media, NodeWithRules, parse } from './css';
+import { mediaSelectorPlugin, pseudoClassPlugin } from './css';
 import {
-  serializedNodeWithId,
+  type serializedNodeWithId,
+  type serializedElementNodeWithId,
+  type serializedTextNodeWithId,
   NodeType,
-  tagMap,
-  elementNode,
-  BuildCache,
-  legacyAttributes,
+  type tagMap,
+  type elementNode,
+  type BuildCache,
+  type legacyAttributes,
 } from './types';
 import { isElement, Mirror, isNodeMetaEqual } from './utils';
+import postcss from 'postcss';
 
 const tagMap: tagMap = {
   script: 'noscript',
@@ -57,15 +60,6 @@ function getTagName(n: elementNode): string {
   return tagName;
 }
 
-// based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
-function escapeRegExp(str: string) {
-  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
-}
-
-const MEDIA_SELECTOR = /(max|min)-device-(width|height)/;
-const MEDIA_SELECTOR_GLOBAL = new RegExp(MEDIA_SELECTOR.source, 'g');
-const HOVER_SELECTOR = /([^\\]):hover/;
-const HOVER_SELECTOR_GLOBAL = new RegExp(HOVER_SELECTOR.source, 'g');
 export function adaptCssForReplay(
   cssText: string,
   cache: BuildCache,
@@ -74,70 +68,11 @@ export function adaptCssForReplay(
   const cachedStyle = cache?.stylesWithHoverClass.get(cssText);
   if (cachedStyle) return cachedStyle;
 
-  const ast = parse(cssText, {
-    silent: true,
-  });
-
-  if (!ast.stylesheet) {
-    return cssText;
-  }
-
-  const selectors: string[] = [];
-  const medias: string[] = [];
-  function getSelectors(rule: Rule | Media | NodeWithRules) {
-    if ('selectors' in rule && rule.selectors) {
-      rule.selectors.forEach((selector: string) => {
-        if (HOVER_SELECTOR.test(selector)) {
-          selectors.push(selector);
-        }
-      });
-    }
-    if ('media' in rule && rule.media && MEDIA_SELECTOR.test(rule.media)) {
-      medias.push(rule.media);
-    }
-    if ('rules' in rule && rule.rules) {
-      rule.rules.forEach(getSelectors);
-    }
-  }
-  getSelectors(ast.stylesheet);
-
-  let result = cssText;
-  if (selectors.length > 0) {
-    const selectorMatcher = new RegExp(
-      selectors
-        .filter((selector, index) => selectors.indexOf(selector) === index)
-        .sort((a, b) => b.length - a.length)
-        .map((selector) => {
-          return escapeRegExp(selector);
-        })
-        .join('|'),
-      'g',
-    );
-    result = result.replace(selectorMatcher, (selector) => {
-      const newSelector = selector.replace(
-        HOVER_SELECTOR_GLOBAL,
-        '$1.\\:hover',
-      );
-      return `${selector}, ${newSelector}`;
-    });
-  }
-  if (medias.length > 0) {
-    const mediaMatcher = new RegExp(
-      medias
-        .filter((media, index) => medias.indexOf(media) === index)
-        .sort((a, b) => b.length - a.length)
-        .map((media) => {
-          return escapeRegExp(media);
-        })
-        .join('|'),
-      'g',
-    );
-    result = result.replace(mediaMatcher, (media) => {
-      // not attempting to maintain min-device-width along with min-width
-      // (it's non standard)
-      return media.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2');
-    });
-  }
+  const ast: { css: string } = postcss([
+    mediaSelectorPlugin,
+    pseudoClassPlugin,
+  ]).process(cssText);
+  let result = ast.css;
   if (removeAnimationCss) result = result.replace(/animation.+?;/g, '');
   cache?.stylesWithHoverClass.set(cssText, result);
   return result;
@@ -150,6 +85,79 @@ export function createCache(): BuildCache {
   };
 }
 
+/**
+ * undo splitCssText/markCssSplits
+ * (would move to utils.ts but uses `adaptCssForReplay`)
+ */
+export function applyCssSplits(
+  n: serializedElementNodeWithId,
+  cssText: string,
+  hackCss: boolean,
+  cache: BuildCache,
+  removeAnimationCss: boolean,
+): void {
+  const childTextNodes: serializedTextNodeWithId[] = [];
+  for (const scn of n.childNodes) {
+    if (scn.type === NodeType.Text) {
+      childTextNodes.push(scn);
+    }
+  }
+  const cssTextSplits = cssText.split('/* rr_split */');
+  while (
+    cssTextSplits.length > 1 &&
+    cssTextSplits.length > childTextNodes.length
+  ) {
+    // unexpected: remerge the last two so that we don't discard any css
+    cssTextSplits.splice(-2, 2, cssTextSplits.slice(-2).join(''));
+  }
+  for (let i = 0; i < childTextNodes.length; i++) {
+    const childTextNode = childTextNodes[i];
+    const cssTextSection = cssTextSplits[i];
+    if (childTextNode && cssTextSection) {
+      // id will be assigned when these child nodes are
+      // iterated over in buildNodeWithSN
+      childTextNode.textContent = hackCss
+        ? adaptCssForReplay(cssTextSection, cache, removeAnimationCss)
+        : cssTextSection;
+    }
+  }
+}
+
+/**
+ * Normally a <style> element has a single textNode containing the rules.
+ * During serialization, we bypass this (`styleEl.sheet`) to get the rules the
+ * browser sees and serialize this to a special _cssText attribute, blanking
+ * out any text nodes. This function reverses that and also handles cases where
+ * there were no textNode children present (dynamic css/or a <link> element) as
+ * well as multiple textNodes, which need to be repopulated (based on presence of
+ * a special `rr_split` marker in case they are modified by subsequent mutations.
+ */
+export function buildStyleNode(
+  n: serializedElementNodeWithId,
+  styleEl: HTMLStyleElement, // when inlined, a <link type="stylesheet"> also gets rebuilt as a <style>
+  cssText: string,
+  options: {
+    doc: Document;
+    hackCss: boolean;
+    cache: BuildCache;
+    removeAnimationCss: boolean;
+  },
+) {
+  const { doc, hackCss, cache, removeAnimationCss } = options;
+  if (n.childNodes.length) {
+    applyCssSplits(n, cssText, hackCss, cache, removeAnimationCss);
+  } else {
+    if (hackCss) {
+      cssText = adaptCssForReplay(cssText, cache, removeAnimationCss);
+    }
+    /**
+       <link> element or dynamic <style> are serialized without any child nodes
+       we create the text node without an ID or presence in mirror as it can't
+    */
+    styleEl.appendChild(doc.createTextNode(cssText));
+  }
+}
+
 function buildNode(
   n: serializedNodeWithId,
   options: {
@@ -159,7 +167,7 @@ function buildNode(
     removeAnimationCss: boolean;
   },
 ): Node | null {
-  const { doc, hackCss, cache, removeAnimationCss } = options;
+  const { doc, hackCss, cache } = options;
   switch (n.type) {
     case NodeType.Document:
       return doc.implementation.createDocument(null, '', null);
@@ -227,14 +235,14 @@ function buildNode(
           continue;
         }
 
-        const isTextarea = tagName === 'textarea' && name === 'value';
-        const isRemoteOrDynamicCss = tagName === 'style' && name === '_cssText';
-        if (isRemoteOrDynamicCss && hackCss && typeof value === 'string') {
-          value = adaptCssForReplay(value, cache, removeAnimationCss);
-        }
-        if ((isTextarea || isRemoteOrDynamicCss) && typeof value === 'string') {
+        if (typeof value !== 'string') {
+          // pass
+        } else if (tagName === 'style' && name === '_cssText') {
+          buildStyleNode(n, node as HTMLStyleElement, value, options);
+          continue; // no need to set _cssText as attribute
+        } else if (tagName === 'textarea' && name === 'value') {
+          // create without an ID or presence in mirror
           node.appendChild(doc.createTextNode(value));
-          // https://github.com/rrweb-io/rrweb/issues/112
           n.childNodes = []; // value overrides childNodes
           continue;
         }
@@ -328,9 +336,9 @@ function buildNode(
         }
 
         if (name === 'rr_width') {
-          (node as HTMLElement).style.width = value.toString();
+          (node as HTMLElement).style.setProperty('width', value.toString());
         } else if (name === 'rr_height') {
-          (node as HTMLElement).style.height = value.toString();
+          (node as HTMLElement).style.setProperty('height', value.toString());
         } else if (
           name === 'rr_mediaCurrentTime' &&
           typeof value === 'number'
@@ -359,6 +367,11 @@ function buildNode(
           (node as HTMLMediaElement).loop = value;
         } else if (name === 'rr_mediaVolume' && typeof value === 'number') {
           (node as HTMLMediaElement).volume = value;
+        } else if (name === 'rr_open_mode') {
+          (node as HTMLDialogElement).setAttribute(
+            'rr_open_mode',
+            value as string,
+          ); // keep this attribute for rrweb to trigger showModal
         }
       }
 
@@ -384,11 +397,11 @@ function buildNode(
       return node;
     }
     case NodeType.Text:
-      return doc.createTextNode(
-        n.isStyle && hackCss
-          ? adaptCssForReplay(n.textContent, cache)
-          : n.textContent,
-      );
+      if (n.isStyle && hackCss) {
+        // support legacy style
+        return doc.createTextNode(adaptCssForReplay(n.textContent, cache));
+      }
+      return doc.createTextNode(n.textContent);
     case NodeType.CDATA:
       return doc.createCDATASection(n.textContent);
     case NodeType.Comment:
diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts
index 40c932e9b1..cd3d7189b7 100644
--- a/packages/rrweb-snapshot/src/snapshot.ts
+++ b/packages/rrweb-snapshot/src/snapshot.ts
@@ -1,17 +1,18 @@
 import {
-  serializedNode,
-  serializedNodeWithId,
+  type serializedNode,
+  type serializedNodeWithId,
   NodeType,
-  attributes,
-  MaskInputOptions,
-  SlimDOMOptions,
-  DataURLOptions,
-  MaskTextFn,
-  MaskInputFn,
-  KeepIframeSrcFn,
-  ICanvas,
-  elementNode,
-  serializedElementNodeWithId,
+  type attributes,
+  type MaskInputOptions,
+  type SlimDOMOptions,
+  type DataURLOptions,
+  type DialogAttributes,
+  type MaskTextFn,
+  type MaskInputFn,
+  type KeepIframeSrcFn,
+  type ICanvas,
+  type elementNode,
+  type serializedElementNodeWithId,
   type mediaAttributes,
 } from './types';
 import {
@@ -25,7 +26,10 @@ import {
   getInputType,
   toLowerCase,
   extractFileExtension,
+  absolutifyURLs,
+  markCssSplits,
 } from './utils';
+import dom from '@rrweb/utils';
 
 let _id = 1;
 const tagNameRegex = new RegExp('[^a-z0-9-_:]');
@@ -53,71 +57,9 @@ function getValidTagName(element: HTMLElement): Lowercase<string> {
   return processedTagName;
 }
 
-function extractOrigin(url: string): string {
-  let origin = '';
-  if (url.indexOf('//') > -1) {
-    origin = url.split('/').slice(0, 3).join('/');
-  } else {
-    origin = url.split('/')[0];
-  }
-  origin = origin.split('?')[0];
-  return origin;
-}
-
 let canvasService: HTMLCanvasElement | null;
 let canvasCtx: CanvasRenderingContext2D | null;
 
-const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm;
-const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i;
-const URL_WWW_MATCH = /^www\..*/i;
-const DATA_URI = /^(data:)([^,]*),(.*)/i;
-export function absoluteToStylesheet(
-  cssText: string | null,
-  href: string,
-): string {
-  return (cssText || '').replace(
-    URL_IN_CSS_REF,
-    (
-      origin: string,
-      quote1: string,
-      path1: string,
-      quote2: string,
-      path2: string,
-      path3: string,
-    ) => {
-      const filePath = path1 || path2 || path3;
-      const maybeQuote = quote1 || quote2 || '';
-      if (!filePath) {
-        return origin;
-      }
-      if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) {
-        return `url(${maybeQuote}${filePath}${maybeQuote})`;
-      }
-      if (DATA_URI.test(filePath)) {
-        return `url(${maybeQuote}${filePath}${maybeQuote})`;
-      }
-      if (filePath[0] === '/') {
-        return `url(${maybeQuote}${
-          extractOrigin(href) + filePath
-        }${maybeQuote})`;
-      }
-      const stack = href.split('/');
-      const parts = filePath.split('/');
-      stack.pop();
-      for (const part of parts) {
-        if (part === '.') {
-          continue;
-        } else if (part === '..') {
-          stack.pop();
-        } else {
-          stack.push(part);
-        }
-      }
-      return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`;
-    },
-  );
-}
-
 // eslint-disable-next-line no-control-regex
 const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; // Don't use \s, to avoid matching non-breaking space
 // eslint-disable-next-line no-control-regex
@@ -254,7 +196,7 @@ export function transformAttribute(
   } else if (name === 'srcset') {
     return getAbsoluteSrcsetString(doc, value);
   } else if (name === 'style') {
-    return absoluteToStylesheet(value, getHref(doc));
+    return absolutifyURLs(value, getHref(doc));
   } else if (tagName === 'object' && name === 'data') {
     return absoluteToDoc(doc, value);
   }
@@ -307,7 +249,7 @@ export function classMatchesRegex(
   if (!node) return false;
   if (node.nodeType !== node.ELEMENT_NODE) {
     if (!checkAncestors) return false;
-    return classMatchesRegex(node.parentNode, regex, checkAncestors);
+    return classMatchesRegex(dom.parentNode(node), regex, checkAncestors);
   }
 
   for (let eIndex = (node as HTMLElement).classList.length; eIndex--; ) {
@@ -317,7 +259,7 @@ export function classMatchesRegex(
     }
   }
   if (!checkAncestors) return false;
-  return classMatchesRegex(node.parentNode, regex, checkAncestors);
+  return classMatchesRegex(dom.parentNode(node), regex, checkAncestors);
 }
 
 export function needMaskingText(
@@ -326,12 +268,21 @@ export function needMaskingText(
   maskTextSelector: string | null,
   checkAncestors: boolean,
 ): boolean {
+  let el: Element;
+  if (isElement(node)) {
+    el = node;
+    if (!dom.childNodes(el).length) {
+      // optimisation: we can avoid any of the below checks on leaf elements
+      // as masking is applied to child text nodes only
+      return false;
+    }
+  } else if (dom.parentElement(node) === null) {
+    // should warn? maybe a text node isn't attached to a parent node yet?
+    return false;
+  } else {
+    el = dom.parentElement(node)!;
+  }
   try {
-    const el: HTMLElement | null =
-      node.nodeType === node.ELEMENT_NODE
-        ? (node as HTMLElement)
-        : node.parentElement;
-    if (el === null) return false;
     if (typeof maskTextClass === 'string') {
       if (checkAncestors) {
         if (el.closest(`.${maskTextClass}`)) return true;
@@ -440,7 +391,7 @@ function serializeNode(
     mirror: Mirror;
     blockClass: string | RegExp;
     blockSelector: string | null;
-    needsMask: boolean | undefined;
+    needsMask: boolean;
     inlineStylesheet: boolean;
     maskInputOptions: MaskInputOptions;
     maskTextFn: MaskTextFn | undefined;
@@ -453,6 +404,7 @@ function serializeNode(
      * `newlyAddedElement: true` skips scrollTop and scrollLeft check
      */
     newlyAddedElement?: boolean;
+    cssCaptured?: boolean;
   },
 ): serializedNode | false {
   const {
@@ -470,6 +422,7 @@ function serializeNode(
     recordCanvas,
     keepIframeSrcFn,
     newlyAddedElement = false,
+    cssCaptured = false,
   } = options;
   // Only record root id when document object is not the base document
   const rootId = getRootId(doc, mirror);
@@ -516,6 +469,7 @@ function serializeNode(
         needsMask,
         maskTextFn,
         rootId,
+        cssCaptured,
       });
     case n.CDATA_SECTION_NODE:
       return {
@@ -526,7 +480,7 @@ function serializeNode(
     case n.COMMENT_NODE:
       return {
         type: NodeType.Comment,
-        textContent: (n as Comment).textContent || '',
+        textContent: dom.textContent(n as Comment) || '',
         rootId,
       };
     default:
@@ -544,52 +498,41 @@ function serializeTextNode(
   n: Text,
   options: {
     doc: Document;
-    needsMask: boolean | undefined;
+    needsMask: boolean;
     maskTextFn: MaskTextFn | undefined;
     rootId: number | undefined;
+    cssCaptured?: boolean;
   },
 ): serializedNode {
-  const { needsMask, maskTextFn, rootId } = options;
+  const { needsMask, maskTextFn, rootId, cssCaptured } = options;
   // The parent node may not be a html element which has a tagName attribute.
   // So just let it be undefined which is ok in this use case.
-  const parentTagName = n.parentNode && (n.parentNode as HTMLElement).tagName;
-  let textContent = n.textContent;
+  const parent = dom.parentNode(n);
+  const parentTagName = parent && (parent as HTMLElement).tagName;
+  let textContent: string | null = '';
   const isStyle = parentTagName === 'STYLE' ? true : undefined;
   const isScript = parentTagName === 'SCRIPT' ? true : undefined;
-  if (isStyle && textContent) {
-    try {
-      // try to read style sheet
-      if (n.nextSibling || n.previousSibling) {
-        // This is not the only child of the stylesheet.
-        // We can't read all of the sheet's .cssRules and expect them
-        // to _only_ include the current rule(s) added by the text node.
-        // So we'll be conservative and keep textContent as-is.
-      } else if ((n.parentNode as HTMLStyleElement).sheet?.cssRules) {
-        textContent = stringifyStylesheet(
-          (n.parentNode as HTMLStyleElement).sheet!,
-        );
-      }
-    } catch (err) {
-      console.warn(
-        `Cannot get CSS styles from text's parentNode. Error: ${err as string}`,
-        n,
-      );
-    }
-    textContent = absoluteToStylesheet(textContent, getHref(options.doc));
-  }
   if (isScript) {
     textContent = 'SCRIPT_PLACEHOLDER';
+  } else if (!cssCaptured) {
+    textContent = dom.textContent(n);
+    if (isStyle && textContent) {
+      // mutation only: we don't need to use stringifyStylesheet
+      // as a <style> text node mutation obliterates any previous
+      // programmatic rule manipulation (.insertRule etc.)
+      // so the current textContent represents the most up to date state
+      textContent = absolutifyURLs(textContent, getHref(options.doc));
+    }
   }
   if (!isStyle && !isScript && textContent && needsMask) {
     textContent = maskTextFn
-      ? maskTextFn(textContent, n.parentElement)
+      ? maskTextFn(textContent, dom.parentElement(n))
       : textContent.replace(/[\S]/g, '*');
   }
 
   return {
     type: NodeType.Text,
     textContent: textContent || '',
-    isStyle,
     rootId,
   };
 }
@@ -645,6 +588,7 @@ function serializeElementNode(
   }
   // remote css
   if (tagName === 'link' && inlineStylesheet) {
+    //TODO: maybe replace this `.styleSheets` with original one
     const stylesheet = Array.from(doc.styleSheets).find((s) => {
       return s.href === (n as HTMLLinkElement).href;
     });
@@ -655,21 +599,18 @@ function serializeElementNode(
     if (cssText) {
       delete attributes.rel;
       delete attributes.href;
-      attributes._cssText = absoluteToStylesheet(cssText, stylesheet!.href!);
+      attributes._cssText = cssText;
     }
   }
-  // dynamic stylesheet
-  if (
-    tagName === 'style' &&
-    (n as HTMLStyleElement).sheet &&
-    // TODO: Currently we only try to get dynamic stylesheet when it is an empty style element
-    !(n.innerText || n.textContent || '').trim().length
-  ) {
-    const cssText = stringifyStylesheet(
+  if (tagName === 'style' && (n as HTMLStyleElement).sheet) {
+    let cssText = stringifyStylesheet(
       (n as HTMLStyleElement).sheet as CSSStyleSheet,
     );
     if (cssText) {
-      attributes._cssText = absoluteToStylesheet(cssText, getHref(doc));
+      if (n.childNodes.length > 1) {
+        cssText = markCssSplits(cssText, n as HTMLStyleElement);
+      }
+      attributes._cssText = cssText;
     }
   }
   // form fields
@@ -704,6 +645,16 @@ function serializeElementNode(
       delete attributes.selected;
     }
   }
+
+  if (tagName === 'dialog' && (n as HTMLDialogElement).open) {
+    // register what type of dialog is this
+    // `modal` or `non-modal`
+    // this is used to trigger `showModal()` or `show()` on replay (outside of rrweb-snapshot, in rrweb)
+    (attributes as DialogAttributes).rr_open_mode = n.matches('dialog:modal')
+      ? 'modal'
+      : 'non-modal';
+  }
+
   // canvas image data
   if (tagName === 'canvas' && recordCanvas) {
     if ((n as ICanvas).__context === '2d') {
@@ -977,6 +928,7 @@ export function serializeNodeWithId(
       node: serializedElementNodeWithId,
     ) => unknown;
     stylesheetLoadTimeout?: number;
+    cssCaptured?: boolean;
   },
 ): serializedNodeWithId | null {
   const {
@@ -1002,14 +954,12 @@ export function serializeNodeWithId(
     stylesheetLoadTimeout = 5000,
     keepIframeSrcFn = () => false,
     newlyAddedElement = false,
+    cssCaptured = false,
   } = options;
   let { needsMask } = options;
   let { preserveWhiteSpace = true } = options;
 
-  if (
-    !needsMask &&
-    n.childNodes // we can avoid the check on leaf elements, as masking is applied to child text nodes only
-  ) {
+  if (!needsMask) {
     // perf: if needsMask = true, children won't also need to check
     const checkAncestors = needsMask === undefined; // if false, we've already checked ancestors
     needsMask = needMaskingText(
@@ -1035,6 +985,7 @@ export function serializeNodeWithId(
     recordCanvas,
     keepIframeSrcFn,
     newlyAddedElement,
+    cssCaptured,
   });
   if (!_serializedNode) {
     // TODO: dev only
@@ -1050,7 +1001,6 @@ export function serializeNodeWithId(
     slimDOMExcluded(_serializedNode, slimDOMOptions) ||
     (!preserveWhiteSpace &&
       _serializedNode.type === NodeType.Text &&
-      !_serializedNode.isStyle &&
       !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)
   ) {
     id = IGNORED_NODE;
@@ -1074,8 +1024,8 @@ export function serializeNodeWithId(
     recordChild = recordChild && !serializedNode.needBlock;
     // this property was not needed in replay side
     delete serializedNode.needBlock;
-    const shadowRoot = (n as HTMLElement).shadowRoot;
-    if (shadowRoot && isNativeShadowDom(shadowRoot))
+    const shadowRootEl = dom.shadowRoot(n);
+    if (shadowRootEl && isNativeShadowDom(shadowRootEl))
       serializedNode.isShadowHost = true;
   }
   if (
@@ -1115,6 +1065,7 @@ export function serializeNodeWithId(
       onStylesheetLoad,
       stylesheetLoadTimeout,
       keepIframeSrcFn,
+      cssCaptured: false,
     };
 
     if (
@@ -1124,7 +1075,14 @@ export function serializeNodeWithId(
     ) {
       // value parameter in DOM reflects the correct value, so ignore childNode
     } else {
-      for (const childN of Array.from(n.childNodes)) {
+      if (
+        serializedNode.type === NodeType.Element &&
+        (serializedNode as elementNode).attributes._cssText !== undefined &&
+        typeof serializedNode.attributes._cssText === 'string'
+      ) {
+        bypassOptions.cssCaptured = true;
+      }
+      for (const childN of Array.from(dom.childNodes(n))) {
         const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
         if (serializedChildNode) {
           serializedNode.childNodes.push(serializedChildNode);
@@ -1132,11 +1090,12 @@ export function serializeNodeWithId(
       }
     }
 
-    if (isElement(n) && n.shadowRoot) {
-      for (const childN of Array.from(n.shadowRoot.childNodes)) {
+    let shadowRootEl: ShadowRoot | null = null;
+    if (isElement(n) && (shadowRootEl = dom.shadowRoot(n))) {
+      for (const childN of Array.from(dom.childNodes(shadowRootEl))) {
         const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
         if (serializedChildNode) {
-          isNativeShadowDom(n.shadowRoot) &&
+          isNativeShadowDom(shadowRootEl) &&
             (serializedChildNode.isShadow = true);
           serializedNode.childNodes.push(serializedChildNode);
         }
@@ -1144,11 +1103,8 @@ export function serializeNodeWithId(
     }
   }
 
-  if (
-    n.parentNode &&
-    isShadowRoot(n.parentNode) &&
-    isNativeShadowDom(n.parentNode)
-  ) {
+  const parent = dom.parentNode(n);
+  if (parent && isShadowRoot(parent) && isNativeShadowDom(parent)) {
     serializedNode.isShadow = true;
   }
 
@@ -1265,7 +1221,7 @@ function snapshot(
     inlineStylesheet?: boolean;
     maskAllInputs?: boolean | MaskInputOptions;
     maskTextFn?: MaskTextFn;
-    maskInputFn?: MaskTextFn;
+    maskInputFn?: MaskInputFn;
     slimDOM?: 'all' | boolean | SlimDOMOptions;
     dataURLOptions?: DataURLOptions;
     inlineImages?: boolean;
diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts
index e4538a827a..95a4024960 100644
--- a/packages/rrweb-snapshot/src/types.ts
+++ b/packages/rrweb-snapshot/src/types.ts
@@ -20,9 +20,18 @@ export type documentTypeNode = {
   systemId: string;
 };
 
-export type attributes = {
-  [key: string]: string | number | true | null;
+type cssTextKeyAttr = {
+  _cssText?: string;
 };
+
+export type attributes = cssTextKeyAttr & {
+  [key: string]:
+    | string
+    | number // properties e.g. rr_scrollLeft or rr_mediaCurrentTime
+    | true // e.g. checked  on <input type="radio">
+    | null; // an indication that an attribute was removed (during a mutation)
+};
+
 export type legacyAttributes = {
   /**
    * @deprecated old bug in rrweb was causing these to always be set
@@ -45,6 +54,10 @@ export type elementNode = {
 export type textNode = {
   type: NodeType.Text;
   textContent: string;
+  /**
+   * @deprecated styles are now always snapshotted against parent <style> element
+   * style mutations can still happen via an added textNode, but they don't need this attribute for correct replay
+   */
   isStyle?: true;
 };
 
@@ -78,6 +91,11 @@ export type serializedElementNodeWithId = Extract<
   Record<'type', NodeType.Element>
 >;
 
+export type serializedTextNodeWithId = Extract<
+  serializedNodeWithId,
+  Record<'type', NodeType.Text>
+>;
+
 export type tagMap = {
   [key: string]: string;
 };
@@ -103,6 +121,23 @@ export type mediaAttributes = {
   rr_mediaVolume?: number;
 };
 
+export type DialogAttributes = {
+  open: string;
+  /**
+   * Represents the dialog's open mode.
+   * `modal` means the dialog is opened with `showModal()`.
+   * `non-modal` means the dialog is opened with `show()` or
+   * by adding an `open` attribute.
+   */
+  rr_open_mode: 'modal' | 'non-modal';
+  /**
+   * Currently unimplemented, but in future can be used to:
+   * Represents the order of which of the dialog was opened.
+   * This is useful for replaying the dialog `.showModal()` in the correct order.
+   */
+  // rr_open_mode_index?: number;
+};
+
 // @deprecated
 export interface INode extends Node {
   __sn: serializedNodeWithId;
diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts
index b3a6fa36f0..862a3e5bf4 100644
--- a/packages/rrweb-snapshot/src/utils.ts
+++ b/packages/rrweb-snapshot/src/utils.ts
@@ -1,4 +1,4 @@
-import {
+import type {
   idNodeMap,
   MaskInputFn,
   MaskInputOptions,
@@ -6,20 +6,26 @@ import {
   IMirror,
   serializedNodeWithId,
   serializedNode,
-  NodeType,
   documentNode,
   documentTypeNode,
   textNode,
   elementNode,
 } from './types';
+import dom from '@rrweb/utils';
+import { NodeType } from './types';
 
 export function isElement(n: Node): n is Element {
   return n.nodeType === n.ELEMENT_NODE;
 }
 
 export function isShadowRoot(n: Node): n is ShadowRoot {
-  const host: Element | null = (n as ShadowRoot)?.host;
-  return Boolean(host?.shadowRoot === n);
+  const hostEl: Element | null =
+    // anchor and textarea elements also have a `host` property
+    // but only shadow roots have a `mode` property
+    (n && 'host' in n && 'mode' in n && dom.host(n as ShadowRoot)) || null;
+  return Boolean(
+    hostEl && 'shadowRoot' in hostEl && dom.shadowRoot(hostEl) === n,
+  );
 }
 
 /**
@@ -93,22 +99,38 @@ export function escapeImportStatement(rule: CSSImportRule): string {
   return statement.join(' ') + ';';
 }
 
+/*
+ * serialize the css rules from the .sheet property
+ * for <link rel="stylesheet"> elements, this is the only way of getting the rules without a FETCH
+ * for <style> elements, this is less preferable to looking at childNodes[0].textContent
+ * (which will include vendor prefixed rules which may not be used or visible to the recorded browser,
+ * but which might be needed by the replayer browser)
+ * however, at snapshot time, we don't know whether the style element has suffered
+ * any programmatic manipulation prior to the snapshot, in which case the .sheet would be more up to date
+ */
 export function stringifyStylesheet(s: CSSStyleSheet): string | null {
   try {
     const rules = s.rules || s.cssRules;
-    return rules
-      ? fixBrowserCompatibilityIssuesInCSS(
-          Array.from(rules, stringifyRule).join(''),
-        )
-      : null;
+    if (!rules) {
+      return null;
+    }
+    let sheetHref = s.href;
+    if (!sheetHref && s.ownerNode && s.ownerNode.ownerDocument) {
+      // an inline <style> element
+      sheetHref = s.ownerNode.ownerDocument.location.href;
+    }
+    const stringifiedRules = Array.from(rules, (rule: CSSRule) =>
+      stringifyRule(rule, sheetHref),
+    ).join('');
+    return fixBrowserCompatibilityIssuesInCSS(stringifiedRules);
   } catch (error) {
     return null;
   }
 }
 
-export function stringifyRule(rule: CSSRule): string {
-  let importStringified;
+export function stringifyRule(rule: CSSRule, sheetHref: string | null): string {
   if (isCSSImportRule(rule)) {
+    let importStringified;
     try {
       importStringified =
         // for same-origin stylesheets,
@@ -117,15 +139,25 @@ export function stringifyRule(rule: CSSRule): string {
         // work around browser issues with the raw string `@import url(...)` statement
         escapeImportStatement(rule);
     } catch (error) {
-      // ignore
+      importStringified = rule.cssText;
+    }
+    if (rule.styleSheet.href) {
+      // url()s within the imported stylesheet are relative to _that_ sheet's href
+      return absolutifyURLs(importStringified, rule.styleSheet.href);
+    }
+    return importStringified;
+  } else {
+    let ruleStringified = rule.cssText;
+    if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) {
+      // Safari does not escape selectors with : properly
+      // see https://bugs.webkit.org/show_bug.cgi?id=184604
+      ruleStringified = fixSafariColons(ruleStringified);
+    }
+    if (sheetHref) {
+      return absolutifyURLs(ruleStringified, sheetHref);
     }
-  } else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) {
-    // Safari does not escape selectors with : properly
-    // see https://bugs.webkit.org/show_bug.cgi?id=184604
-    return fixSafariColons(rule.cssText);
+    return ruleStringified;
   }
-
-  return importStringified || rule.cssText;
 }
 
 export function fixSafariColons(cssStringified: string): string {
@@ -351,3 +383,121 @@ export function extractFileExtension(
   const match = url.pathname.match(regex);
   return match?.[1] ?? null;
 }
+
+function extractOrigin(url: string): string {
+  let origin = '';
+  if (url.indexOf('//') > -1) {
+    origin = url.split('/').slice(0, 3).join('/');
+  } else {
+    origin = url.split('/')[0];
+  }
+  origin = origin.split('?')[0];
+  return origin;
+}
+
+const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm;
+const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i;
+const URL_WWW_MATCH = /^www\..*/i;
+const DATA_URI = /^(data:)([^,]*),(.*)/i;
+export function absolutifyURLs(cssText: string | null, href: string): string {
+  return (cssText || '').replace(
+    URL_IN_CSS_REF,
+    (
+      origin: string,
+      quote1: string,
+      path1: string,
+      quote2: string,
+      path2: string,
+      path3: string,
+    ) => {
+      const filePath = path1 || path2 || path3;
+      const maybeQuote = quote1 || quote2 || '';
+      if (!filePath) {
+        return origin;
+      }
+      if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) {
+        return `url(${maybeQuote}${filePath}${maybeQuote})`;
+      }
+      if (DATA_URI.test(filePath)) {
+        return `url(${maybeQuote}${filePath}${maybeQuote})`;
+      }
+      if (filePath[0] === '/') {
+        return `url(${maybeQuote}${
+          extractOrigin(href) + filePath
+        }${maybeQuote})`;
+      }
+      const stack = href.split('/');
+      const parts = filePath.split('/');
+      stack.pop();
+      for (const part of parts) {
+        if (part === '.') {
+          continue;
+        } else if (part === '..') {
+          stack.pop();
+        } else {
+          stack.push(part);
+        }
+      }
+      return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`;
+    },
+  );
+}
+
+/**
+ * Intention is to normalize by remove spaces, semicolons and CSS comments
+ * so that we can compare css as authored vs. output of stringifyStylesheet
+ */
+export function normalizeCssString(cssText: string): string {
+  return cssText.replace(/(\/\*[^*]*\*\/)|[\s;]/g, '');
+}
+
+/**
+ * Maps the output of stringifyStylesheet to individual text nodes of a <style> element
+ * performance is not considered as this is anticipated to be very much an edge case
+ * (javascript is needed to add extra text nodes to a <style>)
+ */
+export function splitCssText(
+  cssText: string,
+  style: HTMLStyleElement,
+): string[] {
+  const childNodes = Array.from(style.childNodes);
+  const splits: string[] = [];
+  if (childNodes.length > 1 && cssText && typeof cssText === 'string') {
+    const cssTextNorm = normalizeCssString(cssText);
+    for (let i = 1; i < childNodes.length; i++) {
+      if (
+        childNodes[i].textContent &&
+        typeof childNodes[i].textContent === 'string'
+      ) {
+        const textContentNorm = normalizeCssString(childNodes[i].textContent!);
+        for (let j = 3; j < textContentNorm.length; j++) {
+          // find a  substring that appears only once
+          const bit = textContentNorm.substring(0, j);
+          if (cssTextNorm.split(bit).length === 2) {
+            const splitNorm = cssTextNorm.indexOf(bit);
+            // find the split point in the original text
+            for (let k = splitNorm; k < cssText.length; k++) {
+              if (
+                normalizeCssString(cssText.substring(0, k)).length === splitNorm
+              ) {
+                splits.push(cssText.substring(0, k));
+                cssText = cssText.substring(k);
+                break;
+              }
+            }
+            break;
+          }
+        }
+      }
+    }
+  }
+  splits.push(cssText); // either the full thing if no splits were found, or the last split
+  return splits;
+}
+
+export function markCssSplits(
+  cssText: string,
+  style: HTMLStyleElement,
+): string {
+  return splitCssText(cssText, style).join('/* rr_split */');
+}
diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap
index f43a3724b3..d3f4a0e44c 100644
--- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap
+++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap
@@ -1,5 +1,123 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
+exports[`dialog integration tests > should capture open attribute for modal dialogs 1`] = `
+"{
+  \\"type\\": 0,
+  \\"childNodes\\": [
+    {
+      \\"type\\": 2,
+      \\"tagName\\": \\"html\\",
+      \\"attributes\\": {},
+      \\"childNodes\\": [
+        {
+          \\"type\\": 2,
+          \\"tagName\\": \\"head\\",
+          \\"attributes\\": {},
+          \\"childNodes\\": [],
+          \\"id\\": 3
+        },
+        {
+          \\"type\\": 2,
+          \\"tagName\\": \\"body\\",
+          \\"attributes\\": {},
+          \\"childNodes\\": [
+            {
+              \\"type\\": 3,
+              \\"textContent\\": \\"\\\\n    \\",
+              \\"id\\": 5
+            },
+            {
+              \\"type\\": 2,
+              \\"tagName\\": \\"dialog\\",
+              \\"attributes\\": {
+                \\"open\\": \\"\\",
+                \\"rr_open_mode\\": \\"modal\\"
+              },
+              \\"childNodes\\": [
+                {
+                  \\"type\\": 3,
+                  \\"textContent\\": \\"I'm a dialog\\",
+                  \\"id\\": 7
+                }
+              ],
+              \\"id\\": 6
+            },
+            {
+              \\"type\\": 3,
+              \\"textContent\\": \\"\\\\n  \\\\n\\\\n\\",
+              \\"id\\": 8
+            }
+          ],
+          \\"id\\": 4
+        }
+      ],
+      \\"id\\": 2
+    }
+  ],
+  \\"compatMode\\": \\"BackCompat\\",
+  \\"id\\": 1
+}"
+`;
+
+exports[`dialog integration tests > should capture open attribute for non modal dialogs 1`] = `
+"{
+  \\"type\\": 0,
+  \\"childNodes\\": [
+    {
+      \\"type\\": 2,
+      \\"tagName\\": \\"html\\",
+      \\"attributes\\": {},
+      \\"childNodes\\": [
+        {
+          \\"type\\": 2,
+          \\"tagName\\": \\"head\\",
+          \\"attributes\\": {},
+          \\"childNodes\\": [],
+          \\"id\\": 3
+        },
+        {
+          \\"type\\": 2,
+          \\"tagName\\": \\"body\\",
+          \\"attributes\\": {},
+          \\"childNodes\\": [
+            {
+              \\"type\\": 3,
+              \\"textContent\\": \\"\\\\n    \\",
+              \\"id\\": 5
+            },
+            {
+              \\"type\\": 2,
+              \\"tagName\\": \\"dialog\\",
+              \\"attributes\\": {
+                \\"open\\": \\"\\",
+                \\"rr_open_mode\\": \\"non-modal\\"
+              },
+              \\"childNodes\\": [
+                {
+                  \\"type\\": 3,
+                  \\"textContent\\": \\"I'm a dialog\\",
+                  \\"id\\": 7
+                }
+              ],
+              \\"id\\": 6
+            },
+            {
+              \\"type\\": 3,
+              \\"textContent\\": \\"\\\\n  \\\\n\\\\n\\",
+              \\"id\\": 8
+            }
+          ],
+          \\"id\\": 4
+        }
+      ],
+      \\"id\\": 2
+    }
+  ],
+  \\"compatMode\\": \\"BackCompat\\",
+  \\"id\\": 1
+}"
+`;
+
 exports[`iframe integration tests > snapshot async iframes 1`] = `
 "{
   \\"type\\": 0,
@@ -214,6 +332,12 @@ exports[`integration tests > [html file]: cors-style-sheet.html 1`] = `
   <body></body></html>"
 `;
 
+exports[`integration tests > [html file]: dialog.html 1`] = `
+"<html><head></head><body>
+    <dialog>I'm a dialog</dialog>
+  </body></html>"
+`;
+
 exports[`integration tests > [html file]: dynamic-stylesheet.html 1`] = `
 "<!DOCTYPE html><html lang=\\"en\\"><head>
     <meta charset=\\"UTF-8\\" />
@@ -269,7 +393,9 @@ exports[`integration tests > [html file]: hover.html 1`] = `
   <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" />
   <meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\" />
   <title>hover selector</title>
-  <style>div:hover, div.\\\\:hover { background: orange; }div:hover::after, div.\\\\:hover::after { position: absolute; left: 0px; top: 100%; content: \\"dropdown\\"; width: 100px; height: 200px; background: lightblue; }</style>
+  <style>div:hover,
+div.\\\\:hover { background: orange; }div:hover::after,
+div.\\\\:hover::after { position: absolute; left: 0px; top: 100%; content: \\"dropdown\\"; width: 100px; height: 200px; background: lightblue; }</style>
 </head><body>
   <div>hover me</div>
 </body></html>"
@@ -487,7 +613,7 @@ exports[`integration tests > [html file]: with-style-sheet.html 1`] = `
   <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" />
   <meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\" />
   <title>with style sheet</title>
-  <style>body { margin: 0px; background: url(\\"http://localhost:3030/a.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/b.jpg\\"); }body &gt; p { color: yellow; }</style>
+  <style>body { margin: 0px; background: url(\\"http://localhost:3030/should-be-in-root-folder.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/should-be-in-css-folder.jpg\\"); }body &gt; p { color: yellow; }</style>
 </head><body>
 </body></html>"
 `;
@@ -498,11 +624,224 @@ exports[`integration tests > [html file]: with-style-sheet-with-import.html 1`]
   <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" />
   <meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\" />
   <title>with style sheet with import</title>
-  <style>@import url(\\"//fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&amp;family=Roboto:wght@100;300;400;500;700&amp;display=swap\\\\\\"\\");body { margin: 0px; background: url(\\"http://localhost:3030/a.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/b.jpg\\"); }body &gt; p { color: yellow; }</style>
+  <style>@import url(\\"//fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&amp;family=Roboto:wght@100;300;400;500;700&amp;display=swap\\\\\\"\\");body { margin: 0px; background: url(\\"http://localhost:3030/should-be-in-root-folder.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/css/should-be-in-css-folder.jpg\\"); }body &gt; p { color: yellow; }body { margin: 0px; background: url(\\"http://localhost:3030/should-be-in-root-folder.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/alt-css/should-be-in-alt-css-folder.jpg\\"); }body &gt; p { color: yellow; }</style>
+  <style>body { margin: 0px; background: url(\\"http://localhost:3030/should-be-in-root-folder.jpg\\"); border-image: url(\\"data:image/svg+xml;utf8,&lt;svg xmlns=\\\\\\"http://www.w3.org/2000/svg\\\\\\" x=\\\\\\"0px\\\\\\" y=\\\\\\"0px\\\\\\" viewBox=\\\\\\"0 0 256 256\\\\\\"&gt;&lt;g&gt;&lt;g&gt;&lt;polygon points=\\\\\\"79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128\\\\\\"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;\\") 100% / 1 / 0 stretch; }p { color: red; background: url(\\"http://localhost:3030/alt-css/should-be-in-alt-css-folder.jpg\\"); }body &gt; p { color: yellow; }section { background: url(\\"http://localhost:3030/should-be-in-root-folder.jpg\\"); }</style>
 </head><body>
 </body></html>"
 `;
 
+exports[`integration tests > should be able to record elements even when .childNodes has been monkey patched 1`] = `
+"{
+  \\"type\\": 0,
+  \\"childNodes\\": [
+    {
+      \\"type\\": 1,
+      \\"name\\": \\"html\\",
+      \\"publicId\\": \\"\\",
+      \\"systemId\\": \\"\\",
+      \\"id\\": 2
+    },
+    {
+      \\"type\\": 2,
+      \\"tagName\\": \\"html\\",
+      \\"attributes\\": {
+        \\"lang\\": \\"en\\"
+      },
+      \\"childNodes\\": [
+        {
+          \\"type\\": 2,
+          \\"tagName\\": \\"head\\",
+          \\"attributes\\": {},
+          \\"childNodes\\": [
+            {
+              \\"type\\": 3,
+              \\"textContent\\": \\"\\\\n    \\",
+              \\"id\\": 5
+            },
+            {
+              \\"type\\": 2,
+              \\"tagName\\": \\"meta\\",
+              \\"attributes\\": {
+                \\"charset\\": \\"UTF-8\\"
+              },
+              \\"childNodes\\": [],
+              \\"id\\": 6
+            },
+            {
+              \\"type\\": 3,
+              \\"textContent\\": \\"\\\\n    \\",
+              \\"id\\": 7
+            },
+            {
+              \\"type\\": 2,
+              \\"tagName\\": \\"meta\\",
+              \\"attributes\\": {
+                \\"name\\": \\"viewport\\",
+                \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+              },
+              \\"childNodes\\": [],
+              \\"id\\": 8
+            },
+            {
+              \\"type\\": 3,
+              \\"textContent\\": \\"\\\\n    \\",
+              \\"id\\": 9
+            },
+            {
+              \\"type\\": 2,
+              \\"tagName\\": \\"title\\",
+              \\"attributes\\": {},
+              \\"childNodes\\": [
+                {
+                  \\"type\\": 3,
+                  \\"textContent\\": \\"Document\\",
+                  \\"id\\": 11
+                }
+              ],
+              \\"id\\": 10
+            },
+            {
+              \\"type\\": 3,
+              \\"textContent\\": \\"\\\\n    \\",
+              \\"id\\": 12
+            },
+            {
+              \\"type\\": 2,
+              \\"tagName\\": \\"script\\",
+              \\"attributes\\": {},
+              \\"childNodes\\": [
+                {
+                  \\"type\\": 3,
+                  \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                  \\"id\\": 14
+                }
+              ],
+              \\"id\\": 13
+            },
+            {
+              \\"type\\": 3,
+              \\"textContent\\": \\"\\\\n  \\",
+              \\"id\\": 15
+            }
+          ],
+          \\"id\\": 4
+        },
+        {
+          \\"type\\": 3,
+          \\"textContent\\": \\"\\\\n  \\",
+          \\"id\\": 16
+        },
+        {
+          \\"type\\": 2,
+          \\"tagName\\": \\"body\\",
+          \\"attributes\\": {},
+          \\"childNodes\\": [
+            {
+              \\"type\\": 3,
+              \\"textContent\\": \\"\\\\n    \\",
+              \\"id\\": 18
+            },
+            {
+              \\"type\\": 2,
+              \\"tagName\\": \\"ul\\",
+              \\"attributes\\": {},
+              \\"childNodes\\": [
+                {
+                  \\"type\\": 3,
+                  \\"textContent\\": \\"\\\\n      \\",
+                  \\"id\\": 20
+                },
+                {
+                  \\"type\\": 2,
+                  \\"tagName\\": \\"li\\",
+                  \\"attributes\\": {},
+                  \\"childNodes\\": [
+                    {
+                      \\"type\\": 3,
+                      \\"textContent\\": \\"a\\",
+                      \\"id\\": 22
+                    }
+                  ],
+                  \\"id\\": 21
+                },
+                {
+                  \\"type\\": 3,
+                  \\"textContent\\": \\"\\\\n      \\",
+                  \\"id\\": 23
+                },
+                {
+                  \\"type\\": 2,
+                  \\"tagName\\": \\"li\\",
+                  \\"attributes\\": {},
+                  \\"childNodes\\": [
+                    {
+                      \\"type\\": 3,
+                      \\"textContent\\": \\"b\\",
+                      \\"id\\": 25
+                    }
+                  ],
+                  \\"id\\": 24
+                },
+                {
+                  \\"type\\": 3,
+                  \\"textContent\\": \\"\\\\n      \\",
+                  \\"id\\": 26
+                },
+                {
+                  \\"type\\": 2,
+                  \\"tagName\\": \\"li\\",
+                  \\"attributes\\": {},
+                  \\"childNodes\\": [
+                    {
+                      \\"type\\": 3,
+                      \\"textContent\\": \\"c\\",
+                      \\"id\\": 28
+                    }
+                  ],
+                  \\"id\\": 27
+                },
+                {
+                  \\"type\\": 3,
+                  \\"textContent\\": \\"\\\\n      \\",
+                  \\"id\\": 29
+                },
+                {
+                  \\"type\\": 2,
+                  \\"tagName\\": \\"li\\",
+                  \\"attributes\\": {},
+                  \\"childNodes\\": [
+                    {
+                      \\"type\\": 3,
+                      \\"textContent\\": \\"d\\",
+                      \\"id\\": 31
+                    }
+                  ],
+                  \\"id\\": 30
+                },
+                {
+                  \\"type\\": 3,
+                  \\"textContent\\": \\"\\\\n    \\",
+                  \\"id\\": 32
+                }
+              ],
+              \\"id\\": 19
+            },
+            {
+              \\"type\\": 3,
+              \\"textContent\\": \\"\\\\n  \\\\n\\\\n\\",
+              \\"id\\": 33
+            }
+          ],
+          \\"id\\": 17
+        }
+      ],
+      \\"id\\": 3
+    }
+  ],
+  \\"id\\": 1
+}"
+`;
+
 exports[`shadow DOM integration tests > snapshot shadow DOM 1`] = `
 "{
   \\"type\\": 0,
@@ -755,12 +1094,13 @@ exports[`shadow DOM integration tests > snapshot shadow DOM 1`] = `
                 {
                   \\"type\\": 2,
                   \\"tagName\\": \\"style\\",
-                  \\"attributes\\": {},
+                  \\"attributes\\": {
+                    \\"_cssText\\": \\":host { display: inline-block; width: 650px; font-family: \\\\\\"Roboto Slab\\\\\\"; contain: content; }:host([background]) { background: var(--background-color, #9E9E9E); border-radius: 10px; padding: 10px; }#panels { box-shadow: rgba(0, 0, 0, 0.3) 0px 2px 2px; background: white; border-radius: 3px; padding: 16px; height: 250px; overflow: auto; }#tabs { display: inline-flex; user-select: none; }#tabs slot { display: inline-flex; }#tabs ::slotted(*) { font: 400 16px / 22px Roboto; padding: 16px 8px; margin: 0px; text-align: center; width: 100px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; cursor: pointer; border-top-left-radius: 3px; border-top-right-radius: 3px; background: linear-gradient(rgb(250, 250, 250), rgb(238, 238, 238)); border: none; }#tabs ::slotted([aria-selected=\\\\\\"true\\\\\\"]) { font-weight: 600; background: white; box-shadow: none; }#tabs ::slotted(:focus) { z-index: 1; }#panels ::slotted([aria-hidden=\\\\\\"true\\\\\\"]) { display: none; }\\"
+                  },
                   \\"childNodes\\": [
                     {
                       \\"type\\": 3,
-                      \\"textContent\\": \\":host { display: inline-block; width: 650px; font-family: \\\\\\"Roboto Slab\\\\\\"; contain: content; }:host([background]) { background: var(--background-color, #9E9E9E); border-radius: 10px; padding: 10px; }#panels { box-shadow: rgba(0, 0, 0, 0.3) 0px 2px 2px; background: white; border-radius: 3px; padding: 16px; height: 250px; overflow: auto; }#tabs { display: inline-flex; user-select: none; }#tabs slot { display: inline-flex; }#tabs ::slotted(*) { font: 400 16px / 22px Roboto; padding: 16px 8px; margin: 0px; text-align: center; width: 100px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; cursor: pointer; border-top-left-radius: 3px; border-top-right-radius: 3px; background: linear-gradient(rgb(250, 250, 250), rgb(238, 238, 238)); border: none; }#tabs ::slotted([aria-selected=\\\\\\"true\\\\\\"]) { font-weight: 600; background: white; box-shadow: none; }#tabs ::slotted(:focus) { z-index: 1; }#panels ::slotted([aria-hidden=\\\\\\"true\\\\\\"]) { display: none; }\\",
-                      \\"isStyle\\": true,
+                      \\"textContent\\": \\"\\",
                       \\"id\\": 38
                     }
                   ],
diff --git a/packages/rrweb-snapshot/test/alt-css/alt-style.css b/packages/rrweb-snapshot/test/alt-css/alt-style.css
new file mode 100644
index 0000000000..bda88053d8
--- /dev/null
+++ b/packages/rrweb-snapshot/test/alt-css/alt-style.css
@@ -0,0 +1,12 @@
+body {
+    margin: 0;
+    background: url('../should-be-in-root-folder.jpg');
+    border-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 256 256"><g><g><polygon points="79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128"/></g></g></svg>');
+  }
+  p {
+    color: red;
+    background: url('./should-be-in-alt-css-folder.jpg');
+  }
+  body > p {
+    color: yellow;
+  }
diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts
index fc8e5a2946..75e261c102 100644
--- a/packages/rrweb-snapshot/test/css.test.ts
+++ b/packages/rrweb-snapshot/test/css.test.ts
@@ -1,255 +1,250 @@
-import { describe, it, expect } from 'vitest';
-import { parse, Rule, Media } from '../src/css';
-import { fixSafariColons, escapeImportStatement } from './../src/utils';
+/**
+ * @vitest-environment jsdom
+ */
+import { describe, it, beforeEach, expect } from 'vitest';
+import { mediaSelectorPlugin, pseudoClassPlugin } from '../src/css';
+import postcss, { type AcceptedPlugin } from 'postcss';
+import { JSDOM } from 'jsdom';
+import { splitCssText, stringifyStylesheet } from './../src/utils';
+import { applyCssSplits } from './../src/rebuild';
+import {
+  NodeType,
+  type serializedElementNodeWithId,
+  type BuildCache,
+  type textNode,
+} from '../src/types';
+import { Window } from 'happy-dom';
 
 describe('css parser', () => {
-  it('should save the filename and source', () => {
-    const css = 'booty {\n  size: large;\n}\n';
-    const ast = parse(css, {
-      source: 'booty.css',
+  function parse(plugin: AcceptedPlugin, input: string): string {
+    const ast = postcss([plugin]).process(input, {});
+    return ast.css;
+  }
+
+  describe('mediaSelectorPlugin', () => {
+    it('selectors without device remain unchanged', () => {
+      const cssText =
+        '@media only screen and (min-width: 1200px) { .a { width: 10px; }}';
+      expect(parse(mediaSelectorPlugin, cssText)).toEqual(cssText);
     });
 
-    expect(ast.stylesheet!.source).toEqual('booty.css');
-
-    const position = ast.stylesheet!.rules[0].position!;
-    expect(position.start).toBeTruthy();
-    expect(position.end).toBeTruthy();
-    expect(position.source).toEqual('booty.css');
-    expect(position.content).toEqual(css);
+    it('can adapt media rules to replay context', () => {
+      [
+        ['min', 'width'],
+        ['min', 'height'],
+        ['max', 'width'],
+        ['max', 'height'],
+      ].forEach(([first, second]) => {
+        expect(
+          parse(
+            mediaSelectorPlugin,
+            `@media only screen and (${first}-device-${second}: 1200px) { .a { width: 10px; }}`,
+          ),
+        ).toEqual(
+          `@media only screen and (${first}-${second}: 1200px) { .a { width: 10px; }}`,
+        );
+      });
+    });
   });
 
-  it('should throw when a selector is missing', () => {
-    expect(() => {
-      parse('{size: large}');
-    }).toThrow();
+  describe('pseudoClassPlugin', () => {
+    it('parses nested commas in selectors correctly', () => {
+      const cssText =
+        'body > ul :is(li:not(:first-of-type) a.current, li:not(:first-of-type).active a) {background: red;}';
+      expect(parse(pseudoClassPlugin, cssText)).toEqual(cssText);
+    });
 
-    expect(() => {
-      parse('b { color: red; }\n{ color: green; }\na { color: blue; }');
-    }).toThrow();
-  });
+    it("doesn't ignore :hover within :is brackets", () => {
+      const cssText =
+        'body > ul :is(li:not(:first-of-type) a:hover, li:not(:first-of-type).active a) {background: red;}';
+      expect(parse(pseudoClassPlugin, cssText))
+        .toEqual(`body > ul :is(li:not(:first-of-type) a:hover, li:not(:first-of-type).active a),
+body > ul :is(li:not(:first-of-type) a.\\:hover, li:not(:first-of-type).active a) {background: red;}`);
+    });
 
-  it('should throw when a broken comment is found', () => {
-    expect(() => {
-      parse('thing { color: red; } /* b { color: blue; }');
-    }).toThrow();
+    it('should parse selector with comma nested inside ()', () => {
+      const cssText =
+        '[_nghost-ng-c4172599085]:not(.fit-content).aim-select:hover:not(:disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--invalid, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--active) { border-color: rgb(84, 84, 84); }';
+      expect(parse(pseudoClassPlugin, cssText))
+        .toEqual(`[_nghost-ng-c4172599085]:not(.fit-content).aim-select:hover:not(:disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--invalid, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--active),
+[_nghost-ng-c4172599085]:not(.fit-content).aim-select.\\:hover:not(:disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--invalid, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--active) { border-color: rgb(84, 84, 84); }`);
+    });
 
-    expect(() => {
-      parse('/*');
-    }).toThrow();
+    it('ignores ( in strings', () => {
+      const cssText =
+        'li[attr="weirdly("] a:hover, li[attr="weirdly)"] a {background-color: red;}';
+      expect(parse(pseudoClassPlugin, cssText))
+        .toEqual(`li[attr="weirdly("] a:hover, li[attr="weirdly)"] a,
+li[attr="weirdly("] a.\\:hover {background-color: red;}`);
+    });
 
-    /* Nested comments should be fine */
-    expect(() => {
-      parse('/* /* */');
-    }).not.toThrow();
-  });
+    it('ignores escaping in strings', () => {
+      const cssText = `li[attr="weirder\\"("] a:hover, li[attr="weirder\\")"] a {background-color: red;}`;
+      expect(parse(pseudoClassPlugin, cssText))
+        .toEqual(`li[attr="weirder\\"("] a:hover, li[attr="weirder\\")"] a,
+li[attr="weirder\\"("] a.\\:hover {background-color: red;}`);
+    });
 
-  it('should allow empty property value', () => {
-    expect(() => {
-      parse('p { color:; }');
-    }).not.toThrow();
+    it('ignores comma in string', () => {
+      const cssText = 'li[attr="has,comma"] a:hover {background: red;}';
+      expect(parse(pseudoClassPlugin, cssText)).toEqual(
+        `li[attr="has,comma"] a:hover,
+li[attr="has,comma"] a.\\:hover {background: red;}`,
+      );
+    });
   });
+});
 
-  it('should not throw with silent option', () => {
-    expect(() => {
-      parse('thing { color: red; } /* b { color: blue; }', { silent: true });
-    }).not.toThrow();
+describe('css splitter', () => {
+  it('finds css textElement splits correctly', () => {
+    const window = new Window({ url: 'https://localhost:8080' });
+    const document = window.document;
+    document.head.innerHTML = '<style>.a{background-color:red;}</style>';
+    const style = document.querySelector('style');
+    if (style) {
+      // as authored, e.g. no spaces
+      style.append('.a{background-color:black;}');
+
+      // how it is currently stringified (spaces present)
+      const expected = [
+        '.a { background-color: red; }',
+        '.a { background-color: black; }',
+      ];
+      const browserSheet = expected.join('');
+      expect(stringifyStylesheet(style.sheet!)).toEqual(browserSheet);
+
+      expect(splitCssText(browserSheet, style)).toEqual(expected);
+    }
   });
 
-  it('should list the parsing errors and continue parsing', () => {
-    const result = parse(
-      'foo { color= red; } bar { color: blue; } baz {}} boo { display: none}',
-      {
-        silent: true,
-        source: 'foo.css',
-      },
-    );
-
-    const rules = result.stylesheet!.rules;
-    expect(rules.length).toBeGreaterThan(2);
-
-    const errors = result.stylesheet!.parsingErrors!;
-    expect(errors.length).toEqual(2);
-
-    expect(errors[0]).toHaveProperty('message');
-    expect(errors[0]).toHaveProperty('reason');
-    expect(errors[0]).toHaveProperty('filename');
-    expect(errors[0]).toHaveProperty('line');
-    expect(errors[0]).toHaveProperty('column');
-    expect(errors[0]).toHaveProperty('source');
-    expect(errors[0].filename).toEqual('foo.css');
+  it('finds css textElement splits correctly when comments are present', () => {
+    const window = new Window({ url: 'https://localhost:8080' });
+    const document = window.document;
+    // as authored, with comment, missing semicolons
+    document.head.innerHTML =
+      '<style>.a{color:red}.b{color:blue} /* author comment */</style>';
+    const style = document.querySelector('style');
+    if (style) {
+      style.append('/* author comment */.a{color:red}.b{color:green}');
+
+      // how it is currently stringified (spaces present)
+      const expected = [
+        '.a { color: red; } .b { color: blue; }',
+        '.a { color: red; } .b { color: green; }',
+      ];
+      const browserSheet = expected.join('');
+      expect(splitCssText(browserSheet, style)).toEqual(expected);
+    }
   });
 
-  it('should parse selector with comma nested inside ()', () => {
-    const result = parse(
-      '[_nghost-ng-c4172599085]:not(.fit-content).aim-select:hover:not(:disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--invalid, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--active) { border-color: rgb(84, 84, 84); }',
-    );
-
-    expect(result.parent).toEqual(null);
-
-    const rules = result.stylesheet!.rules;
-    expect(rules.length).toEqual(1);
-
-    let rule = rules[0] as Rule;
-    expect(rule.parent).toEqual(result);
-    expect(rule.selectors?.length).toEqual(1);
-
-    let decl = rule.declarations![0];
-    expect(decl.parent).toEqual(rule);
+  it('finds css textElement splits correctly when vendor prefixed rules have been removed', () => {
+    const style = JSDOM.fragment(`<style></style>`).querySelector('style');
+    if (style) {
+      // as authored, with newlines
+      style.appendChild(
+        JSDOM.fragment(`.x {
+  -webkit-transition: all 4s ease;
+  content: 'try to keep a newline';
+  transition: all 4s ease;
+}`),
+      );
+      // TODO: splitCssText can't handle it yet if both start with .x
+      style.appendChild(
+        JSDOM.fragment(`.y {
+  -moz-transition: all 5s ease;
+  transition: all 5s ease;
+}`),
+      );
+      // browser .rules would usually omit the vendored versions and modifies the transition value
+      const expected = [
+        '.x { content: "try to keep a newline"; background: red; transition: 4s; }',
+        '.y { transition: 5s; }',
+      ];
+      const browserSheet = expected.join('');
+
+      // can't do this as JSDOM doesn't have style.sheet
+      // also happy-dom doesn't strip out vendor-prefixed rules like a real browser does
+      //expect(stringifyStylesheet(style.sheet!)).toEqual(browserSheet);
+
+      expect(splitCssText(browserSheet, style)).toEqual(expected);
+    }
   });
+});
 
-  it('parses { and } in attribute selectors correctly', () => {
-    const result = parse('foo[someAttr~="{someId}"] { color: red; }');
-    const rules = result.stylesheet!.rules;
-
-    expect(rules.length).toEqual(1);
-
-    const rule = rules[0] as Rule;
-
-    expect(rule.selectors![0]).toEqual('foo[someAttr~="{someId}"]');
+describe('applyCssSplits css rejoiner', function () {
+  const mockLastUnusedArg = null as unknown as BuildCache;
+  const halfCssText = '.a { background-color: red; }';
+  const otherHalfCssText = halfCssText.replace('.a', '.x');
+  const markedCssText = [halfCssText, otherHalfCssText].join('/* rr_split */');
+  let sn: serializedElementNodeWithId;
+
+  beforeEach(() => {
+    sn = {
+      type: NodeType.Element,
+      tagName: 'style',
+      childNodes: [
+        {
+          type: NodeType.Text,
+          textContent: '',
+        },
+        {
+          type: NodeType.Text,
+          textContent: '',
+        },
+      ],
+    } as serializedElementNodeWithId;
   });
 
-  it('should set parent property', () => {
-    const result = parse(
-      'thing { test: value; }\n' +
-        '@media (min-width: 100px) { thing { test: value; } }',
+  it('applies css splits correctly', () => {
+    // happy path
+    applyCssSplits(sn, markedCssText, false, mockLastUnusedArg);
+    expect((sn.childNodes[0] as textNode).textContent).toEqual(halfCssText);
+    expect((sn.childNodes[1] as textNode).textContent).toEqual(
+      otherHalfCssText,
     );
-
-    expect(result.parent).toEqual(null);
-
-    const rules = result.stylesheet!.rules;
-    expect(rules.length).toEqual(2);
-
-    let rule = rules[0] as Rule;
-    expect(rule.parent).toEqual(result);
-    expect(rule.declarations!.length).toEqual(1);
-
-    let decl = rule.declarations![0];
-    expect(decl.parent).toEqual(rule);
-
-    const media = rules[1] as Media;
-    expect(media.parent).toEqual(result);
-    expect(media.rules!.length).toEqual(1);
-
-    rule = media.rules![0] as Rule;
-    expect(rule.parent).toEqual(media);
-
-    expect(rule.declarations!.length).toEqual(1);
-    decl = rule.declarations![0];
-    expect(decl.parent).toEqual(rule);
-  });
-
-  it('parses : in attribute selectors correctly', () => {
-    const out1 = fixSafariColons('[data-foo] { color: red; }');
-    expect(out1).toEqual('[data-foo] { color: red; }');
-
-    const out2 = fixSafariColons('[data-foo:other] { color: red; }');
-    expect(out2).toEqual('[data-foo\\:other] { color: red; }');
-
-    const out3 = fixSafariColons('[data-aa\\:other] { color: red; }');
-    expect(out3).toEqual('[data-aa\\:other] { color: red; }');
   });
 
-  it('parses nested commas in selectors correctly', () => {
-    const result = parse(
-      `
-body > ul :is(li:not(:first-of-type) a:hover, li:not(:first-of-type).active a) {
-  background: red;
-}
-`,
+  it('applies css splits correctly even when there are too many child nodes', () => {
+    let sn3 = {
+      type: NodeType.Element,
+      tagName: 'style',
+      childNodes: [
+        {
+          type: NodeType.Text,
+          textContent: '',
+        },
+        {
+          type: NodeType.Text,
+          textContent: '',
+        },
+        {
+          type: NodeType.Text,
+          textContent: '',
+        },
+      ],
+    } as serializedElementNodeWithId;
+    applyCssSplits(sn3, markedCssText, false, mockLastUnusedArg);
+    expect((sn3.childNodes[0] as textNode).textContent).toEqual(halfCssText);
+    expect((sn3.childNodes[1] as textNode).textContent).toEqual(
+      otherHalfCssText,
     );
-    expect((result.stylesheet!.rules[0] as Rule)!.selectors!.length).toEqual(1);
-
-    const trickresult = parse(
-      `
-li[attr="weirdly("] a:hover, li[attr="weirdly)"] a {
-  background-color: red;
-}
-`,
-    );
-    expect(
-      (trickresult.stylesheet!.rules[0] as Rule)!.selectors!.length,
-    ).toEqual(2);
-
-    const weirderresult = parse(
-      `
-li[attr="weirder\\"("] a:hover, li[attr="weirder\\")"] a {
-  background-color: red;
-}
-`,
-    );
-    expect(
-      (weirderresult.stylesheet!.rules[0] as Rule)!.selectors!.length,
-    ).toEqual(2);
-
-    const commainstrresult = parse(
-      `
-li[attr="has,comma"] a:hover {
-  background-color: red;
-}
-`,
-    );
-    expect(
-      (commainstrresult.stylesheet!.rules[0] as Rule)!.selectors!.length,
-    ).toEqual(1);
+    expect((sn3.childNodes[2] as textNode).textContent).toEqual('');
   });
 
-  it('parses imports with quotes correctly', () => {
-    const out1 = escapeImportStatement({
-      cssText: `@import url("/foo.css;900;800"");`,
-      href: '/foo.css;900;800"',
-      media: {
-        length: 0,
-      },
-      layerName: null,
-      supportsText: null,
-    } as unknown as CSSImportRule);
-    expect(out1).toEqual(`@import url("/foo.css;900;800\\"");`);
-
-    const out2 = escapeImportStatement({
-      cssText: `@import url("/foo.css;900;800"") supports(display: flex);`,
-      href: '/foo.css;900;800"',
-      media: {
-        length: 0,
-      },
-      layerName: null,
-      supportsText: 'display: flex',
-    } as unknown as CSSImportRule);
-    expect(out2).toEqual(
-      `@import url("/foo.css;900;800\\"") supports(display: flex);`,
+  it('maintains entire css text when there are too few child nodes', () => {
+    let sn1 = {
+      type: NodeType.Element,
+      tagName: 'style',
+      childNodes: [
+        {
+          type: NodeType.Text,
+          textContent: '',
+        },
+      ],
+    } as serializedElementNodeWithId;
+    applyCssSplits(sn1, markedCssText, false, mockLastUnusedArg);
+    expect((sn1.childNodes[0] as textNode).textContent).toEqual(
+      halfCssText + otherHalfCssText,
     );
-
-    const out3 = escapeImportStatement({
-      cssText: `@import url("/foo.css;900;800"");`,
-      href: '/foo.css;900;800"',
-      media: {
-        length: 1,
-        mediaText: 'print, screen',
-      },
-      layerName: null,
-      supportsText: null,
-    } as unknown as CSSImportRule);
-    expect(out3).toEqual(`@import url("/foo.css;900;800\\"") print, screen;`);
-
-    const out4 = escapeImportStatement({
-      cssText: `@import url("/foo.css;900;800"") layer(layer-1);`,
-      href: '/foo.css;900;800"',
-      media: {
-        length: 0,
-      },
-      layerName: 'layer-1',
-      supportsText: null,
-    } as unknown as CSSImportRule);
-    expect(out4).toEqual(`@import url("/foo.css;900;800\\"") layer(layer-1);`);
-
-    const out5 = escapeImportStatement({
-      cssText: `@import url("/foo.css;900;800"") layer;`,
-      href: '/foo.css;900;800"',
-      media: {
-        length: 0,
-      },
-      layerName: '',
-      supportsText: null,
-    } as unknown as CSSImportRule);
-    expect(out5).toEqual(`@import url("/foo.css;900;800\\"") layer;`);
   });
 });
diff --git a/packages/rrweb-snapshot/test/css/style-with-import.css b/packages/rrweb-snapshot/test/css/style-with-import.css
index 5fa59d8039..a24d901947 100644
--- a/packages/rrweb-snapshot/test/css/style-with-import.css
+++ b/packages/rrweb-snapshot/test/css/style-with-import.css
@@ -1,2 +1,3 @@
 @import '//fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&family=Roboto:wght@100;300;400;500;700&display=swap"';
 @import './style.css';
+@import '../alt-css/alt-style.css';
diff --git a/packages/rrweb-snapshot/test/css/style.css b/packages/rrweb-snapshot/test/css/style.css
index 2b3faf2a77..29b1da8ec8 100644
--- a/packages/rrweb-snapshot/test/css/style.css
+++ b/packages/rrweb-snapshot/test/css/style.css
@@ -1,11 +1,11 @@
 body {
   margin: 0;
-  background: url('../a.jpg');
+  background: url('../should-be-in-root-folder.jpg');
   border-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 256 256"><g><g><polygon points="79.093,0 48.907,30.187 146.72,128 48.907,225.813 79.093,256 207.093,128"/></g></g></svg>');
 }
 p {
   color: red;
-  background: url('./b.jpg');
+  background: url('./should-be-in-css-folder.jpg');
 }
 body > p {
   color: yellow;
diff --git a/packages/rrweb-snapshot/test/html/dialog.html b/packages/rrweb-snapshot/test/html/dialog.html
new file mode 100644
index 0000000000..2380b8fade
--- /dev/null
+++ b/packages/rrweb-snapshot/test/html/dialog.html
@@ -0,0 +1,5 @@
+<html>
+  <body>
+    <dialog>I'm a dialog</dialog>
+  </body>
+</html>
diff --git a/packages/rrweb-snapshot/test/html/monkey-patched-elements.html b/packages/rrweb-snapshot/test/html/monkey-patched-elements.html
new file mode 100644
index 0000000000..a48b8fd328
--- /dev/null
+++ b/packages/rrweb-snapshot/test/html/monkey-patched-elements.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <script>
+      // monkey patches .childNodes to make them unaccessible
+      // emulates the effect of some frameworks, specifically salesforce lightning-ui
+      Object.defineProperty(Element.prototype, 'childNodes', {
+        get() {
+          throw new Error('childNodes was hijacked by framework');
+        },
+      });
+
+      Object.defineProperty(Node.prototype, 'childNodes', {
+        get() {
+          throw new Error('childNodes was hijacked by framework');
+        },
+      });
+
+      Node.prototype.contains = function () {
+        throw new Error('contains was hijacked by framework');
+      };
+
+      Node.prototype.getRootNode = function () {
+        throw new Error('getRootNode was hijacked by framework');
+      };
+
+      MutationObserver = class {
+        constructor() {
+          throw new Error('MutationObserver was hijacked by framework');
+        }
+      };
+    </script>
+  </head>
+  <body>
+    <ul>
+      <li>a</li>
+      <li>b</li>
+      <li>c</li>
+      <li>d</li>
+    </ul>
+  </body>
+</html>
diff --git a/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html b/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html
index 6b45f65bc5..d98ff7b9fa 100644
--- a/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html
+++ b/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html
@@ -7,6 +7,10 @@
   <meta http-equiv="X-UA-Compatible" content="ie=edge">
   <title>with style sheet with import</title>
   <link rel="stylesheet" href="/css/style-with-import.css">
+  <style>
+    @import '../alt-css/alt-style.css';
+    section { background: url('./should-be-in-root-folder.jpg'); }
+  </style>
 </head>
 
 <body>
diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts
index 8f4476ecb9..9fa04baf65 100644
--- a/packages/rrweb-snapshot/test/integration.test.ts
+++ b/packages/rrweb-snapshot/test/integration.test.ts
@@ -1,10 +1,20 @@
 import * as fs from 'fs';
-import * as path from 'path';
 import * as http from 'http';
-import * as url from 'url';
+import * as path from 'path';
 import * as puppeteer from 'puppeteer';
-import { vi, assert, describe, it, beforeAll, afterAll, expect } from 'vitest';
-import { waitForRAF, getServerURL } from './utils';
+import * as url from 'url';
+import {
+  afterAll,
+  assert,
+  beforeAll,
+  beforeEach,
+  describe,
+  expect,
+  it,
+  vi,
+} from 'vitest';
+
+import { getServerURL, waitForRAF } from './utils';
 
 const htmlFolder = path.join(__dirname, 'html');
 const htmls = fs.readdirSync(htmlFolder).map((filePath) => {
@@ -60,6 +70,15 @@ function sanitizeSnapshot(snapshot: string): string {
   return snapshot.replace(/localhost:[0-9]+/g, 'localhost:3030');
 }
 
+async function snapshot(page: puppeteer.Page, code: string): Promise<string> {
+  await waitForRAF(page);
+  const result = (await page.evaluate(`${code}
+    const snapshot = rrwebSnapshot.snapshot(document);
+    JSON.stringify(snapshot, null, 2);
+  `)) as string;
+  return result;
+}
+
 function assertSnapshot(snapshot: string): void {
   expect(sanitizeSnapshot(snapshot)).toMatchSnapshot();
 }
@@ -68,6 +87,7 @@ interface ISuite {
   server: http.Server;
   serverURL: string;
   browser: puppeteer.Browser;
+  page: puppeteer.Page;
   code: string;
 }
 
@@ -100,6 +120,9 @@ describe('integration tests', function (this: ISuite) {
     if (html.filePath.substring(html.filePath.length - 1) === '~') {
       continue;
     }
+    // monkey patching breaks rebuild code
+    if (html.filePath.includes('monkey-patched-elements.html')) continue;
+
     const title = '[html file]: ' + html.filePath;
     it(title, async () => {
       const page: puppeteer.Page = await browser.newPage();
@@ -153,7 +176,7 @@ describe('integration tests', function (this: ISuite) {
           'blob:http://localhost:xxxx/...',
         );
 
-      assertSnapshot(rebuildHtml);
+      await assertSnapshot(rebuildHtml);
     });
   }
 
@@ -235,7 +258,6 @@ iframe.contentDocument.querySelector('center').clientHeight
 
   it('correctly saves cross-origin images offline', async () => {
     const page: puppeteer.Page = await browser.newPage();
-
     await page.goto('about:blank', {
       waitUntil: 'load',
     });
@@ -348,7 +370,7 @@ iframe.contentDocument.querySelector('center').clientHeight
 
   it('should save background-clip: text; as the more compatible -webkit-background-clip: test;', async () => {
     const page: puppeteer.Page = await browser.newPage();
-    await page.goto(`http://localhost:3030/html/background-clip-text.html`, {
+    await page.goto(`${serverURL}/html/background-clip-text.html`, {
       waitUntil: 'load',
     });
     await waitForRAF(page); // wait for page to render
@@ -366,13 +388,10 @@ iframe.contentDocument.querySelector('center').clientHeight
   it('images with inline onload should work', async () => {
     const page: puppeteer.Page = await browser.newPage();
 
-    await page.goto(
-      'http://localhost:3030/html/picture-with-inline-onload.html',
-      {
-        waitUntil: 'load',
-      },
-    );
-    await page.waitForSelector('img', { timeout: 1000 });
+    await page.goto(`${serverURL}/html/picture-with-inline-onload.html`, {
+      waitUntil: 'load',
+    });
+    await page.waitForSelector('img', { timeout: 2000 });
     await page.evaluate(`${code}`);
     await page.evaluate(`
     var snapshot = rrwebSnapshot.snapshot(document, {
@@ -386,6 +405,22 @@ iframe.contentDocument.querySelector('center').clientHeight
     )) as string;
     assert(fnName === 'onload');
   });
+
+  it('should be able to record elements even when .childNodes has been monkey patched', async () => {
+    const page: puppeteer.Page = await browser.newPage();
+    await page.goto(`${serverURL}/html/monkey-patched-elements.html`, {
+      waitUntil: 'load',
+    });
+    await waitForRAF(page); // wait for page to render
+    const snapshotResult = JSON.stringify(
+      await page.evaluate(`${code};
+          rrwebSnapshot.snapshot(document);
+        `),
+      null,
+      2,
+    );
+    expect(snapshotResult).toMatchSnapshot();
+  });
 });
 
 describe('iframe integration tests', function (this: ISuite) {
@@ -427,6 +462,53 @@ describe('iframe integration tests', function (this: ISuite) {
       null,
       2,
     );
+    await assertSnapshot(snapshotResult);
+  });
+});
+
+describe('dialog integration tests', function (this: ISuite) {
+  vi.setConfig({ testTimeout: 30_000 });
+  let server: ISuite['server'];
+  let serverURL: ISuite['serverURL'];
+  let browser: ISuite['browser'];
+  let code: ISuite['code'];
+  let page: ISuite['page'];
+
+  beforeAll(async () => {
+    server = await startServer();
+    serverURL = getServerURL(server);
+    browser = await puppeteer.launch({
+      // headless: false,
+    });
+
+    code = fs.readFileSync(
+      path.resolve(__dirname, '../dist/rrweb-snapshot.umd.cjs'),
+      'utf-8',
+    );
+  });
+
+  beforeEach(async () => {
+    page = await browser.newPage();
+    page.on('console', (msg) => console.log(msg.text()));
+    await page.goto(`${serverURL}/html/dialog.html`, {
+      waitUntil: 'load',
+    });
+  });
+
+  afterAll(async () => {
+    await browser.close();
+    await server.close();
+  });
+
+  it('should capture open attribute for non modal dialogs', async () => {
+    page.evaluate('document.querySelector("dialog").show()');
+    const snapshotResult = await snapshot(page, code);
+    assertSnapshot(snapshotResult);
+  });
+
+  it('should capture open attribute for modal dialogs', async () => {
+    await page.evaluate('document.querySelector("dialog").showModal()');
+    const snapshotResult = await snapshot(page, code);
     assertSnapshot(snapshotResult);
   });
 });
@@ -470,6 +552,6 @@ describe('shadow DOM integration tests', function (this: ISuite) {
       null,
       2,
     );
-    assertSnapshot(snapshotResult);
+    await assertSnapshot(snapshotResult);
   });
 });
diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts
index c71e1e8510..a0994a2f88 100644
--- a/packages/rrweb-snapshot/test/rebuild.test.ts
+++ b/packages/rrweb-snapshot/test/rebuild.test.ts
@@ -3,14 +3,34 @@
  */
 import * as fs from 'fs';
 import * as path from 'path';
-import { describe, it, beforeEach, expect } from 'vitest';
+import { beforeEach, describe, expect as _expect, it } from 'vitest';
 import {
   adaptCssForReplay,
   buildNodeWithSN,
   createCache,
 } from '../src/rebuild';
 import { NodeType } from '../src/types';
-import { createMirror, Mirror } from '../src/utils';
+import { createMirror, Mirror, normalizeCssString } from '../src/utils';
+
+const expect = _expect as unknown as {
+  <T = unknown>(actual: T): {
+    toMatchCss(expected: string): void;
+  } & ReturnType<typeof _expect>;
+} & typeof _expect;
+
+expect.extend({
+  toMatchCss: function (received: string, expected: string) {
+    const pass = normalizeCssString(received) === normalizeCssString(expected);
+    const message: () => string = () =>
+      pass
+        ? ''
+        : `Received (${received}) is not the same as expected (${expected})`;
+    return {
+      message,
+      pass,
+    };
+  },
+});
 
 function getDuration(hrtime: [number, number]) {
   const [seconds, nanoseconds] = hrtime;
@@ -52,6 +72,32 @@ describe('rebuild', function () {
     });
   });
 
+  describe('rr_width/rr_height', function () {
+    it('rebuild blocked element with correct dimensions', function () {
+      const node = buildNodeWithSN(
+        {
+          id: 1,
+          tagName: 'svg',
+          type: NodeType.Element,
+          isSVG: true,
+          attributes: {
+            rr_width: '50px',
+            rr_height: '50px',
+          },
+          childNodes: [],
+        },
+        {
+          doc: document,
+          mirror,
+          hackCss: false,
+          cache,
+        },
+      ) as HTMLDivElement;
+      expect(node.style.width).toBe('50px');
+      expect(node.style.height).toBe('50px');
+    });
+  });
+
   describe('shadowDom', function () {
     it('rebuild shadowRoot without siblings', function () {
       const node = buildNodeWithSN(
@@ -86,19 +132,19 @@ describe('rebuild', function () {
   describe('add hover class to hover selector related rules', function () {
     it('will do nothing to css text without :hover', () => {
       const cssText = 'body { color: white }';
-      expect(adaptCssForReplay(cssText, cache)).toEqual(cssText);
+      expect(adaptCssForReplay(cssText, cache)).toMatchCss(cssText);
     });
 
     it('can add hover class to css text', () => {
       const cssText = '.a:hover { color: white }';
-      expect(adaptCssForReplay(cssText, cache)).toEqual(
+      expect(adaptCssForReplay(cssText, cache)).toMatchCss(
         '.a:hover, .a.\\:hover { color: white }',
       );
     });
 
     it('can correctly add hover when in middle of selector', () => {
       const cssText = 'ul li a:hover img { color: white }';
-      expect(adaptCssForReplay(cssText, cache)).toEqual(
+      expect(adaptCssForReplay(cssText, cache)).toMatchCss(
         'ul li a:hover img, ul li a.\\:hover img { color: white }',
       );
     });
@@ -111,14 +157,15 @@ img,
 ul li.specified c:hover img {
   color: white
 }`;
-      expect(adaptCssForReplay(cssText, cache)).toEqual(
-        `ul li.specified a:hover img, ul li.specified a.\\:hover img,
+      expect(adaptCssForReplay(cssText, cache)).toMatchCss(
+        `ul li.specified a:hover img,
 ul li.multiline
 b:hover
-img, ul li.multiline
-b.\\:hover
 img,
-ul li.specified c:hover img, ul li.specified c.\\:hover img {
+ul li.specified c:hover img,
+ul li.specified a.\\:hover img,
+ul li.multiline b.\\:hover img,
+ul li.specified c.\\:hover img {
   color: white
 }`,
       );
@@ -126,48 +173,48 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img {
 
     it('can add hover class within media query', () => {
       const cssText = '@media screen { .m:hover { color: white } }';
-      expect(adaptCssForReplay(cssText, cache)).toEqual(
+      expect(adaptCssForReplay(cssText, cache)).toMatchCss(
         '@media screen { .m:hover, .m.\\:hover { color: white } }',
       );
     });
 
     it('can add hover class when there is multi selector', () => {
       const cssText = '.a, .b:hover, .c { color: white }';
-      expect(adaptCssForReplay(cssText, cache)).toEqual(
-        '.a, .b:hover, .b.\\:hover, .c { color: white }',
+      expect(adaptCssForReplay(cssText, cache)).toMatchCss(
+        '.a, .b:hover, .c, .b.\\:hover { color: white }',
       );
     });
 
     it('can add hover class when there is a multi selector with the same prefix', () => {
       const cssText = '.a:hover, .a:hover::after { color: white }';
-      expect(adaptCssForReplay(cssText, cache)).toEqual(
-        '.a:hover, .a.\\:hover, .a:hover::after, .a.\\:hover::after { color: white }',
+      expect(adaptCssForReplay(cssText, cache)).toMatchCss(
+        '.a:hover, .a:hover::after, .a.\\:hover, .a.\\:hover::after { color: white }',
       );
     });
 
     it('can add hover class when :hover is not the end of selector', () => {
       const cssText = 'div:hover::after { color: white }';
-      expect(adaptCssForReplay(cssText, cache)).toEqual(
+      expect(adaptCssForReplay(cssText, cache)).toMatchCss(
         'div:hover::after, div.\\:hover::after { color: white }',
       );
     });
 
     it('can add hover class when the selector has multi :hover', () => {
       const cssText = 'a:hover b:hover { color: white }';
-      expect(adaptCssForReplay(cssText, cache)).toEqual(
+      expect(adaptCssForReplay(cssText, cache)).toMatchCss(
         'a:hover b:hover, a.\\:hover b.\\:hover { color: white }',
       );
     });
 
     it('will ignore :hover in css value', () => {
       const cssText = '.a::after { content: ":hover" }';
-      expect(adaptCssForReplay(cssText, cache)).toEqual(cssText);
+      expect(adaptCssForReplay(cssText, cache)).toMatchCss(cssText);
     });
 
     it('can adapt media rules to replay context', () => {
       const cssText =
         '@media only screen and (min-device-width : 1200px) { .a { width: 10px; }}';
-      expect(adaptCssForReplay(cssText, cache)).toEqual(
+      expect(adaptCssForReplay(cssText, cache)).toMatchCss(
         '@media only screen and (min-width : 1200px) { .a { width: 10px; }}',
       );
     });
@@ -210,7 +257,7 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img {
     // 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(
+    expect(adaptCssForReplay(should_not_modify, cache)).toMatchCss(
       should_not_modify,
     );
   });
@@ -219,7 +266,7 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img {
     // 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(
+    expect(adaptCssForReplay(should_not_modify, cache)).toMatchCss(
       should_not_modify,
     );
   });
diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts
index de1d79eb6d..5778eb0aff 100644
--- a/packages/rrweb-snapshot/test/snapshot.test.ts
+++ b/packages/rrweb-snapshot/test/snapshot.test.ts
@@ -2,64 +2,79 @@
  * @vitest-environment jsdom
  */
 import { JSDOM } from 'jsdom';
-import { describe, it, expect } from 'vitest';
-import {
-  absoluteToStylesheet,
-  serializeNodeWithId,
+import { describe, expect, it } from 'vitest';
+
+import snapshot, {
   _isBlockedElement,
+  serializeNodeWithId,
 } from '../src/snapshot';
-import snapshot from '../src/snapshot';
-import { serializedNodeWithId, elementNode } from '../src/types';
-import { Mirror } from '../src/utils';
+import { elementNode, serializedNodeWithId } from '../src/types';
+import { Mirror, absolutifyURLs } from '../src/utils';
+
+const serializeNode = (node: Node): serializedNodeWithId | null => {
+  return serializeNodeWithId(node, {
+    doc: document,
+    mirror: new Mirror(),
+    blockClass: 'blockblock',
+    blockSelector: null,
+    maskTextClass: 'maskmask',
+    maskTextSelector: null,
+    skipChild: false,
+    inlineStylesheet: true,
+    maskTextFn: undefined,
+    maskInputFn: undefined,
+    slimDOMOptions: {},
+  });
+};
 
 describe('absolute url to stylesheet', () => {
   const href = 'http://localhost/css/style.css';
 
   it('can handle relative path', () => {
-    expect(absoluteToStylesheet('url(a.jpg)', href)).toEqual(
+    expect(absolutifyURLs('url(a.jpg)', href)).toEqual(
       `url(http://localhost/css/a.jpg)`,
     );
   });
 
   it('can handle same level path', () => {
-    expect(absoluteToStylesheet('url("./a.jpg")', href)).toEqual(
+    expect(absolutifyURLs('url("./a.jpg")', href)).toEqual(
       `url("http://localhost/css/a.jpg")`,
     );
   });
 
   it('can handle parent level path', () => {
-    expect(absoluteToStylesheet('url("../a.jpg")', href)).toEqual(
+    expect(absolutifyURLs('url("../a.jpg")', href)).toEqual(
       `url("http://localhost/a.jpg")`,
     );
   });
 
   it('can handle absolute path', () => {
-    expect(absoluteToStylesheet('url("/a.jpg")', href)).toEqual(
+    expect(absolutifyURLs('url("/a.jpg")', href)).toEqual(
       `url("http://localhost/a.jpg")`,
     );
   });
 
   it('can handle external path', () => {
-    expect(absoluteToStylesheet('url("http://localhost/a.jpg")', href)).toEqual(
+    expect(absolutifyURLs('url("http://localhost/a.jpg")', href)).toEqual(
       `url("http://localhost/a.jpg")`,
     );
   });
 
   it('can handle single quote path', () => {
-    expect(absoluteToStylesheet(`url('./a.jpg')`, href)).toEqual(
+    expect(absolutifyURLs(`url('./a.jpg')`, href)).toEqual(
       `url('http://localhost/css/a.jpg')`,
     );
   });
 
   it('can handle no quote path', () => {
-    expect(absoluteToStylesheet('url(./a.jpg)', href)).toEqual(
+    expect(absolutifyURLs('url(./a.jpg)', href)).toEqual(
       `url(http://localhost/css/a.jpg)`,
     );
   });
 
   it('can handle multiple no quote paths', () => {
     expect(
-      absoluteToStylesheet(
+      absolutifyURLs(
         'background-image: url(images/b.jpg);background: #aabbcc url(images/a.jpg) 50% 50% repeat;',
         href,
       ),
@@ -70,11 +85,11 @@ describe('absolute url to stylesheet', () => {
   });
 
   it('can handle data url image', () => {
+    expect(absolutifyURLs('url(data:image/gif;base64,ABC)', href)).toEqual(
+      'url(data:image/gif;base64,ABC)',
+    );
     expect(
-      absoluteToStylesheet('url(data:image/gif;base64,ABC)', href),
-    ).toEqual('url(data:image/gif;base64,ABC)');
-    expect(
-      absoluteToStylesheet(
+      absolutifyURLs(
         'url(data:application/font-woff;base64,d09GMgABAAAAAAm)',
         href,
       ),
@@ -83,7 +98,7 @@ describe('absolute url to stylesheet', () => {
 
   it('preserves quotes around inline svgs with spaces', () => {
     expect(
-      absoluteToStylesheet(
+      absolutifyURLs(
         "url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M3'/%3E%3C/svg%3E\")",
         href,
       ),
@@ -91,7 +106,7 @@ describe('absolute url to stylesheet', () => {
       "url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M3'/%3E%3C/svg%3E\")",
     );
     expect(
-      absoluteToStylesheet(
+      absolutifyURLs(
         'url(\'data:image/svg+xml;utf8,<svg width="28" height="32" viewBox="0 0 28 32" xmlns="http://www.w3.org/2000/svg"><path d="M27 14C28" fill="white"/></svg>\')',
         href,
       ),
@@ -99,7 +114,7 @@ describe('absolute url to stylesheet', () => {
       'url(\'data:image/svg+xml;utf8,<svg width="28" height="32" viewBox="0 0 28 32" xmlns="http://www.w3.org/2000/svg"><path d="M27 14C28" fill="white"/></svg>\')',
     );
     expect(
-      absoluteToStylesheet(
+      absolutifyURLs(
         'url("data:image/svg+xml;utf8,<svg width="28" height="32" viewBox="0 0 28 32" xmlns="http://www.w3.org/2000/svg"><path d="M27 14C28" fill="white"/></svg>")',
         href,
       ),
@@ -108,7 +123,7 @@ describe('absolute url to stylesheet', () => {
     );
   });
   it('can handle empty path', () => {
-    expect(absoluteToStylesheet(`url('')`, href)).toEqual(`url('')`);
+    expect(absolutifyURLs(`url('')`, href)).toEqual(`url('')`);
   });
 });
 
@@ -139,22 +154,6 @@ describe('isBlockedElement()', () => {
 });
 
 describe('style elements', () => {
-  const serializeNode = (node: Node): serializedNodeWithId | null => {
-    return serializeNodeWithId(node, {
-      doc: document,
-      mirror: new Mirror(),
-      blockClass: 'blockblock',
-      blockSelector: null,
-      maskTextClass: 'maskmask',
-      maskTextSelector: null,
-      skipChild: false,
-      inlineStylesheet: true,
-      maskTextFn: undefined,
-      maskInputFn: undefined,
-      slimDOMOptions: {},
-    });
-  };
-
   const render = (html: string): HTMLStyleElement => {
     document.write(html);
     return document.querySelector('style')!;
@@ -163,44 +162,32 @@ describe('style elements', () => {
   it('should serialize all rules of stylesheet when the sheet has a single child node', () => {
     const styleEl = render(`<style>body { color: red; }</style>`);
     styleEl.sheet?.insertRule('section { color: blue; }');
-    expect(serializeNode(styleEl.childNodes[0])).toMatchObject({
-      isStyle: true,
+    expect(serializeNode(styleEl)).toMatchObject({
       rootId: undefined,
-      textContent: 'section {color: blue;}body {color: red;}',
-      type: 3,
+      attributes: {
+        _cssText: 'section {color: blue;}body {color: red;}',
+      },
+      type: 2,
     });
   });
 
-  it('should serialize individual text nodes on stylesheets with multiple child nodes', () => {
+  it('should serialize all rules on stylesheets with mix of insertion type', () => {
     const styleEl = render(`<style>body { color: red; }</style>`);
+    styleEl.sheet?.insertRule('section.lost { color: unseeable; }'); // browser throws this away after append
     styleEl.append(document.createTextNode('section { color: blue; }'));
-    expect(serializeNode(styleEl.childNodes[1])).toMatchObject({
-      isStyle: true,
+    styleEl.sheet?.insertRule('section.working { color: pink; }');
+    expect(serializeNode(styleEl)).toMatchObject({
       rootId: undefined,
-      textContent: 'section { color: blue; }',
-      type: 3,
+      attributes: {
+        _cssText:
+          'section.working {color: pink;}body {color: red;}/* rr_split */section {color: blue;}',
+      },
+      type: 2,
     });
   });
 });
 
 describe('scrollTop/scrollLeft', () => {
-  const serializeNode = (node: Node): serializedNodeWithId | null => {
-    return serializeNodeWithId(node, {
-      doc: document,
-      mirror: new Mirror(),
-      blockClass: 'blockblock',
-      blockSelector: null,
-      maskTextClass: 'maskmask',
-      maskTextSelector: null,
-      skipChild: false,
-      inlineStylesheet: true,
-      maskTextFn: undefined,
-      maskInputFn: undefined,
-      slimDOMOptions: {},
-      newlyAddedElement: false,
-    });
-  };
-
   const render = (html: string): HTMLDivElement => {
     document.write(html);
     return document.querySelector('div')!;
@@ -222,23 +209,6 @@ describe('scrollTop/scrollLeft', () => {
 });
 
 describe('form', () => {
-  const serializeNode = (node: Node): serializedNodeWithId | null => {
-    return serializeNodeWithId(node, {
-      doc: document,
-      mirror: new Mirror(),
-      blockClass: 'blockblock',
-      blockSelector: null,
-      maskTextClass: 'maskmask',
-      maskTextSelector: null,
-      skipChild: false,
-      inlineStylesheet: true,
-      maskTextFn: undefined,
-      maskInputFn: undefined,
-      slimDOMOptions: {},
-      newlyAddedElement: false,
-    });
-  };
-
   const render = (html: string): HTMLTextAreaElement => {
     document.write(html);
     return document.querySelector('textarea')!;
diff --git a/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts b/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts
new file mode 100644
index 0000000000..1e42bab1a6
--- /dev/null
+++ b/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts
@@ -0,0 +1,37 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { bench } from 'vitest';
+import * as fs from 'fs';
+import * as path from 'path';
+import { stringifyStylesheet } from '../src/utils';
+import * as CSSOM from 'cssom';
+
+describe('stringifyStylesheet', () => {
+  let benchmarkStylesheet: CSSStyleSheet;
+
+  const cssText = fs.readFileSync(
+    path.resolve(__dirname, './css/benchmark.css'),
+    'utf8',
+  );
+  benchmarkStylesheet = CSSOM.parse(cssText);
+  benchmarkStylesheet.href = 'https://example.com/style.css';
+
+  it.skip('stringify', () => {
+    // written just to ensure it's working
+    const cssText = '.x { background: url(./relative.jpg) }';
+    const styleSheet = CSSOM.parse(cssText);
+    styleSheet.href = 'https://example.com/style.css';
+    expect(stringifyStylesheet(styleSheet)).toEqual(
+      'x {background: url(https://example.com/relative.jpg);}',
+    );
+  });
+
+  bench(
+    'stringify',
+    () => {
+      stringifyStylesheet(benchmarkStylesheet);
+    },
+    { time: 1000 },
+  );
+});
diff --git a/packages/rrweb-snapshot/test/utils.test.ts b/packages/rrweb-snapshot/test/utils.test.ts
index 2b09602ae4..885f4ec385 100644
--- a/packages/rrweb-snapshot/test/utils.test.ts
+++ b/packages/rrweb-snapshot/test/utils.test.ts
@@ -3,7 +3,12 @@
  */
 import { describe, it, test, expect } from 'vitest';
 import { NodeType, serializedNode } from '../src/types';
-import { extractFileExtension, isNodeMetaEqual } from '../src/utils';
+import {
+  escapeImportStatement,
+  extractFileExtension,
+  fixSafariColons,
+  isNodeMetaEqual,
+} from '../src/utils';
 import type { serializedNodeWithId } from '@saola.ai/rrweb-snapshot';
 
 describe('utils', () => {
@@ -199,4 +204,80 @@ describe('utils', () => {
       expect(extension).toBe('js');
     });
   });
+
+  describe('escapeImportStatement', () => {
+    it('parses imports with quotes correctly', () => {
+      const out1 = escapeImportStatement({
+        cssText: `@import url("/foo.css;900;800"");`,
+        href: '/foo.css;900;800"',
+        media: {
+          length: 0,
+        },
+        layerName: null,
+        supportsText: null,
+      } as unknown as CSSImportRule);
+      expect(out1).toEqual(`@import url("/foo.css;900;800\\"");`);
+
+      const out2 = escapeImportStatement({
+        cssText: `@import url("/foo.css;900;800"") supports(display: flex);`,
+        href: '/foo.css;900;800"',
+        media: {
+          length: 0,
+        },
+        layerName: null,
+        supportsText: 'display: flex',
+      } as unknown as CSSImportRule);
+      expect(out2).toEqual(
+        `@import url("/foo.css;900;800\\"") supports(display: flex);`,
+      );
+
+      const out3 = escapeImportStatement({
+        cssText: `@import url("/foo.css;900;800"");`,
+        href: '/foo.css;900;800"',
+        media: {
+          length: 1,
+          mediaText: 'print, screen',
+        },
+        layerName: null,
+        supportsText: null,
+      } as unknown as CSSImportRule);
+      expect(out3).toEqual(`@import url("/foo.css;900;800\\"") print, screen;`);
+
+      const out4 = escapeImportStatement({
+        cssText: `@import url("/foo.css;900;800"") layer(layer-1);`,
+        href: '/foo.css;900;800"',
+        media: {
+          length: 0,
+        },
+        layerName: 'layer-1',
+        supportsText: null,
+      } as unknown as CSSImportRule);
+      expect(out4).toEqual(
+        `@import url("/foo.css;900;800\\"") layer(layer-1);`,
+      );
+
+      const out5 = escapeImportStatement({
+        cssText: `@import url("/foo.css;900;800"") layer;`,
+        href: '/foo.css;900;800"',
+        media: {
+          length: 0,
+        },
+        layerName: '',
+        supportsText: null,
+      } as unknown as CSSImportRule);
+      expect(out5).toEqual(`@import url("/foo.css;900;800\\"") layer;`);
+    });
+  });
+  describe('fixSafariColons', () => {
+    it('parses : in attribute selectors correctly', () => {
+      const out1 = fixSafariColons('[data-foo] { color: red; }');
+      expect(out1).toEqual('[data-foo] { color: red; }');
+
+      const out2 = fixSafariColons('[data-foo:other] { color: red; }');
+      expect(out2).toEqual('[data-foo\\:other] { color: red; }');
+
+      const out3 = fixSafariColons('[data-aa\\:other] { color: red; }');
+      expect(out3).toEqual('[data-aa\\:other] { color: red; }');
+    });
+  });
 });
diff --git a/packages/rrweb-snapshot/tsconfig.json b/packages/rrweb-snapshot/tsconfig.json
index 82d5cc086b..cd2eb36538 100644
--- a/packages/rrweb-snapshot/tsconfig.json
+++ b/packages/rrweb-snapshot/tsconfig.json
@@ -1,7 +1,13 @@
 {
   "extends": "../../tsconfig.base.json",
-  "include": ["src"],
-  "exclude": ["vite.config.ts", "vitest.config.ts", "test"],
+  "include": [
+    "src"
+  ],
+  "exclude": [
+    "vite.config.ts",
+    "vitest.config.ts",
+    "test"
+  ],
   "compilerOptions": {
     "rootDir": "src",
     "tsBuildInfoFile": "./tsconfig.tsbuildinfo"
diff --git a/packages/rrweb-snapshot/vitest.config.ts b/packages/rrweb-snapshot/vitest.config.ts
index 39888437cf..1b5a8b7e3e 100644
--- a/packages/rrweb-snapshot/vitest.config.ts
+++ b/packages/rrweb-snapshot/vitest.config.ts
@@ -6,7 +6,7 @@ export default mergeConfig(
   configShared,
   defineProject({
     test: {
-      // ... custom test config here
+      globals: true,
     },
   }),
 );
diff --git a/packages/rrweb/CHANGELOG.md b/packages/rrweb/CHANGELOG.md
index 8686ccbba0..69641c0e3d 100644
--- a/packages/rrweb/CHANGELOG.md
+++ b/packages/rrweb/CHANGELOG.md
@@ -1,5 +1,52 @@
 # rrweb
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb-snapshot@2.0.13
+  - @saola.ai/rrdom@2.0.13
+  - @saola.ai/rrweb-types@2.0.13
+  - @saola.ai/rrweb-utils@2.0.13
+
+## 2.0.0-alpha.17
+
+### Minor Changes
+
+- [#1503](https://github.com/rrweb-io/rrweb/pull/1503) [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158) Thanks [@Juice10](https://github.com/Juice10)! - Support top-layer <dialog> components. Fixes #1381.
+
+### Patch Changes
+
+- [#1417](https://github.com/rrweb-io/rrweb/pull/1417) [`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - fix: duplicate textContent for style elements cause incremental style mutations to be invalid
+
+- [#1527](https://github.com/rrweb-io/rrweb/pull/1527) [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c) Thanks [@arredgroup](https://github.com/arredgroup)! - Export takeFullSnapshot function for a recording process
+
+- [#1515](https://github.com/rrweb-io/rrweb/pull/1515) [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6) Thanks [@okejminja](https://github.com/okejminja)! - Added support for deprecated addRule & removeRule methods
+
+- [#1509](https://github.com/rrweb-io/rrweb/pull/1509) [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414) Thanks [@Juice10](https://github.com/Juice10)! - Reverse monkey patch built in methods to support LWC (and other frameworks like angular which monkey patch built in methods).
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`d350da8`](https://github.com/rrweb-io/rrweb/commit/d350da8552d8616dd118ee550bdfbce082986562), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb-snapshot@2.0.0-alpha.17
+  - rrdom@2.0.0-alpha.17
+  - @rrweb/types@2.0.0-alpha.17
+  - @rrweb/utils@2.0.0-alpha.17
+
+## 2.0.0-alpha.16
+
+### Patch Changes
+
+- [#1386](https://github.com/rrweb-io/rrweb/pull/1386) [`a2c8a1a`](https://github.com/rrweb-io/rrweb/commit/a2c8a1a37bfcf8389b280af792262c8263a979a3) Thanks [@ababik](https://github.com/ababik)! - Fix that the optional `maskInputFn` was being accidentally ignored during the creation of the full snapshot
+
+- [#1512](https://github.com/rrweb-io/rrweb/pull/1512) [`d08624c`](https://github.com/rrweb-io/rrweb/commit/d08624cb28add386c3618a0e6607424c3f1884d8) Thanks [@eoghanmurray](https://github.com/eoghanmurray)! - optimisation: skip mask check on leaf elements
+
+- Updated dependencies [[`a2c8a1a`](https://github.com/rrweb-io/rrweb/commit/a2c8a1a37bfcf8389b280af792262c8263a979a3), [`d08624c`](https://github.com/rrweb-io/rrweb/commit/d08624cb28add386c3618a0e6607424c3f1884d8)]:
+  - rrweb-snapshot@2.0.0-alpha.16
+  - rrdom@2.0.0-alpha.16
+  - @rrweb/types@2.0.0-alpha.16
+
 ## 2.0.12
 
 ### Patch Changes
diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json
index 24e790a36d..3345ec6f26 100644
--- a/packages/rrweb/package.json
+++ b/packages/rrweb/package.json
@@ -1,24 +1,25 @@
 {
   "name": "@saola.ai/rrweb",
-  "version": "2.0.12",
+  "version": "2.0.13",
   "description": "record and replay the web",
   "scripts": {
     "prepare": "npm run prepack",
     "prepack": "npm run build",
-    "retest": "vitest run --exclude test/benchmark",
+    "retest": "cross-env PUPPETEER_HEADLESS=true yarn retest:headful",
+    "retest:headful": "vitest run --exclude test/benchmark",
     "build-and-test": "yarn build && yarn retest",
-    "test:headless": "PUPPETEER_HEADLESS=true yarn build-and-test",
-    "test:headful": "PUPPETEER_HEADLESS=false yarn build-and-test",
+    "test:headless": "cross-env PUPPETEER_HEADLESS=true yarn build-and-test",
+    "test:headful": "cross-env PUPPETEER_HEADLESS=false yarn build-and-test",
     "test": "yarn test:headless",
-    "test:watch": "yarn build && PUPPETEER_HEADLESS=true yarn vitest --exclude test/benchmark",
+    "test:watch": "yarn build && cross-env PUPPETEER_HEADLESS=true yarn vitest --exclude test/benchmark",
     "test:update": "yarn test:headless --update",
-    "retest:update": "PUPPETEER_HEADLESS=true yarn retest --update",
+    "retest:update": "cross-env PUPPETEER_HEADLESS=true yarn retest --update",
     "repl": "yarn build && node scripts/repl.js",
     "live-stream": "yarn build && node scripts/stream.js",
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build",
+    "prepublish": "tsc -noEmit && vite build",
     "lint": "yarn eslint src",
     "benchmark": "vitest run --maxConcurrency 1 --no-file-parallelism test/benchmark"
   },
@@ -68,7 +69,6 @@
     "@types/node": "^18.15.11",
     "@types/offscreencanvas": "^2019.6.4",
     "construct-style-sheets-polyfill": "^3.1.0",
-    "cross-env": "^5.2.0",
     "fast-mhtml": "^1.1.9",
     "identity-obj-proxy": "^3.0.0",
     "ignore-styles": "^5.0.1",
@@ -78,17 +78,18 @@
     "simple-peer-light": "^9.10.0",
     "ts-node": "^10.9.1",
     "tslib": "^2.3.1",
-    "typescript": "^4.7.3",
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1"
+    "typescript": "^5.4.5",
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1"
   },
   "dependencies": {
-    "@saola.ai/rrweb-types": "^2.0.12",
+    "@saola.ai/rrweb-types": "^2.0.13",
+    "@saola.ai/rrweb-utils": "^2.0.13",
     "@types/css-font-loading-module": "0.0.7",
     "@xstate/fsm": "^1.4.0",
     "base64-arraybuffer": "^1.0.1",
     "mitt": "^3.0.0",
-    "@saola.ai/rrdom": "^2.0.12",
-    "@saola.ai/rrweb-snapshot": "^2.0.12"
+    "@saola.ai/rrdom": "^2.0.13",
+    "@saola.ai/rrweb-snapshot": "^2.0.13"
   }
 }
diff --git a/packages/rrweb/src/index.ts b/packages/rrweb/src/index.ts
index e013aab4e7..cbb8c566d9 100644
--- a/packages/rrweb/src/index.ts
+++ b/packages/rrweb/src/index.ts
@@ -17,19 +17,24 @@ export {
   type eventWithTime,
 } from '@saola.ai/rrweb-types';
 
+// exports style.css from replay
+import './replay/styles/style.css';
+
 export type { recordOptions, ReplayPlugin } from './types';
 
 const { addCustomEvent } = record;
 const { freezePage } = record;
+const { takeFullSnapshot } = record;
 
 export {
   record,
   addCustomEvent,
   freezePage,
+  takeFullSnapshot,
   Replayer,
-  playerConfig,
-  PlayerMachineState,
-  SpeedMachineState,
+  type playerConfig,
+  type PlayerMachineState,
+  type SpeedMachineState,
   canvasMutation,
   _mirror as mirror,
   utils,
diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts
index 7320a3c61e..a5bdd977b8 100644
--- a/packages/rrweb/src/record/index.ts
+++ b/packages/rrweb/src/record/index.ts
@@ -1,7 +1,7 @@
 import {
   snapshot,
-  MaskInputOptions,
-  SlimDOMOptions,
+  type MaskInputOptions,
+  type SlimDOMOptions,
   createMirror,
 } from '@saola.ai/rrweb-snapshot';
 import { initObservers, mutationBuffers } from './observer';
@@ -19,14 +19,14 @@ import {
 import type { recordOptions } from '../types';
 import {
   EventType,
-  eventWithoutTime,
-  eventWithTime,
+  type eventWithoutTime,
+  type eventWithTime,
   IncrementalSource,
-  listenerHandler,
-  mutationCallbackParam,
-  scrollCallback,
-  canvasMutationParam,
-  adoptedStyleSheetParam,
+  type listenerHandler,
+  type mutationCallbackParam,
+  type scrollCallback,
+  type canvasMutationParam,
+  type adoptedStyleSheetParam,
 } from '@saola.ai/rrweb-types';
 import type { CrossOriginIframeMessageEventContent } from '../types';
 import { IframeManager } from './iframe-manager';
@@ -39,6 +39,7 @@ import {
   registerErrorHandler,
   unregisterErrorHandler,
 } from './error-handler';
+import dom from '@rrweb/utils';
 
 let wrappedEmit!: (e: eventWithoutTime, isCheckout?: boolean) => void;
 
@@ -383,6 +384,7 @@ function record<T = eventWithTime>(
       inlineStylesheet,
       maskAllInputs: maskInputOptions,
       maskTextFn,
+      maskInputFn,
       slimDOM: slimDOMOptions,
       dataURLOptions,
       recordCanvas,
@@ -395,7 +397,8 @@ function record<T = eventWithTime>(
           stylesheetManager.trackLinkElement(n as HTMLLinkElement);
         }
         if (hasShadowRoot(n)) {
-          shadowDomManager.addShadowRoot(n.shadowRoot, document);
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          shadowDomManager.addShadowRoot(dom.shadowRoot(n as Node)!, document);
         }
       },
       onIframeLoad: (iframe, childSn) => {
diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts
index 23ad493654..53537d03d2 100644
--- a/packages/rrweb/src/record/mutation.ts
+++ b/packages/rrweb/src/record/mutation.ts
@@ -32,6 +32,7 @@ import {
   getShadowHost,
   closestElementOfNode,
 } from '../utils';
+import dom from '@rrweb/utils';
 
 type DoubleLinkedListNode = {
   previous: DoubleLinkedListNode | null;
@@ -168,6 +169,7 @@ export default class MutationBuffer {
   private addedSet = new Set<Node>();
   private movedSet = new Set<Node>();
   private droppedSet = new Set<Node>();
+  private removesSubTreeCache = new Set<Node>();
 
   private mutationCb: observerParam['mutationCb'];
   private blockClass: observerParam['blockClass'];
@@ -285,16 +287,27 @@ export default class MutationBuffer {
       return nextId;
     };
     const pushAdd = (n: Node) => {
-      if (
-        !n.parentNode ||
-        !inDom(n) ||
-        (n.parentNode as Element).tagName === 'TEXTAREA'
-      ) {
+      const parent = dom.parentNode(n);
+      if (!parent || !inDom(n)) {
         return;
       }
-      const parentId = isShadowRoot(n.parentNode)
+      let cssCaptured = false;
+      if (n.nodeType === Node.TEXT_NODE) {
+        const parentTag = (parent as Element).tagName;
+        if (parentTag === 'TEXTAREA') {
+          // genTextAreaValueMutation already called via parent
+          return;
+        } else if (parentTag === 'STYLE' && this.addedSet.has(parent)) {
+          // css content will be recorded via parent's _cssText attribute when
+          // mutation adds entire <style> element
+          cssCaptured = true;
+        }
+      }
+
+      const parentId = isShadowRoot(parent)
         ? this.mirror.getId(getShadowHost(n))
-        : this.mirror.getId(n.parentNode);
+        : this.mirror.getId(parent);
+
       const nextId = getNextId(n);
       if (parentId === -1 || nextId === -1) {
         return addList.addNode(n);
@@ -326,7 +339,8 @@ export default class MutationBuffer {
             );
           }
           if (hasShadowRoot(n)) {
-            this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc);
+            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+            this.shadowDomManager.addShadowRoot(dom.shadowRoot(n)!, this.doc);
           }
         },
         onIframeLoad: (iframe, childSn) => {
@@ -336,6 +350,7 @@ export default class MutationBuffer {
         onStylesheetLoad: (link, childSn) => {
           this.stylesheetManager.attachLinkElement(link, childSn);
         },
+        cssCaptured,
       });
       if (sn) {
         adds.push({
@@ -353,8 +368,8 @@ export default class MutationBuffer {
 
     for (const n of this.movedSet) {
       if (
-        isParentRemoved(this.removes, n, this.mirror) &&
-        !this.movedSet.has(n.parentNode!)
+        isParentRemoved(this.removesSubTreeCache, n, this.mirror) &&
+        !this.movedSet.has(dom.parentNode(n)!)
       ) {
         continue;
       }
@@ -364,7 +379,7 @@ export default class MutationBuffer {
     for (const n of this.addedSet) {
       if (
         !isAncestorInSet(this.droppedSet, n) &&
-        !isParentRemoved(this.removes, n, this.mirror)
+        !isParentRemoved(this.removesSubTreeCache, n, this.mirror)
       ) {
         pushAdd(n);
       } else if (isAncestorInSet(this.movedSet, n)) {
@@ -378,7 +393,7 @@ export default class MutationBuffer {
     while (addList.length) {
       let node: DoubleLinkedListNode | null = null;
       if (candidate) {
-        const parentId = this.mirror.getId(candidate.value.parentNode);
+        const parentId = this.mirror.getId(dom.parentNode(candidate.value));
         const nextId = getNextId(candidate.value);
         if (parentId !== -1 && nextId !== -1) {
           node = candidate;
@@ -391,7 +406,7 @@ export default class MutationBuffer {
           tailNode = tailNode.previous;
           // ensure _node is defined before attempting to find value
           if (_node) {
-            const parentId = this.mirror.getId(_node.value.parentNode);
+            const parentId = this.mirror.getId(dom.parentNode(_node.value));
             const nextId = getNextId(_node.value);
 
             if (nextId === -1) continue;
@@ -403,14 +418,10 @@ export default class MutationBuffer {
             // nextId !== -1 && parentId === -1 This branch can happen if the node is the child of shadow root
             else {
               const unhandledNode = _node.value;
+              const parent = dom.parentNode(unhandledNode);
               // If the node is the direct child of a shadow root, we treat the shadow host as its parent node.
-              if (
-                unhandledNode.parentNode &&
-                unhandledNode.parentNode.nodeType ===
-                  Node.DOCUMENT_FRAGMENT_NODE
-              ) {
-                const shadowHost = (unhandledNode.parentNode as ShadowRoot)
-                  .host;
+              if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+                const shadowHost = dom.host(parent as ShadowRoot);
                 const parentId = this.mirror.getId(shadowHost);
                 if (parentId !== -1) {
                   node = _node;
@@ -441,12 +452,10 @@ export default class MutationBuffer {
       texts: this.texts
         .map((text) => {
           const n = text.node;
-          if (
-            n.parentNode &&
-            (n.parentNode as Element).tagName === 'TEXTAREA'
-          ) {
+          const parent = dom.parentNode(n);
+          if (parent && (parent as Element).tagName === 'TEXTAREA') {
             // the node is being ignored as it isn't in the mirror, so shift mutation to attributes on parent textarea
-            this.genTextAreaValueMutation(n.parentNode as HTMLTextAreaElement);
+            this.genTextAreaValueMutation(parent as HTMLTextAreaElement);
           }
           return {
             id: this.mirror.getId(n),
@@ -506,6 +515,7 @@ export default class MutationBuffer {
     this.addedSet = new Set<Node>();
     this.movedSet = new Set<Node>();
     this.droppedSet = new Set<Node>();
+    this.removesSubTreeCache = new Set<Node>();
     this.movedMap = {};
 
     this.mutationCb(payload);
@@ -524,8 +534,8 @@ export default class MutationBuffer {
       this.attributeMap.set(textarea, item);
     }
     item.attributes.value = Array.from(
-      textarea.childNodes,
-      (cn) => cn.textContent || '',
+      dom.childNodes(textarea),
+      (cn) => dom.textContent(cn) || '',
     ).join('');
   };
 
@@ -535,7 +545,7 @@ export default class MutationBuffer {
     }
     switch (m.type) {
       case 'characterData': {
-        const value = m.target.textContent;
+        const value = dom.textContent(m.target);
 
         if (
           !isBlocked(m.target, this.blockClass, this.blockSelector, false) &&
@@ -663,6 +673,12 @@ export default class MutationBuffer {
                 item.styleDiff[pname] = false; // delete
               }
             }
+          } else if (attributeName === 'open' && target.tagName === 'DIALOG') {
+            if (target.matches('dialog:modal')) {
+              item.attributes['rr_open_mode'] = 'modal';
+            } else {
+              item.attributes['rr_open_mode'] = 'non-modal';
+            }
           }
         }
         break;
@@ -684,7 +700,7 @@ export default class MutationBuffer {
         m.removedNodes.forEach((n) => {
           const nodeId = this.mirror.getId(n);
           const parentId = isShadowRoot(m.target)
-            ? this.mirror.getId(m.target.host)
+            ? this.mirror.getId(dom.host(m.target))
             : this.mirror.getId(m.target);
           if (
             isBlocked(m.target, this.blockClass, this.blockSelector, false) ||
@@ -726,6 +742,7 @@ export default class MutationBuffer {
                   ? true
                   : undefined,
             });
+            processRemoves(n, this.removesSubTreeCache);
           }
           this.mapRemoves.push(n);
         });
@@ -766,9 +783,10 @@ export default class MutationBuffer {
     // if this node is blocked `serializeNode` will turn it into a placeholder element
     // but we have to remove it's children otherwise they will be added as placeholders too
     if (!isBlocked(n, this.blockClass, this.blockSelector, false)) {
-      n.childNodes.forEach((childN) => this.genAdds(childN));
+      dom.childNodes(n).forEach((childN) => this.genAdds(childN));
       if (hasShadowRoot(n)) {
-        n.shadowRoot.childNodes.forEach((childN) => {
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        dom.childNodes(dom.shadowRoot(n)!).forEach((childN) => {
           this.processedNodeManager.add(childN, this);
           this.genAdds(childN, n);
         });
@@ -785,32 +803,36 @@ export default class MutationBuffer {
  */
 function deepDelete(addsSet: Set<Node>, n: Node) {
   addsSet.delete(n);
-  n.childNodes.forEach((childN) => deepDelete(addsSet, childN));
+  dom.childNodes(n).forEach((childN) => deepDelete(addsSet, childN));
 }
 
-function isParentRemoved(
-  removes: removedNodeMutation[],
-  n: Node,
-  mirror: Mirror,
-): boolean {
-  if (removes.length === 0) return false;
+function processRemoves(n: Node, cache: Set<Node>) {
+  const queue = [n];
+
+  while (queue.length) {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const next = queue.pop()!;
+    if (cache.has(next)) continue;
+    cache.add(next);
+    dom.childNodes(next).forEach((n) => queue.push(n));
+  }
+
+  return;
+}
+
+function isParentRemoved(removes: Set<Node>, n: Node, mirror: Mirror): boolean {
+  if (removes.size === 0) return false;
   return _isParentRemoved(removes, n, mirror);
 }
 
 function _isParentRemoved(
-  removes: removedNodeMutation[],
+  removes: Set<Node>,
   n: Node,
-  mirror: Mirror,
+  _mirror: Mirror,
 ): boolean {
-  let node: ParentNode | null = n.parentNode;
-  while (node) {
-    const parentId = mirror.getId(node);
-    if (removes.some((r) => r.id === parentId)) {
-      return true;
-    }
-    node = node.parentNode;
-  }
-  return false;
+  const node: ParentNode | null = dom.parentNode(n);
+  if (!node) return false;
+  return removes.has(node);
 }
 
 function isAncestorInSet(set: Set<Node>, n: Node): boolean {
@@ -819,12 +841,12 @@ function isAncestorInSet(set: Set<Node>, n: Node): boolean {
 }
 
 function _isAncestorInSet(set: Set<Node>, n: Node): boolean {
-  const { parentNode } = n;
-  if (!parentNode) {
+  const parent = dom.parentNode(n);
+  if (!parent) {
     return false;
   }
-  if (set.has(parentNode)) {
+  if (set.has(parent)) {
     return true;
   }
-  return _isAncestorInSet(set, parentNode);
+  return _isAncestorInSet(set, parent);
 }
diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts
index 5ff8f78e6b..ce8f1f8f7e 100644
--- a/packages/rrweb/src/record/observer.ts
+++ b/packages/rrweb/src/record/observer.ts
@@ -1,5 +1,5 @@
 import {
-  MaskInputOptions,
+  type MaskInputOptions,
   maskInputValue,
   Mirror,
   getInputType,
@@ -21,12 +21,16 @@ import {
 } from '../utils';
 import type { observerParam, MutationBufferParam } from '../types';
 import {
+  IncrementalSource,
+  MouseInteractions,
+  PointerTypes,
+  MediaInteractions,
+} from '@saola.ai/rrweb-types';
+import type {
   mutationCallBack,
   mousemoveCallBack,
   mousePosition,
   mouseInteractionCallBack,
-  MouseInteractions,
-  PointerTypes,
   listenerHandler,
   scrollCallback,
   styleSheetRuleCallback,
@@ -34,11 +38,9 @@ import {
   inputValue,
   inputCallback,
   hookResetter,
-  IncrementalSource,
   hooksParam,
   Arguments,
   mediaInteractionCallback,
-  MediaInteractions,
   canvasMutationCallback,
   fontCallback,
   fontParam,
@@ -50,15 +52,7 @@ import {
 } from '@saola.ai/rrweb-types';
 import MutationBuffer from './mutation';
 import { callbackWrapper } from './error-handler';
-
-type WindowWithStoredMutationObserver = IWindow & {
-  __rrMutationObserver?: MutationObserver;
-};
-type WindowWithAngularZone = IWindow & {
-  Zone?: {
-    __symbol__?: (key: string) => string;
-  };
-};
+import dom, { mutationObserverCtor } from '@rrweb/utils';
 
 export const mutationBuffers: MutationBuffer[] = [];
 
@@ -92,31 +86,7 @@ export function initMutationObserver(
   mutationBuffers.push(mutationBuffer);
   // see mutation.ts for details
   mutationBuffer.init(options);
-  let mutationObserverCtor =
-    window.MutationObserver ||
-    /**
-     * Some websites may disable MutationObserver by removing it from the window object.
-     * If someone is using rrweb to build a browser extention or things like it, they
-     * could not change the website's code but can have an opportunity to inject some
-     * code before the website executing its JS logic.
-     * Then they can do this to store the native MutationObserver:
-     * window.__rrMutationObserver = MutationObserver
-     */
-    (window as WindowWithStoredMutationObserver).__rrMutationObserver;
-  const angularZoneSymbol = (
-    window as WindowWithAngularZone
-  )?.Zone?.__symbol__?.('MutationObserver');
-  if (
-    angularZoneSymbol &&
-    (window as unknown as Record<string, typeof MutationObserver>)[
-      angularZoneSymbol
-    ]
-  ) {
-    mutationObserverCtor = (
-      window as unknown as Record<string, typeof MutationObserver>
-    )[angularZoneSymbol];
-  }
-  const observer = new (mutationObserverCtor as new (
+  const observer = new (mutationObserverCtor() as new (
     callback: MutationCallback,
   ) => MutationObserver)(
     callbackWrapper(mutationBuffer.processMutations.bind(mutationBuffer)),
@@ -431,7 +401,7 @@ function initInputObserver({
      * We can treat this change as a value change of the select element the current target belongs to.
      */
     if (target && tagName === 'OPTION') {
-      target = target.parentElement;
+      target = dom.parentElement(target);
     }
     if (
       !target ||
@@ -657,6 +627,17 @@ function initStyleSheetObserver(
     ),
   });
 
+  // Support for deprecated addRule method
+  win.CSSStyleSheet.prototype.addRule = function (
+    this: CSSStyleSheet,
+    selector: string,
+    styleBlock: string,
+    index: number = this.cssRules.length,
+  ) {
+    const rule = `${selector} { ${styleBlock} }`;
+    return win.CSSStyleSheet.prototype.insertRule.apply(this, [rule, index]);
+  };
+
   // eslint-disable-next-line @typescript-eslint/unbound-method
   const deleteRule = win.CSSStyleSheet.prototype.deleteRule;
   win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, {
@@ -686,6 +667,14 @@ function initStyleSheetObserver(
     ),
   });
 
+  // Support for deprecated removeRule method
+  win.CSSStyleSheet.prototype.removeRule = function (
+    this: CSSStyleSheet,
+    index: number,
+  ) {
+    return win.CSSStyleSheet.prototype.deleteRule.apply(this, [index]);
+  };
+
   let replace: (text: string) => Promise<CSSStyleSheet>;
 
   if (win.CSSStyleSheet.prototype.replace) {
@@ -881,7 +870,7 @@ export function initAdoptedStyleSheetObserver(
   // host of adoptedStyleSheets is outermost document or IFrame's document
   if (host.nodeName === '#document') hostId = mirror.getId(host);
   // The host is a ShadowRoot.
-  else hostId = mirror.getId((host as ShadowRoot).host);
+  else hostId = mirror.getId(dom.host(host as ShadowRoot));
 
   const patchTarget =
     host.nodeName === '#document'
diff --git a/packages/rrweb/src/record/observers/canvas/2d.ts b/packages/rrweb/src/record/observers/canvas/2d.ts
index 3e4744dfdd..c095038f7b 100644
--- a/packages/rrweb/src/record/observers/canvas/2d.ts
+++ b/packages/rrweb/src/record/observers/canvas/2d.ts
@@ -1,9 +1,9 @@
 import {
-  blockClass,
+  type blockClass,
   CanvasContext,
-  canvasManagerMutationCallback,
-  IWindow,
-  listenerHandler,
+  type canvasManagerMutationCallback,
+  type IWindow,
+  type listenerHandler,
 } from '@saola.ai/rrweb-types';
 import { hookSetter, isBlocked, patch } from '../../../utils';
 import { serializeArgs } from './serialize-args';
diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts
index 94983e9dbb..9fc1909f60 100644
--- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts
+++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts
@@ -262,7 +262,6 @@ export class CanvasManager {
       win,
       blockClass,
       blockSelector,
-      this.mirror,
     );
 
     this.resetObservers = () => {
diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts
index 12a146034a..3ad20622fc 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl.ts
@@ -1,11 +1,10 @@
-import type { Mirror } from '@saola.ai/rrweb-snapshot';
 import {
-  blockClass,
+  type blockClass,
   CanvasContext,
-  canvasManagerMutationCallback,
-  canvasMutationWithType,
-  IWindow,
-  listenerHandler,
+  type canvasManagerMutationCallback,
+  type canvasMutationWithType,
+  type IWindow,
+  type listenerHandler,
 } from '@saola.ai/rrweb-types';
 import { hookSetter, isBlocked, patch } from '../../../utils';
 import { saveWebGLVar, serializeArgs } from './serialize-args';
@@ -16,7 +15,6 @@ function patchGLPrototype(
   cb: canvasManagerMutationCallback,
   blockClass: blockClass,
   blockSelector: string | null,
-  _mirror: Mirror,
   win: IWindow,
 ): listenerHandler[] {
   const handlers: listenerHandler[] = [];
@@ -93,7 +91,6 @@ export default function initCanvasWebGLMutationObserver(
   win: IWindow,
   blockClass: blockClass,
   blockSelector: string | null,
-  mirror: Mirror,
 ): listenerHandler {
   const handlers: listenerHandler[] = [];
 
@@ -104,7 +101,6 @@ export default function initCanvasWebGLMutationObserver(
       cb,
       blockClass,
       blockSelector,
-      mirror,
       win,
     ),
   );
@@ -117,7 +113,6 @@ export default function initCanvasWebGLMutationObserver(
         cb,
         blockClass,
         blockSelector,
-        mirror,
         win,
       ),
     );
diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts
index c2730ac78e..a60ad3307d 100644
--- a/packages/rrweb/src/record/shadow-dom-manager.ts
+++ b/packages/rrweb/src/record/shadow-dom-manager.ts
@@ -12,6 +12,7 @@ import {
 import { patch, inDom } from '../utils';
 import type { Mirror } from '@saola.ai/rrweb-snapshot';
 import { isNativeShadowDom } from '@saola.ai/rrweb-snapshot';
+import dom from '@saola.ai/rrweb-utils';
 
 type BypassOptions = Omit<
   MutationBufferParam,
@@ -81,7 +82,7 @@ export class ShadowDomManager {
       )
         this.bypassOptions.stylesheetManager.adoptStyleSheets(
           shadowRoot.adoptedStyleSheets,
-          this.mirror.getId(shadowRoot.host),
+          this.mirror.getId(dom.host(shadowRoot)),
         );
       this.restoreHandlers.push(
         initAdoptedStyleSheetObserver(
@@ -128,13 +129,14 @@ export class ShadowDomManager {
         'attachShadow',
         function (original: (init: ShadowRootInit) => ShadowRoot) {
           return function (this: Element, option: ShadowRootInit) {
-            const shadowRoot = original.call(this, option);
+            const sRoot = original.call(this, option);
             // For the shadow dom elements in the document, monitor their dom mutations.
             // For shadow dom elements that aren't in the document yet,
             // we start monitoring them once their shadow dom host is appended to the document.
-            if (this.shadowRoot && inDom(this))
-              manager.addShadowRoot(this.shadowRoot, doc);
-            return shadowRoot;
+            const shadowRootEl = dom.shadowRoot(this);
+            if (shadowRootEl && inDom(this))
+              manager.addShadowRoot(shadowRootEl, doc);
+            return sRoot;
           };
         },
       ),
diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts
index f0ddfa0898..2d054ebd61 100644
--- a/packages/rrweb/src/record/stylesheet-manager.ts
+++ b/packages/rrweb/src/record/stylesheet-manager.ts
@@ -70,7 +70,7 @@ export class StylesheetManager {
         styles.push({
           styleId,
           rules: Array.from(sheet.rules || CSSRule, (r, index) => ({
-            rule: stringifyRule(r),
+            rule: stringifyRule(r, sheet.href),
             index,
           })),
         });
diff --git a/packages/rrweb/src/replay/canvas/index.ts b/packages/rrweb/src/replay/canvas/index.ts
index b31cb6fd5c..207d158e4b 100644
--- a/packages/rrweb/src/replay/canvas/index.ts
+++ b/packages/rrweb/src/replay/canvas/index.ts
@@ -1,9 +1,9 @@
 import type { Replayer } from '..';
 import {
   CanvasContext,
-  canvasMutationCommand,
-  canvasMutationData,
-  canvasMutationParam,
+  type canvasMutationCommand,
+  type canvasMutationData,
+  type canvasMutationParam,
 } from '@saola.ai/rrweb-types';
 import webglMutation from './webgl';
 import canvas2DMutation from './2d';
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index 75e0d4917b..e0939b6f43 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -1,5 +1,5 @@
 import type { Replayer } from '../';
-import { CanvasContext, canvasMutationCommand } from '@saola.ai/rrweb-types';
+import { CanvasContext, type canvasMutationCommand } from '@saola.ai/rrweb-types';
 import { deserializeArg, variableListFor } from './deserialize-args';
 
 function getContext(
diff --git a/packages/rrweb/src/replay/dialog/index.ts b/packages/rrweb/src/replay/dialog/index.ts
new file mode 100644
index 0000000000..97cafa41a8
--- /dev/null
+++ b/packages/rrweb/src/replay/dialog/index.ts
@@ -0,0 +1,67 @@
+import type { attributeMutation } from '@saola.ai/rrweb-types';
+import { RRNode } from '@saola.ai/rrdom';
+
+/**
+ * Checks if the dialog is a top level dialog and applies the dialog to the top level
+ * @param node - potential dialog element to apply top level `showModal()` to, or other node (which will be ignored)
+ * @param attributeMutation - the attribute mutation used to change the dialog (optional)
+ * @returns void
+ */
+export function applyDialogToTopLevel(
+  node: HTMLDialogElement | Node | RRNode,
+  attributeMutation?: attributeMutation,
+): void {
+  if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return;
+  const dialog = node as HTMLDialogElement;
+  const oldIsOpen = dialog.open;
+  const oldIsModalState = oldIsOpen && dialog.matches('dialog:modal');
+  const rrOpenMode = dialog.getAttribute('rr_open_mode');
+
+  const newIsOpen =
+    typeof attributeMutation?.attributes.open === 'string' ||
+    typeof dialog.getAttribute('open') === 'string';
+  const newIsModalState = rrOpenMode === 'modal';
+  const newIsNonModalState = rrOpenMode === 'non-modal';
+
+  const modalStateChanged =
+    (oldIsModalState && newIsNonModalState) ||
+    (!oldIsModalState && newIsModalState);
+
+  if (oldIsOpen && !modalStateChanged) return;
+  // complain if dialog is not attached to the dom
+  if (!dialog.isConnected) {
+    console.warn('dialog is not attached to the dom', dialog);
+    return;
+  }
+
+  if (oldIsOpen) dialog.close();
+  if (!newIsOpen) return;
+
+  if (newIsModalState) dialog.showModal();
+  else dialog.show();
+}
+
+/**
+ * Check if the dialog is a top level dialog and removes the dialog from the top level if necessary
+ * @param node - potential dialog element to remove from top level, or other node (which will be ignored)
+ * @param attributeMutation - the attribute mutation used to change the dialog
+ * @returns void
+ */
+export function removeDialogFromTopLevel(
+  node: HTMLDialogElement | Node | RRNode,
+  attributeMutation: attributeMutation,
+): void {
+  if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return;
+  const dialog = node as HTMLDialogElement;
+
+  // complain if dialog is not attached to the dom
+  if (!dialog.isConnected) {
+    console.warn('dialog is not attached to the dom', dialog);
+    return;
+  }
+
+  if (attributeMutation.attributes.open === null) {
+    dialog.removeAttribute('open');
+    dialog.removeAttribute('rr_open_mode');
+  }
+}
diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts
index bb991c3783..552b9df7b5 100644
--- a/packages/rrweb/src/replay/index.ts
+++ b/packages/rrweb/src/replay/index.ts
@@ -1,13 +1,14 @@
 import {
   rebuild,
+  adaptCssForReplay,
   buildNodeWithSN,
   NodeType,
-  BuildCache,
+  type BuildCache,
   createCache,
   Mirror,
   createMirror,
-  attributes,
-  serializedElementNodeWithId,
+  type attributes,
+  type serializedElementNodeWithId,
   toLowerCase,
 } from '@saola.ai/rrweb-snapshot';
 import {
@@ -41,18 +42,19 @@ import type { playerConfig, missingNodeMap } from '../types';
 import {
   EventType,
   IncrementalSource,
+  MouseInteractions,
+  ReplayerEvents,
+} from '@saola.ai/rrweb-types';
+import type {
   fullSnapshotEvent,
   eventWithTime,
-  MouseInteractions,
   playerMetaData,
   viewportResizeDimension,
   addedNodeMutation,
   incrementalSnapshotEvent,
   incrementalData,
-  ReplayerEvents,
   Handler,
   Emitter,
-  metaEvent,
   mutationData,
   scrollData,
   inputData,
@@ -72,7 +74,7 @@ import {
   polyfill,
   queueToResolveTrees,
   iterateResolveTree,
-  AppendedIframe,
+  type AppendedIframe,
   getBaseDimension,
   hasShadowRoot,
   isSerializedIframe,
@@ -86,6 +88,7 @@ import './styles/style.css';
 import canvasMutation from './canvas';
 import { deserializeArg } from './canvas/deserialize-args';
 import { MediaManager } from './media';
+import { applyDialogToTopLevel, removeDialogFromTopLevel } from './dialog';
 
 const SKIP_TIME_INTERVAL = 5 * 1000;
 
@@ -393,7 +396,7 @@ export class Replayer {
       (e) => e.type === EventType.FullSnapshot,
     );
     if (firstMeta) {
-      const { width, height } = firstMeta.data as metaEvent['data'];
+      const { width, height } = firstMeta.data;
       setTimeout(() => {
         this.emitter.emit(ReplayerEvents.Resize, {
           width,
@@ -849,9 +852,12 @@ export class Replayer {
       );
     }
     this.legacy_missingNodeRetryMap = {};
-    const collected: AppendedIframe[] = [];
+    const collectedIframes: AppendedIframe[] = [];
+    const collectedDialogs = new Set<HTMLDialogElement>();
     const afterAppend = (builtNode: Node, id: number) => {
-      this.collectIframeAndAttachDocument(collected, builtNode);
+      if (builtNode.nodeName === 'DIALOG')
+        collectedDialogs.add(builtNode as HTMLDialogElement);
+      this.collectIframeAndAttachDocument(collectedIframes, builtNode);
       if (this.mediaManager.isSupportedMediaElement(builtNode)) {
         const { events } = this.service.state.context;
         this.mediaManager.addMediaElements(
@@ -889,7 +895,7 @@ export class Replayer {
     });
     afterAppend(this.iframe.contentDocument, event.data.node.id);
 
-    for (const { mutationInQueue, builtNode } of collected) {
+    for (const { mutationInQueue, builtNode } of collectedIframes) {
       this.attachDocumentToIframe(mutationInQueue, builtNode);
       this.newDocumentQueue = this.newDocumentQueue.filter(
         (m) => m !== mutationInQueue,
@@ -897,6 +903,7 @@ export class Replayer {
     }
     const { documentElement, head } = this.iframe.contentDocument;
     this.insertStyleRules(documentElement, head);
+    collectedDialogs.forEach((d) => applyDialogToTopLevel(d));
     if (!this.service.state.matches('playing')) {
       this.iframe.contentDocument
         .getElementsByTagName('html')[0]
@@ -923,6 +930,9 @@ export class Replayer {
         'html.rrweb-paused *, html.rrweb-paused *:before, html.rrweb-paused *:after { animation-play-state: paused !important; }',
       );
     }
+    if (!injectStylesRules.length) {
+      return;
+    }
     if (this.usingVirtualDom) {
       const styleEl = this.virtualDom.createElement('style');
       this.virtualDom.mirror.add(
@@ -959,9 +969,12 @@ export class Replayer {
     type TNode = typeof mirror extends Mirror ? Node : RRNode;
     type TMirror = typeof mirror extends Mirror ? Mirror : RRDOMMirror;
 
-    const collected: AppendedIframe[] = [];
+    const collectedIframes: AppendedIframe[] = [];
+    const collectedDialogs = new Set<HTMLDialogElement>();
     const afterAppend = (builtNode: Node, id: number) => {
-      this.collectIframeAndAttachDocument(collected, builtNode);
+      if (builtNode.nodeName === 'DIALOG')
+        collectedDialogs.add(builtNode as HTMLDialogElement);
+      this.collectIframeAndAttachDocument(collectedIframes, builtNode);
       const sn = (mirror as TMirror).getMeta(builtNode as unknown as TNode);
       if (
         sn?.type === NodeType.Element &&
@@ -996,12 +1009,14 @@ export class Replayer {
     });
     afterAppend(iframeEl.contentDocument! as Document, mutation.node.id);
 
-    for (const { mutationInQueue, builtNode } of collected) {
+    for (const { mutationInQueue, builtNode } of collectedIframes) {
       this.attachDocumentToIframe(mutationInQueue, builtNode);
       this.newDocumentQueue = this.newDocumentQueue.filter(
         (m) => m !== mutationInQueue,
       );
     }
+
+    collectedDialogs.forEach((d) => applyDialogToTopLevel(d));
   }
 
   private collectIframeAndAttachDocument(
@@ -1582,6 +1597,7 @@ export class Replayer {
       const afterAppend = (node: Node | RRNode, id: number) => {
         // Skip the plugin onBuild callback for virtual dom
         if (this.usingVirtualDom) return;
+        applyDialogToTopLevel(node);
         for (const plugin of this.config.plugins || []) {
           if (plugin.onBuild) plugin.onBuild(node, { id, replayer: this });
         }
@@ -1619,20 +1635,39 @@ export class Replayer {
       if (
         parentSn &&
         parentSn.type === NodeType.Element &&
-        parentSn.tagName === 'textarea' &&
         mutation.node.type === NodeType.Text
       ) {
-        const childNodeArray = Array.isArray(parent.childNodes)
+        const prospectiveSiblings = Array.isArray(parent.childNodes)
           ? parent.childNodes
           : Array.from(parent.childNodes);
-        // This should be redundant now as we are either recording the value or the childNode, and not both
-        // keeping around for backwards compatibility with old bad double data, see
-
-        // https://github.com/rrweb-io/rrweb/issues/745
-        // parent is textarea, will only keep one child node as the value
-        for (const c of childNodeArray) {
-          if (c.nodeType === parent.TEXT_NODE) {
-            parent.removeChild(c as Node & RRNode);
+        if (parentSn.tagName === 'textarea') {
+          // This should be redundant now as we are either recording the value or the childNode, and not both
+          // keeping around for backwards compatibility with old bad double data, see
+
+          // https://github.com/rrweb-io/rrweb/issues/745
+          // parent is textarea, will only keep one child node as the value
+          for (const c of prospectiveSiblings) {
+            if (c.nodeType === parent.TEXT_NODE) {
+              parent.removeChild(c as Node & RRNode);
+            }
+          }
+        } else if (
+          parentSn.tagName === 'style' &&
+          prospectiveSiblings.length === 1
+        ) {
+          // https://github.com/rrweb-io/rrweb/pull/1417
+          /**
+           * If both _cssText and textContent are present for a style element due to some existing bugs, the element was ending up with two child text nodes
+           * We need to remove the textNode created by _cssText as it doesn't have an id in the mirror, and thus cannot be further mutated.
+           */
+          for (const cssText of prospectiveSiblings as (Node & RRNode)[]) {
+            if (
+              cssText.nodeType === parent.TEXT_NODE &&
+              !mirror.hasNode(cssText)
+            ) {
+              target.textContent = cssText.textContent;
+              parent.removeChild(cssText);
+            }
           }
         }
       } else if (parentSn?.type === NodeType.Document) {
@@ -1762,7 +1797,14 @@ export class Replayer {
         }
         return this.warnNodeNotFound(d, mutation.id);
       }
-      target.textContent = mutation.value;
+
+      const parentEl = target.parentElement as Element | RRElement;
+      if (mutation.value && parentEl && parentEl.tagName === 'STYLE') {
+        // assumes hackCss: true (which isn't currently configurable from rrweb)
+        target.textContent = adaptCssForReplay(mutation.value, this.cache);
+      } else {
+        target.textContent = mutation.value;
+      }
 
       /**
        * https://github.com/rrweb-io/rrweb/pull/865
@@ -1787,6 +1829,8 @@ export class Replayer {
           const value = mutation.attributes[attributeName];
           if (value === null) {
             (target as Element | RRElement).removeAttribute(attributeName);
+            if (attributeName === 'open')
+              removeDialogFromTopLevel(target, mutation);
           } else if (typeof value === 'string') {
             try {
               // When building snapshot, some link styles haven't loaded. Then they are loaded, they will be inlined as incremental mutation change of attribute. We need to replace the old elements whose styles aren't inlined.
@@ -1843,6 +1887,13 @@ export class Replayer {
                   value,
                 );
               }
+
+              if (
+                attributeName === 'rr_open_mode' &&
+                target.nodeName === 'DIALOG'
+              ) {
+                applyDialogToTopLevel(target, mutation);
+              }
             } catch (error) {
               this.warn(
                 'An error occurred may due to the checkout feature.',
@@ -1859,7 +1910,7 @@ export class Replayer {
                 const svp = styleValues[s] as styleValueWithPriority;
                 targetEl.style.setProperty(s, svp[0], svp[1]);
               } else {
-                const svs = styleValues[s] as string;
+                const svs = styleValues[s];
                 targetEl.style.setProperty(s, svs);
               }
             }
@@ -2092,7 +2143,7 @@ export class Replayer {
     const adoptStyleSheets = (targetHost: Node, styleIds: number[]) => {
       const stylesToAdopt = styleIds
         .map((styleId) => this.styleMirror.getStyle(styleId))
-        .filter((style) => style !== null) as CSSStyleSheet[];
+        .filter((style) => style !== null);
       if (hasShadowRoot(targetHost))
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         (targetHost as HTMLElement).shadowRoot!.adoptedStyleSheets =
@@ -2280,4 +2331,4 @@ export class Replayer {
   }
 }
 
-export { PlayerMachineState, SpeedMachineState, playerConfig };
+export { type PlayerMachineState, type SpeedMachineState, type playerConfig };
diff --git a/packages/rrweb/src/replay/machine.ts b/packages/rrweb/src/replay/machine.ts
index 4716d91ef0..bf2a3d0dd9 100644
--- a/packages/rrweb/src/replay/machine.ts
+++ b/packages/rrweb/src/replay/machine.ts
@@ -1,10 +1,15 @@
-import { createMachine, interpret, assign, StateMachine } from '@xstate/fsm';
+import {
+  createMachine,
+  interpret,
+  assign,
+  type StateMachine,
+} from '@xstate/fsm';
 import type { playerConfig } from '../types';
 import {
-  eventWithTime,
+  type eventWithTime,
   ReplayerEvents,
   EventType,
-  Emitter,
+  type Emitter,
   IncrementalSource,
 } from '@saola.ai/rrweb-types';
 import { Timer, addDelay } from './timer';
diff --git a/packages/rrweb/src/replay/media/index.ts b/packages/rrweb/src/replay/media/index.ts
index ade0e1702c..98b1969710 100644
--- a/packages/rrweb/src/replay/media/index.ts
+++ b/packages/rrweb/src/replay/media/index.ts
@@ -1,5 +1,5 @@
 import {
-  Emitter,
+  type Emitter,
   MediaInteractions,
   ReplayerEvents,
 } from '@saola.ai/rrweb-types';
diff --git a/packages/rrweb/src/replay/timer.ts b/packages/rrweb/src/replay/timer.ts
index 105e4b2752..015d4c84ec 100644
--- a/packages/rrweb/src/replay/timer.ts
+++ b/packages/rrweb/src/replay/timer.ts
@@ -1,6 +1,6 @@
 import {
-  actionWithDelay,
-  eventWithTime,
+  type actionWithDelay,
+  type eventWithTime,
   EventType,
   IncrementalSource,
 } from '@saola.ai/rrweb-types';
diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts
index 3c68a74fd1..2e0c641278 100644
--- a/packages/rrweb/src/utils.ts
+++ b/packages/rrweb/src/utils.ts
@@ -15,7 +15,8 @@ import {
   IGNORED_NODE,
   classMatchesRegex,
 } from '@saola.ai/rrweb-snapshot';
-import type { RRNode, RRIFrameElement } from '@saola.ai/rrdom';
+import { RRNode, RRIFrameElement, BaseRRNode } from '@saola.ai/rrdom';
+import dom from '@saola.ai/rrweb-utils';
 
 export function on(
   type: string,
@@ -188,8 +189,8 @@ export function getWindowScroll(win: Window) {
       ? doc.scrollingElement.scrollLeft
       : win.pageXOffset !== undefined
       ? win.pageXOffset
-      : doc?.documentElement.scrollLeft ||
-        doc?.body?.parentElement?.scrollLeft ||
+      : doc.documentElement.scrollLeft ||
+        (doc?.body && dom.parentElement(doc.body)?.scrollLeft) ||
         doc?.body?.scrollLeft ||
         0,
     top: doc.scrollingElement
@@ -197,7 +198,7 @@ export function getWindowScroll(win: Window) {
       : win.pageYOffset !== undefined
       ? win.pageYOffset
       : doc?.documentElement.scrollTop ||
-        doc?.body?.parentElement?.scrollTop ||
+        (doc?.body && dom.parentElement(doc.body)?.scrollTop) ||
         doc?.body?.scrollTop ||
         0,
   };
@@ -232,7 +233,7 @@ export function closestElementOfNode(node: Node | null): HTMLElement | null {
   const el: HTMLElement | null =
     node.nodeType === node.ELEMENT_NODE
       ? (node as HTMLElement)
-      : node.parentElement;
+      : dom.parentElement(node);
   return el;
 }
 
@@ -304,17 +305,15 @@ export function isAncestorRemoved(target: Node, mirror: Mirror): boolean {
   if (!mirror.has(id)) {
     return true;
   }
-  if (
-    target.parentNode &&
-    target.parentNode.nodeType === target.DOCUMENT_NODE
-  ) {
+  const parent = dom.parentNode(target);
+  if (parent && parent.nodeType === target.DOCUMENT_NODE) {
     return false;
   }
   // if the root is not document, it means the node is not in the DOM tree anymore
-  if (!target.parentNode) {
+  if (!parent) {
     return true;
   }
-  return isAncestorRemoved(target.parentNode, mirror);
+  return isAncestorRemoved(parent, mirror);
 }
 
 export function legacy_isTouchEvent(
@@ -335,24 +334,6 @@ export function polyfill(win = window) {
     win.DOMTokenList.prototype.forEach = Array.prototype
       .forEach as unknown as DOMTokenList['forEach'];
   }
-
-  // https://github.com/Financial-Times/polyfill-service/pull/183
-  if (!Node.prototype.contains) {
-    Node.prototype.contains = (...args: unknown[]) => {
-      let node = args[0] as Node | null;
-      if (!(0 in args)) {
-        throw new TypeError('1 argument is required');
-      }
-
-      do {
-        if (this === node) {
-          return true;
-        }
-      } while ((node = node && node.parentNode));
-
-      return false;
-    };
-  }
 }
 
 type ResolveTree = {
@@ -478,7 +459,11 @@ export function getBaseDimension(
 export function hasShadowRoot<T extends Node | RRNode>(
   n: T,
 ): n is T & { shadowRoot: ShadowRoot } {
-  return Boolean((n as unknown as Element)?.shadowRoot);
+  if (!n) return false;
+  if (n instanceof BaseRRNode && 'shadowRoot' in n) {
+    return Boolean(n.shadowRoot);
+  }
+  return Boolean(dom.shadowRoot(n as unknown as Element));
 }
 
 export function getNestedRule(
@@ -570,10 +555,11 @@ export class StyleSheetMirror {
 export function getShadowHost(n: Node): Element | null {
   let shadowHost: Element | null = null;
   if (
-    n.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE &&
-    (n.getRootNode() as ShadowRoot).host
+    'getRootNode' in n &&
+    dom.getRootNode(n)?.nodeType === Node.DOCUMENT_FRAGMENT_NODE &&
+    dom.host(dom.getRootNode(n) as ShadowRoot)
   )
-    shadowHost = (n.getRootNode() as ShadowRoot).host;
+    shadowHost = dom.host(dom.getRootNode(n) as ShadowRoot);
   return shadowHost;
 }
 
@@ -595,11 +581,11 @@ export function shadowHostInDom(n: Node): boolean {
   const doc = n.ownerDocument;
   if (!doc) return false;
   const shadowHost = getRootShadowHost(n);
-  return doc.contains(shadowHost);
+  return dom.contains(doc, shadowHost);
 }
 
 export function inDom(n: Node): boolean {
   const doc = n.ownerDocument;
   if (!doc) return false;
-  return doc.contains(n) || shadowHostInDom(n);
+  return dom.contains(doc, n) || shadowHostInDom(n);
 }
diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
index 5e15f35ddd..4fa6c2f35b 100644
--- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap
+++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
@@ -1034,7 +1034,7 @@ exports[`record integration tests > can mask character data mutations with regex
 ]"
 `;
 
-exports[`record integration tests > can record attribute mutation 1`] = `
+exports[`record integration tests > can record and replay style mutations 1`] = `
 "[
   {
     \\"type\\": 0,
@@ -1068,83 +1068,91 @@ exports[`record integration tests > can record attribute mutation 1`] = `
           {
             \\"type\\": 2,
             \\"tagName\\": \\"html\\",
-            \\"attributes\\": {},
+            \\"attributes\\": {
+              \\"lang\\": \\"en\\"
+            },
             \\"childNodes\\": [
               {
                 \\"type\\": 2,
                 \\"tagName\\": \\"head\\",
                 \\"attributes\\": {},
-                \\"childNodes\\": [],
-                \\"id\\": 4
-              },
-              {
-                \\"type\\": 2,
-                \\"tagName\\": \\"body\\",
-                \\"attributes\\": {},
                 \\"childNodes\\": [
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\",
-                    \\"id\\": 6
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 5
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"p\\",
-                    \\"attributes\\": {},
-                    \\"childNodes\\": [
-                      {
-                        \\"type\\": 3,
-                        \\"textContent\\": \\"mutation observer\\",
-                        \\"id\\": 8
-                      }
-                    ],
+                    \\"tagName\\": \\"meta\\",
+                    \\"attributes\\": {
+                      \\"charset\\": \\"UTF-8\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 6
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
                     \\"id\\": 7
                   },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"meta\\",
+                    \\"attributes\\": {
+                      \\"name\\": \\"viewport\\",
+                      \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 8
+                  },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"textContent\\": \\"\\\\n    \\",
                     \\"id\\": 9
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"ul\\",
+                    \\"tagName\\": \\"title\\",
                     \\"attributes\\": {},
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"\\\\n    \\",
+                        \\"textContent\\": \\"style\\",
                         \\"id\\": 11
-                      },
-                      {
-                        \\"type\\": 2,
-                        \\"tagName\\": \\"li\\",
-                        \\"attributes\\": {},
-                        \\"childNodes\\": [],
-                        \\"id\\": 12
-                      },
-                      {
-                        \\"type\\": 3,
-                        \\"textContent\\": \\"\\\\n  \\",
-                        \\"id\\": 13
                       }
                     ],
                     \\"id\\": 10
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\",
-                    \\"id\\": 14
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 12
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"canvas\\",
-                    \\"attributes\\": {},
-                    \\"childNodes\\": [],
-                    \\"id\\": 15
+                    \\"tagName\\": \\"style\\",
+                    \\"attributes\\": {
+                      \\"id\\": \\"dual-textContent\\",
+                      \\"_cssText\\": \\"body { background-color: black; }/* rr_split */body { color: orange !important; }\\"
+                    },
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"\\",
+                        \\"id\\": 14
+                      },
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"\\",
+                        \\"id\\": 15
+                      }
+                    ],
+                    \\"id\\": 13
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n\\\\n    \\",
+                    \\"textContent\\": \\"\\\\n    \\",
                     \\"id\\": 16
                   },
                   {
@@ -1162,182 +1170,121 @@ exports[`record integration tests > can record attribute mutation 1`] = `
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\",
+                    \\"textContent\\": \\"\\\\n    \\",
                     \\"id\\": 19
-                  }
-                ],
-                \\"id\\": 5
-              }
-            ],
-            \\"id\\": 3
-          }
-        ],
-        \\"id\\": 1
-      },
-      \\"initialOffset\\": {
-        \\"left\\": 0,
-        \\"top\\": 0
-      }
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 0,
-      \\"texts\\": [],
-      \\"attributes\\": [
-        {
-          \\"id\\": 5,
-          \\"attributes\\": {
-            \\"test\\": \\"true\\"
-          }
-        }
-      ],
-      \\"removes\\": [
-        {
-          \\"parentId\\": 5,
-          \\"id\\": 10
-        }
-      ],
-      \\"adds\\": []
-    }
-  }
-]"
-`;
-
-exports[`record integration tests > can record character data muatations 1`] = `
-"[
-  {
-    \\"type\\": 0,
-    \\"data\\": {}
-  },
-  {
-    \\"type\\": 1,
-    \\"data\\": {}
-  },
-  {
-    \\"type\\": 4,
-    \\"data\\": {
-      \\"href\\": \\"about:blank\\",
-      \\"width\\": 1920,
-      \\"height\\": 1080
-    }
-  },
-  {
-    \\"type\\": 2,
-    \\"data\\": {
-      \\"node\\": {
-        \\"type\\": 0,
-        \\"childNodes\\": [
-          {
-            \\"type\\": 1,
-            \\"name\\": \\"html\\",
-            \\"publicId\\": \\"\\",
-            \\"systemId\\": \\"\\",
-            \\"id\\": 2
-          },
-          {
-            \\"type\\": 2,
-            \\"tagName\\": \\"html\\",
-            \\"attributes\\": {},
-            \\"childNodes\\": [
-              {
-                \\"type\\": 2,
-                \\"tagName\\": \\"head\\",
-                \\"attributes\\": {},
-                \\"childNodes\\": [],
-                \\"id\\": 4
-              },
-              {
-                \\"type\\": 2,
-                \\"tagName\\": \\"body\\",
-                \\"attributes\\": {},
-                \\"childNodes\\": [
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\",
-                    \\"id\\": 6
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"p\\",
-                    \\"attributes\\": {},
+                    \\"tagName\\": \\"style\\",
+                    \\"attributes\\": {
+                      \\"id\\": \\"single-textContent\\",
+                      \\"_cssText\\": \\"a:hover { outline: red solid 1px; }\\"
+                    },
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"mutation observer\\",
-                        \\"id\\": 8
+                        \\"textContent\\": \\"\\",
+                        \\"id\\": 21
                       }
                     ],
-                    \\"id\\": 7
+                    \\"id\\": 20
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\",
-                    \\"id\\": 9
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 22
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"ul\\",
-                    \\"attributes\\": {},
-                    \\"childNodes\\": [
-                      {
-                        \\"type\\": 3,
-                        \\"textContent\\": \\"\\\\n    \\",
-                        \\"id\\": 11
-                      },
-                      {
-                        \\"type\\": 2,
-                        \\"tagName\\": \\"li\\",
-                        \\"attributes\\": {},
-                        \\"childNodes\\": [],
-                        \\"id\\": 12
-                      },
-                      {
-                        \\"type\\": 3,
-                        \\"textContent\\": \\"\\\\n  \\",
-                        \\"id\\": 13
-                      }
-                    ],
-                    \\"id\\": 10
+                    \\"tagName\\": \\"style\\",
+                    \\"attributes\\": {
+                      \\"id\\": \\"empty\\",
+                      \\"_cssText\\": \\"a:hover { outline: blue solid 1px; }\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 23
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\",
-                    \\"id\\": 14
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 24
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"canvas\\",
+                    \\"tagName\\": \\"script\\",
                     \\"attributes\\": {},
-                    \\"childNodes\\": [],
-                    \\"id\\": 15
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 26
+                      }
+                    ],
+                    \\"id\\": 25
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n\\\\n    \\",
-                    \\"id\\": 16
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 27
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"script\\",
-                    \\"attributes\\": {},
+                    \\"tagName\\": \\"style\\",
+                    \\"attributes\\": {
+                      \\"id\\": \\"hover-mutation\\"
+                    },
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
-                        \\"id\\": 18
+                        \\"textContent\\": \\"\\\\n      /* replaceme */\\\\n    \\",
+                        \\"id\\": 29
                       }
                     ],
-                    \\"id\\": 17
+                    \\"id\\": 28
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\",
-                    \\"id\\": 19
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 30
                   }
                 ],
-                \\"id\\": 5
+                \\"id\\": 4
+              },
+              {
+                \\"type\\": 3,
+                \\"textContent\\": \\"\\\\n  \\",
+                \\"id\\": 31
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\\\n    \\",
+                    \\"id\\": 33
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 35
+                      }
+                    ],
+                    \\"id\\": 34
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\\\n\\",
+                    \\"id\\": 36
+                  }
+                ],
+                \\"id\\": 32
               }
             ],
             \\"id\\": 3
@@ -1357,33 +1304,114 @@ exports[`record integration tests > can record character data muatations 1`] = `
       \\"source\\": 0,
       \\"texts\\": [],
       \\"attributes\\": [],
-      \\"removes\\": [
+      \\"removes\\": [],
+      \\"adds\\": [
         {
-          \\"parentId\\": 5,
-          \\"id\\": 10
+          \\"parentId\\": 13,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 3,
+            \\"textContent\\": \\".absolutify { background-image: url(\\\\\\"http://localhost:3030/rel\\\\\\"); }\\",
+            \\"id\\": 37
+          }
         },
         {
-          \\"parentId\\": 7,
-          \\"id\\": 8
+          \\"parentId\\": 13,
+          \\"nextId\\": 37,
+          \\"node\\": {
+            \\"type\\": 3,
+            \\"textContent\\": \\"body { background-color: darkgreen; }\\",
+            \\"id\\": 38
+          }
+        }
+      ]
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [
+        {
+          \\"id\\": 15,
+          \\"value\\": \\"body { color: yellow; }\\"
+        },
+        {
+          \\"id\\": 15,
+          \\"value\\": \\"body { color: yellow; }\\"
+        },
+        {
+          \\"id\\": 38,
+          \\"value\\": \\"body { background-color: purple; }\\"
+        }
+      ],
+      \\"attributes\\": [],
+      \\"removes\\": [],
+      \\"adds\\": []
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [
+        {
+          \\"id\\": 14,
+          \\"value\\": \\"\\\\n      body { background-color: black !important; }\\\\n    \\"
+        },
+        {
+          \\"id\\": 29,
+          \\"value\\": \\"a:hover { outline: cyan solid 1px; }\\"
         }
       ],
+      \\"attributes\\": [],
+      \\"removes\\": [],
       \\"adds\\": [
         {
-          \\"parentId\\": 7,
+          \\"parentId\\": 32,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"style\\",
+            \\"attributes\\": {
+              \\"id\\": \\"goldilocks\\",
+              \\"_cssText\\": \\"body { color: brown; }\\"
+            },
+            \\"childNodes\\": [],
+            \\"id\\": 39
+          }
+        },
+        {
+          \\"parentId\\": 39,
           \\"nextId\\": null,
           \\"node\\": {
             \\"type\\": 3,
-            \\"textContent\\": \\"mutated\\",
-            \\"id\\": 20
+            \\"textContent\\": \\"\\",
+            \\"id\\": 40
           }
         }
       ]
     }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [
+        {
+          \\"id\\": 40,
+          \\"value\\": \\"body { color: gold }\\"
+        }
+      ],
+      \\"attributes\\": [],
+      \\"removes\\": [],
+      \\"adds\\": []
+    }
   }
 ]"
 `;
 
-exports[`record integration tests > can record childList mutations 1`] = `
+exports[`record integration tests > can record and replay textarea mutations correctly 1`] = `
 "[
   {
     \\"type\\": 0,
@@ -1417,64 +1445,58 @@ exports[`record integration tests > can record childList mutations 1`] = `
           {
             \\"type\\": 2,
             \\"tagName\\": \\"html\\",
-            \\"attributes\\": {},
+            \\"attributes\\": {
+              \\"lang\\": \\"en\\"
+            },
             \\"childNodes\\": [
               {
                 \\"type\\": 2,
                 \\"tagName\\": \\"head\\",
                 \\"attributes\\": {},
-                \\"childNodes\\": [],
-                \\"id\\": 4
-              },
-              {
-                \\"type\\": 2,
-                \\"tagName\\": \\"body\\",
-                \\"attributes\\": {},
                 \\"childNodes\\": [
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\",
-                    \\"id\\": 6
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 5
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"p\\",
-                    \\"attributes\\": {},
-                    \\"childNodes\\": [
-                      {
-                        \\"type\\": 3,
-                        \\"textContent\\": \\"mutation observer\\",
-                        \\"id\\": 8
-                      }
-                    ],
+                    \\"tagName\\": \\"meta\\",
+                    \\"attributes\\": {
+                      \\"charset\\": \\"UTF-8\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 6
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
                     \\"id\\": 7
                   },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"meta\\",
+                    \\"attributes\\": {
+                      \\"name\\": \\"viewport\\",
+                      \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 8
+                  },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"textContent\\": \\"\\\\n    \\",
                     \\"id\\": 9
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"ul\\",
+                    \\"tagName\\": \\"title\\",
                     \\"attributes\\": {},
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"\\\\n    \\",
+                        \\"textContent\\": \\"Empty\\",
                         \\"id\\": 11
-                      },
-                      {
-                        \\"type\\": 2,
-                        \\"tagName\\": \\"li\\",
-                        \\"attributes\\": {},
-                        \\"childNodes\\": [],
-                        \\"id\\": 12
-                      },
-                      {
-                        \\"type\\": 3,
-                        \\"textContent\\": \\"\\\\n  \\",
-                        \\"id\\": 13
                       }
                     ],
                     \\"id\\": 10
@@ -1482,19 +1504,39 @@ exports[`record integration tests > can record childList mutations 1`] = `
                   {
                     \\"type\\": 3,
                     \\"textContent\\": \\"\\\\n  \\",
-                    \\"id\\": 14
+                    \\"id\\": 12
+                  }
+                ],
+                \\"id\\": 4
+              },
+              {
+                \\"type\\": 3,
+                \\"textContent\\": \\"\\\\n  \\",
+                \\"id\\": 13
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 15
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"canvas\\",
-                    \\"attributes\\": {},
+                    \\"tagName\\": \\"div\\",
+                    \\"attributes\\": {
+                      \\"id\\": \\"one\\"
+                    },
                     \\"childNodes\\": [],
-                    \\"id\\": 15
+                    \\"id\\": 16
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n\\\\n    \\",
-                    \\"id\\": 16
+                    \\"textContent\\": \\"\\\\n  \\\\n    \\",
+                    \\"id\\": 17
                   },
                   {
                     \\"type\\": 2,
@@ -1504,18 +1546,18 @@ exports[`record integration tests > can record childList mutations 1`] = `
                       {
                         \\"type\\": 3,
                         \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
-                        \\"id\\": 18
+                        \\"id\\": 19
                       }
                     ],
-                    \\"id\\": 17
+                    \\"id\\": 18
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\",
-                    \\"id\\": 19
+                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\\\n\\",
+                    \\"id\\": 20
                   }
                 ],
-                \\"id\\": 5
+                \\"id\\": 14
               }
             ],
             \\"id\\": 3
@@ -1535,34 +1577,132 @@ exports[`record integration tests > can record childList mutations 1`] = `
       \\"source\\": 0,
       \\"texts\\": [],
       \\"attributes\\": [],
-      \\"removes\\": [
-        {
-          \\"parentId\\": 5,
-          \\"id\\": 10
-        }
-      ],
+      \\"removes\\": [],
       \\"adds\\": [
         {
-          \\"parentId\\": 7,
+          \\"parentId\\": 14,
           \\"nextId\\": null,
           \\"node\\": {
             \\"type\\": 2,
-            \\"tagName\\": \\"span\\",
-            \\"attributes\\": {},
+            \\"tagName\\": \\"textarea\\",
+            \\"attributes\\": {
+              \\"id\\": \\"ta2\\"
+            },
             \\"childNodes\\": [],
-            \\"id\\": 20
+            \\"id\\": 21
+          }
+        },
+        {
+          \\"parentId\\": 14,
+          \\"nextId\\": 21,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"textarea\\",
+            \\"attributes\\": {
+              \\"value\\": \\"pre value\\"
+            },
+            \\"childNodes\\": [],
+            \\"id\\": 22
           }
         }
       ]
     }
-  }
-]"
-`;
-
-exports[`record integration tests > can record clicks 1`] = `
-"[
+  },
   {
-    \\"type\\": 0,
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 22,
+          \\"attributes\\": {
+            \\"value\\": \\"ok\\"
+          }
+        },
+        {
+          \\"id\\": 21,
+          \\"attributes\\": {
+            \\"value\\": \\"added\\"
+          }
+        }
+      ],
+      \\"removes\\": [],
+      \\"adds\\": []
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 22,
+          \\"attributes\\": {
+            \\"value\\": \\"ok3\\"
+          }
+        }
+      ],
+      \\"removes\\": [
+        {
+          \\"parentId\\": 14,
+          \\"id\\": 21
+        }
+      ],
+      \\"adds\\": []
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 2,
+      \\"type\\": 5,
+      \\"id\\": 22
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 5,
+      \\"text\\": \\"1ok3\\",
+      \\"isChecked\\": false,
+      \\"id\\": 22
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 22,
+          \\"attributes\\": {
+            \\"value\\": \\"ignore\\"
+          }
+        }
+      ],
+      \\"removes\\": [],
+      \\"adds\\": []
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 5,
+      \\"text\\": \\"12ok3\\",
+      \\"isChecked\\": false,
+      \\"id\\": 22
+    }
+  }
+]"
+`;
+
+exports[`record integration tests > can record attribute mutation 1`] = `
+"[
+  {
+    \\"type\\": 0,
     \\"data\\": {}
   },
   {
@@ -1593,139 +1733,84 @@ exports[`record integration tests > can record clicks 1`] = `
           {
             \\"type\\": 2,
             \\"tagName\\": \\"html\\",
-            \\"attributes\\": {
-              \\"lang\\": \\"en\\"
-            },
+            \\"attributes\\": {},
             \\"childNodes\\": [
               {
                 \\"type\\": 2,
                 \\"tagName\\": \\"head\\",
                 \\"attributes\\": {},
+                \\"childNodes\\": [],
+                \\"id\\": 4
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
                 \\"childNodes\\": [
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 5
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"meta\\",
-                    \\"attributes\\": {
-                      \\"charset\\": \\"UTF-8\\"
-                    },
-                    \\"childNodes\\": [],
+                    \\"textContent\\": \\"\\\\n  \\",
                     \\"id\\": 6
                   },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 7
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"meta\\",
-                    \\"attributes\\": {
-                      \\"name\\": \\"viewport\\",
-                      \\"content\\": \\"width=device-width, initial-scale=1.0\\"
-                    },
-                    \\"childNodes\\": [],
-                    \\"id\\": 8
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 9
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"meta\\",
-                    \\"attributes\\": {
-                      \\"http-equiv\\": \\"X-UA-Compatible\\",
-                      \\"content\\": \\"ie=edge\\"
-                    },
-                    \\"childNodes\\": [],
-                    \\"id\\": 10
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 11
-                  },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"title\\",
+                    \\"tagName\\": \\"p\\",
                     \\"attributes\\": {},
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"Link click\\",
-                        \\"id\\": 13
+                        \\"textContent\\": \\"mutation observer\\",
+                        \\"id\\": 8
                       }
                     ],
-                    \\"id\\": 12
+                    \\"id\\": 7
                   },
                   {
                     \\"type\\": 3,
                     \\"textContent\\": \\"\\\\n  \\",
-                    \\"id\\": 14
-                  }
-                ],
-                \\"id\\": 4
-              },
-              {
-                \\"type\\": 3,
-                \\"textContent\\": \\"\\\\n\\\\n  \\",
-                \\"id\\": 15
-              },
-              {
-                \\"type\\": 2,
-                \\"tagName\\": \\"body\\",
-                \\"attributes\\": {},
-                \\"childNodes\\": [
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 17
+                    \\"id\\": 9
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"span\\",
-                    \\"attributes\\": {
-                      \\"id\\": \\"not-a-link\\"
-                    },
+                    \\"tagName\\": \\"ul\\",
+                    \\"attributes\\": {},
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"not link\\",
-                        \\"id\\": 19
+                        \\"textContent\\": \\"\\\\n    \\",
+                        \\"id\\": 11
+                      },
+                      {
+                        \\"type\\": 2,
+                        \\"tagName\\": \\"li\\",
+                        \\"attributes\\": {},
+                        \\"childNodes\\": [],
+                        \\"id\\": 12
+                      },
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"\\\\n  \\",
+                        \\"id\\": 13
                       }
                     ],
-                    \\"id\\": 18
+                    \\"id\\": 10
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 20
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 14
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"a\\",
-                    \\"attributes\\": {
-                      \\"href\\": \\"about:blank#clicked\\"
-                    },
-                    \\"childNodes\\": [
-                      {
-                        \\"type\\": 3,
-                        \\"textContent\\": \\"link\\",
-                        \\"id\\": 22
-                      }
-                    ],
-                    \\"id\\": 21
+                    \\"tagName\\": \\"canvas\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [],
+                    \\"id\\": 15
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\\\n    \\",
-                    \\"id\\": 23
+                    \\"textContent\\": \\"\\\\n\\\\n    \\",
+                    \\"id\\": 16
                   },
                   {
                     \\"type\\": 2,
@@ -1735,18 +1820,18 @@ exports[`record integration tests > can record clicks 1`] = `
                       {
                         \\"type\\": 3,
                         \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
-                        \\"id\\": 25
+                        \\"id\\": 18
                       }
                     ],
-                    \\"id\\": 24
+                    \\"id\\": 17
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\\\n\\",
-                    \\"id\\": 26
+                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\",
+                    \\"id\\": 19
                   }
                 ],
-                \\"id\\": 16
+                \\"id\\": 5
               }
             ],
             \\"id\\": 3
@@ -1763,90 +1848,29 @@ exports[`record integration tests > can record clicks 1`] = `
   {
     \\"type\\": 3,
     \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 1,
-      \\"id\\": 18
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 0,
-      \\"id\\": 18
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 2,
-      \\"id\\": 18,
-      \\"pointerType\\": 0
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 7,
-      \\"id\\": 18
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 9,
-      \\"id\\": 18
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 2,
-      \\"id\\": 18,
-      \\"pointerType\\": 2
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 1,
-      \\"id\\": 21
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 5,
-      \\"id\\": 21
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 0,
-      \\"id\\": 21
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 2,
-      \\"id\\": 21,
-      \\"pointerType\\": 0
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 5,
+          \\"attributes\\": {
+            \\"test\\": \\"true\\"
+          }
+        }
+      ],
+      \\"removes\\": [
+        {
+          \\"parentId\\": 5,
+          \\"id\\": 10
+        }
+      ],
+      \\"adds\\": []
     }
   }
 ]"
 `;
 
-exports[`record integration tests > can record form interactions 1`] = `
+exports[`record integration tests > can record character data muatations 1`] = `
 "[
   {
     \\"type\\": 0,
@@ -1880,81 +1904,722 @@ exports[`record integration tests > can record form interactions 1`] = `
           {
             \\"type\\": 2,
             \\"tagName\\": \\"html\\",
-            \\"attributes\\": {
-              \\"lang\\": \\"en\\"
-            },
+            \\"attributes\\": {},
             \\"childNodes\\": [
               {
                 \\"type\\": 2,
                 \\"tagName\\": \\"head\\",
                 \\"attributes\\": {},
+                \\"childNodes\\": [],
+                \\"id\\": 4
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
                 \\"childNodes\\": [
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 5
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 6
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"meta\\",
-                    \\"attributes\\": {
-                      \\"charset\\": \\"UTF-8\\"
-                    },
-                    \\"childNodes\\": [],
-                    \\"id\\": 6
+                    \\"tagName\\": \\"p\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"mutation observer\\",
+                        \\"id\\": 8
+                      }
+                    ],
+                    \\"id\\": 7
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 7
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 9
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"meta\\",
-                    \\"attributes\\": {
-                      \\"name\\": \\"viewport\\",
-                      \\"content\\": \\"width=device-width, initial-scale=1.0\\"
-                    },
-                    \\"childNodes\\": [],
-                    \\"id\\": 8
+                    \\"tagName\\": \\"ul\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"\\\\n    \\",
+                        \\"id\\": 11
+                      },
+                      {
+                        \\"type\\": 2,
+                        \\"tagName\\": \\"li\\",
+                        \\"attributes\\": {},
+                        \\"childNodes\\": [],
+                        \\"id\\": 12
+                      },
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"\\\\n  \\",
+                        \\"id\\": 13
+                      }
+                    ],
+                    \\"id\\": 10
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 9
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 14
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"meta\\",
-                    \\"attributes\\": {
-                      \\"http-equiv\\": \\"X-UA-Compatible\\",
-                      \\"content\\": \\"ie=edge\\"
-                    },
+                    \\"tagName\\": \\"canvas\\",
+                    \\"attributes\\": {},
                     \\"childNodes\\": [],
-                    \\"id\\": 10
+                    \\"id\\": 15
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 11
+                    \\"textContent\\": \\"\\\\n\\\\n    \\",
+                    \\"id\\": 16
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"title\\",
+                    \\"tagName\\": \\"script\\",
                     \\"attributes\\": {},
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"form fields\\",
-                        \\"id\\": 13
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 18
                       }
                     ],
-                    \\"id\\": 12
+                    \\"id\\": 17
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\",
-                    \\"id\\": 14
+                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\",
+                    \\"id\\": 19
+                  }
+                ],
+                \\"id\\": 5
+              }
+            ],
+            \\"id\\": 3
+          }
+        ],
+        \\"id\\": 1
+      },
+      \\"initialOffset\\": {
+        \\"left\\": 0,
+        \\"top\\": 0
+      }
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [],
+      \\"removes\\": [
+        {
+          \\"parentId\\": 5,
+          \\"id\\": 10
+        },
+        {
+          \\"parentId\\": 7,
+          \\"id\\": 8
+        }
+      ],
+      \\"adds\\": [
+        {
+          \\"parentId\\": 7,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 3,
+            \\"textContent\\": \\"mutated\\",
+            \\"id\\": 20
+          }
+        }
+      ]
+    }
+  }
+]"
+`;
+
+exports[`record integration tests > can record childList mutations 1`] = `
+"[
+  {
+    \\"type\\": 0,
+    \\"data\\": {}
+  },
+  {
+    \\"type\\": 1,
+    \\"data\\": {}
+  },
+  {
+    \\"type\\": 4,
+    \\"data\\": {
+      \\"href\\": \\"about:blank\\",
+      \\"width\\": 1920,
+      \\"height\\": 1080
+    }
+  },
+  {
+    \\"type\\": 2,
+    \\"data\\": {
+      \\"node\\": {
+        \\"type\\": 0,
+        \\"childNodes\\": [
+          {
+            \\"type\\": 1,
+            \\"name\\": \\"html\\",
+            \\"publicId\\": \\"\\",
+            \\"systemId\\": \\"\\",
+            \\"id\\": 2
+          },
+          {
+            \\"type\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [],
+                \\"id\\": 4
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 6
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"p\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"mutation observer\\",
+                        \\"id\\": 8
+                      }
+                    ],
+                    \\"id\\": 7
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 9
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"ul\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"\\\\n    \\",
+                        \\"id\\": 11
+                      },
+                      {
+                        \\"type\\": 2,
+                        \\"tagName\\": \\"li\\",
+                        \\"attributes\\": {},
+                        \\"childNodes\\": [],
+                        \\"id\\": 12
+                      },
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"\\\\n  \\",
+                        \\"id\\": 13
+                      }
+                    ],
+                    \\"id\\": 10
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 14
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"canvas\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [],
+                    \\"id\\": 15
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n\\\\n    \\",
+                    \\"id\\": 16
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 18
+                      }
+                    ],
+                    \\"id\\": 17
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\",
+                    \\"id\\": 19
+                  }
+                ],
+                \\"id\\": 5
+              }
+            ],
+            \\"id\\": 3
+          }
+        ],
+        \\"id\\": 1
+      },
+      \\"initialOffset\\": {
+        \\"left\\": 0,
+        \\"top\\": 0
+      }
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [],
+      \\"removes\\": [
+        {
+          \\"parentId\\": 5,
+          \\"id\\": 10
+        }
+      ],
+      \\"adds\\": [
+        {
+          \\"parentId\\": 7,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"span\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [],
+            \\"id\\": 20
+          }
+        }
+      ]
+    }
+  }
+]"
+`;
+
+exports[`record integration tests > can record clicks 1`] = `
+"[
+  {
+    \\"type\\": 0,
+    \\"data\\": {}
+  },
+  {
+    \\"type\\": 1,
+    \\"data\\": {}
+  },
+  {
+    \\"type\\": 4,
+    \\"data\\": {
+      \\"href\\": \\"about:blank\\",
+      \\"width\\": 1920,
+      \\"height\\": 1080
+    }
+  },
+  {
+    \\"type\\": 2,
+    \\"data\\": {
+      \\"node\\": {
+        \\"type\\": 0,
+        \\"childNodes\\": [
+          {
+            \\"type\\": 1,
+            \\"name\\": \\"html\\",
+            \\"publicId\\": \\"\\",
+            \\"systemId\\": \\"\\",
+            \\"id\\": 2
+          },
+          {
+            \\"type\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {
+              \\"lang\\": \\"en\\"
+            },
+            \\"childNodes\\": [
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 5
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"meta\\",
+                    \\"attributes\\": {
+                      \\"charset\\": \\"UTF-8\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 6
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 7
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"meta\\",
+                    \\"attributes\\": {
+                      \\"name\\": \\"viewport\\",
+                      \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 8
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 9
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"meta\\",
+                    \\"attributes\\": {
+                      \\"http-equiv\\": \\"X-UA-Compatible\\",
+                      \\"content\\": \\"ie=edge\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 10
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 11
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"title\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"Link click\\",
+                        \\"id\\": 13
+                      }
+                    ],
+                    \\"id\\": 12
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 14
+                  }
+                ],
+                \\"id\\": 4
+              },
+              {
+                \\"type\\": 3,
+                \\"textContent\\": \\"\\\\n\\\\n  \\",
+                \\"id\\": 15
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 17
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"span\\",
+                    \\"attributes\\": {
+                      \\"id\\": \\"not-a-link\\"
+                    },
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"not link\\",
+                        \\"id\\": 19
+                      }
+                    ],
+                    \\"id\\": 18
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 20
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"a\\",
+                    \\"attributes\\": {
+                      \\"href\\": \\"about:blank#clicked\\"
+                    },
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"link\\",
+                        \\"id\\": 22
+                      }
+                    ],
+                    \\"id\\": 21
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\\\n    \\",
+                    \\"id\\": 23
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 25
+                      }
+                    ],
+                    \\"id\\": 24
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\\\n\\",
+                    \\"id\\": 26
+                  }
+                ],
+                \\"id\\": 16
+              }
+            ],
+            \\"id\\": 3
+          }
+        ],
+        \\"id\\": 1
+      },
+      \\"initialOffset\\": {
+        \\"left\\": 0,
+        \\"top\\": 0
+      }
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 2,
+      \\"type\\": 1,
+      \\"id\\": 18
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 2,
+      \\"type\\": 0,
+      \\"id\\": 18
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 2,
+      \\"type\\": 2,
+      \\"id\\": 18,
+      \\"pointerType\\": 0
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 2,
+      \\"type\\": 7,
+      \\"id\\": 18
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 2,
+      \\"type\\": 9,
+      \\"id\\": 18
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 2,
+      \\"type\\": 2,
+      \\"id\\": 18,
+      \\"pointerType\\": 2
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 2,
+      \\"type\\": 1,
+      \\"id\\": 21
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 2,
+      \\"type\\": 5,
+      \\"id\\": 21
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 2,
+      \\"type\\": 0,
+      \\"id\\": 21
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 2,
+      \\"type\\": 2,
+      \\"id\\": 21,
+      \\"pointerType\\": 0
+    }
+  }
+]"
+`;
+
+exports[`record integration tests > can record form interactions 1`] = `
+"[
+  {
+    \\"type\\": 0,
+    \\"data\\": {}
+  },
+  {
+    \\"type\\": 1,
+    \\"data\\": {}
+  },
+  {
+    \\"type\\": 4,
+    \\"data\\": {
+      \\"href\\": \\"about:blank\\",
+      \\"width\\": 1920,
+      \\"height\\": 1080
+    }
+  },
+  {
+    \\"type\\": 2,
+    \\"data\\": {
+      \\"node\\": {
+        \\"type\\": 0,
+        \\"childNodes\\": [
+          {
+            \\"type\\": 1,
+            \\"name\\": \\"html\\",
+            \\"publicId\\": \\"\\",
+            \\"systemId\\": \\"\\",
+            \\"id\\": 2
+          },
+          {
+            \\"type\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {
+              \\"lang\\": \\"en\\"
+            },
+            \\"childNodes\\": [
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 5
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"meta\\",
+                    \\"attributes\\": {
+                      \\"charset\\": \\"UTF-8\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 6
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 7
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"meta\\",
+                    \\"attributes\\": {
+                      \\"name\\": \\"viewport\\",
+                      \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 8
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 9
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"meta\\",
+                    \\"attributes\\": {
+                      \\"http-equiv\\": \\"X-UA-Compatible\\",
+                      \\"content\\": \\"ie=edge\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 10
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 11
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"title\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"form fields\\",
+                        \\"id\\": 13
+                      }
+                    ],
+                    \\"id\\": 12
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 14
                   }
                 ],
                 \\"id\\": 4
@@ -3494,317 +4159,89 @@ exports[`record integration tests > can record node mutations 1`] = `
             \\"tagName\\": \\"li\\",
             \\"attributes\\": {
               \\"class\\": \\"select2-results-dept-0 select2-result select2-result-selectable select2-highlighted\\",
-              \\"role\\": \\"presentation\\"
-            },
-            \\"childNodes\\": [],
-            \\"id\\": 72
-          }
-        },
-        {
-          \\"parentId\\": 72,
-          \\"nextId\\": null,
-          \\"node\\": {
-            \\"type\\": 2,
-            \\"tagName\\": \\"div\\",
-            \\"attributes\\": {
-              \\"class\\": \\"select2-result-label\\",
-              \\"id\\": \\"select2-result-label-2\\",
-              \\"role\\": \\"option\\"
-            },
-            \\"childNodes\\": [],
-            \\"id\\": 73
-          }
-        },
-        {
-          \\"parentId\\": 73,
-          \\"nextId\\": null,
-          \\"node\\": {
-            \\"type\\": 3,
-            \\"textContent\\": \\"A\\",
-            \\"id\\": 74
-          }
-        },
-        {
-          \\"parentId\\": 73,
-          \\"nextId\\": 74,
-          \\"node\\": {
-            \\"type\\": 2,
-            \\"tagName\\": \\"span\\",
-            \\"attributes\\": {
-              \\"class\\": \\"select2-match\\"
-            },
-            \\"childNodes\\": [],
-            \\"id\\": 75
-          }
-        },
-        {
-          \\"parentId\\": 68,
-          \\"nextId\\": 69,
-          \\"node\\": {
-            \\"type\\": 2,
-            \\"tagName\\": \\"span\\",
-            \\"attributes\\": {
-              \\"class\\": \\"select2-match\\"
-            },
-            \\"childNodes\\": [],
-            \\"id\\": 76
-          }
-        }
-      ]
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 5,
-      \\"text\\": \\"\\",
-      \\"isChecked\\": false,
-      \\"id\\": 42
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 5,
-      \\"text\\": \\"\\",
-      \\"isChecked\\": false,
-      \\"id\\": 35
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 0,
-      \\"id\\": 70
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 0,
-      \\"texts\\": [],
-      \\"attributes\\": [
-        {
-          \\"id\\": 36,
-          \\"attributes\\": {
-            \\"style\\": {
-              \\"color\\": [
-                \\"black\\",
-                \\"important\\"
-              ]
-            }
-          }
-        }
-      ],
-      \\"removes\\": [],
-      \\"adds\\": []
-    }
-  }
-]"
-`;
-
-exports[`record integration tests > can record style changes compactly and preserve css var() functions 1`] = `
-"[
-  {
-    \\"type\\": 0,
-    \\"data\\": {}
-  },
-  {
-    \\"type\\": 1,
-    \\"data\\": {}
-  },
-  {
-    \\"type\\": 4,
-    \\"data\\": {
-      \\"href\\": \\"about:blank\\",
-      \\"width\\": 1920,
-      \\"height\\": 1080
-    }
-  },
-  {
-    \\"type\\": 2,
-    \\"data\\": {
-      \\"node\\": {
-        \\"type\\": 0,
-        \\"childNodes\\": [
-          {
-            \\"type\\": 2,
-            \\"tagName\\": \\"html\\",
-            \\"attributes\\": {},
-            \\"childNodes\\": [
-              {
-                \\"type\\": 2,
-                \\"tagName\\": \\"head\\",
-                \\"attributes\\": {},
-                \\"childNodes\\": [],
-                \\"id\\": 3
-              },
-              {
-                \\"type\\": 2,
-                \\"tagName\\": \\"body\\",
-                \\"attributes\\": {},
-                \\"childNodes\\": [
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 5
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"script\\",
-                    \\"attributes\\": {},
-                    \\"childNodes\\": [
-                      {
-                        \\"type\\": 3,
-                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
-                        \\"id\\": 7
-                      }
-                    ],
-                    \\"id\\": 6
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\\\n\\",
-                    \\"id\\": 8
-                  }
-                ],
-                \\"id\\": 4
-              }
-            ],
-            \\"id\\": 2
-          }
-        ],
-        \\"compatMode\\": \\"BackCompat\\",
-        \\"id\\": 1
-      },
-      \\"initialOffset\\": {
-        \\"left\\": 0,
-        \\"top\\": 0
-      }
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 0,
-      \\"texts\\": [],
-      \\"attributes\\": [
+              \\"role\\": \\"presentation\\"
+            },
+            \\"childNodes\\": [],
+            \\"id\\": 72
+          }
+        },
         {
-          \\"id\\": 4,
-          \\"attributes\\": {
-            \\"style\\": \\"background: var(--mystery)\\"
+          \\"parentId\\": 72,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"div\\",
+            \\"attributes\\": {
+              \\"class\\": \\"select2-result-label\\",
+              \\"id\\": \\"select2-result-label-2\\",
+              \\"role\\": \\"option\\"
+            },
+            \\"childNodes\\": [],
+            \\"id\\": 73
           }
-        }
-      ],
-      \\"removes\\": [],
-      \\"adds\\": []
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 0,
-      \\"texts\\": [],
-      \\"attributes\\": [
+        },
         {
-          \\"id\\": 4,
-          \\"attributes\\": {
-            \\"style\\": \\"background: var(--mystery); background-color: black\\"
+          \\"parentId\\": 73,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 3,
+            \\"textContent\\": \\"A\\",
+            \\"id\\": 74
           }
-        }
-      ],
-      \\"removes\\": [],
-      \\"adds\\": []
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 0,
-      \\"texts\\": [],
-      \\"attributes\\": [
+        },
         {
-          \\"id\\": 4,
-          \\"attributes\\": {
-            \\"style\\": \\"\\"
+          \\"parentId\\": 73,
+          \\"nextId\\": 74,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"span\\",
+            \\"attributes\\": {
+              \\"class\\": \\"select2-match\\"
+            },
+            \\"childNodes\\": [],
+            \\"id\\": 75
           }
-        }
-      ],
-      \\"removes\\": [],
-      \\"adds\\": []
-    }
-  },
-  {
-    \\"type\\": 3,
-    \\"data\\": {
-      \\"source\\": 0,
-      \\"texts\\": [],
-      \\"attributes\\": [
+        },
         {
-          \\"id\\": 4,
-          \\"attributes\\": {
-            \\"style\\": \\"display:block\\"
+          \\"parentId\\": 68,
+          \\"nextId\\": 69,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"span\\",
+            \\"attributes\\": {
+              \\"class\\": \\"select2-match\\"
+            },
+            \\"childNodes\\": [],
+            \\"id\\": 76
           }
         }
-      ],
-      \\"removes\\": [],
-      \\"adds\\": []
+      ]
     }
   },
   {
     \\"type\\": 3,
     \\"data\\": {
-      \\"source\\": 0,
-      \\"texts\\": [],
-      \\"attributes\\": [
-        {
-          \\"id\\": 4,
-          \\"attributes\\": {
-            \\"style\\": {
-              \\"color\\": \\"var(--mystery-color)\\"
-            }
-          }
-        }
-      ],
-      \\"removes\\": [],
-      \\"adds\\": []
+      \\"source\\": 5,
+      \\"text\\": \\"\\",
+      \\"isChecked\\": false,
+      \\"id\\": 42
     }
   },
   {
     \\"type\\": 3,
     \\"data\\": {
-      \\"source\\": 0,
-      \\"texts\\": [],
-      \\"attributes\\": [
-        {
-          \\"id\\": 4,
-          \\"attributes\\": {
-            \\"style\\": \\"color:var(--mystery-color);display:block;margin:10px\\"
-          }
-        }
-      ],
-      \\"removes\\": [],
-      \\"adds\\": []
+      \\"source\\": 5,
+      \\"text\\": \\"\\",
+      \\"isChecked\\": false,
+      \\"id\\": 35
     }
   },
   {
     \\"type\\": 3,
     \\"data\\": {
-      \\"source\\": 0,
-      \\"texts\\": [],
-      \\"attributes\\": [
-        {
-          \\"id\\": 4,
-          \\"attributes\\": {
-            \\"style\\": {
-              \\"margin-left\\": \\"Npx\\"
-            }
-          }
-        }
-      ],
-      \\"removes\\": [],
-      \\"adds\\": []
+      \\"source\\": 2,
+      \\"type\\": 0,
+      \\"id\\": 70
     }
   },
   {
@@ -3814,11 +4251,13 @@ exports[`record integration tests > can record style changes compactly and prese
       \\"texts\\": [],
       \\"attributes\\": [
         {
-          \\"id\\": 4,
+          \\"id\\": 36,
           \\"attributes\\": {
             \\"style\\": {
-              \\"margin-top\\": \\"Npx\\",
-              \\"color\\": false
+              \\"color\\": [
+                \\"black\\",
+                \\"important\\"
+              ]
             }
           }
         }
@@ -3830,108 +4269,41 @@ exports[`record integration tests > can record style changes compactly and prese
 ]"
 `;
 
-exports[`record integration tests > can record textarea mutations correctly 1`] = `
-"[
-  {
-    \\"type\\": 0,
-    \\"data\\": {}
-  },
-  {
-    \\"type\\": 1,
-    \\"data\\": {}
-  },
-  {
-    \\"type\\": 4,
-    \\"data\\": {
-      \\"href\\": \\"about:blank\\",
-      \\"width\\": 1920,
-      \\"height\\": 1080
-    }
-  },
-  {
-    \\"type\\": 2,
-    \\"data\\": {
-      \\"node\\": {
-        \\"type\\": 0,
-        \\"childNodes\\": [
-          {
-            \\"type\\": 1,
-            \\"name\\": \\"html\\",
-            \\"publicId\\": \\"\\",
-            \\"systemId\\": \\"\\",
-            \\"id\\": 2
-          },
-          {
-            \\"type\\": 2,
-            \\"tagName\\": \\"html\\",
-            \\"attributes\\": {
-              \\"lang\\": \\"en\\"
-            },
-            \\"childNodes\\": [
-              {
-                \\"type\\": 2,
-                \\"tagName\\": \\"head\\",
-                \\"attributes\\": {},
-                \\"childNodes\\": [
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 5
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"meta\\",
-                    \\"attributes\\": {
-                      \\"charset\\": \\"UTF-8\\"
-                    },
-                    \\"childNodes\\": [],
-                    \\"id\\": 6
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 7
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"meta\\",
-                    \\"attributes\\": {
-                      \\"name\\": \\"viewport\\",
-                      \\"content\\": \\"width=device-width, initial-scale=1.0\\"
-                    },
-                    \\"childNodes\\": [],
-                    \\"id\\": 8
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 9
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"title\\",
-                    \\"attributes\\": {},
-                    \\"childNodes\\": [
-                      {
-                        \\"type\\": 3,
-                        \\"textContent\\": \\"Empty\\",
-                        \\"id\\": 11
-                      }
-                    ],
-                    \\"id\\": 10
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\",
-                    \\"id\\": 12
-                  }
-                ],
-                \\"id\\": 4
-              },
+exports[`record integration tests > can record style changes compactly and preserve css var() functions 1`] = `
+"[
+  {
+    \\"type\\": 0,
+    \\"data\\": {}
+  },
+  {
+    \\"type\\": 1,
+    \\"data\\": {}
+  },
+  {
+    \\"type\\": 4,
+    \\"data\\": {
+      \\"href\\": \\"about:blank\\",
+      \\"width\\": 1920,
+      \\"height\\": 1080
+    }
+  },
+  {
+    \\"type\\": 2,
+    \\"data\\": {
+      \\"node\\": {
+        \\"type\\": 0,
+        \\"childNodes\\": [
+          {
+            \\"type\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [
               {
-                \\"type\\": 3,
-                \\"textContent\\": \\"\\\\n  \\",
-                \\"id\\": 13
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [],
+                \\"id\\": 3
               },
               {
                 \\"type\\": 2,
@@ -3941,21 +4313,7 @@ exports[`record integration tests > can record textarea mutations correctly 1`]
                   {
                     \\"type\\": 3,
                     \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 15
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"div\\",
-                    \\"attributes\\": {
-                      \\"id\\": \\"one\\"
-                    },
-                    \\"childNodes\\": [],
-                    \\"id\\": 16
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\\\n    \\",
-                    \\"id\\": 17
+                    \\"id\\": 5
                   },
                   {
                     \\"type\\": 2,
@@ -3965,23 +4323,24 @@ exports[`record integration tests > can record textarea mutations correctly 1`]
                       {
                         \\"type\\": 3,
                         \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
-                        \\"id\\": 19
+                        \\"id\\": 7
                       }
                     ],
-                    \\"id\\": 18
+                    \\"id\\": 6
                   },
                   {
                     \\"type\\": 3,
                     \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\\\n\\",
-                    \\"id\\": 20
+                    \\"id\\": 8
                   }
                 ],
-                \\"id\\": 14
+                \\"id\\": 4
               }
             ],
-            \\"id\\": 3
+            \\"id\\": 2
           }
         ],
+        \\"compatMode\\": \\"BackCompat\\",
         \\"id\\": 1
       },
       \\"initialOffset\\": {
@@ -3995,23 +4354,33 @@ exports[`record integration tests > can record textarea mutations correctly 1`]
     \\"data\\": {
       \\"source\\": 0,
       \\"texts\\": [],
-      \\"attributes\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 4,
+          \\"attributes\\": {
+            \\"style\\": \\"background: var(--mystery)\\"
+          }
+        }
+      ],
       \\"removes\\": [],
-      \\"adds\\": [
+      \\"adds\\": []
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
         {
-          \\"parentId\\": 14,
-          \\"nextId\\": null,
-          \\"node\\": {
-            \\"type\\": 2,
-            \\"tagName\\": \\"textarea\\",
-            \\"attributes\\": {
-              \\"value\\": \\"pre value\\"
-            },
-            \\"childNodes\\": [],
-            \\"id\\": 21
+          \\"id\\": 4,
+          \\"attributes\\": {
+            \\"style\\": \\"background: var(--mystery); background-color: black\\"
           }
         }
-      ]
+      ],
+      \\"removes\\": [],
+      \\"adds\\": []
     }
   },
   {
@@ -4021,9 +4390,9 @@ exports[`record integration tests > can record textarea mutations correctly 1`]
       \\"texts\\": [],
       \\"attributes\\": [
         {
-          \\"id\\": 21,
+          \\"id\\": 4,
           \\"attributes\\": {
-            \\"value\\": \\"ok\\"
+            \\"style\\": \\"\\"
           }
         }
       ],
@@ -4038,9 +4407,9 @@ exports[`record integration tests > can record textarea mutations correctly 1`]
       \\"texts\\": [],
       \\"attributes\\": [
         {
-          \\"id\\": 21,
+          \\"id\\": 4,
           \\"attributes\\": {
-            \\"value\\": \\"ok3\\"
+            \\"style\\": \\"display:block\\"
           }
         }
       ],
@@ -4051,18 +4420,37 @@ exports[`record integration tests > can record textarea mutations correctly 1`]
   {
     \\"type\\": 3,
     \\"data\\": {
-      \\"source\\": 2,
-      \\"type\\": 5,
-      \\"id\\": 21
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 4,
+          \\"attributes\\": {
+            \\"style\\": {
+              \\"color\\": \\"var(--mystery-color)\\"
+            }
+          }
+        }
+      ],
+      \\"removes\\": [],
+      \\"adds\\": []
     }
   },
   {
     \\"type\\": 3,
     \\"data\\": {
-      \\"source\\": 5,
-      \\"text\\": \\"1ok3\\",
-      \\"isChecked\\": false,
-      \\"id\\": 21
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 4,
+          \\"attributes\\": {
+            \\"style\\": \\"color:var(--mystery-color);display:block;margin:10px\\"
+          }
+        }
+      ],
+      \\"removes\\": [],
+      \\"adds\\": []
     }
   },
   {
@@ -4072,9 +4460,11 @@ exports[`record integration tests > can record textarea mutations correctly 1`]
       \\"texts\\": [],
       \\"attributes\\": [
         {
-          \\"id\\": 21,
+          \\"id\\": 4,
           \\"attributes\\": {
-            \\"value\\": \\"ignore\\"
+            \\"style\\": {
+              \\"margin-left\\": \\"Npx\\"
+            }
           }
         }
       ],
@@ -4085,10 +4475,21 @@ exports[`record integration tests > can record textarea mutations correctly 1`]
   {
     \\"type\\": 3,
     \\"data\\": {
-      \\"source\\": 5,
-      \\"text\\": \\"12ok3\\",
-      \\"isChecked\\": false,
-      \\"id\\": 21
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 4,
+          \\"attributes\\": {
+            \\"style\\": {
+              \\"margin-top\\": \\"Npx\\",
+              \\"color\\": false
+            }
+          }
+        }
+      ],
+      \\"removes\\": [],
+      \\"adds\\": []
     }
   }
 ]"
@@ -5216,12 +5617,13 @@ exports[`record integration tests > mutations should work when blocked class is
                   {
                     \\"type\\": 2,
                     \\"tagName\\": \\"style\\",
-                    \\"attributes\\": {},
+                    \\"attributes\\": {
+                      \\"_cssText\\": \\"#b-class, #b-class-2 { height: 33px; width: 200px; }\\"
+                    },
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"#b-class, #b-class-2 { height: 33px; width: 200px; }\\",
-                        \\"isStyle\\": true,
+                        \\"textContent\\": \\"\\",
                         \\"id\\": 9
                       }
                     ],
@@ -7932,12 +8334,13 @@ exports[`record integration tests > should nest record iframe 1`] = `
                   {
                     \\"type\\": 2,
                     \\"tagName\\": \\"style\\",
-                    \\"attributes\\": {},
+                    \\"attributes\\": {
+                      \\"_cssText\\": \\"iframe { width: 500px; height: 500px; }\\"
+                    },
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"iframe { width: 500px; height: 500px; }\\",
-                        \\"isStyle\\": true,
+                        \\"textContent\\": \\"\\",
                         \\"id\\": 14
                       }
                     ],
@@ -11475,7 +11878,6 @@ exports[`record integration tests > should record dynamic CSS changes 1`] = `
                       {
                         \\"type\\": 3,
                         \\"textContent\\": \\"\\",
-                        \\"isStyle\\": true,
                         \\"id\\": 18
                       }
                     ],
@@ -14743,12 +15145,13 @@ exports[`record integration tests > should record shadow DOM 1`] = `
                   {
                     \\"type\\": 2,
                     \\"tagName\\": \\"style\\",
-                    \\"attributes\\": {},
+                    \\"attributes\\": {
+                      \\"_cssText\\": \\".my-element { margin: 0px 0px 1rem; }iframe { border: 0px; width: 100%; padding: 0px; }body { max-width: 400px; margin: 1rem auto; padding: 0px 1rem; font-family: \\\\\\"comic sans ms\\\\\\"; }\\"
+                    },
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\".my-element { margin: 0px 0px 1rem; }iframe { border: 0px; width: 100%; padding: 0px; }body { max-width: 400px; margin: 1rem auto; padding: 0px 1rem; font-family: \\\\\\"comic sans ms\\\\\\"; }\\",
-                        \\"isStyle\\": true,
+                        \\"textContent\\": \\"\\",
                         \\"id\\": 14
                       }
                     ],
@@ -14826,12 +15229,13 @@ exports[`record integration tests > should record shadow DOM 1`] = `
                       {
                         \\"type\\": 2,
                         \\"tagName\\": \\"style\\",
-                        \\"attributes\\": {},
+                        \\"attributes\\": {
+                          \\"_cssText\\": \\"body { margin: 0px; }p { border: 1px solid rgb(204, 204, 204); padding: 1rem; color: red; font-family: sans-serif; }\\"
+                        },
                         \\"childNodes\\": [
                           {
                             \\"type\\": 3,
-                            \\"textContent\\": \\"body { margin: 0px; }p { border: 1px solid rgb(204, 204, 204); padding: 1rem; color: red; font-family: sans-serif; }\\",
-                            \\"isStyle\\": true,
+                            \\"textContent\\": \\"\\",
                             \\"id\\": 28
                           }
                         ],
@@ -15036,28 +15440,143 @@ exports[`record integration tests > should record shadow DOM 1`] = `
     }
   },
   {
-    \\"type\\": 3,
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [],
+      \\"removes\\": [
+        {
+          \\"parentId\\": 45,
+          \\"id\\": 46
+        }
+      ],
+      \\"adds\\": [
+        {
+          \\"parentId\\": 45,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 3,
+            \\"textContent\\": \\"123\\",
+            \\"id\\": 47
+          }
+        }
+      ]
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [],
+      \\"removes\\": [],
+      \\"adds\\": [
+        {
+          \\"parentId\\": 45,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"span\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [],
+            \\"id\\": 48,
+            \\"isShadow\\": true
+          }
+        },
+        {
+          \\"parentId\\": 48,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 3,
+            \\"textContent\\": \\"nested shadow dom\\",
+            \\"id\\": 49
+          }
+        }
+      ]
+    }
+  }
+]"
+`;
+
+exports[`record integration tests > should record shadow DOM 2 1`] = `
+"[
+  {
+    \\"type\\": 0,
+    \\"data\\": {}
+  },
+  {
+    \\"type\\": 1,
+    \\"data\\": {}
+  },
+  {
+    \\"type\\": 4,
+    \\"data\\": {
+      \\"href\\": \\"about:blank\\",
+      \\"width\\": 1920,
+      \\"height\\": 1080
+    }
+  },
+  {
+    \\"type\\": 2,
     \\"data\\": {
-      \\"source\\": 0,
-      \\"texts\\": [],
-      \\"attributes\\": [],
-      \\"removes\\": [
-        {
-          \\"parentId\\": 45,
-          \\"id\\": 46
-        }
-      ],
-      \\"adds\\": [
-        {
-          \\"parentId\\": 45,
-          \\"nextId\\": null,
-          \\"node\\": {
-            \\"type\\": 3,
-            \\"textContent\\": \\"123\\",
-            \\"id\\": 47
+      \\"node\\": {
+        \\"type\\": 0,
+        \\"childNodes\\": [
+          {
+            \\"type\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [],
+                \\"id\\": 3
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 5
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 7
+                      }
+                    ],
+                    \\"id\\": 6
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\\\n\\",
+                    \\"id\\": 8
+                  }
+                ],
+                \\"id\\": 4
+              }
+            ],
+            \\"id\\": 2
           }
-        }
-      ]
+        ],
+        \\"compatMode\\": \\"BackCompat\\",
+        \\"id\\": 1
+      },
+      \\"initialOffset\\": {
+        \\"left\\": 0,
+        \\"top\\": 0
+      }
     }
   },
   {
@@ -15069,24 +15588,27 @@ exports[`record integration tests > should record shadow DOM 1`] = `
       \\"removes\\": [],
       \\"adds\\": [
         {
-          \\"parentId\\": 45,
+          \\"parentId\\": 4,
           \\"nextId\\": null,
           \\"node\\": {
             \\"type\\": 2,
-            \\"tagName\\": \\"span\\",
+            \\"tagName\\": \\"div\\",
             \\"attributes\\": {},
             \\"childNodes\\": [],
-            \\"id\\": 48,
-            \\"isShadow\\": true
+            \\"id\\": 9,
+            \\"isShadowHost\\": true
           }
         },
         {
-          \\"parentId\\": 48,
+          \\"parentId\\": 9,
           \\"nextId\\": null,
           \\"node\\": {
-            \\"type\\": 3,
-            \\"textContent\\": \\"nested shadow dom\\",
-            \\"id\\": 49
+            \\"type\\": 2,
+            \\"tagName\\": \\"input\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [],
+            \\"id\\": 10,
+            \\"isShadow\\": true
           }
         }
       ]
@@ -15095,7 +15617,7 @@ exports[`record integration tests > should record shadow DOM 1`] = `
 ]"
 `;
 
-exports[`record integration tests > should record shadow DOM 2 1`] = `
+exports[`record integration tests > should record shadow DOM 3 1`] = `
 "[
   {
     \\"type\\": 0,
@@ -15213,7 +15735,7 @@ exports[`record integration tests > should record shadow DOM 2 1`] = `
 ]"
 `;
 
-exports[`record integration tests > should record shadow DOM 3 1`] = `
+exports[`record integration tests > should record shadow doms polyfilled by shadydom 1`] = `
 "[
   {
     \\"type\\": 0,
@@ -15237,17 +15759,83 @@ exports[`record integration tests > should record shadow DOM 3 1`] = `
       \\"node\\": {
         \\"type\\": 0,
         \\"childNodes\\": [
+          {
+            \\"type\\": 1,
+            \\"name\\": \\"html\\",
+            \\"publicId\\": \\"\\",
+            \\"systemId\\": \\"\\",
+            \\"id\\": 2
+          },
           {
             \\"type\\": 2,
             \\"tagName\\": \\"html\\",
-            \\"attributes\\": {},
+            \\"attributes\\": {
+              \\"lang\\": \\"en\\"
+            },
             \\"childNodes\\": [
               {
                 \\"type\\": 2,
                 \\"tagName\\": \\"head\\",
                 \\"attributes\\": {},
-                \\"childNodes\\": [],
-                \\"id\\": 3
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n        \\",
+                    \\"id\\": 5
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 7
+                      }
+                    ],
+                    \\"id\\": 6
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n        \\",
+                    \\"id\\": 8
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {
+                      \\"src\\": \\"https://cdn.jsdelivr.net/npm/@webcomponents/shadydom@1.9.0/shadydom.min.js\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 9
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\\\n    \\",
+                    \\"id\\": 10
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"meta\\",
+                    \\"attributes\\": {
+                      \\"charset\\": \\"UTF-8\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 11
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 12
+                  }
+                ],
+                \\"id\\": 4
+              },
+              {
+                \\"type\\": 3,
+                \\"textContent\\": \\"\\\\n  \\",
+                \\"id\\": 13
               },
               {
                 \\"type\\": 2,
@@ -15257,7 +15845,65 @@ exports[`record integration tests > should record shadow DOM 3 1`] = `
                   {
                     \\"type\\": 3,
                     \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 5
+                    \\"id\\": 15
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"div\\",
+                    \\"attributes\\": {
+                      \\"id\\": \\"target1\\"
+                    },
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 2,
+                        \\"tagName\\": \\"div\\",
+                        \\"attributes\\": {},
+                        \\"childNodes\\": [],
+                        \\"id\\": 17
+                      }
+                    ],
+                    \\"id\\": 16
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 18
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"div\\",
+                    \\"attributes\\": {
+                      \\"id\\": \\"target2\\"
+                    },
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 2,
+                        \\"tagName\\": \\"p\\",
+                        \\"attributes\\": {},
+                        \\"childNodes\\": [],
+                        \\"id\\": 20
+                      }
+                    ],
+                    \\"id\\": 19
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 21
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"div\\",
+                    \\"attributes\\": {
+                      \\"id\\": \\"target3\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 22
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 23
                   },
                   {
                     \\"type\\": 2,
@@ -15267,24 +15913,41 @@ exports[`record integration tests > should record shadow DOM 3 1`] = `
                       {
                         \\"type\\": 3,
                         \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
-                        \\"id\\": 7
+                        \\"id\\": 25
                       }
                     ],
-                    \\"id\\": 6
+                    \\"id\\": 24
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\\\n    \\",
+                    \\"id\\": 26
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 28
+                      }
+                    ],
+                    \\"id\\": 27
                   },
                   {
                     \\"type\\": 3,
                     \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\\\n\\",
-                    \\"id\\": 8
+                    \\"id\\": 29
                   }
                 ],
-                \\"id\\": 4
+                \\"id\\": 14
               }
             ],
-            \\"id\\": 2
+            \\"id\\": 3
           }
         ],
-        \\"compatMode\\": \\"BackCompat\\",
         \\"id\\": 1
       },
       \\"initialOffset\\": {
@@ -15302,27 +15965,14 @@ exports[`record integration tests > should record shadow DOM 3 1`] = `
       \\"removes\\": [],
       \\"adds\\": [
         {
-          \\"parentId\\": 4,
-          \\"nextId\\": null,
-          \\"node\\": {
-            \\"type\\": 2,
-            \\"tagName\\": \\"div\\",
-            \\"attributes\\": {},
-            \\"childNodes\\": [],
-            \\"id\\": 9,
-            \\"isShadowHost\\": true
-          }
-        },
-        {
-          \\"parentId\\": 9,
+          \\"parentId\\": 22,
           \\"nextId\\": null,
           \\"node\\": {
             \\"type\\": 2,
-            \\"tagName\\": \\"input\\",
+            \\"tagName\\": \\"span\\",
             \\"attributes\\": {},
             \\"childNodes\\": [],
-            \\"id\\": 10,
-            \\"isShadow\\": true
+            \\"id\\": 30
           }
         }
       ]
@@ -15331,7 +15981,7 @@ exports[`record integration tests > should record shadow DOM 3 1`] = `
 ]"
 `;
 
-exports[`record integration tests > should record shadow doms polyfilled by shadydom 1`] = `
+exports[`record integration tests > should record shadow doms polyfilled by synthetic-shadow 1`] = `
 "[
   {
     \\"type\\": 0,
@@ -15401,14 +16051,14 @@ exports[`record integration tests > should record shadow doms polyfilled by shad
                     \\"type\\": 2,
                     \\"tagName\\": \\"script\\",
                     \\"attributes\\": {
-                      \\"src\\": \\"https://cdn.jsdelivr.net/npm/@webcomponents/shadydom@1.9.0/shadydom.min.js\\"
+                      \\"src\\": \\"https://cdn.jsdelivr.net/npm/@lwc/synthetic-shadow@2.20.3/dist/synthetic-shadow.js\\"
                     },
                     \\"childNodes\\": [],
                     \\"id\\": 9
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\\\n    \\",
+                    \\"textContent\\": \\"\\\\n      \\\\n    \\",
                     \\"id\\": 10
                   },
                   {
@@ -15455,69 +16105,274 @@ exports[`record integration tests > should record shadow doms polyfilled by shad
                         \\"tagName\\": \\"div\\",
                         \\"attributes\\": {},
                         \\"childNodes\\": [],
-                        \\"id\\": 17
+                        \\"id\\": 17,
+                        \\"isShadow\\": true
                       }
                     ],
-                    \\"id\\": 16
+                    \\"id\\": 16,
+                    \\"isShadowHost\\": true
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 18
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"div\\",
+                    \\"attributes\\": {
+                      \\"id\\": \\"target2\\"
+                    },
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 2,
+                        \\"tagName\\": \\"p\\",
+                        \\"attributes\\": {},
+                        \\"childNodes\\": [],
+                        \\"id\\": 20
+                      }
+                    ],
+                    \\"id\\": 19
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 21
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"div\\",
+                    \\"attributes\\": {
+                      \\"id\\": \\"target3\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 22
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 23
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 25
+                      }
+                    ],
+                    \\"id\\": 24
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\\\n    \\",
+                    \\"id\\": 26
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 28
+                      }
+                    ],
+                    \\"id\\": 27
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\\\n\\",
+                    \\"id\\": 29
+                  }
+                ],
+                \\"id\\": 14
+              }
+            ],
+            \\"id\\": 3
+          }
+        ],
+        \\"id\\": 1
+      },
+      \\"initialOffset\\": {
+        \\"left\\": 0,
+        \\"top\\": 0
+      }
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [],
+      \\"removes\\": [],
+      \\"adds\\": [
+        {
+          \\"parentId\\": 22,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"span\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [],
+            \\"id\\": 30
+          }
+        },
+        {
+          \\"parentId\\": 14,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"div\\",
+            \\"attributes\\": {
+              \\"id\\": \\"target4\\"
+            },
+            \\"childNodes\\": [],
+            \\"id\\": 31,
+            \\"isShadowHost\\": true
+          }
+        },
+        {
+          \\"parentId\\": 31,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"ul\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [],
+            \\"id\\": 32,
+            \\"isShadow\\": true
+          }
+        }
+      ]
+    }
+  }
+]"
+`;
+
+exports[`record integration tests > should record style mutations and replay them correctly 1`] = `
+"[
+  {
+    \\"type\\": 4,
+    \\"data\\": {
+      \\"href\\": \\"about:blank\\",
+      \\"width\\": 1920,
+      \\"height\\": 1080
+    }
+  },
+  {
+    \\"type\\": 2,
+    \\"data\\": {
+      \\"node\\": {
+        \\"type\\": 0,
+        \\"childNodes\\": [
+          {
+            \\"type\\": 1,
+            \\"name\\": \\"html\\",
+            \\"publicId\\": \\"\\",
+            \\"systemId\\": \\"\\",
+            \\"id\\": 2
+          },
+          {
+            \\"type\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {
+              \\"lang\\": \\"en\\"
+            },
+            \\"childNodes\\": [
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n\\\\t        \\",
+                    \\"id\\": 5
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"style\\",
+                    \\"attributes\\": {
+                      \\"_cssText\\": \\"#one { color: rgb(255, 0, 0); }\\"
+                    },
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"\\",
+                        \\"id\\": 7
+                      }
+                    ],
+                    \\"id\\": 6
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n        \\",
+                    \\"id\\": 8
                   },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 10
+                      }
+                    ],
+                    \\"id\\": 9
+                  }
+                ],
+                \\"id\\": 4
+              },
+              {
+                \\"type\\": 3,
+                \\"textContent\\": \\"\\\\n        \\",
+                \\"id\\": 11
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 18
+                    \\"textContent\\": \\"\\\\n\\\\t        \\",
+                    \\"id\\": 13
                   },
                   {
                     \\"type\\": 2,
                     \\"tagName\\": \\"div\\",
                     \\"attributes\\": {
-                      \\"id\\": \\"target2\\"
+                      \\"id\\": \\"one\\"
                     },
-                    \\"childNodes\\": [
-                      {
-                        \\"type\\": 2,
-                        \\"tagName\\": \\"p\\",
-                        \\"attributes\\": {},
-                        \\"childNodes\\": [],
-                        \\"id\\": 20
-                      }
-                    ],
-                    \\"id\\": 19
+                    \\"childNodes\\": [],
+                    \\"id\\": 14
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 21
+                    \\"textContent\\": \\"\\\\n          \\",
+                    \\"id\\": 15
                   },
                   {
                     \\"type\\": 2,
                     \\"tagName\\": \\"div\\",
                     \\"attributes\\": {
-                      \\"id\\": \\"target3\\"
+                      \\"id\\": \\"two\\"
                     },
                     \\"childNodes\\": [],
-                    \\"id\\": 22
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 23
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"script\\",
-                    \\"attributes\\": {},
-                    \\"childNodes\\": [
-                      {
-                        \\"type\\": 3,
-                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
-                        \\"id\\": 25
-                      }
-                    ],
-                    \\"id\\": 24
+                    \\"id\\": 16
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\\\n    \\",
-                    \\"id\\": 26
+                    \\"textContent\\": \\"\\\\n\\\\t        \\",
+                    \\"id\\": 17
                   },
                   {
                     \\"type\\": 2,
@@ -15527,18 +16382,18 @@ exports[`record integration tests > should record shadow doms polyfilled by shad
                       {
                         \\"type\\": 3,
                         \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
-                        \\"id\\": 28
+                        \\"id\\": 19
                       }
                     ],
-                    \\"id\\": 27
+                    \\"id\\": 18
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\\\n\\",
-                    \\"id\\": 29
+                    \\"textContent\\": \\"\\\\n        \\\\n      \\",
+                    \\"id\\": 20
                   }
                 ],
-                \\"id\\": 14
+                \\"id\\": 12
               }
             ],
             \\"id\\": 3
@@ -15561,32 +16416,63 @@ exports[`record integration tests > should record shadow doms polyfilled by shad
       \\"removes\\": [],
       \\"adds\\": [
         {
-          \\"parentId\\": 22,
+          \\"parentId\\": 4,
           \\"nextId\\": null,
           \\"node\\": {
             \\"type\\": 2,
-            \\"tagName\\": \\"span\\",
-            \\"attributes\\": {},
+            \\"tagName\\": \\"style\\",
+            \\"attributes\\": {
+              \\"_cssText\\": \\"#two { color: rgb(255, 0, 0); }\\"
+            },
             \\"childNodes\\": [],
-            \\"id\\": 30
+            \\"id\\": 21
+          }
+        },
+        {
+          \\"parentId\\": 21,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 3,
+            \\"textContent\\": \\"\\",
+            \\"id\\": 22
           }
         }
       ]
     }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 13,
+      \\"id\\": 6,
+      \\"set\\": {
+        \\"property\\": \\"color\\",
+        \\"value\\": \\"rgb(255, 255, 0)\\"
+      },
+      \\"index\\": [
+        0
+      ]
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 13,
+      \\"id\\": 21,
+      \\"set\\": {
+        \\"property\\": \\"color\\",
+        \\"value\\": \\"rgb(255, 255, 0)\\"
+      },
+      \\"index\\": [
+        0
+      ]
+    }
   }
 ]"
 `;
 
-exports[`record integration tests > should record shadow doms polyfilled by synthetic-shadow 1`] = `
+exports[`record integration tests > should record style mutations with multiple child nodes and replay them correctly 1`] = `
 "[
-  {
-    \\"type\\": 0,
-    \\"data\\": {}
-  },
-  {
-    \\"type\\": 1,
-    \\"data\\": {}
-  },
   {
     \\"type\\": 4,
     \\"data\\": {
@@ -15622,18 +16508,25 @@ exports[`record integration tests > should record shadow doms polyfilled by synt
                 \\"childNodes\\": [
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n        \\",
+                    \\"textContent\\": \\"\\\\n\\\\t        \\",
                     \\"id\\": 5
                   },
                   {
                     \\"type\\": 2,
-                    \\"tagName\\": \\"script\\",
-                    \\"attributes\\": {},
+                    \\"tagName\\": \\"style\\",
+                    \\"attributes\\": {
+                      \\"_cssText\\": \\"#one { color: rgb(255, 0, 0); }\\"
+                    },
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"textContent\\": \\"\\",
                         \\"id\\": 7
+                      },
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"\\",
+                        \\"id\\": 8
                       }
                     ],
                     \\"id\\": 6
@@ -15641,43 +16534,28 @@ exports[`record integration tests > should record shadow doms polyfilled by synt
                   {
                     \\"type\\": 3,
                     \\"textContent\\": \\"\\\\n        \\",
-                    \\"id\\": 8
+                    \\"id\\": 9
                   },
                   {
                     \\"type\\": 2,
                     \\"tagName\\": \\"script\\",
-                    \\"attributes\\": {
-                      \\"src\\": \\"https://cdn.jsdelivr.net/npm/@lwc/synthetic-shadow@2.20.3/dist/synthetic-shadow.js\\"
-                    },
-                    \\"childNodes\\": [],
-                    \\"id\\": 9
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n      \\\\n    \\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 11
+                      }
+                    ],
                     \\"id\\": 10
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"meta\\",
-                    \\"attributes\\": {
-                      \\"charset\\": \\"UTF-8\\"
-                    },
-                    \\"childNodes\\": [],
-                    \\"id\\": 11
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\",
-                    \\"id\\": 12
                   }
                 ],
                 \\"id\\": 4
               },
               {
                 \\"type\\": 3,
-                \\"textContent\\": \\"\\\\n  \\",
-                \\"id\\": 13
+                \\"textContent\\": \\"\\\\n        \\",
+                \\"id\\": 12
               },
               {
                 \\"type\\": 2,
@@ -15686,86 +16564,36 @@ exports[`record integration tests > should record shadow doms polyfilled by synt
                 \\"childNodes\\": [
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 15
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"div\\",
-                    \\"attributes\\": {
-                      \\"id\\": \\"target1\\"
-                    },
-                    \\"childNodes\\": [
-                      {
-                        \\"type\\": 2,
-                        \\"tagName\\": \\"div\\",
-                        \\"attributes\\": {},
-                        \\"childNodes\\": [],
-                        \\"id\\": 17,
-                        \\"isShadow\\": true
-                      }
-                    ],
-                    \\"id\\": 16,
-                    \\"isShadowHost\\": true
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 18
+                    \\"textContent\\": \\"\\\\n\\\\t        \\",
+                    \\"id\\": 14
                   },
                   {
                     \\"type\\": 2,
                     \\"tagName\\": \\"div\\",
                     \\"attributes\\": {
-                      \\"id\\": \\"target2\\"
+                      \\"id\\": \\"one\\"
                     },
-                    \\"childNodes\\": [
-                      {
-                        \\"type\\": 2,
-                        \\"tagName\\": \\"p\\",
-                        \\"attributes\\": {},
-                        \\"childNodes\\": [],
-                        \\"id\\": 20
-                      }
-                    ],
-                    \\"id\\": 19
+                    \\"childNodes\\": [],
+                    \\"id\\": 15
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 21
+                    \\"textContent\\": \\"\\\\n          \\",
+                    \\"id\\": 16
                   },
                   {
                     \\"type\\": 2,
                     \\"tagName\\": \\"div\\",
                     \\"attributes\\": {
-                      \\"id\\": \\"target3\\"
+                      \\"id\\": \\"two\\"
                     },
                     \\"childNodes\\": [],
-                    \\"id\\": 22
-                  },
-                  {
-                    \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\",
-                    \\"id\\": 23
-                  },
-                  {
-                    \\"type\\": 2,
-                    \\"tagName\\": \\"script\\",
-                    \\"attributes\\": {},
-                    \\"childNodes\\": [
-                      {
-                        \\"type\\": 3,
-                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
-                        \\"id\\": 25
-                      }
-                    ],
-                    \\"id\\": 24
+                    \\"id\\": 17
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n  \\\\n    \\",
-                    \\"id\\": 26
+                    \\"textContent\\": \\"\\\\n\\\\t        \\",
+                    \\"id\\": 18
                   },
                   {
                     \\"type\\": 2,
@@ -15775,18 +16603,18 @@ exports[`record integration tests > should record shadow doms polyfilled by synt
                       {
                         \\"type\\": 3,
                         \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
-                        \\"id\\": 28
+                        \\"id\\": 20
                       }
                     ],
-                    \\"id\\": 27
+                    \\"id\\": 19
                   },
                   {
                     \\"type\\": 3,
-                    \\"textContent\\": \\"\\\\n    \\\\n    \\\\n\\\\n\\",
-                    \\"id\\": 29
+                    \\"textContent\\": \\"\\\\n        \\\\n      \\",
+                    \\"id\\": 21
                   }
                 ],
-                \\"id\\": 14
+                \\"id\\": 13
               }
             ],
             \\"id\\": 3
@@ -15809,40 +16637,34 @@ exports[`record integration tests > should record shadow doms polyfilled by synt
       \\"removes\\": [],
       \\"adds\\": [
         {
-          \\"parentId\\": 22,
+          \\"parentId\\": 4,
           \\"nextId\\": null,
           \\"node\\": {
             \\"type\\": 2,
-            \\"tagName\\": \\"span\\",
-            \\"attributes\\": {},
+            \\"tagName\\": \\"style\\",
+            \\"attributes\\": {
+              \\"_cssText\\": \\"#two { color: rgb(255, 0, 0); }\\"
+            },
             \\"childNodes\\": [],
-            \\"id\\": 30
+            \\"id\\": 22
           }
         },
         {
-          \\"parentId\\": 14,
+          \\"parentId\\": 22,
           \\"nextId\\": null,
           \\"node\\": {
-            \\"type\\": 2,
-            \\"tagName\\": \\"div\\",
-            \\"attributes\\": {
-              \\"id\\": \\"target4\\"
-            },
-            \\"childNodes\\": [],
-            \\"id\\": 31,
-            \\"isShadowHost\\": true
+            \\"type\\": 3,
+            \\"textContent\\": \\"\\",
+            \\"id\\": 23
           }
         },
         {
-          \\"parentId\\": 31,
-          \\"nextId\\": null,
+          \\"parentId\\": 22,
+          \\"nextId\\": 23,
           \\"node\\": {
-            \\"type\\": 2,
-            \\"tagName\\": \\"ul\\",
-            \\"attributes\\": {},
-            \\"childNodes\\": [],
-            \\"id\\": 32,
-            \\"isShadow\\": true
+            \\"type\\": 3,
+            \\"textContent\\": \\"\\",
+            \\"id\\": 24
           }
         }
       ]
diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap
index 2333d8f251..c1db08aea0 100644
--- a/packages/rrweb/test/__snapshots__/record.test.ts.snap
+++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap
@@ -1386,18 +1386,18 @@ exports[`record > captures inserted style text nodes correctly 1`] = `
                   {
                     \\"type\\": 2,
                     \\"tagName\\": \\"style\\",
-                    \\"attributes\\": {},
+                    \\"attributes\\": {
+                      \\"_cssText\\": \\"div { color: red; }/* rr_split */section { color: blue; }\\"
+                    },
                     \\"childNodes\\": [
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"div { color: red; }\\",
-                        \\"isStyle\\": true,
+                        \\"textContent\\": \\"\\",
                         \\"id\\": 6
                       },
                       {
                         \\"type\\": 3,
-                        \\"textContent\\": \\"section { color: blue; }\\",
-                        \\"isStyle\\": true,
+                        \\"textContent\\": \\"\\",
                         \\"id\\": 7
                       }
                     ],
@@ -1460,7 +1460,6 @@ exports[`record > captures inserted style text nodes correctly 1`] = `
           \\"node\\": {
             \\"type\\": 3,
             \\"textContent\\": \\"h1 { color: pink; }\\",
-            \\"isStyle\\": true,
             \\"id\\": 12
           }
         },
@@ -1470,7 +1469,6 @@ exports[`record > captures inserted style text nodes correctly 1`] = `
           \\"node\\": {
             \\"type\\": 3,
             \\"textContent\\": \\"span { color: orange; }\\",
-            \\"isStyle\\": true,
             \\"id\\": 13
           }
         }
@@ -2196,6 +2194,146 @@ exports[`record > captures stylesheet rules 1`] = `
 ]"
 `;
 
+exports[`record > captures stylesheet rules with deprecated addRule & removeRule properties 1`] = `
+"[
+  {
+    \\"type\\": 4,
+    \\"data\\": {
+      \\"href\\": \\"about:blank\\",
+      \\"width\\": 1920,
+      \\"height\\": 1080
+    }
+  },
+  {
+    \\"type\\": 2,
+    \\"data\\": {
+      \\"node\\": {
+        \\"type\\": 0,
+        \\"childNodes\\": [
+          {
+            \\"type\\": 1,
+            \\"name\\": \\"html\\",
+            \\"publicId\\": \\"\\",
+            \\"systemId\\": \\"\\",
+            \\"id\\": 2
+          },
+          {
+            \\"type\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [],
+                \\"id\\": 4
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n          \\",
+                    \\"id\\": 6
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"input\\",
+                    \\"attributes\\": {
+                      \\"type\\": \\"text\\",
+                      \\"size\\": \\"40\\"
+                    },
+                    \\"childNodes\\": [],
+                    \\"id\\": 7
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n        \\\\n      \\\\n    \\",
+                    \\"id\\": 8
+                  }
+                ],
+                \\"id\\": 5
+              }
+            ],
+            \\"id\\": 3
+          }
+        ],
+        \\"id\\": 1
+      },
+      \\"initialOffset\\": {
+        \\"left\\": 0,
+        \\"top\\": 0
+      }
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [],
+      \\"removes\\": [],
+      \\"adds\\": [
+        {
+          \\"parentId\\": 4,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"style\\",
+            \\"attributes\\": {
+              \\"_cssText\\": \\"body { background: rgb(0, 0, 0); }\\"
+            },
+            \\"childNodes\\": [],
+            \\"id\\": 9
+          }
+        }
+      ]
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 8,
+      \\"id\\": 9,
+      \\"adds\\": [
+        {
+          \\"rule\\": \\"body { color: #fff; }\\",
+          \\"index\\": 1
+        }
+      ]
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 8,
+      \\"id\\": 9,
+      \\"removes\\": [
+        {
+          \\"index\\": 0
+        }
+      ]
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 8,
+      \\"id\\": 9,
+      \\"adds\\": [
+        {
+          \\"rule\\": \\"body { color: #ccc; }\\",
+          \\"index\\": 1
+        }
+      ]
+    }
+  }
+]"
+`;
+
 exports[`record > captures stylesheets in iframes with \`blob:\` url 1`] = `
 "[
   {
diff --git a/packages/rrweb/test/events/bad-style.ts b/packages/rrweb/test/events/bad-style.ts
new file mode 100644
index 0000000000..4c3528f5eb
--- /dev/null
+++ b/packages/rrweb/test/events/bad-style.ts
@@ -0,0 +1,231 @@
+import { EventType, IncrementalSource } from '@saola.ai/rrweb-types';
+import type { eventWithTime } from '@saola.ai/rrweb-types';
+
+/**
+ * https://github.com/rrweb-io/rrweb/pull/1417
+ * make sure we can replay duplicated content in style elements generated by older versions of rrweb
+ * test case demonstrates the duplicated content in question in two places; the initial snapshot, and in a style mutation
+ * (see 'BAD: duplicated content' below)
+ * these should no longer be generated after https://github.com/rrweb-io/rrweb/pull/1437
+ */
+const now = Date.now();
+const events: eventWithTime[] = [
+  {
+    timestamp: now,
+    type: EventType.Meta,
+    data: {
+      href: 'about:blank',
+      width: 1920,
+      height: 1080,
+    },
+  },
+  {
+    timestamp: now,
+    type: EventType.FullSnapshot,
+    data: {
+      node: {
+        type: 0,
+        childNodes: [
+          {
+            type: 1,
+            name: 'html',
+            publicId: '',
+            systemId: '',
+            id: 2,
+          },
+          {
+            type: 2,
+            tagName: 'html',
+            attributes: {
+              lang: 'en',
+            },
+            childNodes: [
+              {
+                type: 2,
+                tagName: 'head',
+                attributes: {},
+                childNodes: [
+                  {
+                    type: 3,
+                    textContent: '\\\\n\\\\t        ',
+                    id: 5,
+                  },
+                  {
+                    type: 2,
+                    tagName: 'style',
+                    attributes: {
+                      _cssText: '#one { color: rgb(255, 0, 0); }',
+                    },
+                    childNodes: [
+                      {
+                        type: 3,
+                        // BAD: duplicated content (tweaked to different color)
+                        textContent: '#one { color: rgb(155, 0, 0); }',
+                        isStyle: true,
+                        id: 7,
+                      },
+                    ],
+                    id: 6,
+                  },
+                  {
+                    type: 3,
+                    textContent: '\\\\n        ',
+                    id: 8,
+                  },
+                  {
+                    type: 2,
+                    tagName: 'script',
+                    attributes: {},
+                    childNodes: [
+                      {
+                        type: 3,
+                        textContent: 'SCRIPT_PLACEHOLDER',
+                        id: 10,
+                      },
+                    ],
+                    id: 9,
+                  },
+                ],
+                id: 4,
+              },
+              {
+                type: 3,
+                textContent: '\\\\n        ',
+                id: 11,
+              },
+              {
+                type: 2,
+                tagName: 'body',
+                attributes: {},
+                childNodes: [
+                  {
+                    type: 3,
+                    textContent: '\\\\n\\\\t        ',
+                    id: 13,
+                  },
+                  {
+                    type: 2,
+                    tagName: 'div',
+                    attributes: {
+                      id: 'one',
+                    },
+                    childNodes: [],
+                    id: 14,
+                  },
+                  {
+                    type: 3,
+                    textContent: '\\\\n          ',
+                    id: 15,
+                  },
+                  {
+                    type: 2,
+                    tagName: 'div',
+                    attributes: {
+                      id: 'two',
+                    },
+                    childNodes: [],
+                    id: 16,
+                  },
+                  {
+                    type: 3,
+                    textContent: '\\\\n\\\\t        ',
+                    id: 17,
+                  },
+                  {
+                    type: 2,
+                    tagName: 'script',
+                    attributes: {},
+                    childNodes: [
+                      {
+                        type: 3,
+                        textContent: 'SCRIPT_PLACEHOLDER',
+                        id: 19,
+                      },
+                    ],
+                    id: 18,
+                  },
+                  {
+                    type: 3,
+                    textContent: '\\\\n        \\\\n      ',
+                    id: 20,
+                  },
+                ],
+                id: 12,
+              },
+            ],
+            id: 3,
+          },
+        ],
+        id: 1,
+      },
+      initialOffset: {
+        left: 0,
+        top: 0,
+      },
+    },
+  },
+  {
+    timestamp: now + 1,
+    type: EventType.IncrementalSnapshot,
+    data: {
+      source: IncrementalSource.Mutation,
+      texts: [],
+      attributes: [],
+      removes: [],
+      adds: [
+        {
+          parentId: 4,
+          nextId: null,
+          node: {
+            type: 2,
+            tagName: 'style',
+            attributes: {
+              _cssText: '#two { color: rgb(255, 0, 0); }',
+            },
+            childNodes: [],
+            id: 21,
+          },
+        },
+        {
+          parentId: 21,
+          nextId: null,
+          node: {
+            type: 3,
+            // BAD: duplicated content (tweaked to different color)
+            textContent: '#two { color: rgb(155, 0, 0); }',
+            isStyle: true,
+            id: 22,
+          },
+        },
+      ],
+    },
+  },
+  {
+    timestamp: now + 2,
+    type: EventType.IncrementalSnapshot,
+    data: {
+      source: IncrementalSource.StyleDeclaration,
+      id: 6,
+      set: {
+        property: 'color',
+        value: 'rgb(255, 255, 0)',
+      },
+      index: [0],
+    },
+  },
+  {
+    timestamp: now + 3,
+    type: EventType.IncrementalSnapshot,
+    data: {
+      source: IncrementalSource.StyleDeclaration,
+      id: 21,
+      set: {
+        property: 'color',
+        value: 'rgb(255, 255, 0)',
+      },
+      index: [0],
+    },
+  },
+];
+
+export default events;
diff --git a/packages/rrweb/test/events/dialog-playback.ts b/packages/rrweb/test/events/dialog-playback.ts
new file mode 100644
index 0000000000..1aedd064ed
--- /dev/null
+++ b/packages/rrweb/test/events/dialog-playback.ts
@@ -0,0 +1,458 @@
+import { eventWithTime, IncrementalSource } from '@saola.ai/rrweb-types';
+
+const startTime = 1900000000;
+export const closedFullSnapshotTime = 132;
+export const showIncrementalAttributeTime = 1500;
+export const closeIncrementalAttributeTime = 2000;
+export const showModalIncrementalAttributeTime = 2500;
+export const switchBetweenShowModalAndShowIncrementalAttributeTime = 2600;
+export const switchBetweenShowAndShowModalIncrementalAttributeTime = 2700;
+export const showFullSnapshotTime = 3000;
+export const showModalFullSnapshotTime = 3500;
+export const showModalIncrementalAddTime = 4000;
+
+const events: eventWithTime[] = [
+  { type: 0, data: {}, timestamp: startTime + 1 },
+  { type: 1, data: {}, timestamp: startTime + closedFullSnapshotTime },
+  {
+    type: 4,
+    data: {
+      href: 'http://127.0.0.1:5500/test/html/dialog.html',
+      width: 1600,
+      height: 900,
+    },
+    timestamp: startTime + closedFullSnapshotTime,
+  },
+  {
+    type: 2,
+    data: {
+      node: {
+        type: 0,
+        childNodes: [
+          { type: 1, name: 'html', publicId: '', systemId: '', id: 2 },
+          {
+            type: 2,
+            tagName: 'html',
+            attributes: { lang: 'en' },
+            childNodes: [
+              {
+                type: 2,
+                tagName: 'head',
+                attributes: {},
+                childNodes: [
+                  { type: 3, textContent: '\n    ', id: 5 },
+                  {
+                    type: 2,
+                    tagName: 'meta',
+                    attributes: { charset: 'UTF-8' },
+                    childNodes: [],
+                    id: 6,
+                  },
+                  { type: 3, textContent: '\n    ', id: 7 },
+                  {
+                    type: 2,
+                    tagName: 'meta',
+                    attributes: {
+                      'http-equiv': 'X-UA-Compatible',
+                      content: 'IE=edge',
+                    },
+                    childNodes: [],
+                    id: 8,
+                  },
+                  { type: 3, textContent: '\n    ', id: 9 },
+                  {
+                    type: 2,
+                    tagName: 'meta',
+                    attributes: {
+                      name: 'viewport',
+                      content: 'width=device-width, initial-scale=1.0',
+                    },
+                    childNodes: [],
+                    id: 10,
+                  },
+                  { type: 3, textContent: '\n    ', id: 11 },
+                  {
+                    type: 2,
+                    tagName: 'title',
+                    attributes: {},
+                    childNodes: [{ type: 3, textContent: '<Dialog>', id: 13 }],
+                    id: 12,
+                  },
+                ],
+                id: 4,
+              },
+              { type: 3, textContent: '\n  ', id: 21 },
+              {
+                type: 2,
+                tagName: 'body',
+                attributes: {},
+                childNodes: [
+                  { type: 3, textContent: '\n    ', id: 23 },
+                  {
+                    type: 2,
+                    tagName: 'dialog',
+                    attributes: {
+                      style: 'outline: blue solid 1px;',
+                    },
+                    childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }],
+                    id: 24,
+                  },
+                  { type: 3, textContent: '\n    ', id: 26 },
+                  {
+                    type: 2,
+                    tagName: 'dialog',
+                    attributes: {
+                      style: 'outline: red solid 1px;',
+                    },
+                    childNodes: [{ type: 3, textContent: 'Dialog 2', id: 28 }],
+                    id: 27,
+                  },
+                  { type: 3, textContent: '\n  ', id: 31 },
+                ],
+                id: 22,
+              },
+            ],
+            id: 3,
+          },
+        ],
+        id: 1,
+      },
+      initialOffset: { left: 0, top: 0 },
+    },
+    timestamp: startTime + closedFullSnapshotTime,
+  },
+  // open dialog with .show()
+  {
+    type: 3,
+    data: {
+      source: IncrementalSource.Mutation,
+      adds: [],
+      removes: [],
+      texts: [],
+      attributes: [
+        {
+          id: 27,
+          attributes: { open: '', rr_open_mode: 'non-modal', class: 'show' },
+        },
+      ],
+    },
+    timestamp: startTime + showIncrementalAttributeTime,
+  },
+  // close dialog with .close()
+  {
+    type: 3,
+    data: {
+      source: IncrementalSource.Mutation,
+      adds: [],
+      removes: [],
+      texts: [],
+      attributes: [
+        {
+          id: 27,
+          attributes: { open: null, class: 'closed' },
+        },
+      ],
+    },
+    timestamp: startTime + closeIncrementalAttributeTime,
+  },
+  // open dialog with .showModal()
+  {
+    type: 3,
+    data: {
+      source: IncrementalSource.Mutation,
+      adds: [],
+      removes: [],
+      texts: [],
+      attributes: [
+        {
+          id: 27,
+          attributes: { rr_open_mode: 'modal', open: '', class: 'showModal' },
+        },
+      ],
+    },
+    timestamp: startTime + showModalIncrementalAttributeTime,
+  },
+  // switch between .showModal() and .show()
+  {
+    type: 3,
+    data: {
+      source: IncrementalSource.Mutation,
+      adds: [],
+      removes: [],
+      texts: [],
+      attributes: [
+        {
+          id: 27,
+          attributes: {
+            rr_open_mode: 'non-modal',
+            class: 'switched-from-show-modal-to-show',
+          },
+        },
+      ],
+    },
+    timestamp:
+      startTime + switchBetweenShowModalAndShowIncrementalAttributeTime,
+  },
+  // switch between .show() and .showModal()
+  {
+    type: 3,
+    data: {
+      source: IncrementalSource.Mutation,
+      adds: [],
+      removes: [],
+      texts: [],
+      attributes: [
+        {
+          id: 27,
+          attributes: {
+            rr_open_mode: 'modal',
+            class: 'switched-from-show-to-show-modal',
+          },
+        },
+      ],
+    },
+    timestamp:
+      startTime + switchBetweenShowAndShowModalIncrementalAttributeTime,
+  },
+  // open dialog with .show()
+  {
+    type: 2,
+    data: {
+      node: {
+        type: 0,
+        childNodes: [
+          { type: 1, name: 'html', publicId: '', systemId: '', id: 2 },
+          {
+            type: 2,
+            tagName: 'html',
+            attributes: { lang: 'en' },
+            childNodes: [
+              {
+                type: 2,
+                tagName: 'head',
+                attributes: {},
+                childNodes: [
+                  { type: 3, textContent: '\n    ', id: 5 },
+                  {
+                    type: 2,
+                    tagName: 'meta',
+                    attributes: { charset: 'UTF-8' },
+                    childNodes: [],
+                    id: 6,
+                  },
+                  { type: 3, textContent: '\n    ', id: 7 },
+                  {
+                    type: 2,
+                    tagName: 'meta',
+                    attributes: {
+                      'http-equiv': 'X-UA-Compatible',
+                      content: 'IE=edge',
+                    },
+                    childNodes: [],
+                    id: 8,
+                  },
+                  { type: 3, textContent: '\n    ', id: 9 },
+                  {
+                    type: 2,
+                    tagName: 'meta',
+                    attributes: {
+                      name: 'viewport',
+                      content: 'width=device-width, initial-scale=1.0',
+                    },
+                    childNodes: [],
+                    id: 10,
+                  },
+                  { type: 3, textContent: '\n    ', id: 11 },
+                  {
+                    type: 2,
+                    tagName: 'title',
+                    attributes: {},
+                    childNodes: [{ type: 3, textContent: '<Dialog>', id: 13 }],
+                    id: 12,
+                  },
+                ],
+                id: 4,
+              },
+              { type: 3, textContent: '\n  ', id: 21 },
+              {
+                type: 2,
+                tagName: 'body',
+                attributes: {},
+                childNodes: [
+                  { type: 3, textContent: '\n    ', id: 23 },
+                  {
+                    type: 2,
+                    tagName: 'dialog',
+                    attributes: {
+                      open: '',
+                      rr_open_mode: 'non-modal',
+                      style: 'outline: blue solid 1px;',
+                    },
+                    childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }],
+                    id: 24,
+                  },
+                  { type: 3, textContent: '\n    ', id: 26 },
+                  {
+                    type: 2,
+                    tagName: 'dialog',
+                    attributes: {
+                      style: 'outline: red solid 1px;',
+                    },
+                    childNodes: [{ type: 3, textContent: 'Dialog 2', id: 28 }],
+                    id: 27,
+                  },
+                  { type: 3, textContent: '\n  ', id: 31 },
+                ],
+                id: 22,
+              },
+            ],
+            id: 3,
+          },
+        ],
+        id: 1,
+      },
+      initialOffset: { left: 0, top: 0 },
+    },
+    timestamp: startTime + showFullSnapshotTime,
+  },
+  {
+    type: 2,
+    data: {
+      node: {
+        type: 0,
+        childNodes: [
+          { type: 1, name: 'html', publicId: '', systemId: '', id: 2 },
+          {
+            type: 2,
+            tagName: 'html',
+            attributes: { lang: 'en' },
+            childNodes: [
+              {
+                type: 2,
+                tagName: 'head',
+                attributes: {},
+                childNodes: [
+                  { type: 3, textContent: '\n    ', id: 5 },
+                  {
+                    type: 2,
+                    tagName: 'meta',
+                    attributes: { charset: 'UTF-8' },
+                    childNodes: [],
+                    id: 6,
+                  },
+                  { type: 3, textContent: '\n    ', id: 7 },
+                  {
+                    type: 2,
+                    tagName: 'meta',
+                    attributes: {
+                      'http-equiv': 'X-UA-Compatible',
+                      content: 'IE=edge',
+                    },
+                    childNodes: [],
+                    id: 8,
+                  },
+                  { type: 3, textContent: '\n    ', id: 9 },
+                  {
+                    type: 2,
+                    tagName: 'meta',
+                    attributes: {
+                      name: 'viewport',
+                      content: 'width=device-width, initial-scale=1.0',
+                    },
+                    childNodes: [],
+                    id: 10,
+                  },
+                  { type: 3, textContent: '\n    ', id: 11 },
+                  {
+                    type: 2,
+                    tagName: 'title',
+                    attributes: {},
+                    childNodes: [{ type: 3, textContent: '<Dialog>', id: 13 }],
+                    id: 12,
+                  },
+                ],
+                id: 4,
+              },
+              { type: 3, textContent: '\n  ', id: 21 },
+              {
+                type: 2,
+                tagName: 'body',
+                attributes: {},
+                childNodes: [
+                  { type: 3, textContent: '\n    ', id: 23 },
+                  {
+                    type: 2,
+                    tagName: 'dialog',
+                    attributes: {
+                      rr_open_mode: 'modal',
+                      open: '',
+                      style: 'outline: blue solid 1px;',
+                      class: 'existing-1',
+                    },
+                    childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }],
+                    id: 24,
+                  },
+                  { type: 3, textContent: '\n    ', id: 26 },
+                  {
+                    type: 2,
+                    tagName: 'dialog',
+                    attributes: {
+                      style: 'outline: red solid 1px;',
+                      class: 'existing-2',
+                    },
+                    childNodes: [{ type: 3, textContent: 'Dialog 2', id: 28 }],
+                    id: 27,
+                  },
+                  { type: 3, textContent: '\n  ', id: 31 },
+                ],
+                id: 22,
+              },
+            ],
+            id: 3,
+          },
+        ],
+        id: 1,
+      },
+      initialOffset: { left: 0, top: 0 },
+    },
+    timestamp: startTime + showModalFullSnapshotTime,
+  },
+  // add open dialog with .showModal()
+  {
+    type: 3,
+    data: {
+      source: IncrementalSource.Mutation,
+      adds: [
+        {
+          parentId: 22,
+          previousId: 23,
+          nextId: 24,
+          node: {
+            type: 2,
+            tagName: 'dialog',
+            attributes: {
+              rr_open_mode: 'modal',
+              open: '',
+              style: 'outline: orange solid 1px;',
+              class: 'new-dialog',
+            },
+            childNodes: [],
+            id: 32,
+          },
+        },
+        {
+          parentId: 32,
+          previousId: null,
+          nextId: null,
+          node: { type: 3, textContent: 'Dialog 3', id: 33 },
+        },
+      ],
+      removes: [],
+      texts: [],
+      attributes: [],
+    },
+    timestamp: startTime + showModalIncrementalAddTime,
+  },
+];
+
+export default events;
diff --git a/packages/rrweb/test/html/dialog.html b/packages/rrweb/test/html/dialog.html
new file mode 100644
index 0000000000..2380b8fade
--- /dev/null
+++ b/packages/rrweb/test/html/dialog.html
@@ -0,0 +1,5 @@
+<html>
+  <body>
+    <dialog>I'm a dialog</dialog>
+  </body>
+</html>
diff --git a/packages/rrweb/test/html/style.html b/packages/rrweb/test/html/style.html
new file mode 100644
index 0000000000..3e79d0e0d2
--- /dev/null
+++ b/packages/rrweb/test/html/style.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>style</title>
+    <style id="dual-textContent">
+      body { background-color: black; }
+    </style>
+    <script>
+      // not the same from the POV of the DOM of just sticking this text in above
+      document.querySelector('style').append(
+        document.createTextNode('body { color: orange !important; }')
+      );
+    </script>
+    <style id="single-textContent">
+      a:hover { outline: 1px solid red; }
+    </style>
+    <style id="empty"></style>
+    <script>
+      // this simulates how <link> is stringified
+      let empty = document.getElementById('empty');
+      empty.sheet.insertRule('a:hover { outline: 1px solid blue; }');
+    </script>
+    <style id="hover-mutation">
+      /* replaceme */
+    </style>
+  </head>
+  <body>
+  </body>
+</html>
diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts
index 34d81da850..0ae70af289 100644
--- a/packages/rrweb/test/integration.test.ts
+++ b/packages/rrweb/test/integration.test.ts
@@ -81,7 +81,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('can record form interactions', async () => {
@@ -98,10 +98,10 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
-  it('can record textarea mutations correctly', async () => {
+  it('can record and replay textarea mutations correctly', async () => {
     const page: puppeteer.Page = await browser.newPage();
     await page.goto('about:blank');
     await page.setContent(getHtml.call(this, 'empty.html'));
@@ -112,20 +112,29 @@ describe('record integration tests', function (this: ISuite) {
       const ta = document.createElement('textarea');
       ta.innerText = 'pre value';
       document.body.append(ta);
+
+      const ta2 = document.createElement('textarea');
+      ta2.id = 'ta2';
+      document.body.append(ta2);
     });
-    await page.waitForTimeout(5);
+    await waitForRAF(page);
     await page.evaluate(() => {
       const t = document.querySelector('textarea') as HTMLTextAreaElement;
       t.innerText = 'ok'; // this mutation should be recorded
+
+      const ta2t = document.createTextNode('added');
+      document.getElementById('ta2').append(ta2t);
     });
-    await page.waitForTimeout(5);
+    await waitForRAF(page);
     await page.evaluate(() => {
       const t = document.querySelector('textarea') as HTMLTextAreaElement;
       (t.childNodes[0] as Text).appendData('3'); // this mutation is also valid
+
+      document.getElementById('ta2').remove(); // done with this
     });
-    await page.waitForTimeout(5);
+    await waitForRAF(page);
     await page.type('textarea', '1'); // types (inserts) at index 0, in front of existing text
-    await page.waitForTimeout(5);
+    await waitForRAF(page);
     await page.evaluate(() => {
       const t = document.querySelector('textarea') as HTMLTextAreaElement;
       // user has typed so childNode content should now be ignored
@@ -136,13 +145,13 @@ 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 waitForRAF(page);
     await page.type('textarea', '2'); // cursor is at index 1
 
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
 
     // check after each mutation and text input
     const replayTextareaValues = await page.evaluate(`
@@ -153,12 +162,18 @@ describe('record integration tests', function (this: ISuite) {
         replayer.pause((e.timestamp - window.snapshots[0].timestamp)+1);
         let ts = replayer.iframe.contentDocument.querySelector('textarea');
         vals.push((e.data.source === 0 ? 'Mutation' : 'User') + ':' + ts.value);
+        let ts2 = replayer.iframe.contentDocument.getElementById('ta2');
+        if (ts2) {
+          vals.push('ta2:' + ts2.value);
+        }
       });
       vals;
     `);
     expect(replayTextareaValues).toEqual([
       'Mutation:pre value',
+      'ta2:',
       'Mutation:ok',
+      'ta2:added',
       'Mutation:ok3',
       'User:1ok3',
       'Mutation:1ok3', // if this gets set to 'ignore', it's an error, as the 'user' has modified the textarea
@@ -166,6 +181,131 @@ describe('record integration tests', function (this: ISuite) {
     ]);
   });
 
+  it('can record and replay style mutations', async () => {
+    // This test shows that the `isStyle` attribute on textContent is not needed in a mutation
+    // TODO: we could get a lot more elaborate here with mixed textContent and insertRule mutations
+    const page: puppeteer.Page = await browser.newPage();
+    await page.goto(`${serverURL}/html`);
+    await page.setContent(getHtml.call(this, 'style.html'));
+
+    await waitForRAF(page); // ensure mutations aren't included in fullsnapshot
+
+    await page.evaluate(() => {
+      let styleEl = document.querySelector('style#dual-textContent');
+      if (styleEl) {
+        styleEl.append(
+          document.createTextNode('body { background-color: darkgreen; }'),
+        );
+        styleEl.append(
+          document.createTextNode(
+            '.absolutify { background-image: url("./rel"); }',
+          ),
+        );
+      }
+    });
+    await waitForRAF(page);
+    await page.evaluate(() => {
+      let styleEl = document.querySelector('style#dual-textContent');
+      if (styleEl) {
+        styleEl.childNodes.forEach((cn) => {
+          if (cn.textContent) {
+            cn.textContent = cn.textContent.replace('darkgreen', 'purple');
+            cn.textContent = cn.textContent.replace(
+              'orange !important',
+              'yellow',
+            );
+          }
+        });
+      }
+    });
+    await waitForRAF(page);
+    await page.evaluate(() => {
+      let styleEl = document.querySelector('style#dual-textContent');
+      if (styleEl) {
+        styleEl.childNodes.forEach((cn) => {
+          if (cn.textContent) {
+            cn.textContent = cn.textContent.replace(
+              'black',
+              'black !important',
+            );
+          }
+        });
+      }
+      let hoverMutationStyleEl = document.querySelector('style#hover-mutation');
+      if (hoverMutationStyleEl) {
+        hoverMutationStyleEl.childNodes.forEach((cn) => {
+          if (cn.textContent) {
+            cn.textContent = 'a:hover { outline: cyan solid 1px; }';
+          }
+        });
+      }
+      let st = document.createElement('style');
+      st.id = 'goldilocks';
+      st.innerText = 'body { color: brown }';
+      document.body.append(st);
+    });
+
+    await waitForRAF(page);
+    await page.evaluate(() => {
+      let styleEl = document.querySelector('style#goldilocks');
+      if (styleEl) {
+        styleEl.childNodes.forEach((cn) => {
+          if (cn.textContent) {
+            cn.textContent = cn.textContent.replace('brown', 'gold');
+          }
+        });
+      }
+    });
+
+    const snapshots = (await page.evaluate(
+      'window.snapshots',
+    )) as eventWithTime[];
+
+    // following ensures that the ./rel url has been absolutized (in a mutation)
+    await assertSnapshot(snapshots);
+
+    // check after each mutation and text input
+    const replayStyleValues = await page.evaluate(`
+      const { Replayer } = rrweb;
+      const replayer = new Replayer(window.snapshots);
+      const vals = [];
+      window.snapshots.filter((e)=>e.data.attributes || e.data.source === 5).forEach((e)=>{
+        replayer.pause((e.timestamp - window.snapshots[0].timestamp)+1);
+        let bodyStyle = getComputedStyle(replayer.iframe.contentDocument.querySelector('body'))
+        vals.push({
+          'background-color': bodyStyle['background-color'],
+          'color': bodyStyle['color'],
+        });
+      });
+      vals.push(replayer.iframe.contentDocument.getElementById('single-textContent').innerText);
+      vals.push(replayer.iframe.contentDocument.getElementById('empty').innerText);
+      vals.push(replayer.iframe.contentDocument.getElementById('hover-mutation').innerText);
+      vals;
+`);
+
+    expect(replayStyleValues).toEqual([
+      {
+        'background-color': 'rgb(0, 100, 0)', // darkgreen
+        color: 'rgb(255, 165, 0)', // orange (from style.html)
+      },
+      {
+        'background-color': 'rgb(128, 0, 128)', // purple
+        color: 'rgb(255, 255, 0)', // yellow
+      },
+      {
+        'background-color': 'rgb(0, 0, 0)', // black !important
+        color: 'rgb(165, 42, 42)', // brown
+      },
+      {
+        'background-color': 'rgb(0, 0, 0)',
+        color: 'rgb(255, 215, 0)', // gold
+      },
+      'a:hover,\na.\\:hover { outline: red solid 1px; }', // has run adaptCssForReplay
+      'a:hover,\na.\\:hover { outline: blue solid 1px; }', // has run adaptCssForReplay
+      'a:hover,\na.\\:hover { outline: cyan solid 1px; }', // has run adaptCssForReplay after text mutation
+    ]);
+  });
+
   it('can record childList mutations', async () => {
     const page: puppeteer.Page = await browser.newPage();
     await page.goto('about:blank');
@@ -183,7 +323,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('can record character data muatations', async () => {
@@ -205,7 +345,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('can record attribute mutation', async () => {
@@ -225,7 +365,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('handles null attribute values', async () => {
@@ -253,7 +393,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('can record node mutations', async () => {
@@ -272,7 +412,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('can record style changes compactly and preserve css var() functions', async () => {
@@ -322,7 +462,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('can freeze mutations', async () => {
@@ -358,7 +498,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should not record input events on ignored elements', async () => {
@@ -394,7 +534,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('can use maskInputOptions to configure which type of inputs should be masked', async () => {
@@ -420,7 +560,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should mask password value attribute with maskInputOptions', async () => {
@@ -444,7 +584,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should mask inputs via function call', async () => {
@@ -474,7 +614,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record input userTriggered values if userTriggeredOnInput is enabled', async () => {
@@ -494,7 +634,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should not record blocked elements and its child nodes', async () => {
@@ -509,7 +649,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should not record blocked elements dynamically added', async () => {
@@ -531,7 +671,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('mutations should work when blocked class is unblocked', async () => {
@@ -552,7 +692,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record DOM node movement 1', async () => {
@@ -572,7 +712,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record DOM node movement 2', async () => {
@@ -589,7 +729,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record dynamic CSS changes', async () => {
@@ -600,7 +740,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record canvas mutations', async () => {
@@ -625,7 +765,7 @@ describe('record integration tests', function (this: ISuite) {
         });
       }
     }
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should not record input values if dynamically added and maskAllInputs is true', async () => {
@@ -662,7 +802,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('can correctly serialize a shader and multiple webgl contexts', async () => {
@@ -677,7 +817,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('will serialize node before record', async () => {
@@ -698,7 +838,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('will defer missing next node mutation', async () => {
@@ -735,7 +875,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record images with blob url', async () => {
@@ -752,7 +892,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record images inside iframe with blob url', async () => {
@@ -769,7 +909,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record images inside iframe with blob url after iframe was reloaded', async () => {
@@ -792,7 +932,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record shadow DOM', async () => {
@@ -842,7 +982,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record shadow DOM 2', async () => {
@@ -867,7 +1007,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record shadow DOM 3', async () => {
@@ -888,7 +1028,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record moved shadow DOM', async () => {
@@ -917,7 +1057,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record moved shadow DOM 2', async () => {
@@ -955,7 +1095,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record nested iframes and shadow doms', async () => {
@@ -1000,7 +1140,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record mutations in iframes accross pages', async () => {
@@ -1030,7 +1170,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   // https://github.com/webcomponents/polyfills/tree/master/packages/shadydom
@@ -1064,7 +1204,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   // https://github.com/salesforce/lwc/tree/master/packages/%40lwc/synthetic-shadow
@@ -1106,7 +1246,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should mask texts', async () => {
@@ -1121,7 +1261,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should mask texts using maskTextFn', async () => {
@@ -1137,7 +1277,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should unmask texts using maskTextFn', async () => {
@@ -1157,7 +1297,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('can mask character data mutations', async () => {
@@ -1186,7 +1326,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('can mask character data mutations with regexp', async () => {
@@ -1219,7 +1359,7 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 
   it('should record after DOMContentLoaded event', async () => {
@@ -1234,6 +1374,175 @@ describe('record integration tests', function (this: ISuite) {
     const snapshots = (await page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
+  });
+
+  /**
+   * the regression part of the following is now handled by replayer.test.ts::'can deal with duplicate/conflicting values on style elements'
+   * so this test could be dropped if we add more robust mixing of `insertRule` into 'can record and replay style mutations'
+   */
+  it('should record style mutations and replay them correctly', async () => {
+    const page: puppeteer.Page = await browser.newPage();
+    const OldColor = 'rgb(255, 0, 0)'; // red color
+    const NewColor = 'rgb(255, 255, 0)'; // yellow color
+
+    await page.setContent(
+      `
+      <!DOCTYPE html><html lang="en">
+        <head>
+	        <style> 
+          </style>
+        </head>
+        <body>
+	        <div id="one"></div>
+          <div id="two"></div>
+	        <script>
+		        document.querySelector("style").sheet.insertRule('#one { color: ${OldColor}; }', 0);
+	        </script>
+        </body></html>
+      `,
+    );
+    // Start rrweb recording
+    await page.evaluate(
+      (code, recordSnippet) => {
+        const script = document.createElement('script');
+        script.textContent = `${code}window.Date.now = () => new Date(Date.UTC(2018, 10, 15, 8)).valueOf();${recordSnippet}`;
+        document.head.appendChild(script);
+      },
+      code,
+      generateRecordSnippet({}),
+    );
+
+    await page.evaluate(
+      async (OldColor, NewColor) => {
+        // Create a new style element with the same content as the existing style element and apply it to the #two div element
+        const incrementalStyle = document.createElement(
+          'style',
+        ) as HTMLStyleElement;
+        incrementalStyle.textContent = ` \n`;
+        document.head.appendChild(incrementalStyle);
+        incrementalStyle.sheet!.insertRule(`#two { color: ${OldColor}; }`, 0);
+
+        await new Promise((resolve) =>
+          requestAnimationFrame(() => {
+            requestAnimationFrame(resolve);
+          }),
+        );
+
+        // Change the color of the #one div element to yellow as an incremental style mutation
+        const styleElement = document.querySelector('style')!;
+        (styleElement.sheet!.cssRules[0] as any).style.setProperty(
+          'color',
+          NewColor,
+        );
+        // Change the color of the #two div element to yellow as an incremental style mutation
+        (incrementalStyle.sheet!.cssRules[0] as any).style.setProperty(
+          'color',
+          NewColor,
+        );
+      },
+      OldColor,
+      NewColor,
+    );
+    await waitForRAF(page);
+
+    const snapshots = (await page.evaluate(
+      'window.snapshots',
+    )) as eventWithTime[];
+    await assertSnapshot(snapshots);
+
+    /**
+     * Replay the recorded events and check if the style mutation is applied correctly
+     */
+    const changedColors = await page.evaluate(`
+      const { Replayer } = rrweb;
+      const replayer = new Replayer(window.snapshots);
+      replayer.pause(1000);
+
+      // Get the color of the element after applying the style mutation event
+      [
+        window.getComputedStyle(
+          replayer.iframe.contentDocument.querySelector('#one'),
+        ).color,
+        window.getComputedStyle(
+          replayer.iframe.contentDocument.querySelector('#two'),
+        ).color,
+      ];
+    `);
+    expect(changedColors).toEqual([NewColor, NewColor]);
+    await page.close();
+  });
+
+  it('should record style mutations with multiple child nodes and replay them correctly', async () => {
+    // ensure that presence of multiple text nodes doesn't interfere with programmatic insertRule operations
+
+    const page: puppeteer.Page = await browser.newPage();
+    const Color = 'rgb(255, 0, 0)'; // red color
+
+    await page.setContent(
+      `
+      <!DOCTYPE html><html lang="en">
+        <head>
+	        <style>
+          /* hello */
+          </style>
+        </head>
+        <body>
+	        <div id="one"></div>
+          <div id="two"></div>
+	        <script>
+		        document.querySelector("style").append(document.createTextNode("/* world */"));
+		        document.querySelector("style").sheet.insertRule('#one { color: ${Color}; }', 0);
+	        </script>
+        </body></html>
+      `,
+    );
+    // Start rrweb recording
+    await page.evaluate(
+      (code, recordSnippet) => {
+        const script = document.createElement('script');
+        script.textContent = `${code};${recordSnippet}`;
+        document.head.appendChild(script);
+      },
+      code,
+      generateRecordSnippet({}),
+    );
+
+    await page.evaluate(async (Color) => {
+      // Create a new style element with the same content as the existing style element and apply it to the #two div element
+      const incrementalStyle = document.createElement(
+        'style',
+      ) as HTMLStyleElement;
+      incrementalStyle.append(document.createTextNode('/* hello */'));
+      incrementalStyle.append(document.createTextNode('/* world */'));
+      document.head.appendChild(incrementalStyle);
+      incrementalStyle.sheet!.insertRule(`#two { color: ${Color}; }`, 0);
+    }, Color);
+
+    const snapshots = (await page.evaluate(
+      'window.snapshots',
+    )) as eventWithTime[];
+    await assertSnapshot(snapshots);
+
+    /**
+     * Replay the recorded events and check if the style mutation is applied correctly
+     */
+    const changedColors = await page.evaluate(`
+      const { Replayer } = rrweb;
+      const replayer = new Replayer(window.snapshots);
+      replayer.pause(1000);
+
+      // Get the color of the element after applying the style mutation event
+      [
+        window.getComputedStyle(
+          replayer.iframe.contentDocument.querySelector('#one'),
+        ).color,
+        window.getComputedStyle(
+          replayer.iframe.contentDocument.querySelector('#two'),
+        ).color,
+      ];
+    `);
+    expect(changedColors).toEqual([Color, Color]);
+    await page.close();
   });
 });
diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts
index d35dd6f6a2..72a8a7fbe8 100644
--- a/packages/rrweb/test/record.test.ts
+++ b/packages/rrweb/test/record.test.ts
@@ -206,7 +206,7 @@ describe('record', function (this: ISuite) {
       }, 10);
     });
     await ctx.page.waitForTimeout(100);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('should record scroll position', async () => {
@@ -223,7 +223,7 @@ describe('record', function (this: ISuite) {
       p.scrollLeft = 10;
     });
     await waitForRAF(ctx.page);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('should record selection event', async () => {
@@ -279,7 +279,7 @@ describe('record', function (this: ISuite) {
       });
     });
     await ctx.page.waitForTimeout(50);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('captures stylesheet rules', async () => {
@@ -297,6 +297,7 @@ describe('record', function (this: ISuite) {
       // begin: pre-serialization
       const ruleIdx0 = styleSheet.insertRule('body { background: #000; }');
       const ruleIdx1 = styleSheet.insertRule('body { background: #111; }');
+
       styleSheet.deleteRule(ruleIdx1);
       // end: pre-serialization
       setTimeout(() => {
@@ -328,8 +329,71 @@ describe('record', function (this: ISuite) {
         rule: 'body { color: #fff; }',
       },
     ]);
+    expect((addRules[1].data as styleSheetRuleData).adds).toEqual([
+      {
+        rule: 'body { color: #ccc; }',
+      },
+    ]);
+    expect(removeRuleCount).toEqual(1);
+    await assertSnapshot(ctx.events);
+  });
+
+  it('captures stylesheet rules with deprecated addRule & removeRule properties', async () => {
+    await ctx.page.evaluate(() => {
+      const { record } = (window as unknown as IWindow).rrweb;
+
+      record({
+        emit: (window as unknown as IWindow).emit,
+      });
+
+      const styleElement = document.createElement('style');
+      document.head.appendChild(styleElement);
+
+      const styleSheet = <CSSStyleSheet>styleElement.sheet;
+      // begin: pre-serialization
+      const ruleIdx0 = styleSheet.addRule('body', 'background: #000;');
+      const ruleIdx1 = styleSheet.addRule('body', 'background: #111;');
+
+      styleSheet.removeRule(ruleIdx1);
+      // end: pre-serialization
+      setTimeout(() => {
+        styleSheet.addRule('body', 'color: #fff;');
+      }, 0);
+      setTimeout(() => {
+        styleSheet.removeRule(ruleIdx0);
+      }, 5);
+      setTimeout(() => {
+        styleSheet.addRule('body', 'color: #ccc;');
+      }, 10);
+    });
+    await ctx.page.waitForTimeout(50);
+    const styleSheetRuleEvents = ctx.events.filter(
+      (e) =>
+        e.type === EventType.IncrementalSnapshot &&
+        e.data.source === IncrementalSource.StyleSheetRule,
+    );
+    const addRules = styleSheetRuleEvents.filter((e) =>
+      Boolean((e.data as styleSheetRuleData).adds),
+    );
+    const removeRuleCount = styleSheetRuleEvents.filter((e) =>
+      Boolean((e.data as styleSheetRuleData).removes),
+    ).length;
+    // pre-serialization insert/delete should be ignored
+    expect(addRules.length).toEqual(2);
+    expect((addRules[0].data as styleSheetRuleData).adds).toEqual([
+      {
+        index: 1,
+        rule: 'body { color: #fff; }',
+      },
+    ]);
+    expect((addRules[1].data as styleSheetRuleData).adds).toEqual([
+      {
+        index: 1,
+        rule: 'body { color: #ccc; }',
+      },
+    ]);
     expect(removeRuleCount).toEqual(1);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   const captureNestedStylesheetRulesTest = async () => {
@@ -375,7 +439,7 @@ describe('record', function (this: ISuite) {
     // sync insert/delete should be ignored
     expect(addRuleCount).toEqual(2);
     expect(removeRuleCount).toEqual(1);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   };
   it('captures nested stylesheet rules', captureNestedStylesheetRulesTest);
 
@@ -426,7 +490,7 @@ describe('record', function (this: ISuite) {
       }, 0);
     });
     await ctx.page.waitForTimeout(50);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('captures inserted style text nodes correctly', async () => {
@@ -446,7 +510,7 @@ describe('record', function (this: ISuite) {
       styleEl.append(document.createTextNode('h1 { color: pink; }'));
     });
     await waitForRAF(ctx.page);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('captures stylesheets with `blob:` url', async () => {
@@ -473,7 +537,7 @@ describe('record', function (this: ISuite) {
       });
     });
     await waitForRAF(ctx.page);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('captures mutations on adopted stylesheets', async () => {
@@ -538,7 +602,7 @@ describe('record', function (this: ISuite) {
       });
     });
     await waitForRAF(ctx.page);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('captures adopted stylesheets in nested shadow doms and iframes', async () => {
@@ -590,7 +654,7 @@ describe('record', function (this: ISuite) {
       }, 150);
     });
     await ctx.page.waitForTimeout(200);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('captures adopted stylesheets of shadow doms in checkout full snapshot', async () => {
@@ -619,7 +683,7 @@ describe('record', function (this: ISuite) {
       });
     });
     await waitForRAF(ctx.page);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('captures stylesheets in iframes with `blob:` url', async () => {
@@ -651,7 +715,7 @@ describe('record', function (this: ISuite) {
       });
     });
     await waitForRAF(ctx.page);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('aggregates mutations', async () => {
@@ -698,7 +762,7 @@ describe('record', function (this: ISuite) {
     );
     expect(mutationEvents.length).toEqual(0); // there was no aggregate effect
 
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('no need for attribute mutations on adds', async () => {
@@ -737,7 +801,7 @@ describe('record', function (this: ISuite) {
     );
     expect(mutationEvents.length).toEqual(1);
 
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   describe('loading stylesheets', () => {
@@ -789,7 +853,7 @@ describe('record', function (this: ISuite) {
       await ctx.page.waitForResponse(`${serverURL}/html/assets/style.css`);
       await waitForRAF(ctx.page);
 
-      assertSnapshot(ctx.events);
+      await assertSnapshot(ctx.events);
     });
 
     it('captures stylesheets in iframes that are still loading', async () => {
@@ -823,7 +887,7 @@ describe('record', function (this: ISuite) {
 
       await waitForRAF(ctx.page);
 
-      assertSnapshot(ctx.events);
+      await assertSnapshot(ctx.events);
     });
   });
 
@@ -849,7 +913,7 @@ describe('record', function (this: ISuite) {
     await ctx.page.waitForResponse(corsStylesheetURL); // wait for stylesheet to be loaded
     await waitForRAF(ctx.page); // wait for rrweb to emit events
 
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('captures adopted stylesheets in shadow doms and iframe', async () => {
@@ -924,7 +988,7 @@ describe('record', function (this: ISuite) {
     });
     await waitForRAF(ctx.page); // wait till events get sent
 
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 });
 
@@ -1025,6 +1089,6 @@ describe('record iframes', function (this: ISuite) {
     expect(styleRelatedEvents.length).toEqual(5);
     expect(addRuleCount).toEqual(2);
     expect(removeRuleCount).toEqual(2);
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 });
diff --git a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap
new file mode 100644
index 0000000000..03526f8c0f
--- /dev/null
+++ b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap
@@ -0,0 +1,487 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`dialog > add dialog and show 1`] = `
+"[
+  {
+    \\"type\\": 4,
+    \\"data\\": {
+      \\"href\\": \\"about:blank\\",
+      \\"width\\": 1920,
+      \\"height\\": 1080
+    }
+  },
+  {
+    \\"type\\": 2,
+    \\"data\\": {
+      \\"node\\": {
+        \\"type\\": 0,
+        \\"childNodes\\": [
+          {
+            \\"type\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {
+                      \\"type\\": \\"text/javascript\\"
+                    },
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 5
+                      }
+                    ],
+                    \\"id\\": 4
+                  }
+                ],
+                \\"id\\": 3
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 7
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"dialog\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"I'm a dialog\\",
+                        \\"id\\": 9
+                      }
+                    ],
+                    \\"id\\": 8
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\\\n\\\\n\\",
+                    \\"id\\": 10
+                  }
+                ],
+                \\"id\\": 6
+              }
+            ],
+            \\"id\\": 2
+          }
+        ],
+        \\"compatMode\\": \\"BackCompat\\",
+        \\"id\\": 1
+      },
+      \\"initialOffset\\": {
+        \\"left\\": 0,
+        \\"top\\": 0
+      }
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [],
+      \\"removes\\": [],
+      \\"adds\\": [
+        {
+          \\"parentId\\": 6,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"dialog\\",
+            \\"attributes\\": {
+              \\"open\\": \\"\\",
+              \\"rr_open_mode\\": \\"non-modal\\"
+            },
+            \\"childNodes\\": [],
+            \\"id\\": 11
+          }
+        }
+      ]
+    }
+  }
+]"
+`;
+
+exports[`dialog > add dialog and showModal 1`] = `
+"[
+  {
+    \\"type\\": 4,
+    \\"data\\": {
+      \\"href\\": \\"about:blank\\",
+      \\"width\\": 1920,
+      \\"height\\": 1080
+    }
+  },
+  {
+    \\"type\\": 2,
+    \\"data\\": {
+      \\"node\\": {
+        \\"type\\": 0,
+        \\"childNodes\\": [
+          {
+            \\"type\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {
+                      \\"type\\": \\"text/javascript\\"
+                    },
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 5
+                      }
+                    ],
+                    \\"id\\": 4
+                  }
+                ],
+                \\"id\\": 3
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 7
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"dialog\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"I'm a dialog\\",
+                        \\"id\\": 9
+                      }
+                    ],
+                    \\"id\\": 8
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\\\n\\\\n\\",
+                    \\"id\\": 10
+                  }
+                ],
+                \\"id\\": 6
+              }
+            ],
+            \\"id\\": 2
+          }
+        ],
+        \\"compatMode\\": \\"BackCompat\\",
+        \\"id\\": 1
+      },
+      \\"initialOffset\\": {
+        \\"left\\": 0,
+        \\"top\\": 0
+      }
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [],
+      \\"removes\\": [],
+      \\"adds\\": [
+        {
+          \\"parentId\\": 6,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"dialog\\",
+            \\"attributes\\": {
+              \\"open\\": \\"\\",
+              \\"rr_open_mode\\": \\"modal\\"
+            },
+            \\"childNodes\\": [],
+            \\"id\\": 11
+          }
+        }
+      ]
+    }
+  }
+]"
+`;
+
+exports[`dialog > switch to show dialog 1`] = `
+"[
+  {
+    \\"type\\": 4,
+    \\"data\\": {
+      \\"href\\": \\"about:blank\\",
+      \\"width\\": 1920,
+      \\"height\\": 1080
+    }
+  },
+  {
+    \\"type\\": 2,
+    \\"data\\": {
+      \\"node\\": {
+        \\"type\\": 0,
+        \\"childNodes\\": [
+          {
+            \\"type\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {
+                      \\"type\\": \\"text/javascript\\"
+                    },
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 5
+                      }
+                    ],
+                    \\"id\\": 4
+                  }
+                ],
+                \\"id\\": 3
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 7
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"dialog\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"I'm a dialog\\",
+                        \\"id\\": 9
+                      }
+                    ],
+                    \\"id\\": 8
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\\\n\\\\n\\",
+                    \\"id\\": 10
+                  }
+                ],
+                \\"id\\": 6
+              }
+            ],
+            \\"id\\": 2
+          }
+        ],
+        \\"compatMode\\": \\"BackCompat\\",
+        \\"id\\": 1
+      },
+      \\"initialOffset\\": {
+        \\"left\\": 0,
+        \\"top\\": 0
+      }
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 8,
+          \\"attributes\\": {
+            \\"open\\": \\"\\",
+            \\"rr_open_mode\\": \\"modal\\"
+          }
+        }
+      ],
+      \\"removes\\": [],
+      \\"adds\\": []
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 8,
+          \\"attributes\\": {
+            \\"open\\": \\"\\",
+            \\"rr_open_mode\\": \\"non-modal\\"
+          }
+        }
+      ],
+      \\"removes\\": [],
+      \\"adds\\": []
+    }
+  }
+]"
+`;
+
+exports[`dialog > switch to showModal dialog 1`] = `
+"[
+  {
+    \\"type\\": 4,
+    \\"data\\": {
+      \\"href\\": \\"about:blank\\",
+      \\"width\\": 1920,
+      \\"height\\": 1080
+    }
+  },
+  {
+    \\"type\\": 2,
+    \\"data\\": {
+      \\"node\\": {
+        \\"type\\": 0,
+        \\"childNodes\\": [
+          {
+            \\"type\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {
+                      \\"type\\": \\"text/javascript\\"
+                    },
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 5
+                      }
+                    ],
+                    \\"id\\": 4
+                  }
+                ],
+                \\"id\\": 3
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\",
+                    \\"id\\": 7
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"dialog\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"I'm a dialog\\",
+                        \\"id\\": 9
+                      }
+                    ],
+                    \\"id\\": 8
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\\\n\\\\n\\",
+                    \\"id\\": 10
+                  }
+                ],
+                \\"id\\": 6
+              }
+            ],
+            \\"id\\": 2
+          }
+        ],
+        \\"compatMode\\": \\"BackCompat\\",
+        \\"id\\": 1
+      },
+      \\"initialOffset\\": {
+        \\"left\\": 0,
+        \\"top\\": 0
+      }
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 8,
+          \\"attributes\\": {
+            \\"open\\": \\"\\",
+            \\"rr_open_mode\\": \\"non-modal\\"
+          }
+        }
+      ],
+      \\"removes\\": [],
+      \\"adds\\": []
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 8,
+          \\"attributes\\": {
+            \\"open\\": \\"\\",
+            \\"rr_open_mode\\": \\"modal\\"
+          }
+        }
+      ],
+      \\"removes\\": [],
+      \\"adds\\": []
+    }
+  }
+]"
+`;
diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts
index 2cdf69f006..32aa4644eb 100644
--- a/packages/rrweb/test/record/cross-origin-iframes.test.ts
+++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts
@@ -53,7 +53,11 @@ async function injectRecordScript(
   } catch (e) {
     // we get this error: `Protocol error (DOM.resolveNode): Node with given id does not belong to the document`
     // then the page wasn't loaded yet and we try again
-    if (!e.message.includes('DOM.resolveNode')) throw e;
+    if (
+      !e.message.includes('DOM.resolveNode') ||
+      !e.message.includes('DOM.describeNode')
+    )
+      throw e;
     await injectRecordScript(frame, options);
     return;
   }
@@ -238,7 +242,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
 
     it('should map scroll events correctly', async () => {
@@ -261,7 +265,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
   });
 
@@ -289,7 +293,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
 
     it('should record DOM node removal', async () => {
@@ -301,7 +305,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
 
     it('should record DOM attribute changes', async () => {
@@ -313,7 +317,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
 
     it('should record DOM text changes', async () => {
@@ -325,7 +329,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
 
     it('should record canvas elements', async () => {
@@ -342,7 +346,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
 
     it('should record custom events', async () => {
@@ -358,7 +362,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
 
     it('captures mutations on adopted stylesheets', async () => {
@@ -421,7 +425,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
 
     it('captures mutations on stylesheets', async () => {
@@ -476,7 +480,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
   });
 
@@ -505,7 +509,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
   });
 
@@ -537,7 +541,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
 
     it('should filter out forwarded cross origin rrweb messages', async () => {
@@ -564,7 +568,7 @@ describe('cross origin iframes', function (this: ISuite) {
       const snapshots = (await ctx.page.evaluate(
         'window.snapshots',
       )) as eventWithTime[];
-      assertSnapshot(snapshots);
+      await assertSnapshot(snapshots);
     });
   });
 });
@@ -592,7 +596,7 @@ describe('same origin iframes', function (this: ISuite) {
     // two events (full snapshot + meta) from main frame,
     // and two (full snapshot + mutation) from iframe
     expect(events.length).toBe(4);
-    assertSnapshot(events);
+    await assertSnapshot(events);
   });
 
   it('should record cross-origin iframe in same-origin iframe', async () => {
@@ -616,6 +620,6 @@ describe('same origin iframes', function (this: ISuite) {
     const snapshots = (await ctx.page.evaluate(
       'window.snapshots',
     )) as eventWithTime[];
-    assertSnapshot(snapshots);
+    await assertSnapshot(snapshots);
   });
 });
diff --git a/packages/rrweb/test/record/dialog.test.ts b/packages/rrweb/test/record/dialog.test.ts
new file mode 100644
index 0000000000..ab6542b547
--- /dev/null
+++ b/packages/rrweb/test/record/dialog.test.ts
@@ -0,0 +1,229 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import { vi } from 'vitest';
+
+import {
+  assertSnapshot,
+  getServerURL,
+  ISuite,
+  launchPuppeteer,
+  startServer,
+  waitForRAF,
+} from '../utils';
+import {
+  attributeMutation,
+  EventType,
+  eventWithTime,
+  listenerHandler,
+} from '@rrweb/types';
+import { recordOptions } from '../../src/types';
+
+interface IWindow extends Window {
+  rrweb: {
+    record: (
+      options: recordOptions<eventWithTime>,
+    ) => listenerHandler | undefined;
+    addCustomEvent<T>(tag: string, payload: T): void;
+  };
+  emit: (e: eventWithTime) => undefined;
+}
+
+const attributeMutationFactory = (
+  mutation: attributeMutation['attributes'],
+) => {
+  return {
+    data: {
+      attributes: [
+        {
+          attributes: mutation,
+        },
+      ],
+    },
+  };
+};
+
+describe('dialog', () => {
+  vi.setConfig({ testTimeout: 100_000 });
+  let code: ISuite['code'];
+  let page: ISuite['page'];
+  let browser: ISuite['browser'];
+  let server: ISuite['server'];
+  let serverURL: ISuite['serverURL'];
+  let events: ISuite['events'];
+
+  beforeAll(async () => {
+    server = await startServer();
+    serverURL = getServerURL(server);
+    browser = await launchPuppeteer();
+
+    const bundlePath = path.resolve(__dirname, '../../dist/rrweb.umd.cjs');
+    code = fs.readFileSync(bundlePath, 'utf8');
+  });
+
+  afterEach(async () => {
+    await page.close();
+  });
+
+  afterAll(async () => {
+    await server.close();
+    await browser.close();
+  });
+
+  beforeEach(async () => {
+    page = await browser.newPage();
+    page.on('console', (msg) => {
+      console.log(msg.text());
+    });
+
+    await page.goto(`${serverURL}/html/dialog.html`);
+    await page.addScriptTag({
+      path: path.resolve(__dirname, '../../dist/rrweb.umd.cjs'),
+    });
+    await waitForRAF(page);
+    events = [];
+
+    await page.exposeFunction('emit', (e: eventWithTime) => {
+      if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) {
+        return;
+      }
+      events.push(e);
+    });
+
+    page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
+
+    await page.evaluate(() => {
+      const { record } = (window as unknown as IWindow).rrweb;
+      record({
+        emit: (window as unknown as IWindow).emit,
+      });
+    });
+
+    await waitForRAF(page);
+  });
+
+  it('show dialog', async () => {
+    await page.evaluate(() => {
+      const dialog = document.querySelector('dialog') as HTMLDialogElement;
+      dialog.show();
+    });
+
+    const lastEvent = events[events.length - 1];
+
+    expect(lastEvent).toMatchObject(attributeMutationFactory({ open: '' }));
+    // assertSnapshot(events);
+  });
+
+  it('showModal dialog', async () => {
+    await page.evaluate(() => {
+      const dialog = document.querySelector('dialog') as HTMLDialogElement;
+      dialog.showModal();
+    });
+
+    const lastEvent = events[events.length - 1];
+
+    expect(lastEvent).toMatchObject(
+      attributeMutationFactory({ rr_open_mode: 'modal' }),
+    );
+  });
+
+  it('showModal & close dialog', async () => {
+    await page.evaluate(() => {
+      const dialog = document.querySelector('dialog') as HTMLDialogElement;
+      dialog.showModal();
+    });
+    await waitForRAF(page);
+    await page.evaluate(() => {
+      const dialog = document.querySelector('dialog') as HTMLDialogElement;
+      dialog.close();
+    });
+
+    const lastEvent = events[events.length - 1];
+
+    expect(lastEvent).toMatchObject(attributeMutationFactory({ open: null }));
+  });
+
+  it('show & close dialog', async () => {
+    await page.evaluate(() => {
+      const dialog = document.querySelector('dialog') as HTMLDialogElement;
+      dialog.show();
+    });
+    await waitForRAF(page);
+    await page.evaluate(() => {
+      const dialog = document.querySelector('dialog') as HTMLDialogElement;
+      dialog.close();
+    });
+
+    const lastEvent = events[events.length - 1];
+
+    expect(lastEvent).toMatchObject(attributeMutationFactory({ open: null }));
+  });
+
+  it('switch to showModal dialog', async () => {
+    await page.evaluate(() => {
+      const dialog = document.querySelector('dialog') as HTMLDialogElement;
+      dialog.show();
+    });
+    await waitForRAF(page);
+    await page.evaluate(() => {
+      const dialog = document.querySelector('dialog') as HTMLDialogElement;
+      dialog.close();
+      dialog.showModal();
+    });
+
+    await assertSnapshot(events);
+  });
+
+  it('switch to show dialog', async () => {
+    await page.evaluate(() => {
+      const dialog = document.querySelector('dialog') as HTMLDialogElement;
+      dialog.showModal();
+    });
+    await waitForRAF(page);
+    await page.evaluate(() => {
+      const dialog = document.querySelector('dialog') as HTMLDialogElement;
+      dialog.close();
+      dialog.show();
+    });
+
+    await assertSnapshot(events);
+  });
+
+  it('add dialog and showModal', async () => {
+    await page.evaluate(() => {
+      const dialog = document.createElement('dialog') as HTMLDialogElement;
+      document.body.appendChild(dialog);
+      dialog.showModal();
+    });
+    await waitForRAF(page);
+
+    await assertSnapshot(events);
+  });
+
+  it('add dialog and show', async () => {
+    await page.evaluate(() => {
+      const dialog = document.createElement('dialog') as HTMLDialogElement;
+      document.body.appendChild(dialog);
+      dialog.show();
+    });
+    await waitForRAF(page);
+
+    await assertSnapshot(events);
+  });
+
+  // TODO: implement me in the future
+  it.skip('should record playback order with multiple dialogs opening', async () => {
+    await page.evaluate(() => {
+      const dialog1 = document.createElement('dialog') as HTMLDialogElement;
+      dialog1.className = 'dialog1';
+      document.body.appendChild(dialog1);
+      const dialog2 = document.createElement('dialog') as HTMLDialogElement;
+      dialog1.className = 'dialog2';
+      document.body.appendChild(dialog2);
+      dialog2.showModal(); // <== Note that dialog TWO is being triggered first
+      dialog1.showModal();
+    });
+
+    await waitForRAF(page);
+    await assertSnapshot(events); // <== This should trigger showModal() on dialog2 first, then dialog1
+  });
+});
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index c19023732a..87999c458f 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -124,7 +124,7 @@ describe('record webgl', function (this: ISuite) {
         ],
       },
     });
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('will record changes to a webgl2 canvas element', async () => {
@@ -150,7 +150,7 @@ describe('record webgl', function (this: ISuite) {
         ],
       },
     });
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('will record changes to a canvas element before the canvas gets added', async () => {
@@ -165,7 +165,7 @@ describe('record webgl', function (this: ISuite) {
 
     await waitForRAF(ctx.page);
 
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('will record changes to a canvas element before the canvas gets added (webgl2)', async () => {
@@ -188,7 +188,7 @@ describe('record webgl', function (this: ISuite) {
     // we need to change this
     await waitForRAF(ctx.page);
 
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('will record webgl variables', async () => {
@@ -203,7 +203,7 @@ describe('record webgl', function (this: ISuite) {
 
     await ctx.page.waitForTimeout(50);
 
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('will record webgl variables in reverse order', async () => {
@@ -219,7 +219,7 @@ describe('record webgl', function (this: ISuite) {
 
     await ctx.page.waitForTimeout(50);
 
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
   });
 
   it('sets _context on canvas.getContext()', async () => {
@@ -264,7 +264,7 @@ describe('record webgl', function (this: ISuite) {
 
     await ctx.page.waitForTimeout(50);
 
-    assertSnapshot(ctx.events);
+    await assertSnapshot(ctx.events);
     expect(ctx.events.length).toEqual(5);
   });
 
@@ -312,7 +312,7 @@ describe('record webgl', function (this: ISuite) {
       await waitForRAF(ctx.page);
 
       // should yield a frame for each change at a max of 60fps
-      assertSnapshot(stripBase64(ctx.events));
+      await assertSnapshot(stripBase64(ctx.events));
     });
   });
 });
diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-closed-dialogs-show-nothing-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-closed-dialogs-show-nothing-1-snap.png
new file mode 100644
index 0000000000000000000000000000000000000000..9fb34401f4292e123559cdb06cd35f8842b1c146
GIT binary patch
literal 10592
zcmeHNJxjw-6g{b-wxyAXOB986aqi$y@T<m13`mjS4~UbC;NYNxLXa*_I!OnAL+kF)
z*IgZS5*!^A1UI3B;JbPD7wFL8yd{^%OL!d4x%Y6$`F1mON;4$@PP7@sz<3JS^+I0n
zygl{T^}{-hLm!=o*>|9VC|KJ$zW#XbJF4QUo5$~?m8*syeA$D&`@0i;T-MTOZ_ZRu
z)A^by8!fABDt8xSrV2b|3QY2tAVEMfK9C_943ta;1Dk_^fz5%<fm)yzz`4M=0D7d1
z1k?hxKrI*!Pz%%owLmQ}NBVQpc)PrKX=x^TuLq|W-GyA+RfboX%6X<r{9%l_RY=TM
zS{!DawQ4G_>osrx2B4}1iB<LD#{gnRYKRsnC$MV(Y>og23c$HZdmv+h_F#1FLD;RX
z0>ugW7hrU6$(zw$2?DfN8UxxZ+AGll?G^2n@PPK}zwJRT?3MRl>hVC}jW(J=$4}0F
E0Mb(<f&c&j

literal 0
HcmV?d00001

diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-alternative.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-alternative.png
new file mode 100644
index 0000000000000000000000000000000000000000..f328c34b5b1b95a917d80f69c2fa391285891f97
GIT binary patch
literal 12721
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

literal 0
HcmV?d00001

diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..f328c34b5b1b95a917d80f69c2fa391285891f97
GIT binary patch
literal 12721
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

literal 0
HcmV?d00001

diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed.png
new file mode 100644
index 0000000000000000000000000000000000000000..9fb34401f4292e123559cdb06cd35f8842b1c146
GIT binary patch
literal 10592
zcmeHNJxjw-6g{b-wxyAXOB986aqi$y@T<m13`mjS4~UbC;NYNxLXa*_I!OnAL+kF)
z*IgZS5*!^A1UI3B;JbPD7wFL8yd{^%OL!d4x%Y6$`F1mON;4$@PP7@sz<3JS^+I0n
zygl{T^}{-hLm!=o*>|9VC|KJ$zW#XbJF4QUo5$~?m8*syeA$D&`@0i;T-MTOZ_ZRu
z)A^by8!fABDt8xSrV2b|3QY2tAVEMfK9C_943ta;1Dk_^fz5%<fm)yzz`4M=0D7d1
z1k?hxKrI*!Pz%%owLmQ}NBVQpc)PrKX=x^TuLq|W-GyA+RfboX%6X<r{9%l_RY=TM
zS{!DawQ4G_>osrx2B4}1iB<LD#{gnRYKRsnC$MV(Y>og23c$HZdmv+h_F#1FLD;RX
z0>ugW7hrU6$(zw$2?DfN8UxxZ+AGll?G^2n@PPK}zwJRT?3MRl>hVC}jW(J=$4}0F
E0Mb(<f&c&j

literal 0
HcmV?d00001

diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..790b97b6a4fd8e9d1f983945737e44897070edb9
GIT binary patch
literal 12261
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

literal 0
HcmV?d00001

diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..679ab53ab6018a241532b95e3e29e22a1d15e676
GIT binary patch
literal 12505
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

literal 0
HcmV?d00001

diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal.png
new file mode 100644
index 0000000000000000000000000000000000000000..5fe6c04ee504091f90b95de22a0827aa7511188c
GIT binary patch
literal 12690
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

literal 0
HcmV?d00001

diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-and-show-modal.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-and-show-modal.png
new file mode 100644
index 0000000000000000000000000000000000000000..5fe6c04ee504091f90b95de22a0827aa7511188c
GIT binary patch
literal 12690
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

literal 0
HcmV?d00001

diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3faffd85acf2672b5411a7b05ffea0471ffe11e
GIT binary patch
literal 12445
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
WNin}nDi?M_AL92B_U7*)ef=|oQDxTv

literal 0
HcmV?d00001

diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-the-dialog-when-open-attribute-gets-added.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-the-dialog-when-open-attribute-gets-added.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3faffd85acf2672b5411a7b05ffea0471ffe11e
GIT binary patch
literal 12445
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
WNin}nDi?M_AL92B_U7*)ef=|oQDxTv

literal 0
HcmV?d00001

diff --git a/packages/rrweb/test/replay/dialog.test.ts b/packages/rrweb/test/replay/dialog.test.ts
new file mode 100644
index 0000000000..7fc1ab99f2
--- /dev/null
+++ b/packages/rrweb/test/replay/dialog.test.ts
@@ -0,0 +1,159 @@
+import * as fs from 'fs';
+import { toMatchImageSnapshot } from 'jest-image-snapshot';
+import * as path from 'path';
+import { vi } from 'vitest';
+
+import dialogPlaybackEvents, {
+  closedFullSnapshotTime,
+  showIncrementalAttributeTime,
+  closeIncrementalAttributeTime,
+  showModalIncrementalAttributeTime,
+  showFullSnapshotTime,
+  showModalFullSnapshotTime,
+  showModalIncrementalAddTime,
+  switchBetweenShowModalAndShowIncrementalAttributeTime,
+  switchBetweenShowAndShowModalIncrementalAttributeTime,
+} from '../events/dialog-playback';
+import {
+  fakeGoto,
+  getServerURL,
+  hideMouseAnimation,
+  ISuite,
+  launchPuppeteer,
+  startServer,
+  waitForRAF,
+} from '../utils';
+
+expect.extend({ toMatchImageSnapshot });
+
+describe('dialog', () => {
+  vi.setConfig({ testTimeout: 100_000 });
+  let code: ISuite['code'];
+  let page: ISuite['page'];
+  let browser: ISuite['browser'];
+  let server: ISuite['server'];
+  let serverURL: ISuite['serverURL'];
+
+  beforeAll(async () => {
+    server = await startServer();
+    serverURL = getServerURL(server);
+    browser = await launchPuppeteer();
+
+    const bundlePath = path.resolve(__dirname, '../../dist/rrweb.umd.cjs');
+    code = fs.readFileSync(bundlePath, 'utf8');
+  });
+
+  afterEach(async () => {
+    await page.close();
+  });
+
+  afterAll(async () => {
+    await server.close();
+    await browser.close();
+  });
+
+  beforeEach(async () => {
+    page = await browser.newPage();
+    page.on('console', (msg) => {
+      console.log(msg.text());
+    });
+
+    await fakeGoto(page, `${serverURL}/html/dialog.html`);
+    await page.evaluate(code);
+    await waitForRAF(page);
+    await hideMouseAnimation(page);
+  });
+
+  [
+    {
+      name: 'show the dialog when open attribute gets added',
+      time: showIncrementalAttributeTime,
+    },
+    {
+      name: 'should close dialog again when open attribute gets removed',
+      time: closeIncrementalAttributeTime,
+    },
+    {
+      name: 'should open dialog with showModal',
+      time: showModalIncrementalAttributeTime,
+    },
+    {
+      name: 'should switch between showModal and show',
+      time: switchBetweenShowModalAndShowIncrementalAttributeTime,
+    },
+    {
+      name: 'should switch between show and showModal',
+      time: switchBetweenShowAndShowModalIncrementalAttributeTime,
+    },
+    {
+      name: 'should open dialog with show in full snapshot',
+      time: showFullSnapshotTime,
+    },
+    {
+      name: 'should open dialog with showModal in full snapshot',
+      time: showModalFullSnapshotTime,
+    },
+    {
+      name: 'should add an opened dialog with showModal in incremental snapshot',
+      time: showModalIncrementalAddTime,
+    },
+    {
+      name: 'should add an opened dialog with showModal in incremental snapshot alternative',
+      time: [showModalFullSnapshotTime, showModalIncrementalAddTime],
+    },
+  ].forEach(({ name, time }) => {
+    [true, false].forEach((useVirtualDom) => {
+      it(`${name} (virtual dom: ${useVirtualDom})`, async () => {
+        await page.evaluate(
+          `let events = ${JSON.stringify(dialogPlaybackEvents)}`,
+        );
+        await page.evaluate(`
+          const { Replayer } = rrweb;
+          window.replayer = new Replayer(events, { useVirtualDom: ${useVirtualDom} });
+        `);
+        const timeArray = Array.isArray(time) ? time : [time];
+        for (let i = 0; i < timeArray.length; i++) {
+          await page.evaluate(`
+            window.replayer.pause(${timeArray[i]});
+          `);
+          await waitForRAF(page);
+        }
+
+        const frameImage = await page!.screenshot({
+          fullPage: false,
+        });
+        const defaultImageFilePrefix =
+          'dialog-test-ts-test-replay-dialog-test-ts-dialog';
+        const kebabCaseName = name
+          .replace(/ /g, '-')
+          .replace(/showModal/g, 'show-modal');
+        const imageFileName = `${defaultImageFilePrefix}-${kebabCaseName}`;
+        expect(frameImage).toMatchImageSnapshot({
+          customSnapshotIdentifier: imageFileName,
+          failureThreshold: 0.05,
+          failureThresholdType: 'percent',
+          dumpDiffToConsole: true,
+          storeReceivedOnFailure: true,
+        });
+      });
+    });
+  });
+
+  it('closed dialogs show nothing', async () => {
+    await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`);
+    await page.evaluate(`
+      const { Replayer } = rrweb;
+      window.replayer = new Replayer(events);
+    `);
+    await waitForRAF(page);
+
+    const frameImage = await page!.screenshot();
+    expect(frameImage).toMatchImageSnapshot({
+      failureThreshold: 0.05,
+      failureThresholdType: 'percent',
+    });
+  });
+
+  // TODO: implement me in the future
+  it.skip('should trigger showModal on multiple dialogs in a specific order');
+});
diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts
index 404357a799..3cdc05abd5 100644
--- a/packages/rrweb/test/replayer.test.ts
+++ b/packages/rrweb/test/replayer.test.ts
@@ -18,7 +18,8 @@ import inputEvents from './events/input';
 import iframeEvents from './events/iframe';
 import selectionEvents from './events/selection';
 import shadowDomEvents from './events/shadow-dom';
-import textareaEvents from './events/bad-textarea';
+import badTextareaEvents from './events/bad-textarea';
+import badStyleEvents from './events/bad-style';
 import StyleSheetTextMutation from './events/style-sheet-text-mutation';
 import canvasInIframe from './events/canvas-in-iframe';
 import adoptedStyleSheet from './events/adopted-style-sheet';
@@ -1142,7 +1143,7 @@ describe('replayer', function () {
   });
 
   it('can deal with legacy duplicate/conflicting values on textareas', async () => {
-    await page.evaluate(`events = ${JSON.stringify(textareaEvents)}`);
+    await page.evaluate(`events = ${JSON.stringify(badTextareaEvents)}`);
 
     const displayValue = await page.evaluate(`
       const { Replayer } = rrweb;
@@ -1155,4 +1156,25 @@ describe('replayer', function () {
     // If the custom element is defined, the display value will be 'block'.
     expect(displayValue).toEqual('this value is used for replay');
   });
+
+  it('can deal with duplicate/conflicting values on style elements', async () => {
+    await page.evaluate(`events = ${JSON.stringify(badStyleEvents)}`);
+
+    const changedColors = await page.evaluate(`
+      const { Replayer } = rrweb;
+      const replayer = new Replayer(events);
+      replayer.pause(1000);
+      // Get the color of the elements after applying the style mutation event
+      [
+        replayer.iframe.contentWindow.getComputedStyle(
+          replayer.iframe.contentDocument.querySelector('#one'),
+        ).color,
+        replayer.iframe.contentWindow.getComputedStyle(
+          replayer.iframe.contentDocument.querySelector('#two'),
+        ).color,
+      ];
+`);
+    const newColor = 'rgb(255, 255, 0)'; // yellow
+    expect(changedColors).toEqual([newColor, newColor]);
+  });
 });
diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts
index c7347d89c3..42c8961304 100644
--- a/packages/rrweb/test/utils.ts
+++ b/packages/rrweb/test/utils.ts
@@ -110,9 +110,17 @@ export function stringifySnapshots(snapshots: eventWithTime[]): string {
     snapshots
       .filter((s) => {
         if (
-          s.type === EventType.IncrementalSnapshot &&
-          (s.data.source === IncrementalSource.MouseMove ||
-            s.data.source === IncrementalSource.ViewportResize)
+          // mouse move or viewport resize can happen on accidental user interference
+          // so we ignore them
+          (s.type === EventType.IncrementalSnapshot &&
+            (s.data.source === IncrementalSource.MouseMove ||
+              s.data.source === IncrementalSource.ViewportResize)) ||
+          // ignore '[vite] connected' messages from vite
+          (s.type === EventType.Plugin &&
+            s.data.plugin === 'rrweb/console@1' &&
+            (s.data.payload as { payload: string[] })?.payload?.find((msg) =>
+              msg.includes('[vite] connected'),
+            ))
         ) {
           return false;
         }
@@ -242,18 +250,18 @@ export function stringifySnapshots(snapshots: eventWithTime[]): string {
 
 function stripBlobURLsFromAttributes(node: {
   attributes: {
-    src?: string;
+    [key: string]: any;
   };
 }) {
-  if (
-    'src' in node.attributes &&
-    node.attributes.src &&
-    typeof node.attributes.src === 'string' &&
-    node.attributes.src.startsWith('blob:')
-  ) {
-    node.attributes.src = node.attributes.src
-      .replace(/[\w-]+$/, '...')
-      .replace(/:[0-9]+\//, ':xxxx/');
+  for (const attr in node.attributes) {
+    if (
+      typeof node.attributes[attr] === 'string' &&
+      node.attributes[attr].startsWith('blob:')
+    ) {
+      node.attributes[attr] = node.attributes[attr]
+        .replace(/[\w-]+$/, '...')
+        .replace(/:[0-9]+\//, ':xxxx/');
+    }
   }
 }
 
diff --git a/packages/rrweb/tsconfig.json b/packages/rrweb/tsconfig.json
index 6a8a5ffb3d..d27cd53179 100644
--- a/packages/rrweb/tsconfig.json
+++ b/packages/rrweb/tsconfig.json
@@ -9,7 +9,6 @@
       "vite/client",
       "@types/dom-mediacapture-transform",
       "@types/offscreencanvas",
-
       // rrweb specific:
       /*
        * @see https://vitest.dev/config/#globals
@@ -18,7 +17,6 @@
        */
       "vitest/globals"
     ],
-
     // TODO: enable me in the future, this is quite a large project
     // at time of writing (April 2024) there are over 100 errors in rrweb
     "strict": false
@@ -27,6 +25,9 @@
     {
       "path": "../types"
     },
+    {
+      "path": "../utils"
+    },
     {
       "path": "../rrdom"
     },
diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md
index 18210906af..b0e111d212 100644
--- a/packages/types/CHANGELOG.md
+++ b/packages/types/CHANGELOG.md
@@ -1,5 +1,32 @@
 # @rrweb/types
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb-snapshot@2.0.13
+
+## 2.0.0-alpha.17
+
+### Minor Changes
+
+- [#1503](https://github.com/rrweb-io/rrweb/pull/1503) [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158) Thanks [@Juice10](https://github.com/Juice10)! - Support top-layer <dialog> components. Fixes #1381.
+
+### Patch Changes
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`d350da8`](https://github.com/rrweb-io/rrweb/commit/d350da8552d8616dd118ee550bdfbce082986562), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb-snapshot@2.0.0-alpha.17
+
+## 2.0.0-alpha.16
+
+### Patch Changes
+
+- Updated dependencies [[`a2c8a1a`](https://github.com/rrweb-io/rrweb/commit/a2c8a1a37bfcf8389b280af792262c8263a979a3), [`d08624c`](https://github.com/rrweb-io/rrweb/commit/d08624cb28add386c3618a0e6607424c3f1884d8)]:
+  - rrweb-snapshot@2.0.0-alpha.16
+
 ## 2.0.12
 
 ### Patch Changes
diff --git a/packages/types/package.json b/packages/types/package.json
index 0019d485e0..f95bff061d 100644
--- a/packages/types/package.json
+++ b/packages/types/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@saola.ai/rrweb-types",
-  "version": "2.0.12",
+  "version": "2.0.13",
   "publishConfig": {
     "access": "public"
   },
@@ -10,9 +10,9 @@
   ],
   "scripts": {
     "dev": "vite build --watch",
-    "build": "tsc -noEmit && vite build",
+    "build": "yarn turbo run prepublish",
     "check-types": "tsc -noEmit",
-    "prepublish": "npm run build",
+    "prepublish": "tsc -noEmit && vite build",
     "lint": "yarn eslint src/**/*.ts"
   },
   "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/types#readme",
@@ -46,19 +46,11 @@
     "package.json"
   ],
   "devDependencies": {
-    "vite": "^5.2.8",
-    "vite-plugin-dts": "^3.8.1"
+    "vite": "^5.3.1",
+    "vite-plugin-dts": "^3.9.1"
   },
   "dependencies": {
-    "@changesets/cli": "^2.27.1",
-    "@monorepo-utils/workspaces-to-typescript-project-references": "^2.8.2",
-    "@saola.ai/rrweb-snapshot": "^2.0.12",
-    "browserslist": "^4.22.1",
-    "concurrently": "^7.1.0",
-    "cssom": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
-    "eslint": "^8.53.0",
-    "eslint-plugin-jest": "^27.6.0",
-    "turbo": "^2.0.3"
+    "@saola.ai/rrweb-snapshot": "^2.0.13"
   },
   "browserslist": [
     "supports es6-class"
diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md
new file mode 100644
index 0000000000..6fd00c059f
--- /dev/null
+++ b/packages/utils/CHANGELOG.md
@@ -0,0 +1,13 @@
+# @rrweb/utils
+
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+## 2.0.0-alpha.17
+
+### Patch Changes
+
+- [#1509](https://github.com/rrweb-io/rrweb/pull/1509) [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414) Thanks [@Juice10](https://github.com/Juice10)! - Reverse monkey patch built in methods to support LWC (and other frameworks like angular which monkey patch built in methods).
diff --git a/packages/utils/Readme.md b/packages/utils/Readme.md
new file mode 100644
index 0000000000..2107a2f228
--- /dev/null
+++ b/packages/utils/Readme.md
@@ -0,0 +1,178 @@
+# @rrweb/utils
+
+This package contains the shared utility functions used across rrweb packages.
+See the [guide](../../guide.md) for more info on rrweb.
+
+## Sponsors
+
+[Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site.
+
+### Gold Sponsors 🥇
+
+<div dir="auto">
+
+<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/0/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/0/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/1/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/1/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/2/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/2/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/3/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/3/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/4/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/4/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/5/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/5/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/6/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/6/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
+
+</div>
+
+### Silver Sponsors 🥈
+
+<div dir="auto">
+
+<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/0/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/0/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/1/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/1/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/2/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/2/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/3/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/3/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/4/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/4/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/5/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/5/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/6/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/6/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
+
+</div>
+
+### Bronze Sponsors 🥉
+
+<div dir="auto">
+
+<a href="https://opencollective.com/rrweb/tiers/sponsors/0/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/0/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/sponsors/1/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/1/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/sponsors/2/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/2/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/sponsors/3/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/3/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/sponsors/4/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/4/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/sponsors/5/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/5/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/sponsors/6/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/6/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/sponsors/7/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/7/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
+<a href="https://opencollective.com/rrweb/tiers/sponsors/8/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/8/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
+
+</div>
+
+### Backers
+
+<a href="https://opencollective.com/rrweb#sponsor" rel="nofollow"><img src="https://opencollective.com/rrweb/tiers/backers.svg?avatarHeight=36"></a>
+
+## Core Team Members
+
+<table>
+  <tr>
+    <td align="center">
+      <a href="https://github.com/Yuyz0112">
+        <img
+          src="https://avatars.githubusercontent.com/u/13651389?s=100"
+          width="100px;"
+          alt=""
+        />
+        <br /><sub><b>Yuyz0112</b></sub>
+        <br /><br />
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://github.com/YunFeng0817">
+        <img
+          src="https://avatars.githubusercontent.com/u/27533910?s=100"
+          width="100px;"
+          alt=""
+        />
+        <br /><sub><b>Yun Feng</b></sub>
+        <br /><br />
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://github.com/eoghanmurray">
+        <img
+          src="https://avatars.githubusercontent.com/u/156780?s=100"
+          width="100px;"
+          alt=""
+        />
+        <br /><sub><b>eoghanmurray</b></sub>
+        <br /><br />
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://github.com/Juice10">
+        <img
+          src="https://avatars.githubusercontent.com/u/4106?s=100"
+          width="100px;"
+          alt=""
+        />
+        <br /><sub><b>Juice10</b></sub>
+        <br /><sub>open for rrweb consulting</sub>
+      </a>
+    </td>
+  </tr>
+</table>
+
+## Who's using rrweb?
+
+<table>
+  <tr>
+    <td align="center">
+      <a href="http://www.smartx.com/" target="_blank">
+        <img width="195px" src="https://www.rrweb.io/logos/smartx.png">
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://posthog.com?utm_source=rrweb&utm_medium=sponsorship&utm_campaign=open-source-sponsorship" target="_blank">
+        <img width="195px" src="https://www.rrweb.io/logos/posthog.png">
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://statcounter.com/session-replay/" target="_blank">
+        <img width="195px" src="https://statcounter.com/images/logo-statcounter-arc-blue.svg">
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://recordonce.com/" target="_blank">
+        <img width="195px" alt="Smart screen recording for SaaS" src="https://uploads-ssl.webflow.com/5f3d133183156245630d4446/5f3d1940abe8db8612c23521_Record-Once-logo-554x80px.svg">
+      </a>
+    </td>
+  </tr>
+    <tr>
+    <td align="center">
+      <a href="https://cux.io" target="_blank">
+        <img style="padding: 8px" alt="The first ever UX automation tool" width="195px" src="https://cux.io/cux-logo.svg">
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://remsupp.com" target="_blank">
+        <img style="padding: 8px" alt="Remote Access & Co-Browsing" width="195px" src="https://remsupp.com/images/logo.png">
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://highlight.io" target="_blank">
+        <img style="padding: 8px" alt="The open source, fullstack Monitoring Platform." width="195px" src="https://github.com/highlight/highlight/raw/main/highlight.io/public/images/logo.png">
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://analyzee.io" target="_blank">
+        <img style="padding: 8px" alt="Comprehensive data analytics platform that empowers businesses to gain valuable insights and make data-driven decisions." width="195px" src="https://cdn.analyzee.io/assets/analyzee-logo.png">
+      </a>
+    </td>
+  </tr>
+  <tr>
+    <td align="center">
+      <a href="https://requestly.io" target="_blank">
+        <img style="padding: 8px" alt="Intercept, Modify, Record & Replay HTTP Requests." width="195px" src="https://github.com/requestly/requestly/assets/16779465/652552db-c867-44cb-9bb5-94a2026e04ca">
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://gleap.io" target="_blank">
+        <img style="padding: 8px" alt="In-app bug reporting & customer feedback platform." width="195px" src="https://assets-global.website-files.com/6506f3f29c68b1724807619d/6506f56010237164c6306591_GleapLogo.svg">
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://uxwizz.com" target="_blank">
+        <img style="padding: 8px" alt="Self-hosted website analytics with heatmaps and session recordings." width="195px" src="https://github.com/UXWizz/public-files/raw/main/assets/logo.png">
+      </a>
+    </td>
+    <td align="center">
+      <a href="https://www.howdygo.com" target="_blank">
+        <img style="padding: 8px" alt="Interactive product demos for small marketing teams" width="195px" src="https://assets-global.website-files.com/650afb446f1dd5bd410f00cc/650b2cec6188ff54dd9b01e1_Logo.svg">
+      </a>
+    </td>
+  </tr>
+</table>
diff --git a/packages/utils/package.json b/packages/utils/package.json
new file mode 100644
index 0000000000..4df05440ad
--- /dev/null
+++ b/packages/utils/package.json
@@ -0,0 +1,53 @@
+{
+  "name": "@saola.ai/rrweb-utils",
+  "version": "2.0.13",
+  "publishConfig": {
+    "access": "public"
+  },
+  "keywords": [
+    "rrweb",
+    "@rrweb/utils"
+  ],
+  "scripts": {
+    "dev": "vite build --watch",
+    "build": "tsc -noEmit && vite build",
+    "check-types": "tsc -noEmit",
+    "prepublish": "npm run build",
+    "lint": "yarn eslint src/**/*.ts"
+  },
+  "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/utils#readme",
+  "bugs": {
+    "url": "https://github.com/rrweb-io/rrweb/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/rrweb-io/rrweb.git"
+  },
+  "license": "MIT",
+  "type": "module",
+  "main": "./dist/rrweb-utils.umd.cjs",
+  "module": "./dist/rrweb-utils.js",
+  "unpkg": "./dist/rrweb-utils.umd.cjs",
+  "typings": "dist/index.d.ts",
+  "exports": {
+    ".": {
+      "import": {
+        "types": "./dist/index.d.ts",
+        "default": "./dist/rrweb-utils.js"
+      },
+      "require": {
+        "types": "./dist/index.d.cts",
+        "default": "./dist/rrweb-utils.umd.cjs"
+      }
+    }
+  },
+  "files": [
+    "dist",
+    "package.json"
+  ],
+  "devDependencies": {
+    "vite": "^5.2.8",
+    "vite-plugin-dts": "^3.8.1"
+  },
+  "dependencies": {}
+}
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
new file mode 100644
index 0000000000..b88d7f452e
--- /dev/null
+++ b/packages/utils/src/index.ts
@@ -0,0 +1,221 @@
+type PrototypeOwner = Node | ShadowRoot | MutationObserver | Element;
+type TypeofPrototypeOwner =
+  | typeof Node
+  | typeof ShadowRoot
+  | typeof MutationObserver
+  | typeof Element;
+
+type BasePrototypeCache = {
+  Node: typeof Node.prototype;
+  ShadowRoot: typeof ShadowRoot.prototype;
+  MutationObserver: typeof MutationObserver.prototype;
+  Element: typeof Element.prototype;
+};
+
+const testableAccessors = {
+  Node: ['childNodes', 'parentNode', 'parentElement', 'textContent'] as const,
+  ShadowRoot: ['host', 'styleSheets'] as const,
+  Element: ['shadowRoot', 'querySelector', 'querySelectorAll'] as const,
+  MutationObserver: [] as const,
+} as const;
+
+const testableMethods = {
+  Node: ['contains', 'getRootNode'] as const,
+  ShadowRoot: ['getSelection'],
+  Element: [],
+  MutationObserver: ['constructor'],
+} as const;
+
+const untaintedBasePrototype: Partial<BasePrototypeCache> = {};
+
+export function getUntaintedPrototype<T extends keyof BasePrototypeCache>(
+  key: T,
+): BasePrototypeCache[T] {
+  if (untaintedBasePrototype[key])
+    return untaintedBasePrototype[key] as BasePrototypeCache[T];
+
+  const defaultObj = globalThis[key] as TypeofPrototypeOwner;
+  const defaultPrototype = defaultObj.prototype as BasePrototypeCache[T];
+
+  // use list of testable accessors to check if the prototype is tainted
+  const accessorNames =
+    key in testableAccessors ? testableAccessors[key] : undefined;
+  const isUntaintedAccessors = Boolean(
+    accessorNames &&
+      // @ts-expect-error 2345
+      accessorNames.every((accessor: keyof typeof defaultPrototype) =>
+        Boolean(
+          Object.getOwnPropertyDescriptor(defaultPrototype, accessor)
+            ?.get?.toString()
+            .includes('[native code]'),
+        ),
+      ),
+  );
+
+  const methodNames = key in testableMethods ? testableMethods[key] : undefined;
+  const isUntaintedMethods = Boolean(
+    methodNames &&
+      methodNames.every(
+        // @ts-expect-error 2345
+        (method: keyof typeof defaultPrototype) =>
+          typeof defaultPrototype[method] === 'function' &&
+          defaultPrototype[method]?.toString().includes('[native code]'),
+      ),
+  );
+
+  if (isUntaintedAccessors && isUntaintedMethods) {
+    untaintedBasePrototype[key] = defaultObj.prototype as BasePrototypeCache[T];
+    return defaultObj.prototype as BasePrototypeCache[T];
+  }
+
+  try {
+    const iframeEl = document.createElement('iframe');
+    document.body.appendChild(iframeEl);
+    const win = iframeEl.contentWindow;
+    if (!win) return defaultObj.prototype as BasePrototypeCache[T];
+
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+    const untaintedObject = (win as any)[key]
+      .prototype as BasePrototypeCache[T];
+    // cleanup
+    document.body.removeChild(iframeEl);
+
+    if (!untaintedObject) return defaultPrototype;
+
+    return (untaintedBasePrototype[key] = untaintedObject);
+  } catch {
+    return defaultPrototype;
+  }
+}
+
+const untaintedAccessorCache: Record<
+  string,
+  (this: PrototypeOwner, ...args: unknown[]) => unknown
+> = {};
+
+export function getUntaintedAccessor<
+  K extends keyof BasePrototypeCache,
+  T extends keyof BasePrototypeCache[K],
+>(
+  key: K,
+  instance: BasePrototypeCache[K],
+  accessor: T,
+): BasePrototypeCache[K][T] {
+  const cacheKey = `${key}.${String(accessor)}`;
+  if (untaintedAccessorCache[cacheKey])
+    return untaintedAccessorCache[cacheKey].call(
+      instance,
+    ) as BasePrototypeCache[K][T];
+
+  const untaintedPrototype = getUntaintedPrototype(key);
+  // eslint-disable-next-line @typescript-eslint/unbound-method
+  const untaintedAccessor = Object.getOwnPropertyDescriptor(
+    untaintedPrototype,
+    accessor,
+  )?.get;
+
+  if (!untaintedAccessor) return instance[accessor];
+
+  untaintedAccessorCache[cacheKey] = untaintedAccessor;
+
+  return untaintedAccessor.call(instance) as BasePrototypeCache[K][T];
+}
+
+type BaseMethod<K extends keyof BasePrototypeCache> = (
+  this: BasePrototypeCache[K],
+  ...args: unknown[]
+) => unknown;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const untaintedMethodCache: Record<string, BaseMethod<any>> = {};
+export function getUntaintedMethod<
+  K extends keyof BasePrototypeCache,
+  T extends keyof BasePrototypeCache[K],
+>(
+  key: K,
+  instance: BasePrototypeCache[K],
+  method: T,
+): BasePrototypeCache[K][T] {
+  const cacheKey = `${key}.${String(method)}`;
+  if (untaintedMethodCache[cacheKey])
+    return untaintedMethodCache[cacheKey].bind(
+      instance,
+    ) as BasePrototypeCache[K][T];
+
+  const untaintedPrototype = getUntaintedPrototype(key);
+  const untaintedMethod = untaintedPrototype[method];
+
+  if (typeof untaintedMethod !== 'function') return instance[method];
+
+  untaintedMethodCache[cacheKey] = untaintedMethod as BaseMethod<K>;
+
+  return untaintedMethod.bind(instance) as BasePrototypeCache[K][T];
+}
+
+export function childNodes(n: Node): NodeListOf<Node> {
+  return getUntaintedAccessor('Node', n, 'childNodes');
+}
+
+export function parentNode(n: Node): ParentNode | null {
+  return getUntaintedAccessor('Node', n, 'parentNode');
+}
+
+export function parentElement(n: Node): HTMLElement | null {
+  return getUntaintedAccessor('Node', n, 'parentElement');
+}
+
+export function textContent(n: Node): string | null {
+  return getUntaintedAccessor('Node', n, 'textContent');
+}
+
+export function contains(n: Node, other: Node): boolean {
+  return getUntaintedMethod('Node', n, 'contains')(other);
+}
+
+export function getRootNode(n: Node): Node {
+  return getUntaintedMethod('Node', n, 'getRootNode')();
+}
+
+export function host(n: ShadowRoot): Element | null {
+  if (!n || !('host' in n)) return null;
+  return getUntaintedAccessor('ShadowRoot', n, 'host');
+}
+
+export function styleSheets(n: ShadowRoot): StyleSheetList {
+  return n.styleSheets;
+}
+
+export function shadowRoot(n: Node): ShadowRoot | null {
+  if (!n || !('shadowRoot' in n)) return null;
+  return getUntaintedAccessor('Element', n as Element, 'shadowRoot');
+}
+
+export function querySelector(n: Element, selectors: string): Element | null {
+  return getUntaintedAccessor('Element', n, 'querySelector')(selectors);
+}
+
+export function querySelectorAll(
+  n: Element,
+  selectors: string,
+): NodeListOf<Element> {
+  return getUntaintedAccessor('Element', n, 'querySelectorAll')(selectors);
+}
+
+export function mutationObserverCtor(): (typeof MutationObserver)['prototype']['constructor'] {
+  return getUntaintedPrototype('MutationObserver').constructor;
+}
+
+export default {
+  childNodes,
+  parentNode,
+  parentElement,
+  textContent,
+  contains,
+  getRootNode,
+  host,
+  styleSheets,
+  shadowRoot,
+  querySelector,
+  querySelectorAll,
+  mutationObserver: mutationObserverCtor,
+};
diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json
new file mode 100644
index 0000000000..1902007d56
--- /dev/null
+++ b/packages/utils/tsconfig.json
@@ -0,0 +1,10 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "include": ["src"],
+  "exclude": ["vite.config.ts"],
+  "compilerOptions": {
+    "rootDir": "src",
+    "tsBuildInfoFile": "./tsconfig.tsbuildinfo"
+  },
+  "references": []
+}
diff --git a/packages/utils/vite.config.js b/packages/utils/vite.config.js
new file mode 100644
index 0000000000..854f2b9ef0
--- /dev/null
+++ b/packages/utils/vite.config.js
@@ -0,0 +1,4 @@
+import path from 'path';
+import config from '../../vite.config.default';
+
+export default config(path.resolve(__dirname, 'src/index.ts'), 'rrwebUtils');
diff --git a/packages/web-extension/CHANGELOG.md b/packages/web-extension/CHANGELOG.md
index 65ef125f1a..d35d11aedb 100644
--- a/packages/web-extension/CHANGELOG.md
+++ b/packages/web-extension/CHANGELOG.md
@@ -1,5 +1,35 @@
 # @rrweb/web-extension
 
+## 2.0.13
+
+### Patch Changes
+
+- Merge from rrweb remote upstream
+
+- Updated dependencies []:
+  - @saola.ai/rrweb-player@2.0.13
+  - @saola.ai/rrweb@2.0.13
+
+## 2.0.0-alpha.17
+
+### Minor Changes
+
+- [#1522](https://github.com/rrweb-io/rrweb/pull/1522) [`b1f9daa`](https://github.com/rrweb-io/rrweb/commit/b1f9daaa42f007a641104613bb07f141585f9e77) Thanks [@kirankunigiri](https://github.com/kirankunigiri)! - Added session downloader for chrome extension
+
+### Patch Changes
+
+- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
+  - rrweb@2.0.0-alpha.17
+  - rrweb-player@2.0.0-alpha.17
+
+## 2.0.0-alpha.16
+
+### Patch Changes
+
+- Updated dependencies [[`a2c8a1a`](https://github.com/rrweb-io/rrweb/commit/a2c8a1a37bfcf8389b280af792262c8263a979a3), [`d08624c`](https://github.com/rrweb-io/rrweb/commit/d08624cb28add386c3618a0e6607424c3f1884d8)]:
+  - rrweb@2.0.0-alpha.16
+  - rrweb-player@2.0.0-alpha.16
+
 ## 2.0.12
 
 ### Patch Changes
diff --git a/packages/web-extension/package.json b/packages/web-extension/package.json
index 251ff17dec..9179a294be 100644
--- a/packages/web-extension/package.json
+++ b/packages/web-extension/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@saola.ai/rrweb-web-extension",
   "private": true,
-  "version": "2.0.12",
+  "version": "2.0.13",
   "description": "The web extension of rrweb which helps to run rrweb on any website out of box",
   "author": "rrweb-io",
   "license": "MIT",
@@ -18,13 +18,12 @@
     "prepublish": "yarn build"
   },
   "devDependencies": {
-    "@saola.ai/rrweb-types": "^2.0.12",
+    "@saola.ai/rrweb-types": "^2.0.13",
     "@types/react-dom": "^18.0.6",
     "@types/webextension-polyfill": "^0.9.1",
     "@vitejs/plugin-react": "^4.2.1",
-    "cross-env": "^7.0.3",
     "type-fest": "^2.19.0",
-    "vite": "^5.2.8",
+    "vite": "^5.3.1",
     "vite-plugin-web-extension": "^4.1.3",
     "vite-plugin-zip-pack": "^1.2.2",
     "webextension-polyfill": "^0.10.0"
@@ -42,7 +41,7 @@
     "react-dom": "^18.2.0",
     "react-icons": "^4.4.0",
     "react-router-dom": "^6.4.1",
-    "@saola.ai/rrweb": "^2.0.12",
-    "@saola.ai/rrweb-player": "^2.0.12"
+    "@saola.ai/rrweb": "^2.0.13",
+    "@saola.ai/rrweb-player": "^2.0.13"
   }
 }
diff --git a/packages/web-extension/src/background/index.ts b/packages/web-extension/src/background/index.ts
index 9a0a7c58b3..4ffda7827c 100644
--- a/packages/web-extension/src/background/index.ts
+++ b/packages/web-extension/src/background/index.ts
@@ -2,11 +2,11 @@ import Browser from 'webextension-polyfill';
 import type { eventWithTime } from '@saola.ai/rrweb-types';
 import Channel from '~/utils/channel';
 import {
-  LocalData,
+  type LocalData,
   LocalDataKey,
   RecorderStatus,
-  Settings,
-  SyncData,
+  type Settings,
+  type SyncData,
   SyncDataKey,
 } from '~/types';
 import { pauseRecording, resumeRecording } from '~/utils/recording';
diff --git a/packages/web-extension/src/components/CircleButton.tsx b/packages/web-extension/src/components/CircleButton.tsx
index abbb68e576..f114a1cb66 100644
--- a/packages/web-extension/src/components/CircleButton.tsx
+++ b/packages/web-extension/src/components/CircleButton.tsx
@@ -1,4 +1,4 @@
-import { Button, ButtonProps } from '@chakra-ui/react';
+import { Button, type ButtonProps } from '@chakra-ui/react';
 
 interface CircleButtonProps extends ButtonProps {
   diameter: number;
diff --git a/packages/web-extension/src/components/SidebarWithHeader.tsx b/packages/web-extension/src/components/SidebarWithHeader.tsx
index bcc07147ff..7895efe613 100644
--- a/packages/web-extension/src/components/SidebarWithHeader.tsx
+++ b/packages/web-extension/src/components/SidebarWithHeader.tsx
@@ -12,8 +12,8 @@ import {
   Drawer,
   DrawerContent,
   useDisclosure,
-  BoxProps,
-  FlexProps,
+  type BoxProps,
+  type FlexProps,
   Heading,
   Stack,
   Text,
diff --git a/packages/web-extension/src/content/index.ts b/packages/web-extension/src/content/index.ts
index 18b35b2a31..673f8ad577 100644
--- a/packages/web-extension/src/content/index.ts
+++ b/packages/web-extension/src/content/index.ts
@@ -1,16 +1,16 @@
-import Browser, { Storage } from 'webextension-polyfill';
+import Browser, { type Storage } from 'webextension-polyfill';
 import { nanoid } from 'nanoid';
 import type { eventWithTime } from '@saola.ai/rrweb-types';
 import {
-  LocalData,
+  type LocalData,
   LocalDataKey,
   RecorderStatus,
   ServiceName,
-  Session,
-  RecordStartedMessage,
-  RecordStoppedMessage,
+  type Session,
+  type RecordStartedMessage,
+  type RecordStoppedMessage,
   MessageName,
-  EmitEventMessage,
+  type EmitEventMessage,
 } from '~/types';
 import Channel from '~/utils/channel';
 import { isInCrossOriginIFrame } from '~/utils';
diff --git a/packages/web-extension/src/content/inject.ts b/packages/web-extension/src/content/inject.ts
index 3ac619e642..ded4a76422 100644
--- a/packages/web-extension/src/content/inject.ts
+++ b/packages/web-extension/src/content/inject.ts
@@ -1,7 +1,7 @@
 import { record } from '@saola.ai/rrweb';
 import type { recordOptions } from '@saola.ai/rrweb';
 import type { eventWithTime } from '@saola.ai/rrweb-types';
-import { MessageName, RecordStartedMessage } from '~/types';
+import { MessageName, type RecordStartedMessage } from '~/types';
 import { isInCrossOriginIFrame } from '~/utils';
 
 /**
diff --git a/packages/web-extension/src/pages/SessionList.tsx b/packages/web-extension/src/pages/SessionList.tsx
index 8aead1834f..d285932029 100644
--- a/packages/web-extension/src/pages/SessionList.tsx
+++ b/packages/web-extension/src/pages/SessionList.tsx
@@ -23,15 +23,15 @@ import {
   useReactTable,
   flexRender,
   getCoreRowModel,
-  SortingState,
+  type SortingState,
   getSortedRowModel,
-  PaginationState,
+  type PaginationState,
 } from '@tanstack/react-table';
 import { VscTriangleDown, VscTriangleUp } from 'react-icons/vsc';
 import { useNavigate } from 'react-router-dom';
-import { Session, EventName } from '~/types';
+import { type Session, EventName } from '~/types';
 import Channel from '~/utils/channel';
-import { deleteSessions, getAllSessions } from '~/utils/storage';
+import { deleteSessions, getAllSessions, downloadSessions } from '~/utils/storage';
 import {
   FiChevronLeft,
   FiChevronRight,
@@ -292,24 +292,38 @@ export function SessionList() {
             ))}
           </Select>
           {Object.keys(rowSelection).length > 0 && (
-            <Button
-              mr={4}
-              size="md"
-              colorScheme="red"
-              onClick={() => {
-                if (table.getSelectedRowModel().flatRows.length === 0) return;
-                const ids = table
-                  .getSelectedRowModel()
-                  .flatRows.map((row) => row.original.id);
-                void deleteSessions(ids).then(() => {
-                  setRowSelection({});
-                  void updateSessions();
-                  channel.emit(EventName.SessionUpdated, {});
-                });
-              }}
-            >
-              Delete
-            </Button>
+            <Flex gap={1}>
+              <Button
+                mr={4}
+                size="md"
+                colorScheme="red"
+                onClick={() => {
+                  if (table.getSelectedRowModel().flatRows.length === 0) return;
+                  const ids = table
+                    .getSelectedRowModel()
+                    .flatRows.map((row) => row.original.id);
+                  void deleteSessions(ids).then(() => {
+                    setRowSelection({});
+                    void updateSessions();
+                    channel.emit(EventName.SessionUpdated, {});
+                  });
+                }}
+              >
+                Delete
+              </Button>
+              <Button
+                mr={4}
+                size="md"
+                colorScheme="green"
+                onClick={() => {
+                  const selectedRows = table.getSelectedRowModel().flatRows;
+                  if (selectedRows.length === 0) return;
+                  void downloadSessions(selectedRows.map((row) => row.original.id));
+                }}
+              >
+                Download
+              </Button>
+            </Flex>
           )}
         </Flex>
       </Flex>
diff --git a/packages/web-extension/src/popup/App.tsx b/packages/web-extension/src/popup/App.tsx
index a6bed7b3f2..c83a03ced4 100644
--- a/packages/web-extension/src/popup/App.tsx
+++ b/packages/web-extension/src/popup/App.tsx
@@ -10,16 +10,13 @@ import {
 } from '@chakra-ui/react';
 import { FiSettings, FiList, FiPause, FiPlay } from 'react-icons/fi';
 import Channel from '~/utils/channel';
-import {
+import type {
   LocalData,
-  LocalDataKey,
-  RecorderStatus,
-  ServiceName,
   RecordStartedMessage,
   RecordStoppedMessage,
   Session,
-  EventName,
 } from '~/types';
+import { LocalDataKey, RecorderStatus, ServiceName, EventName } from '~/types';
 import Browser from 'webextension-polyfill';
 import { CircleButton } from '~/components/CircleButton';
 import { Timer } from './Timer';
@@ -39,9 +36,8 @@ export function App() {
     void Browser.storage.local.get(LocalDataKey.recorderStatus).then((data) => {
       const localData = data as LocalData;
       if (!localData || !localData[LocalDataKey.recorderStatus]) return;
-      const { status, startTimestamp, pausedTimestamp } = localData[
-        LocalDataKey.recorderStatus
-      ];
+      const { status, startTimestamp, pausedTimestamp } =
+        localData[LocalDataKey.recorderStatus];
       setStatus(status);
       if (startTimestamp && pausedTimestamp)
         setStartTime(Date.now() - pausedTimestamp + startTimestamp || 0);
diff --git a/packages/web-extension/src/utils/channel.ts b/packages/web-extension/src/utils/channel.ts
index 1a8e9b2a82..fc090bcc7f 100644
--- a/packages/web-extension/src/utils/channel.ts
+++ b/packages/web-extension/src/utils/channel.ts
@@ -1,5 +1,5 @@
 import mitt from 'mitt';
-import Browser, { Runtime } from 'webextension-polyfill';
+import Browser, { type Runtime } from 'webextension-polyfill';
 
 export type Message = EventType | ServiceType;
 export type EventType = {
diff --git a/packages/web-extension/src/utils/recording.ts b/packages/web-extension/src/utils/recording.ts
index 96f77c5da7..832c5a58fd 100644
--- a/packages/web-extension/src/utils/recording.ts
+++ b/packages/web-extension/src/utils/recording.ts
@@ -2,11 +2,11 @@ import Browser from 'webextension-polyfill';
 import type { eventWithTime } from '@saola.ai/rrweb-types';
 
 import {
-  LocalData,
+  type LocalData,
   LocalDataKey,
   RecorderStatus,
-  RecordStartedMessage,
-  RecordStoppedMessage,
+  type RecordStartedMessage,
+  type RecordStoppedMessage,
   ServiceName,
 } from '~/types';
 import type Channel from './channel';
diff --git a/packages/web-extension/src/utils/storage.ts b/packages/web-extension/src/utils/storage.ts
index 6c4c460ac8..e5fc05ab26 100644
--- a/packages/web-extension/src/utils/storage.ts
+++ b/packages/web-extension/src/utils/storage.ts
@@ -88,3 +88,22 @@ export async function deleteSessions(ids: string[]) {
     return Promise.all([eventTransition.done, sessionTransition.done]);
   });
 }
+
+export async function downloadSessions(ids: string[]) {
+  for (const sessionId of ids) {
+    const events = await getEvents(sessionId);
+    const session = await getSession(sessionId);
+    const blob = new Blob([JSON.stringify({ session, events }, null, 2)], {
+      type: 'application/json',
+    });
+
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = `${session.name}.json`;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+    URL.revokeObjectURL(url);
+  }
+}
diff --git a/packages/web-extension/vite.config.ts b/packages/web-extension/vite.config.ts
index 0fd0602706..2ca58feab9 100644
--- a/packages/web-extension/vite.config.ts
+++ b/packages/web-extension/vite.config.ts
@@ -5,6 +5,8 @@ import * as path from 'path';
 import type { PackageJson } from 'type-fest';
 import react from '@vitejs/plugin-react';
 
+const emptyOutDir = !process.argv.includes('--watch');
+
 function useSpecialFormat(
   entriesToUse: string[],
   format: LibraryFormats,
@@ -46,7 +48,7 @@ export default defineConfig({
       'dist',
       process.env.TARGET_BROWSER as string,
     ),
-    emptyOutDir: true,
+    emptyOutDir,
   },
   // Add the webExtension plugin
   plugins: [
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 6ca3030761..d01ad45552 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -14,7 +14,7 @@
     "sourceMap": true,
     "skipLibCheck": true,
     "declaration": true,
-    "importsNotUsedAsValues": "error",
+    "verbatimModuleSyntax": true,
     "strict": true,
     "removeComments": true,
     "noImplicitAny": true,
diff --git a/turbo.json b/turbo.json
index a48aa0dfc4..54bd1b278f 100644
--- a/turbo.json
+++ b/turbo.json
@@ -7,9 +7,10 @@
     "vite.config.defaults.ts",
     "tsconfig.json"
   ],
+  "globalPassThroughEnv": ["PUPPETEER_HEADLESS"],
   "tasks": {
     "prepublish": {
-      "dependsOn": ["^prepublish"],
+      "dependsOn": ["^prepublish", "//#references:update"],
       "outputs": [
         "lib/**",
         "es/**",
@@ -20,24 +21,30 @@
       ]
     },
     "test": {
-      "dependsOn": ["^prepublish"],
-      "passThroughEnv": ["PUPPETEER_HEADLESS"]
+      "dependsOn": ["^prepublish"]
     },
     "test:watch": {
       "persistent": true,
-      "passThroughEnv": ["PUPPETEER_HEADLESS"]
+      "cache": false
     },
     "test:update": {
-      "dependsOn": ["^prepublish"],
-      "passThroughEnv": ["PUPPETEER_HEADLESS"]
+      "dependsOn": ["^prepublish"]
     },
     "dev": {
-      // "dependsOn": ["^prepublish"],
+      "dependsOn": ["prepublish", "^prepublish"],
       "persistent": true,
-      "cache": false,
-      "passThroughEnv": ["CLEAR_DIST_DIR"]
+      "cache": false
     },
     "lint": {},
-    "check-types": {}
+    "check-types": {
+      "dependsOn": ["^prepublish"]
+    },
+    "//#references:update": {
+      "inputs": ["packages/*/package.json", "packages/plugins/*/package.json"],
+      "outputs": [
+        "packages/*/tsconfig.json",
+        "packages/plugins/*/tsconfig.json"
+      ]
+    }
   }
 }
diff --git a/vite.config.default.ts b/vite.config.default.ts
index 9bfb2fd017..4dd3cc496e 100644
--- a/vite.config.default.ts
+++ b/vite.config.default.ts
@@ -7,7 +7,8 @@ import { build, Format } from 'esbuild';
 import { resolve } from 'path';
 import { umdWrapper } from 'esbuild-plugin-umd-wrapper';
 
-const emptyOutDir = process.env.CLEAR_DIST_DIR !== 'false';
+// don't empty out dir if --watch flag is passed
+const emptyOutDir = !process.argv.includes('--watch');
 
 function minifyAndUMDPlugin({
   name,
diff --git a/yarn.lock b/yarn.lock
index 56d58b1c1a..f68b2f59ab 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1956,11 +1956,16 @@
     globby "^11.0.0"
     read-yaml-file "^1.1.0"
 
-"@mdn/browser-compat-data@^5.2.34", "@mdn/browser-compat-data@^5.3.13":
+"@mdn/browser-compat-data@^5.2.34":
   version "5.5.33"
   resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.5.33.tgz#c1177469bc4d39fa24c2cd3df317039e2b465b4c"
   integrity sha512-uO4uIBFn9D4UNyUmaueIWnE/IJhBlSJ7W1rANvDdaawhTX8CSgqUX8tj9/6a+1WjpL9Bgirf67d//S2VwDsfig==
 
+"@mdn/browser-compat-data@^5.5.19":
+  version "5.6.12"
+  resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.6.12.tgz#ac3e1855c2387334bbfdb2b6249dd95c9d9c2b70"
+  integrity sha512-W/Km+GFczwpoimaXbtHYdjK26VHGszOEZ9EnIyLS2E65x6LEZs7r0FovR/XSkzgNau95sTxI3JfFKQFLIJE7EQ==
+
 "@microsoft/api-extractor-model@7.28.13":
   version "7.28.13"
   resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.28.13.tgz#96fbc52155e0d07e0eabbd9699065b77702fe33a"
@@ -2255,6 +2260,11 @@
   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4"
   integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==
 
+"@rrweb/utils@^2.0.0-alpha.17":
+  version "2.0.0-alpha.17"
+  resolved "https://registry.yarnpkg.com/@rrweb/utils/-/utils-2.0.0-alpha.17.tgz#d13a7326af0311e0f54551e223ace987608eaed5"
+  integrity sha512-HCsasPERBwOS9/LQeOytO2ETKTCqRj1wORBuxiy3t41hKhmi225DdrUPiWnyDdTQm1GdVbOymMRknJVPnZaSXw==
+
 "@rushstack/node-core-library@4.0.2":
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz#e26854a3314b279d57e8abdb4acce7797d02f554"
@@ -3552,7 +3562,7 @@ browser-process-hrtime@^1.0.0:
   resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
   integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
 
-browserslist@^4.21.10, browserslist@^4.22.1, browserslist@^4.22.2:
+browserslist@^4.22.1, browserslist@^4.22.2:
   version "4.23.1"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96"
   integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==
@@ -3562,6 +3572,16 @@ browserslist@^4.21.10, browserslist@^4.22.1, browserslist@^4.22.2:
     node-releases "^2.0.14"
     update-browserslist-db "^1.0.16"
 
+browserslist@^4.23.0:
+  version "4.24.2"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580"
+  integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==
+  dependencies:
+    caniuse-lite "^1.0.30001669"
+    electron-to-chromium "^1.5.41"
+    node-releases "^2.0.18"
+    update-browserslist-db "^1.1.1"
+
 bs-logger@0.x:
   version "0.2.6"
   resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
@@ -3677,7 +3697,12 @@ camelcase@^7.0.1:
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048"
   integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==
 
-caniuse-lite@^1.0.30001524, caniuse-lite@^1.0.30001629:
+caniuse-lite@^1.0.30001605, caniuse-lite@^1.0.30001669:
+  version "1.0.30001677"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz#27c2e2c637e007cfa864a16f7dfe7cde66b38b5f"
+  integrity sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==
+
+caniuse-lite@^1.0.30001629:
   version "1.0.30001636"
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78"
   integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==
@@ -4098,13 +4123,6 @@ create-require@^1.1.0:
   resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
   integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
 
-cross-env@^5.2.0:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.1.tgz#b2c76c1ca7add66dc874d11798466094f551b34d"
-  integrity sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ==
-  dependencies:
-    cross-spawn "^6.0.5"
-
 cross-env@^7.0.3:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
@@ -4135,17 +4153,6 @@ cross-spawn@^5.1.0:
     shebang-command "^1.2.0"
     which "^1.2.9"
 
-cross-spawn@^6.0.5:
-  version "6.0.5"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
-  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
-  dependencies:
-    nice-try "^1.0.4"
-    path-key "^2.0.1"
-    semver "^5.5.0"
-    shebang-command "^1.2.0"
-    which "^1.2.9"
-
 cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -4592,6 +4599,11 @@ electron-to-chromium@^1.4.796:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz#cf55808a5ee12e2a2778bbe8cdc941ef87c2093b"
   integrity sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g==
 
+electron-to-chromium@^1.5.41:
+  version "1.5.50"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.50.tgz#d9ba818da7b2b5ef1f3dd32bce7046feb7e93234"
+  integrity sha512-eMVObiUQ2LdgeO1F/ySTXsvqvxb6ZH2zPGaMYsWzRDdOddUa77tdmI0ltg+L16UpbWdhPmuF3wIQYyQq65WfZw==
+
 emittery@^0.8.1:
   version "0.8.1"
   resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
@@ -4789,6 +4801,11 @@ escalade@^3.1.1, escalade@^3.1.2:
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
   integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
 
+escalade@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
+  integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
+
 escape-goat@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-4.0.0.tgz#9424820331b510b0666b98f7873fe11ac4aa8081"
@@ -4832,18 +4849,19 @@ eslint-compat-utils@^0.5.1:
   dependencies:
     semver "^7.5.4"
 
-eslint-plugin-compat@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-compat/-/eslint-plugin-compat-4.2.0.tgz#eeaf80daa1afe495c88a47e9281295acae45c0aa"
-  integrity sha512-RDKSYD0maWy5r7zb5cWQS+uSPc26mgOzdORJ8hxILmWM7S/Ncwky7BcAtXVY5iRbKjBdHsWU8Yg7hfoZjtkv7w==
+eslint-plugin-compat@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-compat/-/eslint-plugin-compat-5.0.0.tgz#d09dff02397d81c9f5b1ac740ef45b39538aa21d"
+  integrity sha512-29KNWyFkUbNVf6TIKVe9SVCGCtHjML3HnUg9C8LG2GsXf7miAeBOgdMc1n2B5n0sHUzg1/A4IFly7Jyf1gSbgQ==
   dependencies:
-    "@mdn/browser-compat-data" "^5.3.13"
+    "@mdn/browser-compat-data" "^5.5.19"
     ast-metadata-inferer "^0.8.0"
-    browserslist "^4.21.10"
-    caniuse-lite "^1.0.30001524"
+    browserslist "^4.23.0"
+    caniuse-lite "^1.0.30001605"
     find-up "^5.0.0"
+    globals "^13.24.0"
     lodash.memoize "^4.1.2"
-    semver "^7.5.4"
+    semver "^7.6.0"
 
 eslint-plugin-jest@^27.6.0:
   version "27.9.0"
@@ -5578,7 +5596,7 @@ globals@^11.1.0:
   resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
   integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
 
-globals@^13.19.0:
+globals@^13.19.0, globals@^13.24.0:
   version "13.24.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
   integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
@@ -5674,6 +5692,15 @@ growly@^1.3.0:
   resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
   integrity sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==
 
+happy-dom@^14.12.0:
+  version "14.12.0"
+  resolved "https://registry.yarnpkg.com/happy-dom/-/happy-dom-14.12.0.tgz#40c748578c6ebfb707e6ae69179d6c541d8f63b3"
+  integrity sha512-dHcnlGFY2o2CdxfuYpqwSrBrpj/Kuzv4u4f3TU5yHW1GL24dKij4pv1BRjXnXc3uWo8qsCbToF9weaDsm/He8A==
+  dependencies:
+    entities "^4.5.0"
+    webidl-conversions "^7.0.0"
+    whatwg-mimetype "^3.0.0"
+
 hard-rejection@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
@@ -7603,11 +7630,6 @@ netmask@^2.0.2:
   resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
   integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
 
-nice-try@^1.0.4:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
-  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
-
 no-case@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
@@ -7657,6 +7679,11 @@ node-releases@^2.0.14:
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
   integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
 
+node-releases@^2.0.18:
+  version "2.0.18"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f"
+  integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==
+
 normalize-package-data@^2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -7968,11 +7995,6 @@ path-is-absolute@^1.0.0:
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
 
-path-key@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
-  integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
-
 path-key@^3.0.0, path-key@^3.1.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@@ -8027,6 +8049,11 @@ picocolors@^1.0.0, picocolors@^1.0.1:
   resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
   integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
 
+picocolors@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+  integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
 picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
@@ -8841,7 +8868,7 @@ semver-match@0.1.1:
   dependencies:
     semver "^5.1.0"
 
-"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.5.0:
+"semver@2 || 3 || 4 || 5", semver@^5.1.0:
   version "5.7.2"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
   integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
@@ -9761,47 +9788,47 @@ tty-table@^4.1.5:
     wcwidth "^1.0.1"
     yargs "^17.7.1"
 
-turbo-darwin-64@2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.0.4.tgz#83c7835f8ba1f7a5473487ce73cfc8d5ad523614"
-  integrity sha512-x9mvmh4wudBstML8Z8IOmokLWglIhSfhQwnh2gBCSqabgVBKYvzl8Y+i+UCNPxheCGTgtsPepTcIaKBIyFIcvw==
+turbo-darwin-64@2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.2.3.tgz#f0ced75ed031091e52851cbe8bb05d21a161a22b"
+  integrity sha512-Rcm10CuMKQGcdIBS3R/9PMeuYnv6beYIHqfZFeKWVYEWH69sauj4INs83zKMTUiZJ3/hWGZ4jet9AOwhsssLyg==
 
-turbo-darwin-arm64@2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.0.4.tgz#046e5768e9d6b490b7108d5bef3f4a1594aca0ba"
-  integrity sha512-/B1Ih8zPRGVw5vw4SlclOf3C/woJ/2T6ieH6u54KT4wypoaVyaiyMqBcziIXycdObIYr7jQ+raHO7q3mhay9/A==
+turbo-darwin-arm64@2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.2.3.tgz#0b4741383ab5070d8383891a65861a8869cc7202"
+  integrity sha512-+EIMHkuLFqUdJYsA3roj66t9+9IciCajgj+DVek+QezEdOJKcRxlvDOS2BUaeN8kEzVSsNiAGnoysFWYw4K0HA==
 
-turbo-linux-64@2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.0.4.tgz#eab8c183a11b26ddec251d62778313a495971e4f"
-  integrity sha512-6aG670e5zOWu6RczEYcB81nEl8EhiGJEvWhUrnAfNEUIMBEH1pR5SsMmG2ol5/m3PgiRM12r13dSqTxCLcHrVg==
+turbo-linux-64@2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.2.3.tgz#2b339db50c12bc52ce99139c156d5555717a209d"
+  integrity sha512-UBhJCYnqtaeOBQLmLo8BAisWbc9v9daL9G8upLR+XGj6vuN/Nz6qUAhverN4Pyej1g4Nt1BhROnj6GLOPYyqxQ==
 
-turbo-linux-arm64@2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.0.4.tgz#2dcc3f1d3e56209736b2ce3d849b80e0d7116e42"
-  integrity sha512-AXfVOjst+mCtPDFT4tCu08Qrfv12Nj7NDd33AjGwV79NYN1Y1rcFY59UQ4nO3ij3rbcvV71Xc+TZJ4csEvRCSg==
+turbo-linux-arm64@2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.2.3.tgz#a4daf6e0872a4e2652e2d05d68ad18cee5b10e94"
+  integrity sha512-hJYT9dN06XCQ3jBka/EWvvAETnHRs3xuO/rb5bESmDfG+d9yQjeTMlhRXKrr4eyIMt6cLDt1LBfyi+6CQ+VAwQ==
 
-turbo-windows-64@2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.0.4.tgz#b2440d82892c983088ed386f9126d365594fc1a5"
-  integrity sha512-QOnUR9hKl0T5gq5h1fAhVEqBSjpcBi/BbaO71YGQNgsr6pAnCQdbG8/r3MYXet53efM0KTdOhieWeO3KLNKybA==
+turbo-windows-64@2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.2.3.tgz#d44b3385948bd0f2ef5c2d53391f142bdd467b18"
+  integrity sha512-NPrjacrZypMBF31b4HE4ROg4P3nhMBPHKS5WTpMwf7wydZ8uvdEHpESVNMOtqhlp857zbnKYgP+yJF30H3N2dQ==
 
-turbo-windows-arm64@2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.0.4.tgz#e943709535baf233f5b85ed35cd95dcf86815283"
-  integrity sha512-3v8WpdZy1AxZw0gha0q3caZmm+0gveBQ40OspD6mxDBIS+oBtO5CkxhIXkFJJW+jDKmDlM7wXDIGfMEq+QyNCQ==
+turbo-windows-arm64@2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.2.3.tgz#d0625ec53f467013a6f259f87f7fc4ae8670aaa4"
+  integrity sha512-fnNrYBCqn6zgKPKLHu4sOkihBI/+0oYFr075duRxqUZ+1aLWTAGfHZLgjVeLh3zR37CVzuerGIPWAEkNhkWEIw==
 
-turbo@^2.0.3:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.0.4.tgz#4fb6f0bf3be905953825de0368203e849c91e412"
-  integrity sha512-Ilme/2Q5kYw0AeRr+aw3s02+WrEYaY7U8vPnqSZU/jaDG/qd6jHVN6nRWyd/9KXvJGYM69vE6JImoGoyNjLwaw==
+turbo@^2.0.4:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.2.3.tgz#0f45612d62526c98c75da0682aa8c26b902b5e07"
+  integrity sha512-5lDvSqIxCYJ/BAd6rQGK/AzFRhBkbu4JHVMLmGh/hCb7U3CqSnr5Tjwfy9vc+/5wG2DJ6wttgAaA7MoCgvBKZQ==
   optionalDependencies:
-    turbo-darwin-64 "2.0.4"
-    turbo-darwin-arm64 "2.0.4"
-    turbo-linux-64 "2.0.4"
-    turbo-linux-arm64 "2.0.4"
-    turbo-windows-64 "2.0.4"
-    turbo-windows-arm64 "2.0.4"
+    turbo-darwin-64 "2.2.3"
+    turbo-darwin-arm64 "2.2.3"
+    turbo-linux-64 "2.2.3"
+    turbo-linux-arm64 "2.2.3"
+    turbo-windows-64 "2.2.3"
+    turbo-windows-arm64 "2.2.3"
 
 type-check@^0.4.0, type-check@~0.4.0:
   version "0.4.0"
@@ -9919,20 +9946,10 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
 
-typescript@5.4.2:
-  version "5.4.2"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372"
-  integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==
-
-typescript@^4.7.3, typescript@^4.9.0, typescript@^4.9.5:
-  version "4.9.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
-  integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
-
-typescript@^5.0.3:
-  version "5.4.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
-  integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
+typescript@5.4.2, typescript@^5.0.3, typescript@^5.4.5:
+  version "5.6.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
+  integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==
 
 uc.micro@^1.0.1, uc.micro@^1.0.5:
   version "1.0.6"
@@ -10017,6 +10034,14 @@ update-browserslist-db@^1.0.16:
     escalade "^3.1.2"
     picocolors "^1.0.1"
 
+update-browserslist-db@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5"
+  integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==
+  dependencies:
+    escalade "^3.2.0"
+    picocolors "^1.1.0"
+
 update-notifier@6.0.2:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-6.0.2.tgz#a6990253dfe6d5a02bd04fbb6a61543f55026b60"
@@ -10125,7 +10150,7 @@ vite-node@1.6.0:
     picocolors "^1.0.0"
     vite "^5.0.0"
 
-vite-plugin-dts@^3.8.1:
+vite-plugin-dts@^3.8.1, vite-plugin-dts@^3.9.1:
   version "3.9.1"
   resolved "https://registry.yarnpkg.com/vite-plugin-dts/-/vite-plugin-dts-3.9.1.tgz#625ad388ec3956708ccec7960550a7b0a8e8909e"
   integrity sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==
@@ -10163,7 +10188,7 @@ vite-plugin-zip-pack@^1.2.2:
   dependencies:
     jszip "^3.10.1"
 
-vite@^5.0.0, "vite@^5.0.0 || ^4.1.4", vite@^5.2.8:
+vite@^5.0.0, "vite@^5.0.0 || ^4.1.4", vite@^5.2.8, vite@^5.3.1:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.1.tgz#bb2ca6b5fd7483249d3e86b25026e27ba8a663e6"
   integrity sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==
@@ -10307,6 +10332,11 @@ webidl-conversions@^6.1.0:
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
   integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
 
+webidl-conversions@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
+  integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
+
 whatwg-encoding@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
@@ -10319,6 +10349,11 @@ whatwg-mimetype@^2.3.0:
   resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
   integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
 
+whatwg-mimetype@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
+  integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
+
 whatwg-url@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"