fix(replay): guard main-thread WASM fallback in decompression worker - #70859
fix(replay): guard main-thread WASM fallback in decompression worker#70859posthog[bot] wants to merge 2 commits into
Conversation
When the decompression Web Worker fails to start, DecompressionWorkerManager falls back to initializing snappy WASM on the main thread. That fallback fetches the WASM binary over the network, which can fail (offline, ad blocker, transient CDN hiccup). The call was unguarded, so a failed fetch rejected readyPromise as an uncaught exception, and the replay_worker_init_failed capture that followed never fired. Wrap the fallback initSnappy() in try/catch, capture it as replay_snappy_init_failed telemetry, and settle readyPromise into a defined failure state so later decompress() calls reject cleanly instead of throwing an uncaught error. The worker-init capture now fires before the fallback attempt so it always records. Generated-By: PostHog Code Task-Id: 8aec8170-78b3-4128-905f-6a3aee9e331d
🤖 CI report✅ Bundle size — 🟢 -104.5 KiB (-0.2%)Uncompressed size of every built Total: 64.66 MiB · 🟢 -104.5 KiB (-0.2%)
Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report ✅ Eager graph — within budgetHow much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy
🟢 Largest files eagerly shipped from
|
| Size | File |
|---|---|
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 24.6 KiB | ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js |
| 6.3 KiB | ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js |
| 4.5 KiB | ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js |
| 3.9 KiB | ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js |
| 1.4 KiB | ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js |
| 1.3 KiB | src/RootErrorBoundary.tsx |
| 912 B | ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js |
| 789 B | src/scenes/ChunkLoadErrorBoundary.tsx |
| 762 B | src/index.tsx |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
| Size | File |
|---|---|
| 281.3 KiB | ../node_modules/.pnpm/posthog-js@1.406.2/node_modules/posthog-js/dist/rrweb.js |
| 267.7 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 236.0 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 224.7 KiB | ../node_modules/.pnpm/posthog-js@1.406.2/node_modules/posthog-js/dist/module.js |
| 167.1 KiB | src/queries/validators.js |
| 154.3 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 105.8 KiB | src/lib/api.ts |
| 94.0 KiB | ../packages/quill/packages/quill/dist/index.js |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479
⚠️ Dist folder size — 🔺 +42.60 MiB (+3.2%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1357.17 MiB · 🔺 +42.60 MiB (+3.2%)
✅ toolbar-size — eager 2.18 MiB within budget
What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.
| Metric | Size | Δ vs base | Budget |
|---|---|---|---|
| Eager (shipped) entry + static imports |
2.18 MiB · 17 files | (no base measurement) | ████░░░░░░ 38.1% of 5.72 MiB |
| Deferred (lazy) | 2.07 MiB · 33 files | (no base measurement) | n/a — loads on demand |
Loader dist/toolbar.js |
1.1 KiB | (no base measurement) | █░░░░░░░░░ 5.8% of 19.5 KiB |
Largest eagerly-shipped chunks
| Size | File |
|---|---|
| 713.8 KiB | dist/toolbar/toolbar-app-HY7HJI4V.css |
| 543.6 KiB | dist/toolbar/chunk-chunk-UG3THN3N.js |
| 484.2 KiB | dist/toolbar/chunk-chunk-QS5AHYGW.js |
| 133.6 KiB | dist/toolbar/chunk-chunk-MCXISDMN.js |
| 131.8 KiB | dist/toolbar/chunk-chunk-T5KY5WYR.js |
| 71.0 KiB | dist/toolbar/toolbar-app-UDDFB6JG.js |
| 69.0 KiB | dist/toolbar/chunk-chunk-27JL52RE.js |
| 35.6 KiB | dist/toolbar/chunk-chunk-XVKSNBZ7.js |
| 20.9 KiB | dist/toolbar/chunk-chunk-CS7W2KTV.js |
| 12.2 KiB | dist/toolbar/chunk-chunk-PIK3PADE.js |
Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile
…-guard Generated-By: PostHog Code Task-Id: 8aec8170-78b3-4128-905f-6a3aee9e331d
|
Heads up for reviewers on the red check: Visual regression tests - chromium (2/5) is failing on two stories unrelated to this PR:
This PR only changes session-replay snapshot decompression ( |
|
This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, please remove the |
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
Problem
When session replay's decompression Web Worker fails to start,
DecompressionWorkerManagerfalls back to initializing the snappy WASM module on the main thread. That fallback (initSnappy()→snappyInit()) fetches the WASM binary over the network, which can fail (offline, ad blocker, transient CDN or static-asset hiccup, or the same network problem that killed the worker in the first place).The fallback call was unguarded. When the fetch failed the rejection escaped:
readyPromiserejected as an uncaught exception, and thereplay_worker_init_failedcapture on the following lines never fired. The worker-init fallback is a well-traveled path, so it's worth hardening even though the uncaught error itself is rare.Changes
await this.initSnappy()in try/catch, mirroring the existingdecompressWithFallbackpattern.replay_snappy_init_failedtelemetry instead of letting it surface as an uncaught error.snappyInitFailedflag sodecompress()degrades cleanly (rejects with a clear "Decompression unavailable" error the caller already handles) rather than throwing a cryptic WASM error.replay_worker_init_failedcapture ahead of the fallback attempt so it always records when the worker fails, not only when the fallback succeeds.No user-visible UI change: this is a robustness fix on an error path.
How did you test this code?
Added two Jest cases to
DecompressionWorkerManager.test.tsexercising the real manager (the existing suite ran only against the global no-op mock, so it never touched this path):replay_worker_init_failed, does not capturereplay_snappy_init_failed. Guards that reordering the capture didn't regress the common recovery path.readyPromisesettles instead of rejecting uncaught,decompress()rejects cleanly with "Decompression unavailable", and bothreplay_worker_init_failedandreplay_snappy_init_failedare captured. This is the exact regression the fix addresses.Ran the suite locally (
jest DecompressionWorkerManager.test.ts) — all 10 tests pass. TypeScript check and format/lint pass on the changed files. I (an agent) did not manually reproduce the offline/blocked-fetch state in a browser.Automatic notifications
Docs update
No docs affected.
🤖 Agent context
Autonomy: Fully autonomous
Investigated and fixed by Claude (Claude Code) from an inbox report flagging a rare uncaught
TypeError: Failed to fetchon session replay decompression. Root cause was the unguarded fallbackinitSnappy()inDecompressionWorkerManager.initWorker. Invoked the/writing-testsskill before adding tests — the gate confirmed the double-failure case catches a regression (removing the try/catch) that no existing test covered, since the existing suite runs entirely against the globally-mocked manager. Usedjest.requireActualplus a controllablesnappy-wasmmock and a throwingWorkerglobal to exercise the real fallback path.Agent-authored; requires human review.
Created with PostHog Code from an inbox report.