Skip to content

fix(replay): stop polling preload-as-style <link> elements forever#3667

Merged
pauldambra merged 13 commits into
mainfrom
posthog-code/fix-preload-link-listener-leak
May 26, 2026
Merged

fix(replay): stop polling preload-as-style <link> elements forever#3667
pauldambra merged 13 commits into
mainfrom
posthog-code/fix-preload-link-listener-leak

Conversation

@pauldambra
Copy link
Copy Markdown
Member

@pauldambra pauldambra commented May 25, 2026

Problem

A customer reported (with a detailed trace) that the session recorder was saturating the main thread on pages with <link rel="preload" as="style" href="*.css"> elements — common in Next.js-chunked apps. They measured ~1,440 concurrently active polling chains on a single preload link after ~2 hours, firing ~408/s and consuming ~55% of the main thread. Backgrounding the tab + refocusing produced multi-hundred-ms freezes as Chrome flushed the queued timers.

Root cause is in rrweb-snapshot's onceStylesheetLoaded path, which three defects compose into a runaway:

  1. The link-tag predicate treats rel="preload" as="style" the same as rel="stylesheet", but per spec preload links never instantiate a CSSStyleSheetlink.sheet stays null forever, so the timeout always fires.
  2. onceStylesheetLoaded never removes its load listener, has no idempotency guard, and the load handler is not gated by fired — so every real load event multiplies the chain.
  3. The timer-callback recursively re-enters serializeNodeWithId on the same link, scheduling another chain every cycle.

Upstream rrweb has the same bug (rrweb-io/rrweb#1707, still open; fix attempt #1708 closed unmerged).

Changes

packages/rrweb/rrweb-snapshot/src/snapshot.ts:

  • Drop rel="preload" from the stylesheet predicate. Preload links are still serialized normally by the surrounding pass — we just stop scheduling a never-ending wait on them.
  • Replace the recursive serializeNodeWithId call inside the load callback with the already-computed serializedNode from the enclosing scope.
  • Make onceStylesheetLoaded idempotent via a module-level WeakSet; share one fired flag between the timer path and the load path; pass { once: true } so the listener self-removes after firing.

Tests:

  • packages/rrweb/rrweb-snapshot/test/snapshot.test.ts — three new jsdom unit tests covering preload non-tracking, stylesheet load-firing-once, and synthetic-load-event no-multiplication.
  • packages/browser/playwright/mocked/session-recording/session-recording-preload-link-leak.spec.ts + packages/browser/playground/preload-link-leak/index.html — real-browser repro that instruments HTMLLinkElement.prototype.addEventListener, lets the recorder run for two 6-second windows with a synthetic load dispatch in between, and asserts the load-add count stays bounded. Without the fix: 30 leaked listeners. With the fix: 0.

Release info Sub-libraries affected

Libraries affected

  • posthog-js (web)

Checklist

  • Tests for new code
  • Accounted for the impact of any changes across different platforms
  • Accounted for backwards compatibility of any changes (no breaking changes!)
  • Took care not to unnecessarily increase the bundle size

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

Created with PostHog Code

Session recorder treated <link rel="preload" as="style" href="*.css"> as if it
were a stylesheet and waited for link.sheet to populate. Per spec preload
links never instantiate a CSSStyleSheet, so the wait timed out, recursively
re-serialized the link, scheduled another wait, and leaked a 'load' listener
on every cycle - multiplying further on every real load event. Pages with
Next.js-style CSS preloads accumulated thousands of active polling chains,
saturating the main thread and freezing the tab on refocus.

- Drop preload from the predicate so only rel=stylesheet schedules a wait.
- Replace the recursive serializeNodeWithId call with the serializedNode
  already in scope.
- Track tracked links in a WeakSet so repeat calls are no-ops; gate the load
  handler on the same 'fired' guard as the timer; pass { once: true } so the
  listener self-removes if a load event ever fires.

Added jsdom unit tests in rrweb-snapshot and a real-browser Playwright spec
that loads a page with five preload-as-style links, instruments
HTMLLinkElement.prototype.addEventListener, and asserts the count stays
bounded across timer cycles plus dispatched load events. Without the fix
the spec sees ~30 leaked listeners; with the fix it sees zero.

Generated-By: PostHog Code
Task-Id: 18dbe2b5-9a25-4b4c-a756-db029804f620
@vercel
Copy link
Copy Markdown

vercel Bot commented May 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
posthog-example-next-app-router Ready Ready Preview May 26, 2026 1:34pm
posthog-js Ready Ready Preview May 26, 2026 1:34pm
posthog-nextjs-config Ready Ready Preview May 26, 2026 1:34pm

Request Review

Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@pauldambra pauldambra marked this pull request as ready for review May 25, 2026 17:03
@greptile-apps

This comment has been minimized.

Comment thread packages/rrweb/rrweb-snapshot/src/snapshot.ts
Comment thread packages/rrweb/rrweb-snapshot/test/snapshot.test.ts
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 25, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 25, 2026

Size Change: +6.09 kB (+0.04%)

Total Size: 16.4 MB

Filename Size Change
packages/browser/dist/all-external-dependencies.js 260 kB +56 B (+0.02%)
packages/browser/dist/array.full.js 428 kB +49 B (+0.01%)
packages/browser/dist/array.full.no-********.js 502 kB +49 B (+0.01%)
packages/browser/dist/lazy-********.js 151 kB +57 B (+0.04%)
packages/browser/dist/module.full.js 431 kB +49 B (+0.01%)
packages/browser/dist/module.full.no-********.js 504 kB +49 B (+0.01%)
packages/browser/dist/posthog-********.js 151 kB +56 B (+0.04%)
packages/browser/dist/recorder-v2.js 98.7 kB +56 B (+0.06%)
packages/browser/dist/recorder.js 98.7 kB +56 B (+0.06%)
packages/browser/dist/rrweb.js 279 kB +73 B (+0.03%)
packages/rrweb/all/dist/rrweb-all.cjs 611 kB +354 B (+0.06%)
packages/rrweb/all/dist/rrweb-all.js 611 kB +354 B (+0.06%)
packages/rrweb/all/dist/rrweb-all.umd.cjs 614 kB +354 B (+0.06%)
packages/rrweb/all/dist/rrweb-all.umd.min.cjs 290 kB +107 B (+0.04%)
packages/rrweb/record/dist/rrweb-record.cjs 173 kB +354 B (+0.2%)
packages/rrweb/record/dist/rrweb-record.js 173 kB +354 B (+0.2%)
packages/rrweb/record/dist/rrweb-record.umd.cjs 174 kB +354 B (+0.2%)
packages/rrweb/record/dist/rrweb-record.umd.min.cjs 83.3 kB +107 B (+0.13%)
packages/rrweb/rrweb-snapshot/dist/record.cjs 32.2 kB +380 B (+1.19%)
packages/rrweb/rrweb-snapshot/dist/record.js 31.2 kB +350 B (+1.14%)
packages/rrweb/rrweb-snapshot/dist/record.umd.cjs 53.5 kB +378 B (+0.71%)
packages/rrweb/rrweb-snapshot/dist/record.umd.min.cjs 25.6 kB +107 B (+0.42%)
packages/rrweb/rrweb-snapshot/dist/rrweb-********.cjs 2.27 kB +74 B (+3.37%)
packages/rrweb/rrweb-snapshot/dist/rrweb-********.js 1.42 kB +60 B (+4.42%)
packages/rrweb/rrweb-snapshot/dist/rrweb-********.umd.cjs 217 kB +516 B (+0.24%)
packages/rrweb/rrweb-snapshot/dist/rrweb-********.umd.min.cjs 91.5 kB +169 B (+0.19%)
packages/rrweb/rrweb/dist/rrweb.cjs 594 kB +354 B (+0.06%)
packages/rrweb/rrweb/dist/rrweb.js 594 kB +354 B (+0.06%)
packages/rrweb/rrweb/dist/rrweb.umd.cjs 595 kB +354 B (+0.06%)
packages/rrweb/rrweb/dist/rrweb.umd.min.cjs 280 kB +107 B (+0.04%)
ℹ️ View Unchanged
Filename Size Change
packages/ai/dist/anthropic/index.cjs 26 kB 0 B
packages/ai/dist/anthropic/index.mjs 25.6 kB 0 B
packages/ai/dist/gemini/index.cjs 34.4 kB 0 B
packages/ai/dist/gemini/index.mjs 34.2 kB 0 B
packages/ai/dist/index.cjs 171 kB 0 B
packages/ai/dist/index.mjs 170 kB 0 B
packages/ai/dist/langchain/index.cjs 47.9 kB 0 B
packages/ai/dist/langchain/index.mjs 47.3 kB 0 B
packages/ai/dist/openai-agents/index.cjs 25.5 kB 0 B
packages/ai/dist/openai-agents/index.mjs 25.4 kB 0 B
packages/ai/dist/openai/index.cjs 50.7 kB 0 B
packages/ai/dist/openai/index.mjs 50.3 kB 0 B
packages/ai/dist/otel/index.cjs 4.97 kB 0 B
packages/ai/dist/otel/index.mjs 4.86 kB 0 B
packages/ai/dist/vercel/index.cjs 44.5 kB 0 B
packages/ai/dist/vercel/index.mjs 44.5 kB 0 B
packages/browser/dist/array.full.es5.js 348 kB 0 B
packages/browser/dist/array.js 192 kB 0 B
packages/browser/dist/array.no-********.js 209 kB 0 B
packages/browser/dist/conversations.js 67.3 kB 0 B
packages/browser/dist/crisp-chat-integration.js 1.97 kB 0 B
packages/browser/dist/customizations.full.js 18 kB 0 B
packages/browser/dist/dead-clicks-autocapture.js 14.3 kB 0 B
packages/browser/dist/default-extensions.js 190 kB 0 B
packages/browser/dist/element-inference.js 5.69 kB 0 B
packages/browser/dist/exception-autocapture.js 11.8 kB 0 B
packages/browser/dist/extension-bundles.js 106 kB 0 B
packages/browser/dist/external-scripts-loader.js 3.13 kB 0 B
packages/browser/dist/intercom-integration.js 2.03 kB 0 B
packages/browser/dist/logs.js 38.9 kB 0 B
packages/browser/dist/main.js 196 kB 0 B
packages/browser/dist/module.js 196 kB 0 B
packages/browser/dist/module.no-********.js 213 kB 0 B
packages/browser/dist/module.slim.js 102 kB 0 B
packages/browser/dist/module.slim.no-********.js 107 kB 0 B
packages/browser/dist/product-tours-preview.js 76.4 kB 0 B
packages/browser/dist/product-tours.js 115 kB 0 B
packages/browser/dist/rrweb-plugin-console-record.js 6.67 kB 0 B
packages/browser/dist/rrweb-types.js 2.28 kB 0 B
packages/browser/dist/surveys-preview.js 76.3 kB 0 B
packages/browser/dist/surveys.js 94.7 kB 0 B
packages/browser/dist/tracing-headers.js 1.84 kB 0 B
packages/browser/dist/web-vitals-with-attribution.js 11.8 kB 0 B
packages/browser/dist/web-vitals.js 6.39 kB 0 B
packages/browser/react/dist/esm/index.js 21.2 kB 0 B
packages/browser/react/dist/esm/slim/index.js 17.6 kB 0 B
packages/browser/react/dist/esm/surveys/index.js 4.68 kB 0 B
packages/browser/react/dist/umd/index.js 24.4 kB 0 B
packages/browser/react/dist/umd/slim/index.js 20.4 kB 0 B
packages/browser/react/dist/umd/surveys/index.js 5.45 kB 0 B
packages/convex/dist/client/feature-flags/crypto.js 461 B 0 B
packages/convex/dist/client/feature-flags/evaluator.js 16.5 kB 0 B
packages/convex/dist/client/feature-flags/index.js 196 B 0 B
packages/convex/dist/client/feature-flags/match-********.js 14.8 kB 0 B
packages/convex/dist/client/feature-flags/types.js 44 B 0 B
packages/convex/dist/client/index.js 14.7 kB 0 B
packages/convex/dist/component/_generated/api.js 712 B 0 B
packages/convex/dist/component/_generated/component.js 212 B 0 B
packages/convex/dist/component/_generated/dataModel.js 230 B 0 B
packages/convex/dist/component/_generated/server.js 3.71 kB 0 B
packages/convex/dist/component/convex.config.js 133 B 0 B
packages/convex/dist/component/lib.js 19.9 kB 0 B
packages/convex/dist/component/schema.js 694 B 0 B
packages/convex/dist/component/version.js 67 B 0 B
packages/core/dist/cookie.js 5.34 kB 0 B
packages/core/dist/cookie.mjs 3.12 kB 0 B
packages/core/dist/error-tracking/chunk-ids.js 2.54 kB 0 B
packages/core/dist/error-tracking/chunk-ids.mjs 1.31 kB 0 B
packages/core/dist/error-tracking/coercers/dom-exception-coercer.js 2.3 kB 0 B
packages/core/dist/error-tracking/coercers/dom-exception-coercer.mjs 993 B 0 B
packages/core/dist/error-tracking/coercers/error-coercer.js 2.02 kB 0 B
packages/core/dist/error-tracking/coercers/error-coercer.mjs 794 B 0 B
packages/core/dist/error-tracking/coercers/error-event-coercer.js 1.76 kB 0 B
packages/core/dist/error-tracking/coercers/error-event-coercer.mjs 513 B 0 B
packages/core/dist/error-tracking/coercers/event-coercer.js 1.82 kB 0 B
packages/core/dist/error-tracking/coercers/event-coercer.mjs 548 B 0 B
packages/core/dist/error-tracking/coercers/index.js 6.79 kB 0 B
packages/core/dist/error-tracking/coercers/index.mjs 326 B 0 B
packages/core/dist/error-tracking/coercers/object-coercer.js 3.46 kB 0 B
packages/core/dist/error-tracking/coercers/object-coercer.mjs 2.07 kB 0 B
packages/core/dist/error-tracking/coercers/primitive-coercer.js 1.67 kB 0 B
packages/core/dist/error-tracking/coercers/primitive-coercer.mjs 419 B 0 B
packages/core/dist/error-tracking/coercers/promise-rejection-event.js 2.59 kB 0 B
packages/core/dist/error-tracking/coercers/promise-rejection-event.mjs 1.25 kB 0 B
packages/core/dist/error-tracking/coercers/string-coercer.js 2.01 kB 0 B
packages/core/dist/error-tracking/coercers/string-coercer.mjs 820 B 0 B
packages/core/dist/error-tracking/coercers/utils.js 2.06 kB 0 B
packages/core/dist/error-tracking/coercers/utils.mjs 716 B 0 B
packages/core/dist/error-tracking/error-properties-builder.js 5.56 kB 0 B
packages/core/dist/error-tracking/error-properties-builder.mjs 4.23 kB 0 B
packages/core/dist/error-tracking/exception-steps.js 6.87 kB 0 B
packages/core/dist/error-tracking/exception-steps.mjs 4.71 kB 0 B
packages/core/dist/error-tracking/index.js 4.74 kB 0 B
packages/core/dist/error-tracking/index.mjs 191 B 0 B
packages/core/dist/error-tracking/parsers/base.js 1.83 kB 0 B
packages/core/dist/error-tracking/parsers/base.mjs 464 B 0 B
packages/core/dist/error-tracking/parsers/chrome.js 2.73 kB 0 B
packages/core/dist/error-tracking/parsers/chrome.mjs 1.32 kB 0 B
packages/core/dist/error-tracking/parsers/gecko.js 2.47 kB 0 B
packages/core/dist/error-tracking/parsers/gecko.mjs 1.13 kB 0 B
packages/core/dist/error-tracking/parsers/index.js 4.75 kB 0 B
packages/core/dist/error-tracking/parsers/index.mjs 2.1 kB 0 B
packages/core/dist/error-tracking/parsers/node.js 3.94 kB 0 B
packages/core/dist/error-tracking/parsers/node.mjs 2.68 kB 0 B
packages/core/dist/error-tracking/parsers/opera.js 2.26 kB 0 B
packages/core/dist/error-tracking/parsers/opera.mjs 746 B 0 B
packages/core/dist/error-tracking/parsers/safari.js 1.88 kB 0 B
packages/core/dist/error-tracking/parsers/safari.mjs 574 B 0 B
packages/core/dist/error-tracking/parsers/winjs.js 1.72 kB 0 B
packages/core/dist/error-tracking/parsers/winjs.mjs 426 B 0 B
packages/core/dist/error-tracking/types.js 1.33 kB 0 B
packages/core/dist/error-tracking/types.mjs 131 B 0 B
packages/core/dist/error-tracking/utils.js 1.8 kB 0 B
packages/core/dist/error-tracking/utils.mjs 604 B 0 B
packages/core/dist/eventemitter.js 1.78 kB 0 B
packages/core/dist/eventemitter.mjs 571 B 0 B
packages/core/dist/featureFlagUtils.js 6.8 kB 0 B
packages/core/dist/featureFlagUtils.mjs 4.32 kB 0 B
packages/core/dist/gzip.js 5.72 kB 0 B
packages/core/dist/gzip.mjs 3.84 kB 0 B
packages/core/dist/index.js 13.6 kB 0 B
packages/core/dist/index.mjs 1.31 kB 0 B
packages/core/dist/logs/index.js 9.47 kB 0 B
packages/core/dist/logs/index.mjs 7.87 kB 0 B
packages/core/dist/logs/logs-utils.js 5.96 kB 0 B
packages/core/dist/logs/logs-utils.mjs 3.99 kB 0 B
packages/core/dist/logs/types.js 603 B 0 B
packages/core/dist/logs/types.mjs 0 B 0 B 🆕
packages/core/dist/posthog-core-stateless.js 33.8 kB 0 B
packages/core/dist/posthog-core-stateless.mjs 31.1 kB 0 B
packages/core/dist/posthog-core.js 42 kB 0 B
packages/core/dist/posthog-core.mjs 37 kB 0 B
packages/core/dist/surveys/events.js 4.21 kB 0 B
packages/core/dist/surveys/events.mjs 1.99 kB 0 B
packages/core/dist/surveys/index.js 4.57 kB 0 B
packages/core/dist/surveys/index.mjs 894 B 0 B
packages/core/dist/surveys/translations.js 9.4 kB 0 B
packages/core/dist/surveys/translations.mjs 7.03 kB 0 B
packages/core/dist/surveys/validation.js 3.06 kB 0 B
packages/core/dist/surveys/validation.mjs 1.51 kB 0 B
packages/core/dist/testing/index.js 2.93 kB 0 B
packages/core/dist/testing/index.mjs 79 B 0 B
packages/core/dist/testing/PostHogCoreTestClient.js 3.15 kB 0 B
packages/core/dist/testing/PostHogCoreTestClient.mjs 1.74 kB 0 B
packages/core/dist/testing/test-utils.js 2.83 kB 0 B
packages/core/dist/testing/test-utils.mjs 1.15 kB 0 B
packages/core/dist/tracing-headers.js 3.38 kB 0 B
packages/core/dist/tracing-headers.mjs 2.08 kB 0 B
packages/core/dist/types.js 9.62 kB 0 B
packages/core/dist/types.mjs 7.07 kB 0 B
packages/core/dist/utils/bot-detection.js 3.28 kB 0 B
packages/core/dist/utils/bot-detection.mjs 1.95 kB 0 B
packages/core/dist/utils/bucketed-rate-limiter.js 3 kB 0 B
packages/core/dist/utils/bucketed-rate-limiter.mjs 1.62 kB 0 B
packages/core/dist/utils/index.js 11.9 kB 0 B
packages/core/dist/utils/index.mjs 1.98 kB 0 B
packages/core/dist/utils/logger.js 2.58 kB 0 B
packages/core/dist/utils/logger.mjs 1.29 kB 0 B
packages/core/dist/utils/number-utils.js 3.32 kB 0 B
packages/core/dist/utils/number-utils.mjs 1.68 kB 0 B
packages/core/dist/utils/promise-queue.js 2 kB 0 B
packages/core/dist/utils/promise-queue.mjs 768 B 0 B
packages/core/dist/utils/string-utils.js 2.73 kB 0 B
packages/core/dist/utils/string-utils.mjs 1.09 kB 0 B
packages/core/dist/utils/type-utils.js 7.04 kB 0 B
packages/core/dist/utils/type-utils.mjs 3.11 kB 0 B
packages/core/dist/utils/user-agent-utils.js 15.5 kB 0 B
packages/core/dist/utils/user-agent-utils.mjs 12.4 kB 0 B
packages/core/dist/vendor/uuidv7.js 8.29 kB 0 B
packages/core/dist/vendor/uuidv7.mjs 6.72 kB 0 B
packages/next/dist/app/PostHogProvider.js 3.33 kB 0 B
packages/next/dist/client/ClientPostHogProvider.js 1.76 kB 0 B
packages/next/dist/client/hooks.js 172 B 0 B
packages/next/dist/client/PostHogPageView.js 1.76 kB 0 B
packages/next/dist/index.client.js 401 B 0 B
packages/next/dist/index.edge.js 447 B 0 B
packages/next/dist/index.js 444 B 0 B
packages/next/dist/index.react-server.js 420 B 0 B
packages/next/dist/middleware/postHogMiddleware.js 3.7 kB 0 B
packages/next/dist/pages.client.js 502 B 0 B
packages/next/dist/pages.edge.js 570 B 0 B
packages/next/dist/pages.js 414 B 0 B
packages/next/dist/pages/getServerSidePostHog.js 1.99 kB 0 B
packages/next/dist/pages/PostHogPageView.js 1.32 kB 0 B
packages/next/dist/pages/PostHogProvider.js 1.61 kB 0 B
packages/next/dist/server/getPostHog.js 2.79 kB 0 B
packages/next/dist/server/nodeClientCache.js 1.31 kB 0 B
packages/next/dist/shared/browser.js 195 B 0 B
packages/next/dist/shared/config.js 2.08 kB 0 B
packages/next/dist/shared/constants.js 201 B 0 B
packages/next/dist/shared/cookie.js 540 B 0 B
packages/next/dist/shared/identity.js 264 B 0 B
packages/next/dist/shared/tracing-headers.js 2.18 kB 0 B
packages/nextjs-config/dist/config.js 5.82 kB 0 B
packages/nextjs-config/dist/config.mjs 4.34 kB 0 B
packages/nextjs-config/dist/index.js 2.24 kB 0 B
packages/nextjs-config/dist/index.mjs 30 B 0 B
packages/nextjs-config/dist/utils.js 2.94 kB 0 B
packages/nextjs-config/dist/utils.mjs 826 B 0 B
packages/node/dist/client.js 45.5 kB 0 B
packages/node/dist/client.mjs 43.2 kB 0 B
packages/node/dist/entrypoints/index.edge.js 4.25 kB 0 B
packages/node/dist/entrypoints/index.edge.mjs 723 B 0 B
packages/node/dist/entrypoints/index.node.js 6.04 kB 0 B
packages/node/dist/entrypoints/index.node.mjs 1.22 kB 0 B
packages/node/dist/entrypoints/nestjs.js 2.31 kB 0 B
packages/node/dist/entrypoints/nestjs.mjs 42 B 0 B
packages/node/dist/experimental.js 870 B 0 B
packages/node/dist/experimental.mjs 267 B 0 B
packages/node/dist/exports.js 6.75 kB 0 B
packages/node/dist/exports.mjs 582 B 0 B
packages/node/dist/extensions/context/context.js 2.13 kB 0 B
packages/node/dist/extensions/context/context.mjs 863 B 0 B
packages/node/dist/extensions/context/types.js 603 B 0 B
packages/node/dist/extensions/context/types.mjs 0 B 0 B 🆕
packages/node/dist/extensions/error-tracking/autocapture.js 2.66 kB 0 B
packages/node/dist/extensions/error-tracking/autocapture.mjs 1.24 kB 0 B
packages/node/dist/extensions/error-tracking/index.js 4.14 kB 0 B
packages/node/dist/extensions/error-tracking/index.mjs 2.87 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/context-lines.node.js 8.81 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/context-lines.node.mjs 7.15 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/module.node.js 2.78 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/module.node.mjs 1.45 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/relative-path.node.js 1.97 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/relative-path.node.mjs 624 B 0 B
packages/node/dist/extensions/express.js 4.56 kB 0 B
packages/node/dist/extensions/express.mjs 2.45 kB 0 B
packages/node/dist/extensions/feature-flags/cache.js 603 B 0 B
packages/node/dist/extensions/feature-flags/cache.mjs 0 B 0 B 🆕
packages/node/dist/extensions/feature-flags/crypto.js 1.57 kB 0 B
packages/node/dist/extensions/feature-flags/crypto.mjs 395 B 0 B
packages/node/dist/extensions/feature-flags/feature-flags.js 40.6 kB 0 B
packages/node/dist/extensions/feature-flags/feature-flags.mjs 38.5 kB 0 B
packages/node/dist/extensions/nestjs.js 5 kB 0 B
packages/node/dist/extensions/nestjs.mjs 2.9 kB 0 B
packages/node/dist/extensions/sentry-integration.js 4.66 kB 0 B
packages/node/dist/extensions/sentry-integration.mjs 3.17 kB 0 B
packages/node/dist/extensions/tracing-headers.js 3.31 kB 0 B
packages/node/dist/extensions/tracing-headers.mjs 1.53 kB 0 B
packages/node/dist/feature-flag-evaluations.js 5.97 kB 0 B
packages/node/dist/feature-flag-evaluations.mjs 4.63 kB 0 B
packages/node/dist/storage-memory.js 1.52 kB 0 B
packages/node/dist/storage-memory.mjs 297 B 0 B
packages/node/dist/types.js 1.43 kB 0 B
packages/node/dist/types.mjs 224 B 0 B
packages/node/dist/version.js 1.21 kB 0 B
packages/node/dist/version.mjs 46 B 0 B
packages/nuxt/dist/module.mjs 5.29 kB 0 B
packages/nuxt/dist/runtime/composables/useFeatureFlagEnabled.js 566 B 0 B
packages/nuxt/dist/runtime/composables/useFeatureFlagPayload.js 690 B 0 B
packages/nuxt/dist/runtime/composables/useFeatureFlagVariantKey.js 591 B 0 B
packages/nuxt/dist/runtime/composables/usePostHog.js 128 B 0 B
packages/nuxt/dist/runtime/nitro-plugin.js 1.08 kB 0 B
packages/nuxt/dist/runtime/vue-plugin.js 1.14 kB 0 B
packages/plugin-utils/dist/cli.js 3.14 kB 0 B
packages/plugin-utils/dist/cli.mjs 1.64 kB 0 B
packages/plugin-utils/dist/config.js 3.07 kB 0 B
packages/plugin-utils/dist/config.mjs 1.83 kB 0 B
packages/plugin-utils/dist/index.js 4.3 kB 0 B
packages/plugin-utils/dist/index.mjs 217 B 0 B
packages/plugin-utils/dist/spawn-local.js 2.17 kB 0 B
packages/plugin-utils/dist/spawn-local.mjs 918 B 0 B
packages/plugin-utils/dist/utils.js 3.27 kB 0 B
packages/plugin-utils/dist/utils.mjs 1.3 kB 0 B
packages/react-native/dist/autocapture.js 5.05 kB 0 B
packages/react-native/dist/error-tracking/index.js 7.36 kB 0 B
packages/react-native/dist/error-tracking/utils.js 2.58 kB 0 B
packages/react-native/dist/frameworks/wix-navigation.js 1.3 kB 0 B
packages/react-native/dist/hooks/useFeatureFlag.js 1.7 kB 0 B
packages/react-native/dist/hooks/useFeatureFlagResult.js 963 B 0 B
packages/react-native/dist/hooks/useFeatureFlags.js 921 B 0 B
packages/react-native/dist/hooks/useNavigationTracker.js 2.45 kB 0 B
packages/react-native/dist/hooks/usePostHog.js 544 B 0 B
packages/react-native/dist/hooks/utils.js 988 B 0 B
packages/react-native/dist/index.js 4.33 kB 0 B
packages/react-native/dist/logs-********.js 3.32 kB 0 B
packages/react-native/dist/native-deps.js 8.77 kB 0 B
packages/react-native/dist/optional/OptionalAsyncStorage.js 299 B 0 B
packages/react-native/dist/optional/OptionalExpoApplication.js 377 B 0 B
packages/react-native/dist/optional/OptionalExpoDevice.js 347 B 0 B
packages/react-native/dist/optional/OptionalExpoFileSystem.js 386 B 0 B
packages/react-native/dist/optional/OptionalExpoFileSystemLegacy.js 423 B 0 B
packages/react-native/dist/optional/OptionalExpoLocalization.js 383 B 0 B
packages/react-native/dist/optional/OptionalReactNativeDeviceInfo.js 415 B 0 B
packages/react-native/dist/optional/OptionalReactNativeLocalize.js 303 B 0 B
packages/react-native/dist/optional/OptionalReactNativeNavigation.js 415 B 0 B
packages/react-native/dist/optional/OptionalReactNativeNavigationWix.js 443 B 0 B
packages/react-native/dist/optional/OptionalReactNativeSafeArea.js 644 B 0 B
packages/react-native/dist/optional/OptionalReactNativeSvg.js 872 B 0 B
packages/react-native/dist/optional/OptionalSessionReplay.js 455 B 0 B
packages/react-native/dist/posthog-rn.js 46 kB 0 B
packages/react-native/dist/PostHogContext.js 329 B 0 B
packages/react-native/dist/PostHogErrorBoundary.js 3.19 kB 0 B
packages/react-native/dist/PostHogMaskView.js 1.68 kB 0 B
packages/react-native/dist/PostHogProvider.js 4.55 kB 0 B
packages/react-native/dist/storage.js 5.2 kB 0 B
packages/react-native/dist/surveys/components/BottomSection.js 1.46 kB 0 B
packages/react-native/dist/surveys/components/Cancel.js 909 B 0 B
packages/react-native/dist/surveys/components/ConfirmationMessage.js 1.65 kB 0 B
packages/react-native/dist/surveys/components/QuestionHeader.js 1.37 kB 0 B
packages/react-native/dist/surveys/components/QuestionTypes.js 13.3 kB 0 B
packages/react-native/dist/surveys/components/SurveyModal.js 6.27 kB 0 B
packages/react-native/dist/surveys/components/Surveys.js 6.58 kB 0 B
packages/react-native/dist/surveys/getActiveMatchingSurveys.js 2.64 kB 0 B
packages/react-native/dist/surveys/icons.js 9.97 kB 0 B
packages/react-native/dist/surveys/index.js 600 B 0 B
packages/react-native/dist/surveys/PostHogSurveyProvider.js 6.28 kB 0 B
packages/react-native/dist/surveys/survey-translations.js 1.11 kB 0 B
packages/react-native/dist/surveys/surveys-utils.js 14.2 kB 0 B
packages/react-native/dist/surveys/useActivatedSurveys.js 3.67 kB 0 B
packages/react-native/dist/surveys/useSurveyStorage.js 2.16 kB 0 B
packages/react-native/dist/tooling/expoconfig.js 4.02 kB 0 B
packages/react-native/dist/tooling/metroconfig.js 2.32 kB 0 B
packages/react-native/dist/tooling/posthogMetroSerializer.js 4.86 kB 0 B
packages/react-native/dist/tooling/utils.js 4.05 kB 0 B
packages/react-native/dist/tooling/vendor/expo/expoconfig.js 70 B 0 B
packages/react-native/dist/tooling/vendor/metro/countLines.js 237 B 0 B
packages/react-native/dist/tooling/vendor/metro/utils.js 3.35 kB 0 B
packages/react-native/dist/types.js 70 B 0 B
packages/react-native/dist/utils.js 1.14 kB 0 B
packages/react-native/dist/version.js 131 B 0 B
packages/react/dist/esm/index.js 21.2 kB 0 B
packages/react/dist/esm/slim/index.js 17.6 kB 0 B
packages/react/dist/esm/surveys/index.js 4.68 kB 0 B
packages/react/dist/umd/index.js 24.4 kB 0 B
packages/react/dist/umd/slim/index.js 20.4 kB 0 B
packages/react/dist/umd/surveys/index.js 5.45 kB 0 B
packages/rollup-plugin/dist/index.js 2.44 kB 0 B
packages/rrweb/packer/dist/base-********.js 18.2 kB 0 B
packages/rrweb/packer/dist/base-********.cjs 18.3 kB 0 B
packages/rrweb/packer/dist/base-********.umd.cjs 18.7 kB 0 B
packages/rrweb/packer/dist/base-********.umd.min.cjs 9.5 kB 0 B
packages/rrweb/packer/dist/pack.cjs 347 B 0 B
packages/rrweb/packer/dist/pack.js 285 B 0 B
packages/rrweb/packer/dist/pack.umd.cjs 1.63 kB 0 B
packages/rrweb/packer/dist/pack.umd.min.cjs 1.11 kB 0 B
packages/rrweb/packer/dist/packer.cjs 257 B 0 B
packages/rrweb/packer/dist/packer.js 136 B 0 B
packages/rrweb/packer/dist/packer.umd.cjs 662 B 0 B
packages/rrweb/packer/dist/packer.umd.min.cjs 626 B 0 B
packages/rrweb/packer/dist/unpack.cjs 769 B 0 B
packages/rrweb/packer/dist/unpack.js 702 B 0 B
packages/rrweb/packer/dist/unpack.umd.cjs 1.17 kB 0 B
packages/rrweb/packer/dist/unpack.umd.min.cjs 955 B 0 B
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-record/dist/rrweb-plugin-canvas-webrtc-record.cjs 37.6 kB 0 B
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-record/dist/rrweb-plugin-canvas-webrtc-record.js 37.5 kB 0 B
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-record/dist/rrweb-plugin-canvas-webrtc-record.umd.cjs 38 kB 0 B
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-record/dist/rrweb-plugin-canvas-webrtc-record.umd.min.cjs 22.2 kB 0 B
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-replay/dist/rrweb-plugin-canvas-webrtc-replay.cjs 34.3 kB 0 B
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-replay/dist/rrweb-plugin-canvas-webrtc-replay.js 34.2 kB 0 B
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-replay/dist/rrweb-plugin-canvas-webrtc-replay.umd.cjs 34.7 kB 0 B
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-replay/dist/rrweb-plugin-canvas-webrtc-replay.umd.min.cjs 20.5 kB 0 B
packages/rrweb/plugins/rrweb-plugin-console-record/dist/rrweb-plugin-console-record.cjs 14.9 kB 0 B
packages/rrweb/plugins/rrweb-plugin-console-record/dist/rrweb-plugin-console-record.js 14.8 kB 0 B
packages/rrweb/plugins/rrweb-plugin-console-record/dist/rrweb-plugin-console-record.umd.cjs 15.4 kB 0 B
packages/rrweb/plugins/rrweb-plugin-console-record/dist/rrweb-plugin-console-record.umd.min.cjs 7.33 kB 0 B
packages/rrweb/plugins/rrweb-plugin-console-replay/dist/rrweb-plugin-console-replay.cjs 5.01 kB 0 B
packages/rrweb/plugins/rrweb-plugin-console-replay/dist/rrweb-plugin-console-replay.js 4.9 kB 0 B
packages/rrweb/plugins/rrweb-plugin-console-replay/dist/rrweb-plugin-console-replay.umd.cjs 5.44 kB 0 B
packages/rrweb/plugins/rrweb-plugin-console-replay/dist/rrweb-plugin-console-replay.umd.min.cjs 2.64 kB 0 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-record/dist/rrweb-plugin-sequential-id-record.cjs 681 B 0 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-record/dist/rrweb-plugin-sequential-id-record.js 548 B 0 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-record/dist/rrweb-plugin-sequential-id-record.umd.cjs 1.12 kB 0 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-record/dist/rrweb-plugin-sequential-id-record.umd.min.cjs 829 B 0 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-replay/dist/rrweb-plugin-sequential-id-replay.cjs 933 B 0 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-replay/dist/rrweb-plugin-sequential-id-replay.js 820 B 0 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-replay/dist/rrweb-plugin-sequential-id-replay.umd.cjs 1.37 kB 0 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-replay/dist/rrweb-plugin-sequential-id-replay.umd.min.cjs 968 B 0 B
packages/rrweb/replay/dist/rrweb-replay.cjs 440 kB 0 B
packages/rrweb/replay/dist/rrweb-replay.js 439 kB 0 B
packages/rrweb/replay/dist/rrweb-replay.umd.cjs 442 kB 0 B
packages/rrweb/replay/dist/rrweb-replay.umd.min.cjs 209 kB 0 B
packages/rrweb/rrdom-nodejs/dist/rrdom-nodejs.cjs 150 kB 0 B
packages/rrweb/rrdom-nodejs/dist/rrdom-nodejs.js 149 kB 0 B
packages/rrweb/rrdom-nodejs/dist/rrdom-nodejs.umd.cjs 152 kB 0 B
packages/rrweb/rrdom-nodejs/dist/rrdom-nodejs.umd.min.cjs 70.8 kB 0 B
packages/rrweb/rrdom/dist/rrdom.cjs 174 kB 0 B
packages/rrweb/rrdom/dist/rrdom.js 173 kB 0 B
packages/rrweb/rrdom/dist/rrdom.umd.cjs 175 kB 0 B
packages/rrweb/rrdom/dist/rrdom.umd.min.cjs 80.8 kB 0 B
packages/rrweb/rrweb-snapshot/dist/replay.cjs 137 kB 0 B
packages/rrweb/rrweb-snapshot/dist/replay.js 137 kB 0 B
packages/rrweb/rrweb-snapshot/dist/replay.umd.cjs 160 kB 0 B
packages/rrweb/rrweb-snapshot/dist/replay.umd.min.cjs 73.8 kB 0 B
packages/rrweb/rrweb-snapshot/dist/types-********.cjs 18.3 kB 0 B
packages/rrweb/rrweb-snapshot/dist/types-********.umd.cjs 18.8 kB 0 B
packages/rrweb/rrweb-snapshot/dist/types-********.umd.min.cjs 9.31 kB 0 B
packages/rrweb/rrweb-snapshot/dist/types-********.js 17.8 kB 0 B
packages/rrweb/types/dist/rrweb-types.cjs 5.64 kB 0 B
packages/rrweb/types/dist/rrweb-types.js 5.38 kB 0 B
packages/rrweb/types/dist/rrweb-types.umd.cjs 6.04 kB 0 B
packages/rrweb/types/dist/rrweb-types.umd.min.cjs 2.8 kB 0 B
packages/rrweb/utils/dist/rrweb-utils.cjs 6.41 kB 0 B
packages/rrweb/utils/dist/rrweb-utils.js 5.95 kB 0 B
packages/rrweb/utils/dist/rrweb-utils.umd.cjs 6.82 kB 0 B
packages/rrweb/utils/dist/rrweb-utils.umd.min.cjs 3.51 kB 0 B
packages/types/dist/capture-log.js 603 B 0 B
packages/types/dist/capture-log.mjs 0 B 0 B 🆕
packages/types/dist/capture.js 603 B 0 B
packages/types/dist/capture.mjs 0 B 0 B 🆕
packages/types/dist/common.js 603 B 0 B
packages/types/dist/common.mjs 0 B 0 B 🆕
packages/types/dist/feature-flags.js 603 B 0 B
packages/types/dist/feature-flags.mjs 0 B 0 B 🆕
packages/types/dist/index.js 603 B 0 B
packages/types/dist/index.mjs 0 B 0 B 🆕
packages/types/dist/posthog-config.js 603 B 0 B
packages/types/dist/posthog-config.mjs 0 B 0 B 🆕
packages/types/dist/posthog.js 603 B 0 B
packages/types/dist/posthog.mjs 0 B 0 B 🆕
packages/types/dist/request.js 603 B 0 B
packages/types/dist/request.mjs 0 B 0 B 🆕
packages/types/dist/segment.js 603 B 0 B
packages/types/dist/segment.mjs 0 B 0 B 🆕
packages/types/dist/session-recording.js 603 B 0 B
packages/types/dist/session-recording.mjs 0 B 0 B 🆕
packages/types/dist/survey.js 603 B 0 B
packages/types/dist/survey.mjs 0 B 0 B 🆕
packages/types/dist/toolbar.js 603 B 0 B
packages/types/dist/toolbar.mjs 0 B 0 B 🆕
packages/types/dist/tree-shakeable.js 603 B 0 B
packages/types/dist/tree-shakeable.mjs 0 B 0 B 🆕
packages/web/dist/index.cjs 13.8 kB 0 B
packages/web/dist/index.mjs 13.7 kB 0 B
packages/webpack-plugin/dist/config.js 1.53 kB 0 B
packages/webpack-plugin/dist/config.mjs 543 B 0 B
packages/webpack-plugin/dist/index.js 5.38 kB 0 B
packages/webpack-plugin/dist/index.mjs 2.04 kB 0 B
tooling/changelog/dist/index.js 3.31 kB 0 B
tooling/rollup-utils/dist/index.js 1.17 kB 0 B

compressed-size-action

Copy link
Copy Markdown
Member Author

@pauldambra pauldambra left a comment

Choose a reason for hiding this comment

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

Note

🤖 Automated comment by QA Swarm — not written by a human

Multi-perspective review: qa-team (specialists + generalists), paul-reviewer, xp-reviewer, security-audit

Verdict: ⚠️ REQUEST CHANGES

One convergent HIGH correctness finding (paul + qa-team/reliability): the new code reuses the pre-load serializedNode instead of re-running serializeNodeWithId on load, which silently drops _cssText for late-loading external stylesheets — StylesheetManager.attachLinkElement gates the mutation on '_cssText' in attributes, so the CSS never reaches the replay.

Key findings

  • 🟠 HIGH (convergent: paul + qa-team/reliability)snapshot.ts:1301 — late-loading <link rel="stylesheet"> loses _cssText in the replay because the old recursive serializeNodeWithId is gone.
  • 🟡 MEDIUM (convergent: paul + xp + qa-team/generalist-b) — Playwright spec — assertion bound toBeLessThanOrEqual(5) is too loose; should be toBe(0) (preload links now bypass onceStylesheetLoaded entirely).
  • 🟡 MEDIUM (convergent: paul + xp + qa-team/generalist-a)snapshot.test.ts — three near-identical tests; project conventions prefer parameterized tests, and none of them assert that _cssText reaches the onStylesheetLoad payload.
  • 🟡 MEDIUM (qa-team/reliability + paul) — module-level stylesheetLoadTracked WeakSet not reset across recorder lifecycles.
  • 🟡 MEDIUM (qa-team/compatibility) — preload-as-style branch removed; confirm no replay was relying on it for CSS capture.
  • 🟢 LOW (xp) — triple idempotency (WeakSet + {once:true} + fired) reads as belt-and-braces; consider trimming.
  • NIT — WeakSet name, playground comment, changeset wording.

Convergence

  • _cssText regression: paul + qa-team/reliability (highest confidence).
  • Playwright assertion looseness: paul + xp + qa-team/generalist-b.
  • Test parameterization + missing _cssText payload assertion: paul + xp + qa-team/generalist-a.
  • WeakSet cross-lifecycle: paul + qa-team/reliability.

Reviewer summaries

Reviewer Assessment
🔍 qa-team REQUEST CHANGES — leak fix is correct, but the recursive-call removal regresses CSS capture for late-loading stylesheets.
👤 paul REQUEST CHANGES — solid trace-driven write-up, but wants the late-loading _cssText path either restored or covered by a test before stamping.
📐 xp APPROVE WITH NITS — the predicate simplification is good XP work; idempotency triple-guard and the un-parameterised tests are the cleanups he'd want.
🛡 security-audit APPROVE — strictly shrinks attack surface; no new sinks, no new user-input paths, no exploitable change.

Automated by QA Swarm — not a human review

Comment thread packages/rrweb/rrweb-snapshot/src/snapshot.ts Outdated
Comment thread packages/rrweb/rrweb-snapshot/src/snapshot.ts
Comment thread packages/rrweb/rrweb-snapshot/src/snapshot.ts Outdated
Comment thread packages/rrweb/rrweb-snapshot/src/snapshot.ts Outdated
Comment thread packages/rrweb/rrweb-snapshot/src/snapshot.ts Outdated
Comment thread packages/rrweb/rrweb-snapshot/test/snapshot.test.ts
Comment thread packages/browser/playground/preload-link-leak/index.html
Comment thread .changeset/fix-preload-link-listener-leak.md
… replay

qa-swarm found a convergent HIGH-severity regression: dropping the recursive
serializeNodeWithId call inside the load callback meant late-loading
<link rel="stylesheet"> elements would deliver their original pre-load
serializedNode to onStylesheetLoad — which has no _cssText, so
StylesheetManager.attachLinkElement (gated on '_cssText' in attributes) never
emitted the cssText mutation. Every replay with a stylesheet not loaded by
first snapshot would render unstyled.

The new WeakSet idempotency guard makes restoring the recursive call safe:
re-entry into onceStylesheetLoaded sees the link in the WeakSet and returns
early, so the chain cannot re-arm.

Also from qa-swarm:
- Parameterize the three new unit tests and add a fourth case that populates
  link.sheet between first serialize and load, asserting _cssText reaches
  onStylesheetLoad (regression coverage for the bug this commit fixes).
- Tighten the Playwright assertion from toBeLessThanOrEqual(5) to toBe(0) —
  preload-as-style links bypass onceStylesheetLoaded entirely now.
- HTML comment in the leak-repro playground explaining the CSS chunks are
  intentionally non-existent.

Generated-By: PostHog Code
Task-Id: 18dbe2b5-9a25-4b4c-a756-db029804f620
The fix lives in packages/rrweb/rrweb-snapshot/src/snapshot.ts so the
rrweb-snapshot package needs the same patch bump as posthog-js. Flagged by
the changeset-hygiene action.

Generated-By: PostHog Code
Task-Id: 18dbe2b5-9a25-4b4c-a756-db029804f620
…sets

The module-level stylesheetLoadTracked WeakSet persisted for the document
lifetime, so a recorder stop/restart on the same page (SPA toggling
session recording) silently skipped re-tracking any <link rel="stylesheet">
whose load was still pending from the previous lifecycle. The pending
{ once: true } listener was closed over the stopped recorder's
onStylesheetLoad, so the second recorder never received the _cssText
mutation when the sheet finally loaded.

Pre-fix this didn't bite because there was no skip — each call attached a
fresh listener (which was the leak being fixed). The WeakSet now resets
inside StylesheetManager.reset() so the second recorder sees a clean
tracker.

Added a jsdom test that exercises the cross-lifecycle path: serialize a
pending link, call resetStylesheetLoadTracking, serialize it again,
populate sheet, dispatch load — assert the second session receives a
_cssText payload.

Note: the WeakSet still lives in rrweb-snapshot rather than in
StylesheetManager. Moving it into the recorder would couple the snapshot
package to load-tracking state it does not own; threading the WeakSet
through serializeNodeWithId options is the cleaner long-term refactor.
For now the reset hook is the minimal fix.

Generated-By: PostHog Code
Task-Id: 18dbe2b5-9a25-4b4c-a756-db029804f620
Copy link
Copy Markdown
Member Author

@pauldambra pauldambra left a comment

Choose a reason for hiding this comment

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

Note

🤖 Automated comment by QA Swarm — not written by a human

Incremental review of ff5f56d5 (Option A: reset hook for stylesheetLoadTracked)

Verdict: 💬 APPROVE WITH NITS

Fix correctly addresses the previously deferred cross-lifecycle finding. Reset is wired into StylesheetManager.reset(), mirrors the existing resetMaxDepthState precedent in the same file, and the new jsdom test genuinely exercises the fix (without the reset, the second-session capture would never fire). xp and security-audit returned no findings.

Key findings

  • 🟢 LOW (qa-team/reliability)StylesheetManager.reset() runs from takeFullSnapshot on every checkout, not only on recorder stop/restart. Pending links can therefore acquire a second { once: true } listener at each checkout, emitting duplicate attachLinkElement mutations when the sheet finally loads. Same id, same _cssText — not corrupt, just churn. Tracking-ticket worthy.
  • NIT (paul + qa-team/compatibility) — module-level let stylesheetLoadTracked is fine because the only reader (onceStylesheetLoaded) resolves the binding at call time, and no captured references exist. Unusual for this file; flagging for future readers.
  • NIT (paul) — new test assertion expect(firstSessionCalls).toBeLessThanOrEqual(1) is hand-wavy; the actual expected value is 0 since load is dispatched only after the reset.
  • NIT (qa-team/reliability) — new cross-lifecycle test only exercises the load-event delivery path. Timer fallback is covered by noMultiplicationCases but not specifically across reset.

Reviewer summaries

Reviewer Assessment
🔍 qa-team APPROVE WITH NITS — fix is right; flagging the every-checkout reset as follow-up.
👤 paul APPROVE WITH NITS — trust on the WeakSet swap; tighten the firstSessionCalls assertion.
📐 xp APPROVE — small, surgical, mirrors resetMaxDepthState precedent. No findings.
🛡 security-audit APPROVE — module-level reset hook only; no new sinks or user-input paths. No findings.

Automated by QA Swarm — not a human review

Comment thread packages/rrweb/rrweb/src/record/stylesheet-manager.ts
Comment thread packages/rrweb/rrweb-snapshot/src/snapshot.ts Outdated
Comment thread packages/rrweb/rrweb-snapshot/test/snapshot.test.ts
Comment thread packages/rrweb/rrweb-snapshot/test/snapshot.test.ts Outdated
qa-swarm v2 flagged toBeLessThanOrEqual(1) as hand-wavy. The actual value
is 1, not 0 — even after resetStylesheetLoadTracking(), the first session's
{ once: true } load listener is still attached to the link element (reset
only clears the dedup tracker, not the listeners themselves). When the
sheet finally loads, both session 1's and session 2's listeners fire.

Pinning to toBe(1) documents this trade-off explicitly. The cost is one
duplicate attachLinkElement mutation per checkout-pending link (same id,
same _cssText — applied idempotently by the replayer). Tracked separately
as a follow-up to abort previous-snapshot listeners on reset.

Generated-By: PostHog Code
Task-Id: 18dbe2b5-9a25-4b4c-a756-db029804f620
CI lint failed: my multi-line function signature broke prettier's printWidth.
Auto-fixed with `prettier --write`.

Generated-By: PostHog Code
Task-Id: 18dbe2b5-9a25-4b4c-a756-db029804f620
Copy link
Copy Markdown
Member Author

@pauldambra pauldambra left a comment

Choose a reason for hiding this comment

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

Note

🤖 Automated comment by QA Swarm — not written by a human

Incremental review of b74d3926 (Map<…, {timer, onLoad}> → Map<…, AbortController>)

Verdict: 💬 APPROVE WITH NITS

3ed123d + b74d392 together resolve the previous LOW-severity duplicate-mutation concern. Pending watches now carry an AbortController, resetStylesheetLoadTracking() aborts each, and the { signal }-bound load listener is removed declaratively. Test assertion flip from 1 to 0 documents the desired behaviour (previous session's listener is genuinely gone after reset).

Findings (all NIT)

  • paulcontroller.abort() in fire()'s finally reads as belt-and-braces; worth confirming it's intentional.
  • paul — module-level mutable state + reset() aborting in-flight waits has the #1708 concern shape; flagged for awareness.
  • xp — using AbortController for cleanup is indirect compared to a cleanup() closure that does clearTimeout + removeEventListener. Trade-off, not a bug.
  • xptry/finally intent ("always tear down even if listener throws") could be named via extracted cleanup closure.
  • paul + xp (convergent) — test name re-tracks a pending stylesheet link after resetStylesheetLoadTracking is fine; a more behaviour-focused name ("does not invoke first-session listener after reset") would protect regression intent.
  • paul — changeset addition for @posthog/rrweb: positive note.

qa-team and security-audit: no findings.

Reviewer summaries

Reviewer Assessment
🔍 qa-team APPROVE — refactor is sound, semantics correct, AbortController scoping right.
👤 paul APPROVE WITH NITS — high trust, small change, ship it.
📐 xp APPROVE WITH NITS — refactor is a genuine lifecycle simplification; nits are about expressiveness.
🛡 security-audit APPROVE — internal helper, no new sinks, no new untrusted input.

Automated by QA Swarm — not a human review

Comment thread packages/rrweb/rrweb-snapshot/src/snapshot.ts
Comment thread packages/rrweb/rrweb-snapshot/src/snapshot.ts
Comment thread packages/rrweb/rrweb-snapshot/src/snapshot.ts
Comment thread packages/rrweb/rrweb-snapshot/src/snapshot.ts
Comment thread packages/rrweb/rrweb-snapshot/test/snapshot.test.ts
Comment thread .changeset/fix-preload-link-listener-leak.md
qa-swarm convergent NIT (paul + xp): the old name described the mechanism
("re-tracks a pending stylesheet link after resetStylesheetLoadTracking")
not the behaviour the test guards. The new name leads with what the
test asserts — that the previous session's listener is torn down — so
a future reader sees the regression intent without diffing the body.

Generated-By: PostHog Code
Task-Id: 18dbe2b5-9a25-4b4c-a756-db029804f620
Copy link
Copy Markdown
Contributor

@fasyy612 fasyy612 left a comment

Choose a reason for hiding this comment

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

Sweet find, LGTM!

… tracking

End-to-end coverage of the listener-teardown-on-reset fix in a real browser.

The spec:
- Loads a page with no stylesheet in HTML.
- Holds the CSS response via a Playwright route.
- Injects a <link rel="stylesheet"> after recording starts (mutation
  observer picks it up).
- Configures session_recording.full_snapshot_interval_millis to 1500ms so
  rrweb's takeFullSnapshot fires repeatedly, each one calling
  StylesheetManager.reset() -> resetStylesheetLoadTracking(). Each
  checkout reschedules a fresh load watch on the still-pending link.
- Releases the CSS only after several checkouts have happened.
- Asserts the captured event stream contains exactly one _cssText
  attribute mutation for the link's mirror id.

Verified the spec catches the regression: with resetStylesheetLoadTracking
neutered to skip the controller.abort() loop, the spec sees 3 _cssText
mutations (one per checkout listener that wasn't torn down). With the
abort restored, exactly 1.

Generated-By: PostHog Code
Task-Id: 18dbe2b5-9a25-4b4c-a756-db029804f620
CI showed the spec failing on Firefox and WebKit with Received: 0 — those
browsers do not populate link.sheet.cssRules from a Playwright-route-
fulfilled CSS response within our wait window. The end-to-end load
delivery doesn't complete in time, so the mutation is never emitted.

The fix being verified is JS-internal (Map+AbortController teardown on
reset) and browser-agnostic. The jsdom unit tests cover the logic
deterministically. The Playwright spec adds Chromium end-to-end
confirmation; non-Chromium coverage would need a different fixture
strategy (e.g., serving the CSS via a real test server instead of route
interception) and isn't worth the complexity here.

Generated-By: PostHog Code
Task-Id: 18dbe2b5-9a25-4b4c-a756-db029804f620
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants