fix(deploy): reload documents left running on a superseded deployment - #2563
fix(deploy): reload documents left running on a superseded deployment#2563innolope-dev wants to merge 1 commit into
Conversation
A loaded document can outlive arbitrarily many deploys, keeping its original JS chunks, API assumptions and response headers. The service worker's controllerchange reload is skipped in standalone PWA (an Android standalone session can bounce out to Chrome), App Router navigations fetch RSC payloads rather than documents, and chunk-error recovery only fires once an asset 404s. 1,603 users were on weeks-old bundles — visible as CSP violation reports replaying allow-lists that had already been fixed. /api/version returns the live deployment's commit; useStaleDeploymentReload compares it against the build-inlined NEXT_PUBLIC_GIT_COMMIT_HASH on mount, on tab re-focus and on a 30-minute poll. The mount-only case is exactly the shape of the sessions that stayed stale for days, so the poll is what makes this land on the real population. Reload timing is deliberately conservative: a forced reload mid-flow can destroy state that only lives in component memory (a Sumsub WebSDK session, a half-filled card application) or leave a user unsure whether a payment went through. Staleness is latched on detection and acted on only at a safe boundary — gated on pending balance-decreasing mutations, the zerodev user-op flag, the loading-state context, and a path deny-list. Document caches are purged before reloading: a cached Response replays the headers it was stored with, so the reload could otherwise be answered from cache with the same stale document. A one-attempt sessionStorage latch stands the hook down if a reload does not clear the mismatch, instead of looping. purgeCaches/isStandalonePwa are extracted to cache.utils and reused by logout, which had the purge inline.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Code-analysis diffPainscore total: 6656.05 → 6675.28 (+19.23) 🆕 New findings (18)
✅ Resolved (10)
📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
…s, PIX exit, verification tasks Cherry-picks the open native-facing work onto the release branch: - KYC now routes every native entry point through the Sumsub Cordova SDK instead of the WebSDK (peanut-ui #2562) — the one change here that genuinely requires a new binary rather than an OTA. - Deferred deep linking survives the store install, so a link followed before install lands on its destination on first launch (#2560, TASK-20772). - PIX deposit "Done" exits to home from both the completed and processing states instead of dropping the user into a new deposit (#2548). - Pending Bridge verification tasks card on home, dismissible and resurfaced under Unlocked regions (#2549). - Native demo/passkey follow-ups: the awaited token clear in clearAuthState plus the two callers it changed (#2517 — the rest shipped in 1.0.40). - Documents left running on a superseded deployment now reload (#2563). - Android safe zone sized from natively measured insets. Not carried: the CSP allow-list gaps (#2564). It builds on the #2519 collector rework — a /api/csp-report route and ten commits that are not on this branch — and the policy is report-only, so the gaps cost report fidelity, not behaviour.
Summary
Investigated as a "CSP regression removing RPC endpoints from
connect-src". That turned out not to exist — but the investigation surfaced a real defect underneath it, which is what this PR fixes.The reported CSP regression is not real
connect-srccontains all 14 chain RPC hosts. Nothing was removed;mainanddevare byte-identical innext.config.js.frame-ancestors 'self'; object-src 'none'; base-uri 'self'— noconnect-srcat all. Every violation isdisposition: report. No request has ever been blocked by CSP.wss:+ the same-origin collector).It also was not a spike that started 22 hours ago:
PEANUT-UI-R79has been open since 2026-07-22 (8,002 events / 406 users). Sentry's issue search reports "first seen" relative to the query window, which is what made a week-old issue look new.What those 7,800 events actually are
Every sampled violation carries a superseded
original-policy:report-uriPEANUT-UI-R79(chainstack, 8,002 occ / 406 users)PEANUT-UI-R7M(wss://api.peanut.me, 5,083 occ / 1,603 users)wss:They come from documents loaded days earlier and still open. Those documents also carry the old
report-uripointing straight at Sentry's ingest endpoint, so #2519's collector (/api/csp-report, de-dup + 1% sampling) is structurally unable to filter them. No code change suppresses that traffic.The actual defect — and what this PR fixes
1,603 users were running weeks-old bundles. A loaded document can outlive arbitrarily many deployments, keeping its original JS chunks, API assumptions and response headers, because three things compound:
src/app/layout.tsxskipswindow.location.reload()oncontrollerchangein standalone PWA — deliberate, an Android standalone session can bounce out to Chrome.src/app/sw.tsspreads serwist'sdefaultCache; browser navigations fall through itsothersNetworkFirst rule, and a cachedResponsereplays the headers it was stored with.There was no deployment-version check anywhere. The only reload-on-stale machinery,
src/utils/chunk-error-recovery.ts, is reactive (fires once an asset 404s), rate-limited, and also PWA-disabled.Changes
src/app/api/version/route.tsforce-dynamic,no-store, returns the live deployment's commitsrc/hooks/useStaleDeploymentReload.tssrc/components/Global/StaleDeploymentReload/index.tsxsrc/app/ClientProviders.tsxsrc/constants/cache.consts.tsDOCUMENT_CACHE_PATTERNSsrc/utils/cache.utils.tspurgeCaches/isStandalonePwasrc/context/authContext.tsxpurgeCachesinstead of the inline copyDetection.
NEXT_PUBLIC_GIT_COMMIT_HASHis already inlined at build time (next.config.js), so a stale bundle holds the old hash while/api/version— always executed by the live deployment — returns the current one. Checked on mount (catches a document served back out of the SW cache), on tab re-focus, and on a 30-minute poll. The poll matters: a tab that is never backgrounded and never navigated is exactly the shape of the sessions that stayed stale for days, and mount-only detection would miss them entirely.Reload timing is deliberately conservative. A forced reload mid-flow can destroy state that only lives in component memory (a Sumsub WebSDK session, a half-filled card application) or leave a user unsure whether a payment went through. Staleness is latched on detection and acted on only at a safe boundary — a route change, or a flow finishing — with the gate clear:
hasPendingTransactionsusePendingTransactions(BALANCE_DECREASEmutations)isSendingUserOpisLoading'Executing transaction','Withdrawing','Paying','Submitting Offramp','Awaiting KYC confirmation')card,withdraw,setup,add-money,kyc— matched per segment, so/en/cardis covered tooCaches are purged before reloading. Otherwise the reload can be answered out of Cache Storage with the same stale document and its stale headers.
Loop guard. A one-attempt
sessionStoragelatch: if a reload does not clear the mismatch, the reload is not the fix and the hook stands down for the session rather than looping. The latch is cleared once the bundle is current again, so a later deploy can still act.Mounted inside the providers rather than called in
ClientProviders' body likeuseOtaUpdates, because it reads the query client, redux and the loading-state context. Inert on Capacitor — native serves local files and updates through Capgo.Verification
tsc --noEmit: no errors in any touched file.pnpm buildsucceeds;/api/versionbuilds asƒ(dynamic).{"commit":"134cf01"}withCache-Control: no-store, no-cache, must-revalidate, proxy-revalidate, matchinggit rev-parse --short=7 HEAD.countryCurrencyMapping,lib/content) are fresh-worktree artifacts — uninitializedsrc/contentsubmodule and missing generatedpublic/flags— not touched here.Risks
window.location.replace()rather thanreload()to avoid the documented Android PWA→Chrome bounce, but that has only been reasoned about, not observed on hardware. If the bounce reproduces, the fallback is to degrade standalone to a "New version available — Reload" prompt and keep the automatic path for browser tabs.Not in scope
#2543's collector follow-ups (Vercel WAF rate-limit on
/api/csp-report, route-level tests) are unchanged and still open.