Skip to content

Track posthog-js rrweb divergences to contribute back upstream #3766

Description

@pauldambra

Track posthog-js rrweb divergences to contribute back upstream

We maintain a divergent fork of rrweb-io/rrweb under packages/rrweb/*. Where a divergence is a general improvement (not a PostHog-product decision), we want to contribute it back so our fork stays thin and upstream benefits.

This replaces the old needs-to-be-contributed-back label workflow from the retired PostHog/posthog-rrweb repo. The label has been recreated here — apply needs-to-be-contributed-back to any future posthog-js PR that adds an rrweb divergence, and add a row below.

Each candidate is assessed against current upstream (2.1.2 + open PRs) as one of: novel (worth opening a PR), in-flight (we already have an open upstream PR — just needs landing), adopted (upstream already fixed it — drop), or product-specific (keep in fork). Companion pull-in tracker: #3765.


Strongest novel candidates — open these upstream

These are general, low-coupling, and PostHog-agnostic.

Divergence Source Upstream status Notes
CSS var() shorthand <style> textContent preservation #3542 (rrweb-snapshot/src/utils.ts hasEmptyShorthandLonghand) novel — fixes OPEN bug Upstream issue #1667 is open and unfixed; PR #1322 adds only a failing test. Our fix + #1322's test = a ready-made PR. Top priority.
preload-as-style infinite polling/listener leak #3667 (snapshot.ts stylesheetLoadTracked + resetStylesheetLoadTracking) novel Upstream onceStylesheetLoaded has only a local fired flag + timeout — no dedupe Map, no AbortController, no reset. Real leak on SPAs re-snapshotting <link rel=preload as=style>.
Replay scroll re-apply after fast-forward/seek #3736 (replay/index.ts lastScrollMap, re-applied on Flush) novel Upstream replayer does a single scrollTo with no retry; scrolls to not-yet-scrollable targets clamp to 0. Clean standalone replayer fix.
<link> missing-href guard old #18 (snapshot.ts hrefFrom()) novel Upstream still does unguarded (n as HTMLLinkElement).href. Tiny defensive fix.
maxDepth cap on DOM serialization old #130 (snapshot.ts) novel Upstream serializeNodeWithId has no depth cap; general crash/payload fix (default 50 + one-time warn). No upstream PR/issue exists.
empty-CDATA rebuild guard old #70 (rebuild.ts) novel (overlaps OPEN #1740) Upstream createCDATASection has no empty guard; OPEN PR #1740 fixes the same area differently (text-node swap). Coordinate / pick one approach.
Angular Zone __symbol__ unpatched-original detection old #24 (utils/src/index.ts angularZoneUnpatchedAlternative) novel Upstream still uses the older isAngularZonePresent(). Could fold into our open #1633.
Replayer destroy() cleanup + idempotency old #92 (+#122) (replay/index.ts emitterHandlers) novel Upstream destroy() only pauses + resets mirrors/media; doesn't track handlers, isn't idempotent, doesn't clear imageMap/canvasEventMap/timeouts.
Keep replay autofill off through attribute mutations #4861 (rrweb/src/replay/index.ts:2064) novel — closes a hole in merged #1771 Upstream #1771 (merged, shipped in 2.1.3) forces autocomplete="off" at rebuild time only. A recorded attribute mutation on that field then sets it back to the page's real value, or removes it outright, and the viewer's browser offers autofill again part-way through the replay — the exact leak #1771 exists to prevent. Ours also skips autocomplete mutations on <input>/<textarea> when applying them, in both the live and fast-forward paths. Small, self-contained, and strictly completes work upstream already accepted, so it should be an easy sell. Covered by rrweb/test/replayer.test.ts + test/events/input-autocomplete-mutation.ts, which fail without the guard (expected [ 'email', null ] to deeply equal [ 'off', 'off' ]) — the two failure modes at once. Best next PR on this list.
Narrow custom-event re-cast on seek shouldCastInSyncMode (rrweb/src/replay/index.ts:835) novel — upstream took the blunt version Upstream #1769 (merged, shipped in 2.1.2; the same author's #1771 then landed in 2.1.3) simply deletes the case EventType.Custom skip, so every custom event re-fires on every seek. Ours re-casts only the event that carries DOM state (rrweb/fullscreen) and keeps side-effect-free signals skipped. Offer it as a refinement on top of #1769 — or drop ours and take upstream's. Decide first.
Page-lifetime keepalive for the untainted-prototype iframe #4128 (utils/src/index.ts keepIframeAttached) mostly overtaken by OPEN #1934 — do not open a PR We kept the shared iframe attached for the page lifetime because upstream removed it on recorder teardown, which detaches it while live per-document observers still need the cached prototype, and breaks stop/restart cycles. Upstream has since converged on the same conclusion from the other side: #1934 keeps the WebKit keepalive but narrows it to MutationObserver only, which is the single key that needs a live ScriptExecutionContext. Ours is now the over-broad one — we retain an iframe for all four BasePrototypeCache keys. The part still worth saying upstream is small: the page-lifetime vs teardown argument for MutationObserver. Say it as a comment on #1934, not as a PR. Our ph-no-capture tag stays fork-only (our blockClass differs).
attributeFilter shadow-root plumbing + empty-array guard #4129 (record/index.ts, record/observer.ts) novel — fixes OPEN #1873 Reviewing #1873 for adoption found two gaps: upstream never threads attributeFilter through shadow roots (bypassOptions), and an empty array silently disables all attribute recording instead of being treated as unset. Both are small comments/commits on the open PR — cheapest contribute-back on the list.
Hoist the non-user-initiated source list out of the emit path #4131 (record/index.ts nonUserInitiatedSources) novel — refines OPEN #1697 Upstream allocates the source list per emitted event; ours hoists it into a module-level Set. One-line comment on the open PR.
Plumb dataURLOptions (type/quality) into canvas toDataURL old #95 (record/observers/canvas/serialize-args.ts) novel (plumbing only) Upstream calls toDataURL() with no args. Contribute the plumbing; leave PostHog's webp/q0.4 defaults out.

Coordinate with in-flight / open PRs (don't open parallel PRs)

Divergence Source Related upstream Action
sheet.href for SPA stylesheet-href stability #3635 (snapshot.ts transformAttribute/serializeElementNode) OPEN #1825 (d0ugal, different code path — CSS content, not the href attribute); merged #1705; closed #1700 Frame ours as the companion to #1825 (href attribute vs CSS content). Coordinate with d0ugal.
WebGL context-lost canvas skip #3527 (canvas-manager.ts isContextLost() pre-flight + getCanvas try/catch) OPEN #1469 (daibhin, catches createImageBitmap errors); merged #1777 (unrelated WebGL-exists guard) Ours is the better mechanism (skip lost contexts before they poison the fingerprint-dedup map). Land jointly with / on top of #1469.
iframe mutation-buffer cleanup race #3558 (observer.ts findAndRemoveIframeBuffer(knownDocs) + attachedDocuments) OPEN #1755 (megboehlert) Tightly coupled with #3570 below.
same-origin iframe state leak on removal #3570 (record/index.ts iframeObserverCleanups; IframeManager.destroy()) seeds OPEN #1755; prior art closed #1746/#1747 Contribute #3558+#3570 as one combined iframe-lifecycle PR that extends #1755.
iframe attach/re-attach + lifecycle cleanup cluster old #108, #121, #123, #142, #91, #94, #100 (iframe-manager.ts, record/index.ts) addresses upstream issue #1720; overlaps external OPEN #1791 "Address Iframe memory leaks" Same surface as #1755/#1791. Bundle into the combined iframe-lifecycle PR rather than many small ones.

Our PRs already open / merged / closed upstream

PostHog-authored (or PostHog-seeded) PRs on rrweb-io/rrweb. Verified individually by PR number (the multi-author search index is unreliable cross-repo).

PR State Corresponds to Next action
#1597 merged Angular wrapped MutationObserver detection (pre-fork) Done — in our fork base.
#1705 merged node.baseURI for stylesheet hrefs (related to #3635) Done. Our #3635 layers on top.
#1633 open untainted prototype access in Angular Nudge to land.
#1635 open iframe + custom web components (Chrome) Nudge to land.
#1641 open preserve adopted styles (virtual dom) Nudge to land.
#1686 open stringifyRule error swallowing Reflected in our utils.ts (cites #1686). Nudge to land.
#1712 merged (2026-09-04, shipped in 2.1.2) console.log -> suppressible warn Done — and already present in our fork (rrweb/src/replay/index.ts:298).
#1755 open iframe pagehide buffer/mirror cleanup -> seeds #3558+#3570 Extend into the combined iframe-lifecycle PR.
#1806 closed unmerged (2026-06-17, by author) virtual-dom backward-skip playback Re-propose or vendor from the closed branch — the fix exists nowhere else. Also a pull-in item in #3765.
#1814 open untainted add/remove listeners Nudge to land.
#1825 closed unmerged (2026-06-17) link.sheet for CSS capture on WebKit (companion to #3635) Superseded by maintainer work on the stylesheet-assets branch. Our #3635 href-attribute companion is still unproposed — open it standalone.
#1826 closed unmerged (2026-06-17) Zone.js unpatched MutationObserver on Safari Superseded upstream by the merged #1854. No action.
#1685 closed stringifyRule (superseded by #1686) None.
#1700 closed document.baseURI sheetHref (superseded by #1705) None.
#1746 / #1747 closed iframe mem-leak (prior art for #3570) Cite as prior art in the combined PR.
#1818 closed swallow CSS insertRule parse errors Decide whether to re-propose.
#1915 closed same-day (2026-08-26) event-listener leak (kevinatown) Confirm whether this was a PostHog attempt and whether the leak is one our fork also carries; if so, re-propose properly.

Possibly-ours (high-trust contributors — confirm affiliation before claiming): #1642 (kevinatown, blockElementFn), #1469 (daibhin, bitmap errors), #1769 / #1771 (heathdutton, seek custom events / replay autocomplete — both merged 2026-09-08, so this is now only a question of who to credit, not of landing anything). Note that we have since adopted #1771 and extended it (see the autofill-mutation row above), so a PR there lands on heathdutton's code.


Adopted upstream / product-specific — drop or carve out

Item Source Verdict
applyStyleDeclaration null-safety old #86 Adopted upstream via merged #1775. Drop.
splitCssText/normalizeCssString caching old #17 Gone from our fork (lost in an upstream re-sync; upstream replaced it with 0px-normalization). Moot.
Don't bundle css parsing / postcss into recorder old #96, #101 Packaging-specific (@posthog/rrweb-snapshot/record subpath exports + our vite config). Keep fork-only. (Upstream's own version is OPEN #1784.)
base64 image recompression + striped-placeholder new (maxBase64ImageLength, recompressBase64Image, STRIPED_PLACEHOLDER_SVG) + old #99 Product/storage-cost policy (lossy webp q0.4 + fixed placeholder). Keep in fork; at most offer upstream as opt-in config.
ph-no-capture blocked-element layout fix #3678 Carve out: contribute the general rr_position/rr_transform/rr_display flow-preservation; drop the ph_rr_could_not_detect_modal diagnostic attribute (PostHog-specific).
CanvasManager reference-counting teardown #3726 Novel and correct, but higher coupling — changes the manager lifecycle contract (every per-root teardown must acquire()/reset() in pairs). Get a maintainer design nod before opening a PR; don't lead with it.

Summary

Assessed against upstream rrweb-io/rrweb on 2026-06-08; PR states and new divergences re-verified on 2026-09-08 against the 2.1.2, then the 2.1.3 / 2.1.4 releases; the autofill-mutation divergence added 2026-09-10 when we adopted #1771. Re-verify a row's status before acting on it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions