Skip to content

fix(record): recover CSS rules when another script displaces the insertRule patch - #38

Merged
Vadman97 merged 2 commits into
mainfrom
vkorolik/css-rule-resync
Aug 13, 2026
Merged

fix(record): recover CSS rules when another script displaces the insertRule patch#38
Vadman97 merged 2 commits into
mainfrom
vkorolik/css-rule-resync

Conversation

@Vadman97

@Vadman97 Vadman97 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

CSS-in-JS rules only reached the replayer via the CSSStyleSheet.prototype.insertRule patch, so any script that reinstalls a previously captured insertRule silently unhooks recording and the replay freezes with the CSS that existed at snapshot time. Recording now probes that its patch still runs, reinstalls it over whatever displaced it, re-sends the missing rules, and on teardown only unwinds its own layer.

Found while investigating a production session that kept 734 of 2929 emotion rules and replayed a MUI-heavy modal completely unstyled.

  - End-to-end against the real session with the published player: injecting the events the fix would emit took the emotion sheet from 734 → 2929 rules and made .css-df1p9r/.css-q679rt/.css-wqv4tk resolve; at session start it took the sheet 32 → 614 rules
  and #programsTable from 998×3277 (broken) to 1080×700 — the same geometry as the healthy render. Modal classes with no rule went 47 → 15, the residual only because I borrowed rule text from a later page load that lacks that load's styled-components
  hashes.

🤖 Generated with Claude Code


Note

Overview
When another script restores a captured CSSStyleSheet.prototype.insertRule, CSS-in-JS runtime rules stop reaching the recording and replay stays stuck at snapshot-time CSS. Recording now periodically probes whether its patches still run (via a throwaway constructed stylesheet), reinstalls displaced insertRule/deleteRule proxies over whatever is on the prototype, and re-emits missing rules as normal StyleSheetRule events.

The new styleSheetResyncInterval record option controls that interval (default 2000 ms; 0 disables). Teardown uses restorePatchedMethod so only rrweb’s layer is removed—stopping the recorder no longer strips another vendor’s wrapper; proxies that remain in the chain go silent via a stopped flag, with re-entrancy guards so chained self-repatches do not double-report rules.

Puppeteer tests cover displacement, repatch, no duplicate resync, partial displacement, cooperative wrappers, and teardown behavior.

Reviewed by Cursor Bugbot for commit dfead46. Bugbot is set up for automated code reviews on this repo. Configure here.

…rtRule patch

CSS-in-JS rules only ever reached the replayer through the monkey patch on
CSSStyleSheet.prototype.insertRule, since the <style> elements emotion and
styled-components own hold no text and so have no copy of their contents beyond
the _cssText taken at serialization. Any script that reinstalls a previously
captured insertRule silently unhooks us and the replay is stuck with the CSS that
existed at snapshot time; a production session kept 734 of 2929 emotion rules
that way and replayed a MUI-heavy UI completely unstyled.

Recording now probes that its patch still runs, reinstalls it over whatever
displaced it, and re-sends the rules the replayer is missing. Teardown only
unwinds its own layer so that stopping a recorder cannot blind another script
that patched on top of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Vadman97
Vadman97 requested a review from a team August 11, 2026 20:25
@Vadman97
Vadman97 marked this pull request as ready for review August 11, 2026 20:25

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9b21f4f. Configure here.

Comment thread packages/rrweb/src/record/observer.ts
Comment thread packages/rrweb/src/record/observer.ts Outdated
Comment thread packages/rrweb/src/record/observer.ts

@RobertPakkoLD RobertPakkoLD left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would resolve the bugbot comments, but general approach seems fine to me.

…ng stop

Bugbot found three ways the recovery path could misbehave once more than one
layer of patches is in play on CSSStyleSheet.prototype.

Health is now judged per method. A script that reinstalls only insertRule -- the
common case -- used to make us reinstall deleteRule as well, even though our
delete patch was still in the call chain, so every later deletion was reported
twice and the rule bookkeeping drifted.

The proxies also guard against being chained over themselves: only the outermost
of ours reports a call. That is what makes the identity based health check safe
on browsers without constructed stylesheets, where a wrapper that calls through
cannot be told apart from a displacement and we would otherwise stack a second
insert proxy on top of the first.

Finally, teardown cannot uninstall a patch another script has wrapped, so the
patches now fall silent once recording has stopped rather than carrying on
emitting stylesheet mutations into a recording that is supposed to be over.

Co-Authored-By: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Vadim Korolik <vkorolik@launchdarkly.com>
@Vadman97
Vadman97 merged commit fe8256d into main Aug 13, 2026
14 checks passed
@Vadman97
Vadman97 deleted the vkorolik/css-rule-resync branch August 13, 2026 19:04
Vadman97 added a commit to launchdarkly/observability-sdk that referenced this pull request Aug 13, 2026
…ks rrweb (#725)

Bumps the rrweb submodule to
[fe8256d5](launchdarkly/rrweb@fe8256d)
([launchdarkly/rrweb#38](launchdarkly/rrweb#38))
so recording recovers CSS-in-JS rules when another script on the page
displaces rrweb's `CSSStyleSheet.prototype.insertRule` patch. A
production session kept 734 of 2929 emotion rules that way and replayed
a MUI-heavy UI completely unstyled.

The README touch is what gives release-please a path inside
`sdk/highlight-run/` to attribute the release to — same approach as
#615. `highlight.run` gets a patch bump, and
`@launchdarkly/session-replay` / `@launchdarkly/observability` follow
via the node-workspace plugin.

Also picks up launchdarkly/rrweb#36 and #37; both are replay-side and
inert for the recorder bundles these packages ship. No dependency
changes in the bumped range, so `yarn.lock` is untouched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Bumps the **launchdarkly/rrweb** submodule (via upstream PR #38) so
session replay **periodically reconciles CSS-in-JS stylesheets**
(emotion, styled-components) against what recording has already
reported. That recovers style rules when another on-page script
**replaces rrweb’s `CSSStyleSheet.prototype.insertRule` hook**, which
previously left MUI-heavy replays largely unstyled.
> 
> The **README** adds a short note describing that behavior and gives
**release-please** a file under `sdk/highlight-run/` to attribute a
patch release (same pattern as #615). Related rrweb replay-only fixes
(#36, #37) ride along but do not change recorder bundles; **no
`yarn.lock` changes** in the bumped range.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
7492160. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants